django by example 实践 bookmarks 项目(二)


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


本项目相关内容( github传送 )包括:

实现过程

CH4 创建社交网站

CH5 在网站中分享内容

CH6 追踪用户动作

项目总结及改进

网站应用实现微信登录


CH5 在网站中分享内容

上一章,我们为网站创建了用户注册和权限功能,学习了如何为用户创建自定义 profile 模型以及使用主要的社交网站账号登录网站。

在这一章中,我们将学习如何创建 JavaScript bookmarklet 实现分享其它网站内容的功能,以及使用 JQuery 和Django 实现 AJAX 特性。

这一章,我们将学习以下内容:

  • 创建多对多关系

  • 为表单自定义行为

  • 在 Django 中使用 jQuery

  • 创建 jQuery bookmarklet

  • 使用 sore-thumbnail 生成图像缩略图

  • 执行 AJAX 视图并使用 jQuery 集成

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

  • 创建 AJAX 分页

创建一个图片标签网站

我们将实现用户为图像添加标签、分享从其它网站上找到的图片、以及在我们的网站上分享图片。为实现这个功能,我们需要完成以下工作:

  1. 定义保存图像及其信息的模型;
  2. 创建表单和视图来实现上传图片功能;
  3. 创建用户可以发布从其它网站上找到的图片的系统。

首先,在 bookmarks 项目中新建一个应用:

django-admin startapp images

在项目的 settings.py 文件的 INSTALLED_APPS 中加入 ‘images’ :

INSTALLED_APPS = ['account',
                  'django.contrib.admin',
                  'django.contrib.auth',
                  'django.contrib.contenttypes',
                  'django.contrib.sessions',
                  'django.contrib.messages',
                  'django.contrib.staticfiles',
                  'social_django',
                  'images']

现在,新的应用已经激活了。

创建图片模型

编辑image应用的models.py模型并添加以下代码:

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


# Create your models here.


class Image(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             related_name='images_created')
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, blank=True)
    url = models.URLField()
    image = models.ImageField(upload_to='images/%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 对象。这是一个外键,它指定了一个一对多关系。一个用户可以发布多张图片,但是每个图片只有一个用户。

  • title :图片的标题。

  • slug :只包括字母、数字、下划线或者连字符来创建 SEO 友好的 URLs 。

  • url : 这个图片的原始 URL 。

  • image:图片文件。

  • describe:可选的图片描述。

  • created:对象创建日期。由于我们使用了 auto_now_add ,创建对象时会自动填充这个字段。我们使用db_index=True ,这样 Django 将在数据库中为这个字段创建一个索引。

注意:

数据库索引将改善查询表现。对于频繁使用 filter()、exclude()、order_by() 进行查询的字段要设置db_index=True 。外键字段或者 unique=True 的字段会自动设置索引。还可以使用 Meta.index_together 来为多个字段创建索引。

​ 我们将重写 Image 模型的 save() 方法,从而实现根据 title 字段自动生成 slug 字段的功能。导入 slugify() 函数并为 Image 模型添加 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(Image, self).save(*args, **kwargs)

笔者注:

在 blog 项目中,我们在 admin网站中设置 prepopulated_field 实现输入 title 时自动生成 slug 。也可以为 blog 项目的 Posts 模型添加这个 save 方法,从而支持在其它页面写文章。

代码中,如果用户没有提供 slug ,我们将使用 slufigy() 函数根据标题自动生成图像的 slug 。然后保存对象。自动生成 slug 可以避免用户为每张图片填写 slug 字段。

创建多对多关系

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

在 Image 模型中添加下面的字段:

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

定义一个多对多字段时,Django 使用两个数据库表的主键创建了一个内联表。ManyToManyField 可以在两个相关模型中的任何一个模型中。

与在 ForeignKey 字段中一样,ManyToManyField 的 related_name 属性允许相关对象使用这个名字访问这个对象。ManyToManyField 字段提供一个多对多管理器,这个管理器可以获取相关对象,比如 image.users_like.all()或者从用户端查询 user.image_liked.all() 。

打开命令行并执行以下命令:

python manage.py makemigrations images

我们将看到这样的输出:

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

现在运行以下命令实现迁移:

python manage.py migrate images

我们将看到这样的输出:

Operations to perform:
  Apply all migrations: images
Running migrations:
  Applying images.0001_initial... OK

现在,Image 模型同步到数据库中了。

在 admin网站中注册 image 模型

编辑 images 应用的 admin.py 文件并在 admin网站中注册 image 模型:

from django.contrib import admin

from .models import Image


# Register your models here.
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/admin ,将在 admin网站中看到 image 模型:

image_admin.png

发布其它网站上找到的内容

我们将帮助用户从外部网站标记 image 。用户将提供图像的 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, }

