《Django By Example》第七章 中文 翻譯 (個人學習,渣翻)

全文鏈接

第一章 創建一個blog應用
第二章 使用高級特性來增強你的blog
第三章 擴展你的blog應用
第四章上 創建一個社交網站
第四章下 創建一個社交網站
第五章 在你的網站中分享內容
第六章 跟蹤用戶動作
第七章 建立一個在線商店
第八章 管理付款和訂單
第九章上 擴展你的商店
第九章下 擴展你的商店
第十章上 創建一個在線學習平臺
第十章下 創建一個在線學習平臺
第十一章 緩存內容
第十二章 構建一個API

書籍出處:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé

(譯者@ucag注:咳咳,第七章終于來了。其實在一月份就翻譯完了但是后來我回老家了,就沒發出來。各位久等了~)

(譯者@夜夜月注:真羨慕有寒假和暑假的人- -愿你們的寒暑假作業越多越好,粗略的校對了下,精校版本請大家繼續等待)

第七章

建立一個在線商店

在上一章,你創建了一個用戶跟蹤系統和建立了一個用戶活躍流。你也學習了 Django 信號是如何工作的,并且把 Redis 融合進了項目中來為圖像視圖計數。在這一章中,你將學會如何建立一個最基本的在線商店。你將會為你的產品創建目錄和使用 Django sessions 實現一個購物車。你也將學習怎樣定制上下文處理器( context processors )以及用 Celery 來激活動態任務。

在這一章中,你將學會:

  • 創建一個產品目錄
  • 使用 Django sessions 建立購物車
  • 管理顧客的訂單
  • 用 Celery 發送異步通知

創建一個在線商店項目(project)

我們將從新建一個在線商店項目開始。我們的用戶可以瀏覽產品目錄并且可以向購物車中添加商品。最后,他們將清點購物車然后下單。這一章涵蓋了在線商店的以下幾個功能:

  • 創建產品目錄模型(模型),將它們添加到管理站點,創建基本的視圖(view)來展示目錄
  • 使用 Django sessions 建立一個購物車系統,使用戶可以在瀏覽網站的過程中保存他們選中的商品
  • 創建下單表單和功能
  • 發送一封異步的確認郵件在用戶下單的時候

首先,用以下命令來為你的新項目創建一個虛擬環境,然后激活它:

mkdir env
virtualenv env/myshop
source env/myshop/bin/activate

用以下命令在你的虛擬環境中安裝 Django :

pip install django==1.8.6

創建一個叫做 myshop 的新項目,再創建一個叫做 shop 的應用,命令如下:

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

編輯你項目中的 settings.py 文件,像下面這樣將你的應用添加到 INSTALLED_APPS 中:

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

現在你的應用已經在項目中激活。接下來讓我們為產品目錄定義模型(models)。

創建產品目錄模型(models)

我們商店中的目錄將會由不同分類的產品組成。每一個產品會有一個名字,一段可選的描述,一張可選的圖片,價格,以及庫存。 編輯位于shop應用中的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 模型(models)。Category 模型(models)由一個 name 字段和一個唯一的 slug 字段構成。Product 模型(model):

  • category: 這是一個鏈接向 CategoryForeignKey 。這是個多對一(many-to-one)關系。一個產品可以屬于一個分類,一個分類也可包含多個產品。
  • name: 這是產品的名字
  • slug: 用來為這個產品建立 URL 的 slug
  • image: 可選的產品圖片
  • description: 可選的產品描述
  • price: 這是個 DecimalField(譯者@ucag注:十進制字段)。這個字段使用 Python 的 decimal.Decimal 元類來保存一個固定精度的十進制數。max_digits 屬性可用于設定數字的最大值, decimal_places 屬性用于設置小數位數。
  • stock: 這是個 PositiveIntegerField(譯者@ucag注:正整數字段) 來保存這個產品的庫存。
  • available: 這個布爾值用于展示產品是否可供購買。這使得我們可在目錄中使產品廢棄或生效。
  • created: 當對象被創建時這個字段被保存。
  • update: 當對象最后一次被更新時這個字段被保存。

對于 price 字段,我們使用 DecimalField 而不是 FloatField 來避免精度問題。

我們總是使用 DecimalField 來保存貨幣值。 FloatField 在內部使用 Python 的 float 類型。反之, DecimalField 使用的是 Python 中的 Decimal 類型,使用 Decimal 類型可以避免精度問題。

Product 模型(model)中的 Meta 類中,我們使用 index_together 元選項來指定 idslug 字段的共同索引。我們定義這個索引,因為我們準備使用這兩個字段來查詢產品,兩個字段被索引在一起來提高使用雙字段查詢的效率。

由于我們會在模型(models)中和圖片打交道,打開 shell ,用下面的命令安裝 Pillow :

pip isntall Pillow==2.9.0

