第五章 分享內(nèi)容到你的網(wǎng)站

5 分享內(nèi)容到你的網(wǎng)站

上一章中,你在網(wǎng)站中構(gòu)建了用戶注冊和認(rèn)證。你學(xué)會了如何為用戶創(chuàng)建自定義的個人資料模型,并添加了主流社交網(wǎng)站的社交認(rèn)證。

在這一章中,你會學(xué)習(xí)如何創(chuàng)建JavaScript書簽工具,來從其它網(wǎng)站分享內(nèi)容到你的網(wǎng)站,你還會使用jQuery和Django實(shí)現(xiàn)AJAX特性。

本章會覆蓋以下知識點(diǎn):

  • 創(chuàng)建多對多的關(guān)系
  • 定制表單行為
  • 在Django中使用jQuery
  • 構(gòu)建jQuery書簽工具
  • 使用sorl-thumbnail生成圖片縮略圖
  • 實(shí)現(xiàn)AJAX視圖,并與jQuery集成
  • 為視圖創(chuàng)建自定義裝飾器
  • 構(gòu)建AJAX分頁

5.1 創(chuàng)建圖片標(biāo)記網(wǎng)站

我們將允許用戶在其他網(wǎng)站上標(biāo)記和分享他們發(fā)現(xiàn)的圖片,并將其分享到我們的網(wǎng)站。為了實(shí)現(xiàn)這個功能,我們需要完成以下任務(wù):

  1. 定義一個存儲圖片和圖片信息的模型。
  2. 創(chuàng)建處理圖片上傳的表單和視圖。
  3. 為用戶構(gòu)建一個系統(tǒng),讓用戶可以上傳在其它網(wǎng)站找到的圖片。

首先在bookmarks項(xiàng)目目錄中,使用以下命令創(chuàng)建一個新的應(yīng)用:

django-admin startapp images

settings.py文件的INSTALLED_APPS設(shè)置中添加images

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

現(xiàn)在Django知道新應(yīng)用已經(jīng)激活了。

5.1.1 創(chuàng)建圖片模型

編輯images應(yīng)用的models.py文件,添加以下代碼:

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

class Image(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='images_created')
    title = models.CharField(max_length=200)
    slug = models.CharField(max_length=200, blank=True)
    url = models.URLField()
    image = models.ImageField(upload_to='/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

我們將使用這個模型存儲來自不同網(wǎng)站的被標(biāo)記的圖片。讓我們看看這個模型中的字段:

  • user:標(biāo)記這張圖片的User對象。這是一個ForeignKey字段,它指定了一對多的關(guān)系:一個用戶可以上傳多張圖片,但一張圖片只能由一個用戶上傳。
  • title:圖片的標(biāo)題。
  • slug:只包括字母,數(shù)據(jù),下劃線或連字符的短標(biāo)簽,用于構(gòu)建搜索引擎友好的URL。
  • url:圖片的原始URL。
  • image:圖片文件。
  • description:一個可選的圖片描述。
  • created:在數(shù)據(jù)庫中創(chuàng)建對象的時間。因?yàn)槲覀兪褂昧?code>auto_now_add,所以創(chuàng)建對象時會自動設(shè)置時間。我們使用了db_index=True,所以Django會在數(shù)據(jù)庫中為該字段創(chuàng)建一個索引。

數(shù)據(jù)庫索引會提高查詢效率??紤]為經(jīng)常使用filter()exclude()order_by()查詢的字段設(shè)置db_index=True。ForeignKey字段或帶unique=True的字段隱式的創(chuàng)建了索引。你也可以使用Meta.index_together為多個字段創(chuàng)建索引。

我們會覆寫Image模型的save()方法,根據(jù)title字段的值自動生成slug字段。在Image模型中導(dǎo)入slugify()函數(shù),并添加save()方法,如下所示:

from django.utils.text import slugify

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

沒有提供別名(slug)時,我們根據(jù)給定的標(biāo)題,使用Django提供的slufigy()函數(shù)自動生成圖片的slug字段。然后保存對象。我們?yōu)閳D片自動生成別名,所以用戶不需要為每張圖片輸入slug字段。

5.1.2 創(chuàng)建多對多的關(guān)系

我們將會在Image模型中添加另一個字段,用于存儲喜歡這張圖片的用戶。這種情況下,我們需要一個多對多的關(guān)系,因?yàn)橐粋€用戶可能喜歡多張圖片,每張圖片也可能被多個用戶喜歡。

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

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

當(dāng)你定義一個ManyToManyFeild時,Django會使用兩個模型的主鍵創(chuàng)建一張中介連接表。ManyToManyFeild可以在兩個關(guān)聯(lián)模型的任何一個中定義。

ForeignKey字段一樣,ManyToManyFeild允許我們命名從關(guān)聯(lián)對象到這個對象的逆向關(guān)系。ManyToManyFeild字段提供了一個多對多管理器,允許我們檢索關(guān)聯(lián)的對象,比如:image.users_like.all(),或者從user對象檢索:user.images_liked.all()。

打開命令行,執(zhí)行以下命令創(chuàng)建初始數(shù)據(jù)庫遷移:

python manage.py makemigrations images

