13

I want to build an webapp like Quora or Medium, where a user can follow users or some topics.

eg: userA is following (userB, userC, tag-Health, tag-Finance).

These are the models:

class Relationship(models.Model):
    user = AutoOneToOneField('auth.user')
    follows_user = models.ManyToManyField('Relationship', related_name='followed_by')
    follows_tag = models.ManyToManyField(Tag)

class Activity(models.Model):
    actor_type = models.ForeignKey(ContentType, related_name='actor_type_activities')
    actor_id = models.PositiveIntegerField()
    actor = GenericForeignKey('actor_type', 'actor_id')
    verb = models.CharField(max_length=10)
    target_type = models.ForeignKey(ContentType, related_name='target_type_activities')
    target_id = models.PositiveIntegerField()
    target = GenericForeignKey('target_type', 'target_id')
    tags = models.ManyToManyField(Tag)

Now, this would give the following list:

following_user = userA.relationship.follows_user.all()
following_user
[<Relationship: userB>, <Relationship: userC>]
following_tag = userA.relationship.follows_tag.all()
following_tag
[<Tag: tag-job>, <Tag: tag-finance>]

To filter I tried this way:

Activity.objects.filter(Q(actor__in=following_user) | Q(tags__in=following_tag))

But since actor is a GenericForeignKey I am getting 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.

How can I filter the activities that will be unique, with the list of users and list of tags that the user is following? To be specific, how will I filter GenericForeignKey with the list of the objects to get the activities of the following users.

4
  • OT: I successfully used this package (in production on a non trivial webapp) for the use case you describe : github.com/bitmazk/django-object-events. Commented Jun 27, 2016 at 7:42
  • hi @Aamu, Could you plz let me know, if just doing this gets you results without problems.. Activity.objects.filter(actor__in=following_user) works.? Commented Jun 27, 2016 at 7:46
  • @simonecittadini Thank you. I will surely check this out. Commented Jun 27, 2016 at 15:57
  • @Trying2Learn No, Activity.objects.filter(actor__in=following_user) is what's throwing the error. Commented Jun 27, 2016 at 15:57

5 Answers 5

6

You should just filter by ids.

First get ids of objects you want to filter on

following_user = userA.relationship.follows_user.all().values_list('id', flat=True)
following_tag = userA.relationship.follows_tag.all()

Also you will need to filter on actor_type. It can be done like this for example.

actor_type = ContentType.objects.get_for_model(userA.__class__)

Or as @Todor suggested in comments. Because get_for_model accepts both model class and model instance

actor_type = ContentType.objects.get_for_model(userA)

And than you can just filter like this.

Activity.objects.filter(Q(actor_id__in=following_user, actor_type=actor_type) | Q(tags__in=following_tag))
Sign up to request clarification or add additional context in comments.

2 Comments

I think this is the way to go as well, as far as I understand a GenericForeignKey will not give you access to the QuerySet, so you have to filter yourself on the Model and object id's, a bit like a composite key.
I just want to mention that ContentType.objects.get_for_model works with a model class or an instance of a model. So its perfectly fine to: ContentType.objects.get_for_model(userA).
3
+100

What the docs are suggesting is not a bad thing.

The problem is that when you are creating Activities you are using auth.User as an actor, therefore you can't add GenericRelation to auth.User (well maybe you can by monkey-patching it, but that's not a good idea).

So what you can do?

@Sardorbek Imomaliev solution is very good, and you can make it even better if you put all this logic into a custom QuerySet class. (the idea is to achieve DRY-ness and reausability)

class ActivityQuerySet(models.QuerySet):
    def for_user(self, user):
        return self.filter(
            models.Q(
                actor_type=ContentType.objects.get_for_model(user),
                actor_id__in=user.relationship.follows_user.values_list('id', flat=True)
            )|models.Q(
                tags__in=user.relationship.follows_tag.all()
            )
        )

class Activity(models.Model):
    #..
    objects = ActivityQuerySet.as_manager()

#usage
user_feed = Activity.objects.for_user(request.user)

but is there anything else?