这个表单是一个 ModelForm,根据 image 模型创建,只包含 title、url 和 description 字段。用户不需要在表单中填入图片的 URL 。他们使用 JavaScript 工具从外部网站选择一个图片,我们的表单将以参数的形式获得这个图片的 URL 。我们重写了 url 字段的默认组件来使用 HiddenInput 组件。这个组件被渲染为一个具有type=‘hidden’ 属性的 HTML 输入元素。我们使用这个组件是因为不希望用户看到这个字段。

验证表单字段

这里只允许上传 JPG 格式的图片,为了验证提供的图片的 URL 有效,我们将检查文件名是否以 .jpg 或者 .jpeg 结尾。Django 允许用户通过定义表单的 clean_() 方法来验证表单字段,当对表单实例调用 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 获得文件扩展名并验证是否有效。如果该 URL 使用一个无效的扩展名,将引发 ValidationError ,表单将无法通过验证。我们只是实现了一个非常简单的验证,你可以使用更高级的方法检查给定的 URL 是否提供了有效的图片文件。

除了验证给定的 URL ,我们还需要下载图片文件并保存。我们可以使用处理表单的视图来下载图片文件。这里我们使用更加通用的方法来实现,重写模型表单的 save() 方法在保存表单时实现下载。

重写 ModelForm 的 save() 方法

ModelForm 提供一个 save() 方法将当前模型实例保存到数据库中并返回该模型对象。这个方法接收一个布尔参数commit ,这个参数指定是否需要提交到数据库。如果 commit 为 False ,save() 方法将返回一个模型实例但是并不保存到数据库。我们将重写表单的 save() 方法来获得给定图片并保存。

在 forms.py 文件的开头部分导入以下模块:

from requests 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(ImageCreateForm,self).save(commit=False)
    image_url = self.cleaned_data['url']
    image_name = '{}.{}'.format(slugify(image.title),image_url.rsplit('.',1)[1].lower())
    # download image form given URL
    response = request('GET',image_url)
    image.image.save(image_name,ContentFile(response.content),save=False)
    if commit:
        image.save()
    return image

笔者注:

注意,定义 image_name 时使用 rsplit 方法对 image_url 进行拆分, rsplit 与 split 功能类似,只是它从右侧开始拆分,rsplit 中的参数 1 表示只拆分一次,即取出文件后缀即可。

我们重写 save() 方法来保存 ModelForm 需要的参数,下面是代码如何执行的:

  1. 通过调用设置 commit=False 的 save() 方法获得 image 实例。
  2. 从表单的 cleaned_data 字典中读取 URL 。
  3. image 的名称 slug 结合初始文件扩展名生成图片名称;
  4. 使用 Python 的 request 库下载文件,然后调用 image 字段的 save() 方法,并传入一个 ContentFile 对象, ContentFile 对象是下载文件内容的实例。这样我们将文件保存到项目文件目录下。我们还传入save=False 避免保存到数据库。
  5. 为了与重写前的 save() 方法保持一样的行为,只有在 commit 参数设置为 True 时才将表单保存到数据库。

笔者注:

视图可能会多次调用表单的 save() 方法,这将导致多次请求及下载图片,我们可以将代码改为:

def save(self,force_insert=False,force_update=False,commit=True):
  image = super(ImageCreateForm,self).save(commit=False)
  if commit:
      image_url = self.cleaned_data['url']
      image_name = '{}.{}'.format(slugify(image.title),image_url.rsplit('.',1)[1].lower())
      # download image form given URL
      response = request('GET',image_url)
      image.image.save(image_name,ContentFile(response.content),save=False)
        image.save()
    return image

这样,只在数据库保存对象实例时才下载图片。

我们也可以另外设置标志位来判断是否下载图片。

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

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect

from .forms import ImageCreateForm


# Create your views here.

