Search code examples
djangodjango-modelsabstract-classunique-constraint

Constraining instantiation of a regular class based on foreginkeys to a regular class and an abstract class


I am creating my first django project and after having created a model A I realize that I want to create other models B, C ... that have certain traits in common with A but which needs to be separat models. Thus I created an abstract class 'InteractableItem' with these traits. I want users to be able to like interactable items, however I also want to constrain the instantiation of a 'Like' model such that each user only can like a given interactable item once. To solve this I tried creating a models.UniqueConstraint in the like model between the fields 'user' and 'interactableitem'. This gave me the following error

ERRORS:
feed.Like.interactableitem: (fields.E300) Field defines a relation with model 'InteractableItem', which is either not installed, or is abstract.
feed.Like.interactableitem: (fields.E307) The field feed.Like.interactableitem was declared with a lazy reference to 'feed.interactableitem', but app 'feed' doesn't provide model 'interactableitem'.

I realise that my error is the referencing of an abstract class through a ForeignKey, however I dont see a way to constrain the instantiation of the like if the 'user' liking and the 'interactableitem' being like are not both fields in 'like'. This is where I need help.

How do you establish an instantiation constraint on such a 'Like' model?

Here I provide my referenced models:

class InteractableItem(models.Model):
    date_created = models.DateTimeField(auto_now_add=True)
    date_update = models.DateTimeField(auto_now=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)

    class Meta:
        abstract = True

class Like(models.Model):
    date_created = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True)
    interactableitem = models.ForeignKey(InteractableItem, on_delete=models.CASCADE, null=True, blank=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['user', 'interactableitem'], name='user_unique_like'),
        ]

Solution

  • After having tested different ways to constrain the instatiation of the 'like' model I realised that there was a way around this. I let the abstract class 'BaseItem' have a ForeignKey to a regular class 'interactableitem'. 'Like' now has a models.Uniqueconstrait with two ForeignKey fields to regular classes which works. This way I just have to makesure that interactableitem is only created once, I'm sure there are better ways of doing this but this if self.pk should work for all scenarios I can think of, as it checks if the item is in the database before creation.

    class InteractableItem(models.Model):
    pass
    
    
    class SomeItem(models.Model):
        date_created = models.DateTimeField(auto_now_add=True)
        date_update = models.DateTimeField(auto_now=True)
        interactableitem = models.ForeignKey(InteractableItem, on_delete=models.CASCADE, null=True, blank=True)
        author = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
    
        class Meta:
            abstract = True
    
    
        def save(self, *args, **kwargs):
            if not self.pk:
                self.interactableitem = InteractableItem.objects.create()
            super(SomeItem, self).save(*args, **kwargs)
    
    
    
    class Like(models.Model):
        date_created = models.DateTimeField(auto_now_add=True)
        user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True)
        interactableitem = models.ForeignKey(InteractableItem, on_delete=models.CASCADE, null=True, blank=True)
    
        class Meta:
            constraints = [
                models.UniqueConstraint(fields=['user', 'interactableitem'], name='user_unique_like'),
            ]