現在,運行下面的命令來為你的項目創建初始遷移:

python manage.py makemigrations

你將會看到以下輸出:

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

用下面的命令來同步你的數據庫:

python mange.py migrate

你將會看到包含下面這一行的輸出:

 Applying shop.0001_initial... OK

現在數據庫已經和你的模型(models)同步了。

注冊目錄模型(models)到管理站點

讓我們把模型(models)注冊到管理站點,這樣我們就可以輕松管理產品和產品分類了。編輯 shop 應用的 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 屬性來指定那些要使用其他字段來自動賦值的字段。正如你以前看到的那樣,這樣做可以很方便的生成 slugs 。我們在 ProductAdmin 類中使用 list_editable 屬性來設置可被編輯的字段,并且這些字段都在管理站點的列表頁被列出。這樣可以讓你一次編輯多行。任何在 list_editable 的字段也必須在 list_display 中,因為只有這樣被展示的字段才可以被編輯。

現在,使用如下命令為你的站點創建一個超級用戶:

python manage.py createsuperuser

使用命令 python manage.py runserver 啟動開發服務器。 訪問 http://127.0.0.1:8000/admin/shop/product/add ,登錄你剛才創建的超級用戶。在管理站點的交互界面添加一個新的品種和產品。 product 的更改頁面如下所示:

django-7-1

創建目錄視圖(views)

為了展示產品目錄, 我們需要創建一個視圖(view)來列出所有產品或者是給出的篩選后的產品。編輯 shop 應用中的 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 的查詢集來檢索可用的產品。我們使用一個可選參數 category_slug 通過所給產品類別來有選擇性的篩選產品。

我們也需要一個視圖來檢索和展示單一的產品。把下面的代碼添加進去:

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 視圖(view)接收 idslug 參數來檢索 Product 實例。我們可以只用 ID 就可以得到這個實例,因為它是一個獨一無二的屬性。盡管,我們在 URL 中引入了 slug 來建立搜索引擎友好(SEO-friendly)的 URL。

在創建了產品列表和明細視圖(views)之后,我們該為它們定義 URL 模式了。在 shop 應用的路徑下創建一個新的文件,命名為 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模式。 我們為 product_list 視圖(view)定義了兩個不同的 URL 模式。 命名為product_list 的模式不帶參數調用 product_list 視圖(view);命名為 product_list_by_category 的模式向視圖(view)函數傳遞一個 category_slug 參數,以便通過給定的產品種類來篩選產品。我們為 product_detail 視圖(view)添加的模式傳遞了 idslug 參數來檢索特定的產品。

像這樣編輯 myshop 項目中的 urls.py 文件:

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

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

在項目的主要 URL 模式中,我們引入了 shop 應用的 URL 模式,并指定了一個命名空間,叫做 shop

現在,編輯 shop 應用中的 models.py 文件,導入 reverse() 函數,然后給 Category 模型和 Product 模型添加 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])

正如你已經知道的那樣, get_absolute_url() 是檢索一個對象的 URL 約定俗成的方法。這里,我們將使用我們剛剛在 urls.py 文件中定義的 URL 模式。

創建目錄模板(templates)

現在,我們需要為產品列表和明細視圖創建模板(templates)。在 shop 應用的路徑下創建如下路徑和文件:

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

我們需要定義一個基礎模板(template),然后在產品列表和明細模板(templates)中繼承它。 編輯 shop/base.html 模板(template),添加如下代碼:

{% 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>

這就是我們將為我們的商店應用使用的基礎模板(template)。為了引入模板使用的 CSS 和圖像,你需要復制這一章示例代碼中的靜態文件,位于 shop 應用中的 static/ 路徑下。把它們復制到你的項目中相同的地方。

編輯 shop/product/list.html 模板(template),然后添加如下代碼:

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

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

{% 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 category %}{{ category.name }}{% else %}Products{% endif %}</h1>
        {% for product in products %}
            <div class="item">
                <a href="{{ product.get_absolute_url }}">
                    <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}">
                </a>
                <a href="{{ product.get_absolute_url }}">{{ product.name }}</a><br>
                ${{ product.price }}
            </div>
        {% endfor %}
    </div>
{% endblock %} 

這是產品列表模板(template)。它繼承了 shop/base.html 并且使用了 categories 上下文變量來展示所有在側邊欄里的產品種類,以及 products 上下文變量來展示當前頁面的產品。相同的模板用于展示所有的可用的產品以及經目錄分類篩選后的產品。由于Product 模型的 image 字段可以為空,我們需要為沒有圖片的產品提供一個默認圖像。這個圖片位于我們的靜態文件路徑下,相對路徑為 img/no_image.png

因為我們在使用 ImageField 來保存產品圖片,我們需要開發服務器來服務上傳圖片文件。編輯 myshop 項目的 settings.py 文件,添加以下設置:

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