你會看到以下輸出:

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

現(xiàn)在運(yùn)行這條命令,讓遷移生效:

python manage.py migrate images

你會看到包括這一行的輸出:

Applying images.0001_initial... OK

現(xiàn)在Image模型已經(jīng)同步到數(shù)據(jù)庫中。

5.1.3 在管理站點(diǎn)注冊圖片模型

編輯images應(yīng)用的admin.py文件,在管理站點(diǎn)注冊Image模型,如下所示:

from django.contrib import admin
from .models import Image

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

admin.site.register(Image, ImageAdmin)

執(zhí)行python manage.py runserver命令啟動開發(fā)服務(wù)器。在瀏覽器中打開http://127.0.0.1:8000/amdin/,可以看到Image模型已經(jīng)在管理站點(diǎn)注冊,如下圖所示:

5.2 從其它網(wǎng)站上傳內(nèi)容

我們將允許用戶標(biāo)記從其它網(wǎng)站找到的圖片。用戶將提供圖片的URL,一個標(biāo)題和一個可選的描述。我們的應(yīng)用會下載圖片,并在數(shù)據(jù)庫中創(chuàng)建一個新的Image對象。

我們從構(gòu)建一個提交新圖片的表單開始。在images應(yīng)用目錄中創(chuàng)建forms.py文件,并添加以下代碼:

from django import forms
from .models import Image

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

正如你所看到的,這是一個從Image模型創(chuàng)建的ModelForm表單,只包括title,urldescription字段。用戶不會直接在表單中輸入圖片URL。而是使用一個JavaScript工具,從其它網(wǎng)站選擇一張圖片,我們的表單接收這張圖片的URL作為參數(shù)。我們用HiddenInput組件覆蓋了url字段的默認(rèn)組件。這個組件渲染為帶有type="hidden"屬性的HTML輸入元素。使用這個組件是因?yàn)槲覀儾幌胗脩艨匆娺@個字段。

5.2.1 清理表單字段

為了確認(rèn)提供的圖片URL是有效的,我們會檢查文件名是否以.jpg.jpeg擴(kuò)展名結(jié)尾,只允許JPG文件。Django允許你通過形如clean_<filedname>()的方法,定義表單方法來清理指定字段。如果存在這個方法,它會在調(diào)用表單實(shí)例的is_valid()方法時執(zhí)行。在清理方法中,你可以修改字段的值,或者需要時,為這個字段拋出任何驗(yàn)證錯誤。在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獲得文件擴(kuò)展名,并檢查是否為合法的擴(kuò)展名。如果不是,拋出ValidationError,表單實(shí)例不會通過驗(yàn)證。我們執(zhí)行了一個非常簡單的驗(yàn)證。你可以使用更好的方法驗(yàn)證給定的URL是否提供了有效的圖片。

除了驗(yàn)證給定的URL,我們還需要下載并保存圖片。比如,我們可以用處理這個表單的視圖來下載圖片文件。不過我們會使用更通用的方式:覆寫模型表單的save()方法,在每次保存表單時執(zhí)行這個任務(wù)。

5.2.2 覆寫ModelForm的save()方法

你知道,ModelForm提供了save()方法,用于把當(dāng)前模型的實(shí)例保存到數(shù)據(jù)庫中,并返回該對象。這個方法接收一個commit布爾參數(shù),允許你指定是否把該對象存儲到數(shù)據(jù)庫中。如果commitFalsesave()方法會返回模型的實(shí)例,但不會保存到數(shù)據(jù)庫中。我們會覆寫表單的save()方法下載指定的圖片,然后保存。

forms.py文件頂部添加以下導(dǎo)入:

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

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

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

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

    if commit:
        image.save()
    return image

我們覆寫了save()方法,保留了ModelForm必需的參數(shù)。這段代碼完成以下操作:

  1. 我們用commit=False調(diào)用表單的save()方法,創(chuàng)建了一個新的image實(shí)例。
  2. 我們從表單的cleaned_data字典中獲得URL。
  3. 我們用image的標(biāo)題別名和原始文件擴(kuò)展名的組合生成圖片名。
  4. 我們使用urllib模塊下載圖片,然后調(diào)用image字段的save()方法,并傳遞一個ContentFile對象,這個對象由下載的文件內(nèi)容實(shí)例化。這樣就把文件保存到項(xiàng)目的media目錄中了。我們還傳遞了save=False參數(shù),避免把對象保存到數(shù)據(jù)庫中。
  5. 為了與被我們覆寫的save()方法保持一致的行為,只有在commit參數(shù)為True時,才把表單保存到數(shù)據(jù)庫中。

現(xiàn)在我們需要一個處理表單的視圖。編輯images應(yīng)用的views.py文件,添加以下代碼:

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

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

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

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

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