1. Do you really need GenericForeignKey for actor? I don't know your business logic, so probably you do, but using just a regular FK for actor (just like for the tags) will make it possible to do staff like actor__in=users_following.

2. Did you check if there isn't an app for that? One example for a package already solving your problem is django-activity-steam check on it.

3. IF you don't use auth.User as an actor you can do exactly what the docs suggest -> adding a GenericRelation field. In fact, your Relationship class is suitable for this purpose, but I would really rename it to something like UserProfile or at least UserRelation. Consider we have renamed Relation to UserProfile and we create new Activities using userprofile instead. The idea is:

class UserProfile(models.Model):
    user = AutoOneToOneField('auth.user')
    follows_user = models.ManyToManyField('UserProfile', related_name='followed_by')
    follows_tag = models.ManyToManyField(Tag)

    activies_as_actor = GenericRelation('Activity',
        content_type_field='actor_type',
        object_id_field='actor_id',
        related_query_name='userprofile'
    )


class ActivityQuerySet(models.QuerySet):
    def for_userprofile(self, userprofile):
        return self.filter(
            models.Q(
                userprofile__in=userprofile.follows_user.all()
            )|models.Q(
                tags__in=userprofile.relationship.follows_tag.all()
            )
        )


class Activity(models.Model):
    #..
    objects = ActivityQuerySet.as_manager()

#usage

#1st when you create activity use UserProfile
Activity.objects.create(actor=request.user.userprofile, ...)

#2nd when you fetch. 
#Check how `for_userprofile` is implemented this time
Activity.objects.for_userprofile(request.user.userprofile)

Comments

2

As stated in the documentation:

Due to the way GenericForeignKey is implemented, you cannot use such fields directly with filters (filter() and exclude(), for example) via the database API. Because a GenericForeignKey isn’t a normal field object, these examples will not work:

You could follow what the error message is telling you, I think you'll have to add a GenericRelation relation to do that. I do not have experience doing that, and I'd have to study it but...

Personally I think this solution is too complex to what you're trying to achieve. If only the user model can follow a tag or authors, why not include a ManyToManyField on it. It would be something like this:

class Person(models.Model):
    user = models.ForeignKey(User)
    follow_tag = models.ManyToManyField('Tag')
    follow_author = models.ManyToManyField('Author')

You could query all followed tag activities per Person like this:

Activity.objects.filter(tags__in=person.follow_tag.all())

And you could search 'persons' following a tag like this:

Person.objects.filter(follow_tag__in=[<tag_ids>])

The same would apply to authors and you could use querysets to do OR, AND, etc.. on your queries.

If you want more models to be able to follow a tag or author, say a System, maybe you could create a Following model that does the same thing Person is doing and then you could add a ForeignKey to Follow both in Person and System

Note that I'm using this Person to meet this recomendation.

2 Comments

Sorry, I had the model like yours, I forgot to update it.
What are you trying to achieve with the GenericRelation?
1

You can query seperately for both usrs and tags and then combine them both to get what you are looking for. Please do something like below and let me know if this works..

usrs = Activity.objects.filter(actor__in=following_user)    
tags = Activity.objects.filter(tags__in=following_tag)

result = usrs | tags

4 Comments

Activity.objects.filter(actor__in=following_user) this is throwing the above error. Activity.objects.filter(tags__in=following_tag) is working fine.
Does this work user_ids = following_user.values_list('id', flat=True) ... usrs = Activity.objects.filter(actor__in=user_ids)?
Activity.objects.filter(actor__in=user_ids) giving me the same 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. ).
Did you try creating a GenericRelation for the same? docs.djangoproject.com/en/1.9/ref/contrib/contenttypes/…
1

You can use annotate to join the two primary keys as a single string then use that to filter your queryset.

from django.db.models import Value, TextField
from django.db.models.functions import Concat

following_actor = [
    # actor_type, actor
    (1, 100),
    (2, 102),
]
searchable_keys = [str(at) + "__" + str(actor) for at, actor in following_actor]

result = MultiKey.objects.annotate(key=Concat('actor_type', Value('__'), 'actor_id', 
                                              output_field=TextField()))\
    .filter(Q(key__in=searchable_keys) | Q(tags__in=following_tag))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.