第十一章

緩存內(nèi)容

(譯者 @ucag 注:這是倒數(shù)第二章,最后一個(gè)項(xiàng)目即將完成。 @夜夜月 將會接過翻譯的最后一棒。這本書的翻譯即將完成。這也是我翻譯的最后一章,作為英語專業(yè)的學(xué)生,我對于翻譯有了更多的思考。同樣,校對是 @夜夜月)

(譯者 @夜夜月注:贊,倒數(shù)第二章了,接下來就是最后一章了!)

在上一章中,你使用模型繼承和一般關(guān)系創(chuàng)建了一個(gè)靈活的課程內(nèi)容模型。你也使用基于類的視圖,表單集,以及 內(nèi)容的 AJAX 排序,創(chuàng)建了一個(gè)課程管理系統(tǒng)。在這一章中,你將會:

  • 創(chuàng)建展示課程信息的公共視圖

  • 創(chuàng)建一個(gè)學(xué)生注冊系統(tǒng)

  • courses中管理學(xué)生注冊

  • 創(chuàng)建多樣化的課程內(nèi)容

  • 使用緩存框架緩存內(nèi)容

我們將會從創(chuàng)建一個(gè)課程目錄開始,好讓學(xué)生能夠?yàn)g覽當(dāng)前的課程以及注冊這些課程。

展示課程

對于課程目錄,我們需要?jiǎng)?chuàng)建以下的功能:

  • 列出所有的可用課程,可以通過可選科目過濾

  • 展示一個(gè)單獨(dú)的課程概覽

編輯 courses 應(yīng)用的 views.py ,添加以下代碼:

from django.db.models import Countfrom .models import Subjectclass 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 。在這個(gè)視圖中,我們實(shí)現(xiàn)了下面的功能:

  • 1 我們檢索所有的課程,包括它們當(dāng)中的每個(gè)課程總數(shù)。我們使用 ORM 的 annotate() 方法和 Count() 聚合方法來實(shí)現(xiàn)這一功能。

  • 2 我們檢索所有的可用課程,包括在每個(gè)課程中包含的模塊總數(shù)。

  • 3 如果給了科目的 slug URL 參數(shù),我們就檢索對應(yīng)的課程對象,然后我們將會把查詢限制在所給的科目之內(nèi)。

  • 4 我們使用 TemplateResponseMixin 提供的 render_to_response() 方法來把對象渲染到模板中,然后返回一個(gè) HTTP 響應(yīng)。

讓我們創(chuàng)建一個(gè)詳情視圖來展示單一課程的概覽。在 views.py 中添加以下代碼:

from django.views.generic.detail import DetailViewclass CourseDetailView(DetailView):
    model = Course
    template_name = 'courses/course/detail.html'

這個(gè)視圖繼承了 Django 提供的通用視圖 DetailView 。我們定義了 model 和 template_name 屬性。Django 的 DetailView 期望一個(gè) 主鍵(pk) 或者 slug URL 參數(shù)來檢索對應(yīng)模型的一個(gè)單一對象。然后它就會渲染 template_name 中的模板,同樣也會把上下文中的對象渲染進(jìn)去。

編輯 educa 項(xiàng)目中的主 urls.py 文件,添加以下 URL 模式:

from courses.views import CourseListView

urlpatterns = [    # ...
    url(r'^$', CourseListView.as_view(), name='course_list'),
]

我們把 course_list 的 URL 模式添加進(jìn)了項(xiàng)目的主 urls.py 中,因?yàn)槲覀兿胍颜n程列表展示在 http://127.0.0.1:8000/中,然后 courses 應(yīng)用的所有的其他 URL 都有 /course/ 前綴。

編輯 courses 應(yīng)用的 urls.py ,添加以下 URL 模式:

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

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

我們定義了以下 URL 模式:

  • course_list_subject:用于展示一個(gè)科目的所有課程

  • course_detail:用于展示一個(gè)課程的概覽

讓我們?yōu)?nbsp;CourseListView 和 CourseDetailView 創(chuàng)建模板。在 courses 應(yīng)用的 templates/courses/ 路徑下創(chuàng)建以下路徑:

  • course/

  • list.html

  • detail.html

編輯 courses/course/list.html 模板,寫入以下代碼:

{% extends "base.html" %}

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

{% block content %}<h1>
    {% if subject %}
        {{ subject.title }} courses
    {% else %}
        All courses
    {% endif %}</h1><div class="contents">
    <h3>Subjects</h3>
    <ul id="modules">
        <li {% if not subject %}class="selected"{% endif %}>
            <a href="{% url "course_list" %}">All</a>
        </li>
        {% for s in subjects %}            <li {% if subject == s %}class="selected"{% endif %}>
                <a href="{% url "course_list_subject" s.slug %}">
                    {{ s.title }}                    <br><span>{{ s.total_courses }} courses</span>
                </a>
            </li>
        {% endfor %}    </ul></div><div class="module">
    {% for course in courses %}
        {% with subject=course.subject %}            <h3><a href="{% url "course_detail" course.slug %}">{{ course.title }}</a></h3>
            <p>
                <a href="{% url "course_list_subject" subject.slug %}">{{ subject }}</a>.
                {{ course.total_modules }} modules.
                Instructor: {{ course.owner.get_full_name }}            </p>
        {% endwith %}
    {% endfor %}</div>{% endblock %}

