第七章 構(gòu)建在線商店

7 構(gòu)建在線商店

在上一章中,你創(chuàng)建了關(guān)注系統(tǒng)和用戶活動流。你還學(xué)習(xí)了Django信號是如何工作的,并在項(xiàng)目中集成了Redis,用于計算圖片的瀏覽次數(shù)。在這一章中,你會學(xué)習(xí)如何構(gòu)建一個基本的在線商店。你會創(chuàng)建商品目錄(catalog),并用Django會話(session)實(shí)現(xiàn)購物車。你還會學(xué)習(xí)如果創(chuàng)建自定義上下文管理器,以及用Celery啟動異步任務(wù)。

在這一章中,你會學(xué)習(xí):

  • 創(chuàng)建商品目錄
  • 使用Django會話創(chuàng)建購物車
  • 管理客戶訂單
  • 用Celery給客戶發(fā)送異步通知

7.1 創(chuàng)建在線商店項(xiàng)目

我們將創(chuàng)建一個新的Django項(xiàng)目來構(gòu)建在線商店。用戶可以通過商品目錄瀏覽,并把商品添加到購物車中。最后,客戶結(jié)賬并下單。本章將會覆蓋在線商店以下幾個功能:

  • 創(chuàng)建商品目錄模型,把它們添加到管理站點(diǎn),并創(chuàng)建一個基礎(chǔ)視圖,用于顯示目錄
  • 使用Django會話構(gòu)建購物車系統(tǒng),允許用戶瀏覽網(wǎng)站時保留選定的商品
  • 創(chuàng)建用于下單的表單和功能
  • 用戶下單后,發(fā)送一封異步確認(rèn)郵件給用戶

首先,我們?yōu)樾马?xiàng)目創(chuàng)建虛機(jī)環(huán)境,并用以下命令激活虛擬環(huán)境:

mkdiv env
virtualenv env/myshop
source env/myshop/bin/activate

使用以下命令在虛擬環(huán)境中安裝Django:

pip install Django

打開終端,執(zhí)行以下命令,創(chuàng)建myshop項(xiàng)目,以及shop應(yīng)用:

django-admin startproject myshop
cd myshop
django-admin startapp shop

編輯項(xiàng)目的settings.py文件,在INSTALLED_APPS設(shè)置中添加shop應(yīng)用:

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

現(xiàn)在項(xiàng)目中的應(yīng)用已經(jīng)激活。讓我們?yōu)樯唐纺夸浂x模型。

7.1.1 創(chuàng)建商品目錄模型

商店的目錄由屬于不同類別的商品組成。每個商品有名字,可選的描述,可選的圖片,價格,以及有效的庫存。編輯你剛創(chuàng)建的shop應(yīng)用的models.py文件,添加以下代碼:

from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=200, db_index=True)
    slug = models.SlugField(max_length=200, db_index=True, unique=True)

    class Meta:
        ordering = ('name', )
        verbose_name = 'category'
        verbose_name_plural = 'categories'
    
    def __str__(self):
        return self.name

class Product(models.Model):
    category = models.ForeignKey(Category, related_name='products')
    name = models.CharField(max_length=200, db_index=True)
    slug = models.SlugField(max_length=200, db_index=True)
    image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
    description = models.TextField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.PositiveIntegerField()
    available = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ('name', )
        index_together = (('id', 'slug'), )

    def __str__(self):
        return self.name

這是我們的CategoryProduct模型。Category模型由name字段和唯一的slug字段組成。Product模型包括以下字段:

  • category:這是指向Catetory模型的ForeignKey。這是一個多對一的關(guān)系:一個商品屬于一個目錄,而一個目錄包括多個商品。
  • name:這是商品的名稱。
  • slug:這是商品的別名,用于構(gòu)建友好的URL。
  • image:這是一張可選的商品圖片。
  • description:這是商品的可選描述。
  • price:這是DecimalField。這個字段用Python的decimal.Decimal類型存儲固定精度的十進(jìn)制數(shù)。使用max_digits屬性設(shè)置最大的位數(shù)(包括小數(shù)位),使用decimal_places屬性設(shè)置小數(shù)位。
  • stock:這個PositiveIntegerField存儲商品的庫存。
  • available:這個布爾值表示商品是否有效。這允許我們在目錄中啟用或禁用商品。
  • created:對象創(chuàng)建時存儲該字段。
  • updated:對象最后更新時存儲該字段。

對于price字段,我們使用DecimalField代替FloatField,來避免四舍五入的問題。

總是使用DecimalField存儲貨幣值。在Python內(nèi)部,FloatField使用float類型,而DecimalField使用Decimal類型。使用Decimal類型可以避免float的四舍五入問題。

Product模型的Meta類中,我們用index_together元選項(xiàng)為idslug字段指定共同索引。這是因?yàn)槲覀冇媱澩ㄟ^idslug來查詢商品。兩個字段共同索引可以提升用這兩個字段查詢的性能。

因?yàn)槲覀円谀P椭刑幚韴D片,打開終端,用以下命令安裝Pillow

pip install Pillow

現(xiàn)在,執(zhí)行以下命令,創(chuàng)建項(xiàng)目的初始數(shù)據(jù)庫遷移:

python manage.py makemigrations

你會看到以下輸出:

Migrations for 'shop':
  shop/migrations/0001_initial.py
    - Create model Category
    - Create model Product
    - Alter index_together for product (1 constraint(s))

執(zhí)行以下命令同步數(shù)據(jù):

python manage.py migrate

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

Applying shop.0001_initial... OK

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

7.1.2 在管理站點(diǎn)注冊目錄模型

讓我們把模型添加到管理站點(diǎn),從而可以方便的管理目錄和商品。編輯shop應(yīng)用的admin.py文件,添加以下代碼:

from django.contrib import admin
from .models import Category, Product

class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug')
    prepopulated_fields = {'slug': ('name', )}
admin.site.register(Category, CategoryAdmin)

class ProductAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug', 'price', 'stock', 'available', 'created', 'updated')
    list_filter = ('available', 'created', 'updated')
    list_editable = ('price', 'stock', 'available')
    prepopulated_fields = {'slug': ('name', )}
admin.site.register(Product, ProductAdmin)

記住,我們使用prepopulated_fields屬性指定用其它字段的值自動填充的字段。正如你前面看到的,這樣可以很容易的生成別名。我們在ProductAdmin類中使用list_editable屬性設(shè)置可以在管理站點(diǎn)的列表顯示頁面編輯的字段。這樣可以一次編輯多行。list_editable屬性中的所有字段都必須列在list_display屬性中,因?yàn)橹挥酗@示的字段才可以編輯。

現(xiàn)在使用以下命令為網(wǎng)站創(chuàng)建超級用戶:

python manage.py createsuperuser

執(zhí)行python manage.py runserver命令啟動開服務(wù)器。在瀏覽器中打開http://127.0.0.1:8000/admin/shop/product/add/,然后用剛創(chuàng)建的用戶登錄。使用管理界面添加一個新的目錄和商品。管理頁面的商品修改列表頁面看起來是這樣的:

7.1.3 構(gòu)建目錄視圖

為了顯示商品目錄,我們需要創(chuàng)建一個視圖列出所有商品,或者通過制定的目錄過濾商品。編輯shop應(yīng)用的views.py文件,添加以下代碼:

from django.shortcuts import render, get_object_or_404
from .models import Category, Product

def product_list(request, category_slug=None):
    category = None
    categories = Category.objects.all()
    products = Product.objects.filter(available=True)
    if category_slug:
        category = get_object_or_404(Category, slug=category_slug)
        products = products.filter(category=category)
    return render(request, 
                  'shop/product/list.html', 
                  {'category': category, 
                  'categories': categories, 
                  'products': products})

我們用available=True過濾QuerySet,只檢索有效地商品。我們用可選的category_slug參數(shù),過濾指定目錄的商品。

我們還需要一個查詢和顯示單個商品的視圖。添加以下代碼到views.py文件中:

def product_detail(request, id, slug):
    product = get_object_or_404(Product, id=id, slug=slug, available=True)
    return render(request,
                'shop/product/detail.html',
                {'product': product})

product_detail視圖接收idslug參數(shù)來查詢Product實(shí)例。我們可以只使用ID獲得該實(shí)例,因?yàn)镮D是唯一性的屬性。但是我們會在URL中包括別名,為商品構(gòu)建搜索引擎友好的URL。

創(chuàng)建商品列表和詳情視圖后,我們需要為它們定義URL模式。在shop應(yīng)用目錄中創(chuàng)建urls.py文件,添加以下代碼:

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

urlpatterns = [
    url(r'^$', views.product_list, name='product_list'),
    url(r'^(?P<category_slug>[-\w]+)/$', views.product_list, name='product_list_by_category'),
    url(r'^(?P<id>\d+)/(?P<slug>[-\w]+)/$', views.product_detail, name='product_detail'),
]

這些是商品目錄的URL模式。我們?yōu)?code>product_list視圖定義了兩個不同的URL模式:product_list模式不帶任何參數(shù)調(diào)用product_list視圖;product_list_by_category模式給視圖提供category_slug參數(shù),用于過濾指定目錄的商品。我們添加了product_detail模式,傳遞idslug參數(shù)給視圖,用于檢索特定商品。

編輯myshop項(xiàng)目的urls.py文件,如下所示:

from django.conf.urls import url, include
from django.contrib import admin

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

我們在項(xiàng)目的主URL模式中引入了shop應(yīng)用的URL,并指定命名空間為shop。

現(xiàn)在編輯shop應(yīng)用的models.py文件,導(dǎo)入reverse()函數(shù),并為CategoryProduct模型添加get_absolute_url()方法,如下所示:

from django.core.urlresolvers import reverse
# ...
class Category(models.Model):
    # ...
    def get_absolute_url(self):
        return reverse('shop:product_list_by_category', args=[self.slug])
        
class Product(models.Model):
    # ...
    def get_absolute_url(self):
        return reverse('shop:product_detail', args=[self.id, self.slug])

你已經(jīng)知道,get_absolute_url()是檢索指定對象URL的約定成俗的方法。我們在這里使用之前在urls.py文件中定義的URL模式。

7.1.4 創(chuàng)建目錄模板

現(xiàn)在我們需要為商品列表和詳情視圖創(chuàng)建模板。在shop應(yīng)用目錄中創(chuàng)建以下目錄和文件結(jié)構(gòu):

templates/
    shop/
        base.html
        product/
            list.html
            detail.html

我們需要定義一個基礎(chǔ)模板,并在商品列表和詳情模板中繼承它。編輯shop/base.html模板,添加以下代碼:

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>{% block title %}My shop{% endblock %}</title>
    <link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
    <div id="header">
        <a href="/" class="logo">My shop</a>
    </div>
    <div id="subheader">
        <div class="cart">
            Your cart is empty.
        </div>
    </div>
    <div id="content">
        {% block content %}
        {% endblock %}
    </div>
</body>
</html>

這是商店的基礎(chǔ)模板。為了引入模板使用的CSS樣式表和圖片,你需要拷貝本章實(shí)例中的靜態(tài)文件,它們位于shop應(yīng)用的static/目錄。把它們拷貝到你的項(xiàng)目中的相同位置。

編輯shop/product/list.html模板,添加以下代碼:

{% extends "shop/base.html" %}
{% load static %}

{% block title %}
    {% if category %}{{ category.name }}{% else %}Products{% endif %}
{% endblock title %}

{% block content %}
    <div id="sidebar">
        <h3>Categories</h3>
        <ul>
            <li {% if not category %}class="selected"{% endif %}>
                <a href="{% url "shop:product_list" %}">All</a>
            </li>
            {% for c in categories %}
                <li {% if category.slug == c.slug %}class="selected"{% endif %}>
                    <a href="{{ c.get_absolute_url }}">{{ c.name }}</a>
                </li>
            {% endfor %}
        </ul>
    </div>
    <div id="main" class="product-list">
        <h1>{% if catetory %}{{ category.name }}{% else %}Products{% endif %}</h1>
        {% for product in products %}
            <div class="item">
                <a href="{{ product.get_absolute_url }}">
                    ![]({% if product.image %}{{ product.image.url }}{% else %}{% static )
                </a>
                <a href="{{ product.get_absolute_url }}">{{ product.name }}</a><br/>
                ${{ product.price }}
            </div>
        {% endfor %}
    </div>
{% endblock content %}

這是商品列表目錄。它繼承自shop/base.html目錄,用categories上下文變量在側(cè)邊欄顯示所有目錄,用products顯示當(dāng)前頁商品。用同一個模板列出所有有效商品和通過目錄過濾的所有商品。因?yàn)?code>Product模型的image字段可以為空,所以如果商品沒有圖片時,我們需要提供一張默認(rèn)圖片。圖片位于靜態(tài)文件目錄,相對路徑為img/no_image.png。

因?yàn)槲覀冇?code>ImageField存儲商品圖片,所以需要開發(fā)服務(wù)器管理上傳的圖片文件。編輯myshopsettings.py文件,添加以下設(shè)置:

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')

MEDIA_URL是管理用戶上傳的多媒體文件的基礎(chǔ)URL。MEDIA_ROOT是這些文件的本地路徑,我們在前面添加BASE_DIR變量,動態(tài)生成該路徑。

要讓Django管理通過開發(fā)服務(wù)器上傳的多媒體文件,需要編輯myshop項(xiàng)目的urls.py文件,如下所示:

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    # ...
]
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

記住,我們只在開發(fā)階段這么做。在生產(chǎn)環(huán)境,你不應(yīng)該用Django管理靜態(tài)文件。

使用管理站點(diǎn)添加一些商品,然后在瀏覽器中打開http://127.0.0.1:8000/。你會看到商品列表頁面,如下圖所示:

如果你用管理站點(diǎn)創(chuàng)建了一個商品,但是沒有上傳圖片,則會顯示默認(rèn)圖片:

讓我們編輯商品詳情模板。編輯shop/product/detail.html模板,添加以下代碼:

{% extends "shop/base.html" %}
{% load static %}

{% block titie %}
    {% if category %}{{ category.title }}{% else %}Products{% endif %}
{% endblock titie %}

{% block content %}
    <div class="product-detail">
        ![]({% if product.image %}{{ product.image.url }}{% else %} {% static )
        <h1>{{ product.name }}</h1>
        <h2><a href="{{ product.category.get_absolute_url }}">{{ product.category }}</a></h2>
        <p class="price">${{ product.price }}</p>
        {{ product.description|linebreaks }}
    </div>
{% endblock content %}

我們在關(guān)聯(lián)的目錄對象上調(diào)用get_absolute_url()方法,來顯示屬于同一個目錄的有效商品?,F(xiàn)在在瀏覽器中打開http://127.0.0.1/8000/,點(diǎn)擊某個商品查看詳情頁面,如下圖所示:

我們現(xiàn)在已經(jīng)創(chuàng)建了一個基本的商品目錄。

7.2 構(gòu)建購物車

創(chuàng)建商品目錄之后,下一步是創(chuàng)建購物車,讓用戶選擇他們希望購買的商品。當(dāng)用戶瀏覽網(wǎng)站時,購物車允許用戶選擇并暫時存儲他們想要的商品,直到最后下單。購物車存儲在會話中,所以在用戶訪問期間可以保存購物車?yán)锏纳唐贰?/p>

我們將使用Django的會話框架保存購物車。購物車會一直保存在會話中,直到完成購物或者用戶結(jié)賬離開。我們還需要為購物車和它的商品創(chuàng)建額外的Django模型。

7.2.1 使用Django會話

Django提供了一個會話框架,支持匿名和用戶會話。會話框架允許你為每個訪問者存儲任意數(shù)據(jù)。會話數(shù)據(jù)保存在服務(wù)端,cookies包括會話ID,除非你使用基于cookie的會話引擎。會話中間件負(fù)責(zé)發(fā)送和接收cookies。默認(rèn)的會話引擎在數(shù)據(jù)庫中存儲會話數(shù)據(jù),但是接下來你會看到,可以選擇不同的會話引擎。要使用會話,你必須確保項(xiàng)目的MIDDLEWARE_CLASSES設(shè)置中包括django.contrib.sessions.middleware.SessionMiddleware。這個中間件負(fù)責(zé)管理會話,當(dāng)你用startproject命令創(chuàng)建新項(xiàng)目時,會默認(rèn)添加這個中間件。

會話中間件讓當(dāng)前會話在request對象中生效。你可以使用request.session訪問當(dāng)前會話,與使用Python字典類似的存儲和檢索會話數(shù)據(jù)。會話字典默認(rèn)接收所有可以序列化為JSON的Python對象。你可以這樣在會話中設(shè)置變量:

request.session['foo'] = 'bar'

查詢一個會話的鍵:

request.session.get('foo')

刪除存儲在會話中的鍵:

del request.session['foo']

正如你所看到的,我們把request.session當(dāng)做標(biāo)準(zhǔn)的Python字典。

當(dāng)用戶登錄到網(wǎng)站時,他們的匿名會話丟失,并未認(rèn)證用戶創(chuàng)建新的會話。如果你在匿名會話中存儲了數(shù)據(jù),并想在用戶登錄后保留,你需要舊的會話數(shù)據(jù)拷貝到新的會話中。

7.2.2 會話設(shè)置

你可以使用幾種設(shè)置為項(xiàng)目配置會話。其中最重要的是SESSION_ENGINE。該設(shè)置允許你設(shè)置會話存儲的位置。默認(rèn)情況下,Django使用django.contrib.sessions應(yīng)用的Session模型,把會話存儲在數(shù)據(jù)庫中。

Django為存儲會話數(shù)據(jù)提供了以下選項(xiàng):

  • Database sessions:會話數(shù)據(jù)存儲在數(shù)據(jù)庫中。這是默認(rèn)的會話引擎。
  • File-based sessions:會話數(shù)據(jù)存儲在文件系統(tǒng)中。
  • Cached sessions:會話數(shù)據(jù)存儲在緩存后臺。你可以使用CACHES設(shè)置指定婚車后臺。在緩存系統(tǒng)中存儲會話數(shù)據(jù)的性能最好。
  • Cached database sessions:會話數(shù)據(jù)存儲在連續(xù)寫入的緩存(write-through cache)和數(shù)據(jù)庫中。只有在緩存中沒有數(shù)據(jù)時才讀取數(shù)據(jù)庫。
  • Cookie-based sessions:會話數(shù)據(jù)存儲于發(fā)送到瀏覽器的cookies。

使用cache-based會話引擎有更好的性能。Django支持Memcached,以及其它支持Redis的第三方緩存后臺和緩存系統(tǒng)。

你可以只是用其它設(shè)置自定義會話。以下是一些重要的會話相關(guān)設(shè)置:

  • SESSION_COOKIE_AGE:這是會話cookies的持續(xù)時間(單位是秒)。默認(rèn)值是1209600(兩周)。
  • SESSION_COOKIE_DOMAIN:會話cookies使用的域。設(shè)置為.mydomain.com可以啟用跨域cookies。
  • SESSION_EXPIRE_AT_BROWSER_CLOSE:當(dāng)瀏覽器關(guān)閉后,表示會話是否過期的一個布爾值。
  • SESSION_SAVE_EVERY_REQUEST:如果這個布爾值為True,則會在每次請求時把會話保存到數(shù)據(jù)庫中。會話的過期時間也會每次更新。

你可以在這里查看所有會話設(shè)置。

7.2.3 會話過期

你可以使用SESSTION_EXPIRE_AT_BROWSER_CLOSE設(shè)置選擇browser-length會話或者持久會話。默認(rèn)值為False,強(qiáng)制把會話的有效期設(shè)置為SESSION_COOKIE_AGE的值。如果設(shè)置SESSTION_EXPIRE_AT_BROWSER_CLOSETrue,當(dāng)用戶關(guān)閉瀏覽器后,會話會過期,而SESSION_COOKIE_AGE不會起任何作用。

你可以使用request.sessionset_expiry()方法覆寫當(dāng)前會話的有效期。

7.2.4 在會話中存儲購物車

我們需要創(chuàng)建一個簡單的可以序列號為JSON的結(jié)構(gòu)體,在會話中存儲購物車商品。購物車的每一件商品必須包括以下數(shù)據(jù):

  • Product實(shí)例的id
  • 選擇該商品的數(shù)量
  • 該商品的單價

因?yàn)樯唐穬r格可能變化,所以當(dāng)商品添加到購物車時,我們把商品的價格和商品本身同事存入購物車。這樣的話,即使之后商品的價格發(fā)生變化,用戶看到的還是添加到購物車時的價格。