為了阻止未認(rèn)證用戶訪問,我們在image_create視圖上添加了login_required裝飾器。這個視圖是這樣工作的:

  1. 我們期望通過GET請求獲得創(chuàng)建表單實(shí)例的初始數(shù)據(jù)。數(shù)據(jù)由其它網(wǎng)站的圖片urltitle屬性組成,這個數(shù)據(jù)由我們之后會創(chuàng)建的JavaScript工具提供。現(xiàn)在我們假設(shè)初始的時候有數(shù)據(jù)。
  2. 如果提交了表單,我們檢查表單是否有效。如果有效,我們創(chuàng)建一個新的Image實(shí)例,但我們通過傳遞commit=False來阻止對象保存到數(shù)據(jù)庫中。
  3. 我們把當(dāng)前對象賦值給新的image對象。這樣就知道每張圖片是誰上傳的。
  4. 我們把圖片對象保存到數(shù)據(jù)庫中。
  5. 最后,我們用Django消息框架創(chuàng)建一條成功消息,并重定向到新圖片的標(biāo)準(zhǔn)URL。我們還沒有實(shí)現(xiàn)Image模型的get_absolute_url()方法,我們會馬上完成這個工作。

images應(yīng)用中創(chuàng)建urls.py文件,添加以下代碼:

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

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

編輯項(xiàng)目的主urls.py文件,引入我們剛創(chuàng)建的images應(yīng)用的模式:

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

最近,你需要創(chuàng)建模板來渲染表單。在images應(yīng)用目錄中創(chuàng)建以下目錄結(jié)構(gòu):

templates/
    images/
        image/
            create.html

編輯create.hmtl文件,添加以下代碼:

{% extends "base.html" %}

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

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

現(xiàn)在在瀏覽器中打開http://127.0.0.1:8000/images/create/?title=...&url=...,其中包括titleurl參數(shù),后者是現(xiàn)有的JPG圖片的URL。

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

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

你會看到帶一張預(yù)覽圖片的表單,如下所示:

添加描述并點(diǎn)擊Bookmark it!按鈕。一個新的Image對象會保存到數(shù)據(jù)庫中。你會得到一個錯誤,顯示Image對象沒有get_absolute_url()方法?,F(xiàn)在不用擔(dān)心,我們之后會添加這個方法。在瀏覽器打開http://127.0.0.1:8000/admin/images/image/,確認(rèn)新的圖片對象已經(jīng)保存了。

5.2.3 用jQuery構(gòu)建書簽工具

書簽工具是保存在web瀏覽器中的書簽,其中包括JavaScript代碼,可以擴(kuò)展瀏覽器的功能。當(dāng)你點(diǎn)擊書簽,JavaScript代碼會在瀏覽器正在顯示的網(wǎng)頁中執(zhí)行。這對構(gòu)建與其它網(wǎng)站交互的工具非常有用。

某些在線服務(wù)(比如Pinterest)實(shí)現(xiàn)了自己的書簽工具,讓用戶可以在自己的平臺分享其它網(wǎng)站的內(nèi)容。我們會創(chuàng)建一個書簽工具,讓用戶以類似的方式在我們的網(wǎng)站中分享其它網(wǎng)站的圖片。

我們將使用jQuery構(gòu)建書簽工具。jQuery是一個非常流行的JavaScript框架,可以快速開發(fā)客戶端的功能。你可以在官方網(wǎng)站進(jìn)一步了解jQuery。

以下是用戶如何在瀏覽器中添加書簽工具,并使用它:

  1. 用戶從你的網(wǎng)站中拖拽一個鏈接到瀏覽器的書簽中。該鏈接在href屬性中包含JavaScript代碼。這段代碼會存儲在書簽中。
  2. 用戶導(dǎo)航到任意網(wǎng)站,并點(diǎn)擊該書簽。該書簽的JavaScript代碼會執(zhí)行。

因?yàn)镴avaScript代碼會以書簽的形式存儲,所以之后你不能更新它。這是一個顯著的缺點(diǎn),但你可以實(shí)現(xiàn)一個簡單的啟動腳本解決這個問題。該腳本從URL中加載實(shí)際的JavaScript書簽工具。用戶會以書簽的形式保存啟動腳本,這樣你就可以在任何時候更新書簽工具的代碼了。我們將采用這種方式構(gòu)建書簽工具。讓我們開始吧。

images/templates/中創(chuàng)建一個bookmarklet_launcher.js模板。這是啟動腳本,并添加以下代碼:

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

這個腳本檢查是否定義myBookmarklet變量,來判斷書簽工具是否加載。這樣我們就避免了用戶重復(fù)點(diǎn)擊書簽時多次加載它。如果沒有定義myBookmarklet,我們通過在文檔中添加<script>元素,來加載另一個JavaScript文件。這個scrip標(biāo)簽加載bookmarklet.js腳本,并用一個隨機(jī)參數(shù)作為變量,防止從瀏覽器緩存中加載文件。

真正的書簽工具代碼位于靜態(tài)文件bookmarklet.js中。這樣就能更新我們的書簽工具代碼,而不用要求用戶更新之前在瀏覽器中添加的書簽。讓我們把書簽啟動器添加到儀表盤頁面,這樣用戶就可以拷貝到他們的書簽中。

編輯account應(yīng)用中的account/dashboard.html模板,如下所示:

{% extends "base.html" %}

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

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

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

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

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