這個(gè)模板用于展示可用課程列表。我們創(chuàng)建了一個(gè) HTML 列表來展示所有的 Subject 對象,然后為它們每一個(gè)都創(chuàng)建了一個(gè)鏈接,這個(gè)鏈接鏈接到 course_list_subject 的 URL 。 如果存在當(dāng)前科目,我們就把 selected HTML 類添加到當(dāng)前科目中高亮顯示該科目。我們迭代每個(gè) Course 對象,展示模塊的總數(shù)和教師的名字。

使用 python manage.py runserver 打開代發(fā)服務(wù)器,訪問 http://127.0.0.1:8000/ 。你看到的應(yīng)該是像下面這個(gè)樣子:

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

左邊的側(cè)邊欄包含了所有的科目,包括每個(gè)科目的課程總數(shù)。你可以點(diǎn)擊任意一個(gè)科目來篩選展示的課程。

編輯 courses/course/detail.html 模板,添加以下代碼:

{% extends "base.html" %}

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

{% block content %}
    {% with subject=course.subject %}        <h1>
            {{ object.title }}        </h1>
        <div class="module">
            <h2>Overview</h2>
            <p>
                <a href="{% url "course_list_subject" subject.slug %}">{{ subject.title }}</a>.
                {{ course.modules.count }} modules.
                Instructor: {{ course.owner.get_full_name }}            </p>
            {{ object.overview|linebreaks }}        </div>
    {% endwith %}
{% endblock %}

在這個(gè)模板中,我們展示了每個(gè)單一課程的概覽和詳情。訪問 http://127.0.0.1:8000/ ,點(diǎn)擊任意一個(gè)課程。你就應(yīng)該看到有下面結(jié)構(gòu)的頁面了:

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

我們已經(jīng)創(chuàng)建了一個(gè)展示課程的公共區(qū)域了。下面,我們需要讓用戶可以注冊為學(xué)生以及注冊他們的課程。

添加學(xué)生注冊

使用下面的命令創(chuàng)鍵一個(gè)新的應(yīng)用:

python manage.py startapp students

編輯 educa 項(xiàng)目的 settings.py ,把 students 添加進(jìn) INSTALLED_APPS 設(shè)置中:

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

創(chuàng)建一個(gè)學(xué)生注冊視圖

編輯 students 應(yīng)用的 views.py ,寫入下下面的代碼:

from django.core.urlresolvers import reverse_lazyfrom django.views.generic.edit import CreateViewfrom django.contrib.auth.forms import UserCreationFormfrom django.contrib.auth import authenticate, loginclass StudentRegistrationView(CreateView):
    template_name = 'students/student/registration.html'
    form_class = UserCreationForm
    success_url = reverse_lazy('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

這個(gè)視圖讓學(xué)生可以注冊進(jìn)我們的網(wǎng)站里。我們使用了可以提供創(chuàng)建模型對象功能的通用視圖 CreateView 。這個(gè)視圖要求以下屬性:

  • template_name:渲染這個(gè)視圖的模板路徑。

  • form_class:用于創(chuàng)建對象的表單,我們使用 Django 的 UserCreationForm 作為注冊表單來創(chuàng)建 User 對象。

  • success_url:當(dāng)表單成功提交時(shí)要將用戶重定向到的 URL 。我們逆序了 student_course_list URL,我們稍候?qū)⒔ㄋ鼇碚故緦W(xué)生已報(bào)名的課程。

當(dāng)合法的表單數(shù)據(jù)被提交時(shí) form_valid() 方法就會執(zhí)行。它必須返回一個(gè) HTTP 響應(yīng)。我們覆寫了這個(gè)方法來讓用戶在成功注冊之后登錄。

在 students 應(yīng)用路徑下創(chuàng)建一個(gè)新的文件,命名為 urls.py ,添加以下代碼:

from django.conf.urls import urlfrom . import views

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

編輯 educa 的主 urls.py ,然后把 students 應(yīng)用的 URLs 引入進(jìn)去:

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

在 students 應(yīng)用內(nèi)創(chuàng)建如下的文件結(jié)構(gòu):

templates/
    students/
        student/
            registration.html

編輯 students/student/registration.html 模板,然后添加以下代碼:

{% extends "base.html" %}

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

{% block content %}    <h1>
        Sign up    </h1>
    <div class="module">
        <p>Enter your details to create an account:</p>
        <form action="" method="post">
            {{ form.as_p }}
            {% csrf_token %}            <p><input type="submit" value="Create my account"></p>
        </form>
    </div>{% endblock %}

最后編輯 educa 的設(shè)置文件,添加以下代碼:

from django.core.urlresolvers import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('student_course_list')

這個(gè)是由 auth 模型用來給用戶在成功的登錄之后重定向的設(shè)置,如果請求中沒有 next 參數(shù)的話。

打開開發(fā)服務(wù)器,訪問 http://127.0.0.1:8000/students/register/ ,你可以看到像這樣的注冊表單:

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

報(bào)名

在用戶創(chuàng)建一個(gè)賬號之后,他們應(yīng)該就可以在 courses 中報(bào)名了。為了保存報(bào)名表,我們需要在 Course 和 User 模型之間創(chuàng)建一個(gè)多對多關(guān)系。編輯 courses 應(yīng)用的 models.py 然后把下面的字段添加進(jìn) Course 模型中:

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