MEDIA_URL 是基礎 URL,它為用戶上傳的媒體文件提供服務。MEDIA_ROOT 是一個本地路徑,媒體文件就在這個路徑下,并且是由我們動態的將 BASE_DIR 添加到它的前面而得到的。

為了讓 Django 給通過開發服務器上傳的媒體文件提供服務,編輯myshop 中的 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)

記住,我們僅僅在開發中像這樣提供靜態文件服務。在生產環境下,你不應該用 Django 來服務靜態文件。

使用管理站點為你的商店添加幾個產品,然后訪問 http://127.0.0.1:8000/ 。你可以看到如下的產品列表頁:

django-7-2

如果你用管理站點創建了幾個產品,并且沒有上傳任何圖片的話,就會顯示默認的 no_img.png

django-7-3

讓我們編輯產品明細模板(template)。 編輯 shop/product/detail.html 模板(template),添加以下代碼:

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

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

{% 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 %}

我們可以調用相關聯的產品類別的 get_absolute_url() 方法來展示有效的屬于同一目錄的產品。現在,訪問 http://127.0.0.1:8000 ,點擊任意產品,查看產品明細頁面。看起來像這樣:

django-7-4

創建購物車

在創建了產品目錄之后,下一步我們要創建一個購物車系統,這個購物車系統可以讓用戶選中他們想買的商品。購物車允許用戶在最終下單之前選中他們想要的物品并且可以在用戶瀏覽網站時暫時保存它們。購物車存在于會話中,所以購物車中的物品會在用戶訪問期間被保存。

我們將會使用 Django 的會話框架(seesion framework)來保存購物車。在購物車最終被完成或用戶下單之前,購物車將會保存在會話中。我們需要為購物車和購物車里的商品創建額外的 Django 模型(models)。

使用 Django 會話

Django 提供了一個會話框架,這個框架支持匿名會話和用戶會話。會話框架允許你為任意訪問對象保存任何數據。會話數據保存在服務端,并且如果你使用基于 cookies 的會話引擎的話, cookies 會包含 session ID 。會話中間件控制發送和接收 cookies 。默認的會話引擎把會話保存在數據庫中,但是正如你一會兒會看到的那樣,你也可以選擇不同的會話引擎。為了使用會話,你必須確認你項目的 MIDDLEWARE_CLASSES 設置中包含了 django.contrib.sessions.middleware.SessionMiddleware 。這個中間件負責控制會話,并且是在你使用命令startproject創建項目時被默認添加的。

會話中間件使當前會話在 request 對象中可用。你可以用 request.seesion 連接當前會話,它的使用方式和 Python 的字典相似。會話字典接收任何默認的可被序列化為 JSON 的 Python 對象。你可以在會話中像這樣設置變量:

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

檢索會話中的鍵:

request.session.get('foo')

刪除會話中已有鍵:

del request.session['foo']

正如你所見,我們像使用 Python 字典一樣使用 request.session

當用戶登錄時,他們的匿名會話將會丟失,然后新的會話將會為認證后的用戶創建。如果你在匿名會話中儲存了在登錄后依然需要被持有的數據,你需要從舊的會話中復制數據到新的會話。

會話設置

你可以使用幾種設置來為你的項目配置會話系統。最重要的部分是 SESSION_ENGINE .這個設置讓你可以配置會話將會在哪里被儲存。默認地, Django 用 django.contrib.sessionsSessions 模型把會話保存在數據庫中。

Django 提供了以下幾個選擇來保存會話數據:

  • Database sessions(數據庫會話):會話數據將會被保存在數據庫中。這是默認的會話引擎。
  • File-based sessions(基于文件的會話):會話數據保存在文件系統中。
  • Cached sessions(緩存會話):會話數據保存在緩存后端中。你可以使用 CACHES 設置來指定一個緩存后端。在緩存系統中保存會話擁有最好的性能。
  • Cached sessions(緩存會話):會話數據儲存于緩存后端。你可以使用 CACHES 設置來制定一個緩存后端。在緩存系統中儲存會話數據會有更好的性能表現。
  • Cached database sessions(緩存于數據庫中的會話):會話數據保存于可高速寫入的緩存和數據庫中。只會在緩存中沒有數據時才會從數據庫中讀取數據。
  • Cookie-based sessions(基于 cookie 的會話):會話數據儲存于發送向瀏覽器的 cookie 中。

為了得到更好的性能,使用基于緩存的會話引擎( cach-based session engine)吧。 Django 支持 Mercached ,以及 Redis 的第三方緩存后端和其他的緩存系統。

