djangocascadedjango-class-based-viewsgeneric-foreign-key

Django failing to cascade-delete related generic foreign key object


I have the following defined in my model:

class TaskLink(models.Model):
    task = model.ForeignKey(Task)
    link_choices = (
        models.Q(app_label="accounts", model="location"),
        # Other models are also linked to here.
    )
    linked_content_type = \
        models.ForeignKey(
            ContentType,
            limit_choices_to=link_choices
        )
    linked_object_id = models.PositiveIntegerField()
    linked_object = \
        generic.GenericForeignKey(
            'linked_object_content_type',
            'linked_object_id'
        )

This model links Task objects with any of the models in the link_choices tuple. In this case, the accounts.Location model is in this list.

My problem comes when the deletion of a Location object results in the cascade-deletion of related TaskLink objects. Deletion fails with the following error message:

django.core.exceptions.FieldError: Cannot resolve keyword 'object_id' into field. Choices are: id, linked_object, linked_object_content_type, linked_object_content_type_id, linked_object_id, task, task_id

The view is an instance of django.views.generic.DeleteView with only the pk_url_kwarg parameter and model set (and permissions decorators added to the dispatch method); it worked linked_object_fine before I added the TaskLink model to the mix.

What am I missing?

EDIT: It seems this may be a bug in Django; when cascade-deleting objects through generic foreign keys, Django ignores the field name strings you pass to the constructor for the GenericForeignKey field and looks instead for the content_type and object_id fields, which, in my case, didn't exist. This effectively limits the number of generic foreign keys a model may have to 1 unless you won't be running into cascade deletion.

I have sent this issue through the Django mailing list as that behavior may be intentional.


Solution

  • rename field names of TaskLink

    linked_content_type >>> content_type
    linked_object_id >>> object_id
    

    or write pre signal while deleting "Location" object to delete linked object "TaskLink"

    from django.db.models.signals import pre_delete
    from django.dispatch import receiver
    
    @receiver(pre_delete, sender=Location, dispatch_uid='location_delete_signal')
    def deleted_gfk_TaskLink(sender, instance, using, **kwargs):
        ctype = ContentType.objects.get_for_model(sender)
        obj = TaskLink.objects.get(linked_content_type=ctype, linked_object_id=instance.id)
        obj.delete()
    

    reference for custom signals:
    https://micropyramid.com/blog/using-djangos-built-in-signals-and-writing-custom-signals/