現(xiàn)在你需要創(chuàng)建購物車,并與會話關(guān)聯(lián)起來。購物車必須這樣工作:

  • 需要購物車時,我們檢查是否設(shè)置了自定義會話鍵。如果會話中沒有設(shè)置購物車,則創(chuàng)建一個新的購物車,并保存在購物車會話鍵中。
  • 對于連續(xù)的請求,我們執(zhí)行相同的檢查,并從購物車會話鍵中取出購物車的商品。我們從會話中檢索購物車商品,并從數(shù)據(jù)庫中檢索它們關(guān)聯(lián)的Product對象。

編輯項(xiàng)目settings.py文件,添加以下設(shè)置:

CART_SESSION_ID = 'cart'

我們在用戶會話用這個鍵存儲購物車。因?yàn)槊總€訪客的Django會話是獨(dú)立的,所以我們可以為所有會話使用同一個購物車會話鍵。

讓我們創(chuàng)建一個管理購物車的應(yīng)用。打開終端,執(zhí)行以下命令創(chuàng)建一個新應(yīng)用:

python manage.py startapp cart

然后編輯項(xiàng)目的settings.py文件,把cart添加到INSTALLED_APPS

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

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

from decimal import Decimal
from django.conf import settings
from shop.models import Product

class Cart:
    def __init__(self, request):
        self.session = request.session
        cart = self.session.get(settings.CART_SESSION_ID)
        if not cart:
            # save an empty cart in the session
            cart = self.session[settings.CART_SESSION_ID] = {}
        self.cart = cart

這個Cart類用于管理購物車。我們要求用request對象初始化購物車。我們用self.session = request.session存儲當(dāng)前會話,以便在Cart類的其它方法中可以訪問。首先,我們用self.session.get(settings.CART_SESSION_ID)嘗試從當(dāng)前會話中獲得購物車。如果當(dāng)前會話中沒有購物車,通過在會話中設(shè)置一個空字典來設(shè)置一個空的購物車。我們希望購物車字典用商品ID做為鍵,一個帶數(shù)量和價格的字典作為值。這樣可以保證一個商品不會在購物車中添加多次;同時還可以簡化訪問購物車的數(shù)據(jù)。

讓我們創(chuàng)建一個方法,用于向購物車中添加商品,或者更新商品數(shù)量。在Cart類中添加add()save()方法:

def add(self, product, quantity=1, update_quantity=False):
    product_id = str(product.id)
    if product_id not in self.cart:
        self.cart[product_id] = {
            'quantity': 0,
            'price': str(product.price)
        }
    if update_quantity:
        self.cart[product_id]['quantity'] = quantity
    else:
        self.cart[product_id]['quantity'] += quantity
    self.save()

def save(self):
    # update the session cart
    self.session[settings.CART_SESSION_ID] = self.cart
    # mark the sessions as "modified" to make sure it is saved
    self.session.modified = True

add()方法接收以下參數(shù):

  • product:在購物車中添加或更新的Product實(shí)例。
  • quantity:可選的商品數(shù)量。默認(rèn)為1.
  • update_quantity:一個布爾值,表示使用給定的數(shù)量更新數(shù)量(True),或者把新數(shù)量加到已有的數(shù)量上(False)。

我們用商品id作為購物車內(nèi)容字典的鍵。因?yàn)镈jango使用JSON序列號會話數(shù)據(jù),而JSON只允許字符串類型的鍵名,所以我們把商品id轉(zhuǎn)換為字符串。商品id是鍵,保存的值是帶商品quantityprice的字典。為了序列號,我們把商品價格轉(zhuǎn)換為字符串。最后,我們調(diào)用save()方法在會話中保存購物車。

save()方法在會話中保存購物車的所有修改,并使用session.modified = True標(biāo)記會話已修改。這告訴Django,會話已經(jīng)修改,需要保存。

我們還需要一個方法從購物車中移除商品。在Cart類中添加以下方法:

def remove(self, product):
    product_id = str(product.id)
    if product_id in self.cart:
        del self.cart[product_id]
        self.save()

remove()方法從購物車字典中移除指定商品,并調(diào)用save()方法更新會話中的購物車。

我們將需要迭代購物車中的商品,并訪問關(guān)聯(lián)的Product實(shí)例。因?yàn)樾枰陬愔卸x__iter__()方法。在Cart類中添加以下方法:

def __iter__(self):
    product_ids = self.cart.keys()
    # get the product objects and add them to the cart
    products = Product.objects.filter(id__in=product_ids)
    for product in products:
        self.cart[str(product.id)]['product'] = product

    for item in self.cart.values():
        item['price'] = Decimal(item['price'])
        item['total_price'] = item['price'] * item['quantity']
        yield item

__iter__()方法中,我們檢索購物車中的Product實(shí)例,并把它們包括在購物車商品中。最后,我們迭代購物車商品,把price轉(zhuǎn)換回Decimal類型,并為每一項(xiàng)添加total_price屬性?,F(xiàn)在我們可以在購物車中方便的迭代商品。

我們還需要返回購物車中商品總數(shù)量。當(dāng)在一個對象上調(diào)用len()函數(shù)時,Python會調(diào)用__len__()方法返回對象的長度。我們定義一個__len__()方法,返回購物車中所有商品的總數(shù)量。在Cart類中添加__len__()方法:

def __len__(self):
    return sum(item['quantity'] for item in self.cart.values())

我們返回購物車中所有商品數(shù)量的總和。

添加以下方法,計算購物車中所有商品的總價:

def get_total_price(self):
    return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())

最后,添加一個清空購物車會話的方法:

def clear(self):
    del self.session[settings.CART_SESSION_ID]
    self.session.modified = True

我們的Cart類已經(jīng)可以管理購物車了。

7.2.5 創(chuàng)建購物車視圖

現(xiàn)在我們已經(jīng)創(chuàng)建了Cart類來管理購物車,我們需要創(chuàng)建添加,更新和移除購物車商品的視圖。我們需要創(chuàng)建以下視圖:

  • 一個添加或更新購物車商品的視圖,可以處理當(dāng)前和新的數(shù)量
  • 一個從購物車中移除商品的視圖
  • 一個顯示購物車商品和總數(shù)的視圖

7.2.5.1 添加商品到購物車

要添加商品到購物車中,我們需要一個用戶可以選擇數(shù)量的表單。在cart應(yīng)用目錄中創(chuàng)建forms.py文件,并添加以下代碼:

from django import forms

PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]

class CartAddProductForm(forms.Form):
    quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES, coerce=int)
    update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)

我們用這個表單向購物車中添加商品。CartAddProductForm類包括以下兩個字段:

  • quantity:允許用戶選擇1-20之間的數(shù)量。我們使用帶coerce=intTypedChoiceField字段把輸入的值轉(zhuǎn)換為整數(shù)。
  • update:允許你指定把數(shù)量累加到購物車中已存在的商品數(shù)量上(False),還是用給定的數(shù)量更新已存在商品數(shù)量(True)。我們?yōu)樵撟侄问褂?code>HiddenInput組件,因?yàn)槲覀儾幌胱層脩艨匆娝?/li>

讓我們創(chuàng)建向購物車添加商品的視圖。編輯cart應(yīng)用的views.py文件,并添加以下代碼:

from django.shortcuts import render, redirect, get_object_or_404
from django.views.decorators.http import require_POST
from shop.models import Product
from .cart import Cart
from .forms import CartAddProductForm

@require_POST
def cart_add(request, product_id):
    cart = Cart(request)
    product = get_object_or_404(Product, id=product_id)
    form = CartAddProductForm(request.POST)
    if form.is_valid():
        cd = form.cleaned_data
        cart.add(product=product, quantity=cd['quantity'], update_quantity=cd['update'])
    return redirect('cart:cart_detail')

這個視圖用于向購物車中添加商品或者更新已有商品的數(shù)量。因?yàn)檫@個視圖會修改數(shù)據(jù),所以我們只允許POST請求。視圖接收商品ID作為參數(shù)。我們用給定的商品ID檢索Product實(shí)例,并驗(yàn)證CartAddProductForm。如果表單有效,則添加或更新購物車中的商品。該視圖重定向到cart_detail URL,它會顯示購物車中的內(nèi)容。之后我們會創(chuàng)建cart_detail視圖。

我們還需要一個從購物車中移除商品的視圖。在cart應(yīng)用的views.py文件中添加以下代碼:

def cart_remove(request, product_id):
    cart = Cart(request)
    product = get_object_or_404(Product, id=product.id)
    cart.remove(product)
    return redirect('cart:cart_detail')

cart_remove視圖接收商品ID作為參數(shù)。我們用給定的商品ID檢索Product實(shí)例,并從購物車中移除該商品。接著我們重定向到cart_detail URL。

最后,我們需要一個顯示購物車和其中的商品的視圖。在views.py文件中添加以下代碼:

def cart_detail(request):
    cart = Cart(request)
    return render(request, 'cart/detail.html', {'cart': cart})

cart_detail視圖獲得當(dāng)前購物車,并顯示它。

我們已經(jīng)創(chuàng)建了以下視圖:向購物車中添加商品,更新數(shù)量,從購物車中移除商品,已經(jīng)顯示購物車。讓我們?yōu)檫@些視圖添加URL。在cart應(yīng)用目錄中創(chuàng)建urls.py文件,并添加以下URL模式:

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

urlpatterns = [
    url(r'^$', views.cart_detail, name='cart_detail'),
    url(r'^add/(?P<product_id>\d+)/$', views.cart_add, name='cart_add'),
    url(r'^remove/(?P<product_id>\d+)/$', views.cart_remove, name='cart_remove'),
]

最后,編輯myshop項(xiàng)目的主urls.py文件,引入cart的URL模式:

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

確保在shop.urls模式之前引入這個URL模式,因?yàn)樗惹罢吒邢薅ㄐ浴?/p>

7.2.5.2 構(gòu)建顯示購物車的模板

cart_addcart_remove視圖不需要渲染任何模板,但是我們需要為cart_detail視圖創(chuàng)建顯示購物車和總數(shù)量的模板。

cart應(yīng)用目錄中創(chuàng)建以下文件結(jié)構(gòu):

templates/
    cart/
        detail.html

編輯cart/detail.html目錄,并添加以下代碼:

{% extends "shop/base.html" %}
{% load static %}

{% block title %}
    Your shopping cart
{% endblock title %}

{% block content %}
    <h1>Your shopping cart</h1>
    <table class="cart">
        <thead>
            <tr>
                <th>Image</th>
                <th>Product</th>
                <th>Quantity</th>
                <th>Remove</th>
                <th>Unit price</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            {% for item in cart %}
                {% with product=item.product %}
                    <tr>
                        <td>
                            <a href="{{ prouct.get_absolute_url }}">
                                ![]({% if product.image %}{{ product.image.url}}{% else %}{% static )
                            </a>
                        </td>
                        <td>{{ product.name }}</td>
                        <td>{{ item.quantity }}</td>
                        <td><a href="{% url "cart:cart_remove" product.id %}">Remove</a></td>
                        <td class="num">${{ item.price }}</td>
                        <td class="num">${{ item.total_price }}</td>
                    </tr>
                {% endwith %}
            {% endfor %}
            <tr class="total">
                <td>Total</td>
                <td colspan="4"></td>
                <td class="num">${{ cart.get_total_price }}</td>
            </tr>
        </tbody>
    </table>
    <p class="text-right">
        <a href="{% url "shop:product_list" %}" class="button light">Continue shopping</a>
        <a href="#" class="button">Checkout</a>
    </p>
{% endblock content %}

這個模板用于顯示購物車的內(nèi)容。它包括一個當(dāng)前購物車中商品的表格。用戶通過提交表單到cart_add視圖,來修改選中商品的數(shù)量。我們?yōu)槊總€商品提供了Remove鏈接,用戶可以從購物車移除商品。

7.2.5.3 添加商品到購物車

現(xiàn)在我們需要在商品詳情頁面添加Add to cart按鈕。編輯shop應(yīng)用的views.py文件,修改product_detail視圖,如下所示:

from cart.forms import CartAddProductForm

def product_detail(request, id, slug):
    product = get_object_or_404(Product, id=id, slug=slug, available=True)
    cart_product_form = CartAddProductForm()
    return render(request,
                'shop/product/detail.html',
                {'product': product,
                'cart_product_form': cart_product_form})

編輯shop應(yīng)用的shop/product/detail.html模板,在商品價格之后添加表單,如下所示:

<p class="price">${{ product.price }}</p>
<form action="{% url "cart:cart_add" product.id %}" method="post">
    {{ cart_product_form }}
    {% csrf_token %}
    <input type="submit" value="Add to cart">
</form>

使用python manage.py runserver命令啟動開發(fā)服務(wù)器。在瀏覽器中打開127.0.0.1/8000/,然后導(dǎo)航到商品詳情頁面。它現(xiàn)在包括一個選擇數(shù)量的表單,如下圖所示:

選擇數(shù)量,然后點(diǎn)擊Add to cart按鈕。表單通過POST提交到cart_add視圖。該視圖把商品添加到會話中的購物車,包括當(dāng)前價格和選擇的數(shù)量。然后重定義到購物車詳情頁面,如下圖所示:

7.2.5.4 在購物車中更新商品數(shù)量

當(dāng)用戶查看購物車時,他們可能希望在下單前修改商品數(shù)量。我們接下來實(shí)現(xiàn)在購物車詳情頁面修改數(shù)量。

編輯cart應(yīng)用的views.py文件,如下修改cart_detail視圖:

def cart_detail(request):
    cart = Cart(request)
    for item in cart:
        item['update_quantity_form'] = CartAddProductForm(
            initial={'quantity': item['quantity'], 'update': True})
    return render(request, 'cart/detail.html', {'cart': cart})

我們?yōu)橘徫镘囍械拿總€商品創(chuàng)建了一個CartAddProductForm實(shí)例,允許用戶修改商品數(shù)量。我們用當(dāng)前商品數(shù)量初始化表單,并設(shè)置update字段為True。因此,當(dāng)我們把表單提交到cart_add視圖時,會用新數(shù)量了代替當(dāng)前數(shù)量。