你可以用其他的設置來定制你的會話。這里有一些和會話有關的重要設置:

  • SESSION_COOKIE_AGE:cookie 會話保持的時間。以秒為單位。默認值為 1209600 (2 周)。
  • SESSION_COOKIE_DOMAIN:這是為會話 cookie 使用的域名。把它的值設置為 .mydomain.com 來使跨域名 cookie 生效。
  • SESSION_COOKIE_SECURE:這是一個布爾值。它表示只有在連接為 HTTPS 時 cookie 才會被發送。
  • SESSION_EXPIRE_AT_BROWSER_CLOSE:這是一個布爾值。它表示會話會在瀏覽器關閉時就過期。
  • SESSION_SAVE_EVERY_REQUEST:這是一個布爾值。如果為 True ,每一次請求的 session 都將會被儲存進數據庫中。 session 的過期時間也會每次刷新。

在這個網站你可以看到所有的 session 設置:https://docs.djangoproject.com/en/1.8/ref/settings/#sessions

會話過期

你可以通過 SESSION_EXPIRE_AT_BROWSER_CLOSE 選擇使用 browser-length 會話或者持久會話。默認的設置是 False ,強制把會話的有效期設置為 SESSION_COOKIE_AGE 的值。如果你把 SESSION_EXPIRE_AT_BROWSER_CLOSE 的值設為 True,會話將會在用戶關閉瀏覽器時過期,且 SESSION_COOKIE_AGE 將不會對此有任何影響。

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

在會話中保存購物車

我們需要創建一個能序列化為 JSON 的簡單結構,這樣就可以把購物車中的東西儲存在會話中。購物車必須包含以下數據,每個物品的數據都要包含在其中:

  • Product 實例的 id
  • 選擇的產品數量
  • 產品的總價格

因為產品的價格可能會變化,我們采取當產品被添加進購物車時同時保存產品價格和產品本身的辦法。這樣做,我們就可以保持用戶在把商品添加進購物車時他們看到的商品價格不變了,即使產品的價格在之后有了變更。

現在,你需要把購物車和會話關聯起來。購物車像下面這樣工作:

  • 當需要一個購物車時,我們檢查顧客是否已經設置了一個會話鍵( session key)。如果會話中沒有購物車,我們就創建一個新的購物車,然后把它保存在購物車的會話鍵中。
  • 對于連續的請求,我們在會話鍵中執行相同的檢查和獲取購物車內物品的操作。我們在會話中檢索購物車的物品和他們在數據庫中相關聯的 Product 對象。

編輯你的項目中 settings.py,把以下設置添加進去:

CART_SESSION_ID = 'cart'

添加的這個鍵將會用于我們的會話中來儲存購物車。因為 Django 的會話對于每個訪問者是獨立的(譯者@ucag注:原文為 per-visitor ,沒能想出一個和它對應的中文詞,根據上下文,我就把這個詞翻譯為了一個短語),我們可以在所有的會話中使用相同的會話鍵。

讓我們創建一個應用來管理我們的購物車。打開終端,然后創建一個新的應用,在項目路徑下運行以下命令:

python manage.py startapp cart

然后編輯你添加的項目中的 settings.py ,在 INSTALLED_APPS 中添加 cart

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

cart 應用路徑內創建一個新的文件,命名為 cart.py ,把以下代碼添加進去:

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

class Cart(object):
    def __init__(self, request):
        """
        Initialize the cart.
        """
        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 保存當前會話以便使其對 Cart類的其他方法可用。首先,我們使用 self.session.get(settings.CART_SESSION_ID) 嘗試從當前會話中獲取購物車。如果當前會話中沒有購物車,我們就在會話中設置一個空字典,這樣就可以在會話中設置一個空的購物車。我們希望我們的購物車字典使用產品 ID 作為鍵,以數量和價格為鍵值對的字典為值。這樣做,我們就能保證一個產品在購物車當中不被重復添加;我們也能簡化獲取任意購物車物品數據的步驟。

讓我們寫一個方法來向購物車當中添加產品或者更新產品的數量。把 save()add() 方法添加進 Cart 類當中:

def add(self, product, quantity=1, update_quantity=False):
    """
    Add a product to the cart or update its quantity.
    """
    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 session as "modified" to make sure it is saved
    self.session.modified = True

