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.