In the previous chapter, you implemented AJAX views into your project using jQuery and built a JavaScript bookmarklet for sharing content from other websites in your platform.
In this chapter, you will learn how to build a follower system and create a user activity stream. You will discover how Django signals work and integrate Redis fast I/O storage into your project to store item views.
This chapter will cover the following points:
We will build a follower system into our project. Our users will be able to follow each other and track what other users share on the platform. The relationship between users is a many-to-many relationship, A user can follow multiple users and can be followed back by multiple users.
In previous chapters, you created many-to-many relationships by adding a ManyToManyField
to one of the related models and letting Django create the database table for the relationship. This is suitable for most of the cases, but sometimes you might need to create an intermediate model for the relation. Creating an intermediary model is necessary when you want to store additional information for the relationship, for example the date when the relation was created or a field that describes the type of the relationship.
We will create an intermediary model for building relationships between users. There are two reasons why we want to use an intermediate model:
Edit the models.py
file of your account
application and add the following code to it:
from django.contrib.auth.models import User class Contact(models.Model): user_from = models.ForeignKey(User, related_name='rel_from_set') user_to = models.ForeignKey(User, related_name='rel_to_set') created = models.DateTimeField(auto_now_add=True, db_index=True) class Meta: ordering = ('-created',) def __str__(self): return '{} follows {}'.format(self.user_from, self.user_to)
This is the Contact
model we will use for user relationships. It contains the following fields:
user_from
: A ForeignKey
for the user that creates the relationshipuser_to
: A ForeignKey
for the user being followedcreated
: A DateTimeField
field with auto_now_add=True
to store the time when the relationship was createdA database index is automatically created on ForeignKey
fields. We use db_index=True
to create a database index for the created
field. This will improve query performance when ordering QuerySets by this field.
Using the ORM, we could create a relationship for a user user1
following another user user2
, like this:
user1 = User.objects.get(id=1) user2 = User.objects.get(id=2) Contact.objects.create(user_from=user1, user_to=user2)
The related managers rel_from_set
and rel_to_set
will return a queryset for the Contact
model. In order to access the end side of the relationship from the User
model, it would be desirable that User
contained a ManyToManyField
as follows:
following = models.ManyToManyField('self', through=Contact, related_name='followers', symmetrical=False)
In this example, we tell Django to use our custom intermediary model for the relationship by adding through=Contact
to the ManyToManyField
. This is a many-to-many relationship from the User
model to itself: We refer to 'self'
in the ManyToManyField
field to create a relationship to the same model.
If the User
model was part of our application, we could add the previous field to the model. However, we cannot alter the User
class directly because it belongs to the django.contrib.auth
application. We are going to take a slightly different approach, by adding this field dynamically to the User
model. Edit the models.py
file of the account
application and add the following lines:
# Add following field to User dynamically User.add_to_class('following', models.ManyToManyField('self', through=Contact, related_name='followers', symmetrical=False))
In this code, we use the add_to_class()
method of Django models to monkey-patch
the User
model. Be aware that using add_to_class()
is not the recommended way for adding fields to models. However, we take advantage from using it in this case because of the following reasons:
user.followers.all()
and user.following.all()
. We use the intermediary Contact
model and avoid complex queries that would involve additional database joins, as it would have been if we had defined the relationship in our custom Profile
model.Contact
model. Thus, the ManyToManyField
added dynamically will not imply any database changes for the Django User
model.User
.Keep in mind that in most cases, it is preferable to add fields to the Profile
model we created before, instead of monkey-patching the User
model. Django also allows you to use custom user models. If you want to use your custom user model, take a look at the documentation at https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#specifying-a-custom-user-model.
You can see that the relationship includes symmetrical=False
. When you define a ManyToManyField
to the model itself, Django forces the relationship to be symmetrical. In this case, we are setting symmetrical=False
to define a non-symmetric relation. This is, if I follow you, it doesn't mean you automatically follow me.
Run the following command to generate the initial migrations for the account
application:
python manage.py makemigrations account
You will see the following output:
Migrations for 'account': 0002_contact.py: - Create model Contact
Now run the following command to sync the application with the database:
python manage.py migrate account
You should see an output that includes the following line:
Applying account.0002_contact... OK
The Contact
model is now synced to the database and we are able to create relationships between users . However, our site doesn't offer a way to browse through users or see a particular user profile yet. Let's build list and detail views for the User
model.
Open the views.py
file of the account
application and add the following code to it:
from django.shortcuts import get_object_or_404 from django.contrib.auth.models import User @login_required def user_list(request): users = User.objects.filter(is_active=True) return render(request, 'account/user/list.html', {'section': 'people', 'users': users}) @login_required def user_detail(request, username): user = get_object_or_404(User, username=username, is_active=True) return render(request, 'account/user/detail.html', {'section': 'people', 'user': user})
These are simple list and detail views for User
objects. The user_list
view gets all active users. The Django User
model contains a flag is_active
to designate whether the user account is considered active. We filter the query by is_active=True
to return only active users. This view returns all results, but you can improve it by adding pagination the same way we did for the image_list
view.
The user_detail
view uses the get_object_or_404()
shortcut to retrieve the active user with the given username. The view returns an HTTP 404 response if no active user with the given username is found.
Edit the urls.py
file of the account
application, and add an URL pattern for each view as follows:
urlpatterns = [ # ... url(r'^users/$', views.user_list, name='user_list'), url(r'^users/(?P<username>[-w]+)/$', views.user_detail, name='user_detail'), ]
We are going to use the user_detail
URL pattern to generate the canonical URL for users. You have already defined a get_absolute_url()
method in a model to return the canonical URL for each object. Another way to specify an URL for a model is by adding the ABSOLUTE_URL_OVERRIDES
setting to your project.
Edit the settings.py
file of your project and add the following code to it:
ABSOLUTE_URL_OVERRIDES = { 'auth.user': lambda u: reverse_lazy('user_detail', args=[u.username]) }
Django adds a get_absolute_url()
method dynamically to any models that appear in the ABSOLUTE_URL_OVERRIDES
setting. This method returns the corresponding URL for the given model specified in the setting. We return the user_detail
URL for the given user. Now you can use get_absolute_url()
on a User
instance to retrieve its corresponding URL. Open the Python shell with the command python manage.py shell
and run the following code to test it:
>>> from django.contrib.auth.models import User >>> user = User.objects.latest('id') >>> str(user.get_absolute_url()) '/account/users/ellington/'
The returned URL is as expected. We need to create templates for the views we just built. Add the following directory and files to the templates/account/
directory of the account
application:
/user/
detail.html
list.html
Edit the account/user/list.html
template and add the following code to it:
{% extends "base.html" %} {% load thumbnail %} {% block title %}People{% endblock %} {% block content %} <h1>People</h1> <div id="people-list"> {% for user in users %} <div class="user"> <a href="{{ user.get_absolute_url }}"> {% thumbnail user.profile.photo "180x180" crop="100%" as im %} <img src="{{ im.url }}"> {% endthumbnail %} </a> <div class="info"> <a href="{{ user.get_absolute_url }}" class="title"> {{ user.get_full_name }} </a> </div> </div> {% endfor %} </div> {% endblock %}
This template allows us to list all the active users in the site. We iterate over the given users and use sorl-thumbnail's {% thumbnail %}
template tag to generate profile image thumbnails.
Open the base.html
template of your project and include the user_list
URL in the href
attribute of the following menu item:
<li {% if section == "people" %}class="selected"{% endif %}><a href="{% url "user_list" %}">People</a></li>
Start the development server with the command python manage.py runserver
and open http://127.0.0.1:8000/account/users/
in your browser. You should see a list of users like the following one:
Edit account/user/detail.html
template of the account
application and add the following code to it:
{% extends "base.html" %} {% load thumbnail %} {% block title %}{{ user.get_full_name }}{% endblock %} {% block content %} <h1>{{ user.get_full_name }}</h1> <div class="profile-info"> {% thumbnail user.profile.photo "180x180" crop="100%" as im %} <img src="{{ im.url }}" class="user-detail"> {% endthumbnail %} </div> {% with total_followers=user.followers.count %} <span class="count"> <span class="total">{{ total_followers }}</span> follower{{ total_followers|pluralize }} </span> <a href="#" data-id="{{ user.id }}" data-action="{% if request.user in user.followers.all %}un{% endif %}follow" class="follow button"> {% if request.user not in user.followers.all %} Follow {% else %} Unfollow {% endif %} </a> <div id="image-list" class="image-container"> {% include "images/image/list_ajax.html" with images=user.images_created.all %} </div> {% endwith %} {% endblock %}
In the detail template we display the user profile and we use the {% thumbnail %}
template tag to display the profile image. We show the total number of followers and a link to follow
/unfollow
the user. We prevent users from following themselves by hiding this link if the user is watching their own profile. We are going to perform an AJAX request to follow/unfollow a particular user. We add data-id
and data-action
attributes to the <a>
HTML element including the user ID and the initial action to perform when it's clicked, follow or unfollow, that depends on the user requesting the page being or not a follower of this user. We display the images bookmarked by the user with the list_ajax.html
template.
Open your browser again and click on a user that has bookmarked some images. You will see a profile detail like the following one:
We will create a simple view to follow/unfollow a user using AJAX. Edit the views.py
file of the account
application and add the following code to it:
from django.http import JsonResponse from django.views.decorators.http import require_POST from common.decorators import ajax_required from .models import Contact @ajax_required @require_POST @login_required def user_follow(request): user_id = request.POST.get('id') action = request.POST.get('action') if user_id and action: try: user = User.objects.get(id=user_id) if action == 'follow': Contact.objects.get_or_create( user_from=request.user, user_to=user) else: Contact.objects.filter(user_from=request.user, user_to=user).delete() return JsonResponse({'status':'ok'}) except User.DoesNotExist: return JsonResponse({'status':'ko'}) return JsonResponse({'status':'ko'})
The user_follow
view is quite similar to the image_like
view we created before. Since we are using a custom intermediary model for the users' many-to-many relationship, the default add()
and remove()
methods of the automatic manager of ManyToManyField
are not available. We use the intermediary Contact
model to create or delete user relationships.
Import the view you just created in the urls.py
file of the account
application and add the following URL pattern to it:
url(r'^users/follow/$', views.user_follow, name='user_follow'),
Make sure that you place this pattern before the user_detail
URL pattern. Otherwise, any requests to /users/follow/
will match the regular expression of the user_detail
pattern and it will be executed instead. Remember that in every HTTP request Django checks the requested URL against each pattern in order of appearance and stops at the first match.
Edit the user/detail.html
template of the account application
and append the following code to it:
{% block domready %} $('a.follow').click(function(e){ e.preventDefault(); $.post('{% url "user_follow" %}', { id: $(this).data('id'), action: $(this).data('action') }, function(data){ if (data['status'] == 'ok') { var previous_action = $('a.follow').data('action'); // toggle data-action $('a.follow').data('action', previous_action == 'follow' ? 'unfollow' : 'follow'); // toggle link text $('a.follow').text( previous_action == 'follow' ? 'Unfollow' : 'Follow'); // update total followers var previous_followers = parseInt( $('span.count .total').text()); $('span.count .total').text(previous_action == 'follow' ? previous_followers + 1 : previous_followers - 1); } } ); }); {% endblock %}
This is the JavaScript code to perform the AJAX request to follow or unfollow a particular user and also toggle the follow/unfollow link. We use jQuery to perform the AJAX request and set both the data-action
attribute and the text of the HTML <a>
element based on its previous value. When the AJAX action is performed, we also update the count of total followers displayed on the page. Open the user detail page of an existing user and click the Follow link to try the functionality we just built.