Chapter 6. Tracking User Actions

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:

  • Creating many-to-many relationships with an intermediary model
  • Building AJAX views
  • Creating an activity stream application
  • Adding generic relations to models
  • Optimizing QuerySets for related objects
  • Using signals for denormalizing counts
  • Storing item views in Redis

Building a follower system

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.

Creating many-to-many relationships with an intermediary model

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:

  • We are using the user model provided by Django and we want to avoid altering it
  • We want to store the time when the relation is created

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 relationship
  • user_to: A ForeignKey for the user being followed
  • created: A DateTimeField field with auto_now_add=True to store the time when the relationship was created

A 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.

Note

When you need additional fields in a many-to-many relationship, create a custom model with a ForeignKey for each side of the relationship. Add a ManyToManyField in one of the related models and indicate Django to use your intermediary model by including it in the through parameter.

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:

  • We simplify the way we retrieve related objects using the Django ORM with 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.
  • The table for this many-to-many relationship will be created using the Contact model. Thus, the ManyToManyField added dynamically will not imply any database changes for the Django User model.
  • We avoid creating a custom user model, keeping all the advantages of Django's built-in 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.

Note

When you use an intermediate model for many-to-many relationships some of the related manager's methods are disabled, such as add(), create() or remove(). You need to create or delete instances of the intermediate model instead.

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.

Creating list and detail views for user profiles

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:

Creating list and detail views for user profiles

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:

Creating list and detail views for user profiles

Building an AJAX view to follow users

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset