So an Activity can possibly interact with a Post, Question, Answer or a Comment instance. In a practical scenario an Activity instance would represent a single interaction. For example, the User with ID 1 up voted a Question with ID 125:
And if I wanted to calculate how many up votes the Question 125 received, I could do something like that:
question = Question.objects.get(pk=125)up_votes = question.activity_set.filter(activity_type=Activity.UP_VOTE)# Display how many up votescount = up_votes.count()# Display the names of users who up votedup_voters = up_votes.values_list('user__first_name')
In a similar way we could work with the Post, Answer and Comment models.
Using the Generic Relations
To achieve the same result using Generic Relations, here is what the Activity models should look like:
from django.contrib.contenttypes.fields import GenericForeignKeyfrom django.contrib.contenttypes.models import ContentTypeclassActivity(models.Model): FAVORITE ='F' LIKE ='L' UP_VOTE ='U' DOWN_VOTE ='D' ACTIVITY_TYPES = ( (FAVORITE,'Favorite'), (LIKE,'Like'), (UP_VOTE,'Up Vote'), (DOWN_VOTE,'Down Vote'), ) user = models.ForeignKey(User) activity_type = models.CharField(max_length=1, choices=ACTIVITY_TYPES) date = models.DateTimeField(auto_now_add=True)# Below the mandatory fields for generic relation content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object =GenericForeignKey()
Now we are no longer keeping ForeignKey to other models we want to track the favorite, like, up vote and down vote activities. Meaning we can now track those activities to any model we want without having to modify the Activity model.
The relation is created in the model you want to track the Activity:
This also enables you to define a more meaningful name for the relations. For example, the Users can only interact with Post and Comment models to like it. While with the Answer model, they can only up vote/down vote. And finally with the Question model, the Users can up vote/down vote and favorite it.
Now if you want to like a Post, you could do something like this:
# Get the post objectpost = Post.objects.get(pk=1)# Add a like activitypost.likes.create(activity_type=Activity.LIKE, user=request.user)# Or in a similar way using the Activity model to add the likeActivity.objects.create(content_object=post, activity_type=Activity.LIKE, user=request.user)# Get all Activity instances related to Postpost.likes.all()# Count the number of likespost.likes.count()# Get the users who liked the postpost.likes.values_list('user__first_name', flat=True)
A good thing about it is that if have a new model that you wants to interact with Activity, you simply add a GenericRelation:
from django.contrib.contenttypes.fields import GenericRelationclassPicture(models.Model): user = models.ForeignKey(User) picture_file = models.ImageField(upload_to='uploads/pictures') date = models.DateTimeField(auto_now_add=True) favorites =GenericRelation(Activity)
You may also define an extra parameter in the GenericRelation:
from django.contrib.contenttypes.fields import GenericRelationclassPicture(models.Model): user = models.ForeignKey(User) picture_file = models.ImageField(upload_to='uploads/pictures') date = models.DateTimeField(auto_now_add=True) favorites =GenericRelation(Activity, related_query_name='pictures')
Then you can use it to query for example all favorited pictures that was uploaded by a given user:
user = User.objects.get(pk=1)
Activity.objects.filter(pictures__user=user)
Caveats
Adds an extra layer of complexity and will eventually make things slower.
GenericForeignKey does not accept an on_delete argument to customize this behavior. The default behavior will cascade all the relations.
One way to avoid the default behavior is to not define a GenericRelation. Example:
classComment(models.Model): text = models.CharField(max_length=500, blank=True) date = models.DateTimeField(auto_now_add=True)# Add a new instance of Commentcomment = Comment.objects.create(text='This is a test comment')# Like the commentActivity.objects.create(content_object=comment, activity_type=Activity.LIKE, user=request.user)
Now to get the list of likes this Comment received you must use the ContentType class:
from django.contrib.contenttypes.models import ContentType# Pass the instance we created in the snippet abovect = ContentType.objects.get_for_model(comment)# Get the list of likesActivity.objects.filter(content_type=ct, object_id=comment.id, activity_type=Activity.LIKE)
This is also an option if you want to interact with a model from Django’s contrib module or any third party model that you don’t have access to add a GenericRelation.