1
\$\begingroup\$

In language dictionary application users can add new words and do other staff with this word such as adding additional information to the word (description, etymology, speech parts, noun class etc.) I'll count all this information and maybe later create some application gamification based on this information. I decided to create custom model and count all this contribution inside this model. There are one base abstract model which defines common fields:

class BaseModel(models.Model, ModelDiffMixin):
    WORD_MODERATE_STATUS = [
        ('UNDER_CONCIDERATION', 'under consideration'),
        ('PREVIOUSLY_ACCEPTED', 'previously accepted')
    ]
    moderate_status = models.CharField(
        max_length=50, choices=WORD_MODERATE_STATUS, blank=True)
    created_on = models.DateTimeField(auto_now_add=True)
    modified_on = models.DateTimeField(auto_now=True)
    created_by = models.ForeignKey(
        get_user_model(), on_delete=models.CASCADE,
        related_name='%(class)s_createdby')
    modified_by = models.ForeignKey(
        get_user_model(), on_delete=models.CASCADE,
        related_name='%(class)s_modifiedby', null=True, blank=True)
    accepted = models.BooleanField(default=False)
    accepted_by = models.ForeignKey(
        get_user_model(), on_delete=models.CASCADE,
        related_name='%(class)s_acceptedby')

    class Meta:
        abstract = True

And then I've found solution how to watch field change and based on this I've created post_save signal which does update desired field value.