在 shell 中執(zhí)行下面的命令來創(chuàng)建遷移:

python manage.py makemigrations

你可以看到類似下面的輸出:

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

接下來執(zhí)行下面的命令來應(yīng)用遷移:

python manage.py migrate

你可以看到以下輸出:

Operations to perform:
    Apply all migrations: courses
Running migrations:
    Rendering model states... DONE
    Applying courses.0004_course_students... OK

我們現(xiàn)在就可以把學(xué)生和他們報(bào)名的課程相關(guān)聯(lián)起來了。
讓我們創(chuàng)建學(xué)生報(bào)名課程的功能吧。

在 students 應(yīng)用內(nèi)創(chuàng)建一個(gè)新的文件,命名為 forms.py ,添加以下代碼:

from django import formsfrom courses.models import Courseclass CourseEnrollForm(forms.Form):
    course = forms.ModelChoiceField(queryset=Course.objects.all(),
                                    widget=forms.HiddenInput)

我們將會把這張表用于學(xué)生報(bào)名。course 字段是學(xué)生報(bào)名的課程。所以,它是一個(gè) ModelChoiceField 。我們使用 HiddenInput 控件,因?yàn)槲覀儾淮蛩惆堰@個(gè)字段展示給用戶。我們將會在 CourseDetailView 視圖中使用這個(gè)表單來展示一個(gè)報(bào)名按鈕。

編輯 students 應(yīng)用的 views.py ,添加以下代碼:

from django.views.generic.edit import FormViewfrom braces.views import LoginRequiredMixinfrom .forms import CourseEnrollFormclass 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])

這就是 StudentEnrollCourseView 。它負(fù)責(zé)學(xué)生在 courses 中報(bào)名。新的視圖繼承了 LoginRequiredMixin ,所以只有登錄了的用戶才可以訪問到這個(gè)視圖。我們把 CourseEnrollForm表單用在了 form_class 屬性上,同時(shí)我們也定義了一個(gè) course 屬性來儲存所給的 Course 對象。當(dāng)表單合法時(shí),我們把當(dāng)前用戶添加到課程中已報(bào)名學(xué)生中去。

如果表單提交成功,get_success_url 方法就會返回用戶將會被重定向到的 URL 。這個(gè)方法相當(dāng)于 success_url 屬性。我們反序 student_course_detail URL ,我們稍候?qū)?chuàng)建它來展示課程中的學(xué)生。

編輯 students 應(yīng)用的 urls.py ,添加以下 URL 模式:

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

讓我們把報(bào)名按鈕表添加進(jìn)課程概覽頁。編輯 course 應(yīng)用的 views.py ,然后修改 CourseDetailView 讓它看起來像這樣:

from students.forms import CourseEnrollFormclass 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() 方法來在渲染進(jìn)模板中的上下文里引入報(bào)名表。我們初始化帶有當(dāng)前 Course 對象的表單的隱藏 course 字段,這樣它就可以被直接提交了。

編輯 courses/course/detail.html 模板,然后找到下面的這一行:

{{ object.overview|linebreaks }}

起始行應(yīng)該被替換為下面的這幾行:

{{ object.overview|linebreaks }}
{% if request.user.is_authenticated %}    <form action="{% url "student_enroll_course" %}" method="post">
        {{ enroll_form }}
        {% csrf_token %}        <input type="submit" class="button" value="Enroll now">
    </form>{% else %}    <a href="{% url "student_registration" %}" class="button">
        Register to enroll    </a>{% endif %}

這個(gè)按鈕就是用于報(bào)名的。如果用戶是被認(rèn)證過的,我們就展示包含了隱藏表單字段的報(bào)名按鈕,這個(gè)表單指向了 student_enroll_course URL。如果用戶沒有被認(rèn)證,我們將會展示一個(gè)注冊鏈接。

確保已經(jīng)打開了開發(fā)服務(wù)器,訪問 http://127.0.0.1:8000/ ,然后點(diǎn)擊一個(gè)課程。如果你登錄了,你就可以在底部看到 ENROLL NOW 按鈕,就像這樣:

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

如果你沒有登錄,你就會看到一個(gè)Register to enroll 的按鈕。

獲取課程內(nèi)容

我們需要一個(gè)視圖來展示學(xué)生已經(jīng)報(bào)名的課程,和一個(gè)獲取當(dāng)前課程內(nèi)容的視圖。編輯 students 應(yīng)用的 views.py ,添加以下代碼:

from django.views.generic.list import ListViewfrom courses.models import Courseclass 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])

這個(gè)是用于列出學(xué)生已經(jīng)報(bào)名課程的視圖。它繼承 LoginRequiredMixin 來確保只有登錄的用戶才可以連接到這個(gè)視圖。同時(shí)它也繼承了通用視圖 ListView 來展示 Course 對象列表。我們覆寫了 get_queryset() 方法來檢索用戶已經(jīng)報(bào)名的課程。我們通過學(xué)生的 ManyToManyField 字段來篩選查詢集以達(dá)到這個(gè)目的。

把下面的代碼添加進(jìn) views.py 文件中:

from django.views.generic.detail import DetailViewclass 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:            # get current module
            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 方法把查詢集限制在用戶報(bào)名的課程之內(nèi)。我們同樣也覆寫了 get_context_data() 方法來把課程的一個(gè)模塊賦值在上下文內(nèi),如果給了 model_id URL 參數(shù)的話。否則,我們就賦值課程的第一個(gè)模塊。這樣,學(xué)生就可以在課程之內(nèi)瀏覽各個(gè)模塊了。

編輯 students 應(yīng)用的 urls.py ,添加以下 URL 模式:

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

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

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

在 students 應(yīng)用的 templates/students/ 路徑下創(chuàng)建以下文件結(jié)構(gòu):

course/
    detail.html    list.html

編輯 students/course/list.html 模板,然后添加下列代碼:

{% extends "base.html" %}

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

{% block content %}    <h1>My courses</h1>

    <div class="module">
        {% for course in object_list %}            <div class="course-info">
                <h3>{{ course.title }}</h3>
                <p><a href="{% url "student_course_detail" course.id %}">Access contents</a></p>
            </div>
        {% empty %}            <p>
                You are not enrolled in any courses yet.                <a href="{% url "course_list" %}">Browse courses</a>to enroll in a course.            </p>
        {% endfor %}    </div>{% endblock %}

這個(gè)模板展示了用戶報(bào)名的課程。編輯 students/course/detail.html 模板,添加以下代碼:

{% extends "base.html" %}

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

{% block content %}    <h1>
        {{ module.title }}    </h1>
    <div class="contents">
        <h3>Modules</h3>
        <ul id="modules">
        {% for m in object.modules.all %}            <li data-id="{{ m.id }}" {% if m == module %}class="selected"{% endif %}>
                <a href="{% url "student_course_detail_module" object.id m.id %}">
                    <span>
                        Module <span class="order">{{ m.order|add:1 }}</span>
                    </span>
                    <br>
                    {{ m.title }}                </a>
            </li>
        {% empty %}            <li>No modules yet.</li>
        {% endfor %}        </ul>
    </div>
    <div class="module">
        {% for content in module.contents.all %}
            {% with item=content.item %}                <h2>{{ item.title }}</h2>
                {{ item.render }}
            {% endwith %}
        {% endfor %}    </div>{% endblock %}

這個(gè)模板用于報(bào)名了的學(xué)生連接到課程內(nèi)容。首先我們創(chuàng)建了一個(gè)包含所有課程模塊的 HTML 列表且高亮當(dāng)前模塊。然后我們迭代當(dāng)前的模塊內(nèi)容,之后使用 {{ item.render }} 來連接展示內(nèi)容。接下來我們將會在內(nèi)容模型中添加 render() 方法。這個(gè)方法將會負(fù)責(zé)精準(zhǔn)的展示內(nèi)容。

渲染不同類型的內(nèi)容

我們需要提供一個(gè)方法來渲染不同類型的內(nèi)容。編輯 course 應(yīng)用的 models.py ,把 render() 方法添加進(jìn) ItemBase 模型中:

from django.template.loader import render_to_stringfrom django.utils.safestring import mark_safeclass ItemBase(models.Model):
    # ...
    def render(self):
        return render_to_string('courses/content/{}.html'.format(                            self._meta.model_name), {'item': self})

這個(gè)方法使用了 render_to_string() 方法來渲染模板以及返回一個(gè)作為字符串的渲染內(nèi)容。每種內(nèi)容都使用以內(nèi)容模型命名的模板渲染。我們使用 self._meta.model_name 來為 la 創(chuàng)建合適的模板名。 render() 方法提供了一個(gè)渲染不同頁面的通用接口。

在 courses 應(yīng)用的 templates/courses/ 路徑下創(chuàng)建如下文件結(jié)構(gòu):

content/
    text.html    file.html
    image.html
    video.html

編輯 courses/content/text.html 模板,寫入以下代碼:

{{ item.content|linebreaks|safe }}

編輯 courses/content/file.html 模板,寫入以下代碼:

<p><a href="{{ item.file.url }}" class="button">Download file</a></p>

編輯 courses/content/image.html 模板,寫入以下代碼:

<p><img src="{{ item.file.url }}"></p>

為了使上傳帶有 ImageField 和 FielField 的文件工作,我們需要配置我們的項(xiàng)目以使用開發(fā)服務(wù)器提供媒體文件服務(wù)。編輯你的項(xiàng)目中的 settings.py ,添加以下代碼:

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

記住 MEDIA_URL 是服務(wù)上傳文件的基本 URL 路徑, MEDIA_ROOT 是放置文件的本地路徑。

編輯你的項(xiàng)目的主 urls.py ,添加以下 imports:

from django.conf import settingsfrom django.conf.urls.static import static

然后,把下面這幾行寫入文件的結(jié)尾:

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

你的項(xiàng)目現(xiàn)在已經(jīng)準(zhǔn)備好使用開發(fā)服務(wù)器上傳和服務(wù)文件了。記住開發(fā)服務(wù)器不能用于生產(chǎn)環(huán)境中。我們將會在下一章中學(xué)習(xí)如何配置生產(chǎn)環(huán)境。

我們也需要?jiǎng)?chuàng)建一個(gè)模板來渲染 Video 對象。我們將會使用 django-embed-video 來嵌入視頻內(nèi)容。 Django-embed-video 是一個(gè)第三方 Django 應(yīng)用,它使你可以通過提供一個(gè)視頻的公共 URL 來在模板中嵌入視頻,類似來自 YouTube 或者 Vimeo 的資源。

使用下面的命令來安裝這個(gè)包:

pip isntall django-embed-video==1.0.0

然后編輯項(xiàng)目的 settings.py 然后添加 embed_video 到 INSTALLED_APPS設(shè)置 中。你可以在這里找到 django-embed-video 的文檔:
http://django-embed-video.readthedocs.org/en/v1.0.0/ 。

編輯 courses/content/video.html 模板,寫入以下代碼:

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

現(xiàn)在運(yùn)行開發(fā)服務(wù)器,訪問 http://127.0.0.1:8000/course/mine/ 。用屬于教師組或者超級管理員的用戶訪問站點(diǎn),然后添加一些內(nèi)容到一個(gè)課程中。為了引入視頻內(nèi)容,你也可以復(fù)制任何一個(gè) YouTube 視頻 URL ,比如 :https://www.youtube.com/watch?n=bgV39DlmZ2U ,然后把它引入到表單的 url 字段中。在添加內(nèi)容到課程中之后,訪問 http://127.0.0.1:8000/ ,點(diǎn)擊課程然后點(diǎn)擊ENROLL NOW按鈕。你就可以在課程中報(bào)名了,然后被重定向到 student_course_detail URL 。下面這張圖片展示了一個(gè)課程內(nèi)容樣本:

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

真棒!你已經(jīng)創(chuàng)建了一個(gè)渲染課程的通用接口了,它們中的每一個(gè)都會被用特定的方式渲染。

使用緩存框架

你的應(yīng)用的 HTTP 請求通常是數(shù)據(jù)庫鏈接,數(shù)據(jù)處理,和模板渲染的。就處理數(shù)據(jù)而言,它的開銷可比服務(wù)一個(gè)靜態(tài)網(wǎng)站大多了。

請求開銷在你的網(wǎng)站有越來越多的流量時(shí)是有意義的。這也使得緩存變得很有必要。通過緩存 HTTP 請求中 的查詢結(jié)果,計(jì)算結(jié)果,或者是渲染上下文,你將會避免在接下來的請求中巨大的開銷。這使得服務(wù)端的響應(yīng)時(shí)間和處理時(shí)間變短。

Django 配備有一個(gè)健碩的緩存系統(tǒng),這使得你可以使用不同級別的顆粒度來緩存數(shù)據(jù)。你可以緩存單一的查詢,一個(gè)特定的輸出視圖,部分渲染的模板上下文,或者整個(gè)網(wǎng)站。緩存系統(tǒng)中的內(nèi)容會在默認(rèn)時(shí)間內(nèi)被儲存。你可以指定緩存數(shù)據(jù)過期的時(shí)間。

這是當(dāng)你的站點(diǎn)收到一個(gè) HTTP 請求時(shí)將會通常使用的緩存框架的方法:

1. 試著在緩存中尋找緩存數(shù)據(jù)2. 如果找到了,就返回緩存數(shù)據(jù)3. 如果沒有找到,就執(zhí)行下面的步驟:    1. 執(zhí)行查詢或者處理請求來獲得數(shù)據(jù)    2. 在緩存中保存生成的數(shù)據(jù)    3. 返回?cái)?shù)據(jù)

你可以在這里找到更多關(guān)于 Django 緩存系統(tǒng)的細(xì)節(jié)信息:https://docs.djangoproject.com/en/1.8/topics/cache/ 。

激活緩存后端

Django 配備有幾個(gè)緩存后端,他們是:

  • backends.memcached.MemcachedCache 或 backends.memcached.PyLibMCCache:一個(gè)內(nèi)存緩存后端。內(nèi)存緩存是一個(gè)快速、高效的基于內(nèi)存的緩存服務(wù)器。后端的使用取決于你選擇的 Python 綁定(bindings)。

  • backends.db.DatabaseCache: 使用數(shù)據(jù)庫作為緩存系統(tǒng)。

  • backends.filebased.FileBasedCache:使用文件儲存系統(tǒng)。把每個(gè)緩存值序列化和儲存為單一的文件。

  • backends.locmem.LocMemCache:本地內(nèi)存緩存后端。這是默認(rèn)的緩存后端

  • backends.dummy.DummyCache:一個(gè)用于開發(fā)的虛擬緩存后端。它實(shí)現(xiàn)了緩存交互界面而不用真正的緩存任何東西。緩存是獨(dú)立進(jìn)程且是線程安全的

對于可選的實(shí)現(xiàn),使用內(nèi)存的緩存后端吧,比如 Memcached 后端。

安裝 Memcached

我們將會使用 Memcached 緩存后端。內(nèi)存緩存運(yùn)行在內(nèi)存中,它在 RAM 中分配了指定的數(shù)量。當(dāng)分配的 RAM 滿了時(shí),Memcahed 就會移除最老的數(shù)據(jù)來保存新的數(shù)據(jù)。

在這個(gè)網(wǎng)址下載 Memcached: http://memcached.org/downloads 。如果你使用 Linux, 你可以使用下面的命令安裝:

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

如果你在使用 Mac OS X, 你可以使用命令 brew install Memcached 通過 Homebrew 包管理器來安裝 Memcached 。你可以在這里下載 Homebrew http://brew.sh

如果你正在使用 Windwos ,你可以在這里找到一個(gè) Windows 的 Memcached 二進(jìn)制版本:http://code.jellycan.com/memcached/ 。

在安裝 Memcached 之后,打開 shell ,使用下面的命令運(yùn)行它:

memcached -l 127.0.0.1:11211

Memcached 將會默認(rèn)地在 11211 運(yùn)行。當(dāng)然,你也可以通過 -l 選項(xiàng)指定一個(gè)特定的主機(jī)和端口。你可以在這里找到更多關(guān)于 Memcached 的信息:http://memcached.org 。

在安裝 Memcached 之后,你需要安裝它的 Python 綁定(bindings)。使用下面的命令安裝:】

python install python3-memcached==1.51

緩存設(shè)置

Django 提供了如下的緩存設(shè)置:

  • CACHES:一個(gè)包含所有可用的項(xiàng)目緩存。

  • CACHE_MIDDLEWARE_ALIAS:用于儲存的緩存別名。

  • CACHE_MIDDLEWARE_KEY_PREFIX:緩存鍵的前綴。設(shè)置一個(gè)緩存前綴來避免鍵的沖突,如果你在幾個(gè)站點(diǎn)中分享相同的緩存的話。

  • CACHE_MIDDLEWARE_SECONDS :默認(rèn)的緩存頁面秒數(shù)

項(xiàng)目的緩存系統(tǒng)可以使用 CACHES 設(shè)置來配置。這個(gè)設(shè)置是一個(gè)字典,讓你可以指定多個(gè)緩存的配置。每個(gè) CACHES 字典中的緩存可以指定下列數(shù)據(jù):

  • BACKEND:使用的緩存后端。

  • KEY_FUNCTION:包含一個(gè)指向回調(diào)函數(shù)的點(diǎn)路徑的字符,這個(gè)函數(shù)以prefix(前綴)、verision(版本)、和 key (鍵) 作為參數(shù)并返回最終緩存鍵(cache key)。

  • KEY_PREFIX:一個(gè)用于所有緩存鍵的字符串,避免沖突。

  • LOCATION:緩存的位置?;谀愕木彺婧蠖?,這可能是一個(gè)路徑、一個(gè)主機(jī)和端口,或者是內(nèi)存中后端的名字。

  • OPTIONS:任何額外的傳遞向緩存后端的參數(shù)。

  • TIMEOUT:默認(rèn)的超時(shí)時(shí)間,以秒為單位,用于儲存緩存鍵。默認(rèn)設(shè)置是 300 秒,也就是五分鐘。如果把它設(shè)置為 None ,緩存鍵將不會過期。

  • VERSION:默認(rèn)的緩存鍵的版本。對于緩存版本是很有用的。

把 memcached 添加進(jìn)你的項(xiàng)目

讓我們?yōu)槲覀兊捻?xiàng)目配置緩存。編輯 educa 項(xiàng)目的 settings.py 文件,添加以下代碼:

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

我們正在使用 MemcachedCache 后端。我們使用 address:port 標(biāo)記指定了它的位置。如果你有多個(gè) memcached 實(shí)例,你可以在 LOCATION 中使用列表。

監(jiān)控緩存

這里有一個(gè)第三方包叫做 django-memcached-status ,它可以在管理站點(diǎn)展示你的項(xiàng)目的 memcached 實(shí)例的統(tǒng)計(jì)數(shù)據(jù)。為了兼容 Python3(譯者夜夜月注:python3大法好。) ,從下面的分支中安裝它:

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

編輯 settings.py ,然后把 memcached_status 添加進(jìn) INSTALLED_APPS 設(shè)置中。確保 memcached 正在運(yùn)行,在另外一個(gè) shell 中打開開發(fā)服務(wù)器,然后訪問 http://127.0.0.1:8000/adim/ ,使用超級用戶登錄進(jìn)管理站點(diǎn),你就可以看到如下的區(qū)域:

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

這張圖片展示了緩存使用。綠色代表了空閑的緩存,紅色的表示使用了的空間。如果你點(diǎn)擊方框的標(biāo)題,它展示了你的 memcached 實(shí)例的統(tǒng)計(jì)詳情。

我們已經(jīng)為項(xiàng)目安裝好了 memcached 并且可以監(jiān)控它。讓我們開始緩存數(shù)據(jù)吧!

緩存級別

Django 提供了以下幾個(gè)級別按照顆粒度上升的緩存排列:

  • Low-level cache API:提供了最高顆粒度。允許你緩存具體的查詢或計(jì)算結(jié)果。

  • Per-view cache:提供單一視圖的緩存。

  • Template cache:允許你緩存模板片段。

  • Per-site cache:最高級的緩存。它緩存你的整個(gè)網(wǎng)站。

在你執(zhí)行緩存之請仔細(xì)考慮下緩存策略。首先要考慮那些不是單一用戶為基礎(chǔ)的查詢和計(jì)算開銷

使用 low-level cache API (低級緩存API)

低級緩存 API 讓你可以緩存任意顆粒度的對象。它位于 django.core.cache 。你可以像這樣導(dǎo)入它:

from django.core.cache import cache

這使用的是默認(rèn)的緩存。它相當(dāng)于 caches['default'] 。通過它的別名來連接一個(gè)特定的緩存也是可能的:

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

讓我們看看緩存 API 是如何工作的。使用命令 python manage.py shell 打開 shell 然后執(zhí)行下面的代碼:

>>> from django.core.cache import cache>>> cache.set('musician', 'Django Reinhardt', 20)

我們連接的是默認(rèn)的緩存后端,使用 set{key,value, timeout} 來保存一個(gè)名為 musician 的鍵和它的為字符串 Django Reinhardt 的值 20 秒鐘。如果我們不指定過期時(shí)間,Django 會使在 CACHES 設(shè)置中緩存后端的默認(rèn)過期時(shí)間。現(xiàn)在執(zhí)行下面的代碼:

>>> cache.get('musician')'Django Reinhardt'

我們在緩存中檢索鍵。等待 20 秒然后指定相同的代碼:

>>> cache.get('musician')None

musician 緩存鍵已經(jīng)過期了,get() 方法返回了 None 因?yàn)殒I已經(jīng)不在緩存中了。

在緩存鍵中要避免儲存 None 值,因?yàn)檫@樣你就無法區(qū)分緩存值和緩存過期了

讓我們緩存一個(gè)查詢集:

>>> from courses.models import Subject>>> subjects = Subject.objects.all()>>> cache.set('all_subjects', subjects)

我們執(zhí)行了一個(gè)在 Subject 模型上的查詢集,然后把返回的對象儲存在 all_subjects 鍵中。讓我們檢索一下緩存數(shù)據(jù):

>>> cache.get('all_subjects')[<Subject: Mathematics>, <Subject: Music>, <Subject: Physics>,
<Subject: Programming>]

我們將會在視圖中緩存一些查詢集。編輯 courses 應(yīng)用的 views.py ,添加以下 導(dǎo)入:

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_students 鍵。如果所給的鍵沒有找到,返回的是 None 。如果鍵沒有被找到(沒有被緩存,或者緩存了但是過期了),我們就執(zhí)行查詢來檢索所有的 Subject 對象和它們課程的數(shù)量,我們使用 cache.set() 來緩存結(jié)果。

打開代發(fā)服務(wù)器,訪問 http://127.0.0.1:8000 。當(dāng)視圖被執(zhí)行的時(shí)候,緩存鍵沒有被找到的話查詢集就會被執(zhí)行。訪問http://127.0.0.1:8000/adim/ 然后打開 memcached 統(tǒng)計(jì)。你可以看到類似于下面的緩存的使用數(shù)據(jù):

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

看一眼 Curr Items 應(yīng)該是 1 。這表示當(dāng)前有一個(gè)內(nèi)容被緩存。Get Hits 表示有多少的 get 操作成功了,Get Miss表示有多少的請求丟失了。Miss Ratio 是使用它們倆來計(jì)算的。

現(xiàn)在導(dǎo)航回 http://127.0.0.1:8000/ ,重載頁面幾次。如果你現(xiàn)在看緩存統(tǒng)計(jì)的話,你就會看到更多的(Get HitsCmd Get被執(zhí)行了)

基于動態(tài)數(shù)據(jù)的緩存

有很多時(shí)候你都會想使用基于動態(tài)數(shù)據(jù)的緩存的。基于這樣的情況,你必須要?jiǎng)?chuàng)建包含所有要求信息的動態(tài)鍵來特別定以緩存數(shù)據(jù)。編輯 courses 應(yīng)用的 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})

在這個(gè)場景中,我們把課程和根據(jù)科目篩選的課程都緩存了。我們使用 all_courses 緩存鍵來儲存所有的課程,如果沒有給科目的話。如果給了一個(gè)科目的話我們就用 'subject_()_course'.format(subject.id)動態(tài)的創(chuàng)建緩存鍵。

注意到我們不能用一個(gè)緩存查詢集來創(chuàng)建另外的查詢集是很重要的,因?yàn)槲覀円呀?jīng)緩存了當(dāng)前的查詢結(jié)果,所以我們不能這樣做:

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

相反,我們需要?jiǎng)?chuàng)建基本的查詢集 Course.objects.annotate(total_modules=Count('modules')) ,它不會被執(zhí)行除非你強(qiáng)制執(zhí)行它,然后用它來更進(jìn)一步的用 all_courses.filter(subject=subject) 限制查詢集萬一數(shù)據(jù)沒有在緩存中找到的話。

緩存模板片段

緩存模板片段是一個(gè)高級別的方法。你需要使用 {% load cache %} 在模板中載入緩存模板標(biāo)簽。然后你就可以使用 {% cache %}模板標(biāo)簽來緩存特定的模板片段了。你通常可以像下面這樣使用緩存標(biāo)簽:

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

{% cache %} 標(biāo)簽要求兩個(gè)參數(shù):過期時(shí)間,以秒為單位,和一個(gè)片段名稱。如果你需要緩存基于動態(tài)數(shù)據(jù)的內(nèi)容,你可以通過傳遞額外的參數(shù)給 {% cache %} 模板標(biāo)簽來特別的指定片段。

