django by example 实践 educa 项目(二)


点我查看本文集的说明及目录。


本项目相关内容包括:

实现过程

CH10 创建一个在线学习平台

CH11 缓存内容

CH12 创建API


CH11 缓存内容


上一章,我们使用模型继承和通用关系创建了灵活的课程内容模型,还使用类视图、formsets 创建了课程管理系统并使用 AJAX 对内容进行排序。本章,我们将学习一下内容:

  • 创建公共视图来展示课程信息;
  • 创建学生注册系统;
  • 管理学生订阅课程;
  • 渲染不同课程内容;
  • 使用缓存框架缓存内容;

首先为学生创建浏览已有课程的课程目录并实现学生订阅课程功能。

展示课程

课程目录需要实现下面的功能:

  • 列出课程,可以通过主题进行过滤;
  • 包括课程的简介;

编辑 courses 应用的 views.py 文件并添加以下代码:

from django.db.models import Count
from .models import Subject


class CourseListView(TemplateResponseMixin, View):
    model = Course
    template_name = 'courses/course/list.html'

    def get(self, request, subject=None):
        subjects = Subject.objects.annotate(total_courses=Count('courses'))
        courses = Course.objects.annotate(total_modules=Count('modules'))
        if subject:
            subject = get_object_or_404(Subject, slug=subject)
            courses = courses.filter(subject=subject)
        return self.render_to_response(
            {'subjects': subjects, 'subject': subject, 'courses': courses})

这是 CourseListView 视图,继承 TemplateResponseMixin 和 View 视图。实现以下功能:

  1. 获取所有主题,包括每个主题中课程的总数;使用 ORM 的 annotate() 方法和 Count() 函数实现这个功能;
  2. 获取所有课程,包括每个课程中模块的总数;
  3. 如果 URL 包含主题 slug 参数,则获取对应主题对象并将课程查询限制到该主题。
  4. 使用 TemplateResponseMixin 提供的 render_to_response() 方法将对象渲染到模板中并返回 HTTP 响应。

创建详情视图来展示单个课程的简介。向 views.py 文件中添加以下代码:

from django.views.generic.detail import DetailView


class CourseDetailView(DetailView):
    model = Course
    template_name = 'courses/course/detail.html'

视图继承 Django 的 DetailView 视图,这里指定了 model 和 template_name 属性,Django 的 DetailView 需要主键( pk )或者 slug URL 参数来获得给定模型的对象。它将渲染 template_name 指定的模板,模板中使用 {{ object }} 获取模型对象。

编辑 educa 项目的 urls.py 文件并添加以下 URL 模式:

from courses.views import CourseListView

urlpatterns = [url(r'^accounts/login/$', auth_views.login, name='login'),
               url(r'^accounts/logout/$', auth_views.logout, name='logout'),
               url(r'^admin/', admin.site.urls),
               url(r'course/', include('courses.urls')),
               url(r'^$', CourseListView.as_view(), name='course_list')]

将 course_list URL 模型添加到项目的 urls.py 文件是为了使用 http://127.0.0.1:8000/ 展示课程列表,使用 /course/ 前缀展示 courses 应用中的其它 URL 。

编辑 courses 应用的 urls.py 文件并添加以下 URL 模式:

url(r'^subject/(?P[\w-]+)/$',
    views.CourseListView.as_view(),
    name='course_list_subject'),
url(r'^(?P[\w-]+)/$',
    views.CourseDetailView.as_view(),name='course_detail'),

定义的 URL 模式为:

  • course_list_subject:展示某一主题的所有课程。

  • course_detail:展示单个课程的简介。

接下来创建 CourseListView 和 CourseDetailView 视图,在 courses 应用的 templates/courses/ 目录下创建文件结构:

django by example 实践 educa 项目(二)_第1张图片
courses_structure.png

编辑 courses/course/list.html 模板并添加以下代码:

{% extends "base.html" %}

{% block title %}
    {% if subject %}
        {{ subject.title }} courses
    {% else %}
        All courses
    {% endif %}
{% endblock %}

{% block content %}
    

{% if subject %} {{ subject.title }} courses {% else %} All courses {% endif %}

Subjects

{% for course in courses %} {% with subject=course.subject %}

{{ course.title }}

{{ subject }}. {{ course.total_modules }} modules. Instructors: {{ course.owner.get_full_name }}

{% endwith %} {% endfor %}
{% endblock %}

