第二章 为博客添加高级功能

帝国CMS在列表页中,只要信息没有标题图片,就不显示,有标题图片才显示,这样没标题图片的情况下,就不会显示一个X框框。

5 分享内容到你的网站

上一章中,你在网站中构建了用户注册和认证。你学会了如何为用户创建自定义的个人资料模型,并添加了主流社交网站的社交认证。

在这一章中,你会学习如何创建JavaScript书签工具,来从其它网站分享内容到你的网站,你还会使用jQuery和Django实现AJAX特性。

本章会覆盖以下知识点:

  • 创建多对多的关系
  • 定制表单行为
  • 在Django中使用jQuery
  • 构建jQuery书签工具
  • 使用sorl-thumbnail生成图片缩略图
  • 实现AJAX视图,并与jQuery集成
  • 为视图创建自定义装饰器
  • 构建AJAX分页

2 为博客添加高级功能

上一章中,你创建了一个基础的博客应用。现在,利用一些高级特性,你要把它打造成一个功能完整的博客,比如通过邮件分享帖子,添加评论,为帖子打上标签,以及通过相似度检索帖子。在这一章中,你会学习以下主题:

  • 使用Django发送邮件
  • 在视图中创建和处理表单
  • 通过模型创建表单
  • 集成第三方应用
  • 构造复杂的QuerySet

list.var中勾选 使用程序代码

5.1 创建图片标记网站

我们将允许用户在其他网站上标记和分享他们发现的图片,并将其分享到我们的网站。为了实现这个功能,我们需要完成以下任务:

  1. 定义一个存储图片和图片信息的模型。
  2. 创建处理图片上传的表单和视图。
  3. 为用户构建一个系统,让用户可以上传在其它网站找到的图片。

首先在bookmarks项目目录中,使用以下命令创建一个新的应用:

django-admin startapp images

settings.py文件的INSTALLED_APPS设置中添加images

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

现在Django知道新应用已经激活了。

2.1 通过邮件分享帖子

首先,我们将会允许用户通过邮件分享帖子。花一点时间想想,通过上一章学到的知识,你会如何使用视图,URL和模板来完成这个功能。现在核对一下,允许用户通过邮件发送帖子需要完成哪些操作:

  • 为用户创建一个填写名字,邮箱,收件人和评论(可选的)的表单
  • views.py中创建一个视图,用于处理post数据和发送邮件
  • blog应用的urls.py文件中,为新视图添加URL模式
  • 创建一个显示表单的模板

方法一:

5.1.1 创建图片模型

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

from django.db import models
from django.conf import settings

class Image(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='images_created')
    title = models.CharField(max_length=200)
    slug = models.CharField(max_length=200, blank=True)
    url = models.URLField()
    image = models.ImageField(upload_to='%Y/%m/%d')
    description = models.TextField(blank=True)
    created = models.DateField(auto_now_add=True, db_index=True)

    def __str__(self):
        return self.title

我们将使用这个模型存储来自不同网站的被标记的图片。让我们看看这个模型中的字段:

  • user:标记这张图片的User对象。这是一个ForeignKey字段,它指定了一对多的关系:一个用户可以上传多张图片,但一张图片只能由一个用户上传。
  • title:图片的标题。
  • slug:只包括字母,数据,下划线或连字符的短标签,用于构建搜索引擎友好的URL。
  • url:图片的原始URL。
  • image:图片文件。
  • description:一个可选的图片描述。
  • created:在数据库中创建对象的时间。因为我们使用了auto_now_add,所以创建对象时会自动设置时间。我们使用了db_index=True,所以Django会在数据库中为该字段创建一个索引。

数据库索引会提高查询效率。考虑为经常使用filter()exclude()order_by()查询的字段设置db_index=TrueForeignKey字段或带unique=True的字段隐式的创建了索引。你也可以使用Meta.index_together为多个字段创建索引。

我们会覆写Image模型的save()方法,根据title字段的值自动生成slug字段。在Image模型中导入slugify()函数,并添加save()方法,如下所示:

from django.utils.text import slugify

class Image(models.Model):
    # ...
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
            super().save(*args, **kwargs)

没有提供别名(slug)时,我们根据给定的标题,使用Django提供的slufigy()函数自动生成图片的slug字段。然后保存对象。我们为图片自动生成别名,所以用户不需要为每张图片输入slug字段。

2.1.1 使用Django创建表单

让我们从创建分享帖子的表单开始。Django有一个内置的表单框架,让你很容易的创建表单。表单框架允许你定义表单的字段,指定它们的显示方式,以及如何验证输入的数据。Django的表单框架还提供了一种灵活的方式,来渲染表单和处理数据。

Django有两个创建表单的基础类:

  • Form:允许你创建标准的表单
  • ModelForm:允许你通过创建表单来创建或更新模型实例

首先,在blog应用目录中创建forms.py文件,添加以下代码:

from django import forms

class EmailPostForm(forms.Form):
    name = forms.CharField(max_length=25)
    email = forms.EmailField()
    to = forms.EmailField()
    comments = forms.CharField(required=False, 
                               widget=forms.Textarea)

这是你的第一个Django表单。这段代码通过继承基类Form创建了一个表单。我们使用不同的字段类型,Django可以相应的验证字段。

表单可以放在Django项目的任何地方,但惯例是放在每个应用的forms.py文件中。

name字段是一个CharField。这种字段的类型渲染为<input type="text">
HTML元素。每种字段类型都有一个默认组件,决定了该字段如何在HTML中显示。可以使用widget属性覆盖默认组件。在comments字段中,我们使用Textarea组件显示为<textarea>
HTML元素,而不是默认的<input>元素。

字段的验证也依赖于字段类型。例如,emailto字段是EmailField。这两个字段都要求一个有效的邮箱地址,否则字段验证会抛出forms.ValidationError异常,导致表单无效。表单验证时,还会考虑其它参数:我们定义name字段的最大长度为25个字符,并使用required=Falsecomments字段是可选的。字段验证时,这些所有因素都会考虑进去。这个表单中使用的字段类型只是Django表单字段的一部分。在这里查看所有可用的表单字段列表。

没有标题图片时显示指定图片:

5.1.2 创建多对多的关系

我们将会在Image模型中添加另一个字段,用于存储喜欢这张图片的用户。这种情况下,我们需要一个多对多的关系,因为一个用户可能喜欢多张图片,每张图片也可能被多个用户喜欢。

添加以下代码到Image模型中:

users_like = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                    related_name='images_liked',
                                    blank=True)

当你定义一个ManyToManyFeild时,Django会使用两个模型的主键创建一张中介连接表。ManyToManyFeild可以在两个关联模型的任何一个中定义。

ForeignKey字段一样,ManyToManyFeild允许我们命名从关联对象到这个对象的逆向关系。ManyToManyFeild字段提供了一个多对多管理器,允许我们检索关联的对象,比如:image.users_like.all(),或者从user对象检索:user.images_liked.all()

打开命令行,执行以下命令创建初始数据库迁移:

python manage.py makemigrations images

你会看到以下输出:

Migrations for 'images':
  images/migrations/0001_initial.py
    - Create model Image

现在运行这条命令,让迁移生效:

python manage.py migrate images

你会看到包括这一行的输出:

Applying images.0001_initial... OK

现在Image模型已经同步到数据库中。

2.1.2 在视图中处理表单

你需要创建一个新视图,用于处理表单,以及提交成功后发送一封邮件。编辑blog应用的views.py文件,添加以下代码:

from .forms import EmailPostForm

def post_share(request, post_id):
    # Retrieve post by id
    post = get_object_or_404(Post, id=post_id, status='published')

    if request.method == 'POST':
        # Form was submitted
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # Form fields passed validation
            cd = form.cleaned_data
            # ... send email
    else:
        form = EmailPostForm()
    return render(request, 
                    'blog/post/share.html', 
                    {'post': post, 'form': form})

该视图是这样工作的:

  • 我们定义了post_share视图,接收request对象和post_id作为参数。
  • 我们通过ID,使用get_object_or_404()快捷方法检索状态为published的帖子。
  • 我们使用同一个视图=显示初始表单和处理提交的数据。根据request.method区分表单是否提交。我们将使用POST提交表单。如果我们获得一个GET请求,需要显示一个空的表单;如果获得一个POST请求,表单会被提交,并且需要处理它。因此,我们使用request.method == 'POST'来区分这两种场景。

以下是显示和处理表单的过程:

  1. 当使用GET请求初始加载视图时,我们创建了一个新的表单实例,用于在模板中显示空表单。

form = EmailPostForm()

  1. 用户填写表单,并通过POST提交。接着,我们使用提交的数据创建一个表单实例,提交的数据包括在request.POST中:

if request.POST == 'POST':
    # Form was submitted
    form = EmailPostForm(request.POST)
  1. 接着,我们使用表单的is_valid()方法验证提交的数据。该方法会验证表单中的数据,如果所有字段都是有效数据,则返回True。如果任何字段包含无效数据,则返回False。你可以访问form.errors查看验证错误列表。
  2. 如果表单无效,我们使用提交的数据在模板中再次渲染表单。我们将会在模板中显示验证错误。
  3. 如果表单有效,我们访问form.cleaned_data获得有效的数据。该属性是表单字段和值的字典。

如果你的表单数据无效,cleaned_data只会包括有效的字段。

现在,你需要学习如何使用Django发送邮件,把所有功能串起来。

