3
\$\begingroup\$

I am creating a translation application and the users can add new translations to the "core" language, e.g. a Latin translation of the German word, and other users can vote on this translation. A translation can only be voted up or down once by the same user.

Here are my django models for these purposes, could you please look through this code and give me some feedback.

class BaseModel(models.Model):
    class PublishingStatus(models.TextChoices):
        DRAFT = 'draft', _('Draft')
        ACCEPTED = 'accepted', _('Accepted'),
        REJECTED = 'rejected', _('Rejected')

    publishing_status = models.CharField(
        max_length=9,
        choices=PublishingStatus.choices,
        default=PublishingStatus.DRAFT,
        help_text="Publishing status represents the state of the object. By default it is 'draft'"
    )
    created_at = models.DateTimeField(auto_now_add=True)
    created_by = models.ForeignKey(
        get_user_model(), on_delete=models.CASCADE,
        related_name='%(class)s_created_by', null=True, blank=True)

    modified_at = models.DateTimeField(auto_now=True)
    modified_by = models.ForeignKey(
        get_user_model(), on_delete=models.CASCADE,
        related_name='%(class)s_modified_by', null=True, blank=True)

    accepted_at = models.DateTimeField(null=True)
    accepted_by = models.ForeignKey(
        get_user_model(), on_delete=models.CASCADE,
        related_name='%(class)s_accepted_by', null=True, blank=True)

    rejected_at = models.DateTimeField(null=True)
    rejected_by = models.ForeignKey(
        get_user_model(), on_delete=models.CASCADE,
        related_name='%(class)s_rejected_by', null=True, blank=True)
    rejection_reason = models.TextField(blank=True, default="")

    class Meta:
        abstract = True


class LatinGermanTranslation(BaseModel):
    lt = models.ForeignKey(
        LtWords, on_delete=models.CASCADE, related_name="lt_de_translations", null=True, blank=True)
    de = models.ForeignKey(
        'GermanWord', on_delete=models.CASCADE, related_name="de_lt_translations", null=True, blank=True)
    vote = models.IntegerField(default=0)

    class Meta:
        verbose_name = 'Latin German Translations'
        verbose_name_plural = 'Latin German Translations'

    def up_vote(self, created_by):
        try:
            self.de_transl.create(
                voted_by=created_by, transl=self, vote_type=GermanTranslationVote.Vote.UP)
            self.vote += 1
            self.save()
        except (DatabaseError, IntegrityError, ValueError) as e:
            raise Exception(f'Some error occurred during up vote: {e}')
        return 'ok'

    def down_vote(self, created_by):
        try:
            self.de_transl.create(
                voted_by=created_by, transl=self, vote_type=GermanTranslationVote.Vote.DOWN)
            self.vote -= 1
            self.save()
        except (DatabaseError, IntegrityError, ValueError) as e:
            raise Exception(f'Some error occurred during down vote: {e}')
        return 'ok'

    def __str__(self):
        return f'{self.lt} - {self.de}'


class GermanWord(BaseModel):
    word = models.CharField(max_length=255, default="", blank=True)
    lt_word = models.ManyToManyField(
        LtWords, through=LatinGermanTranslation, through_fields=('de', 'lt'), blank=True)

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

    def __str__(self):
        return self.word


class GermanTranslationVote(models.Model):

    class Vote(models.TextChoices):
        UP = 'up', _('Up')
        DOWN = 'down', _('Down')

    translation = models.OneToOneField(
        LatinGermanTranslation, on_delete=models.CASCADE,
        related_name='de_transl')
    voted_by = models.OneToOneField(
        get_user_model(), on_delete=models.CASCADE,
        related_name='vore_de', null=True, blank=True)
    vote_type = models.CharField(
        max_length=4, choices=Vote.choices, default=None)

    class Meta:
        indexes = [models.Index(fields=['translation', 'voted_by'])]
        verbose_name = 'Latin Word Vote'
        verbose_name_plural = 'Latin Words Votes'

Espetially I'd like to hear about the translation and voted_by attributes inside GermanTranslationVote model, since I'm not sure about OneToOne implementation.

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I've honestly never used Django before, so I'm not going to touch on that aspect.

You can reduce a little duplication that exists in your *_vote methods though:

def _vote(self, created_by, vote_change, vote_type):
    try:
        self.de_transl.create(
            voted_by=created_by, transl=self, vote_type=vote_type)
        self.vote += vote_change
        self.save()
    except (DatabaseError, IntegrityError, ValueError) as e:
        direction = "down" if vote_change < 0 else "up"  # Will say "up" on 0.
        raise Exception(f'Some error occurred during {direction} vote: {e}')
    return 'ok'

def up_vote(self, created_by):
    return self._vote(created_by, 1, GermanTranslationVote.Vote.UP)

def down_vote(self, created_by):
    return self._vote(created_by, -1, GermanTranslationVote.Vote.DOWN)

Now you only need to have the bulky exception handling and database management once.


You can do something similar for the repeated *_at and *_by variables inside BaseModel:

def atByPair(related_name):
    at_model = models.DateTimeField(auto_now_add=True)
    by_model = models.ForeignKey(
                   get_user_model(), on_delete=models.CASCADE,
                   related_name=related_name, null=True, blank=True)

    return at_model, by_model   

created_at, created_by = atByPair('%(class)s_created_by')

modified_at, modified_by = atByPair('%(class)s_modified_by')

accepted_at, accepted_by = atByPair('%(class)s_accepted_by')

rejected_at, rejected_by = atByPair('%(class)s_rejected_by')
rejection_reason = models.TextField(blank=True, default="")

Much less duplication and worrying about needing to make consistent changes in multiple places.

\$\endgroup\$
3
  • \$\begingroup\$ Thank you so much! I especially like your implementation of the _vote method. I'm not sure about defining attributes with the atByPair method, because django suggests to set all methods after the Meta class. \$\endgroup\$
    – khashashin
    Commented Apr 8, 2020 at 13:31
  • \$\begingroup\$ You're welcome. And I was going to comment on how odd the setup is here (classes inside of classes and many class-level attributes), but I figured that it had something to do with Django. If something I suggested doesn't work with Django, then ignore it. I'm not sure how Django operates. \$\endgroup\$ Commented Apr 8, 2020 at 13:34
  • \$\begingroup\$ atByPair should be converted to lowercase (Python code style). \$\endgroup\$
    – Uri
    Commented May 31, 2020 at 12:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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