儀表盤顯示用戶現(xiàn)在添加書簽的圖片總數(shù)。我們使用{% with %}模板標(biāo)簽設(shè)置當(dāng)前用戶添加書簽的圖片總數(shù)為一個變量。我們還包括了一個帶href屬性的鏈接,該屬性指向書簽工具啟動腳本。我們從bookmarklet_launcher.js模板中引入這段JavaScript代碼。

在瀏覽器中打開http://127.0.0.1:8000/account/,你會看下如下所示的頁面:

拖拽Bookmark it!鏈接到瀏覽器的書簽工具欄中。

現(xiàn)在,在images應(yīng)用目錄中創(chuàng)建以下目錄和文件:

static/
    js/
        bookmarklet.js

你在本章示例代碼的images應(yīng)用目錄下可以找到static/css目錄??截?code>css/目錄到你代碼的static/目錄中。css/bookmarklet.css文件為我們的JavaScript書簽工具提供了樣式。

編輯bookmarklet.js靜態(tài)文件,添加以下JavaScript代碼:

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

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

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

這是主要的jQuery加載腳本。如果當(dāng)前網(wǎng)站已經(jīng)加載了jQuery,那么它會使用jQuery;否則會從Google CDN中加載jQuery。加載jQuery后,它會執(zhí)行包含書簽工具代碼的bookmarklet()函數(shù)。我們還在文件頂部設(shè)置了幾個變量:

  • jquery_version:要加載的jQuery版本
  • site_urlstatic_url:我們網(wǎng)站的主URL和各個靜態(tài)文件的主URL
  • min_widthmin_height:我們的書簽工具在網(wǎng)站中查找的圖片的最小寬度和高度(單位是像素)

現(xiàn)在讓我們實(shí)現(xiàn)bookmarklet()函數(shù),如下所示:

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. 為了避免瀏覽器緩存,我們使用一個隨機(jī)數(shù)作為參數(shù),來加載bookmarklet.css樣式表。
  2. 我們添加了定制的HTML到當(dāng)前網(wǎng)站的<body>元素中。它由一個<div>元素組成,里面會包括在當(dāng)前網(wǎng)頁中找到的圖片。
  3. 我們添加了一個事件。當(dāng)用戶點(diǎn)擊我們的HTML塊中的關(guān)閉鏈接時,我們會從文檔中移除我們的HTML。我們使用#bookmarklet #close選擇器查找ID為close,父元素ID為bookmarklet的HTML元素。一個jQuery選擇器允許你查找多個HTML元素。一個jQuery選擇器返回指定CSS選擇器找到的所有元素。你可以在這里找到j(luò)Query選擇器列表。

加載CSS樣式和書簽工具需要的HTML代碼后,我們需要找到網(wǎng)站中的圖片。在bookmarklet()函數(shù)底部添加以下JavaScript代碼:

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

這段代碼使用img[src$="jpg"]選擇器查找所有src屬性以jpg結(jié)尾的<img>元素。這意味著我們查找當(dāng)前網(wǎng)頁中顯示的所有JPG圖片。我們使用jQuery的each()方法迭代結(jié)果。我們把尺寸大于min_widthmin_height變量的圖片添加到<div class="images">容器中。

現(xiàn)在,HTML容器中包括可以添加標(biāo)簽圖片。我們希望用戶點(diǎn)擊需要的圖片,并為它添加標(biāo)簽。在bookmarklet()函數(shù)底部添加以下代碼:

// 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. 當(dāng)用戶點(diǎn)擊一張圖片時,我們設(shè)置一個新變量——selected_image,其中包含了選中圖片的URL。
  3. 我們隱藏書簽工具,并在我們網(wǎng)站中打開一個新的瀏覽器窗口為新圖片添加標(biāo)簽。傳遞網(wǎng)站的<title>元素和選中的圖片URL作為GET參數(shù)。

在瀏覽器中隨便打開一個網(wǎng)址(比如http://z.cn),并點(diǎn)擊你的書簽工具。你會看到一個新的白色框出現(xiàn)在網(wǎng)頁上,其中顯示所有尺寸大于100*100px的JPG圖片,如下圖所示:

因?yàn)槲覀兪褂玫氖荄jango開發(fā)服務(wù)器,它通過HTTP提供頁面,所以出于瀏覽器安全限制,書簽工具不能在HTTPS網(wǎng)站上工作。

如果你點(diǎn)擊一張圖片,會重定向到圖片創(chuàng)建頁面,并傳遞網(wǎng)站標(biāo)題和選中圖片的URL作為GET參數(shù):

恭喜你!這是你的第一個JavaScript書簽工具,并且完全集成到你的Django項(xiàng)目中了。

5.3 為圖片創(chuàng)建詳情視圖

我們將創(chuàng)建一個簡單的詳情視圖,用于顯示一張保存在我們網(wǎng)站的圖片。打開images應(yīng)用的views.py文件,添加以下代碼:

from django.shortcuts import get_object_or_404
from .models import Image

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

這是顯示一張圖片的簡單視圖。編輯images應(yīng)用的urls.py文件,添加以下URL模式:

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

編輯images應(yīng)用的models.py文件,在Image模型中添加get_absolute_url()方法,如下所示:

from django.core.urlresolvers import reverse

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

記住,為對象提供標(biāo)準(zhǔn)URL的通用方式是在模型中添加get_absolute_url()方法。

