《Django By Example》第六章 中文 翻译 (我的学习,渣翻)

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

(译者注:无他,祝你们年会都中奖!)html

第六章

跟踪用户动做

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

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

本章将会覆盖如下几点:linux

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

建立一个粉丝系统

咱们将要在咱们的项目中建立一个粉丝系统。咱们的用户在平台中可以彼此关注而且跟踪其余用户的分享。这个关系在用户中的是多对多的关系,一个用户可以关注多个用户而且能被多个用户关注。web

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

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

咱们会建立一个中介模型(intermediate model)用来在用户之间构建关系。有两个缘由能够解释为何咱们要用一个中介模型(intermediate model):redis

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

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

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提供了一个简单的方式一次性取回有关联的对象,避免对数据库进行额外的查询。

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()将会极大的提升执行时间

如你所见,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🆔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执行异步任务。

译者总结:

这一章好长啊!最后部分的Redis感受最实用。准备全书翻译好后再抽时间把翻译好的全部章节所有从新校对下!那么你们下章再见!祈祷我年终中大奖!

相关文章
相关标签/搜索