if(empty($r[titlepic])){$r[titlepic]=/images/img.gif;}$listtemp=liahref=!--titleurl--]imgsrc=!--titlepic--]/a/li;

5.1.3 在管理站点注册图片模型

编辑images应用的admin.py文件,在管理站点注册Image模型,如下所示:

from django.contrib import admin
from .models import Image

class ImageAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'image', 'created')
    list_filter = ['created']

admin.site.register(Image, ImageAdmin)

执行python manage.py runserver命令启动开发服务器。在浏览器中打开http://127.0.0.1:8000/amdin/,可以看到Image模型已经在管理站点注册,如下图所示:

2.1.3 使用Django发送邮件

使用Django发送邮件非常简单。首先,你需要一个本地SMTP服务,或者在项目的settings.py文件中添加以下设置,定义一个外部SMTP服务的配置:

  • EMAIL_HOST:SMTP服务器地址。默认是localhost
  • EMAIL_PORT:SMTP服务器端口,默认25。
  • EMAIL_HOST_USER:SMTP服务器的用户名。
  • EMAIL_HOST_PASSWORD:SMTP服务器的密码。
  • EMAIL_USE_TLS:是否使用TLS加密连接。
  • EMAIL_USE_SSL:是否使用隐式TLS加密连接。

如果你没有本地SMTP服务,可以使用你的邮箱提供商的SMTP服务。下面这个例子中的配置使用Google账户发送邮件:

EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'your_account@gmail.com'
EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_PORT = 587
EMAIL_USE_TLS = True

运行python manage.py shell命令打开Python终端,如下发送邮件:

>>> from django.core.mail import send_mail
>>> send_mail('Django mail', 'This e-mail was sent with Django',
'your_account@gmail.com', ['your_account@gmail.com'], 
fail_silently=False)

send_mail()的必填参数有:主题,内容,发送人,以及接收人列表。通过设置可选参数fail_silently=False,如果邮件不能正确发送,就会抛出异常。如果看到输出1,则表示邮件发送成功。如果你使用前面配置的Gmail发送邮件,你可能需要在这里启用低安全级别应用访问权限。

现在,我们把它添加到视图中。编辑blog应用中views.py文件的post_share视图,如下所示:

from django.core.mail import send_mail

def post_share(request, post_id):
    # Retrieve post by id
    post = get_object_or_404(Post, id=post_id, status='published')
    sent = False

    if request.method == 'POST':
        # Form was submitted
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # Form fields passed validation
            cd = form.cleaned_data
            post_url = request.build_absolute_uri(post.get_absolute_url())
            subject = '{} ({}) recommends you reading "{}"'.format(cd['name'], cd['email'], post.title)
            message = 'Read "{}" at {}nn{}'s comments: {}'.format(post.title, post_url, cd['name'], cd['comments'])
            send_mail(subject, message, 'admin@blog.com', [cd['to']])
            sent = True
    else:
        form = EmailPostForm()
    return render(request, 
                   'blog/post/share.html', 
                   {'post': post, 'form': form, 'sent': sent}) 

注意,我们声明了一个sent变量,当帖子发送后,设置为True。当表单提交成功后,我们用该变量在模板中显示一条成功的消息。因为我们需要在邮件中包含帖子的链接,所以使用了get_absolute_url()方法检索帖子的绝对路径。我们把这个路径作为request.build_absolute_uri()的输入,构造一个包括HTTP模式(schema)和主机名的完整URL。我们使用验证后的表单数据构造邮件的主题和内容,最后发送邮件到表单to字段中的邮件地址。

现在,视图的开发工作已经完成,记得为它添加新的URL模式。打开blog应用的urls.py文件,添加post_share的URL模式:

urlpatterns = [
    # ...
    url(r'^(?P<post_id>d+)/share/$', views.post_share, name='post_share'),
]

说明:$r[titlepic]为标题图片字段变量。$listtemp为模板内容变量。

5.2 从其它网站上传内容

我们将允许用户标记从其它网站找到的图片。用户将提供图片的URL,一个标题和一个可选的描述。我们的应用会下载图片,并在数据库中创建一个新的Image对象。

我们从构建一个提交新图片的表单开始。在images应用目录中创建forms.py文件,并添加以下代码:

from django import forms
from .models import Image

class ImageCreateForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ('title', 'url', 'description')
        widgets = {
            'url': forms.HiddenInput,
        }

正如你所看到的,这是一个从Image模型创建的ModelForm表单,只包括titleurldescription字段。用户不会直接在表单中输入图片URL。而是使用一个JavaScript工具,从其它网站选择一张图片,我们的表单接收这张图片的URL作为参数。我们用HiddenInput组件覆盖了url字段的默认组件。这个组件渲染为带有type="hidden"属性的HTML输入元素。使用这个组件是因为我们不想用户看见这个字段。

2.1.4 在模板中渲染表单

完成创建表单,编写视图和添加URL模式后,我们只缺少该视图的模板了。在blog/templates/blog/post/目录中创建share.html文件,添加以下代码:

{% extends "blog/base.html" %}

{% block title %}Share a post{% endblock %}

{% block content %}
    {% if sent %}
        <h1>E-mail successfully sent</h1>
        <p>
            "{{ post.title }}" was successfully sent to {{ cd.to }}.
        </p>
    {% else %}
        <h1>Share "{{ post.title }}" by e-mail</h1>
        <form action="." method="post">
            {{ form.as_p }}
            {% csrf_token %}
            <input type="submit" value="Send e-mail">
        </form>
    {% endif %}
{% endblock %}

这个模板用于显示表单,或者表单发送后的一条成功消息。正如你所看到的,我们创建了一个HTML表单元素,指定它需要使用POST方法提交:

<form action="." method="post">

然后,我们包括了实际的表单实例。我们告诉Django使用as_p方法,在HTML的<p>元素中渲染表单的字段。我们也可以使用as_ul把表单渲染为一个无序列表,或者使用as_table渲染为HTML表格。如果你想渲染每一个字段,我们可以这样迭代字段:

{% for field in form %}
    <div>
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
    </div>
{% endfor %}

模板标签{% csrf_token %}使用自动生成的令牌引入一个隐藏字段,以避免跨站点请求伪造(CSRF)的攻击。这些攻击包含恶意网站或程序,对你网站上的用户执行恶意操作。你可以在这里找到更多相关的信息。

上述标签生成一个类似这样的隐藏字段:

<input type="hidden" name="csrfmiddlewaretoken" value="26JjKo2lcEtYkGoV9z4XmJIEHLXN5LDR" />

默认情况下,Django会检查所有POST请求中的CSRF令牌。记得在所有通过POST提交的表单中包括csrf_token标签。

编辑blog/post/detail.html模板,在{{ post.body|linebreaks }}变量之后添加链接,用于分享帖子的URL:

<p>
    <a href="{%%20url%20"blog:post_share" post.id %}">
        Share this post
    </a>
</p>

记住,我们使用Django提供的{%%20url%20%}模板标签,动态生成URL。我们使用名为blog命名空间和名为post_share的URL,并传递帖子ID作为参数来构造绝对路径的URL。

现在,使用python manage.py runserver命令启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/blog/。点击任何一篇帖子的标题,打开详情页面。在帖子正文下面,你会看到我们刚添加的链接,如下图所示:

点击Share this post,你会看到一个包含表单的页面,该页面可以通过邮件分享帖子。如下图所示:

该表单的CSS样式在static/css/blog.css文件中。当你点击Send e-mail按钮时,该表单会被提交和验证。如果所有字段都是有效数据,你会看到一条成功消息,如下图所示:

如果你输入了无效数据,会再次渲染表单,其中包括了所有验证错误:

译者注:不知道是因为浏览器不同,还是Django的版本不同,这里显示的验证错误跟原书中不一样。我用的是Chrome浏览器。

也不一定是标题图片,其它字段也可以。

5.2.1 清理表单字段

为了确认提供的图片URL是有效的,我们会检查文件名是否以.jpg.jpeg扩展名结尾,只允许JPG文件。Django允许你通过形如clean_<filedname>()的方法,定义表单方法来清理指定字段。如果存在这个方法,它会在调用表单实例的is_valid()方法时执行。在清理方法中,你可以修改字段的值,或者需要时,为这个字段抛出任何验证错误。在ImageCreateForm中添加以下方法:

def clean_url(self):
    url = self.cleaned_data['url']
    valid_extensions = ['jpg', 'jpeg']
    extension = url.rsplit('.', 1)[1].lower()
    if extension not in valid_extensions:
        raise forms.ValidationError('The given URL does not match valid image extensions.')
    return url

我们在这段代码中定义了clean_url()方法来清理url字段。它是这样工作的:

  1. 从表单示例的cleaned_data字典中获得url字段的值。
  2. 通过分割URL获得文件扩展名,并检查是否为合法的扩展名。如果不是,抛出ValidationError,表单实例不会通过验证。我们执行了一个非常简单的验证。你可以使用更好的方法验证给定的URL是否提供了有效的图片。

除了验证给定的URL,我们还需要下载并保存图片。比如,我们可以用处理这个表单的视图来下载图片文件。不过我们会使用更通用的方式:覆写模型表单的save()方法,在每次保存表单时执行这个任务。

2.2 创建评论系统

现在,我们开始为博客构建评论系统,让用户可以评论帖子。要构建评论系统,你需要完成以下工作:

  • 创建一个保存评论的模型
  • 创建一个提交表单和验证输入数据的表单
  • 添加一个视图,处理表单和保存新评论到数据库中
  • 编辑帖子详情模板,显示评论列表和添加新评论的表单