最后,在images應(yīng)用的/images/image/模板目錄中創(chuàng)建detail.html模板,添加以下代碼:

{% extends "base.html" %}

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

{% block content %}
    <h1>{{ image.title }}</h1>
    ![]({{ image.image.url }})
    {% 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>
                    ![]({{ user.profile.photo.url }})
                    <p>{{ user.first.name }}</p>
                </div>
            {% empty %}
                Nobody likes this image yet.
            {% endfor %}
        </div>
    {% endwith %}
{% endblock  %}

這是顯示一張?zhí)砑恿藰?biāo)簽的圖片的詳情模板。我們使用{% with %}標(biāo)簽存儲QuerySet的結(jié)果,這個QuerySettotal_likes變量中統(tǒng)計(jì)所有喜歡這張圖片的用戶。這樣就能避免計(jì)算同一個QuerySet兩次。我們還包括了圖片的描述,并迭代image.users_like.all()來顯示所有喜歡這張圖片的用戶。

使用{% width %}模板標(biāo)簽可以有效地阻止Django多次計(jì)算QuerySet。

現(xiàn)在用書簽工具標(biāo)記一張新圖片。當(dāng)你上傳圖片后,會重定向到圖片詳情頁面。該頁面會包括一條成功消息,如下圖所示:

5.4 使用sorl-thumbnail創(chuàng)建縮略圖

現(xiàn)在,我們在詳情頁面顯示原圖,但是不同圖片的尺寸各不相同。同時,有些圖片的源文件可能很大,需要很長時間才能加載。用統(tǒng)一的方式顯示優(yōu)化圖像的最好方法是生成縮略圖。因此我們將使用一個名為sorl-thumbnail的Django應(yīng)用。

打開終端,執(zhí)行以下命令安裝sorl-thumbnail

pip install sorl-thumbnail

編輯bookmarks項(xiàng)目的settings.py文件,把sorl.thumbnail添加到INSTALLED_APPS設(shè)置中:

接著執(zhí)行以下命令同步應(yīng)用和數(shù)據(jù)庫:

python manage.py makemigrations thumbnail
python manage.py migrate

sorl-thumbnail應(yīng)用提供了多種定義圖片縮略圖的方式。它提供了{% thumbnail %}模板標(biāo)簽,可以在模板中生成縮略圖;如果你想在模型中定義縮略圖,還提供自定義的ImageField。我們將使用模板標(biāo)簽的方式。編輯images/image/detail.html模板,把這一行代碼:

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

替換為:

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

我們在這里定義了一張固定寬度為300像素的縮略圖。用戶第一次加載這個頁面時,會創(chuàng)建一張縮略圖。之后的請求會使用生成的縮略圖。用python manage.py runserver啟動開發(fā)服務(wù)器后,訪問一張已存在的圖片詳情頁。此時會生成一張縮略圖并顯示。

sorl-thumbmail應(yīng)用提供了一些選項(xiàng)來定制縮略圖,包括裁剪算法和不同的效果。如果你在生成縮略圖時遇到問題,可以在設(shè)置中添加THUMBNAIL_DEBUG=True,就能查看調(diào)試信息。你可以在這里閱讀sorl-thumbnail應(yīng)用的完整文檔。

5.5 使用JQuery添加AJAX操作

現(xiàn)在我們將向應(yīng)用中添加AJAX操作。AJAX是Asynchronous JavaScript and XML的縮寫。這個術(shù)語包括一組異步HTTP請求技術(shù)。它包括從服務(wù)器異步發(fā)送和接收數(shù)據(jù),而不用加載整個頁面。盡管名字中有XML,但它不是必需的。你可以使用其它格式發(fā)送或接收數(shù)據(jù),比如JSON,HTML或者普通文本。

我們將會在圖片詳情頁面添加一個鏈接,用戶點(diǎn)擊鏈接表示喜歡這張圖片。我們會用AJAX執(zhí)行這個操作,避免加載整個頁面。首先,我們需要創(chuàng)建一個視圖,讓用戶喜歡或不喜歡圖片。編輯images應(yīng)用的views.py文件,添加以下代碼:

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

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

我們在這個視圖上使用了兩個裝飾器。login_required裝飾阻止沒有登錄的用戶訪問這個視圖;如果HTTP請求不是通過POST完成,required_ POST裝飾器返回一個HttpResponseNotAllowed對象(狀態(tài)碼為405)。這樣就只允許POST請求訪問這個視圖。Django還提供了required_GET裝飾器,只允許GET請求,以及required_http_methods裝飾器,你可以把允許的方法列表作為參數(shù)傳遞。

我們在這個視圖中使用了兩個POST參數(shù):

  • image_id:用戶執(zhí)行操作的圖片對象的ID。
  • action:用戶希望執(zhí)行的操作,我們假設(shè)為likeunlike字符串。

我們使用Django為Image模型的users_like多對多字段提供的管理器的add()remove()方法從關(guān)系中添加或移除對象。調(diào)用add()方法時,如果傳遞一個已經(jīng)存在關(guān)聯(lián)對象集中的對象,不會重復(fù)添加這個對象;同樣,調(diào)用remove()方法時,如果傳遞一個不存在關(guān)聯(lián)對象集中的對象,不會執(zhí)行任何操作。另一個多對多管理器方法是clear(),會從關(guān)聯(lián)對象集中移除所有對象。