編輯 students 應(yīng)用的 /students/course/detail.html 。在頂部添加以下代碼,就在 {% extends %} 標(biāo)簽的后面:

{% load cache %}

然后把下面幾行:

{% for content in module.contents.all %}
    {% with item=content.item %}        <h2>{{ item.title }}</h2>
        {{ item.render }}
    {% endwith %}
{% endfor %}

替換為:

{% cache 600 module_contents module %}
    {% for content in module.contents.all %}
        {% with item=content.item %}            <h2>{{ item.title }}</h2>
            {{ item.render }}
        {% endwith %}
    {% endfor %}
{% endcache %}

我們使用名字 module_contents 和傳遞當(dāng)前的 Module 對象來緩存模板片段。這對于當(dāng)請求不同的模型是避免緩存一個(gè)模型的內(nèi)容和服務(wù)錯(cuò)誤的內(nèi)容來說是很重要的。

如果 USE_I18N 設(shè)置是為 True,單一站點(diǎn)的中間件緩存將會遵照當(dāng)前激活的語言。如果你使用了 {% cache %} 模板標(biāo)簽以及可用翻譯特定的變量中的一個(gè),那么他們的效果將會是一樣的,比如:{% cache 600 name request.LANGUAGE_CODE %}

緩存視圖

你可以使用位于 django.views.decrators.cache 的 cache_page 裝飾器來煥春輸出的單個(gè)視圖。裝飾器要求一個(gè)過期時(shí)間的參數(shù)(以秒為單位)。

讓我們在我們的視圖中使用它。編輯 students 應(yīng)用的 urls.py ,添加以下 導(dǎo)入:

from django.views.decorators.cache import cache_page

然后按照如下在 student_course_detail_module URL 模式上應(yīng)用 cache_page 裝飾器:

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

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

現(xiàn)在 StudentCourseDetailView 的結(jié)果就會被緩存 15 分鐘了。

單一的視圖緩存使用 URL 來創(chuàng)建緩存鍵。多個(gè)指向同一個(gè)視圖的 URLs 將會被分開儲存

使用單一站點(diǎn)緩存

這是最高級的緩存。他讓你可以緩存你的整個(gè)站點(diǎn)。

為了使用單一站點(diǎn)緩存,你需要編輯項(xiàng)目中的 settings.py ,把 UpdateCacheMiddleware 和 FetchFromCacheMiddleware 添加進(jìn) MIDDLEWARE_CLASSES 設(shè)置中:

MIDDLEWARE_CLASSES = (    'django.contrib.sessions.middleware.SessionMiddleware',    'django.middleware.cache.UpdateCacheMiddleware',    'django.middleware.common.CommonMiddleware',    'django.middleware.cache.FetchFromCacheMiddleware',    'django.middleware.csrf.CsrfViewMiddleware',    # ...)

記住在請求的過程中,中間件是按照所給的順序來執(zhí)行的,在相應(yīng)過程中是逆序執(zhí)行的。UpdateCacheMiddleware 被放在 CommonMiddleware 之前,因?yàn)樗谙鄳?yīng)時(shí)才執(zhí)行,此時(shí)中間件是逆序執(zhí)行的。FetchFromCacheMiddleware 被放在 CommonMiddleware 之后,是因?yàn)樗枰B接后者的的請求數(shù)據(jù)集。

然后,把下列設(shè)置添加進(jìn) settings.py 文件:

CACHE_MIDDLEWARE_ALIAS = 'default'CACHE_MIDDLEWARE_SECONDS = 60 * 15 # 15 minutesCACHE_MIDDLEWARE_KEY_PREFIX = 'educa'

在這些設(shè)置中我們?yōu)橹虚g件使用了默認(rèn)的緩存,然后我們把全局緩存過期時(shí)間設(shè)置為 15 分鐘。我們也指定了所有的緩存鍵前綴來避免沖突萬一我們?yōu)槎鄠€(gè)項(xiàng)目使用了相同的 memcached 后端。我們的站點(diǎn)現(xiàn)在將會為所有的 GET 請求緩存以及返回緩存內(nèi)容。

我們已經(jīng)完成了這個(gè)來測試單一站點(diǎn)緩存功能。盡管,以站點(diǎn)的緩存對于我們來說是不怎么合適的,因?yàn)槲覀兊恼n程管理視圖需要展示更新數(shù)據(jù)來實(shí)時(shí)的給出任何的修改。我們項(xiàng)目中的最好的方法是緩存用于展示給學(xué)生的課程內(nèi)容的模板或者視圖數(shù)據(jù)。

我們已經(jīng)大致體驗(yàn)過了 Django 提供的方法來緩存數(shù)據(jù)。你應(yīng)合適的定義你自己的緩存策略,優(yōu)先考慮開銷最大的查詢集或者計(jì)算。

總結(jié)

在這一章中,我們創(chuàng)建了一個(gè)用于課程的公共視圖,創(chuàng)建了一個(gè)用于學(xué)生注冊和報(bào)名課程的系統(tǒng)。我們安裝了 memcached 以及實(shí)現(xiàn)了不同級別的緩存。

在下一章中,我們將會為你的項(xiàng)目創(chuàng)建 RESTful API。

(譯者 @夜夜月注:終于只剩下最后一章了!)