这是展示所有课程的模板。我们创建了一个 HTML 列表来展示所有的 Subject 对象并为每一个 Subject 对象添加了一个 course_list_subject URL 链接。HTML 中添加了 selected class 来高亮显示当前主题,遍历 Course 对象,展示模块总数和教师的名字。

使用 python manage.py runserver 命令运行开发服务器,在浏览器中打开 http://127.0.0.1:8000/,你将看到下面的页面:

django by example 实践 educa 项目(二)_第2张图片
course_list.png

左侧边栏包含所有主题(包括主题的课程总数),可以通过点击主题来过滤课程。

编辑 courses/course/detail.html 模板并添加以下代码:

{% extends "base.html" %}

{% block title %}
    {{ object.title }}
{% endblock %}

{% block content %}
    {% with subject=course.subject %}
        

{{ object.title }}

Overview

{{ subject.title }}. {{ course.modules.count }} modules. Instructor: {{ course.owner.get_full_name }}

{{ object.overview|linebreaks }}
{% endwith %} {% endblock %}

在这个模板中,我们展示了课程的概览和细节。在浏览器中打开 http://127.0.0.1:8000/ 并点击任意一个课程,应该可以看到下面的页面:

django by example 实践 educa 项目(二)_第3张图片
course_detail.png

我们已经创建了一个公共区域来展示课程。下一步将允许用户以学生的身份注册并订阅课程。

添加学生注册

使用以下命令来创建一个新的应用:

python manage.py startapp students

编辑 educa 项目的 settings.py 文件并将 ‘students' 添加到 INSTALLED_APPS 中:

INSTALLED_APPS = [
    'courses',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'students',
]

创建学生注册视图

编辑 students 应用的 views.py 文件并添加以下代码:

from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm
from django.core.urlresolvers import reverse_lazy
from django.views.generic.edit import CreateView


# Create your views here.


class StudentRegistrationView(CreateView):
    template_name = 'students/student/registration.html'
    form_class = UserCreationForm
    success_url = reverse_lazy('students:student_course_list')

    def form_valid(self, form):
        result = super(StudentRegistrationView, self).form_valid(form)
        cd = form.cleaned_data
        user = authenticate(username=cd['username'], password=cd['password1'])
        login(self.request, user)
        return result

这是学生注册视图。使用通用类视图 CreateView 实现创建模型对象功能。这个视图需要设置以下属性:

  • template_name:视图渲染的模板;
  • form_class:创建对象的表单,需要表单为 ModelForm;使用 Django 的 UserCreationForm 作为注册表单来创建 User 对象。
  • success_url:表单成功提交后重定向的页面。使用 student_course_list 的 URL,稍后将创建这个 URL 来展示学生订阅的课程目录。

获取到有效的表单数据后将执行 form_valid() 方法 ,它会返回一个 HTTP 响应。我们重写这个方法实现用户注册成功后登录的功能。

在 students 应用目录下创建一个 urls.py 文件,并添加以下代码:

from django.conf.urls import url

from . import views

urlpatterns = [url(r'^register/$', views.StudentRegistrationView.as_view(),
                   name='student_registration')]

编辑 educa 项目的 urls.py 文件并在 URL 配置中添加以下模式来添加 students 应用的 URLs :

url(r'^students/', include('students.urls')),

在 students 应用中创建以下文件结构:


django by example 实践 educa 项目(二)_第4张图片
student_structure.png

编辑 students/student/registration.html 模板并添加以下代码:

{% extends "base.html" %}

{% block title %}
    Sign up
{% endblock %}

{% block content %}
    

Sign up

Enter your details to create an account:

{{ form.as_p }} {% csrf_token %}

{% endblock %}

最后,编辑 educa 项目的 settings.py 文件并添加以下代码:

from django.core.urlresolvers import reverse_lazy

LOGIN_REDIRECT_URL = reverse_lazy('students:student_course_list')

如果用户请求中没有参数,auth 模型通过这个设置在用户成功登录后对用户进行重定向。

运行开发服务器,在浏览器中打开 http://127.0.0.1:8000/students/register/ ,你将会看到:

django by example 实践 educa 项目(二)_第5张图片
student_register.png

报读课程

用户创建完账户后应该能够报读课程。为了保存报读情况,我们需要创建一个 Course 和 User 模型的多对多关系,编辑 courses 应用的 models.py 文件并在 Course 模型中添加以下字段:

students = models.ManyToManyField(User, related_name='courses_joined',
                                  blank=True)

在 shell中执行以下命令来创建这个更改的迁移文件:

python manage.py makemigrations

你将看到这样的输出:

Migrations for 'courses':
  courses/migrations/0004_course_students.py
    - Add field students to course

然后执行下面的命令:

python manage.py migrate    

你将看到这样的输出:

Migrations for 'courses':
  courses/migrations/0004_course_students.py
    - Add field students to course
(educa3_env) appledeMacBook:educa3 apple$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, courses, sessions
Running migrations:
  Applying courses.0004_course_students... OK

现在可以将学生与他们报读的课程连在一起了。下面来实现学生报读课程的功能。

在 students 应用目录下新建 forms.py 的文件,并添加以下代码:

from django import forms

from courses.models import Course


class CourseEnrollForm(forms.Form):
    course = forms.ModelChoiceField(queryset=Course.objects.all(),
                                    widget=forms.HiddenInput)

我们将使用这个表单来实现报读课程,course 字段是用户报读的课程,它是一个 ModelChoiceField 字段。这里我们使用 HiddenInput 控件来不让用户看到这个字段。下面将在 CourseDetailView 视图中显示一个订阅按钮。

编辑 students 应用的 views.py 文件并添加以下代码:

from django.views.generic.edit import FormView
from django.contrib.auth.mixins import LoginRequiredMixin
from .forms import CourseEnrollForm


class StudentEnrollCourseView(LoginRequiredMixin, FormView):
    course = None
    form_class = CourseEnrollForm

    def form_valid(self, form):
        self.course = form.cleaned_data['course']
        self.course.students.add(self.request.user)
        return super(StudentEnrollCourseView, self).form_valid(form)

    def get_success_url(self):
        return reverse_lazy('student_course_detail', args=[self.course.id])

这是 courses 中处理学生报读的 StudentEnrollCourseView 视图。这个视图继承 LoginRequiredMixin ,只有登录的用户才能访问这个视图。由于需要提交表单,视图还继承了 Django 的 FormView ,并将 form_class 属性设置为 CourseEnrollForm 并定义存储 Course 对象的 course 属性,如果保单有效,则将当前用户添加到课程报读学生中。

get_success_url() 方法返回表单成功提交后重定向到的页面。这个方法与 success_url 属性等价。这里我们反向解析后续创建的用户展示课程内容的 student_course_detail URL 。

编辑 students 应用的 urls.py 文件并添加以下代码:

url(r'^enroll-course/$', views.StudentEnrollCourseView.as_view(),
    name='student_enroll_course')

下面在课程详情页面添加报读按钮,编辑 courses 应用 views.py 文件并更改 CourseDetailView 视图:

from django.views.generic.detail import DetailView
from students.forms import CourseEnrollForm


class CourseDetailView(DetailView):
    model = Course
    template_name = 'courses/course/detail.html'

    def get_context_data(self, **kwargs):
        context = super(CourseDetailView, self).get_context_data(**kwargs)
        context['enroll_form'] = CourseEnrollForm(
            initial={'course': self.object})
        return context

get_context_data() 方法会将报读表单放到渲染模板的内容中。表单隐藏字段 course 的初始值设置为当前 Course 对象,这样可以直接提交表单。

编辑 courses/course/detail.html 模板并找到下面一行:

{{ object.overview|linebreaks }}

将其替换为:

{{ object.overview|linebreaks }}
{% if request.user.is_authenticated %}
    
{{ enroll_form }} {% csrf_token %}
{% else %} Register to enroll {% endif %}

这是报读课程的按钮,如果用户已经授权,我们展示包含隐藏字段指向 student_enroll_course URL 的表单的报读按钮。如果用户没有授权,我们将显示平台注册链接。

运行开发服务器,在浏览器中打开 http://127.0.0.1:8000/ 并选择一个课程,如果处于登录状态,你应该可以在课程简介后面看到 ENROLL NOW 按钮:

django by example 实践 educa 项目(二)_第6张图片
CH11-1.png

如果没有登录,则会看到一个 Register to enroll 按钮。

笔者注:

建议使用之前创建的 admin 账户或者教师账户登录。 如果 Register to enroll 注册后会跳到下一节才创建的 student_course_list 视图,现在运行会出错。

访问课程内容

我们需要一个展示学生报读的课程的视图和一个访问课程内容的视图。编辑 students 应用的 views.py 文件并添加以下代码:

from django.views.generic.list import ListView
from courses.models import Course


class StudentCourseListView(LoginRequiredMixin, ListView):
    model = Course
    template_name = 'students/course/list.html'

    def get_queryset(self):
        qs = super(StudentCourseListView, self).get_queryset()
        return qs.filter(students__in=[self.request.user])

