Django By Example 第六章

书籍出处:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé

第六章

跟踪用户动作

在上一章中,你在你的项目中实现了AJAX视图(views),通过使用jQuery并创建了一个JavaScript书签在你的平台中分享别的网站的内容。

在本章中,你会学习如何创建一个粉丝系统以及创建一个用户活动流(activity stream)。你会学习到Django信号(signals)的工作方式以及在你的项目中集成Redis快速 I/O 仓库用来存储视图(views)项。

本章将会覆盖以下几点:

  • 通过一个中介模型(intermediate model)创建多对对的关系
  • 创建 AJAX 视图(views)
  • 创建一个活动流(activity stream)应用
  • 给模型(modes)添加通用关系
  • 取回对象的最优查询集(QuerySets)
  • 使用信号(signals)给非规范化的计数
  • 存储视图(views)项到 Redis 中

创建一个粉丝系统

我们将要在我们的项目中创建一个粉丝系统。我们的用户在平台中能够彼此关注并且跟踪其他用户的分享。这个关系在用户中的是多对多的关系,一个用户能够关注多个用户并且能被多个用户关注。

通过一个中介模型(intermediate model)(intermediary model)创建多对对的关系

在上一章中,你创建了多对对关系通过在其中一个有关联的模型(model)上添加了一个ManyToManyField然后让Django为这个关系创建了数据库表。这种方式支持大部分的场景,但是有时候你需要为这种关系创建一个中介模型(intermediate model)。创建一个中介模型(intermediate model)是非常有必要的当你想要为当前关系存储额外的信息,例如当前关系创建的时间点或者一个描述当前关系类型的字段。

我们会创建一个中介模型(intermediate model)用来在用户之间构建关系。有两个原因可以解释为什么我们要用一个中介模型(intermediate model):

  • 我们使用Django提供的user模型(model)并且我们想要避免修改它。
  • 我们想要存储关系建立的时间

编辑你的account应用中的models.py文件添加如下代码:

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)

这个Contact模型我们将会给用户关系使用。它包含以下字段:

  • user_form:一个ForeignKey指向创建关系的用户
  • user_to:一个ForeignKey指向被关注的用户
  • created:一个auto_now_add=TrueDateTimeField字段用来存储关系创建时的时间

ForeignKey字段上会自动生成一个数据库索引。我们使用db_index=True来创建一个数据库索引给created字段。这会提升查询执行的效率当通过这个字段对查询集(QuerySets)进行排序的时候。

使用 ORM ,我们可以创建一个关系给一个用户 user1 关注另一个用户 user2,如下所示:

user1 = User.objects.get(id=1)
user2 = User.objects.get(id=2)
Contact.objects.create(user_from=user1, user_to=user2)

关系管理器 rel_form_setrel_to_set 会返回一个查询集(QuerySets)给Contace模型(model)。为了
User模型(model)中存取最终的关系侧,Contace模型(model)会期望User包含一个ManyToManyField,如下所示(译者注:以下代码是作者假设的,实际上User不会包含以下代码):

following = models.ManyToManyField('self',
                                   through=Contact,
                                   related_name='followers',
                                   symmetrical=False)

在这个例子中,我们告诉Django去使用我们定制的中介模型(intermediate model)来创建关系通过给ManyToManyField添加through=Contact。这是一个从User模型到本身的多对对关系:我们在ManyToMnyfIELD字段中引用 'self'来创建一个关系给相同的模型(model)。

当你在多对多关系中需要额外的字段,创建一个定制的模型(model),一个关系侧就是一个ForeignKey。添加一个 ManyToManyField 在其中一个有关联的模型(models)中然后通过在through参数中包含该中介模型(intermediate model)指示Django去使用你的定制中介模型(intermediate model)。

如果User模型(model)是我们应用的一部分,我们可以添加以上的字段给模型(model)(译者注:所以说,上面的代码是作者假设存在)。但实际上,我们无法直接修改User类,因为它是属于django.contrib.auth应用的。我们将要做些轻微的改动,给User模型动态的添加这个字段。编辑account应用中的model.py文件,添加如下代码:

# Add following field to User dynamically
User.add_to_class('following',
                   models.ManyToManyField('self',
                                          through=Contact,
                                          related_name='followers',
                                          symmetrical=False))

在以上代码中,我们使用Django模型(models)的add_to_class()方法给User模型(model)添加monkey-patch(译者注:猴子补丁 Monkey patch 就是在运行时对已有的代码进行修改,而不需要修改原始代码)。你需要意识到,我们不推荐使用add_to_class()为模型(models)添加字段。我们在这个场景中利用这种方法是因为以下的原因:

  • 我们可以非常简单的取回关系对象使用Django ORM的user.followers.all()以及user.following.all()。我们使用中介(intermediary) Contact 模型(model)可以避免复杂的查询例如使用到额外的数据库操作joins,如果在我们的定制Profile模型(model)中定义过了关系。
  • 这个多对多关系的表将会被创建通过使用Contact模型(model)。因此,动态的添加ManyToManyField将不会对Django User 模型(model)的数据库进行任意改变。
  • 我们避免了创建一个定义的用户模型(model),保持了所有Django内置User的特性。

请记住,在大部分的场景中,在我们之前创建的Profile模型(model)添加字段是更好的方法,可以替代在User模型(model)上打上monkey-patch。Django还允许你使用定制的用户模型(models)。如果你想要使用你的定制用户模型(model),可以访问 https://docs.djangoproject.com/en/1.8/topics/auth/customizing/#specifying-a-custom-user-model 获得更多信息。

你能看到上述代码中的关系包含了symmetrical=Flase来定义一个非对称(non-symmetric)关系。这表示如果我关注了你,你不会自动的关注我。

