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_<filename>() 方法来验证表单字段,当对表单实例调用 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应用目录下新建下面的目录:

create_menu.png

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

{% extends "base.html" %}

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

{% block content %}
  <h1>Bookmark an image</h1>
  <img src="{{ request.GET.url }}" class="image-preview">
  <form action="." method="post">
    {{ form.as_p }}
    {% csrf_token %}
    <input type="submit" value="Bookmark it!">
  </form>
{% 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 ,我们加载另外一个向文件添加 <script> 元素的 JavaScript 文件。script 标签使用随机数作为参数加载 bookmark.js 脚本以避免从浏览器缓存中加载文件。

真正的 bookmarklet 代码位于 bookmarklet.js 静态文件中。这样用户无需更新添加到浏览器中的书签即可更新 bookmarklet 代码。我们将 bookmarklet 启动脚本添加到 dashboard 页面,这样用户可以将它拖动到自己的书签中。

编辑 account 应用中的 account/dashboard.html 模板,最终模板的代码为:

{% extends "base.html" %}

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

{% block content %}
    <h1>Dashboard</h1>
    {% with total_image_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 image
        from other
        websites → <a href="javascript:{% include 'bookmarklet_launcher.js' %}"
                      class="button">Bookmark it</a><p>

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

现在 dashboard 显示用户标记的图片总数。我们使用 {% with %} 标签来设置存储当前用户标记的图片总数的变量。这里还包括一个 href 属性设置 bookmarklet 加载脚本的链接,我们从 bookmarklet_launcher.js 模板加载JavaScript 代码。

在浏览器中打开 http://127.0.0.1/account/ ,应该可以看到下面的页面:

bookmark_it.png

Bookmark it按钮拖到浏览器的书签工具条中。

bookmark_it_toolbar.png

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

bookmarklet_js.png

在本章代码中找到 images 应用目录下的 static/css 目录并拷贝到应用的 static 目录下,css/bookmarklet.css 文件为我们的 JavaScript bookmarklet 提供格式。

编辑 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',
            'https://cdn.staticfile.org/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();
            }
        })();
    }
})()

笔者注:

原文 script.setAttribute 中使用的是 http://ajax.googleapis.com/ajax/libs/jquery/ ,由于国内无法使用google,这里改成了 https://cdn.staticfile.org/jquery/

这是主要的 jQuery 加载器脚本。如果当前网站已经加载则使用当前网站的 jQuery ,如果没有则从 staticfile (原文为Google) CDN 中下载 jQuery 。当 jQuery 加载后,它将执行 bookmarklet 代码中的 bookmarklet() 函数,我们还在文件头部设置了一些变量:

  • jquery_version:要加载的 jQuery 代码;

  • site_url 和 static_url :网站的基础URL 和静态文件的基础 URL 。

  • min_width 和 min_height :bookmarklet 在网站中查找图片的最小宽度像素和最小高度像素。

现在,我们来实现上面的 bookmarklet 函数,编辑 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. 使用随机数作为参数加载 bookmarket.css 样式以避免浏览器缓存。

  2. 向当前网站的 <body> 元素中添加自定义 HTML 。它包含一个 <div> 元素来存放在当前网站中找到的图片。

  3. 添加一个事件,该事件用于用户点击 HTML 的关闭链接移除我们在当前网站中添加的 HTML 。使用#bookmarklet #close 选择器来找到 HTML 元素中ID 为 close 的元素(这个元素的父元素的 ID 为bookmarklet )。 JQuery 选择器可以找到这个 HTML 元素。JQuery 选择器返回 CSS 选择器指定的所有元素。我们可以从以下网站找到可用的 JQuery 选择器 http://api.jquery.com/category/selectors/

为 bookmarklet 加载完 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="#"><img src="' + image_url + '" /></a>');
    }
});

代码使用img[src$=“jpg”]选择器找到所有 <img> HTML 元素,这些元素的 src 属性以 jpg 字符串结尾。这意味着找到当前网站的所有 JPG 图片。我们使用 jQuery 的 each 方法对结果进行迭代。添加<div class='image'> HTML 存放尺寸处于 min_width 和 min_height 变量设置的尺寸之间的图片。

现在 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. 隐藏 bookmarklet 并使用 URL 打开一个新的浏览器窗口跳转到我们的网站中编辑一张新图片。我们以网站的<title> 元素和选中的图片 URL 作为参数调用网站的 GET 方法。