首先,我们创建一个模型存储评论。打开blog应用的models.py文件,添加以下代码:

class Comment(models.Model):
    post = models.ForeignKey(Post, related_name='comments')
    name = models.CharField(max_length=80)
    email = models.EmailField()
    body = models.TextField()
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    active = models.BooleanField(default=True)

    class Meta:
        ordering = ('created', )

    def __str__(self):
        return 'Comment by {} on {}'.format(self.name, self.post)

这就是我们的Comment模型。它包含一个外键,把评论与单篇帖子关联在一起。这个多对一的关系在Comment模型中定义,因为每条评论对应一篇帖子,而每篇帖子可能有多条评论。从关联对象反向到该对象的关系由related_name属性命名。定义这个属性后,我们可以使用comment.post检索评论对象的帖子,使用post.comments.all()检索帖子的所有评论。如果你没有定义related_name属性,Django会使用模型名加_set(即comment_set)命名关联对象反向到该对象的管理器。

你可以在这里学习更多关于多对一的关系。

我们使用了active布尔字段,用于手动禁用不合适的评论。我们使用created字段排序评论,默认按时间排序。

刚创建的Comment模型还没有同步到数据库。运行以下命令,生成一个新的数据库迁移,反射创建的新模型:

python manage.py makemigrations blog

你会看到以下输出:

Migrations for 'blog'
  0002_comment.py:
    - Create model Comment

Django在blog应用的migrations/目录中生成了0002_comment.py文件。现在,你需要创建一个相关的数据库架构,并把这些改变应用到数据库中。运行以下命令,让已存在的数据库迁移生效:

python manage.py migrate

你会得到一个包括下面这一行的输出:

Apply blog.0002_comment... OK

我们刚创建的数据库迁移已经生效,数据库中已经存在一张新的blog_comment表。

现在我们可以添加新的模型到管理站点,以便通过简单的界面管理评论。打开blog应用的admin.py文件,导入Comment模型,并增加CommentAdmin类:

from .models import Post, Comment

class CommentAdmin(admin.ModelAdmin):
    list_display = ('name', 'email', 'post', 'created', 'active')
    list_filter = ('active', 'created', 'updated')
    search_fields = ('name', 'email', 'body')
admin.site.register(Comment, CommentAdmin)

使用python manage.py runserver命令启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/admin/。你会在Blog中看到新的模型,如下图所示:

我们的模型已经在管理站点注册,并且可以使用简单的界面管理Comment实例。

示例代码,eweima 字段

5.2.2 覆写ModelForm的save()方法

你知道,ModelForm提供了save()方法,用于把当前模型的实例保存到数据库中,并返回该对象。这个方法接收一个commit布尔参数,允许你指定是否把该对象存储到数据库中。如果commitFalsesave()方法会返回模型的实例,但不会保存到数据库中。我们会覆写表单的save()方法下载指定的图片,然后保存。

forms.py文件顶部添加以下导入:

from urllib import request
from django.core.files.base import ContentFile
from django.utils.text import slugify

接着在ImageCreateForm中添加save()方法:

def save(self, force_insert=False, force_update=False, commit=True):
    image = super().save(commit=False)
    image_url = self.cleaned_data['url']
    image_name = '{}.{}'.format(slugify(image.title), image_url.rsplit('.', 1)[1].lower())

    #download image from the given URL
    response = request.urlopen(image_url)
    image.image.save(image_name, ContentFile(response.read()), save=False)

    if commit:
        image.save()
    return image

我们覆写了save()方法,保留了ModelForm必需的参数。这段代码完成以下操作:

  1. 我们用commit=False调用表单的save()方法,创建了一个新的image实例。
  2. 我们从表单的cleaned_data字典中获得URL。
  3. 我们用image的标题别名和原始文件扩展名的组合生成图片名。
  4. 我们使用urllib模块下载图片,然后调用image字段的save()方法,并传递一个ContentFile对象,这个对象由下载的文件内容实例化。这样就把文件保存到项目的media目录中了。我们还传递了save=False参数,避免把对象保存到数据库中。
  5. 为了与被我们覆写的save()方法保持一致的行为,只有在commit参数为True时,才把表单保存到数据库中。

现在我们需要一个处理表单的视图。编辑images应用的views.py文件,添加以下代码:

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import message
from .forms import ImageCreateForm

@login_required
def image_create(request):
    if request.method == 'POST':
        # form is sent
        form = ImageCreateForm(data=request.POST)
        if form.is_valid():
            # form data is valid
            cd = form.cleaned_data
            new_item = form.save(commit=False)

            # assign current user to the item
            new_item.user = request.user
            new_item.save()
            message.success(request, 'Image added successfully')

            # redirect to new created item detail view
            return redirect(new_item.get_absolute_url())
    else:
        # build form with data provided by the bookmarklet via GET
        form = ImageCreateForm(data=request.GET)

    return render(request, 'images/image/create.html', {'section': 'images', 'form': form})

为了阻止未认证用户访问,我们在image_create视图上添加了login_required装饰器。这个视图是这样工作的:

  1. 我们期望通过GET请求获得创建表单实例的初始数据。数据由其它网站的图片urltitle属性组成,这个数据由我们之后会创建的JavaScript工具提供。现在我们假设初始的时候有数据。
  2. 如果提交了表单,我们检查表单是否有效。如果有效,我们创建一个新的Image实例,但我们通过传递commit=False来阻止对象保存到数据库中。
  3. 我们把当前对象赋值给新的image对象。这样就知道每张图片是谁上传的。
  4. 我们把图片对象保存到数据库中。
  5. 最后,我们用Django消息框架创建一条成功消息,并重定向到新图片的标准URL。我们还没有实现Image模型的get_absolute_url()方法,我们会马上完成这个工作。

images应用中创建urls.py文件,添加以下代码:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^create/$', views.image_create, name='create'),
]

编辑项目的主urls.py文件,引入我们刚创建的images应用的模式:

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^account/', include('account.urls')),
    url(r'^images/', include('images.urls', namespace='images')),
]

最近,你需要创建模板来渲染表单。在images应用目录中创建以下目录结构:

templates/
    images/
        image/
            create.html

编辑create.hmtl文件,添加以下代码:

{% extends "base.html" %}

{% block title %}Bookmark an image{% endblock %}

{% block content %}
    <h1>Bookmark an image</h1>
    ![]({{ request.GET.url }})
    <form action="." method=POST>
        {{ form.as_p }}
        {% csrf_token %}
        <input type="submit" value="Bookmark it!">
    </form>
{% endblock %}

现在在浏览器中打开http://127.0.0.1:8000reate/?title=...&url=...,其中包括titleurl参数,后者是现有的JPG图片的URL。

例如,你可以使用以下URL:

http://127.0.0.1:8000reate/?title=%20Django%20and%20Duke&url=http%3A%2F%2Fmvimg2.meitudata.com%2F56d7967dd02951453.jpg

你会看到带一张预览图片的表单,如下所示:

添加描述并点击Bookmark it!按钮。一个新的Image对象会保存到数据库中。你会得到一个错误,显示Image对象没有get_absolute_url()方法。现在不用担心,我们之后会添加这个方法。在浏览器打开http://127.0.0.1:8000/adminimage/,确认新的图片对象已经保存了。

2.2.1 通过模型创建表单

我们仍然需要创建一个表单,让用户可以评论博客的帖子。记住,Django有两个基础类用来创建表单:FormModelForm。之前你使用了第一个,让用户可以通过邮件分享帖子。在这里,你需要使用ModelForm,因为你需要从Comment模型中动态的创建表单。编辑blog应用的forms.py文件,添加以下代码:

from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('name', 'email', 'body')

要通过模型创建表单,我们只需要在表单的Meta类中指定,使用哪个模型构造表单。Django自省模型,并动态的为我们创建表单。每种模型字段类型都有相应的默认表单字段类型。我们定义模型字段的方式考虑了表单的验证。默认情况下,Django为模型中的每个字段创建一个表单字段。但是,你可以使用fields列表明确告诉框架,你想在表单中包含哪些字段,或者使用exclude列表定义你想排除哪些字段。对应CommentForm,我们只使用nameemail,和body字段,因为用户只可能填写这些字段。

$img=divclass=a字段为空时显示这里/div;if(empty($r[eweima])){$img=divclass=adivclass=bbb!---eweima--]/div/div;}$listtemp=.$img.;

5.2.3 用jQuery构建书签工具

书签工具是保存在web浏览器中的书签,其中包括JavaScript代码,可以扩展浏览器的功能。当你点击书签,JavaScript代码会在浏览器正在显示的网页中执行。这对构建与其它网站交互的工具非常有用。

某些在线服务(比如Pinterest)实现了自己的书签工具,让用户可以在自己的平台分享其它网站的内容。我们会创建一个书签工具,让用户以类似的方式在我们的网站中分享其它网站的图片。

我们将使用jQuery构建书签工具。jQuery是一个非常流行的JavaScript框架,可以快速开发客户端的功能。你可以在官方网站进一步了解jQuery。

以下是用户如何在浏览器中添加书签工具,并使用它:

  1. 用户从你的网站中拖拽一个链接到浏览器的书签中。该链接在href属性中包含JavaScript代码。这段代码会存储在书签中。
  2. 用户导航到任意网站,并点击该书签。该书签的JavaScript代码会执行。