class ModelDiffMixin(object):
    """
    A model mixin that tracks model fields values and provide some useful api
    to know what fields have been changed.
    """

    def __init__(self, *args, **kwargs):
        super(ModelDiffMixin, self).__init__(*args, **kwargs)
        self.__initial = self._dict

    @property
    def diff(self):
        d1 = self.__initial
        d2 = self._dict
        diffs = [(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]
        return dict(diffs)

    @property
    def has_changed(self):
        return bool(self.diff)

    @property
    def changed_fields(self):
        return self.diff.keys()

    def get_field_diff(self, field_name):
        """
        Returns a diff for field if it's changed and None otherwise.
        """
        return self.diff.get(field_name, None)

    def save(self, *args, **kwargs):
        """
        Saves model and set initial state.
        """
        super(ModelDiffMixin, self).save(*args, **kwargs)
        self.__initial = self._dict

    @property
    def _dict(self):
        return model_to_dict(
            self, fields=[field.name for field in self._meta.fields])

Using this mixin I'll be able to check which field values was changed and update the user statistics respectively. Here is the core word model WordCoreModel in which users can do some data manipulations through front-ent(using Django REST Framework):

class CoreWordCategoryModel(models.Model):
    core_abbreviation = models.CharField(max_length=10, blank=True)
    core_description = models.CharField(max_length=50, blank=True)
    ru_abbreviation = models.CharField(max_length=10, blank=True)
    ru_description = models.CharField(max_length=50, blank=True)
    en_abbreviation = models.CharField(max_length=10, blank=True)
    en_description = models.CharField(max_length=50, blank=True)

    def __str__(self):
        return "{} - {} - {}".format(self.core_description, self.ru_description, self.en_description)


class WordCoreModel(BaseModel):
    NOUN_CLASSESS = [
        ('V_CLASS', 'В класс'),
        ('Y_CLASS', 'Й класс'),
        ('Y_CLASS_SECOND', 'Й класс II'),
        ('D_CLASS', 'Д класс'),
        ('B_CLASS', 'Б класс'),
        ('B_CLASS_SECOND', 'Д класс II')
    ]
    PART_OF_SPEECH = [
        ('NOUN', 'Noun'),
        ('VERB', 'Verb'),
        ('ADVERB', 'Adverb'),
        ('ADJECTIVE', 'Adjective'),
        ('PRONOUN', 'Pronoun'),
        ('PREPOSITION', 'Preposition'),
        ('CONJUNCTION', 'Conjunction'),
        ('INTERJECTION', 'Interjection')
    ]
    word_core = models.CharField(max_length=255, default="")
    word_russian_typed = models.CharField(max_length=255, default="", blank=True)
    word_english_typed = models.CharField(max_length=255, default="", blank=True)

    homonyms = models.ManyToManyField('self', blank=True)
    synonyms = models.ManyToManyField('self', blank=True)
    antonyms = models.ManyToManyField('self', blank=True)

    word_category = models.ForeignKey(
        CoreWordCategoryModel, on_delete=models.CASCADE,
        related_name='wordcategory', null=True, blank=True)

    description = models.TextField(default="", blank=True)
    latin_version = models.CharField(max_length=255, default="", blank=True)
    transcription = models.CharField(max_length=255, default="", blank=True)
    ipa = models.CharField(max_length=200, default="", blank=True)
    root = models.CharField(max_length=80, default="", blank=True)
    word_class = models.CharField(max_length=20, choices=NOUN_CLASSESS, blank=True)
    part_of_speech = models.CharField(max_length=20, choices=PART_OF_SPEECH, blank=True)
    etymology = models.TextField(default="", blank=True)

    # Declensions Singular
    declension_singular_absolutive = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_genitive = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_dative = models.CharField(max_length=255, default="", blank=True)
    declension_singular_ergative = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_allative = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_instrumental = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_locative_stay = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_locative_direction = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_locative_outcome = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_locative_through = models.CharField(
        max_length=255, default="", blank=True)
    declension_singular_comparative = models.CharField(
        max_length=255, default="", blank=True)

    # Declension Plural
    declension_plural_absolutive = models.CharField(
        max_length=255, default="", blank=True)
    declension_plural_genitive = models.CharField(max_length=255, default="", blank=True)
    declension_plural_dative = models.CharField(max_length=255, default="", blank=True)
    declension_plural_ergative = models.CharField(max_length=255, default="", blank=True)
    declension_plural_allative = models.CharField(max_length=255, default="", blank=True)
    declension_plural_instrumental = models.CharField(
        max_length=255, default="", blank=True)
    declension_plural_locative_stay = models.CharField(
        max_length=255, default="", blank=True)
    declension_plural_locative_direction = models.CharField(
        max_length=255, default="", blank=True)
    declension_plural_locative_outcome = models.CharField(
        max_length=255, default="", blank=True)
    declension_plural_locative_through = models.CharField(
        max_length=255, default="", blank=True)
    declension_plural_comparative = models.CharField(
        max_length=255, default="", blank=True)

    # Tenses
    imperative_tense_basic_form = models.CharField(max_length=255, default="", blank=True)
    imperative_tense_causative = models.CharField(max_length=255, default="", blank=True)
    imperative_tense_permissive = models.CharField(max_length=255, default="", blank=True)
    imperative_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    imperative_tense_potential = models.CharField(max_length=255, default="", blank=True)
    imperative_tense_inceptive = models.CharField(max_length=255, default="", blank=True)
    ######
    simple_present_tense_basic_form = models.CharField(
        max_length=255, default="", blank=True)
    simple_present_tense_causative = models.CharField(
        max_length=255, default="", blank=True)
    simple_present_tense_permissive = models.CharField(
        max_length=255, default="", blank=True)
    simple_present_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    simple_present_tense_potential = models.CharField(
        max_length=255, default="", blank=True)
    simple_present_tense_inceptive = models.CharField(
        max_length=255, default="", blank=True)
    ######
    near_preterite_tense_basic_form = models.CharField(
        max_length=255, default="", blank=True)
    near_preterite_tense_causative = models.CharField(
        max_length=255, default="", blank=True)
    near_preterite_tense_permissive = models.CharField(
        max_length=255, default="", blank=True)
    near_preterite_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    near_preterite_tense_potential = models.CharField(
        max_length=255, default="", blank=True)
    near_preterite_tense_inceptive = models.CharField(
        max_length=255, default="", blank=True)
    ######
    witnessed_past_tense_basic_form = models.CharField(
        max_length=255, default="", blank=True)
    witnessed_past_tense_causative = models.CharField(
        max_length=255, default="", blank=True)
    witnessed_past_tense_permissive = models.CharField(
        max_length=255, default="", blank=True)
    witnessed_past_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    witnessed_past_tense_potential = models.CharField(
        max_length=255, default="", blank=True)
    witnessed_past_tense_inceptive = models.CharField(
        max_length=255, default="", blank=True)
    ######
    perfect_tense_basic_form = models.CharField(max_length=255, default="", blank=True)
    perfect_tense_causative = models.CharField(max_length=255, default="", blank=True)
    perfect_tense_permissive = models.CharField(max_length=255, default="", blank=True)
    perfect_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    perfect_tense_potential = models.CharField(max_length=255, default="", blank=True)
    perfect_tense_inceptive = models.CharField(max_length=255, default="", blank=True)
    ######
    plusquamperfect_tense_basic_form = models.CharField(
        max_length=255, default="", blank=True)
    plusquamperfect_tense_causative = models.CharField(
        max_length=255, default="", blank=True)
    plusquamperfect_tense_permissive = models.CharField(
        max_length=255, default="", blank=True)
    plusquamperfect_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    plusquamperfect_tense_potential = models.CharField(
        max_length=255, default="", blank=True)
    plusquamperfect_tense_inceptive = models.CharField(
        max_length=255, default="", blank=True)
    ######
    repeated_past_tense_basic_form = models.CharField(
        max_length=255, default="", blank=True)
    repeated_past_tense_causative = models.CharField(
        max_length=255, default="", blank=True)
    repeated_past_tense_permissive = models.CharField(
        max_length=255, default="", blank=True)
    repeated_past_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    repeated_past_tense_potential = models.CharField(
        max_length=255, default="", blank=True)
    repeated_past_tense_inceptive = models.CharField(
        max_length=255, default="", blank=True)
    ######
    possible_future_tense_basic_form = models.CharField(
        max_length=255, default="", blank=True)
    possible_future_tense_causative = models.CharField(
        max_length=255, default="", blank=True)
    possible_future_tense_permissive = models.CharField(
        max_length=255, default="", blank=True)
    possible_future_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    possible_future_tense_potential = models.CharField(
        max_length=255, default="", blank=True)
    possible_future_tense_inceptive = models.CharField(
        max_length=255, default="", blank=True)
    ######
    real_future_tense_basic_form = models.CharField(
        max_length=255, default="", blank=True)
    real_future_tense_causative = models.CharField(max_length=255, default="", blank=True)
    real_future_tense_permissive = models.CharField(
        max_length=255, default="", blank=True)
    real_future_tense_permissive_causative = models.CharField(
        max_length=255, default="", blank=True)
    real_future_tense_potential = models.CharField(max_length=255, default="", blank=True)
    real_future_tense_inceptive = models.CharField(max_length=255, default="", blank=True)

    class Meta:
        indexes = [models.Index(fields=['word_core'])]
        verbose_name = 'Core Word'
        verbose_name_plural = 'Core Words'

    def __str__(self):
        return self.word_core

Adding new word or modifying word triggers post_save signal and executes following method:

@receiver(post_save, sender=WordCoreModel)
def update_user_statistics(sender, instance, **kwargs):
    if instance.has_changed and instance.accepted:
        changed_fields = instance.changed_fields
        for field in changed_fields:
            _field = '{}_added'.format(field)
            if hasattr(UserStats, _field):
                user_stats = UserStats.objects.get_or_create(
                    user_id=instance.modified_by.id)[0]
                user_stats.increment(_field)
                user_stats.save()

The custom model to collect all interaction of users UserStats looks like(it's a huge but the fields are almost the same so if I need to change the logic or something just apply it to one field and I'll correct rest of them):

class UserStats(models.Model):
    user = models.OneToOneField(get_user_model(), on_delete=models.CASCADE)
    reputation = models.IntegerField(default=0, blank=True)

    word_core_added = models.IntegerField(default=0, blank=True)
    word_russian_typed_added = models.IntegerField(default=0, blank=True)
    word_english_typed_added = models.IntegerField(default=0, blank=True)

    description_added = models.IntegerField(default=0, blank=True)
    latin_version_added = models.IntegerField(default=0, blank=True)
    transcription_added = models.IntegerField(default=0, blank=True)
    ipa_added = models.IntegerField(default=0, blank=True)

    root_added = models.IntegerField(default=0, blank=True)
    prefix_added = models.IntegerField(default=0, blank=True)
    suffix_added = models.IntegerField(default=0, blank=True)
    ending_added = models.IntegerField(default=0, blank=True)

    syllable_added = models.IntegerField(default=0, blank=True)
    phraseologism_added = models.IntegerField(default=0, blank=True)
    source_added = models.IntegerField(default=0, blank=True)
    word_class_added = models.IntegerField(default=0, blank=True)
    part_of_speech_added = models.IntegerField(default=0, blank=True)
    etymology_added = models.IntegerField(default=0, blank=True)

    declension_singular_absolutive_added = models.IntegerField(default=0, blank=True)
    declension_singular_genitive_added = models.IntegerField(default=0, blank=True)
    declension_singular_dative_added = models.IntegerField(default=0, blank=True)
    declension_singular_ergative_added = models.IntegerField(default=0, blank=True)
    declension_singular_allative_added = models.IntegerField(default=0, blank=True)
    declension_singular_instrumental_added = models.IntegerField(default=0, blank=True)
    declension_singular_locative_stay_added = models.IntegerField(default=0, blank=True)
    declension_singular_locative_direction_added = models.IntegerField(
        default=0, blank=True)
    declension_singular_locative_outcome_added = models.IntegerField(
        default=0, blank=True)
    declension_singular_locative_through_added = models.IntegerField(
        default=0, blank=True)
    declension_singular_comparative_added = models.IntegerField(default=0, blank=True)

    declension_plural_absolutive_added = models.IntegerField(default=0, blank=True)
    declension_plural_genitive_added = models.IntegerField(default=0, blank=True)
    declension_plural_dative_added = models.IntegerField(default=0, blank=True)
    declension_plural_ergative_added = models.IntegerField(default=0, blank=True)
    declension_plural_allative_added = models.IntegerField(default=0, blank=True)
    declension_plural_instrumental_added = models.IntegerField(default=0, blank=True)
    declension_plural_locative_stay_added = models.IntegerField(default=0, blank=True)
    declension_plural_locative_direction_added = models.IntegerField(
        default=0, blank=True)
    declension_plural_locative_outcome_added = models.IntegerField(default=0, blank=True)
    declension_plural_locative_through_added = models.IntegerField(default=0, blank=True)
    declension_plural_comparative_added = models.IntegerField(default=0, blank=True)

    imperative_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    imperative_tense_causative_added = models.IntegerField(default=0, blank=True)
    imperative_tense_permissive_added = models.IntegerField(default=0, blank=True)
    imperative_tense_permissive_causative_added = models.IntegerField(
        default=0, blank=True)
    imperative_tense_potential_added = models.IntegerField(default=0, blank=True)
    imperative_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    simple_present_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    simple_present_tense_causative_added = models.IntegerField(default=0, blank=True)
    simple_present_tense_permissive_added = models.IntegerField(default=0, blank=True)
    simple_present_tense_permissive_causative_added = models.IntegerField(
        default=0, blank=True)
    simple_present_tense_potential_added = models.IntegerField(default=0, blank=True)
    simple_present_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    near_preterite_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    near_preterite_tense_causative_added = models.IntegerField(default=0, blank=True)
    near_preterite_tense_permissive_added = models.IntegerField(default=0, blank=True)
    near_preterite_tense_permissive_causative_added = models.IntegerField(
        default=0, blank=True)
    near_preterite_tense_potential_added = models.IntegerField(default=0, blank=True)
    near_preterite_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    witnessed_past_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    witnessed_past_tense_causative_added = models.IntegerField(default=0, blank=True)
    witnessed_past_tense_permissive_added = models.IntegerField(default=0, blank=True)
    witnessed_past_tense_permissive_causative_added = models.IntegerField(
        default=0, blank=True)
    witnessed_past_tense_potential_added = models.IntegerField(default=0, blank=True)
    witnessed_past_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    perfect_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    perfect_tense_causative_added = models.IntegerField(default=0, blank=True)
    perfect_tense_permissive_added = models.IntegerField(default=0, blank=True)
    perfect_tense_permissive_causative_added = models.IntegerField(default=0, blank=True)
    perfect_tense_potential_added = models.IntegerField(default=0, blank=True)
    perfect_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    plusquamperfect_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    plusquamperfect_tense_causative_added = models.IntegerField(default=0, blank=True)
    plusquamperfect_tense_permissive_added = models.IntegerField(default=0, blank=True)
    plusquamperfect_tense_permissive_causative_added = models.IntegerField(
        default=0, blank=True)
    plusquamperfect_tense_potential_added = models.IntegerField(default=0, blank=True)
    plusquamperfect_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    repeated_past_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    repeated_past_tense_causative_added = models.IntegerField(default=0, blank=True)
    repeated_past_tense_permissive_added = models.IntegerField(default=0, blank=True)
    repeated_past_tense_permissive_causative_added = models.IntegerField(
        default=0, blank=True)
    repeated_past_tense_potential_added = models.IntegerField(default=0, blank=True)
    repeated_past_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    possible_future_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    possible_future_tense_causative_added = models.IntegerField(default=0, blank=True)
    possible_future_tense_permissive_added = models.IntegerField(default=0, blank=True)
    possible_future_tense_permissive_causative_added = models.IntegerField(
        default=0, blank=True)
    possible_future_tense_potential_added = models.IntegerField(default=0, blank=True)
    possible_future_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    real_future_tense_basic_form_added = models.IntegerField(default=0, blank=True)
    real_future_tense_causative_added = models.IntegerField(default=0, blank=True)
    real_future_tense_permissive_added = models.IntegerField(default=0, blank=True)
    real_future_tense_permissive_causative_added = models.IntegerField(
        default=0, blank=True)
    real_future_tense_potential_added = models.IntegerField(default=0, blank=True)
    real_future_tense_inceptive_added = models.IntegerField(default=0, blank=True)

    word_category_added = models.IntegerField(default=0, blank=True)

    homonyms_added = models.IntegerField(default=0, blank=True)
    synonyms_added = models.IntegerField(default=0, blank=True)
    antonyms_added = models.IntegerField(default=0, blank=True)

    def increment(self, name):
        """Increments a counter specified by the 'name' argument."""
        self.__dict__[name] += 1

I'm not sure about this post_save method. I can exclude counting due to minor changes by users like changing 1 letter in word or description etc. through the logic of the field "accepted" in WordCoreModel. Any change will set accepted to false until some of the moderators set it manually to true again and this will prevent counting statistics in minor changes or abuse the statistics. But another thing is that only accepted words will be available in the application and setting it to false each time some one change it not that much good idea.

\$\endgroup\$
0
\$\begingroup\$

Instead of keeping a single version of each word, you can add an integer field for its version/revision. This way you can have multiple versions of the word and keep the last validated available while having pending edits. This will also allows you to more easily review who modified what.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.