最后,我們使用Django提供的JsonResponse類返回一個HTTP響應(yīng),其中內(nèi)容類型為application/json,它會把給定對象轉(zhuǎn)換為JSON輸出。

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

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

5.5.1 加載jQuery

我們需要添加AJAX功能到圖片詳情模板中。為了在模板中使用jQuery,首先在項(xiàng)目的base.html模板中引入它。編輯account應(yīng)用的base.html模板,在</body>標(biāo)簽之前添加以下代碼:

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

我們從Google加載jQuery框架,Google在高速內(nèi)容分發(fā)網(wǎng)絡(luò)中托管了流行的JavaScript框架。你也可以從http://jquery.com/下載jQuery,然后把它添加到應(yīng)用的static目錄中。

我們添加一個<script>標(biāo)簽來包括JavaScript代碼。$(document).ready()是一個jQuery函數(shù),參數(shù)是一個處理函數(shù),當(dāng)DOM層次構(gòu)造完成后,會執(zhí)行這個處理函數(shù)。DOM是Document Object Model的縮寫。DOM是網(wǎng)頁加載時瀏覽器創(chuàng)建的一個樹對象。在這個函數(shù)中包括我們的代碼,可以確保我們要交互的HTML元素都已經(jīng)在DOM中加載完成。我們的代碼只有在DOM準(zhǔn)備就緒后才執(zhí)行。

在文檔準(zhǔn)備就緒后的處理函數(shù)中,我們包括了一個名為domready的Django模板塊,在擴(kuò)展了基礎(chǔ)模板的模板中可以包括特定的JavaScript。

不要將JavaScript代碼和Django模板標(biāo)簽搞混了。Django模板語言在服務(wù)端渲染,輸出為最終的HTML文檔;JavaScript在客戶端執(zhí)行。某些情況下,使用Django動態(tài)生成JavaScript非常有用。

本章的示例中,我們在Django模板中引入了JavaScript代碼。引入JavaScript代碼更好的方式是加載作為靜態(tài)文件的.js文件,尤其當(dāng)它們是代碼量很大的腳本時。

5.5.2 AJAX請求的跨站請求偽造

你已經(jīng)在第二章中學(xué)習(xí)了跨站點(diǎn)請求偽造。在激活了CSRF保護(hù)的情況下,Django會檢查所有POST請求的CSRF令牌。當(dāng)你提交表單時,可以使用{% csrf_token %}模板標(biāo)簽發(fā)送帶令牌的表單。但是,對于每個POST請求,AJAX請求都將CSRF令牌作為POST數(shù)據(jù)傳遞是不方便的。因此,Django允許你在AJAX請求中,用CSRF令牌的值設(shè)置一個自定義的X-CSRFToken頭。這允許你用jQuery或其它任何JavaScript庫,在每次請求中自動設(shè)置X-CSRFToken頭。

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

  1. csrftoken cookie中檢索CSRF令牌,如果激活了CSRF,它就會被設(shè)置。
  2. 在AJAX請求中,使用X-CSRFToken頭發(fā)送令牌。

你可以在這里找到更多關(guān)于CSRF保護(hù)和AJAX的信息。

編輯你在base.html中最后引入的代碼,修改為以下代碼:

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

這段代碼完成以下工作:

  1. 我們從一個公有CDN加載jQuery Cookie插件,因此我們可以與cookies交互。
  2. 我們讀取csrftoken cookie中的值。
  3. 我們定義csrfSafeMethod()函數(shù),檢查HTTP方法是否安全。安全的方法不需要CSRF保護(hù),包括GET,HEAD,OPTIONSTRACE
  4. 我們使用$.ajaxSetup()設(shè)置jQuery AJAX請求。每個AJAX請求執(zhí)行之前,我們檢查請求方法是否安全,以及當(dāng)前請求是否跨域。如果請求不安全,我們用從cookie中獲取的值設(shè)置X-CSRFToken頭。這個設(shè)置會應(yīng)用到j(luò)Query執(zhí)行的所有AJAX請求。

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

5.5.3 使用jQuery執(zhí)行AJAX請求

編輯images應(yīng)用的images/image/detail.htmlt模板,把這一行代碼:

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

替換為下面這行:

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

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

<div class="image-info">
    <div>
        <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 %}模板標(biāo)簽中,用于存儲image.users_like.all的查詢結(jié)果,避免執(zhí)行兩次查詢。我們顯示喜歡這張圖片的用戶總數(shù),以及一個like/unlike鏈接:我們檢查用戶是否在users_like關(guān)聯(lián)對象集中,根據(jù)當(dāng)前用戶跟這樣圖片的關(guān)系顯示likeunlike。我們在<a>元素中添加了以下屬性:

  • data-id:顯示的圖片的ID。
  • data-action:用戶點(diǎn)擊鏈接時執(zhí)行的操作??赡苁?code>like或unlike