在浏览器中打开一个网站并点击 bookmarklet 。你将看到一个新的白色盒子出现在网站中,他包含了网站中所有大于100*100px 的照片。应该是下面例子中的样子:

bookmarked_figure.png

由于我们使用的是 Django 开发服务器,并且通过 HTTP 为网页提供服务,由于浏览器的安全机制,bookmarklet无法在 HTTPS 网站中工作。

笔者注:

第一次测试时使用的全景网,当时随意点链接都可以选到图片,现在只有http://www.quanjing.com/Design/ 可以选到图片,但是得到的图片 url 不满足要求,跳转到 image/create/ 页面时会显示如下错误:

url_field_error.png

然后,使用懒人图库 可以选到图片,并进行保存。

如果点击一个图片,将重定向到图片创建页面, GET 请求参数包括选择图片的 title 和 URL :

bookmarked_figure_add.png

祝贺你,这是你的第一个 JavaScript bookmarklet ,而且它已经集成到你的 Django 项目中了。

为图片创建一个详细视图

我们将创建一个简单地详细视图来展示保存到网站上的图片。打开 image 应用的 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, 'image/image/detail.html',
                  {'section': 'images', "image": image})

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

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

编辑 image 应用的 models.py 文件,为 Image 模型添加 get_absolute_url() 方法:

from django.urls import reverse

def get_absolute_url(self):
    return reverse('images:detail', args=[self.id, self.slug])

为对象提供 URL 的常用做法为在它的模型中定义 get_absolute_url() 方法。

最后,我们在 image 应用的 templates/image/image 目录下新建名为 detail.html 文件,并添加以下代码:

{% extends "base.html" %}

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

{% block content %}
    <h1>{{ image.title }}</h1>
    <img src="{{ image.image.url }}" class="image-detail">
    {% with total_likes=image.users_like.count %}
        <div class="image-info">
            <div>
        <span class="count">
          {{ total_likes }} like{{ total_likes|pluralize }}
        </span>
            </div>
            {{ image.description|linebreaks }}
        </div>
        <div class="image-likes">
            {% for user in image.users_like.all %}
                <div>
                    <img src="{{ user.profile.photo.url }}">
                    <p>{{ user.first_name }}</p>
                </div>
                {% empty %}
                Nobody likes this image yet.
            {% endfor %}
        </div>
    {% endwith %}
{% endblock %}”

这个模板展示标记的图片的详细信息。我们使用{% with %}标签通过total_likes变量来保存有多少人喜欢这幅图片的查询结果。这样,我们可以避免进行两次查询。我们还添加了图片描述并迭代image.users_like.all来展示喜欢这幅图片的人们。

笔者注:

由于 account/models.py 中的 Profile 允许 photo 字段为空,< div class="image-likes"> 中的 <img src="{{ user.profile.photo.url }}"> 可能导致模板解析过程中找不到 photo 的 url 属性而产生错误。

解决方案为:

在 account/models.py 中的 Profile 模型中添加以下方法:

@property
def photo_url(self):
    if self.photo and hasattr(self.photo, 'url'):
        return self.photo.url

并将

<img src="{{ user.profile.photo.url }}">

修改为:

<img src="{{ user.profile.photo_url|default_if_none:'#' }}">

注意:

使用 {%with %} 模板标签可以有效阻止 Django 多次进行数据库查询。

现在使用 bookmarklet 标记一副新图片。提交图片后将重定向到图片详情页面。这个页面将包含如下的 success 信息。


bookmark_it_figure_added.png

使用 sorl-thumbnail 实现图片缩略图

我们正在详情页面展示原始图片,但是不同图片的尺寸可能差别较大。而且一些图片的原始文件可能很大,加载它们可能需要很多时间。最好的方法展示使用相同的方法生成的缩略图。我们将使用 Django 的 sorl-thumbnail 应用来实现缩略图。

打开 terminal 并使用如下命令安装 sorl-thumbnail :

pip install sorl-thumbnail

编辑 bookmarks 项目的 settings.py 文件并将 sorl 添加到 INSTALLED_APPS 中。

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

然后运行下面的命令来同步数据库。

python manage.py migrate

你将看到下面的输出:

Operations to perform:
  Apply all migrations: account, admin, auth, contenttypes, images, sessions, social_django, thumbnail
