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 防御的使用方法見:http://www.lxweimin.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 的新特定,詳細說明見 http://www.lxweimin.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閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370