add() 函數接受以下參數:

  • product:需要在購物車中更新或者向購物車添加的 Product 對象
  • quantity:一個產品數量的可選參數。默認為 1
  • update_quantity:這是一個布爾值,它表示數量是否需要按照給定的數量參數更新(True),不然新的數量必須要被加進已存在的數量中(False

我們在購物車字典中把產品 id 作為鍵。我們把產品 id 轉換為字符串,因為 Django 使用 JSON 來序列化會話數據,而 JSON 又只接受支字符串的鍵名。產品 id 為鍵,一個有 quantityprice 的字典作為值。產品的價格從十進制數轉換為了字符串,這樣才能將它序列化。最后,我們調用 save() 方法把購物車保存到會話中。

save() 方法會把購物車中所有的改動都保存到會話中,然后用 session.modified = True 標記改動了的會話。這是為了告訴 Django 會話已經被改動,需要將它保存起來。

我們也需要一個方法來從購物車當中刪除購物車。把下面的方法添加進 Cart 類當中:

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

remove 方法從購物車字典中刪除給定的產品,然后調用 save() 方法來更新會話中的購物車。

我們將迭代購物車當中的物品,然后獲取相應的 Product 實例。為惡劣達到我們的目的,你需要定義 __iter__() 方法。把下列代碼添加進 Cart 類中:

def __iter__(self):
    """
    Iterate over the items in the cart and get the products
    from the database.
    """
    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 實例來把他們添加進購物車的物品中。之后,我們迭代所有的購物車物品,把他們的 price 轉換回十進制數,然后為每個添加一個 total_price 屬性。現在我們就可以很容易的在購物車當中迭代物品了。

我們還需要一個方法來返回購物車中物品的總數量。當 len() 方法在一個對象上執行時,Python 會調用對象的 __len__() 方法來檢索它的長度。我們將會定義一個定制的 __len__() 方法來返回保存在購物車中保存的所有物品數量。把下面這個 __len__() 方法添加進 Cart 類中:

def __len__(self):
    """
    Count all items in the cart.
    """
    return sum(item['quantity'] for item in self.cart.values())

我們返回所有購物車物品的數量。

添加下列方法來計算購物車中物品的總價:

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

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

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

我們的 Cart 類現在已經準備好管理購物車了。

創建購物車視圖

既然我們已經創建了 Cart 類來管理購物車,我們就需要創建添加,更新,或者刪除物品的視圖了。我們需要創建以下視圖:

  • 用于添加或者更新物品的視圖,且能夠控制當前的和更新的數量
  • 從購物車中刪除物品的視圖
  • 展示購物車物品和總數的視圖

添加物品

為了把物品添加進購物車,我們需要一個允許用戶選擇數量的表單。在 cart 應用路徑下創建一個 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 之間選擇產品的數量。我們使用了帶有 coerce=intTypeChoiceField 字段來把輸入轉換為整數
  • update:讓你展示數量是否要被加進已當前的產品數量上(False),否則如果當前數量必須被用給定的數量給更新(True)。我們為這個字段使用了HiddenInput 控件,因為我們不想把它展示給用戶。

讓我們一個新的視圖來想購物車中添加物品。編輯 cart 應用的 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')

這個視圖是為了想購物車添加新的產品或者更新當前產品的數量。我們使用 require_POST 裝飾器來只響應 POST 請求,因為這個視圖將會變更數據。這個視圖接收產品 ID 作為參數。我們用給定的 ID 來檢索 Product 實例,然后驗證 CartAddProductForm。如果表單是合法的,我們將在購物車中添加或者更新產品。我們將創建 cart_detail 視圖。

我們還需要一個視圖來刪除購物車中的物品。將以下代碼添加進 cart 應用的 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_detail 視圖接收產品 ID 作為參數。我們根據給定的產品 ID 檢索相應的 Product 實例,然后將它從購物車中刪除。然后,我們將用戶重定向到 cart_detail URL。

最后,我們需要一個視圖來展示購物車和其中的物品。講一下代碼添加進 veiws.py 中:

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

cart_detail 視圖獲取當前購物車并展示它。

我們已經創建了視圖來向購物車中添加物品,或從購物車中更新數量,刪除物品,還有展示他們。然我們為這些視圖添加 URL 模式。在 cart 應用中創建一個新的文件,命名為 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 應用的主 urls.py 文件,添加以下 URL 模式來引用 cart URLs:

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

確保你在 shop.urls 之前引用它,因為它比前者更加有限制性。

創建展示購物車的模板

cart_addcart_remove 視圖沒有渲染任何模板,但是我們需要為 cart_detail 創建模板。

cart 應用路徑下創建以下文件結構:

templates/
    cart/
        detail.html

編輯 cart/detail.html 模板,然后添加以下代碼:

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

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

{% 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="{{ product.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 %}

這個模板被用于展示購物車的內容。它包含了一個保存于當前購物車物品的表格。我們允許用用戶使用發送到 cart_add 表單來改變選中的產品數量。我們通過提供一個 Remove 鏈接來允許用戶從購物車中刪除物品。

向購物車中添加物品

現在,我們需要在產品詳情頁添加一個 Add to cart 按鈕。編輯 shop 應用中的 views.py,然后把 CartAddProductForm 添加進 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 應用的 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 運行開發服務器。現在,打開 http://127.0.0.1:8000/,導航到產品詳情頁。現在它包含了一個表單來選擇數量在將產品添加進購物車之前。這個頁面看起來像這樣:

django-7-5

選擇一個數量,然后點擊 Add to cart 按鈕。表單將會通過 POST 方法提交到 cart_add 視圖。視圖會把產品添加進當前會話的購物車當中,包括當前產品的價格和選定的數量。然后,用戶將會被重定向到購物車詳情頁,它長得像這個樣子:

django-7-6

在購物車中更新產品數量

當用戶看到購物車時,他們可能想要在下單之前改變產品數量。我們將會允許用戶在詳情頁改變產品數量。

編輯 cart 應用的 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})