@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()
            messages.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 方法得到初始数据来创建表单实例。实例化时将从外部网站获得图片的 url 和 title 数据,我们之后创建的 JavaScript 工具的GET方法提供这些数据,这里我们只是假设获得了初始化数据。
  2. 如果提交表单,我们将检查它是否有效。如果表单有效我们将创建一个新的 image 实例,但是设置commit=False 来阻止将对象保存到数据库中。
  3. 为新的 image 对象设置当前用户。这样我们就可以知道谁上传了这张图片。
  4. 将 image 对象保存到数据库。
  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 应用中创建的 URL模式:

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

最后,我们需要新建模板来渲染表单。在image应用目录下新建下面的目录:

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

编辑 create.html 模板并添加以下代码:

{% extends "base.html" %}

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

{% block content %}
  

Bookmark an image

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

现在,在浏览器中打开 http://127.0.0.1:8000/images/create/?title=...&url=... ,GET 请求中包含后续提供的 title 和 url 参数。

笔者注:

原文这里有个 URL 及 URL 对应的图片,本章完成后可以实现该功能,因此,这里先完成后面的内容,再进行测试。

使用 jQuery 创建 bookmarklet

bookmarklet 是保存在浏览器中使用 JavaScript 代码扩展浏览器功能的书签。当你点击书签时,将在浏览器的当前网页中执行 JavaScript 代码。这对于创建与其他网站进行交互的工具非常有帮助。

一些在线服务(比如 Pinterest )通过自己的 bookmarklet 帮助用户将其它网站的内容分享到自己的平台上。我们将使用相似的方法创建一个 bookmarklet 帮助用户将在其它网站看到的图片分享到我们的网站上。

我们将使用 jQuery 实现 bookmarklet 。jQuery 是一个用于快速开发客户端功能的 JavaScript 框架。你可以从它的网站上了解更多内容:http://jquery.com/ 。

下面是用户如何在自己的浏览器中添加 bookmarklet 并使用:

  1. 用户将我们网站上的链接拖动到自己浏览器的书签中。该链接的 href 属性为 JavaScript 代码。这些代码将被保存到书签中。

  2. 用户浏览任何网站并点击书签,书签将执行保存的 JavaScript 代码。

由于 JavaScript 代码以书签的形式保存,保存之后我们无法对其进行更新。这个一个重大缺陷,但是可以通过执行简单的启动脚本从 URL 加载 JavaScript 代码来解决这个问题。你的用户将以书签的形式保存这个启动脚本,这样我们可以在任意时刻更新 bookmarklet 。这是我们创建 bookmarklet 时采用的方法,让我们开始吧!

在 image/templates/ 中新建 bookmarklet_launcher.js 模板 。这是一个启动脚本,在脚本中添加以下JavaScript代码:

(function () {
    if (window.myBookmarklet !== undefined) {
        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 变量来判断是否加载了 bookmarklet 。这样可以在用户重复点击 bookmarklet 时避免重复加载。如果没有定义 myBookmarklet ,我们加载另外一个向文件添加

我们从 https://www.staticfile.org/ 加载 jQuery 框架,https://www.staticfile.org/ 使用高速可靠的内容交付网络托管流行的 JavaScript 框架。 您也可以从 http://jquery.com/ 下载 jQuery ,并将其添加到应用程序的静态目录中。

我们添加

这些代码是这样工作的:

  1. 从一个公共 CDN 加载 jQuery cookie 插件,这样我们可以与 cookie 交互。
  2. 读取 csrftoken cookie 的值;
  3. 定义 csrfSafeMethod() 函数来检查一个 HTTP 方法是否安全。安全的方法(包括 GET、HEAD、OPTIONS和TRACE)不需要 CSRF 防御。
  4. 使用 $.ajaxSetup() 设置 jQuery AJAX 请求。我们对每个 AJAX 请求检查请求方法是否安全以及当前请求是否跨域。如果请求不安全,我们将使用从 cookie 中获取的值设置 X-CSRFToken 标头。jQuery 的所有 AJAX 请求都将进行这种设置。

CSRF令牌将用在所有使用不安全的 HTTP 方法(比如 POST、PUT )的 AJAX 请求中。

笔者注:

我们可以直接使用 {{ csrf_token }} 从模板内容中获取 CSRF令牌,这样代码简化为:



关于 CSRF 防御的使用方法见:https://www.jianshu.com/p/235876c75d79。

使用 jQuery 实现 AJAX 请求


编辑 image 应用的 images/image/details.html 模板并将以下行:

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

替换为:

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

然后,将 class 为 image-info 的

更改为:

{{ image.description|linebreaks }}

首先,我们在 {% with %} 模板标签中添加了另一个变量来保存 image.users_like.all 查询结果以避免重复执行。然后展示喜欢这个图片的用户数和一个包含喜欢/不喜欢这张图片的操作链接:基于检查用户是否在 users_like 相关对象集合中来展示喜欢或者不喜欢选项。在 元素添加下面的属性:

  • data-id: 展示的图片的 ID ;

  • data-action : 用户点击链接时执行的动作,可以是 like 或者 unlike 。

笔者注:

这是 HTML 5 的新特定,详细说明见 https://www.jianshu.com/p/bfa872c93d23。

我们将这两个属性的值传入 AJAX 请求中。当用户点击 like/unlike 链接时,需要在用户端实现以下动作:

  1. 调用 AJAX 视图传输图片的 ID 和动作参数;
  2. 如果 AJAX 请求成功,使用相反的动作更新 HTML 元素的 data-action 属性,并相应更改展示的文本。
  3. 更改展示的 like 总数。

在 images/image/detail.html 模板底部添加 domready 块并添加以下 JavaScript 代码:

{% block domready %}
    $('a.like').click(function(e){
        e.preventDefault();
        $.post(
            '{% url "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') 选择器来找到 HTML 文档中 class 为 like 的 元素;

  2. 为点击事件定义一个处理函数,这个函数将在每次用户点击 like/unlike 链接时触发;

  3. 在处理函数内部,我们使用 e.preventDefault() 来避免 元素的默认行为。这样避免链接将我们引导到其它地方。

  4. 使用 $.post() 实现异步 POST 服务器请求。jQuery 还提供$.get() 方法来实现 GET 请求,以及小写的$.ajax() 方法。

  5. 使用 {% url %} 模板语言为 AJAX 请求创建 URL 。

  6. 设置 POST 请求发送的参数字典,字典包括 Django 视图需要的 ID 和 action 参数。我们从 元素的 data-id 和 data-action 属性获得相应的值。

  7. 定义接收 HTTP 响应的回调函数。它接收响应返回的参数。

  8. 获取接收数据中的 status 属性并检查它是否等于 'ok' 。如果返回的数据符合预期,我们反转链接的 data-action 属性和文本。这将允许撤销操作。

  9. 根据动作,增加或者减少一个喜欢这幅图片的人的数量。

笔者注:

为了避免与第六章中添加的统计查看人数的

混淆,这里为

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

中 class 为 total 的 span 添加了 id,并将 jQuery 通过 $('span.count .total') 获取元素改为通过 $('#like')获取。

即 detail.html 中的代码变为:

{% extends "base.html" %}

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

{% block content %}
    

{{ image.title }}

{# #} {% load thumbnail %} {% thumbnail image.image "300" as im %}
{% endthumbnail %} {% with total_likes=image.users_like.count users_like=image.users_like.all %}
{{ image.description|linebreaks }}
{% for user in image.users_like.all %}

{{ user }}

{% empty %} Nobody likes this image yet. {% endfor %}
{% endwith %} {% endblock %} {% block domready %} $('a.like').click(function(e){ e.preventDefault(); $.post( '{% url "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($('#like').text()); $('#like').text(previous_action == 'like' ? previous_likes + 1 : previous_likes - 1); } }); }); {% endblock %}

在浏览器中打开已经上传的图片的图片详细页面,应该可以看到下面的初始喜欢数量和 LIKE 按钮:


ajax_0.png

点击 LIKE 按钮。将看到总的喜欢数量增加了一个而且按钮变成了 UNLIKE :

ajax_1.png

当点击 UNLIKE 按钮后按钮变回 LIKE ,总的喜欢数量相应发生改变。

使用 JavaScript 编程,尤其是实现 AJAX 请求时,推荐使用 Firebug 之类的工具进行调试。Firebug 是一个可以调试 JavaScript 并且可以监测 CSS 和 HTML 变化的 FireFox 插件。可以从 http://getfirebug.com 下载 Firebug 。其它的浏览器,比如 Chrome 或者 Safari 也提供内置开发工具来调试 JavaScript 。在这些浏览器中,你可以右击浏览器中的任何位置并点击 Inspect element 来访问 web开发工具。

为视图创建自定义装饰器


我们将限制 AJAX 视图只接收 AJAX 请求,Django 请求对象提供一个 is_ajax() 方法来判断请求是否是XMLHttpRequest 生成的(这意味将是一个 AJAX 请求)。大多数 JavaScript 库的 AJAX 请求将这个值设置到 HTTP_X_REQUESTED_WITH HTTP 标头中。

我们创建一个装饰器来检查视图的 HTTP_X_REQUESTED_WITH 标头。装饰器是一个函数,它将输入另一个函数并且在不改变该函数的基础上扩展它的行为。如果你不了解这个概念,你可以先看一个这个链接的内容 https://www.python.org/dev/peps/pep-0318/。

由于装饰器是通用的,它可以用于任意视图上。我们将在项目中新建 common Python库。在bookmarket项目下新建以下文件结构:

decorators_str.png

编辑 decorators.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 请求则返回一个HttpResponseBadReques t对象(HTTP 400)。否则返回装饰器函数。

现在可以编辑 images 应用的 views.py 文件并在 image_like AJAX 视图上添加这个装饰器:

from common.decorators import ajax_required

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

如果在浏览器中尝试访问 http://127.0.0.1:8000/images/like ,将会得到一个HTTP 400响应。

注意:

如果发现在许多视图中重复同一项检查,请为视图创建自定义装饰器。

为列表视图添加 AJAX 分页


如果需要在网站中列出所有标注的图片,我们要使用 AJAX 分页实现无限滚动功能。无限滚动是指当用户滚动到页面底部时自动加载其它结果。

我们将实现一个图片列表视图,该视图既可以处理标准浏览器请求,也可以处理包含分页的 AJAX 请求。用户第一次加载图像列表页面时,我们展示图像的第一页。当页面滚动到最底部时将通过 AJAX 加载后面页面的内容。

同一个视图将处理标准请求和 AJAX 分页请求。编辑 image 应用的 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 the first page
        images = paginator.page(1)
    except EmptyPage:
        if request.is_ajax():
            # If the request is AJAX and 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 %}
    
{% endfor %} 

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

{% extends "base.html" %}

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

{% block content %}
    

Images bookmarked

{% include "images/image/list_ajax.html" %}
{% 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 %}

这段代码实现了无限滚动功能。我们将 JavaScript代码 放到 base.html 中定义的 domready 块中,代码实现的功能包括:

  1. 定义以下变量:

    • page :保存当前页码;
  • empty_page :判断用户是否在最后一页并获取一个空页面。当我们获得一个空页面时表示没有其它结果了,我们将停止发送额外的 AJAX 请求。
  • block_request:处理一个 AJAX 请求时阻止发送额外请求;
  1. 使用 $(window).scroll() 来获得滚动时间并为其定义一个处理函数;

  2. 计算表示总文档高度和窗口高度的差的 margin 变量,这是用户滚动获得额外内容的高度。我们将结果减去200 以便在用户接近底部 200 像素的位置加载下一页;

  3. 只在没有实现其他 AJAX请求( block_request 为 False )并且用户没有到达最后一个页面( empty_page 为 Flase )的情况下发送一个 AJAX请求;

  4. 将 block_request 设为 True 来避免滚动事件触发另一个 AJAX 请求,并将页面数增加 1 来获得另外一个页面;

  5. 使用 $.get() 实现一个AJAX GET 请求并将 HTML 响应返回到 data 的变量中,这里有两种情况:

    • 响应不包含内容:我们已经到了底端没有更多页面需要加载了。设置 empty_page 为 true 阻止更多的 AJAX请求;
    • 响应包括数据:我们将数据添加到 id 为 image-list 的 HTML 元素底部。当用户到达页面底端时页面内容垂直扩展。

在浏览器中打开 http://127.0.0.1:8000/images/ 。你将看到标记过的图片列表,看起来是这样的:

django by example 实践 bookmarks 项目(二)_第7张图片
image_list.png

滚动到页面的底部来加载剩下的页面。确保你使用 bookmarklet 标记的图片多于 8 张,那是我们一页显示的图片数量。记住,我们可以使用 Firebug 或类似工具来追踪 AJAX请求和调试 JavaScript代码。

最后,编辑 account 应用的 base.html 模板并为主目录的 Image 项添加到图片列表的链接:

  • Images
  • 现在可以从主目录访问图片列表了。

    总结


    本章,我们使用 JavaScript bookmarklet 实现从其它网站分享图片到自己的网站,使用 jQuery 实现了 AJAX 视图,并添加了 AJAX 分页。

    下一章,我们将学习如何创建一个关注系统和一个活动流。将会用到通用关系、signals 和 demormalization ,还将学习如何在 Django 中使用 Redis 。

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