現(xiàn)在編輯cart應(yīng)用的cart/detail.html模板,找到這一行代碼:

<td>{{ item.quantity }}</td>

把這行代碼替換為:

<td>
    <form action="{% url "cart:cart_add" product.id %}" method="post">
        {{ item.update_quantity_form.quantity }}
        {{ item.update_quantity_form.update }}
        <input type="submit" value="Update">
        {% csrf_token %}
    </form>
</td>

在瀏覽器中打開http://127.0.0.1:8000/cart/。你會看到購物車中每個商品都有一個修改數(shù)量的表單,如下圖所示:

修改商品數(shù)量,然后點(diǎn)擊Update按鈕,測試一下新功能。

7.2.6 為當(dāng)前購物車創(chuàng)建上下文處理器

你可能已經(jīng)注意到了,我們的網(wǎng)站頭部還是顯示Your cart is emtpy。當(dāng)我們開始向購物車中添加商品,我們將看到它替換成購物車中商品的總數(shù)量和總價錢。因?yàn)檫@是需要在所有頁面顯示,所以我們將創(chuàng)建一個上下文處理器(context processor),將當(dāng)前購物車包含在請求上下文中,而不管已經(jīng)處理的視圖。

7.2.6.1 上下文處理器

上下文處理器是一個Python函數(shù),它將request對象作為參數(shù),并返回一個添加到請求上下文中的字典。當(dāng)你需要讓某些東西在所有模板都可用時,它會派上用場。

默認(rèn)情況下,當(dāng)你使用startproject命令創(chuàng)建新項(xiàng)目時,項(xiàng)目中會包括以下模板上下文處理器,它們位于TEMPLATES設(shè)置的context_processors選項(xiàng)中:

  • django.template.context_processors.debug:在上下文中設(shè)置debug布爾值和sql_queries變量,表示請求中執(zhí)行的SQL查詢列表
  • django.template.context_processors.request:在上下文中設(shè)置request變量
  • django.contrib.auth.context_processors.auth:在請求中設(shè)置user變量
  • django.contrib.messages.context_processors.messages:在上下文中設(shè)置message變量,其中包括所有已經(jīng)用消息框架發(fā)送的消息。

Django還啟用了django.template.context_processors.csrf來避免跨站點(diǎn)請求偽造攻擊。這個上下文處理器不在設(shè)置中,但它總是啟用的,并且為了安全不能關(guān)閉。

你可以在這里查看所有內(nèi)置的上下文處理器列表。

7.2.6.2 在請求上下文中設(shè)置購物車

讓我們創(chuàng)建一個上下文處理器,把當(dāng)前購物車添加到模板的請求上下文中。我們可以在所有模板中訪問購物車。

cart應(yīng)用目錄中創(chuàng)建context_processors.py文件。上下文處理器可以位于代碼的任何地方,但是在這里創(chuàng)建他們將保持代碼組織良好。在文件中添加以下代碼:

from .cart import Cart

def cart(request):
    return {'cart': Cart(request)}

正如你所看到的,上下文處理器是一個函數(shù),它將request對象作為參數(shù),并返回一個對象的字典,這些對象可用于所有使用RequestContext渲染的模板。在我們的上下文處理器中,我們用request對象實(shí)例化購物車,模板可以通過cart變量名訪問它。

編輯項(xiàng)目的settings.py文件,在TEMPLATES設(shè)置的context_processors選項(xiàng)中添加cart.context_processors.cart,如下所示:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'cart.context_processors.cart',
            ],
        },
    },
]

每次使用RequestContext渲染模板時,會執(zhí)行你的上下文處理器。cart變量會設(shè)置在模板的上下文中。

上下文處理器會在所有使用RequestContext的請求中執(zhí)行。如果你想訪問數(shù)據(jù)庫的話,可能希望創(chuàng)建一個自定義模板標(biāo)簽來代替上下文處理器。

現(xiàn)在編輯shop應(yīng)用的shop/base.html模板,找到以下代碼:

<div class="cart">
    Your cart is empty.
</div>

用下面的代碼替換上面的代碼:

<div class="cart">
    {% with total_items=cart|length %}
        {% if cart|length > 0 %}
            Your cart:
            <a href="{% url "cart:cart_detail" %}">
                {{ total_items }} item{{ total_items|pluralize }},
                ${{ cart.get_total_price }}
            </a>
        {% else %}
            Your cart is empty.
        {% endif %}
    {% endwith %}
</div>

使用python manage.py runserver重啟開發(fā)服務(wù)器。在瀏覽器中打開http://127.0.0.1:8000/,并添加一些商品到購物車中。在網(wǎng)站頭部,你會看到當(dāng)前購物車總數(shù)量和總價錢,如下所示:

7.3 注冊用戶訂單

當(dāng)購物車結(jié)賬后,你需要在數(shù)據(jù)庫中保存訂單。訂單包括用戶信息和他們購買的商品。

使用以下命令創(chuàng)建一個新應(yīng)用,來管理用戶訂單:

python manage.py startapp orders

編輯項(xiàng)目的settings.py文件,在INSTALLED_APPS設(shè)置中添加orders

INSTALLED_APPS = [
    # ...
    'orders',
]

你已經(jīng)激活了新應(yīng)用。

7.3.1 創(chuàng)建訂單模型

你需要創(chuàng)建一個模型存儲訂單詳情,以及一個模型存儲購買的商品,包括價格和數(shù)量。編輯orders應(yīng)用的models.py文件,添加以下代碼:

from django.db import models
from shop.models import Product

class Order(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    address = models.CharField(max_length=250)
    postal_code = models.CharField(max_length=20)
    city = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    paid = models.BooleanField(default=False)

    class Meta:
        ordering = ('-created', )

    def __str__(self):
        return 'Order {}'.format(self.id)

    def get_total_cost(self):
        return sum(item.get_cost() for item in self.items.all())

class OrderItem(models.Model):
    order = models.ForeignKey(Order, related_name='items')
    product = models.ForeignKey(Product, related_name='order_items')
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveIntegerField(default=1)

    def __str__(self):
        return '{}'.format(self.id)

    def get_cost(self):
        return self.price * self.quantity

Order模型包括幾個用戶信息字段和一個默認(rèn)值為Falsepaid布爾字段。之后,我們將用這個字段區(qū)分已支付和未支付的訂單。我們還定義了get_total_cost()方法,獲得這個訂單中購買商品的總價錢。

OrderItem模型允許我們存儲商品,數(shù)量和每個商品的支付價格。我們用get_cost()返回商品價錢。

運(yùn)行以下命令,為orders應(yīng)用創(chuàng)建初始數(shù)據(jù)庫遷移:

python manage.py makemigrations

你會看到以下輸出:

Migrations for 'orders':
  orders/migrations/0001_initial.py
    - Create model Order
    - Create model OrderItem

運(yùn)行以下命令讓新的遷移生效:

python manage.py migrate

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

7.3.2 在管理站點(diǎn)引入訂單模型

讓我們在管理站點(diǎn)添加訂單模型。編輯orders應(yīng)用的admin.py文件,添加以下代碼:

from django.contrib import admin
from .models import Order, OrderItem

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    raw_id_fields = ['product']

class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email', 
        'address', 'postal_code', 'city', 'paid', 'created', 'updated']
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]