当你使用了一个中介模型(intermediate model)给多对多关系,一些关系管理器的方法将不可用,例如:add()create()以及remove()。你需要创建或删除中介模型(intermediate model)的实例来代替。

运行如下命令来生成account应用的初始迁移:

python manage.py makemigrations account

你会看到如下输出:

Migrations for 'account':
     0002_contact.py:
       - Create model Contact

现在继续运行以下命令来同步应用到数据库中:

python manage.py migrate account

你会看到如下内容包含在输出中:

Applying account.0002_contact... OK

Contact模型(model)现在已经被同步进了数据库,我们可以在用户之间创建关系。但是,我们的网站还没有提供一个方法来浏览用户或查看详细的用户profile。让我们为User模型构建列表和详情视图(views)。

为用户profiles创建列表和详情视图(views)

打开account应用中的views.py文件添加如下代码:

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})

以上是User对象的简单列表和详情视图(views)。user_list视图(view)获得了所有的可用用户。Django User 模型(model)包含了一个标志(flag)is_active来指示用户账户是否可用。我们通过is_active=True来过滤查询只返回可用的用户。这个视图(vies)返回了所有结果,但是你可以改善它通过添加页码,这个方法我们在image_list视图(view)中使用过。

user_detail视图(view)使用get_object_or_404()快捷方法来返回所有可用的用户通过传入的用户名。当使用传入的用户名无法找到可用的用户这个视图(view)会返回一个HTTP 404响应。

编辑account应用的urls.py文件,为以上两个视图(views)添加URL模式,如下所示:

urlpatterns = [
       # ...
       url(r'^users/$', views.user_list, name='user_list'),
       url(r'^users/(?P<username>[-\w]+)/$',
           views.user_detail,
           name='user_detail'),
]

我们会使用 user_detail URL模式来给用户生成规范的URL。你之前就在模型(model)中定义了一个get_absolute_url()方法来为每个对象返回规范的URL。另外一种方式为一个模型(model)指定一个URL是为你的项目添加ABSOLUTE_URL_OVERRIDES设置。

编辑项目中的setting.py文件,添加如下代码:

ABSOLUTE_URL_OVERRIDES = {
    'auth.user': lambda u: reverse_lazy('user_detail',
                                        args=[u.username])
}

Django会为所有出现在ABSOLUTE_URL_OVERRIDES设置中的模型(models)动态添加一个get_absolute_url()方法。这个方法会给设置中指定的模型返回规范的URL。我们给传入的用户返回user_detail URL。现在你可以在一个User实例上使用get_absolute_url()来取回他自身的规范URL。打开Python shell输入命令python manage.py shell运行以下代码来进行测试:

>>> from django.contrib.auth.models import User
>>> user = User.objects.latest('id')
>>> str(user.get_absolute_url())
'/account/users/ellington/'

返回的URL如同期望的一样。我们需要为我们刚才创建的视图(views)创建模板(templates)。在account应用下的*templates/account/目录下添加以下目录和文件:

/user/
    detail.html
    list.html

编辑account/user/list.html模板(template)给它添加如下代码:

{% 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 %}
               ![]({{ 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 %}

这个模板(template)允许我们在网站中排列所有可用的用户。我们对给予的用户进行迭代并且使用`{% thumbnail %}模板(template)标签(tag)来生成profile图片缩微图。

打开项目中的base.html模板(template),在以下菜单项的href属性中包含user_listURL:

<li {% if section == "people" %}class="selected"{% endif %}>
    <a href="{% url "user_list" %}">People</a>
</li>

通过命令python manage.py runserver启动开发服务器然后在浏览器打开 http://127.0.0.1:8000/account/users/ 。你会看到如下所示的用户列:


django-6-1

(译者注:图灵,特斯拉,爱因斯坦,都是大牛啊)

编辑account应用下的account/user/detail.html模板,添加如下代码:

{% 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 %}
        ![]({{ im.url }})
    {% 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="followbutton">
        {% if request.user not in user.followers.all %}
            Follow
        {% else %}
            Unfollow
        {% endif %}
    </a>
    <div id="image-list" class="imget-container">
        {% include "images/image/list_ajax.html" with images = user.images_create.all %}
    </div>
    {% endwith %}
{% endblock %}

在详情模板(template)中我们展示用户profile并且我们使用{% thumbnail %}模板(template)标签(tag)来显示profile图片。我们显示粉丝的总数以及一个链接可以 follow/unfollow 该用户。我们会隐藏关注链接当用户在查看他们自己的profile,防止用户自己关注自己。我们会执行一个AJAX请求来 follow/unfollow 一个指定用户。我们给 <a> HTML元素添加data-iddata-action属性包含用户ID以及当该链接被点击的时候会执行的初始操作,follow/unfollow ,这个操作依赖当前页面的展示的用户是否已经被正在浏览的用户所关注。我们展示当前页面用户的图片书签通过list_ajax.html模板。

再次打开你的浏览器,点击一个拥有图片书签的用户链接,你会看到一个profile详情如下所示:


django-6-2

创建一个AJAX视图(view)来关注用户

我们将会创建一个简单的视图(view)使用AJAX来 follow/unfollow 用户。编辑account应用中的views.py文件添加如下代码:

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'})

user_follow视图(view)有点类似与我们之前创建的image_like视图(view)。因为我们使用了一个定制中介模型(intermediate model)给用户的多对多关系,所以ManyToManyField管理器默认的add()remove()方法将不可用。我们使用中介Contact模型(model)来创建或删除用户关系。

account应用中的urls.py文件中导入你刚才创建的视图(view)然后为它添加URL模式:

    url(r'^users/follow/$', views.user_follow, name='user_follow'),

请确保你放置的这个URL模式的位置在user_detailURL模式之前。否则,任何对 /users/follow/ 的请求都会被user_detail模式给正则匹配然后执行。请记住,每一次的HTTP请求Django都会对每一条存在的URL模式进行匹配直到第一条匹配成功才会停止继续匹配。

编辑account应用下的user/detail.html模板添加如下代码:

{% 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 %}

这段JavaScript代码执行AJAX请求来关注或不关注一个指定用户并且触发 follow/unfollow 链接。我们使用jQuery去执行AJAX请求的同时会设置 follow/unfollow 两种链接的data-aciton属性以及HTML<a>元素的文本基于它上一次的值。当AJAX操作执行完成,我们还会对显示在页面中的粉丝总数进行更新。打开一个存在的用户的详情页面,然后点击Follow链接尝试下我们刚才构建的功能是否正常。

创建一个通用的活动流(activity stream)应用

许多社交网站会给他们的用户显示一个活动流(activity stream),这样他们可以跟踪其他用户在平台中的操作。一个活动流(activity stream)是一个用户或一个用户组最近活动的列表。举个例子,FacebookNews Feed就是一个活动流(activity stream)。用户X给Y图片打上了书签或者用户X关注了用户Y也是例子操作。我们将会构建一个活动流(activity stream)应用这样每个用户都能看到他关注的用户最近进行的交互。为了做到上述功能,我们需要一个模型(modes)来保存用户在网站上的操作执行,还需要一个简单的方法来添加操作给feed。

运行以下命令在你的项目中创建一个新的应用命名为actions

django-admin startapp actions

在你的项目中的settings.py文件中的INSTALLED_APPS设置中添加'actions',这样可以让Django知道这个新的应用是可用状态:

INSTALLED_APPS = (
    # ...
    'actions',    
)

编辑actions应用下的models.py文件添加如下代码:

from django.db import models
from django.contrib.auth.models import User

class Action(models.Model):
    user = models.ForeignKey(User,
                            related_name='actions',
                            db_index=True)
    verb = models.CharField(max_length=255)
    created = models.DateTimeField(auto_now_add=True,
                                    db_index=True)

    class Meta:
        ordering = ('-created',)

这个Action模型(model)将会用来记录用户的活动。模型(model)中的字段解释如下:

  • user:执行该操作的用户。这个一个指向Django User模型(model)的 ForeignKey
  • verb:这是用户执行操作的动作描述。
  • created:这个时间日期会在动作执行的时候创建。我们使用auto_now_add=True来动态设置它为当前的时间当这个对象第一次被保存在数据库中。

通过这个基础模型(model),我们只能够存储操作例如用户X做了哪些事情。我们需要一个额外的ForeignKey字段为了保存操作会涉及到的一个target(目标)对象,例如用户X给图片Y打上了暑期那或者用户X现在关注了用户Y。就像你之前知道的,一个普通的ForeignKey只能指向一个其他的模型(model)。但是,我们需要一个方法,可以让操作的target(目标)对象是任何一个已经存在的模型(model)的实例。这个场景就由Django内容类型框架来上演。

使用内容类型框架

Django包含了一个内容类型框架位于django.contrib.contenttypes。这个应用可以跟踪你的项目中所有的模型(models)以及提供一个通用接口来与你的模型(models)进行交互。

当你使用startproject命令创建一个新的项目的时候这个contenttypes应用就被默认包含在INSTALLED_APPS设置中。它被其他的contrib包使用,例如认证(authentication)框架以及admin应用。

contenttypes应用包含一个ContentType模型(model)。这个模型(model)的实例代表了你的应用中真实存在的模型(models),并且新的ContentTYpe实例会动态的创建当新的模型(models)安装在你的项目中。ContentType模型(model)有以下字段:

  • app_label:模型(model)属于的应用名,它会自动从模型(model)Meta选项中的app_label属性获取到。举个例子:我们的Image模型(model)属于images应用
  • model:模型(model)类的名字
  • name:模型的可读名,它会自动从模型(model)Meta选项中的verbose_name获取到。

让我们看一下我们如何实例化ContentType对象。打开Python终端使用python manage.py shell命令。你可以获取一个指定模型(model)对应的ContentType对象通过执行一个带有app_labelmodel属性的查询,例如:

>>> from django.contrib.contenttypes.models import ContentType
>>> image_type = ContentType.objects.get(app_label='images',model='image')
>>> image_type
<ContentType: image>

你还能反过来获取到模型(model)类从一个ContentType对象中通过调用它的model_class()方法:

>>> from images.models import Image
>>> ContentType.objects.get_for_model(Image)
<ContentType: image>

以上就是内容类型的一些例子。Django提供了更多的方法来使用他们进行工作。你可以访问 https://docs.djangoproject.com/en/1.8/ref/contrib/contenttypes/ 找到关于内容类型框架的官方文档。

添加通用的关系给你的模型(models)

在通用关系中ContentType对象扮演指向模型(model)的角色被关联所使用。你需要3个字段在模型(model)中组织一个通用关系:

  • 一个ForeignKey字段ContentType。这个字段会告诉我们给这个关联的模型(model)。
  • 一个字段用来存储被关联对象的primary key。这个字段通常是一个PositiveIntegerField用来匹配Django自动的primary key字段。
  • 一个字段用来定义和管理通用关系通过使用前面的两个字段。内容类型框架提供一个GenericForeignKey字段来完成这个目标。

编辑actions应用的models.py文件,添加如下代码:

from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
class Action(models.Model):
    user = models.ForeignKey(User,
                             related_name='actions',
                             db_index=True)
    verb = models.CharField(max_length=255)
    target_ct = models.ForeignKey(ContentType,
                                  blank=True,
                                  null=True,
                                  related_name='target_obj')
    target_id = models.PositiveIntegerField(null=True,
                                            blank=True,
                                            db_index=True)
    target = GenericForeignKey('target_ct', 'target_id')
    created = models.DateTimeField(auto_now_add=True,
                                   db_index=True)
    class Meta:
        ordering = ('-created',)

我们给Action模型添加了以下字段:

  • target_ct:一个ForeignKey字段指向ContentType模型(model)。
  • target_id:一个PositiveIntegerField用来存储被关联对象的primary key。
  • target:一个GenericForeignKey字段指向被关联的对象基于前面两个字段的组合之上。

Django没有创建任何字段在数据库中给GenericForeignKey字段。只有target_cttarget_id两个字段被映射到数据库字段。两个字段都有blank=Truenull=True属性所以一个target(目标)对象不是必须的当保存Action对象的时候。

你可以让你的应用更加灵活通过使用通用关系替代外键当它对拥有一个通用关系有意义。

运行以下命令来创建初始迁移为这个应用:

python manage.py makemigrations actions

你会看到如下输出:

    Migrations for 'actions':
        0001_initial.py:
            - Create model Action

接着,运行下一条命令来同步应用到数据库中:

python manage.py migrate

这条命令的输出表明新的迁移已经被应用:

Applying actions.0001_initial... OK

让我们在管理站点中添加Action模型(model)。编辑actions应用的admin.py文件,添加如下代码:

from django.contrib import admin
from .models import Action

class ActionAdmin(admin.ModelAdmin):
    list_display = ('user', 'verb', 'target', 'created')
    list_filter = ('created',)
    search_fields = ('verb',)

admin.site.register(Action, ActionAdmin)

你已经将Action模型(model)注册到了管理站点中。运行命令python manage.py runserver来初始化开发服务器然后在浏览器中打开 http://127.0.0.1:8000/admin/actions/action/add/ 。你会看到如下页面可以创建一个新的Action对象:


django-6-3

如你所见,只有target_cttarget_id两个字段是映射为真实的数据库字段显示,并且GenericForeignKey字段不在这儿出现。target_ct允许你选择任何一个在你的Django项目中注册的模型(models)。你可以限制内容类型从一个限制的模型(models)集合中选择通过在target-ct字段中使用limit_choices_to属性:limit_choices_to属性允许你限制ForeignKey字段的内容通过给予一个特定值的集合。

actions应用目录下创建一个新的文件命名为utils.py。我们会定义一个快捷函数,该函数允许我们使用一种简单的方式创建新的Action对象。编辑这个新的文件添加如下代码给它:

from django.contrib.contenttypes.models import ContentType
from .models import Action
def create_action(user, verb, target=None):
    action = Action(user=user, verb=verb, target=target)
    action.save()

create_action()函数允许我们创建actions,该actions可以包含一个target对象或不包含。我们可以使用这个函数在我们代码的任何地方添加新的actions给活动流(activity stream)。

在活动流(activity stream)中避免重复的操作

有时候你的用户可能多次执行同个动作。他们可能在短时间内多次点击 like/unlike 按钮或者多次执行同样的动作。这会导致你停止存储和显示重复的动作。为了避免这种情况我们需要改善create_action()函数来避免大部分的重复动作。

编辑actions应用中的utils.py文件使它看上去如下所示:

import datetime
from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
from .models import Action

def create_action(user, verb, target=None):
    # check for any similar action made in the last minute
    now = timezone.now()
    last_minute = now - datetime.timedelta(seconds=60)
    similar_actions = Action.objects.filter(user_id=user.id,
                                            verb= verb,
                                        timestamp__gte=last_minute)
    if target:
        target_ct = ContentType.objects.get_for_model(target)
        similar_actions = similar_actions.filter(
                                            target_ct=target_ct,
                                            target_id=target.id)
    if not similar_actions:
        # no existing actions found
        action = Action(user=user, verb=verb, target=target)
        action.save()
        return True
    return False

我们通过修改create_action()函数来避免保存重复的动作并且返回一个布尔值来告诉该动作是否保存。下面来解释我们是如何避免重复动作的:

  • 首先,我们通过Django提供的timezone.now()方法来获取当前时间。这个方法同datetime.datetime.now()相同,但是返回的是一个*timezone-aware*对象。Django提供一个设置叫做*USE_TZ*用来启用或关闭时区的支持。通过使用*startproject*命令创建的默认*settings.py*包含USE_TZ=True`。
  • 我们使用last_minute变量来保存一分钟前的时间,然后我们取回用户从那以后执行的任意一个相同操作。
  • 我们会创建一个Action对象如果在最后的一分钟内没有存在同样的动作。我们会返回True如果一个Action对象被创建,否则返回False

添加用户动作给活动流(activity stream)

是时候添加一些动作给我们的视图(views)来给我的用户构建活动流(activity stream)了。我们将要存储一个动作为以下的每一个实例:

  • 一个用户给某张图片打上书签
  • 一个用户喜欢或不喜欢某张图片
  • 一个用户创建一个账户
  • 一个用户关注或不关注某个用户

编辑images应用下的views.py文件添加以下导入:

from actions.utils import create_action

image_create视图(view)中,在保存图片之后添加create-action(),如下所示:

new_item.save()
create_action(request.user, 'bookmarked image', new_item)

image_like视图(view)中,在添加用户给users_like关系之后添加create_action(),如下所示:

image.users_like.add(request.user)
create_action(request.user, 'likes', image)

现在编辑account应用中的view.py文件添加以下导入:

from actions.utils import create_action

register视图(view)中,在创建Profile对象之后添加create-action(),如下所示:

new_user.save()
profile = Profile.objects.create(user=new_user)
create_action(new_user, 'has created an account')

user_follow视图(view)中添加create_action(),如下所示:

Contact.objects.get_or_create(user_from=request.user,user_to=user)
create_action(request.user, 'is following', user)

就像你所看到的,感谢我们的Action模型(model)和我们的帮助函数,现在保存新的动作给活动流(activity stream)是非常简单的。

显示活动流(activity stream)

最后,我们需要一种方法来给每个用户显示活动流(activity stream)。我们将会在用户的dashboard中包含活动流(activity stream)。编辑account应用的views.py文件。导入Action模型然后修改dashboard视图(view)如下所示:

from actions.models import Action

@login_required
def dashboard(request):
    # Display all actions by default
    actions = Action.objects.exclude(user=request.user)
    following_ids = request.user.following.values_list('id',flat=True)
    if following_ids:
        # If user is following others, retrieve only their actions
        actions = actions.filter(user_id__in=following_ids)
    actions = actions[:10]

    return render(request,
                  'account/dashboard.html',
                  {'section': 'dashboard',
                    'actions': actions})

在这个视图(view),我们从数据库取回所有的动作(actions),不包含当前用户执行的动作。如果当前用户还没有关注过任何人,我们展示在平台中的其他用户的最新动作执行。这是一个默认的行为当当前用户还没有关注过任何其他的用户。如果当前用户已经关注了其他用户,我们就限制查询只显示当前用户关注的用户的动作执行。最后,我们限制结果只返回最前面的10个动作。我们在这儿并不使用order_by(),因为我们依赖之前已经在Action模型(model)的Meta的排序选项。最新的动作会首先返回,因为我们在Action模型(model)中设置过ordering = ('-created',)

优化涉及被关联的对想的查询集(QuerySets)

每次你取回一个Aciton对象,你都可能存取它的有关联的User对象,
并且可能这个用户也关联它的Profile对象。Django ORM提供了一个简单的方式一次性取回有关联的对象,避免对数据库进行额外的查询。

使用select_related

Django提供了一个叫做select_related()的查询集(QuerySets)方法允许你取回关系为一对多的关联对象。该方法将会转化成一个单独的,更加复杂的查询集(QuerySets),但是你可以避免额外的查询当存取这些关联对象。select_relate方法是给ForeignKeyOneToOne字段使用的。它通过执行一个 SQL JOIN并且包含关联对象的字段在SELECT 声明中。

为了利用select_related(),编辑之前代码中的以下行(译者注:请注意双下划线):

actions = actions.filter(user_id__in=following_ids)

添加select_related在你将要使用的字段上:

actions = actions.filter(user_id__in=following_ids)\
                    .select_related('user', 'user__profile')

我们使用user__profile(译者注:请注意是双下划线)来连接profile表在一个单独的SQL查询中。如果你调用select_related()而不传入任何参数,它会取回所有ForeignKey关系的对象。给select_related()限制的关系将会在随后一直访问。

小心的使用select_related()将会极大的提高执行时间

使用prefetch_related

如你所见,select_related()将会帮助你提高取回一对多关系的关联对象的执行效率。但是,select_related()无法给多对多或者多对一关系(ManyToMany或者倒转ForeignKey字段)工作。Django提供了一个不同的查询集(QuerySets)方法叫做prefetch_realted,该方法在select_related()方法支持的关系上增加了多对多和多对一的关系。prefetch_related()方法为每一种关系执行单独的查找然后对各个结果进行连接通过使用Python。这个方法还支持GeneriRelationGenericForeignKey的预先读取。

完成你的查询通过为它添加prefetch_related()给目标GenericForeignKey字段,如下所示:

actions = actions.filter(user_id__in=following_ids)\
                 .select_related('user', 'user__profile')\
                 .prefetch_related('target')

这个查询现在已经被充分利用用来取回包含关联对象的用户动作(actions)。

actions创建模板(templates)

我们要创建一个模板(template)用来显示一个独特的Action对象。在actions应用中创建一个新的目录命名为templates。添加如下文件结构:

actions/
    action/
        detail.html

编辑actions/action/detail.html模板(template)文件添加如下代码:

明天添加

这个模板用来显示一个Action对象。首先,我们使用{% with %}模板标签(template tag)来获取用户操作的动作(action)和他们的profile。然后,我们显示目标对象的图片如果Action对象有一个关联的目标对象。最后,如果有执行过的动作(action),包括动作和目标对象,我们就显示链接给用户。

现在,编辑account/dashboard.html模板(template)添加如下代码到content区块下方:

<h2>What's happening</h2>
<div id="action-list">
    {% for action in actions %}
        {% include "actions/action/detail.html" %}
    {% endfor %}
</div>

在浏览器中打开 http://127.0.0.1:8000/account/ 。登录一个存在的用户并且该用户执行过一些操作已经被存储在数据库中。然后,登录其他用户,关注之前登录的用户,在dashboard页面可以看到生成的动作流。如下所示:


django-6-4

我们刚刚创建了一个完整的活动流(activity stream)给我们的用户并且我们还能非常容易的添加新的用户动作给它。你还可以添加无限的滚动功能给活动流(activity stream)通过集成AJAX分页处理,和我们之前在image_list视图(view)使用过的一样。

给非规范化(denormalizing)计数使用信号

有一些场景,你想要使你的数据非规范化。非规划化使指在一定的程度上制造一些数据冗余用来优化读取的性能。你必须十分小心的使用非规划化并且只有在你真的非常需要它的时候才能使用。你会发现非规划化的最大问题就是保持你的非规范化数据更新是非常困难的。

我们将会看到一个例子关于如何改善(improve)我们的查询通过使用非规范化计数。缺点就是我们不得不保持冗余数据的更新。我们将要从我们的Image模型(model)中使数据非规范化然后使用Django信号来保持数据的更新。

使用信号进行工作

Django自带一个信号调度程序允许receiver函数在某个动作出现的时候去获取通知。信号非常有用,当你需要你的代码去执行某些事件的时候同时正在发生其他事件。你还能够创建你自己的信号这样一来其他人可以在某个事件发生的时候获得通知。

Django模型(models)提供了几个信号,它们位于django.db.models.signales。举几个例子:

  • pre_savepost_save:前者会在调用模型(model)的save()方法前发送信号,后者反之。
  • pre_deletepost_delete:前者会在调用模型(model)或查询集(QuerySets)的delete()方法之前发送信号,后者反之。
  • m2m_changed:当在一个模型(model)上的ManayToManayField被改变的时候发送信号。

以上只是Django提供的一小部分信号。你可以通过访问 https://docs.djangoproject.com/en/1.8/ref/signals/ 获得更多信号资料。

打个比方,你想要获取热门图片。你可以使用Django的聚合函数来获取图片,通过图片获取的用户喜欢数量来进行排序。要记住你已经使用过Django聚合函数在第三章 扩展你的blog应用。以下代码将会获取图片并进行排序通过它们被用户喜欢的数量:

from django.db.models import Count
from images.models import Image
images_by_popularity = Image.objects.annotate(
    total_likes=Count('users_like')).order_by('-total_likes')

但是,通过统计图片的总喜欢数量进行排序比直接使用一个已经存储总统计数的字段进行排序要消耗更多的性能。你可以添加一个字段给Image模型(model)用来非规范化喜欢的数量用来提升涉及该字段的查询的性能。那么,问题来了,我们该如何保持这个字段是最新更新过的。

编辑images应用下的models.py文件,给Image模型(model)添加以下字段:

total_likes = models.PositiveIntegerField(db_index=True,
                                          default=0)

total_likes字段允许我们给每张图片存储被用户喜欢的总数。非规范化数据非常有用当你想要使用他们来过滤或排序查询集(QuerySets)。

在你使用非规范化字段之前你必须考虑下其他几种提高性能的方法。考虑下数据库索引,最佳化查询以及缓存在开始规范化你的数据之前。

运行以下命令将新添加的字段迁移到数据库中:

python manage.py makemigrations images

你会看到如下输出:

Migrations for 'images':
    0002_image_total_likes.py:
        - Add field total_likes to image

接着继续运行以下命令来应用迁移:

python manage.py migrate images

输出中会包含以下内容:

Applying images.0002_image_total_likes... OK

我们要给m2m_changed信号附加一个receiver函数。在images应用目录下创建一个新的文件命名为signals.py。给该文件添加如下代码:

from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from .models import Image
@receiver(m2m_changed, sender=Image.users_like.through)
def users_like_changed(sender, instance, **kwargs):
    instance.total_likes = instance.users_like.count()
    instance.save()

首先,我们使用receiver()装饰器将users_like_changed函数注册成一个receiver函数,然后我们将该函数附加给m2m_changed信号。我们将这个函数与Image.users_like.through连接,这样这个函数只有当m2m_changed信号被Image.users_like.through执行的时候才被调用。还有一个可以替代的方式来注册一个receiver函数,由使用Signal对象的connect()方法组成。

Django信号是同步阻塞的。不要使用异步任务导致信号混乱。但是,你可以联合两者来执行异步任务当你的代码只接受一个信号的通知。

你必须连接你的receiver函数给一个信号,只有这样它才会被调用当连接的信号发送的时候。有一个推荐的方法用来注册你的信号是在你的应用配置类中导入它们到ready()方法中。Django提供一个应用注册允许你对你的应用进行配置和内省。

典型的应用配置类

django允许你指定配置类给你的应用们。为了提供一个自定义的配置给你的应用,创建一个继承django.appsAppconfig类的自定义类。这个应用配置类允许你为应用存储元数据和配置并且提供
内省。

你可以通过访问 https://docs. djangoproject.com/en/1.8/ref/applications/ 获取更多关于应用配置的信息。

为了注册你的信号receiver函数,当你使用receiver()装饰器的时候,你只需要导入信号模块,这些信号模块被包含在你的应用的AppConfig类中的ready()方法中。这个方法在应用注册被完整填充的时候就调用。其他给你应用的初始化都可以被包含在这个方法中。

images应用目录下创建一个新的文件命名为apps.py。为该文件添加如下代码:

from django.apps import AppConfig
class ImagesConfig(AppConfig):
    name = 'images'
    verbose_name = 'Image bookmarks'
    def ready(self):
        # import signal handlers
        import images.signals

name属性定义该应用完整的Python路径。verbose_name属性设置了这个应用可读的名字。它会在管理站点中显示。ready()方法就是我们为这个应用导入信号的地方。

现在我们需要告诉Django我们的应用配置位于哪里。编辑位于images应用目录下的init.py文件添加如下内容:

default_app_config = 'images.apps.ImagesConfig'

打开你的浏览器浏览一个图片的详细页面然后点击like按钮。再进入管理页面看下该图片的total_like属性。你会看到total_likes属性已经更新了最新的like数如下所示:


django-6-5

现在,你可以使用totla_likes属性来进行热门图片的排序或者在任何地方显示这个值,从而避免了复杂的查询操作。以下获取图片的查询通过图片的喜欢数量进行排序:

images_by_popularity = Image.objects.annotate(
    likes=Count('users_like')).order_by('-likes')

现在我们可以用新的查询来代替上面的查询:

images_by_popularity = Image.objects.order_by('-total_likes')

以上查询的返回结果只需要很少的SQL查询性能。以上就是一个例子关于如何使用Django信号。

小心使用信号,因为它们会给理解控制流制造困难。在很多场景下你可以避免使用信号如果你知道哪个接收器需要被通知。

使用Redis来存储视图(views)项

Redis是一个高级的key-value(键值)数据库允许你保存不同类型的数据并且在I/O(输入/输出)操作上非常非常的快速。Redis可以在内存中存储任何东西,但是这些数据能够持续通过偶尔存储数据集到磁盘中或者添加每一条命令到日志中。Redis是非常出彩的通过与其他的键值存储对比:它提供了一个强大的设置命令,并且支持多种数据结构,例如string,hashes,lists,sets,ordered sets,甚至bitmaps和HyperLogLogs。

SQL最适合用于模式定义的持续数据存储,而Redis提供了许多优势当需要处理快速变化的数据,易失性存储,或者需要一个快速缓存的时候。让我们看下Redis是如何被使用的,当构建新的功能到我们的项目中。

安装Redis

http://redis.io/download 下载最新的Redis版本。解压tar.gz文件,进入redis目录然后编译Redis通过使用以下make命令:

cd redis-3.0.4(译者注:版本根据自己下载的修改)
make (译者注:这里是假设你使用的是linux或者mac系统才有make命令,windows如何操作请看下官方文档)

在Redis安装完成后允许以下shell命令来初始化Redis服务:

src/redis-server

你会看到输出的结尾如下所示:

# Server started, Redis version 3.0.4
* DB loaded from disk: 0.001 seconds
* The server is now ready to accept connections on port 6379

默认的,Redis运行会占用6379端口,但是你也可以指定一个自定义的端口通过使用--port标志,例如:redis-server --port 6655。当你的服务启动完毕,你可以在其他的终端中打开Redis客户端通过使用如下命令:

src/redis-cli

你会看到Redis客户端shell如下所示:

127.0.0.1:6379>

Redis客户端允许你在当前shell中立即执行Rdis命令。来我们来尝试一些命令。键入SET命令在Redis客户端中存储一个值到一个键中:

127.0.0.1:6379> SET name "Peter"
ok

以上的命令创建了一个带有字符串“Peter”值的name键到Redis数据库中。OK输出表明该键已经被成功保存。然后,使用GET命令获取之前的值,如下所示:

127.0.0.1:6379> GET name
"Peter"

你还可以检查一个键是否存在通过使用EXISTS命令。如果检查的键存在会返回1,反之返回0:

127.0.0.1:6379> EXISTS name
(integer) 1

你可以给一个键设置到期时间通过使用EXPIRE命令,该命令允许你设置该键能在几秒内存在。另一个选项使用EXPIREAT命令来期望一个Unix时间戳。键的到期消失是非常有用的当将Redis当做缓存使用或者存储易失性的数据:

127.0.0.1:6379> GET name
"Peter"
127.0.0.1:6379> EXPIRE name 2
(integer) 1
Wait for 2 seconds and try to get the same key again:
127.0.0.1:6379> GET name
(nil)

(nil)响应是一个空的响应说明没有找到键。你还可以通过使用DEL命令删除任意键,如下所示:

127.0.0.1:6379> SET total 1
OK
127.0.0.1:6379> DEL total
(integer) 1
127.0.0.1:6379> GET total
(nil)

以上只是一些键选项的基本命令。Redis包含了庞大的命令设置给一些数据类型,例如strings,hashes,sets,ordered sets等等。你可以通过访问 http://redis.io/commands 看到所有Reids命令以及通过访问 http://redis.io/topics/data-types 看到所有Redis支持的数据类型。

通过Python使用Redis

我们需要绑定Python和Redis。通过pip渠道安装redis-py命令如下:

pip install redis==2.10.3(译者注:版本可能有更新,如果需要最新版本,可以不带上'==2.10.3'后缀)

你可以访问 http://redis-py.readthedocs.org/ 得到redis-py文档。

redis-py提供两个类用来与Redis交互:StrictRedisRedis。两者提供了相同的功能。StrictRedis类尝试遵守官方的Redis命令语法。Redis类型继承Strictredis重写了部分方法来提供向后的兼容性。我们将会使用StrictRedis类,因为它遵守Redis命令语法。打开Python shell执行以下命令:

>>> import redis
>>> r = redis.StrictRedis(host='localhost', port=6379, db=0)

上面的代码创建了一个与Redis数据库的连接。在Redis中,数据库通过一个整形索引替代数据库名字来辨识。默认的,一个客户端被连接到数据库 0 。Reids数据库可用的数字设置到16,但是你可以在redis.conf文件中修改这个值。

现在使用Python shell设置一个键:

>>> r.set('foo', 'bar')
True

以上命令返回Ture表明这个键已经创建成功。现在你可以使用get()命令取回该键:

>>> r.get('foo')
'bar'

如你所见,StrictRedis方法遵守Redis命令语法。

让我们集成Rdies到我们的项目中。编辑bookmarks项目的settings.py文件添加如下设置:

REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 0

以上设置了Redis服务器和我们将要在项目中使用到的数据库。

存储视图(vies)项到Redis中

让我们存储一张图片被查看的总次数。如果我们通过Django ORM来完成这个操作,它会在每次该图片显示的时候执行一次SQL UPDATE声明。使用Redis,我们只需要对一个计数器进行增量存储在内存中,从而带来更好的性能。

编辑images应用下的views.py文件,添加如下代码:

import redis
from django.conf import settings
# connect to redis
r = redis.StrictRedis(host=settings.REDIS_HOST,
                      port=settings.REDIS_PORT,
                      db=settings.REDIS_DB)

在这儿我们建立了Redis的连接为了能在我们的视图(views)中使用它。编辑images_detail视图(view)使它看上去如下所示:

def image_detail(request, id, slug):
image = get_object_or_404(Image, id=id, slug=slug)
# increment total image views by 1
total_views = r.incr('image:{}:views'.format(image.id)) 
return render(request,
              'images/image/detail.html',
              {'section': 'images',
               'image': image,
               'total_views': total_views})

在这个视图(view)中,我们使用INCR命令,它会从1开始增量一个键的值,在执行这个操作之前如果键不存在,它会将值设定为0.incr()方法在执行操作后会返回键的值,然后我们可以存储该值到total_views变量中。我们构建Rddis键使用一个符号,比如 object-type:id:field (for example image:33:id)

对Redis的键进行命名有一个惯例是使用冒号进行分割来创建键的命名空间。做到这点,键的名字会特别冗长,有关联的键会分享部分相同的模式在它们的名字中。

编辑image/detail.html模板(template)在已有的<span class="count">元素之后添加如下代码:

<span class="count">
     <span class="total">{{ total_views }}</span>
     view{{ total_views|pluralize }}
</span>

现在在浏览器中打开一张图片的详细页面然后多次加载该页面。你会看到每次该视图(view)被执行的时候,总的观看次数会增加 1 。如下所示:


django-6-6

你已经成功的集成Redis到你的项目中来存储项统计。

存储一个排名到Reids中

让我们使用Reids构建更多的功能。我们要在我们的平台中创建一个最多浏览次数的图片排行。为了构建这个排行我们将要使用Redis分类集合。一个分类集合是一个非重复的字符串采集,其中每个成员和一个分数关联。其中的项根据它们的分数进行排序。

编辑images引用下的views.py文件,使image_detail视图(view)看上去如下所示:

def image_detail(request, id, slug):
image = get_object_or_404(Image, id=id, slug=slug)
# increment total image views by 1
total_views = r.incr('image:{}:views'.format(image.id)) # increment image ranking by 1 
r.zincrby('image_ranking', image.id, 1)
return render(request,
              'images/image/detail.html',
              {'section': 'images',
               'image': image,
               'total_views': total_views})

我们使用zincrby()命令存储图片视图(views)到一个分类集合中通过键image:ranking。我们存储图片id,和一个分数1,它们将会被加到分类集合中这个元素的总分上。这将允许我们在全局上持续跟踪所有的图片视图(views),并且有一个分类集合,该分类集合通过图片的浏览次数进行排序。

现在创建一个新的视图(view)用来展示最多浏览次数图片的排行。在views.py文件中添加如下代码:

@login_required
def image_ranking(request):
    # get image ranking dictionary
    image_ranking = r.zrange('image_ranking', 0, -1,
                             desc=True)[:10]
    image_ranking_ids = [int(id) for id in image_ranking]
    # get most viewed images
    most_viewed = list(Image.objects.filter(
                       id__in=image_ranking_ids))
    most_viewed.sort(key=lambda x: image_ranking_ids.index(x.id))
    return render(request,
                  'images/image/ranking.html',
                  {'section': 'images',
                   'most_viewed': most_viewed})

以上就是image_ranking视图。我们使用zrange()命令获得分类集合中的元素。这个命令期望一个自定义的范围,最低分和最高分。通过将 0 定为最低分, -1 为最高分,我们告诉Redis返回分类集合中的所有元素。最终,我们使用[:10]对结果进行切片获取最前面十个最高分的元素。我们构建一个返回的图片IDs的列,然后我们将该列存储在image_ranking_ids变量中,这是一个整数列。我们通过这些IDs取回对应的Image对象,并将它们强制转化为列通过使用list()函数。强制转化查询集(QuerySets)的执行是非常重要的,因为接下来我们要在该列上使用列的sort()方法(就是因为这点所以我们需要的是一个对象列而不是一个查询集(QuerySets))。我们排序这些Image对象通过它们在图片排行中的索引。现在我们可以在我们的模板(template)中使用most_viewed列来显示10个最多浏览次数的图片。

创建一个新的image/ranking.html模板(template)文件,添加如下代码:

{% extends "base.html" %}

{% block title %}Images ranking{% endblock %}

{% block content %}
    <h1>Images ranking</h1>
     <ol>
       {% for image in most_viewed %}
         <li>
           <a href="{{ image.get_absolute_url }}">
             {{ image.title }}
           </a> 
         </li>
       {% endfor %}
     </ol>
{% endblock %}

这个模板(template)非常简单明了,我们只是对包含在most_viewed中的Image对象进行迭代。

最后为新的视图(view)创建一个URL模式。编辑images应用下的urls.py文件,添加如下内容:

url(r'^ranking/$', views.image_ranking, name='create'),

在浏览器中打开 http://127.0.0.1:8000/images/ranking/ 。你会看到如下图片排行:


django-6-7

Redis的下一步

Redis并不能替代你的SQL数据库,但是它是一个内存中的快速存储,更适合某些特定任务。将它添加到你的栈中使用当你真的感觉它很需要。以下是一些适合Redis的场景:

  • Counting:如你之前看到的,通过Redis管理计数器非常容易。你可以使用incr()和`incrby()。
  • Storing latest items:你可以添加项到一个列的开头和结尾通过使用lpush()rpush()。移除和返回开头和结尾的元素通过使用lpop()以及rpop()。你可以削减列的长度通过使用ltrim()来维持它的长度。
  • Queues:除了push和pop命令,Redis还提供堵塞的队列命令。
  • Caching:使用expire()expireat()允许你将Redis当成缓存使用。你还可以找到第三方的Reids缓存后台给Django使用。
  • Pub/Sub:Redis提供命令给订阅或不订阅,并且给渠道发送消息。
  • Rankings and leaderboards:Redis使用分数的分类集合使创建排行榜非常的简单。
  • Real-time tracking:Redis快速的I/O(输入/输出)使它能完美支持实时场景。

总结

在本章中,你构建了一个粉丝系统和一个用户活动流(activity stream)。你学习了Django信号是如何进行工作并且在你的项目中集成了Redis。

在下一章中,你会学习到如何构建一个在线商店。你会创建一个产品目录并且通过会话(sessions)创建一个购物车。你还会学习如何通过Celery执行异步任务。