我們為每一個購物車中的物品創建了 CartAddProductForm 實例來允許用戶改變產品的數量。我們把表單和當前物品數量一同初始化,然后把 update 字段設為 True ,這樣當我們提交表單到 cart_add 視圖時,當前的數量就被新的數量替換了。

現在,編輯 cart 應用的 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/ 。你將會看到一個表單來編輯每個物品的數量,長得像下面這樣:

django-7-7

改變物品的數量,然后點擊 Update 按鈕來測試新的功能。

為當前購物車創建上下文處理器

你可能已經注意到我們在網站的頭部展示了 Your cart is empty 的信息。當我們開始向購物車添加物品時,我們將看到它已經替換為了購物車中物品的總數和總花費。由于這是個展示在整個頁面的東西,我們將創建一個上下文處理器來引用當前請求中的購物車,盡管我們的視圖函數已經處理了它。

上下文處理器

上下文處理器是一個接收 request 對象為參數并返回一個已經添加了請求上下文字典的 Python 函數。他們在你需要讓什么東西在所有模板都可用時遲早會派上用場。

一般的,當你用 startproject 命令創建一個新的項目時,你的項目將會包含下面的模板上下文處理器,他們位于 TEMPLATES 設置中的 context_processors 內:

  • django.template.context_processors.debug:在上下文中設置 debug 布爾值和 sql_queries 變量,來表示在 request 中執行的 SQL 查詢語句表
  • django.template.context_processors.request:在上下文中設置 request 變量
  • django.contrib.auth.context_processors.auth:在請求中設置用戶變量
  • django.contrib.messages.context_processors.messages:在包含所有使用消息框架發送的信息的上下文中設置一個 messages 變量

Django 也使用 django.template.context_processors.csrf 來避免跨站請求攻擊。這個上下文處理器不在設置中,但是它總是可用的并且由安全原因不可被關閉。

你可以在這個網站看到所有的內建上下文處理器:https://docs.djangoproject.com/en/1.8/ref/templates/api/#built-in-template-context-processors

把購物車添加進請求上下文中

讓我們創建一個上下文處理器來將當前購物車添加進模板請求上下文中。這樣我們就可以在任意模板中獲取任意購物車了。

cart 應用路徑里添加一個新文件,并命名為 context_processors.py 。上下文處理器可以位于你代碼中的任何地方,但是在這里創建他們將會使你的代碼變得組織有序。將以下代碼添加進去:

from .cart import Cart

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

如你所見,一個上下文處理器是一個函數,這個函數接收一個 request 對象作為參數,然后返回一個對象字典,這些對象可用于所有使用 RequestContext 渲染的模板。在我們的上下文處理器中,我們使用 request 對象實例化了購物車,然后讓它作為一個名為 cart 的參數對模板可用。

編輯項目中的 settings.py ,然后把 cart.context_processors.cart 添加進 TEMPLATE 內的 context_processors 選項中。改變后的設置如下:

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 渲染 模板時執行。 cart 變量將會被設置在模板上下文中。

上下文處理器會在所有的使用 RequestContext 的請求中執行。你可能想要創建一個定制的模板標簽來代替一個上下文處理器,如果你想要鏈接到數據庫的話。

現在,編輯 shop應用的 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 重載你的服務器。打開 http://127.0.0.1:8000/ ,添加一些產品到購物車里。在網站頭里,你可以看到當前物品總數和總花費,就象這樣:

django-7-8

保存用戶訂單

當購物車已經結賬完畢時,你需要把訂單保存進數據庫中。訂單將要保存客戶信息和他們購買的產品信息。

使用下面的命令創建一個新的應用來管理用戶訂單:

python manage.py startapp orders

編輯項目中的 settings.py ,然后把 orders 添加進 INSTALLED_APPS 中:

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

現在你已經激活了你的新應用。

創建訂單模型

你需要一個模型來保存訂單的詳細信息,第二個模型用來保存購買的物品,包括物品的價格和數量。編輯 orders 應用的 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 模型包含幾個用戶信息的字段和一個 paid 布爾值字段,這個字段默認值為 False 。待會兒,我們將使用這個字段來區分支付和未支付訂單。我們也定義了一個 get_total_cost() 方法來得到訂單中購買物品的總花費。

OrderItem 模型讓我們可以保存物品,數量和每個物品的支付價格。我們引用 get_cost() 來返回物品的花費。

orders 應用下運行首次遷移:

python manage.py makemigrations

你將看到如下輸出:

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

運行以下命令來應用新的遷移:

python manage.py migrate

你的訂單模型已經同步到了數據庫中