admin.site.register(Order, OrderAdmin)

我們?yōu)?code>OrderItem模型使用ModeInline,把它作為內(nèi)聯(lián)模型引入OrderAdmin類。內(nèi)聯(lián)可以包含一個模型,與父模型在同一個編輯頁面顯示。

使用python manage.py runserver命令啟動開發(fā)服務(wù)器,然后在瀏覽器中打開http:127.0.0.1/8000/admin/order/add/。你會看到以下界面:

7.3.3 創(chuàng)建用戶訂單

當(dāng)用戶最終下單時,我們需要使用剛創(chuàng)建的訂單模型來保存購物車中的商品。創(chuàng)建一個新訂單的工作流程是這樣的:

  1. 向用戶顯示一個填寫數(shù)據(jù)的訂單表單。
  2. 用用戶輸入的數(shù)據(jù)創(chuàng)建一個新的Order實(shí)例,然后為購物車中的每件商品創(chuàng)建關(guān)聯(lián)的OrderItem實(shí)例。
  3. 清空購物車中所有內(nèi)容,然后重定向到成功頁面。

首先,我們需要一個輸入訂單詳情的表單。在orders應(yīng)用目錄中創(chuàng)建forms.py文件,并添加以下代碼:

from django import forms
from .models import Order

class OrderCreateForm(forms.ModelForm):
    class Meta:
        model = Order
        fields = ['first_name', 'last_name', 'email', 
            'address', 'postal_code', 'city']

這是我們用于創(chuàng)建新Order對象的表單。現(xiàn)在我們需要一個視圖處理表單和創(chuàng)建新表單。編輯orders應(yīng)用的views.py文件,并添加以下代碼:

from django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from cart.cart import Cart

def order_create(request):
    cart = Cart(request)
    if request.method == 'POST':
        form = OrderCreateForm(request.POST)
        if form.is_valid():
            order = form.save()
            for item in cart:
                OrderItem.objects.create(order=order, product=item['product'], 
                    price=item['price'], quantity=item['quantity'])
            # clear the cart
            cart.clear()
            return render(request, 'orders/order/created.html', {'order': order})
    else:
        form = OrderCreateForm()
    return render(request, 'orders/order/create.html', {'cart': cart, 'form': form})

order_create視圖中,我們用cart = Cart(request)從會話中獲得當(dāng)前購物車。根據(jù)請求的方法,我們執(zhí)行以下任務(wù):

  • GET請求:實(shí)例化OrderCreateForm表單,并渲染orders/order/create.html模板。
  • POST請求:驗(yàn)證提交的數(shù)據(jù)。如果數(shù)據(jù)有效,則使用order = form.save()創(chuàng)建一個新的Order實(shí)例。然后我們會將它保存到數(shù)據(jù)庫中,并存儲在order變量中。創(chuàng)建order之后,我們會迭代購物車中的商品,并為每個商品創(chuàng)建OrderItem。最后,我們會清空購物車的內(nèi)容。

現(xiàn)在,在orders應(yīng)用目錄中創(chuàng)建urls.py文件,并添加以下代碼:

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

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

這是order_create視圖的URL模式。編輯myshop項(xiàng)目的urls.py文件,并引入以下模式。記住,把它放在shop.urls模式之前:

url(r'^orders/', include('orders.urls', namespace='orders')),

編輯cart應(yīng)用的cart/detail.html模板,找到這行代碼:

<a href="#" class="button">Checkout</a>

把這樣代碼替換為以下代碼:

<a href="{% url "orders:order_create" %}" class="button">Checkout</a>

現(xiàn)在用戶可以從購物車詳情頁面導(dǎo)航到訂單表單。我們還需要為下單定義模板。在orders應(yīng)用目錄中創(chuàng)建以下文件結(jié)構(gòu):

templates/
    orders/
        order/
            create.html
            created.html

編輯orders/order/create.html模板,并添加以下代碼:

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

{% block title %}
    Checkout
{% endblock title %}

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

    <div class="order-info">
        <h3>Your order</h3>
        <ul>
            {% for item in cart %}
                <li>
                    {{ item.quantity }}x {{ item.product.name }}
                    <span>${{ item.total_price }}</span>
                </li>
            {% endfor %}
        </ul>
        <p>Total: ${{ cart.get_total_price }}</p>
    </div>

    <form action="." method="post" class="order-form">
        {{ form.as_p }}
        <p><input type="submit" value="Place order"></p>
        {% csrf_token %}
    </form>
{% endblock content %}

這個模板顯示購物車中的商品,包括總數(shù)量和下單的表單。

編輯orders/order/created.html模板,并添加以下代碼:

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

{% block title %}
    Thank you
{% endblock title %}

{% block content %}
    <h1>Thank you</h1>
    <p>Your order has been successfully completed. 
        Your order number is <stong>{{ order.id }}</stong>
    </p>
{% endblock content %}

成功創(chuàng)建訂單后,我們渲染這個模板。啟動開發(fā)服務(wù)器,并在瀏覽器中打開http://127.0.0.1:8000/。在購物車中添加一些商品,然后跳轉(zhuǎn)到結(jié)賬界面。如下圖所示:

用有效的數(shù)據(jù)填寫表單,然后點(diǎn)擊Place order按鈕。訂單會被創(chuàng)建,你將看到成功頁面,如下圖所示:

7.4 使用Celery啟動異步任務(wù)

你在視圖中執(zhí)行的所有操作都會影響響應(yīng)時間。在很多場景中,你可能希望盡快給用戶返回響應(yīng),并讓服務(wù)器執(zhí)行一些異步處理。對于費(fèi)時處理,或者失敗后可能需要重試策略的處理尤其重要。例如,一個視頻分享平臺允許用戶上傳視頻,但轉(zhuǎn)碼上傳的視頻需要很長的時間。網(wǎng)站可能給用戶返回一個響應(yīng),告訴用戶馬上開始轉(zhuǎn)碼,然后開始異步轉(zhuǎn)碼。另一個例子是給用戶發(fā)送郵件。如果網(wǎng)站在視圖中發(fā)送郵件通知,SMTP連接可能失敗,或者減慢響應(yīng)時間。啟動異步任務(wù)避免阻塞操作是必不可少的。

Celery是一個可以處理大量消息的分布式任務(wù)隊(duì)列。它既可以實(shí)時處理,也支持任務(wù)調(diào)度。使用Celery不僅可以很容易的創(chuàng)建異步任務(wù),還可以盡快執(zhí)行任務(wù),但也可以在一個指定時間執(zhí)行任務(wù)。

你可以在這里查看Celery文檔。

7.4.1 安裝Celery

讓我們安裝Celery,并在項(xiàng)目中集成它。使用以下pip命令安裝Celery:

pip install celery