因为JavaScript代码会以书签的形式存储,所以之后你不能更新它。这是一个显著的缺点,但你可以实现一个简单的启动脚本解决这个问题。该脚本从URL中加载实际的JavaScript书签工具。用户会以书签的形式保存启动脚本,这样你就可以在任何时候更新书签工具的代码了。我们将采用这种方式构建书签工具。让我们开始吧。

images/templates/中创建一个bookmarklet_launcher.js模板。这是启动脚本,并添加以下代码:

(function() {
    if (window.myBookmarklet !== underfined) {
        myBookmarklet();
    }
    else {
        document.body.appendChild(document.createElement('script'))
            .src='http://127.0.0.1:8000/static/js/bookmarklet.js?r='+
            Math.floor(Math.random()*99999999999999999999);
    }
})();

这个脚本检查是否定义myBookmarklet变量,来判断书签工具是否加载。这样我们就避免了用户重复点击书签时多次加载它。如果没有定义myBookmarklet,我们通过在文档中添加<script>元素,来加载另一个JavaScript文件。这个scrip标签加载bookmarklet.js脚本,并用一个随机参数作为变量,防止从浏览器缓存中加载文件。

真正的书签工具代码位于静态文件bookmarklet.js中。这样就能更新我们的书签工具代码,而不用要求用户更新之前在浏览器中添加的书签。让我们把书签启动器添加到仪表盘页面,这样用户就可以拷贝到他们的书签中。

编辑account应用中的account/dashboard.html模板,如下所示:

{% extends "base.html" %}

{% block title %}Dashboard{% endblock %}

{% block content %}
    <h1>Dashboard</h1>

    {% with total_images_created=request.user.images_created.count %}
        <p>
            Welcome to your dashboard. 
            You have bookmarked {{ total_images_created }} image{{ total_images_created|pluralize }}.
        </p>
    {% endwith %} 

    <p>
        Drag the following button to your bookmarks toolbar to bookmark images from other websites
        → <a href="javascript:{%%20include%20"bookmarklet_launcher.js" %}" class="button">Bookmark it</a>
    </p>

    <p>
        You can also <a href="{%%20url%20"edit" %}">edit your profile</a> 
        or <a href="{%%20url%20"password_change" %}">change your password</a>.
    <p>
{% endblock %}

仪表盘显示用户现在添加书签的图片总数。我们使用{% with %}模板标签设置当前用户添加书签的图片总数为一个变量。我们还包括了一个带href属性的链接,该属性指向书签工具启动脚本。我们从bookmarklet_launcher.js模板中引入这段JavaScript代码。

在浏览器中打开http://127.0.0.1:8000/account/,你会看下如下所示的页面:

拖拽Bookmark it!链接到浏览器的书签工具栏中。

现在,在images应用目录中创建以下目录和文件:

static/
    js/
        bookmarklet.js

你在本章示例代码的images应用目录下可以找到static/css目录。拷贝css/目录到你代码的static/目录中。css/bookmarklet.css文件为我们的JavaScript书签工具提供了样式。

编辑bookmarklet.js静态文件,添加以下JavaScript代码:

(function() {
    var jquery_version = '2.1.4';
    var site_url = 'http://127.0.0.1:8000/';
    var static_url = site_url + 'static/';
    var min_width = 100;
    var min_height = 100;

    function bookmarklet(msg) {
        // Here goes our bookmarklet code
    };

    // Check if jQuery is loaded
    if(typeof window.jQuery != 'undefined') {
        bookmarklet();
    } else {
        // Check for conflicts
        var conflict = typeof window.$ != 'undefined';
        // Create the script and point to Google API
        var script = document.createElement('script');
        script.setAttribute('src', 
            'http://ajax.googleapis.com/ajax/libs/jquery/' + 
            jquery_version + '/jquery.min.js');
        // Add the script to the 'head' for processing
        document.getElementsByTagName('head')[0].appendChild(script);
        // Create a way to wait until script loading
        var attempts = 15;
        (function(){
            // Check again if jQuery is undefined
            if (typeof window.jQuery == 'undefined') {
                if(--attempts > 0) {
                    // Calls himself in a few milliseconds
                    window.setTimeout(arguments.callee, 250);
                } else {
                    // Too much attempts to load, send error
                    alert('An error ocurred while loading jQuery')
                }
            } else {
                bookmarklet();
            }
        })();
    }
})()

这是主要的jQuery加载脚本。如果当前网站已经加载了jQuery,那么它会使用jQuery;否则会从Google
CDN中加载jQuery。加载jQuery后,它会执行包含书签工具代码的bookmarklet()函数。我们还在文件顶部设置了几个变量:

  • jquery_version:要加载的jQuery版本
  • site_urlstatic_url:我们网站的主URL和各个静态文件的主URL
  • min_widthmin_height:我们的书签工具在网站中查找的图片的最小宽度和高度(单位是像素)

现在让我们实现bookmarklet()函数,如下所示:

function bookmarklet(msg) {
    // load CSS
    var css = jQuery('<link>');
    css.attr({
        rel: 'stylesheet',
        type: 'text/css',
        href: static_url + 'css/bookmarklet.css?r=' + Math.floor(Math.random()*99999999999999999999)
    });
    jQuery('head').append(css);

    // load HTML
    box_html = '<div id="bookmarklet"><a href="#" id="close">&times;</a><h1>Select an image to bookmark:</h1><div class="images"></div></div>';
    jQuery('body').append(box_html);

    // close event
    jQuery('#bookmarklet #close').click(function() {
        jQuery('#bookmarklet').remove();
    });
};

这段代码是这样工作的:

  1. 为了避免浏览器缓存,我们使用一个随机数作为参数,来加载bookmarklet.css样式表。
  2. 我们添加了定制的HTML到当前网站的<body>元素中。它由一个<div>元素组成,里面会包括在当前网页中找到的图片。
  3. 我们添加了一个事件。当用户点击我们的HTML块中的关闭链接时,我们会从文档中移除我们的HTML。我们使用#bookmarklet #close选择器查找ID为close,父元素ID为bookmarklet的HTML元素。一个jQuery选择器允许你查找多个HTML元素。一个jQuery选择器返回指定CSS选择器找到的所有元素。你可以在这里找到jQuery选择器列表。

加载CSS样式和书签工具需要的HTML代码后,我们需要找到网站中的图片。在bookmarklet()函数底部添加以下JavaScript代码:

// find images and display them
jQuery.each(jQuery('img[src$="jpg"]'), function(index, image) {
    if (jQuery(image).width() >= min_width && jQuery(image).height() >= min_height) {
        image_url = jQuery(image).attr('src');
        jQuery('#bookmarklet .images').append('<a href="#">![](' + image_url + ')</a>');
    }
});

这段代码使用img[src$="jpg"]选择器查找所有src属性以jpg结尾的<img>元素。这意味着我们查找当前网页中显示的所有JPG图片。我们使用jQuery的each()方法迭代结果。我们把尺寸大于min_widthmin_height变量的图片添加到<div class="images">容器中。

现在,HTML容器中包括可以添加标签图片。我们希望用户点击需要的图片,并为它添加标签。在bookmarklet()函数底部添加以下代码:

// when an image is selected open URL with it
jQuery('#bookmarklet .images a').click(function(e) {
    selected_image = jQuery(this).children('img').attr('src');
    // hide bookmarklet
    jQuery('#bookmarklet').hide();
    // open new window to submit the image
    window.open(site_url + 'images/create/?url=' 
        + encodeURIComponent(selected_image)
        + '&title='
        + encodeURIComponent(jQuery('title').text()),
        '_blank');
});

这段代码完成以下工作:

  1. 我们绑定一个click()事件到图片的链接元素。
  2. 当用户点击一张图片时,我们设置一个新变量——selected_image,其中包含了选中图片的URL。
  3. 我们隐藏书签工具,并在我们网站中打开一个新的浏览器窗口为新图片添加标签。传递网站的<title>元素和选中的图片URL作为GET参数。

在浏览器中随便打开一个网址(比如http://z.cn),并点击你的书签工具。你会看到一个新的白色框出现在网页上,其中显示所有尺寸大于100*100px的JPG图片,如下图所示:

因为我们使用的是Django开发服务器,它通过HTTP提供页面,所以出于浏览器安全限制,书签工具不能在HTTPS网站上工作。

如果你点击一张图片,会重定向到图片创建页面,并传递网站标题和选中图片的URL作为GET参数:

恭喜你!这是你的第一个JavaScript书签工具,并且完全集成到你的Django项目中了。

2.2.2 在视图中处理ModelForm

为了简单,我们将会使用帖子详情页面实例化表单,并处理它。编辑views.py文件,导入Comment模型和CommentForm表单,并修改post_detail视图,如下所示:

译者注:原书中是编辑models.py文件,应该是作者的笔误。

from .models import Post, Comment
from .forms import EmailPostForm, CommentForm

def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post, slug=post,
                                         status='published',
                                         publish__year=year,
                                         publish__month=month,
                                         publish__day=day)
    # List of active comments for this post
    comments = post.comments.filter(active=True)
    new_comment = None

    if request.method == 'POST':
        # A comment was posted
        comment_form = CommentForm(data=request.POST)
        if comment_form.is_valid():
            # Create Comment object but don't save to database yet
            new_comment = comment_form.save(commit=False)
            # Assign the current post to comment
            new_comment.post = post
            # Save the comment to the database
            new_comment.save()
    else:
        comment_form = CommentForm()
    return render(request, 
                     'blog/post/detail.html',
                     {'post': post,
                      'comments': comments,
                      'new_comment': new_comment,
                      'comment_form': comment_form})

