Search code examples
djangoormsignalsfixtures

Django signals: conflicts with models inheritining from the same class


I encountered a strange behavior while applying a signal to a new model, I'm not sure to understand what is wrong but it seems related with the fact that I used abstract classes.

The models (simplified)

Basically, I have Article, Photo (inheriting from Post)

class Post(models.Model): 
    class Meta:
        abstract        = True

    some_field    = models.Something()

class Article(Post):
    category = models.ForeignKey(Article_category, null=True, on_delete=models.SET_NULL)
    some_field     = models.Something()

class Photo(Post):
    category = models.ForeignKey(Photo_category, null=True, on_delete=models.SET_NULL)
    some_field     = models.Something()
    

and their respective Categories

class Category(models.Model):
    class Meta: 
        abstract = True 

    parent = models.ForeignKey('self', null=True, blank=True, related_name='nested_category', on_delete=models.SET_NULL)
    name   = models.CharField(max_length=50)
    count  = models.PositiveSmallIntegerField(default=0, editable=False)
    
class Article_category(Category):

    @classmethod
    def load(cls):
        cache.set('{}'.format(cls.__name__), cls.objects.all()) 

class Photo_category(Category):

    @classmethod
    def load(cls):
        cache.set('{}'.format(cls.__name__), cls.objects.all())

The signal

A straighforward incremental counter. Every time an article/photo is created, it's corresponding category count is updated and the entire model is saved in the cache (for templating purposes)

from django.db.models import F

@receiver(post_save, sender=Article) ----> here comes trouble
@receiver(post_save, sender=Photo)
def add_one_to_count(sender, instance, **kwargs):
    cat = type(instance.category).objects.get(name=instance.category)
    cat.count = F('count')+1
    cat.save()
    cache.set('{}_category'.format(sender.__name__), type(instance.category).objects.all())

The problem

What you saw above works like a charm for @receiver(post_save, sender=Photo) but when I add @receiver(post_save, sender=Article), DB initialization with fixture fails and I only get emptyset tables (mariaDB). This very line is the only one changing fail to success and I can't figure why. Since count is defined in the abstract class, I wondered whether it had something to do with it, for I did not have any issue applying a similar logic to categories:

# this works perfectly
@receiver(post_save, sender=Photo_category)
@receiver(post_delete, sender=Photo_category)
@receiver(post_save, sender=Article_category)
@receiver(post_delete, sender=Article_category)
def refresh_cached_category(sender, instance, using, **kwargs):
    cache.set('{}'.format(type(instance).__name__), type(instance).objects.all())

Thanks for any enlightenment

The complete Traceback

 Traceback (most recent call last):
   File "manage.py", line 21, in <module>
     main()
   File "manage.py", line 17, in main
     execute_from_command_line(sys.argv)
   File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 381, in execute_from_command_line
     utility.execute()
  File "/usr/local/lib/python3.7/site-packages/django/core/management/__init__.py", line 375, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 323, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/base.py", line 364, in execute
    output = self.handle(*args, **options)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/commands/loaddata.py", line 72, in handle
    self.loaddata(fixture_labels)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/commands/loaddata.py", line 114, in loaddata
    self.load_label(fixture_label)
  File "/usr/local/lib/python3.7/site-packages/django/core/management/commands/loaddata.py", line 181, in load_label
    obj.save(using=self.using)
  File "/usr/local/lib/python3.7/site-packages/django/core/serializers/base.py", line 223, in save
    models.Model.save_base(self.object, using=using, raw=True, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 790, in save_base
    update_fields=update_fields, raw=raw, using=using,
  File "/usr/local/lib/python3.7/site-packages/django/dispatch/dispatcher.py", line 175, in send
    for receiver in self._live_receivers(sender)
  File "/usr/local/lib/python3.7/site-packages/django/dispatch/dispatcher.py", line 175, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "/usr/src/cms/website/observers.py", line 26, in add_one_to_count
    cat = type(instance.category).objects.get(name=instance.category)
  File "/usr/local/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/db/models/query.py", line 408, in get
    self.model._meta.object_name
 website.models.DoesNotExist: Problem installing fixture '/usr/src/cms/../test/data_dev.yaml': Article_category matching query does not exist.

Solution

  • You can't filter on name=instance.category in your query, because that's not a str. You need to filter on name=instance.category.name but first you also need to make sure instance.category isn't None (since it can be).

    The thing I don't understand is why you perform a query in the first place, just to fetch the same object: instance.category is the same as ArticleCategory.objects.get(name=instance.category.name) assuming the name is unique, except you do an extra query to the db.

    Also the query will raise an exception if you have two categories with the same name (which you don't exclude in your model). So your code should be:

    def add_one_to_count(sender, instance, **kwargs):
        if instance.category:
            instance.category.count = F('count')+1
            instance.category.save()
            cache.set('{}_category'.format(sender.__name__), type(instance.category).objects.all())