这是展示用户报读的所有课程的列表。继承 LoginRequiredMixin 来确保只有登录的用户才能访问视图,并且继承通用 ListView 视图来展示 Course 对象列表。我们重写 get_queryset() 方法来获取用户报读的课程(通过过滤 ManyToManyField 字段来进行过滤)。

然后,向 views.py 文件添加以下代码:

from django.views.generic.detail import DetailView


class StudentCourseDetailView(DetailView):
    model = Course
    template_name = 'students/course/detail.html'

    def get_queryset(self):
        qs = super(StudentCourseDetailView, self).get_queryset()
        return qs.filter(students__in=[self.request.user])

    def get_context_data(self, **kwargs):
        context = super(StudentCourseDetailView, self).get_context_data(
            **kwargs)
        # get course object
        course = self.get_object()
        if 'module_id' in self.kwargs:
            context['module'] = course.modules.get(id=self.kwargs['module_id'])
        else:
            # get first module
            context['module'] = course.modules.all()[0]
        return context

这是 StudentCourseDetailView 视图。我们重写 get_queryset() 方法来将课程 queryset 限制到用户报读的 course。我们还重写了原来的 get_context_data() 方法来在内容中设置课程模块,如果URL中给定了 ‘module_id’ 那么使用 module_id 获得课程模块,否则设置课程的第一个模块为课程模块。这样,学生可以浏览课程中的模块了。

在 students 应用的 urls.py 文件并添加以下代码:

url(r'^courses/$', views.StudentCourseListView.as_view(),
    name='student_course_list'),

url(r'^course/(?P\d+)/$',
    views.StudentCourseDetailView.as_view(),
    name='student_course_detail'),

url(r'^course/(?P\d+)/(?P\d+)/$',
    views.StudentCourseDetailView.as_view(),
    name='student_course_detail_module'),

在 students 应用中的 templates/students/ 目录下创建以下文件结构:

CH11-2.png

编辑 students/course/list.html 模板,并添加以下代码:

{% extends "base.html" %}

{% block title %}My courses{% endblock %}

{% block content %}
    

My courses

{% for course in object_list %}

{{ course.title }}

Access contents

{% empty %}

You are not enrolled in any courses yet. Browse courses to enroll in a course.

{% endfor %}
{% endblock %}

这个模板展示用户报读的课程,编辑 students/course/detail.html 模板,并添加以下代码:

{% extends "base.html" %}

{% block title %}
    {{ object.title }}
{% endblock %}

{% block content %}
    

{{ module.title }}

Modules

{% for content in module.contents.all %} {% with item=content.item %}

{{ item.title }}

{{ item.render }} {% endwith %} {% endfor %}
{% endblock %}

这个模板用于报读的学生访问课程内容,首先,创建了一个包含所有课程模块并高亮当前模块的 HTML 列表。然后,遍历当前模块的内容,并使用 {{ item.render }} 展示内容项。下一步将在内容模型中添加 render() 方法。这个方法可以恰当的渲染课程内容。

渲染不同类型的课程

我们需要提供一种渲染各种类型内容的方法。编辑 courses 应用中的 models.py 文件并在 ItemBase 模型中添加 render() 方法:

from django.template.loader import render_to_string
from django.utils.safestring import mark_safe

class ItemBase(models.Model):
    owner = models.ForeignKey(User, related_name='%(class)s_related')
    title = models.CharField(max_length=250)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

    def __str__(self):
        return self.title

    def render(self):
        return render_to_string(
            'courses/content/{}.html'.format(self._meta.model_name),
            {'item': self})

这个方法使用 render_to_string() 函数来渲染模板并返回字符串形式的渲染内容。每种类型的内容渲染内容模型定义的模板。使用 self._meta.model_name 来创建合适的模板名称,render() 方法为渲染各种类型的内容提供了通用接口。

在 courses 应用的 templates/courses/ 目录下创建这样的文件结构:

CH11-3.png

编辑 courses/content/text.html 模板并添加以下代码:

{{ item.content|linebreaks|safe }}

编辑 courses/content/file.html 模板并添加以下代码:

Download file

编辑 courses/content/image.html 模板并添加以下代码:

ImageField 和 FileField 的文件上传正常工作需要项目可以在开发服务器上处理媒体文件,编辑项目的 settings.py 文件并添加以下代码:

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')

MEDIA_URL 是上传媒体文件的基础 URL ,MEDIA_ROOT 是文件的本地地址。

