Search code examples
pythondjangoclassdjango-modelsinstance

Access model instance inside model field


I have a model (Event) that has a ForeignKey to the User model (the owner of the Event). This User can invite other Users, using the following ManyToManyField:

    invites = models.ManyToManyField(
                  User, related_name="invited_users", 
                  verbose_name=_("Invited Users"), blank=True
              )

This invite field generates a simple table, containing the ID, event_id and user_id.

In case the Event owner deletes his profile, I don't want the Event to be deleted, but instead to pass the ownership to the first user that was invited.

So I came up with this function:

    def get_new_owner():
        try:
            invited_users = Event.objects.get(id=id).invites.order_by("-id").filter(is_active=True)
            if invited_users.exists():
                return invited_users.first()
            else:
                Event.objects.get(id=id).delete()
        except ObjectDoesNotExist:
            pass

This finds the Event instance, and returns the active invited users ordered by the Invite table ID, so I can get the first item of this queryset, which corresponds to the first user invited.

In order to run the function when a User gets deleted, I used on_delete=models.SET:

    owner = models.ForeignKey(User, related_name='evemt_owner', verbose_name=_("Owner"), on_delete=models.SET(get_new_owner()))

Then I ran into some problems:

  1. It can't access the ID of the field I'm passing
  2. I could'n find a way to use it as a classmethod or something, so I had to put the function above the model. Obviously this meant that it could no longer access the class below it, so I tried to pass the Event model as a parameter of the function, but could not make it work.

Any ideas?


Solution

  • First we can define a strategy for the Owner field that will call the function with the object that has been updated. We can define such deletion, for example in the <i.app_name/deletion.py file:

    # app_name/deletion.py
    
    def SET_WITH(value):
        if callable(value):
            def set_with_delete(collector, field, sub_objs, using):
                for obj in sub_objs:
                    collector.add_field_update(field, value(obj), [obj])
        else:
            def set_with_delete(collector, field, sub_objs, using):
                collector.add_field_update(field, value, sub_objs)
        set_with_delete.deconstruct = lambda: ('app_name.SET_WITH', (value,), {})
        return set_with_delete

    You should pass a callable to SET, not call the function, so you implement this as:

    from django.conf import settings
    from django.db.models import Q
    from app_name.deletion import SET_WITH
    
    def get_new_owner(event):
        invited_users = event.invites.order_by(
            'eventinvites__id'
        ).filter(~Q(pk=event.owner_id), is_active=True).first()
        if invited_users is not None:
            return invited_users
        else:
            event.delete()
    
    
    class Event(models.Model):
        # …
        owner = models.ForeignKey(
            settings.AUTH_USER_MODEL,
            related_name='owned_events',
            verbose_name=_('Owner'),
            on_delete=models.SET_WITH(get_new_owner)
        )

    Here we thus will look at the invites to find a user to transfer the object to. Perhaps you need to exclude the current .owner of the event in your get_new_owner from the collection of .inivites.

    We can, as @AbdulAzizBarkat says, better work with a CASCADE than explicitly delete the Event object , since that will avoid infinite recursion where an User delete triggers an Event delete that might trigger a User delete: at the moment this is not possible, but later if extra logic is implemented one might end up in such case. In that case we can work with:

    from django.db.models import CASCADE
    
    def SET_WITH(value):
        if callable(value):
            def set_with_delete(collector, field, sub_objs, using):
                for obj in sub_objs:
                    val = value(obj)
                    if val is None:
                        CASCADE(collector, field, [obj], using)
                    else:
                        collector.add_field_update(field, val, [obj])
        else:
            def set_with_delete(collector, field, sub_objs, using):
                collector.add_field_update(field, value, sub_objs)
        set_with_delete.deconstruct = lambda: ('app_name.SET_WITH', (value,), {})
        return set_with_delete

    and rewrite the get_new_owner to:

    def get_new_owner(event):
        invited_users = event.invites.order_by(
            'eventinvites__id'
        ).filter(~Q(pk=event.owner_id), is_active=True).first()
        if invited_users is not None:
            return invited_users
        else:  # strictly speaking not necessary, but explicit over implicit
            return None