我們將會在AJAX請求發(fā)送這兩個屬性的值給image_like視圖。當(dāng)用戶點(diǎn)擊like/unlike鏈接時,我們需要在客戶端執(zhí)行以下操作:

  1. 調(diào)用AJAX視圖,并傳遞圖片ID和action參數(shù)。
  2. 如果AJAX請求成功,用相反操作(like/unlike)更新<a>元素的data-action屬性,并相應(yīng)的修改顯示文本。
  3. 更新顯示的喜歡總數(shù)。

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

{% 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') jQuery選擇器查找HTML文檔中classlike<a>元素。
  2. 我們?yōu)?code>click事件定義了一個處理函數(shù)。用戶每次點(diǎn)擊like/unlike鏈接時,會執(zhí)行這個函數(shù)。
  3. 在處理函數(shù)內(nèi)部,我們使用e.preventDefault()阻止<a>元素的默認(rèn)行為。這會阻止鏈接調(diào)轉(zhuǎn)到其它地方。
  4. 我們使用$.post()向服務(wù)器執(zhí)行一個異步請求。jQuery還提供了執(zhí)行GET請求的$.get()方法,以及一個底層的$.ajax()方法。
  5. 我們使用Django的{% url %}模板標(biāo)簽為AJAX請求構(gòu)建URL。
  6. 我們構(gòu)建在請求中發(fā)送的POST參數(shù)字典。我們的Django視圖需要idaction參數(shù)。我們從<a>元素的<data-id><data-action>屬性中獲得這兩個值。
  7. 我們定義了回調(diào)函數(shù),當(dāng)收到HTTP響應(yīng)時,會執(zhí)行這個函數(shù)。它的data屬性包括響應(yīng)的內(nèi)容。
  8. 我們訪問收到的datastatus屬性,檢查它是否等于ok。如果返回的data是期望的那樣,我們切換鏈接的data-action屬性和文本。這樣就允許用戶取消這個操作。
  9. 根據(jù)執(zhí)行的操作,我們在喜歡的總數(shù)上加1或減1。

在瀏覽器中打開之前上傳的圖片詳情頁面。你會看到以下初始的喜歡總數(shù)和LIKE按鈕:

點(diǎn)擊LIKE按鈕。你會看到喜歡總數(shù)加1,并且按鈕的文本變?yōu)?code>UNLIKE:

當(dāng)你點(diǎn)擊UNLIKE按鈕時,會執(zhí)行這個操作,按鈕的文本變回LIKE,總數(shù)也會相應(yīng)的改變。

編寫JavaScript代碼時,尤其是執(zhí)行AJAX請求時,推薦使用Firebug等調(diào)試工具。Firebug是一個Firefox插件,允許你調(diào)試JavaScript代碼,并監(jiān)控CSS和HTML的變化。你可以從這里下載Firebug。其它瀏覽器,比如Chrome或Safari,也有調(diào)試JavaScript的開發(fā)者工具。在這些瀏覽器中,右鍵網(wǎng)頁中的任何一個地方,然后點(diǎn)擊Inspect element訪問開發(fā)者工具。

5.6 為視圖創(chuàng)建自定義裝飾器

我們將限制AJAX視圖只允許由AJAX發(fā)起的請求。Django的Request對象提供一個is_ajax()方法,用于檢查請求是否帶有XMLHttpRequest,也就是說是否是一個AJAX請求。這個值在HTTP頭的HTTP_X_REQUESTED_WITH中設(shè)置,絕大部分JavaScript庫都會在AJAX請求中包括它。

我們將創(chuàng)建一個裝飾器,用于在視圖中檢查HTTP_X_REQUESTED_WITH頭。裝飾器是一個接收另一個函數(shù)為參數(shù)的函數(shù),并且不需要顯式修改作為參數(shù)的函數(shù),就能擴(kuò)展它的行為。如果你忘了裝飾器的概念,你可能需要先閱讀這里。

因?yàn)檫@是一個通用的裝飾器,可以作用于任何視圖,所以我們會在項(xiàng)目中創(chuàng)建一個common包。在bookmarks項(xiàng)目目錄中創(chuàng)建以下目錄和文件:

common/
    __init__.py
    decrorators.py

編輯decrorators.py文件,添加以下代碼:

from django.http import HttpResponseBadRequest

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

這是我們自定義的ajax_required裝飾器。它定義了一個wrap函數(shù),如果不是AJAX請求,則返回HttpResponseBadRequest對象(HTTP 400)。否則返回被裝飾的函數(shù)。

現(xiàn)在編輯images應(yīng)用的views.py文件,添加這個裝飾器到image_like視圖中:

from common.decrorators import ajax_required

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

如果你直接在瀏覽器中訪問http://127.0.0.1:8000/images/like/,你會得到一個HTTP 400的響應(yīng)。

如果你在多個視圖中重復(fù)同樣的驗(yàn)證,則可以為視圖構(gòu)建自定義裝飾器。

5.7 為列表視圖創(chuàng)建AJAX分頁

我們需要在網(wǎng)站中列出所有標(biāo)記過的圖片。我們將使用AJAX分頁構(gòu)建一個無限滾動功能。當(dāng)用戶滾動到頁面底部時,通過自動加載下一頁的結(jié)果實(shí)現(xiàn)無限滾動。