编辑项目的 urls.py 文件并添加以下代码:

from django.conf import settings
from django.conf.urls.static import static

然后,在文件最后添加以下代码:

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

我们的项目现在可以使用开发服务器上传和处理媒体文件了。开发服务器不能用于生产,我们将在第 13 章中配置生产环境。

我们还需要创建渲染 Video 对象的模板。这里将使用 django-embed-video 来嵌入音频内容。Django-embed-video 是一个第三方 Django 应用,可以在模板中通过音频的公共 URL 嵌入 YouTube 或者 Vimeo 等来源的音频。

笔者注:

django-embed-video 只能嵌入 YouTube 、 Vimeo 的视频和 SoundCloud 的音乐。django-embed-video 只为这三网站创建了后端。国内无法访问这三个网站,因此无法使用这个第三方应用。

下面按照原文翻译了相关内容,同时提供了使用 video.js 及

使用以下命令安装 django-embed-video:

pip install django-embed-video

编辑项目的 settings.py 文件并将 embed_video 添加到 INSTALLED_APPS 设置中。我们可以从http://django-embed-video.readthedocs.io/en/v1.1.2/找到 django-embed-video 的文档。

编辑 courses/content/video.html 模板并添加以下代码:

{% load embed_video_tags %}

{{ item.url }}