在管理站點引用訂單模型

讓我們把訂單模型添加到管理站點。編輯 orders 應用的 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)

我們在 OrderItem 使用 ModelInline 來把它引用為 OrderAdmin 類的內聯元素。一個內聯元素允許你在同一編輯頁引用模型,并且將這個模型作為父模型。

python manage.py runserver 命令打開開發服務器,訪問 http://127.0.1:8000/admin/orders/order/add/ 。你將會看到如下頁面:

django-7-9

創建顧客訂單

我們需要使用訂單模型來保存在用戶最終下單時在購物車中的物品,創建新的訂單的工作流程如下:

    1. 向用戶展示一個訂單表來讓他們填寫數據
    1. 我們用用戶輸入的數據創建一個新的 Order 實例,然后我們創建每個物品相關聯的 OrderItem 實例。
    1. 我們清空購物車,然后把用戶重定向到成功頁面

首先,我們需要一個表單來輸入訂單詳情。在orders 應用路徑內創建一個新的文件,命名為 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']

這是我們將要用于創建新的 Order 對象的表單。現在,我們需要一個視圖來管理表格以及創建一個新的訂單。編輯orders應用的 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) 獲取到當前會話中的購物車。基于請求方法,我們將執行以下幾個任務:

  • GET 請求:實例化 OrderCreateForm 表單然后渲染模板 orders/order/create.html
  • POST 請求:驗證提交的數據。如果數據是合法的,我們將使用 order = form.save() 來創建一個新的 Order 實例。然后我們將會把它保存進數據庫中,之后再把它保存進 order 變量里。在創建 order 之后,我們將迭代無購車的物品然后為每個物品創建 OrderItem。最后,我們清空購物車。

現在,在 orders 應用路徑下創建一個新的文件,把它命名為 urls.py。添加以下代碼:

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

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

這個是 order_create 視圖的 URL 模式。編輯 myshopurls.py ,把下面的模式引用進去。記得要把它放在 shop.urls 模式之前:

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

編輯 cart 應用的 cart/detail.html 模板,找到下面這一行:

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

替換為:

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

用戶現在可以從購物車詳情頁導航到訂單表了。我們依然需要定義一個下單模板。在 orders 應用路徑下創建如下文件結構:

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

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

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

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

{% 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 %}

模板展示的購物車物品包括物品總量和下單表。

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

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

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

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

這是當訂單成功創建時我們渲染的模板。打開開發服務器,訪問 http://127.0.0.1:8000/ ,在購物車當中添加幾個產品進去,然后結賬。
你就會看到下面這個頁面:

django-7-10

用合法的數據填寫表單,然后點擊 Place order 按鈕。訂單就會被創建,然后你將會看到成功頁面:

django-7-11

使用 Celery 執行異步操作

你在視圖執行的每個操作都會影響響應的時間。在很多場景下你可能想要盡快的給用戶返回響應,并且讓服務器異步地執行一些操作。這特別和耗時進程或從屬于失敗的進程時需要重新操作時有著密不可分的關系。比如,一個視頻分享平臺允許用戶上傳視頻但是需要相當長的時間來轉碼上傳的視頻。這個網站可能會返回一個響應給用戶,告訴他們轉碼即將開始,然后開始異步轉碼。另一個例子是給用戶發送郵件。如果你的網站發送了通知郵件,SMTP 連接可能會失敗或者減慢響應的速度。執行異步操作來避免阻塞執行就變得必要起來。

Celery 是一個分發隊列,它可以處理大量的信息。它既可以執行實時操作也支持任務調度。使用 Celery 不僅可以讓你很輕松的創建異步任務還可以讓這些任務盡快執行,但是你需要在一個指定的時間調度他們執行。

你可以在這個網站找到 Celery 的官方文檔:http://celery.readthedocs.org/en/latest/

安裝 Celery

讓我們安裝 Celery 然后把它整合進你的項目中。用下面的命令安裝 Celery:

pip install celery==3.1.18

Celery 需要一個消息代理(message broker)來管理請求。這個代理負責向 Celery 的 worker 發送消息,當接收到消息時 worker 就會執行任務。讓我們安裝一個消息代理。

安裝 RabbitMQ

有幾個 Celery 的消息代理可供選擇,包括鍵值對儲存,比如 Redis 或者是實時消息系統,比如 RabbitMQ。我們會用 RabbitMQ 配置 Celery ,因為它是 Celery 推薦的 message worker。

如果你用的是 Linux,你可以用下面這個命令安裝 RabbitMQ :

apt-get install rabbitmg

(譯者@夜夜月注:這是debian系linux的安裝方式)

如果你需要在 Mac OSX 或者 Windows 上安裝 RabbitMQ,你可以在這個網站找到獨立的支持版本:
https://www.rabbitmq.com/download.html

在安裝它之后,使用下面的命令執行 RabbitMQ:

rabbitmg-server

你將會在最后一行看到這樣的輸出:

Starting broker... completed with 10 plugins

RabbitMQ 正在運行了,準備接收消息。

把 Celery 添加進你的項目

你必須為 Celery 實例提供配置。在 myshopsettings.py 文件的旁邊創建一個新的文件,命名為 celery.py 。這個文件會包含你項目的 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)

在這段代碼中,我們為 Celery 命令行程序設置了 DJANGO_SETTINGS_MODULE 變量。然后我們用 app=Celery('myshop') 創建了一個實例。我們用 config_from_object() 方法來加載項目設置中任意的定制化配置。最后,我們告訴 Celery 自動查找我們列舉在 INSTALLED_APPS 設置中的異步應用任務。Celery 將在每個應用路徑下查找 task.py 來加載定義在其中的異步任務。

你需要在你項目中的 __init__.py 文件中導入 celery來確保在 Django 開始的時候就會被加載。編輯 myshop/__init__.py 然后添加以下代碼:

# import celery
from .celery import app as celery_app

現在,你可以為你的項目開始編寫異步任務了。

CELERY_ALWAYS_EAGER 設置允許你在本地用異步的方式執行任務而不是把他們發送向隊列中。這在不運行 Celery 的情況下, 運行單元測試或者是運行在本地環境中的項目是很有用的。

向你的應用中添加異步任務

我們將創建一個異步任務來發送消息郵件來讓用戶知道他們下單了。

約定俗成的一般用法是,在你的應用路徑下的 tasks 模型里引入你應用的異步任務。在 orders 應用內創建一個新的文件,并命名為 task.py 。這是 Celery 尋找異步任務的地方。添加以下代碼:

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

@task
def order_created(order_id):
    """
    Task to send an e-mail notification when an order is
    successfully created.
    """
    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 任務。如你所見,一個 Celery 任務 只是一個用 task 裝飾的 Python 函數。我們的 task 函數接收一個 order_id 參數。通常推薦的做法是只傳遞 ID 給任務函數然后在任務被執行的時候需找相關的對象,我們使用 Django 提供的 send_mail() 函數來發送一封提示郵件給用戶告訴他們下單了。如果你不想安裝郵件設置,你可以通過一下 settings.py` 中的設置告訴 Django 把郵件傳給控制臺:

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

異步任務不僅僅適用于耗時進程,也適用于失敗進程組中的進程,這些進程或許不會消耗太多時間,但是他們或許會鏈接失敗或者需要再次嘗試連接策略。

現在我們要把任務添加到 order_create 視圖中。打開 orders 應用的 views.py 文件,按照如下導入任務:

from .tasks import order_created

然后在清除購物車之后調用 order_created 異步任務:

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

我們調用任務的 delay() 方法并異步地執行它。之后任務將會被添加進隊列中,將會盡快被一個 worker 執行。

打開另外一個 shell ,使用以下命令開啟 celery worker :

celery -A myshop worker -l info

Celery worker 現在已經運行,準備好執行任務了。確保 Django 的開發服務器也在運行當中。訪問 http://127.0.0.1:8000/ ,在購物車中添加一些商品,然后完成一個訂單。在 shell 中,你已經打開過了 Celery worker 所以你可以看到以下的相似輸出:

[2015-09-14 19:43:47,526: INFO/MainProcess] Received task: orders.
tasks.order_created[933e383c-095e-4cbd-b909-70c07e6a2ddf]
[2015-09-14 19:43:50,851: INFO/MainProcess] Task orders.tasks.
order_created[933e383c-095e-4cbd-b909-70c07e6a2ddf] succeeded in
3.318835098994896s: 1

任務已經被執行了,你會接收到一封訂單通知郵件。

監控 Celery

你或許想要監控執行了的異步任務。下面就是一個基于 web 的監控 Celery 的工具。你可以用下面的命令安裝 Flower:

pip install flower

安裝之后,你可以在你的項目路徑下用以下命令啟動 Flower :

celery -A myshop flower

在你的瀏覽器中訪問 http://localhost:5555/dashboard ,你可以看到激活了的 Celery worker 和正在執行的異步任務統計:

django-7-12

你可以在這個網站找到 Flower 的文檔:http://flower.readthedocs.org/en/latest/

總結

在這一章中,你創建了一個最基本的商店應用。你創建了產品目錄以及使用會話的購物車。你實現了定制化的上下文處理器來使購物車在你的模板中可用,實現了一個下單表格。你也學到了如何用 Celery 執行異步任務。

在下一章中,你將會學習在你的商店中整合一個支付網關,添加管理站點的定制化動作,以 CSV 的形式導出數據,以及動態的生成 PDF 文件。

(譯者@夜夜月注:接下來就是第八章了,請大家等待,正在進行中= =,不確定啥時候放出來= =,我是懶人)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容