Running migrations:
  Applying thumbnail.0001_initial... OK

sorl 提供几种定义图像缩略图的方法。它提供一个 {% thumbnail %} 模板标签在模板中生成缩略图,还可以使用自定义 ImageField 在模型中定义缩略图。我们使用模板标签的方法。编辑 image/image/detail.html 模板将以下行:

<img src="{{ image.image.url }}" class="image-detail">

替换为:

{% load thumbnail %}
{% thumbnail image.image "300" as im %}
    <a href="{{ image.image.url }}">
        <img src="{{ im.url }}" class="image-detail">
    </a>
{% endthumbnail %}

笔者注:

如果对这里的图片进行 bookmarkit 标记,则会在 image 的 create页面出现 URL 无效错误,这是由于 ImageCreateForm 的 url 字段为 URLField ,缩略图生成的 im 的 url 为内部地址,无法满足 URLField 的有效性验证。

现在,我们定义了 300 像素的缩略图。用户第一次加载页面时将创建一个缩略图,生成的缩略图将用于后面的请求。输入 python manage.py runserver 命令运行开发服务器,并访问一个存在的图片的图片详细信息页面,将生成该图片的缩略图并在网站上展示。

sorl-thumbnail 应用提供几个自定义缩略图的选项,包括图片剪裁算法以及可以应用的不同效果。如果生成缩略图时遇到了困难,可以在 settings.py 中设置 THUMBNAIL_DEBUG=True 来得到调试信息。sorl-thumbnail 的完整文档链接为 http://sorl-thumbnail.readthedocs.org/

使用 jQuery 添加 AJAX 动作

现在,我们开始在应用中添加 AJAX 动作。AJAX 来源于异步 JavaScript 和 XML(Asynchronous JavaScript and XML ) 。AJAX 包含一组实现异步 HTTP 请求的技术。 AJAX 不用重新加载整个页面就可以从服务器异步发送和检索数据。 尽管名字中包含 XML ,但是 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 装饰器阻止没有登录的用户访问该视图。如果没有通过 POST 方法访问该视图,required_POST 装饰器返回一个 HttpResponseNotAllowed 对象(状态码为 405 ),这样我们只能通过 POST 请求访问该视图。Django 还提供 require_GET 方法来只允许 GET 请求,require_http_method 装饰器可以将允许的请求方法以参数的形式传入。

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

  • id : 用户操作的图片对象的 ID ;

  • action : 用户的操作,应该是 like 或者 unlike 字符串。

我们使用 Django 管理器为 Image 模型的多对多字段 users_like 提供的 add()、remove() 方法来为关系添加或者对象。调用 add() 传入相关对象集合中已经存在的对象并处理重复问题,调用 remove() 在相关对象集合中移除该对象。多对多管理器的另外一个很有用的方法是 clear() ,它将移除相关对象集合中的所有对象。

最后,我们使用 Django 提供的 JsonResponse 类(它将提供一个 application/json 格式的 HTTP 响应)将给定对象转换为 JSON 输出。

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

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

加载 jQuery

我们需要在 image 详细信息模板中添加 AJAX 函数。为了在模板中使用 jQuery ,我们首先在 base.html 模板中进行加载。编辑 account 应用的 base.html 模板并在底端的</body> HTML 标签前添加以下代码:

<script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
<script>
    $(document).ready(function () {
        {% block domready %}
        {% endblock %}
    });
</script>

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

我们添加 <script> 标签来使用 JavaScript 代码,$(document).ready() 是一个 jQuery 函数,它的功能是 DOM 层次结构构件完成后执行该函数包含的代码。加载网页时浏览器以对象树的形式构建 DOM 。通过将函数放到这个函数内可以保证我们交互所需要的 HTML 元素已经包含在 DOM 中了。我们的代码只有在 DOM 加载完之后才能执行。

在 $(document).ready() 处理函数内容,我们使用了一个 domready 的模板块,这样扩展基础模板的模板可以使用特定的 JavaScript 。

不要将 JavaScript 代码和 Django 模板标签弄混了。Django 模板语言在服务器侧渲染并输出最终的 HTML ,JavaScript 在客户端执行。在某些案例中,使用 Django 动态生成 JavaScript 代码很有用。

注意:

在本章的例子中,我们将 JavaScript 代码放到 Django 模板中,更好的方法是使用.js 文件(静态文件的一种)加载 JavaScript 代码,特别是在脚本比较大的情况下。

AJAX 请求的 CSRF 防御


我们已经在第二章中了解了 CSRF 防御,CSRF 防御激活后,Django 将检查所有 POST 请求的 CSRF令牌(token)。提交表单时可以使用 {% csrf_token %} 模板标签与表单一起发送令牌。然而,AJAX 在每个 POST 请求中以 POST 数据的形式传输 CSRF 令牌却有些麻烦。因此,Django 提供了在 AJAX 请求中使用 CSRF 令牌的值设置一个自定义 X-CSRFToken 标头的方法。这样可以使用 jQuery 或任何其他 JavaScript 库为请求自动设置 X-CSRFToken 标头。

为了所有的请求都包含令牌,我们需要:

  1. 从 csrftoken cookie 中得到 CSRF 令牌(CSRF 防御激活时将设置 csrftoken cookie ) 。
  2. 在 AJAX 请求中使用 X-CSRFToken 标头发送令牌。

你可以在下面的网页找到更多关于 CSRF 防御和 AJAX 的信息https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax

编辑我们上次在 base.html 中添加的代码,使它看起来是这样的:

<script src="https://cdn.staticfile.org/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 require 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 插件,这样我们可以与 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令牌,这样代码简化为:

<script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
<script>
    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }

    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", '{{ csrf_token }}');
            }
        }
    });
    $(document).ready(function () {
        {% block domready %}
        {% endblock %}
    });
</script>

关于 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 的<div>更改为:

<div class="image-info">
    <div>
        <span class="count">
            <span class="total">{{ total_likes }}</span>
            like{{ total_likes|pluralize }}
        </span>
        <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 查询结果以避免重复执行。然后展示喜欢这个图片的用户数和一个包含喜欢/不喜欢这张图片的操作链接:基于检查用户是否在 users_like 相关对象集合中来展示喜欢或者不喜欢选项。在 <a> 元素添加下面的属性:

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

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

笔者注:

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

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

  1. 调用 AJAX 视图传输图片的 ID 和动作参数;
  2. 如果 AJAX 请求成功,使用相反的动作更新 HTML <a> 元素的 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 的 <a> 元素;

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

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

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

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

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

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

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

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

笔者注:

为了避免与第六章中添加的统计查看人数的<div>混淆,这里为

                <span class="count">
                    <span id="like" class="total">{{ total_likes }}</span>
                    like{{ total_likes|pluralize }}
                </span>

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

即 detail.html 中的代码变为:

{% extends "base.html" %}

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

{% block content %}
    <h1>{{ image.title }}</h1>
{#        <img src="{{ image.image.url }}" class="image-detail">#}
    {% load thumbnail %}
    {% thumbnail image.image "300" as im %}
        <a href="{{ image.image.url }}">
            <img src="{{ im.url }}" class="image-detail">
        </a>
    {% endthumbnail %}

    {% with total_likes=image.users_like.count users_like=image.users_like.all %}
        <div class="image-info">
            <div>
                <span class="count">
                    <span id="like" class="total">{{ total_likes }}</span>
                    like{{ total_likes|pluralize }}
                </span>
                <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>
        <div class="image-likes">
            {% for user in image.users_like.all %}
                <div>
                    <img src="{{ user.profile.photo_url|default_if_none:'#' }}">
                    <p>{{ user }}</p>
                </div>
                {% empty %}
                Nobody likes this image yet.
            {% endfor %}
        </div>
    {% 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 %}
    <div class="image">
        <a href="{{ image.get_absolute_url }}">
            {% thumbnail image.image "300x300" crop="100%" as im %}
                <a href="{{ image.get_absolute_url }}">
                    <img src="{{ im.url }}">
                </a>
            {% endthumbnail %}
        </a>
        <div class="info">
            <a href="{{ image.get_absolute_url }}" class="title">
                {{ image.title }}
            </a>
        </div>
    </div>
{% 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 %}

这段代码实现了无限滚动功能。我们将 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/ 。你将看到标记过的图片列表,看起来是这样的:

image_list.png

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

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

<li {% ifequal section "images" %}class="selected"{% endifequal %}>
    <a href="{% url 'images:list' %}" >Images</a>
</li>

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

总结


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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269