我們將實(shí)現(xiàn)一個圖片列表視圖,同時處理標(biāo)準(zhǔn)瀏覽器請求和包括分頁的AJAX請求。當(dāng)用戶首次加載圖片列表頁面,我們顯示第一頁的圖片。當(dāng)用戶滾動到頁面底部,我們通過AJAX加載下一頁的項(xiàng),并添加到主頁面的底部。

我們用同一個視圖處理標(biāo)準(zhǔn)和AJAX分頁。編輯images應(yīng)用的views.py文件,添加以下代碼:

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

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

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

我們在這個視圖中創(chuàng)建了一個返回?cái)?shù)據(jù)庫中所有圖片的QuerySet。然后我們構(gòu)造了一個Paginator對象來分頁查詢結(jié)果,每頁顯示八張圖片。如果請求的頁數(shù)超出范圍,則拋出EmptyPage異常。這種情況下,如果是通過AJAX發(fā)送請求,則返回一個空的HttpResponse對象,幫助我們在客戶端停止AJAX分頁。我們把結(jié)果渲染到兩個不同的模板中:

  • 對于AJAX請求,我們渲染list_ajax.html模板。該模板只包括請求頁的圖片。
  • 對于標(biāo)準(zhǔn)請求,我們渲染list.html模板。該模板繼承自base.html模板,并顯示整個頁面,同時還包括list_ajax.html模板,用來引入圖片列表。

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

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

最后,我們需要創(chuàng)建上面提到的模板。在images/image/模板目錄中創(chuàng)建list_ajax.html模板,添加以下代碼:

{% load thumbnail %}

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

這個模板顯示圖片的列表。我們將用它返回AJAX請求的結(jié)果。在同一個目錄下創(chuàng)建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模板。為了避免重復(fù)代碼,我們引入了list_ajax.html模板來顯示圖片。list.html模板會包括JavaScript代碼,當(dāng)用戶滾動到頁面底部時,負(fù)責(zé)加載額外的頁面。

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

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

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

這段代碼提供了無限滾動功能。我們在base.html模板中定義的domready塊中引入了JavaScript代碼。這段代碼是這樣工作的:

  1. 我們定義了以下變量:
  • page:存儲當(dāng)前頁碼。
  • empty_page:讓我們知道是否到了最后一頁,如果是則接收一個空的頁面。只要我們得到一個空的頁面,就會停止發(fā)送額外的AJAX請求,因?yàn)槲覀兗僭O(shè)此時沒有結(jié)果了。
  • block_request:正在處理AJAX請求時,阻止發(fā)送另一個請求。
  1. 我們使用$(window).scroll()捕獲滾動事件,并為它定義一個處理函數(shù)。
  2. 我們計(jì)算邊距變量來獲得文檔總高度和窗口高度之間的差值,這是用戶滾動的剩余內(nèi)容的高度。我們從結(jié)果中減去200,因此,當(dāng)用戶距離頁面底部小于200像素時,我們會加載下一頁。
  3. 如果沒有執(zhí)行其它AJAX請求(block_request必須為false),并且用戶沒有獲得最后一頁的結(jié)果時(empty_page也為false),我們才發(fā)送AJAX請求。
  4. 我們設(shè)置block_requesttrue,避免滾動事件觸發(fā)額外的AJAX請求,同時給page計(jì)算器加1來獲取下一頁。
  5. 我們使用$.get()執(zhí)行AJAX GET請求,然后在名為data的變量中接收HTML響應(yīng)。這里有兩種情況:
  • 響應(yīng)不包括內(nèi)容:我們已經(jīng)到了結(jié)果的末尾,沒有更多頁面需要加載。我們設(shè)置empty_pagetrue阻止額外的AJAX請求。
  • 響應(yīng)包括內(nèi)容:我們把數(shù)據(jù)添加到id為image-list的HTML元素中。當(dāng)用戶接近頁面底部時,頁面內(nèi)容會垂直擴(kuò)展附加的結(jié)果。

在瀏覽器中打開http://127.0.0.1:8000/images/,你會看到目前已經(jīng)標(biāo)記過的圖片列表,如下圖所示:

滾動到頁面底部來加載下一頁。確保你用書簽工具標(biāo)記了八張以上圖片,因?yàn)槲覀兠宽擄@示八張圖片。記住,你可以使用Firebug或類似工具追蹤AJAX請求和調(diào)試JavaScript代碼。

最后,編輯account應(yīng)用的base.html模板,為主菜單的Images項(xiàng)添加URL:

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

現(xiàn)在你可以從主菜單中訪問圖片列表。

5.8 總結(jié)

在本章中,我們構(gòu)建了一個JavaScript書簽工具,可以分享其它網(wǎng)站的圖片到我們的網(wǎng)站中。你用jQuery實(shí)現(xiàn)了AJAX視圖,并添加了AJAX分頁。

下一章會教你如何構(gòu)建關(guān)注系統(tǒng)和活動流。你會使用通用關(guān)系(generic relations),信號(signals)和反規(guī)范化(denormalization)。你還會學(xué)習(xí)如何在Django中使用Redis。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容