让我们回顾一下,我们往视图里添加了什么。我们使用post_detail视图显示帖子和它的评论。我们添加了一个QuerySet,用于检索该帖子所有有效的评论:

comments = post.comments.filter(active=True)

我们从post对象开始创建这个QuerySet。我们在Comment模型中使用related_name属性,定义了关联对象的管理器为comments。这里使用了这个管理器。

同时,我们使用同一个视图让用户添加新评论。因此,如果视图通过GET调用,我们使用comment_form = CommentForm()创建一个表单实例。如果是POST请求,我们使用提交的数据实例化表单,并使用is_valid()方法验证。如果表单无效,我们渲染带有验证错误的模板。如果表单有效,我们完成以下操作:

  1. 通过调用表单的save()方法,我们创建一个新的Comment对象:

new_comment = comment_form.save(commit=False)

save()方法创建了一个链接到表单模型的实例,并把它存到数据库中。如果使用commit=False调用,则只会创建模型实例,而不会存到数据库中。当你想在存储之前修改对象的时候,会非常方便,之后我们就是这么做的。save()只对ModelForm实例有效,对Form实例无效,因为它们没有链接到任何模型。

  1. 我们把当前的帖子赋值给刚创建的评论:

new_comment.post = post

通过这个步骤,我们指定新评论属于给定的帖子。

  1. 最后,使用下面的代码,把新评论存到数据库中:

new_comment.save()

现在,我们的视图已经准备好了,可以显示和处理新评论了。

方法二:

5.3 为图片创建详情视图

我们将创建一个简单的详情视图,用于显示一张保存在我们网站的图片。打开images应用的views.py文件,添加以下代码:

from django.shortcuts import get_object_or_404
from .models import Image

def image_detail(request, id, slug):
    image = get_object_or_404(Image, id=id, slug=slug)
    return render(request, 'images/image/detail.html', {'section': 'images', 'image': image})

这是显示一张图片的简单视图。编辑images应用的urls.py文件,添加以下URL模式:

url(r'^detail/(?P<id>d+)/(?P<slug>[-w]+)/$', views.image_detail, name='detail'),

编辑images应用的models.py文件,在Image模型中添加get_absolute_url()方法,如下所示:

from django.core.urlresolvers import reverse

class Image(models.Model):
    # ...
    def get_absolute_url(self):
        return reverse('image:detail', args=[self.id, self.slug])

记住,为对象提供标准URL的通用方式是在模型中添加get_absolute_url()方法。

最后,在images应用的image/模板目录中创建detail.html模板,添加以下代码:

{% extends "base.html" %}

{% block title %}{{ image.title }}{% endblock %}

{% block content %}
    <h1>{{ image.title }}</h1>
    ![]({{%20image.image.url%20}})
    {% with total_likes=image.users_like.count %}
        <div class="image-info">
            <div>

                    {{ total_likes }} like{{ total_likes|pluralize }}

            </div>
            {{ image.description|linebreaks }}
        </div>
        <div class="image-likes">
            {% for user in image.users_like.all %}
                <div>
                    ![]({{ user.profile.photo.url }})
                    <p>{{ user.first.name }}</p>
                </div>
            {% empty %}
                Nobody likes this image yet.
            {% endfor %}
        </div>
    {% endwith %}
{% endblock  %}

这是显示一张添加了标签的图片的详情模板。我们使用{% with %}标签存储QuerySet的结果,这个QuerySettotal_likes变量中统计所有喜欢这张图片的用户。这样就能避免计算同一个QuerySet两次。我们还包括了图片的描述,并迭代image.users_like.all()来显示所有喜欢这张图片的用户。

使用{% width %}模板标签可以有效地阻止Django多次计算QuerySet

现在用书签工具标记一张新图片。当你上传图片后,会重定向到图片详情页面。该页面会包括一条成功消息,如下图所示:

2.2.3 在帖子详情模板中添加评论

我们已经为帖子创建了管理评论的功能。现在我们需要修改blog/post/detail.html模板,完成以下工作:

  • 为帖子显示评论总数
  • 显示评论列表
  • 显示一个表单,用户增加评论

首先,我们会添加总评论数。打开detail.html模板,在content块中添加以下代码:

{% with comments.count as total_comments %}
    <h2>
        {{ total_comments }} comment{{ total_comments|pluralize }}
    </h2>
{% endwith %}

我们在模板中使用Django
ORM执行comments.count()这个QuerySet。注意,Django模板语言调用方法时不带括号。{% with %}标签允许我们把值赋给一个变量,我们可以在{% endwith %}标签之前一直使用它。

{% with %}模板标签非常有用,它可以避免直接操作数据库,或者多次调用昂贵的方法。

我们使用了pluralize模板过滤器,根据total_comments的值决定是否显示单词comment的复数形式。模板过滤器把它们起作用变量的值作为输入,并返回一个计算后的值。我们会在第三章讨论模板过滤器。

如果值不是1,pluralize模板过滤器会显示一个“s”。上面的文本会渲染为0 comments1 comment,或者N comments。Django包括大量的模板标签和过滤器,可以帮助你以希望的方式显示信息。

现在,让我们添加评论列表。在上面代码后面添加以下代码:

{% for comment in comments %}
    <div class="comment">
        <p class="info">
            Comment {{ forloop.counter }} by {{ comment.name }}
            {{ comment.created }}
        </p>
        {{ comment.body|linebreaks }}
    </div>
{% empty %}
    <p>There are no comments yet.</p>
{% endfor %}

我们使用{% for %}模板标签循环所有评论。如果comments列表为空,显示一个默认消息,告诉用户该帖子还没有评论。我们使用{{ forloop.counter }}变量枚举评论,它包括每次迭代中循环的次数。然后我们显示提交评论的用户名,日期和评论的内容。

最后,当表单成功提交后,我们需要渲染表单,或者显示一条成功消息。在上面的代码之后添加以下代码:

{% if new_comment %}
    <h2>Your comment has been added.</h2>
{% else %}
    <h2>Add a new comment</h2>
    <form action="." method="post">
        {{ comment_form.as_p }}
        {% csrf_token %}
        <p><input type="submit" value="Add comment"></p>
    </form>
{% endif %}

代码非常简单:如果new_comment对象存在,则显示一条成功消息,因为已经创建评论成功。否则渲染表单,每个字段使用一个<p>元素,以及POST请求必需的CSRF令牌。在浏览器中打开http://127.0.0.1:8000/blog/,点击一条帖子标题,打开详情页面,如下图所示:

使用表单添加两条评论,它们会按时间顺序显示在帖子下方,如下图所示:

在浏览器中打开http://127.0.0.1:8000/admin/blog/comment/,你会看到带有刚创建的评论列表的管理页面。点击某一条编辑,不选中Active选择框,然后点击Save按钮。你会再次被重定向到评论列表,该评论的Active列会显示一个禁用图标。类似下图的第一条评论:

如果你回到帖子详情页面,会发现被删除的评论没有显示;同时也没有算在评论总数中。多亏了active字段,你可以禁用不合适的评论,避免它们在帖子中显示。

$img=dtatarget=”_blank”href=”[!–titleurl–]”imgalt=”[!–title–]”title=”[!–title–]”width=”120″height=”100″src=”[!–titlepic–]”//a/dtddaid=”newstitle”name=”newstitle”target=”_blank”href=”[!–titleurl–]”[!–title–]/a/dd;if(empty($r[titlepic])){$img=ddaid=”newstitle”name=”newstitle”target=”_blank”href=”[!–titleurl–]”[!–title–]/a/dd;}$listtemp=dl’.$img./dl;

5.4 使用sorl-thumbnail创建缩略图

现在,我们在详情页面显示原图,但是不同图片的尺寸各不相同。同时,有些图片的源文件可能很大,需要很长时间才能加载。用统一的方式显示优化图像的最好方法是生成缩略图。因此我们将使用一个名为sorl-thumbnail的Django应用。

打开终端,执行以下命令安装sorl-thumbnail

pip install sorl-thumbnail

编辑bookmarks项目的settings.py文件,把sorl.thumbnail添加到INSTALLED_APPS设置中:

接着执行以下命令同步应用和数据库:

python manage.py makemigrations thumbnail
python manage.py migrate

sorl-thumbnail应用提供了多种定义图片缩略图的方式。它提供了{% thumbnail %}模板标签,可以在模板中生成缩略图;如果你想在模型中定义缩略图,还提供自定义的ImageField。我们将使用模板标签的方式。编辑images/image/detail.html模板,把这一行代码:

![]({{%20image.image.url%20}})

替换为:

{% load thumbnail %}
{% thumbnail image.image "300" as im %}
    <a href="{{%20image.image.url%20}}">
        ![]({{ im.url }})
    </a>
{% endthumbnail %}

我们在这里定义了一张固定宽度为300像素的缩略图。用户第一次加载这个页面时,会创建一张缩略图。之后的请求会使用生成的缩略图。用python manage.py runserver启动开发服务器后,访问一张已存在的图片详情页。此时会生成一张缩略图并显示。

sorl-thumbmail应用提供了一些选项来定制缩略图,包括裁剪算法和不同的效果。如果你在生成缩略图时遇到问题,可以在设置中添加THUMBNAIL_DEBUG=True,就能查看调试信息。你可以在这里阅读sorl-thumbnail应用的完整文档。

