Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Filter GenericForeignKey by list of different model objects

I am trying to build activity logs by a User or any other model object, to which a user can follow or subscribe to.

These are the models related with logging the activities:

class Activity(models.Model):
    """
    An activity log : Actor acts on target
    Actor can be a User or a model Object
    """
    actor_content_type = models.ForeignKey(
        ContentType, on_delete=models.CASCADE, related_name="actor_type")
    actor_object_id = models.PositiveIntegerField()
    actor = GenericForeignKey('actor_content_type', 'actor_object_id')
    description = models.TextField()
    target_content_type = models.ForeignKey(
        ContentType, on_delete=models.CASCADE, related_name="target_type")
    target_object_id = models.PositiveIntegerField()
    target = GenericForeignKey('target_content_type', 'target_object_id')


class Follow(models.Model):
    """
    A user can follow any User or model objects.
    """
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    follow_object = GenericForeignKey('content_type', 'object_id')

Suppose:

  • Thor follows user Loki // User follows User
  • Thor follows user Stark // User follows User
  • Thor follows project Avenger // User follows Project

And suppose these are activities:

  • Nick Fury Created a project "Shield"
  • Nick Fury Created a project "Avengers"
  • Stark created a project "Mark II"
  • Avengers added Vision
  • Dr Strange created a project Dormammu

I can get the user Thor:

thor = User.objects.get(email="[email protected]")

And get the list of users/objects followed by Thor:

thor.follow_set.all()
<QuerySet [<Follow: [email protected] follows [email protected]>, <Follow: [email protected] follows [email protected]>, <Follow: [email protected] follows Avengers>]>

Now to get the list of activities followed by the user Thor, I tried this:

q = (Q(actor__in=thor.follow_set.all())| Q(target__in=thor.follow_set.all())

Activity.objects.filter(q)

But its throwing an error:

FieldError: Field 'actor' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.

I can get all the activities for a single object followed by user Thor:

followed_last = thor.follow_set.last().follow_object

q = (Q(actor_content_type=ContentType.objects.get_for_model(followed_last), actor_object_id=followed_last.id)| Q(target_content_type=ContentType.objects.get_for_model(followed_last), target_object_id=followed_last.id))

Activity.objects.filter(q)
<QuerySet [<Activity: Avengers Added [email protected]>, <Activity: [email protected] Created Avengers>]>

But how can I get all the activities Thor is following from the above activities:

  • Nick Fury Created a project "Avengers"
  • Stark created a project "Mark II"
  • Avengers added Vision

Update

Added User model as requested:

class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(max_length=255, unique=True)
    fullname = models.CharField(max_length=255, validators=[validate_fullname])
    is_active = models.BooleanField(default=False)
    is_admin = models.BooleanField(default=False)
    date_joined = models.DateTimeField(auto_now_add=True)

    objects = UserManager()

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["fullname"]

    def __str__(self):
        return self.email
like image 623
Msw Tm Avatar asked Nov 19 '25 12:11

Msw Tm


1 Answers

A GenericForeignKey is in simple words a composition of two fields (a foreign key to ContentType and a field to store the related primary key) to point to a specific object. Looking at your code:

q = (Q(actor__in=thor.follow_set.all())| Q(target__in=thor.follow_set.all())

Activity.objects.filter(q)

This kind of filtering does not work with a GenericForeignKey basically because if it did, in case one tries to access related attributes Django would have to perform tasks like:

  • Make several joins
  • Figure out which tables to join so as to not get errors

This would be quite complex and have several problems like long query times if there are too many joins, etc.

Also your code is somewhat incorrect as it filters Activity objects related to Follow objects and not the followed object.

To solve your problem you can instead filter on actor_content_type, actor_object_id, etc. instead:

follows = thor.follow_set.all()

filters = [Q(actor_content_type=follow.content_type)
     & Q(actor_object_id=follow.object_id)
     | Q(target_content_type=follow.content_type)
     & Q(target_object_id=follow.object_id) for follow in follows]

Activity.objects.filter(Q(*filters, _connector=Q.OR))
like image 191
Abdul Aziz Barkat Avatar answered Nov 21 '25 06:11

Abdul Aziz Barkat



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!