Django: NameError with instance of model with Generic Foreign Field in, created by post_save signal

I have 3 models that I am dealing with here: SurveyQuestion, Update, and Notification. I use a post_save signal to create an instance of the Notification model whenever an instance of SurveyQuestion or Update was created.

The Notification model has a GenericForeignKey which goes to whichever model created it. Inside the Notification model I try to use the ForeignKey to set __str__ as the title field of the instance of the model that created it. Like so:

class Notification(models.Model):
    source_object = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    source = GenericForeignKey("source_object", "object_id")
    #more stuff

    def __str__(self):
        return f'{self.source.title} notification'

I am able to create instances of SurveyQuestion and Update from the admin panel, which is then (supposed to be) creating an instance of Notification. However, when I query instances of Notification in the shell:

from hotline.models import Notification
notifications = Notification.objects.all()
for notification in notifications:
    print (f"Notification object: {notification}")

NoneType
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
      1 for notification in notifications:
----> 2     print (notification)
File ~/ygzsey/hotline/models.py:27, in Notification.__str__(self)
     26 def __str__(self):
---> 27     return f'{self.source.title} notification'
AttributeError: 'NoneType' object has no attribute 'title'

When I query instances of SurveyQuestion:

from hotline.models import SurveyQuestion
surveys = SurveyQuestion.objects.all()
for survey in surveys:
    print (f"Model: {survey.__class__.__name__}")

Model: SurveyQuestion

When I query instances of Notification and try to print the class name of their ForeignKey field (I labled it source), I get this:

for notification in notifications:
    print (f"Notification for {notification.source.__class__.__name__}")

Notification for NoneType
Notification for NoneType
Notification for NoneType

So it seems that the SurveyQuestion, Update, and Notification instances are saving properly, but there is some problem with the GenericForeignKey. Any help would be appreciated.

Note: If it is relevant, I had the post_save create an instance of Notification using Notification(source_object=instance, start_date=instance.start_date, end_date=instance.end_date), but that would give me an error when trying to save an instance of SurveyQuestion or Update in the admin panel:

ValueError at /admin/hotline/update/add/
Cannot assign "<Update: Update - ad>": "Notification.source_object" must be a "ContentType" instance.

So I changed it to Notification(source_object=ContentType.objects.get_for_model(instance), start_date=instance.start_date, end_date=instance.end_date), I don't remember the question+answer on here that I based it on, but it was somwhere on here.

Thanks!

My full models.py:

from django.db import models
from datetime import timedelta
from django.utils import timezone
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import post_save
from django.dispatch import receiver

def tmrw():
    return timezone.now() + timedelta(days=1)

class Notification(models.Model):
    source_object = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    source = GenericForeignKey("source_object", "object_id")
    start_date = models.DateTimeField(default=timezone.now)
    end_date = models.DateTimeField(default=tmrw)

    class Meta:
        verbose_name = 'Notification'
        verbose_name_plural = f'{verbose_name}s'

    def __str__(self):
        return f'{self.source.title} notification'

class Update(models.Model):
    title = models.CharField(max_length=25)
    update = models.TextField()
    start_date = models.DateTimeField(default=timezone.now)
    end_date = models.DateTimeField(default=tmrw)
    #notification = GenericRelation(Notification, related_query_name='notification')

    class Meta:
        verbose_name = 'Update'
        verbose_name_plural = f'{verbose_name}s'

    def __str__(self):
        return f'{self.__class__.__name__} - {self.title}'

class SurveyQuestion(models.Model):
    title = models.CharField(max_length=25)
    question = models.TextField()
    start_date = models.DateTimeField(default=timezone.now)
    end_date = models.DateTimeField(default=tmrw)
    #notification = GenericRelation(Notification, related_query_name='notification')

    class Meta:
        verbose_name = 'Survey'
        verbose_name_plural = f'{verbose_name}s'

    def __str__(self):
        return f'{self.__class__.__name__} - {self.title}'

class SurveyOption(models.Model):
    survey = models.ForeignKey(SurveyQuestion, on_delete=models.CASCADE, related_name='options')
    option = models.TextField()
    id = models.AutoField(primary_key=True)

    class Meta:
        verbose_name = 'Survey option'
        verbose_name_plural = f'{verbose_name}s'

    def __str__(self):
        return f'{self.survey.title} option #{self.id}'

@receiver(post_save)
def create_notification(instance, **kwargs):
    #"""
    print (f"instance: {instance}")
    print (f"instance.__class__: {instance.__class__}")
    print (f"instance.__class__.__name__: {instance.__class__.__name__}")
    #"""
    senders = ['SurveyQuestion', 'Update']
    if instance.__class__.__name__ in senders:
        notification = Notification(source_object=ContentType.objects.get_for_model(instance), start_date=instance.start_date, end_date=instance.end_date)
        notification.save()

post_save.connect(create_notification)

You should use source, not source_object:

Notification(
    source=instance, start_date=instance.start_date, end_date=instance.end_date
)

A GenericForeignKey essentially combines two columns, the source_object (very bad name) that points to the type of the item the GenericForeignKey refers to, and a column that stores the primary key (or another unique column) of that object.

Вернуться на верх