2.3 增加标签功能

实现评论系统之后,我们准备为帖子添加标签。我们通过在项目中集成一个第三方的Django标签应用,来实现这个功能。django-taggit是一个可复用的应用,主要提供了一个Tag模型和一个管理器,可以很容易的为任何模型添加标签。你可以在这里查看它的源码。

首先,你需要通过pip安装django-taggit,运行以下命令:

pip install django-taggit

然后打开mysite项目的settings.py文件,添加taggitINSTALLED_APPS设置中:

INSTALLED_APPS = (
    # ...
    'blog',
    'taggit',
)

打开blog应用的models.py文件,添加django-taggit提供的TaggableManager管理器到Post模型:

from taggit.managers import TaggableManager

class Post(models.Model):
    # ...
    tags = TaggableManager()

tags管理器允许你从Post对象中添加,检索和移除标签。

运行以下命令,为模型改变创建一个数据库迁移:

python manage.py makemigrations blog

你会看下以下输出:

Migrations for 'blog'
  0003_post_tags.py:
    - Add field tags to post

现在,运行以下命令创建django-taggit模型需要的数据库表,并同步模型的变化:

python manage.py migrate

你会看到迁移数据库生效的输入,如下所示:

Applying taggit.0001_initial... OK
Applying taggit.0002_auto_20150616_2121... OK
Applying blog.0003_post_tags... OK

你的数据库已经为使用django-taggit模型做好准备了。使用python manage.py shell打开终端,学习如何使用tags管理器。

首先,我检索其中一个帖子(ID为3的帖子):

>>> from blog.models import Post
>>> post = Post.objects.get(id=3)

接着给它添加标签,并检索它的标签,检查是否添加成功:

>>> post.tags.add('music', 'jazz', 'django')
>>> post.tags.all()
[<Tag: jazz>, <Tag: django>, <Tag: music>]

最后,移除一个标签,并再次检查标签列表:

>>> post.tags.remove('django')
>>> post.tags.all()
[<Tag: jazz>, <Tag: music>]

这很容易,对吧?运行python manage.py runserver,再次启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/admin/taggit/tag/。你会看到taggit应用管理站点,其中包括Tag对象的列表:

导航到http://127.0.0.1:8000/admin/blog/post/,点击一条帖子编辑。你会看到,现在帖子包括一个新的Tags字段,如下图所示,你可以很方便的编辑标签:

现在,我们将会编辑博客帖子,来显示标签。打开blog/post/list.html模板,在帖子标题下面添加以下代码:

<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>

模板过滤器join与Python字符串的join()方法类似,用指定的字符串连接元素。在浏览器中打开http://127.0.0.1:8000/blog/。你会看到每篇帖子标题下方有标签列表:

现在,我们将要编辑post_list视图,为用户列出具有指定标签的所有帖子。打开blog应用的views.py文件,从django-taggit导入Tag模型,并修改post_list视图,可选的通过标签过滤帖子:

from taggit.models import Tag

def post_list(request, tag_slug=None):
    object_list = Post.published.all()
    tag = None

    if tag_slug:
        tag = get_object_or_404(Tag, slug=tag_slug)
        object_list = object_list.filter(tags__in=[tag])
        # ...

该视图是这样工作的:

  1. 该视图接收一个默认值为None的可选参数tag_slug。该参数会在URL中。
  2. 在视图中,我们创建了初始的QuerySet,检索所有已发布的帖子,如果给定了标签别名,我们使用get_object_or_404()快捷方法获得给定别名的Tag对象。
  3. 然后,我们过滤包括给定标签的帖子列表。因为这是一个多对多的关系,所以我们需要把过滤的标签放在指定列表中,在这个例子中只包含一个元素。

记住,QeurySet是懒惰的。这个QuerySet只有在渲染模板时,循环帖子列表时才会计算。

最后,修改视图底部的render()函数,传递tag变量到模板中。视图最终是这样的:

def post_list(request, tag_slug=None):
    object_list = Post.published.all()
    tag = None

    if tag_slug:
        tag = get_object_or_404(Tag, slug=tag_slug)
        object_list = object_list.filter(tags__in=[tag])

    paginator = Paginator(object_list, 3)
    page = request.GET.get('page')
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        posts = paginator.page(1)
    excpet EmptyPage:
        posts = paginator.page(paginator.num_pages)
    return render(request,
                     'blog/post/list.html',
                     {'page': page,
                      'posts': posts,
                      'tag': tag})

打开blog应用的urls.py文件,注释掉基于类PostListView的URL模式,取消post_list视图的注释:

url(r'^$', views.post_list, name='post_list'),
# url(r'^$', views.PostListView.as_view(), name='post_list'),

添加以下URL模式,通过标签列出帖子:

url(r'^tag/(?P<tag_slug>[-w]+)/$', views.post_list,
    name='post_list_by_tag'),

正如你所看到的,两个模式指向同一个视图,但是名称不一样。第一个模式不带任何可选参数调用post_list视图,第二个模式使用tag_slug参数调用视图。

因为我们使用的是post_list视图,所以需要编辑blog/post/list.hmlt模板,修改pagination使用posts参数:

{% include "pagination.html" with page=posts %}

{% for %}循环上面添加以下代码:

{% if tag %}
    <h2>Posts tagged with "{{ tag.name }}"</h2>
{% endif %}

如果用户正在访问博客,他会看到所有帖子列表。如果他通过指定标签过滤帖子,就会看到这个信息。现在,修改标签的显示方式:

<p class="tag">
    Tags:
    {% for tag in post.tags.all %}
        <a href="{%%20url%20"blog:post_list_by_tag" tag.slug %}">
            {{ tag.name }}
        </a>
    {% if not forloop.last %}, {% endif %}
    {% endfof %}
</p>

现在,我们循环一篇帖子的所有标签,显示一个自定义链接到URL,以便使用该便签过滤帖子。我们用{%%20url%20"blog:post_list_by_tag" tag.slug %}构造URL,把URL名和标签的别名作为参数。我们用逗号分隔标签。

在浏览器中打开http://127.0.0.1:8000/blog/,点击某一个标签链接。你会看到由该标签过滤的帖子列表:

就是,有标题图片与没有的情况下分别调用。

5.5 使用JQuery添加AJAX操作

现在我们将向应用中添加AJAX操作。AJAX是Asynchronous JavaScript and XML的缩写。这个术语包括一组异步HTTP请求技术。它包括从服务器异步发送和接收数据,而不用加载整个页面。尽管名字中有XML,但它不是必需的。你可以使用其它格式发送或接收数据,比如JSON,HTML或者普通文本。

我们将会在图片详情页面添加一个链接,用户点击链接表示喜欢这张图片。我们会用AJAX执行这个操作,避免加载整个页面。首先,我们需要创建一个视图,让用户喜欢或不喜欢图片。编辑images应用的views.py文件,添加以下代码:

from django.http import JsonResponse
from django.views.decorators.http import require_POST

@login_required
@require_POST
def image_like(request):
    image_id = request.POST.get('id')
    action = request.POST.get('action')
    if image_id and action:
        try:
            image = Image.objects.get(id=image_id)
            if action == 'like':
                image.users_like.add(request.user)
            else:
                image.users_like.remove(request.user)
            return JsonResponse({'status': 'ok'})
        except:
            pass
    return JsonResponse({'status': 'ko'})

我们在这个视图上使用了两个装饰器。login_required装饰阻止没有登录的用户访问这个视图;如果HTTP请求不是通过POST完成,required_ POST装饰器返回一个HttpResponseNotAllowed对象(状态码为405)。这样就只允许POST请求访问这个视图。Django还提供了required_GET装饰器,只允许GET请求,以及required_http_methods装饰器,你可以把允许的方法列表作为参数传递。

我们在这个视图中使用了两个POST参数:

  • image_id:用户执行操作的图片对象的ID。
  • action:用户希望执行的操作,我们假设为likeunlike字符串。

我们使用Django为Image模型的users_like多对多字段提供的管理器的add()remove()方法从关系中添加或移除对象。调用add()方法时,如果传递一个已经存在关联对象集中的对象,不会重复添加这个对象;同样,调用remove()方法时,如果传递一个不存在关联对象集中的对象,不会执行任何操作。另一个多对多管理器方法是clear(),会从关联对象集中移除所有对象。

最后,我们使用Django提供的JsonResponse类返回一个HTTP响应,其中内容类型为application/json,它会把给定对象转换为JSON输出。

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

url(r'^like/$', views.image_like, name='like'),

2.4 通过相似度检索帖子

现在,我们已经为博客帖子添加了标签,我们还可以用标签做更多有趣的事。通过便签,我们可以很好的把帖子分类。主题类似的帖子会有几个共同的标签。我们准备增加一个功能:通过帖子共享的标签数量来显示类似的帖子。在这种情况下,当用户阅读一篇帖子的时候,我们可以建议他阅读其它相关帖子。

为某个帖子检索相似的帖子,我们需要:

  • 检索当前帖子的所有标签。
  • 获得所有带这些便签中任何一个的帖子。
  • 从列表中排除当前帖子,避免推荐同一篇帖子。
  • 通过和当前帖子共享的标签数量来排序结果。
  • 如果两篇或以上的帖子有相同的标签数量,推荐最近发布的帖子。
  • 限制我们想要推荐的帖子数量。

这些步骤转换为一个复杂的QuerySet,我们需要在post_detail视图中包含它。打开blog应用的views.py文件,在顶部添加以下导入:

from django.db.models import Count

这是Django
ORM的Count汇总函数。此函数允许我们执行汇总计数。然后在post_detail视图的render()函数之前添加以下代码:

# List of similar posts
post_tags_ids = post.tags.values_list('id', flat=True)
similar_posts = Post.published.filter(tags__in=post_tags_ids)
                                    .exclude(id=post.id)
similar_posts = similar_posts.annotate(same_tags=Count('tags'))
                             .order_by('-same_tags', '-publish')[:4]

这段代码完成以下操作:

  1. 我们获得一个包含当前帖子所有标签的ID列表。values_list()这个QuerySet返回指定字段值的元组。我们传递flat=True给它,获得一个[1, 2, 3, ...]的列表。
  2. 我们获得包含这些标签中任何一个的所有帖子,除了当前帖子本身。
  3. 我们使用Count汇总函数生成一个计算后的字段same_tags,它包含与所有查询标签共享的标签数量。
  4. 我们通过共享的标签数量排序结果(降序),共享的标签数量相等时,用publish优先显示最近发布的帖子。我们对结果进行切片,只获取前四篇帖子。

render()函数添加similar_posts对象到上下文字典中:

return render(request,
              'blog/post/detail.html',
              {'post': post,
               'comments': comments,
               'new_comment':new_comment,
               'comment_form': comment_form,
               'similar_posts': similar_posts})

现在,编辑blog/post/detail.html模板,在帖子的评论列表前添加以下代码:

<h2>Similar posts</h2>
{% for post in similar_posts %}
    <p>
        <a href="{{%20post.get_absolute_url%20}}">{{ post.title }}</a>
    </p>
{% empty %}
    There are no similar post yet.
{% endfor %}

推荐你在帖子详情模板中也添加标签列表,就跟我们在帖子列表模板中所做的那样。现在,你的帖子详情页面应该看起来是这样的:

译者注:需要给其它帖子添加标签,才能看到上图所示的相似的帖子。

你已经成功的推荐了相似的帖子给用户。django-taggit也包含一个similar_objects()管理器,可以用来检索共享的标签。你可以在这里查看所有django-taggit管理器。

列表页 内容页 判断有图或者无图时调用的代码:

5.5.1 加载jQuery

我们需要添加AJAX功能到图片详情模板中。为了在模板中使用jQuery,首先在项目的base.html模板中引入它。编辑account应用的base.html模板,在</body>标签之前添加以下代码:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
    $(document).ready(function() {
        {% block domready %}
        {% endblock %}
    });
</script>

我们从Google加载jQuery框架,Google在高速内容分发网络中托管了流行的JavaScript框架。你也可以从http://jquery.com/下载jQuery,然后把它添加到应用的static目录中。

我们添加一个<script>标签来包括JavaScript代码。$(document).ready()是一个jQuery函数,参数是一个处理函数,当DOM层次构造完成后,会执行这个处理函数。DOM是Document Object Model的缩写。DOM是网页加载时浏览器创建的一个树对象。在这个函数中包括我们的代码,可以确保我们要交互的HTML元素都已经在DOM中加载完成。我们的代码只有在DOM准备就绪后才执行。

在文档准备就绪后的处理函数中,我们包括了一个名为domready的Django模板块,在扩展了基础模板的模板中可以包括特定的JavaScript。

不要将JavaScript代码和Django模板标签搞混了。Django模板语言在服务端渲染,输出为最终的HTML文档;JavaScript在客户端执行。某些情况下,使用Django动态生成JavaScript非常有用。

本章的示例中,我们在Django模板中引入了JavaScript代码。引入JavaScript代码更好的方式是加载作为静态文件的.js文件,尤其当它们是代码量很大的脚本时。

2.5 总结

在这一章中,你学习了如何使用Django表单和模型表单。你创建了一个可以通过邮件分享网站内容的系统,还为博客创建了评论系统。你为帖子添加了标签,集成了一个可复用的应用,并创建了一个复杂的QuerySet,通过相似度检索对象。

下一章中,你会学习如何创建自定义模板标签和过滤器。你还会构建一个自定义的站点地图和帖子的RSS源,并在应用中集成一个高级的搜索引擎。

if(empty($r[titlepic])){$con=无图的模板代码列如:liimgsrc=图片地址无图模板/li;}else{$con=有图的模板代码列如:divliimgsrc=图片地址有图模板/li/div;}$listtemp=$con;

5.5.2 AJAX请求的跨站请求伪造

你已经在第二章中学习了跨站点请求伪造。在激活了CSRF保护的情况下,Django会检查所有POST请求的CSRF令牌。当你提交表单时,可以使用{% csrf_token %}模板标签发送带令牌的表单。但是,对于每个POST请求,AJAX请求都将CSRF令牌作为POST数据传递是不方便的。因此,Django允许你在AJAX请求中,用CSRF令牌的值设置一个自定义的X-CSRFToken头。这允许你用jQuery或其它任何JavaScript库,在每次请求中自动设置X-CSRFToken头。

要在所有请求中包括令牌,你需要:

  1. csrftoken cookie中检索CSRF令牌,如果激活了CSRF,它就会被设置。
  2. 在AJAX请求中,使用X-CSRFToken头发送令牌。

你可以在这里找到更多关于CSRF保护和AJAX的信息。

编辑你在base.html中最后引入的代码,修改为以下代码:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="http://cdn.jsdelivr.net/jquery.cookie/1.4.1/jquery.cookie.min.js"></script>
<script>
    var csrftoken = $.cookie('csrftoken');
    function csrfSafeMethod(method) {
        // these HTTP methods do not required CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }
    $.ajaxSetup({
        beforeSend: function(xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", csrftoken);
            }
        }
    });
    $(document).ready(function() {
        {% block domready %}
        {% endblock %}
    });
</script>

这段代码完成以下工作:

  1. 我们从一个公有CDN加载jQuery Cookie插件,因此我们可以与cookies交互。
  2. 我们读取csrftoken cookie中的值。
  3. 我们定义csrfSafeMethod()函数,检查HTTP方法是否安全。安全的方法不需要CSRF保护,包括GETHEADOPTIONSTRACE
  4. 我们使用$.ajaxSetup()设置jQuery
    AJAX请求。每个AJAX请求执行之前,我们检查请求方法是否安全,以及当前请求是否跨域。如果请求不安全,我们用从cookie中获取的值设置X-CSRFToken头。这个设置会应用到jQuery执行的所有AJAX请求。

CSRF令牌会在所有使用不安全的HTTP方法的AJAX请求中引入,比如POSTPUT

5.5.3 使用jQuery执行AJAX请求

编辑images应用的images/image/detail.htmlt模板,把这一行代码:

{% with total_likes=image.users_like.count %}

替换为下面这行:

{% with total_likes=image.users_like.count users_like=image.users_like.all %}

然后修改classimage-info<div>元素,如下所示:

<div class="image-info">
    <div>

            {{ total_likes }}
            like{{ total_likes|pluralize }}

        <a href="#" data-id="{{ image.id }}" 
            data-action="{% if request.user in users_like %}un{% endif %}like" class="like button">
            {% if request.user not in users_like %}
                Like
            {% else %}
                Unlike
            {% endif %}
        </a>
    </div>
    {{ image.description|linebreaks }}
</div>

首先,我们添加了另一个变量到{% with %}模板标签中,用于存储image.users_like.all的查询结果,避免执行两次查询。我们显示喜欢这张图片的用户总数,以及一个like/unlike链接:我们检查用户是否在users_like关联对象集中,根据当前用户跟这样图片的关系显示likeunlike。我们在<a>元素中添加了以下属性:

  • data-id:显示的图片的ID。
  • data-action:用户点击链接时执行的操作。可能是likeunlike

我们将会在AJAX请求发送这两个属性的值给image_like视图。当用户点击like/unlike链接时,我们需要在客户端执行以下操作:

  1. 调用AJAX视图,并传递图片ID和action参数。
  2. 如果AJAX请求成功,用相反操作(like/unlike)更新<a>元素的data-action属性,并相应的修改显示文本。
  3. 更新显示的喜欢总数。

images/image/detail.html模板底部添加包括以下代码的domready块:

{% block domready %}
    $('a.like').click(function(e) {
        e.preventDefault();
        $.post('{%%20url%20"images:like" %}', 
            {
                id: $(this).data('id'),
                action: $(this).data('action')
            },
            function(data) {
                if (data['status'] == 'ok') {
                    var previous_action = $('a.like').data('action');

                    // toggle data-action
                    $('a.like').data('action', previous_action == 'like' ? 'unlike' : 'like');
                    // toggle link text
                    $('a.like').text(previous_action == 'like' ? 'Unlike' : 'Like');
                    // update total likes
                    var previous_likes = parseInt($('span.count .total').text());
                    $('span.count .total').text(previous_action == 'like' ? previous_likes+1 : previous_likes-1);
                }
            }
        );
    });
{% endblock %}