Celery必須有一個消息代理(message broker)處理外部請求。代理負(fù)責(zé)發(fā)送消息給Celery的worker,worker收到消息后處理任務(wù)。讓我們安裝一個消息代理。

7.4.2 安裝RabbitMQ

Celery有幾個消息代理可供選擇,包括鍵值對存儲(比如Redis),或者一個實(shí)際的消息系統(tǒng)(比如RabbitMQ)。我們將用RabbitMQ配置Celery,因?yàn)樗荂elery的推薦消息worker。

如果你使用的是Linux,可以在終端執(zhí)行以下命令安裝RabbitMQ:

apt-get install rabbitmq

如果你需要在Max OS X或者Windows上安裝RabbitMQ,你可以在這里找到獨(dú)立的版本。

安裝后,在終端執(zhí)行以下命令啟動RabbitMQ:

rabbitmq-server

你會看到以這一行結(jié)尾的輸出:

Starting broker... completed with 10 plugins.

7.4.3 在項(xiàng)目中添加Celery

你需要為Celery實(shí)例提供一個配置。在myshop中創(chuàng)建celery.py文件,該文件會包括項(xiàng)目的Celery配置,并添加以下代碼:

import os
from celery import Celery
from django.conf import settings

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myshop.settings')

app = Celery('myshop')

app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

在這段代碼中,我們?yōu)镃elery命令行程序設(shè)置DJANGO_SETINGS_MODULE變量。然后用app = Celery('myshop.)創(chuàng)建了一個應(yīng)用實(shí)例。我們用config_from_object()方法從項(xiàng)目設(shè)置中加載所有自定義設(shè)置。最后我們告訴Celery,為INSTALLED_APPS設(shè)置中列出的應(yīng)用自動查找異步任務(wù)。Celery會在每個應(yīng)用目錄中查找tasks.py文件,并加載其中定義的異步任務(wù)。

你需要在項(xiàng)目的__init__.py文件中導(dǎo)入celery模塊,確保Django啟動時會加載Celery。編輯myshop/__init__.py文件,并添加以下代碼:

from .celery import app as celery_app

現(xiàn)在你可以開始為應(yīng)用編寫異步任務(wù)了。

CELERY_ALWAYS_EAGER設(shè)置允許你以同步方式在本地執(zhí)行任務(wù),而不是將其發(fā)送到隊(duì)列。這對于運(yùn)行單元測試,或者在不運(yùn)行Celery的情況下,運(yùn)行本地環(huán)境中的項(xiàng)目時非常有用。

7.4.4 在應(yīng)用中添加異步任務(wù)

當(dāng)用戶下單后,我們將創(chuàng)建一個異步任務(wù),給用戶發(fā)送一封郵件通知。

一般的做法是在應(yīng)用目錄的tasks模塊中包括應(yīng)用的異步任務(wù)。在orders應(yīng)用目錄中創(chuàng)建tasks.py文件。Celery會在這里查找異步任務(wù)。在其中添加以下代碼:

from celery import task
from django.core.mail import send_mail
from .models import Order

@task
def order_created(order_id):
    order = Order.objects.get(id=order_id)
    subject = 'Order nr. {}'.format(order.id)
    message = 'Dear {},\n\nYou have successfully placed an order.\
        Your order id is {}.'.format(order.first_name, order.id)
    mail_sent = send_mail(subject, message, 'admin@myshop.com', [order.email])
    return mail_sent

我們使用task裝飾器定義了order_created任務(wù)。正如你所看到的,Celery任務(wù)就是一個用task裝飾的Python函數(shù)。我們的task函數(shù)接收order_id作為參數(shù)。推薦只傳遞ID給任務(wù)函數(shù),并在任務(wù)執(zhí)行時查詢對象。我們用Django提供的send_mail()函數(shù),當(dāng)用戶下單后發(fā)送郵件通知。如果你不想配置郵件選項(xiàng),你可以在項(xiàng)目的settings.py文件中添加以下設(shè)置,讓Django在控制臺輸出郵件:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

異步任務(wù)不僅可以用于費(fèi)時的操作,還可以用于可能失敗的操作,這些操作不會執(zhí)行很長時間,但它們可能會連接失敗,或者需要重試策略。

現(xiàn)在我們需要在order_create視圖中添加任務(wù)。打開orders應(yīng)用的views.py文件,并導(dǎo)入任務(wù):

from .tasks import order_created

然后在清空購物車之后調(diào)用order_created異步任務(wù):

# clear the cart
cart.clear()
# launch asynchronous task
order_created.delay(order.id)

我們調(diào)用任務(wù)的delay()方法異步執(zhí)行任務(wù)。任務(wù)會被添加到隊(duì)列中,worker會盡快執(zhí)行。

打開另一個終端,并使用以下命令啟動Celery的worker

celery -A myshop worker -l info

譯者注:必須在myshop項(xiàng)目目錄下執(zhí)行上面的命令。

現(xiàn)在Celery的worker已經(jīng)運(yùn)行,準(zhǔn)備好處理任務(wù)了。確保Django開發(fā)服務(wù)器也在運(yùn)行。在瀏覽器中打開http://127.0.0.1/8000/,添加一些商品到購物車中,然后完成訂單。在終端,你已經(jīng)啟動了Celery worker,你會看到類似這樣的輸出:

[2017-05-11 06:40:27,416: INFO/MainProcess] Received task: orders.tasks.order_created[4d6f667b-7cc7-4310-82fc-8323810fae54]
[2017-05-11 06:40:27,825: INFO/PoolWorker-3] Task orders.tasks.order_created[4d6f667b-7cc7-4310-82fc-8323810fae54] succeeded in 0.12212000600266038s: 1

任務(wù)已經(jīng)執(zhí)行,你會收到一封訂單的郵件通知。

7.4.5 監(jiān)控Celery

你可能希望監(jiān)控已經(jīng)執(zhí)行的異步任務(wù)。Flower是一個基于網(wǎng)頁的監(jiān)控Celery工具。你可以使用pip install flower安裝Flower。

安裝后,你可以在項(xiàng)目目錄下執(zhí)行以下命令啟動Flower:

celery -A myshop flower

在瀏覽器中打開http://127.0.0.1:5555/dashboard。你會看到活動的Celery worker和異步任務(wù)統(tǒng)計:

你可以在這里查看Flower的文檔。

7.5 總結(jié)

在這章中,你創(chuàng)建了一個基礎(chǔ)的在線商店應(yīng)用。你創(chuàng)建了商品目錄,并用會話構(gòu)建了購物車。你實(shí)現(xiàn)了自定義上下文處理器,讓模板可以訪問購物車,并創(chuàng)建了下單的表單。你還學(xué)習(xí)了如何使用Celery啟動異步任務(wù)。

在下一章中,你會學(xué)習(xí)在商店中集成支付網(wǎng)關(guān)(payment gateway),在管理站點(diǎn)添加用戶操作,導(dǎo)出CVS格式數(shù)據(jù),以及動態(tài)生成PDF文件。

最后編輯于
?著作權(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)容