{#{% video item.url "small" %}#}

笔者注:

笔者使用

{#{% load embed_video_tags %}#}
{#{% video item.url 'small' %}#}

为了


    
    {% block title %}Educa{% endblock %}
    
    
    
    

现在运行开发服务器并在浏览器中访问 http://127.0.0.1:8000/course/mine/。使用属于 Instructor 组的用户或者超级管理员用户登录,为一个课程添加多项内容,如果要包含音频内容,只需要拷贝音频的 URL,例如http://v.youku.com/v_show/id_XNjQ4NDYzMjcy.html,并将其放在表单的 url 字段中。向课程添加完内容后打开 http://127.0.0.1:8000/,点击课程并点击 ENROLL NOW 按钮,你将看到订阅的课程并重定向到 student_course_detail URL 中。下面的图片显示了一个简单的课程内容:

django by example 实践 educa 项目(二)_第7张图片
CH11-4.png

笔者注:
上面 video 需要提供 mp4 和 webm格式的视频文件,这里只是测试,使用的 video.js 的 demo。即这样添加的视频:

django by example 实践 educa 项目(二)_第8张图片
CH11-5.png

太棒了,我们已经创建了一个订阅课程内容的通用接口,每种内容采用不同的渲染方式。

使用缓存框架

web 应用的 HTTP 请求通常需要访问数据库,进行数据处理并渲染模板。这些处理比提供静态页面的消耗大很多。

当网站获得越来越多的流量时,一些请求的开销可能会很大。这时缓存变得很有用。通过在 HTTP 请求中缓存查询、计算结果、或者渲染的内容,从而避免后面的请求带来的成本高昂的操作。这样做可以缩短响应时间,减少服务端的处理。

Django 内置强大的缓存系统,可以用来缓存不同粒度的数据。我们可以缓存一个查询、某个视图的输出、模板渲染内容的一部分或者整个网站。数据在特定时间范围内在缓存系统中进行保存。我们可以指定数据的缓存时间。

下面是应用接收到 HTTP 请求后如何使用缓存框架的:

  1. 尝试从缓存中查找请求数据;
  2. 如果找到了,返回缓存的数据;
  3. 如果没有找到,执行以下步骤:
    1. 进行数据查询或处理;
    2. 将生成的数据保存到缓存中;
    3. 返回数据。

Django 缓存系统的详细信息见 https://docs.djangoproject.com/en/1.11/topics/cache/。

可选的缓存后端

Django 内置几个缓存后端,它们包括:

  • backends.memcached.MemcachedCache 或者 backends.memcached.PyLibMCCache memcached 后端。memcached 是快速有效的基于内存的缓存服务器。我们可以根据 Python 绑定的memcached 决定使用这两个中的哪一个。
  • backends.db.DatabaseCache:使用数据库作为缓存系统;
  • backends.filebased.FileBasedCache:使用文件存储系统。像文件一样序列化并保存缓存值。
  • backends.locmem.LocMemCache:本地内存缓存后端,这是默认的缓存后端。
  • backends.dummy.DummyCache:仅用于开发的虚拟缓存后端。它实现缓存接口但是并不实际缓存任何内容。这个缓存是预处理并且线程安全的。

注意:

为了优化性能,使用 mamcached 后端等基于内存的缓存终端。

安装 memcached

我们将使用 memcached 后端。memcached 在内存中运行并被分配一定数量的 RAM。当被分配的 RAM 用完后,Memcached 开始删除最早的内容来存储最新的内容。

从 http://memcached.org/downloads 下载 memcached 。Linux 系统可以使用以下命令安装 memcached :

./configure && make && make test && sudo make install

Mac OS X 可以使用 HomeBrew 安装 memcached,安装命令为 :

brew install memcached

安装完 Memcached 之后,打开 shell 并使用以下命令启动:

memcached -l 127.0.0.1:11211

Memcached 默认在 11211 端口运行。但是可以使用 -l 选项指定自定义主机和端口。 http://memcached.org 有更多相关信息。

安装完 Memcached 之后,我们需要安装它的 Python 绑定,我们可以通过以下命令进行安装:

pip install python3-memcached

笔者注:

python3 版本使用 pip install python3-memcached,

Python 2版本使用 pip install python-memcached。

缓存设置

Django 提供以下缓存设置:

  • CACHES:项目所有可用缓存的字典;
  • CACHE_MIDDLEWARE_ALIAS:存储使用的缓存别名;
  • CACHE_MIDDLEWARE_KEY_PREFIX:缓存键的前缀。如果多个网站共用一个缓存,设置前缀可以避免冲突。
  • CACHE_MIDDLEWARE_SECONDS:缓存页面的默认时间(秒数)。

项目的缓存系统可以通过 CACHES 进行配置。CACHES 是一个可以配置多个缓存的字典,CACHES 字段中每个缓存可以设置以下数据:

  • BACKEND:使用的缓存后端;

  • KEY_FUNCTION:点路径字符串,用于调用生成缓存键的函数(可以输入前缀、版本以及键作为参数)。

  • KEY_PREFIX:所有缓存主键的前缀,用来避免冲突。

  • LOCATION:缓存所在位置。取决于缓存后端。可能是一个目录、一个主机和端口、或者内存后端的名称。

  • OPTIONS:存入缓存后端的任意其它参数。

  • TIMEOUT:缓存键的有效时间,以秒为单位。默认为 300 秒( 5 分钟),如果设置为 None,将永久有效。

  • VERSION:缓存键的默认版本。用于缓存版本。

在项目中添加 memcached

我们来为项目配置缓存。编辑 educa 项目的 settings.py 文件并添加以下代码:

# cache config

CACHES = {'default': {
    'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
    'LOCATION': '127.0.0.1:11211', }}

这里使用 MemcachedCache 后端,使用 address:port 格式指定位置。如果有多个 memcached 实例,你可以使用 LOCATION 列表。

监测 memcached

django-memcache-status 第三方库可以用于在 admin 网站展示 memcached 实例的状态。使用以下命令安装 django-memcache-status:

pip install django-memcache-status

笔者注:

原文为:为了兼容 Python 3,使用以下命令进行安装:

pip install git+git://github.com/zenx/django-memcache-status.git

2016 年3 月的 V1.2 及之后的版本可以兼容 python 3 ,因此这里直接使用 pip install django-memcache-status。

编辑项目的 settings.py 文件,在 INSTALLED_APPS 中添加 memcache_status 。保证缓存正在运行,在另一个 shell 中启动开发服务器并在浏览器中打开 http://127.0.0.1:8000/admin/。使用超级用户账号登录 admin 网站,你将看到下面的页面:

django by example 实践 educa 项目(二)_第9张图片
CH11-6.png

图片显示缓存的使用情况,绿色条表示空,红色条表示使用,点击标题将会显示 memcached 实例详情。

我们已经为项目设置了 memcached 并且可以对其进行监控。下面我们将开始缓存数据。

缓存级别


Django 按照粒度划分提供以下级别的缓存:

  • Low-level cache API:提供最高粒度,可以缓存指定的查询或者计算。
  • Pre-view cache:缓存单个视图;
  • Template cache:缓存模板片段;
  • Pre-site cache:最高级别的缓存,缓存整个网站。

注意:

实现缓存之前考虑一个缓存策略,应该首先考虑费时的查询或计算,使用缓存代替为每个用户进行计算。

使用低级别缓存 API


低级别的缓存 API 允许用户在缓存中保存任何粒度的对象。它位于 django.core.cache,你可以这样导入:

from django.core.cache import cache

使用默认缓存,等价于 caches['default']。还可以使用缓存的别名来指定缓存:

from django.core.cache import caches
my_cache = caches['alias']

我们来看下 缓存 API 如何工作。使用 python manage.py shell 命令打开 shell并执行以下代码:

In [1]: from django.core.cache import cache

In [2]: cache.set('musician','Django Reinhardt',20)

访问默认的缓存后端并使用 set(key, value, timeout) 来保存名为 ’musician' 的主键,它的值设置为字符串 'Django Reinhardt',有效时间为 20 秒。如果不指定失效时间, Django 将使用 CACHES 设置中 cache 后端的默认失效时间。现在执行以下代码:

In [3]: cache.get('musician')

Out[3]: 'Django Reinhardt'

我们从缓存中获取主键,等待 20 秒并执行相同的代码:

In [4]: cache.get('musician')

’musician' 缓存键已经失效,由于缓存中已经没有这个键,get() 方法没有返回值。

注意:

永远不要将缓存键的值设为 None ,你将无法分清哪些是实际的值哪些是主键缺失。

我们来缓存一个 QuerySet:

In [5]: from courses.models import Subject

In [6]: subjects = Subject.objects.all()

In [7]: cache.set('all_subjects',subjects)

这里实现了一个 Subject 模型的 QuerySet ,并将返回值保存到 all_subjects 中,下面来获取缓存数据:

In [8]: cache.get('all_subjects')
Out[8]: , , , ]>

我们将在视图中缓存一些查询,编辑 courses 应用的 views.py 文件,并添加以下代码:

from django.core.cache import cache

在 CourseListView 的 get() 方法中,找到下面一行:

subjects = Subject.objects.annotate(total_courses=Count('courses'))

将其替换为:

subjects = cache.get('all_subjects')
if not subjects:
     subjects = Subject.objects.annotate(total_courses=Count('courses'))
     cache.set('all_subjects', subjects)

首先尝试使用 cache.get() 方法从缓存中获取 all_subjects 的值,如果缓存没有找到对应键则返回 None,如果没有找到(还没有缓存,或者缓存过但是过期了),我们需要查询得到所有 Subject 对象以及课程数量,并使用 cache.set() 进行缓存。

运行开发服务器并在浏览器中打开 http://127.0.0.1:8000/ 。执行视图时,没有找到缓存主键并且执行了 QuerySet。在浏览器中打开 http://127.0.0.1:8000/admin/ 并展开 memcached 。你应该看到类似这样的使用数据:

django by example 实践 educa 项目(二)_第10张图片
CH11-7.png

查看一下 Curr Items 项,它的值应该是 1 ,表示缓存中保存了一个变量。GET Hits 表示成功执行的 get 命令的数量,Get Misses 表示 get 请求缺少缓存数据的数量。 Miss Ratio 是通过两者计算得到的值。

现在在浏览器中打开 http://127.0.0.1:8000/ 并重新载入页面几次,将会看到缓存被读取的次数增多了( Get Hits 和 Cmd Get 都增加了)。

基于动态数据的缓存

很多时候我们需要基于动态数据进行缓存。这种情况下需要创建动态键,动态键包含所有唯一识别缓存数据所需要的信息。编辑 courses 应用的 views.py 文件并更改 CourseListView 视图:

class CourseListView(TemplateResponseMixin, View):
    model = Course
    template_name = 'courses/course/list.html'

    def get(self, request, subject=None):
        subjects = cache.get('all_subjects')
        if not subjects:
            subjects = Subject.objects.annotate(total_courses=Count('courses'))
            cache.set('all_subjects', subjects)

        all_courses = Course.objects.annotate(total_modules=Count('modules'))
        if subject:
            subject = get_object_or_404(Subject, slug=subject)
            key = 'subject_{}_courses'.format(subject.id)
            courses = cache.get(key)
            if not courses:
                courses = all_courses.filter(subject=subject)
                cache.set(key, courses)
        else:
            courses = cache.get('all_courses')
        if not courses:
            courses = all_courses
            cache.set('all_courses', courses)
        return self.render_to_response(
            {'subjects': subjects, 'subject': subject, 'courses': courses})

这种情况下,我们还缓存了所有主题对应的课程。如果没有给定的主题,使用 all_courses 缓存所有课程,如果给定了主题,则使用动态创建的 'subject_{}_courses'.format(subject.id)

这里需要注意的是缓存的是 queryset 结果,因此不能使用缓存的 queryset 来创建其他 querysets,也就是说不能这样做:

courses = cache.get('all_courses')
courses.filter(subject=subject)

我们还需要创建 queryset Course.objects.annotate(total_modules=Count('modules')),这个 queryset 只在强制执行情况下执行,如果缓存中没有找到数据还可以使用 all_courses.filter(subject=subject) 对 queryset 进行进一步限制。

缓存模板片段


缓存模板片段是高级别方法。我们需要在模板中使用 {% load cache %} 加载缓存模板标签,然后就可以使用 {% cache %} 模板标签缓存特定的模板片段。经常这样使用模板标签:

{% cache 300 fragment_name %}
    ...
{% endcache %}

{% cache %} 标签需要两个参数:过期时间(以秒为单位)和片段名称。如果需要基于动态数据缓存内容,我们可以通过向 {% cache %} 模板标签传入额外的参数来唯一识别片段。

编辑 students 应用的 /students/course/detail.html 。在模板顶部 {% extends %} 标签之后添加以下代码:

{% load cache %}

在模板尾部找到:

{% for content in module.contents.all %}
    {% with item=content.item %}
        

{{ item.title }}

{{ item.render }} {% endwith %} {% endfor %}

将其替换为:

{% cache 600 module_contents module %}
    {% for content in module.contents.all %}
        {% with item=content.item %}
            

{{ item.title }}

{{ item.render }} {% endwith %} {% endfor %} {% endcache %}

使用传入当前 Module 对象的 module_contents 缓存模板片段。这样可以唯一识别片段,避免缓存模块内容以及请求其它模块时提供错误的内容。

注意:

如果 USE_I18N 设置为 True,网站中间件缓存将使用激活的语言。如果使用 {% cache %} 模板标签对于特定翻译变量实现相同的结果,则可以使用 {% cache 600 name request.LANGUAGE_CODE %}。

缓存视图


django.views.decorators.cache 中的 cache_page 装饰器可以用来缓存单个视图的输出。装饰器需要输入失效时间参数(以秒为单位)。

我们在视图中使用 cache_page。编辑 students 应用的 urls.py 文件并添加以下代码:

from django.views.decorators.cache import cache_page    

然后对 student_course_detail 和 student_course_detail_module URL模式使用装饰器 cache_page:

url(r'^course/(?P\d+)/$',
    cache_page(60*15)(views.StudentCourseDetailView.as_view()),
    name='student_course_detail'),

url(r'^course/(?P\d+)/(?P\d+)/$',
    cache_page(60 * 15)(views.StudentCourseDetailView.as_view()),
    name='student_course_detail_module'),

现在,StudentCourseDetailView 的结果缓存 15 分钟。

注意:

视图缓存使用 URL 构建缓存键,指向同一视图的多个 URLs 需要进行单独缓存。

使用网站缓存


这是最高级别的缓存,可以缓存整个网站。

为了实现网站缓存,我们需要编辑项目的 settings.py 文件并在 MIDDLEWARE_ClASSES 设置中添加 UpdateCacheMiddleware 和 FetchFromCacheMiddleware 类:

'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
'django.moddleware.cache.FetchFromCacheMiddleware',

middleware 在请求阶段按照特定的顺序执行,在响应阶段按照相反的顺序执行。UpdateCacheMiddleware 位于 CommonMiddleware 之前是由于在响应阶段按照相反的顺序执行。FetchFromCacheMiddleware 放在 CommonMiddleware 之后是由于 FetchFromCacheMiddleware 需要访问 CommonMiddleware 设置的请求数据。

然后,在 settings.py 文件中添加以下设置:

CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 60 * 15  # 15 minutes
CACHE_MIDDLEWARE_KEY_PREFIX = 'educa'

这些设置中,缓存中间件使用默认缓存并将全局缓存时间设置为 15 分钟。所有的缓存键还都设置了特定前缀以避免多个项目使用相同缓存后端时造成的冲突。现在,可以缓存网站并为所有的 GET 请求返回缓存的内容。

我们这样做的目的是测试网站缓存功能。然而,由于课程管理视图需要展示更新的数据来对任何更改进行反馈,因此我们的网站不适合网站缓存。项目的最佳缓存实践是缓存向学生展示课程内容的模板或者视图。

我们现在已经对 Django 提供的缓存数据的方法有了大致的了解,我们应该明智的设计缓存策略并优先考虑最昂贵的查询或计算。

总结


本章,我们为课程创建了公用视图,创建了学生注册及报读系统,安装了 memcached 并实现了不同级别的缓存。下一章,我们将为项目创建一个 REST API 。

你可能感兴趣的:(django by example 实践 educa 项目(二))