这段代码是这样工作的:

  1. 我们使用$('a.like')
    jQuery选择器查找HTML文档中classlike<a>元素。
  2. 我们为click事件定义了一个处理函数。用户每次点击like/unlike链接时,会执行这个函数。
  3. 在处理函数内部,我们使用e.preventDefault()阻止<a>元素的默认行为。这会阻止链接调转到其它地方。
  4. 我们使用$.post()向服务器执行一个异步请求。jQuery还提供了执行GET请求的$.get()方法,以及一个底层的$.ajax()方法。
  5. 我们使用Django的{%%20url%20%}模板标签为AJAX请求构建URL。
  6. 我们构建在请求中发送的POST参数字典。我们的Django视图需要idaction参数。我们从<a>元素的<data-id><data-action>属性中获得这两个值。
  7. 我们定义了回调函数,当收到HTTP响应时,会执行这个函数。它的data属性包括响应的内容。
  8. 我们访问收到的datastatus属性,检查它是否等于ok。如果返回的data是期望的那样,我们切换链接的data-action属性和文本。这样就允许用户取消这个操作。
  9. 根据执行的操作,我们在喜欢的总数上加1或减1。

在浏览器中打开之前上传的图片详情页面。你会看到以下初始的喜欢总数和LIKE按钮:

点击LIKE按钮。你会看到喜欢总数加1,并且按钮的文本变为UNLIKE

当你点击UNLIKE按钮时,会执行这个操作,按钮的文本变回LIKE,总数也会相应的改变。

编写JavaScript代码时,尤其是执行AJAX请求时,推荐使用Firebug等调试工具。Firebug是一个Firefox插件,允许你调试JavaScript代码,并监控CSS和HTML的变化。你可以从这里下载Firebug。其它浏览器,比如Chrome或Safari,也有调试JavaScript的开发者工具。在这些浏览器中,右键网页中的任何一个地方,然后点击Inspect element访问开发者工具。

5.6 为视图创建自定义装饰器

我们将限制AJAX视图只允许由AJAX发起的请求。Django的Request对象提供一个is_ajax()方法,用于检查请求是否带有XMLHttpRequest,也就是说是否是一个AJAX请求。这个值在HTTP头的HTTP_X_REQUESTED_WITH中设置,绝大部分JavaScript库都会在AJAX请求中包括它。

我们将创建一个装饰器,用于在视图中检查HTTP_X_REQUESTED_WITH头。装饰器是一个接收另一个函数为参数的函数,并且不需要显式修改作为参数的函数,就能扩展它的行为。如果你忘了装饰器的概念,你可能需要先阅读这里。

因为这是一个通用的装饰器,可以作用于任何视图,所以我们会在项目中创建一个common包。在bookmarks项目目录中创建以下目录和文件:

common/
    __init__.py
    decrorators.py

编辑decrorators.py文件,添加以下代码:

from django.http import HttpResponseBadRequest

def ajax_required(f):
    def wrap(request, *args, **kwargs):
        if not request.is_ajax():
            return HttpResponseBadRequest()
        return f(request, *args, **kwargs)
    wrap.__doc__ = f.__doc__
    wrap.__name__ = f.__name__
    return wrap

这是我们自定义的ajax_required装饰器。它定义了一个wrap函数,如果不是AJAX请求,则返回HttpResponseBadRequest对象(HTTP
400)。否则返回被装饰的函数。

现在编辑images应用的views.py文件,添加这个装饰器到image_like视图中:

from common.decrorators import ajax_required

@ajax_required
@login_required
@require_POST
def image_like(request):
    # ...

如果你直接在浏览器中访问http://127.0.0.1:8000like/,你会得到一个HTTP
400的响应。

如果你在多个视图中重复同样的验证,则可以为视图构建自定义装饰器。

5.7 为列表视图创建AJAX分页

我们需要在网站中列出所有标记过的图片。我们将使用AJAX分页构建一个无限滚动功能。当用户滚动到页面底部时,通过自动加载下一页的结果实现无限滚动。

我们将实现一个图片列表视图,同时处理标准浏览器请求和包括分页的AJAX请求。当用户首次加载图片列表页面,我们显示第一页的图片。当用户滚动到页面底部,我们通过AJAX加载下一页的项,并添加到主页面的底部。

我们用同一个视图处理标准和AJAX分页。编辑images应用的views.py文件,添加以下代码:

from django.http import HttpResponse
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

@login_required
def image_list(request):
    images = Image.objects.all()
    paginator = Paginator(images, 8)
    page = request.GET.get('page')
    try:
        images = paginator.page(page)
    except PageNotAnInteger:
        # If page is not an integer deliver first page
        images = paginator.page(1)
    except EmptyPage:
        if request.is_ajax():
            # If the request is AJAX an the page is out of range
            # return an empty page
            return HttpResponse('')
        # If page is out of range deliver last page of results
        images = paginator.page(paginator.num_pages)

    if request.is_ajax():
        return render(request, 'images/image/list_ajax.html', {'section': 'images', 'images': images})

    return render(request, 'images/image/list.html', {'section': 'images', 'images': images})

我们在这个视图中创建了一个返回数据库中所有图片的QuerySet。然后我们构造了一个Paginator对象来分页查询结果,每页显示八张图片。如果请求的页数超出范围,则抛出EmptyPage异常。这种情况下,如果是通过AJAX发送请求,则返回一个空的HttpResponse对象,帮助我们在客户端停止AJAX分页。我们把结果渲染到两个不同的模板中:

  • 对于AJAX请求,我们渲染list_ajax.html模板。该模板只包括请求页的图片。
  • 对于标准请求,我们渲染list.html模板。该模板继承自base.html模板,并显示整个页面,同时还包括list_ajax.html模板,用来引入图片列表。

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

url(r'^$', views.image_list, name='list'),

最后,我们需要创建上面提到的模板。在images/image/模板目录中创建list_ajax.html模板,添加以下代码:

{% load thumbnail %}

{% for image in images %}
    <div class="image">
        <a href="{{%20image.get_absolute_url%20}}">
            {% thumbnail image.image "300*300" crop="100%" as im %}
                <a href="{{%20image.get_absolute%20url%20}}">
                    ![]({{ im.url }})
                </a>
            {% endthumbnail %}
        </a>
        <div class="info">
            <a href="{{%20image.get_absolute_url%20}}" class="title">
                {{ image.title }}
            </a>
        </div>
    </div>s
{% endfor %}

这个模板显示图片的列表。我们将用它返回AJAX请求的结果。在同一个目录下创建list.html模板,添加以下代码:

{% extends "base.html" %}

{% block title %}Images bookmarked{% endblock %}

{% block content %}
    <h1>Images bookmarked</h1>
    <div id="image-list">
        {% include "images/image/list_ajax.html" %}
    </div>
{% endblock %}

列表模板继承自base.html模板。为了避免重复代码,我们引入了list_ajax.html模板来显示图片。list.html模板会包括JavaScript代码,当用户滚动到页面底部时,负责加载额外的页面。

list.html模板中添加以下代码:

{% block domready %}
    var page = 1;
    var empty_page = false;
    var block_request = false;

    $(window).scroll(function() {
        var margin = $(document).height() - $(window).height() - 200;
        if ($(window).scrollTop() > margin && empty_page == false && block_request == false) {
            block_request = true;
            page += 1;
            $.get('?page=' + page, function(data) {
                if (data == '') {
                    empty_page = true;
                } else {
                    block_request = false;
                    $('#image-list').append(data);
                }
            });
        }
    });
{% endblock %}

这段代码提供了无限滚动功能。我们在base.html模板中定义的domready块中引入了JavaScript代码。这段代码是这样工作的:

  1. 我们定义了以下变量:
  • page:存储当前页码。
  • empty_page:让我们知道是否到了最后一页,如果是则接收一个空的页面。只要我们得到一个空的页面,就会停止发送额外的AJAX请求,因为我们假设此时没有结果了。
  • block_request:正在处理AJAX请求时,阻止发送另一个请求。
  1. 我们使用$(window).scroll()捕获滚动事件,并为它定义一个处理函数。
  2. 我们计算边距变量来获得文档总高度和窗口高度之间的差值,这是用户滚动的剩余内容的高度。我们从结果中减去200,因此,当用户距离页面底部小于200像素时,我们会加载下一页。
  3. 如果没有执行其它AJAX请求(block_request必须为false),并且用户没有获得最后一页的结果时(empty_page也为false),我们才发送AJAX请求。
  4. 我们设置block_requesttrue,避免滚动事件触发额外的AJAX请求,同时给page计算器加1来获取下一页。
  5. 我们使用$.get()执行AJAX
    GET请求,然后在名为data的变量中接收HTML响应。这里有两种情况:
  • 响应不包括内容:我们已经到了结果的末尾,没有更多页面需要加载。我们设置empty_pagetrue阻止额外的AJAX请求。
  • 响应包括内容:我们把数据添加到id为image-list的HTML元素中。当用户接近页面底部时,页面内容会垂直扩展附加的结果。

在浏览器中打开http://127.0.0.1:8000,你会看到目前已经标记过的图片列表,如下图所示:

滚动到页面底部来加载下一页。确保你用书签工具标记了八张以上图片,因为我们每页显示八张图片。记住,你可以使用Firebug或类似工具追踪AJAX请求和调试JavaScript代码。

最后,编辑account应用的base.html模板,为主菜单的Images项添加URL:

<li {% if section == "images" %}class="selected"{% endif %}>
    <a href="{%%20url%20"images:list" %}">Images</a>
</li>

现在你可以从主菜单中访问图片列表。

5.8 总结

在本章中,我们构建了一个JavaScript书签工具,可以分享其它网站的图片到我们的网站中。你用jQuery实现了AJAX视图,并添加了AJAX分页。

下一章会教你如何构建关注系统和活动流。你会使用通用关系(generic
relations),信号(signals)和反规范化(denormalization)。你还会学习如何在Django中使用Redis。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图