django by example 實踐 myshop 項目(一)


點我查看本文集的說明及目錄。


本項目相關內容包括:

實現過程

CH7 創建在線商店

CH8 管理支付和訂單

CH9 擴展商店


CH7 創建在線商店


上一章,我們創建了關注系統和用戶活動流,我們還學習了如何使用 Django signal ,以及在項目中集成 Redis 來對視圖進行計數。本章,我們將學習如何創建基本的在線商店,創建商品目錄和使用 Django sessions 將商品放入購物車。我們還將學習如何創建自定義內容處理器和使用 Celery 加載異步任務。

本章將包含以下內容:

  • 創建產品目錄
  • 使用 Django session 創建購物車
  • 管理用戶訂單
  • 使用 Celery 為用戶發送異步通知

創建一個在線商店項目


我們將創建一個在線商店項目。我們的用戶將能夠通過商品目錄瀏覽商品并將商品放入購物車。最后,檢查購物車并下單。本章將包括在線商店的以下功能:

  • 創建商品目錄模型,將其添加到 admin網站,并且創建展示商品目錄的基本視圖;
  • 使用 Django session 創建購物車系統幫助用戶瀏覽網站時保存選擇的商品;
  • 創建表單和下單功能;
  • 用戶下單成功后為用戶發送同步郵件。

首先,打開 teminal 并使用以下命令來為新項目創建虛擬環境并激活:

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

筆者注:

可以將虛擬環境放到前兩個項目 ( blog、bookmarks ) 虛擬環境所在的文件夾下,這時, mkdir env 改為 cd env

筆者這里仍然使用第一章中用到的 PyCharm 創建虛擬環境。

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

 pip install django

運行以下命令并創建名為 myshop 的新項目,并在項目中創建名為 shop 的新應用:

django-admin startporject myshop
cd myshop/
django-admin startapp shop

然后在項目 settings.py 文件的 INSTALLED_APPS 中添加應用名稱:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'shop',
]

現在,shop 應用已經激活,我們來為商品目錄定義模型。

from django.db import models


# Create your models here.

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

這是 Category 和 Product 模型。 Category 模型包含 name 字段和 slug 唯一字段。Product 模型字段如下:

  • category: Category 模型的外鍵。這是一個多對一關系,一個商品屬于一個目錄,一個目錄下包含多個商品。
  • name: 商品名稱。
  • slug:商品 slug,用于生成漂亮的 URLs 。
  • image:商品圖片,可選。
  • description:商品描述,可選。
  • price:DecimalField,該字段使用 Python 的 decimal.Decimal 類型來存儲固定精度的decimal。max_digit 屬性設置數字的最大值(包括 decimal 位),decimal_places 設置 decimal 位。
  • stock:PositiveIntegerField ,用于保存商品庫存。
  • available:布爾值,表示是否可以獲得商品。它可以幫助我們控制商品是否出現在商品目錄中。
  • created:商品創建時間。
  • updated:商品更新時間。

對于 price 字段,我們使用 DecimalField 代替 FloatField 以防止小數位數問題。

注意:

一定要使用 Decimal 保存錢數。 FloatField 使用 Python float 類型, DecimalField 使用 Python 的 decimal.Decimal 類型。通過使用 DecimalField,可以防止小數位數問題。

在 Product 模型的 Meta 類中,由于我們計劃使用 id 和 slug 進行索引,這里使用 index_together 選項指定使用 id 和 slug 進行索引。兩個索引組合可以改善兩個字段的查詢性能。

由于模型需要處理圖片,打開 shell 并使用以下命令安裝 Pillow :

  pip install Pillow

現在,運行另一個命令來為項目創建初始遷移文件:

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))

運行以下命令同步數據庫:

python manage.py migrate

可以看到以下輸出:

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, shop
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK
  Applying shop.0001_initial... OK

現在,數據庫與模型同步了。

將產品目錄模型注冊到 admin網站

將產品目錄模型注冊到 admin網站可以幫助我們管理目錄和產品。編輯 shop 應用的 admin.py 文件并添加以下代碼:

from django.contrib import admin

from .models import Category, Product


# Register your models here.

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 屬性用來指定使用其它字段的值自動生成值的字段。正如我們前面看到的,這是生成 slug 的簡便方法。在 ProductAdmin 類中使用 list_editable 屬性設置 admin網站的列表展示頁面可以更改的字段。這樣可以同時編輯多行,由于只有展示的內容才能進行編輯,list_editable 的任何字段都必須在 list_display 中。

筆者注:

可以采用第五章中的方法, 分別重寫 Category 和 Product 的 save 方法自動生成 slug :

from django.utils.text import slugify


class Category(models.Model):
    ...

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super(Category, self).save(*args, **kwargs)
        
        
class Product(models.Model):
  ...
  
  def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super(Product, self).save(*args, **kwargs)

現在,使用以下命令為網站創建超級用戶:

python manage.py createsuperuser

使用 python manage.py runserver 命令啟動開發服務器,在瀏覽器中打開 http://127.0.0.1:8000/admin/shop/product/add/并使用剛剛創建的賬號登錄。使用 admin網站添加一個新的商品目錄和一個新的商品,admin網站的商品更改列表頁面看起來是這樣的:

CH7_1.png

創建產品目錄視圖


為了展示產品目錄,我們需要創建一個視圖列出所有產品或者通過給定類別對產品進行過濾。編輯 shop 應用的views.py 文件并添加以下代碼:

from django.shortcuts import render, get_object_or_404

from .models import Category, Product


# Create your views here.



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 參數來獲得給定類別的商品。

我們還需要一個視圖來獲取和展示單個產品。在 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 視圖需要id 和 slug 參數來檢索 Product 實例。由于 id 的唯一屬性,我們只通過 id 就可以獲得實例。然而我們包含URL 中的 slug 來為商品創建SEO-友好的 URL。

創建完產品列表和詳情視圖,我們需要為它們定義 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 視圖設置了兩個不同的 URL模式:一個模式為 product_list ,可以在不輸入任何參數的情況下的調用 product_list 視圖,另一個模式為 product_list_by_category ,需要向視圖提供 category_slug 參數來告訴視圖通過給定類別對進行過濾。我們為 product_detail 視圖設置了一個模式,該視圖需要提供 id 和 slug 參數獲得指定的產品。

編輯 myshop 項目的 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')), ]

在項目的主 URLs模式中,使用自定義命名空間 shop 包含 shop 應用的 URLs。

現在,編輯 shop 應用的 models.py 文件,導入 reverse() 函數,并向 Category 和 Product 模型添加 get_absolute_url() 方法:

from django.urls 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])

筆者注:

這里使用 from django.urls import reverse 代替了原文的 from django.core.urlresolvers import reverse

我們已經知道,get_absolute_url() 是獲得指定對象 url 的簡便方法,這里,我們將使用剛剛在 urls.py 文件中定義的 URLs模式。

創建產品目錄模板

現在,我們需要為商品列表和詳情視圖創建模板。在 shop 應用目錄下創建下面的目錄和文件結構:

CH7_2.png

我們需要定義基礎模板,然后在產品列表和詳情模板中對其進行擴展。編輯 shop/base.html 模板并添加以下代碼:

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>{% block title %}My shop{% endblock %}</title>
    <link href="{% static "shop/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>

這是商店的基礎模板。為了包含模板使用的 CSS 文件和圖片,我們需要將本章 shop 應用的 static/ 目錄下的靜態文件拷貝到相同的路徑下。

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

{% 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 "shop/img/no_image.png" %}{% endif %}">
                    
                    
                </a>
                
                <a href="{{ product.get_absolute_url }}">{{ product.name }}</a><br>
                ${{ product.price }}
            </div>
        {% endfor %}
    </div>
{% endblock %}

筆者注:

如果想統一商品圖片的大小以及節約空間,這里可以使用縮略圖,我們在第五章學習了縮略圖的用法。

使用 pip install sorl-thumbnail 安裝,在項目 settings.py 的 INSTALLED_APPS 中添加 'sorl.thumbnail'。

使用 python manage.py migrate 同步數據庫。

然后將 <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}">

更改為:

{% load thumbnail %}

{% if product.image %}
    {% thumbnail product.image '300x200' as im %}
        <img src="{{ im.url }}">
    {% endthumbnail %}

{% else %}
    <img src="{% static 'shop/img/no_image.png' %}">
{% endif %}

這是產品列表模板。它擴展 shop/base.html 模板并在邊欄中使用 categories 變量展示所有分類,使用 products 展示當前頁面的產品。這個模板適用于兩種情況:列出所有可獲得的產品和通過類別過濾到的產品。由于 Product 模型的 image 字段可以為空,我們需要為沒有圖片的產品設置默認圖片。默認圖片位于靜態文件中的 img/no_image.png 。

由于使用 ImageField 存儲產品圖片,我們需要開發服務器提供上傳圖片文件服務。編輯 myshop 的 settings.py 文件并添加以下設置:

# Media files

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
from django.contrib import admin

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

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

只在開發過程中這樣處理靜態文件,在生產過程中,不要使用 Django 處理靜態文件。

使用 admin網站為商店添加幾個商品并在瀏覽器中打開 http://127.0.0.1:8000/。你將看到商品列表頁面:

CH7_3.png

如果你使用 admin 網站創建了商品但是沒有上傳圖片,那么將看到 no_image.png:

CH7_4.png

編輯 shop/product/detail.html 來編輯產品詳情模板,模板并添加以下代碼:

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

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

{% block content %}
    <div class="product-detail">
    <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static 'shop/img/no_image.png' %}{% endif %}">
    <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 %}

我們將調用 category 的 get_absolute_url() 方法來獲得同類的產品列表。現在,在瀏覽器中打開 http://127.0.0.1:8000/,點擊任意產品來查看產品詳情。看起來是這樣的:

CH7_5.png

創建購物車


創建完商品目錄之后,下一步是創建保存用戶選擇的商品的購物車。購物車幫助用戶選擇想要的商品并在用戶瀏覽網站時暫時保存選中的商品直到用戶下單。購物車應該放在 session 中以便用戶瀏覽時將商品放入購物車。

我們將使用 Django session 框架來存放購物車。 用戶結賬或者退出登錄之前購物車將保留在 session 中。我們需要創建額外的 Django 模型來保存購物車中的商品。

使用 Django session


Django 提供 session 框架來支持匿名和用戶會話。 session 框架幫助我們為每位瀏覽者保存任何數據。 除非使用基于 cookie 的 session 引擎,Session 數據一般存儲在服務端, cookie 則存儲 session ID。 session 中間件負責管理發送和接收 cookies 。默認的 session 引擎在數據庫中保存 session 數據,當然,也可以選擇其它的 session 引擎。

為了使用 session,我們需要項目設置的 MIDDLEWARE_CLASSES 中包含django.contrib.sessions.middleware.SessionMiddleware。這個引擎用于管理 sessions ,如果使用startproject 命令創建新項目時默認添加。

session 中間件可以實現從 request 對象中訪問當前 session 。我們可以通過 request.session 得到當前 session ,可以像使用 Python 字典一樣保存和獲得 session 數據。session 字典接收任何可以序列化為 JSON 的 Python 對象,我們可以這樣設置 session 變量:

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

獲得 session 的值:

request.session.get('foo')

刪除 session 中保存的一個值:

del request.session['foo']

我們可以看到,可以像操作 Python 字典一樣處理 request.session 。

注意:

當用戶登錄網站時,將丟棄他們的匿名會話并為有權限的用戶創建一個新的 session 。如果需要保存一個登錄后可用的匿名 session ,那么需要將舊的 session 數據拷貝到新的 session 數據中。

session 設置


可以使用幾種方法為配置項目 sessions 。最重要的是 SESSION_ENGINE。這個設置允許用戶設置 session 存儲位置。默認情況下,Django 使用 django.contrib.sessions 應用的 Session 模型將數據保存到數據庫中。

Django 提供以下存儲 session 數據的選項:

  • Database sessions: Session 數據保存在數據庫中,默認的 session 引擎。
  • File-based sessions: Session 數據保存在文件系統中。
  • Cached sessions: Session 數據保存在緩存后端,可以使用 CACHES 設置指定緩存后端,將 session 數據保存在緩存后端可以實現最好的性能。
  • Cached database sessions: Session 數據保存在 write-through 緩存和數據庫。數據不在緩存中時才讀取數據庫。
  • Cookie-based sessions: Session 數據保存在發送到瀏覽器的 cookies 中。

注意:

為了獲得更好的性能可以使用 cache-based session 引擎。Django 支持 Memcached 和其它 Redis 第三方緩存后端以及其他緩存系統。

你可以使用其它設置自定義 sessions 。這里有一些非常重要的 session 設置:

SESSION_COOKIE_AGE: session cookie 保存時間(秒為單位)。默認值為 1209600 (2 周)。

SESSION_COOKIE_DOMAIN: session cookies 使用的域,將其設置為 .mydomain.com 可以實現跨域 cookies 。

SESSION_COOKIE_SECURE : 布爾值,是否只有HTTPS連接才能發送cookie。

SESSION_EXPIRE_AT_BROWSER_CLOSE: 布爾值,關閉瀏覽器時 session 是否過期。

SESSION_SAVE_EVERY_REQUEST: 布爾值,如果為 True,每個request 都會將 session 保存到數據庫,并且每次更新 session 到期時間。

所有的 session 設置參考 https://docs.djangoproject.com/en/1.11/ref/settings/#sessions

session 過期


你可以通過設置 SESSION_EXPIRE_AT_BROWSER_CLOSE 選擇使用瀏覽器長度 session 或者持久 session 。這里的默認設置為 False ,session 的有效期將取決于 SESSION_COOKIE_AGE 設置的值。如果將 SESSION_EXPIRE_AT_BROWSER_CLOSE 設置為 True ,session 將在關閉瀏覽器時失效, SESSION_COOKIE_AGE 設置的值不會起作用。

你可以使用request.session 的 set_expiry() 方法重寫當前 session 的有效時間。

在 sessions 中保存購物車


我們需要創建一個可以序列化為 JSON 的簡單結構來在 session 中保存購物車內的商品 。 購物車中的每種商品需要包含以下數據:

  • Product 實例的 id ;

  • 商品的數量;

  • 這個商品的單價;

由于商品價格可能變動,當添加到購物車時,我們將商品價格和商品放在一起。這樣,可以將價格保持在顧客將商品添加到購物車時的價格,即使價格隨后可能發生改變也不會受到影響。

現在,我們需要創建購物車并將數據保存到 session 中。購物車需要這樣工作:

  • 當需要購物車時,我們檢查是否設置了一個自定義 session 鍵,如果 session 沒有設置購物車,我們將創建新的購物車并將其保存到購物車 session 鍵中。

  • 對于成功請求,我們進行相同的檢查并從 購物車 session 鍵中獲得值。我們從 session 中獲得購物車中的商品并從數據庫中獲取對應 Product 對象。

編輯項目的 settings.py 文件并添加以下設置:

# session settings
CART_SESSION_ID = 'cart'

這是我們在用戶 session 中保存購物車的鍵。由于 Django session 是 pre-visitor,所有 session 都使用相同的購物車 session 鍵。

我們來創建一個管理購物車的應用,打開 teminal 并創建一個新的應用,在項目目錄下運行以下命令:

python manage.py startapp cart

然后,然后在項目 settings.py 文件的 INSTALLED_APPS 中添加應用名稱:

INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth',
    'django.contrib.contenttypes', 'django.contrib.sessions',
    'django.contrib.messages', 'django.contrib.staticfiles', 'shop', 'cart']

在 cart 應用的目錄下創建一個 cart.py 的新文件,并添加以下代碼:

from django.conf import settings

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 對象對 cart 進行初始化。使用 self.session = request.session 來保存當前 session,以便 Cart 類的其它方法可以訪問它。首先,我們使用self.session.get(settings.CART_SESSION_ID) 從當前 session 中獲得 cart,如果當前session 中沒有 cart ,那么在 session 中設置一個空字典來設置一個空的 cart 。我們期望 cart 字典使用商品 id 作為鍵,由商品數量和價格組成的字典作為值。這樣,可以保證 cart 不能多次添加同一商品,還便于訪問 cart 中的任意商品數據。

我們來創建一個方法在購物車中添加商品或更新商品數量。向 Cart 類添加以下 add() 和 save() 方法:

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 實例;
  • quality: 商品數量,可選的整數,默認值為 1 ;
  • update_quality :布爾值,是否使用輸入的數量對數量進行更新的標志位,如果為True,則根據輸入的數量更新數量,如果為 False ,新值與原來的值相加。

我們使用商品 id 作為 cart 字典的鍵,由于Django 使用 JSON 進行序列化,而 JSON 只允許字符串鍵,因此這里將商品 id 轉換為字符串。商品 id 為鍵,quality 和 price 組成的字典為值。由于序列化要求,商品的價格也由 Decimal 格式轉換為 string 格式。最后,調用 save() 方法將 cart 保存到 session 中。

save() 方法在 session 中保存 cart 的所有變化,并通過 session.modified = True 將 session 標記為更改狀態。這將告訴 django 發生了更改需要進行保存。

我們還需要一個從購物車中刪除商品的方法,向 Cart 類添加以下方法:

def remove(self, product):
    """
    Remove a product from the cart
    :param product: 
    :return: 
    """
    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 的格式更改回 Decimal,并為每一項添加 total_price 屬性。現在,我們可以很容易的遍歷購物車中的商品了。

我們還需要返回購物車中的所有商品數量,當我們對一個對象執行 len() 函數時,Python 調用它的 __len__方法來獲得長度。我們將定義自定義 __len__方法來返回購物車中商品的總數量。在 Cart 類中添加 __len__方法:

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())

最后,添加方法來清理購物車 session:

def clear(self):
    """
    remove cart from session
    :return:
    """
    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 。我們使用 TypedChoiceField 字段和 coerce=int 來將輸入轉換為整型;

  • update: 標志位,如果為False,則在購物車原數量的基礎上增加quantity,如果為True,則將數量設置為 quantity。由于不想讓用戶看到,這個字段使用了 HiddenInput 小控件。

創建一個向購物車添加商品的視圖。編輯 cart 應用的 views.py 視圖:

from django.shortcuts import 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
# Create your views here.
@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 URL 來展示購物車中的商品。我們稍后將創建 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_remove 視圖接收商品 id 。使用給定的 ID 獲得 Product 實例并從購物車中移除該商品,然后重定向到 cart_detail URL。

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

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

cart_detail 獲取當前的購物車并進行展示。

我們已經創建了購物車添加商品、更新數量、刪除商品、展示商品的視圖。接下來我們為這些視圖添加 URL 模式。 在 cart 應用目錄下創建一個 urls.py 的新文件,并添加下面的內容:

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 文件并添加 cart 的 URLs :

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

確保 cart.urls 在 shop.urls 之前,因為它比 shop.url 限制更多。

創建展示購物車的模板


cart_add 和 cart_delete 視圖不需要渲染模板,但是我們需要為 cart_detail 視圖創建模板來展示購物車中的商品和總數。

在 cart 應用目錄下創建下面的文件結構:

編輯 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 }}">
                            <img src="
                                    {% if product.image %}{{ product.image.url }}{% else %}{% static "shop/img/no_image.png" %}{% endif %}">
                        </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 視圖的表單更改選中產品的數量。我們還通過為每個商品提供刪除鏈接來刪除商品。

將商品添加到購物車


現在,我們需要為商品詳情頁面添加一個 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/ 并點擊某個商品到商品詳情頁面。現在頁面在添加到購物車前面有一個選擇數量的選項。頁面看起來是這樣的:

CH7_6.png

選擇數量并點擊 Add to cart 按鈕。表單通過 POST 方法提交到 cart_add 視圖。視圖將商品(包括商品的價格和選擇的數量)添加到 session 的購物車中。然后,重定向到購物車詳情頁面,看起來是這樣的:

CH7_7.png

在購物車中更新產品數量


用戶查看購物車時,他們在下單之前可能需要更改產品數量。下面將實現購物車詳細頁面更改商品數量的功能。

編輯 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/,可以看到每個商品都包含編輯數量的表單:

CH7_8.png

更改某個商品的數量,并點擊 Update 按鈕對新功能進行測試。

為當前購物車創建內容處理器


你可能已經注意到,頁面頭部還在顯示 Your cart is empty 的信息。當我們開始向購物車添加商品時,我們應該可以看到購物車添加商品的數量和總金額。由于需要在所有頁面展示這些信息,我們需要創建內容處理器將當前購物車包含在請求內容中,這些內容與被處理的視圖無關。

內容處理器


內容處理器是一個 Python 函數,它的輸入參數為 request 對象,返回添加到請求內容中的字典。它可以用來生成所有模板都需要的內容。

默認情況下,使用 startproject 命令創建一個新的項目,項目將包含下面的內容處理器(在 TEMPLATES 設置的 context_processors 選項中):

  • django.template.context_processors.debug:設置內容中表示請求執行的 SQL 查詢列表的 debug 布爾值和 sql_queries 變量;

  • django.template.context_processor.request:設置內容中的 request 變量;

  • django.contrib.auth.context_processors.auth:設置請求中的用戶變量;

  • django.contrib.messages.context_processors.messages:設置message 變量,message 變量包括消息框架中的所有消息;

Django 還將啟用 django.template.context_processors.csrf 來避免跨網站請求偽造攻擊。這個內容處理器沒有出現在設置中,但是它一直處于啟用狀態,并且由于安全原因無法關閉。

我們可以在以下頁面了解所有的內容內容處理器https://docs.djangoproject.com/en/1.11/ref/templates/api/#built-in-template-context-processors

在請求內容中設置購物車


我們創建內容處理器來將購物車放到模板的 request 內容中。這樣任意模板都可以訪問這個購物車。

在 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' 添加到 TEMPLATES 設置的 context_processors 選項中。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        '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',
            ],
        },
    },
]

現在,每次使用 Django 的 RequestContext 渲染模板時都會執行我們剛剛創建的模板處理器,模板內容中將包含模板變量。

注意:

內容處理器在所有使用 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/并在購物車中添加一些商品,在頁面的頭部,你將看到商品的總數和總價格:

CH7_9.png

管理用戶訂單


結算時,我們需要將訂單保存到數據庫中,訂單將包含用戶信息以及他們購買的商品。

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

python manage.py startapp orders

編輯項目的 settings.py 文件并在 INSTALLED_APPS 設置中添加 'orders':

INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth',
                  'django.contrib.contenttypes', 'django.contrib.sessions',
                  'django.contrib.messages', 'django.contrib.staticfiles',
                  'shop', 'cart', 'orders', ]

你已經激活了一個新應用。

創建訂單模型


我們需要創建一個存儲訂單詳情的模型以及一個存儲訂單購買的商品(包含價格和數量)的模型。編輯 orders 應用的 models.py 文件并添加以下代碼:

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


# Create your models here.


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 模型包含幾個用戶信息的字段和一個默認設置為 False 的布爾字段 paid 。后面我們將使用這個字段區分訂單是否付款。此外,還定義了一個 get_total_cost() 方法來計算訂單商品的總費用。

OrderItem 模型用來保存每個訂單的商品、數量和價格,并使用 get_cost() 計算商品費用。

運行以下命令對 order 應用進行第一次遷移:

python manage.py makemigrations 

你將看到這樣的輸出:

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

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

python manage.py migrate

現在我們的訂單模型已經同步到數據庫了。

將訂單模型注冊到 admin網站


我們將訂單模型添加到 admin網站。編輯 order 應用的 admin.py 文件并添加以下代碼:


from django.contrib import admin

from .models import OrderItem, Order


# Register your models here.
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)

我們在 OrderAdmin 中對 OrderItem 模型使用 ModelInline 來將其包含在 OrderAdmin 類中。內聯(inline)幫助我們將一個模型放入父模型的編輯頁面中。

使用 python manage.py runserver 啟動開發服務器。在瀏覽器中打開http://127.0.0.1:8000/admin/orders/order/add/ ,你將看到下面的頁面:

CH7_10.png

創建用戶訂單


我們需要在用戶下單時使用剛剛創建的訂單模型來保存購物車中的商品。創建新訂單需要完成以下工作:

  1. 為用戶提供一個訂單表單來填寫用戶數據;

  2. 使用用戶輸入的數據創建一個新的 Order 實例,然后為購物車的每件商品關聯一個 OrderItem 實例;

  3. 清空購物車的內容并重定向到成功頁面。

    ?

首先,我們需要一個表單來填寫訂單詳情。在 order 應用目錄下新建一個 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 cart.cart import Cart
from django.shortcuts import render

from .forms import OrderCreateForm
from .models import OrderItem


# Create your views here.


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 請求:驗證 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模式。編輯 myshop 的 urls.py 并添加以下模式。記得將其放在 shop.urls 模式之前:

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

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

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

將其替換為:

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

用戶現在可以從購物車詳情頁面跳轉到訂單表單了。我們還需要為訂單定義模板。在 orders 應用下創建如下結構:

CH7_11.png

編輯 orders/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/,在購物車中添加一些商品,然后跳轉到結算頁面。你將看到下面的內容:

CH7_12.png

在表單中填寫有效數據并點擊 Place order 按鈕,訂單將被創建,你見看到一個下面這樣的成功頁面。


CH7_13.png

使用 Celery 加載異步任務


視圖執行的所有內容都對響應時間有影響。我們希望盡快返回響應以及在服務器上異步執行一些進程。這對耗時的進程或需要重試策略的進程而言尤為重要。 比如,用戶可以在視頻共享平臺上傳視頻,但對上傳的視頻進行轉碼需要很長時間。 網站可能向用戶返回響應,告訴他很快開始轉碼,并開始進行異步轉碼。 另一個例子是給用戶發送電子郵件。 如果您的站點使用視圖發送電子郵件通知,SMTP 連接可能失敗或者影響響應速度。 啟動異步任務對避免阻塞執行非常重要。

Celery 是一個可以處理大量信息的分布式任務隊列。它進行實時處理,同時支持任務計劃。使用 Celery 不僅可以更輕松的創建異步任務,并且可以盡快執行,但是也可以為這些任務設定執行時間。

你可以在這里找到 Celery 文檔 http://celery.readthedocs.io/en/latest/

安裝 Celery


我們來安裝 Celery 并集成到項目中。通過 pip 使用以下命令安裝 Celery :

pip install celery

Celery 需要一個消息中間件來處理外部請求。 消息中間件負責將消息發送到 Celery ,Celery 收到消息時處理任務。 下面來安裝一個消息中間件。

安裝 RabbitMQ


Celery 可以使用幾種消息中間件,包括鍵值對存儲(如 redis )和實際消息系統( 如 RabbitMQ )。由于 Celery 推薦使用 RabbitMQ,這里使用 RabbitMQ 配置 Celery 。

如果使用 Linux,可以使用如下命令安裝 RabbitMQ :

apt-get install rabbitmq

如果需要在 Mac OS X 或者 Windows 上安裝 RabbitMQ,可以在 https://www.rabbitmq.com/download.html 中找到獨立版本。

安裝完后,使用以下命令加載 RabbitMQ :

rabbitmq-server

你將看到以下面內容結尾的輸出:

Starting broker... completed with 6 plugins.

RabbitMQ 開始運行并準備接收消息了。

在項目中添加 Celery


我們需要為 Celery 實例提供配置文件。在 myshop 項目中(與 settings.py 同級)新建 celery_task.py 的文件,這個文件保存項目的 Celery 配置。添加以下代碼:

import os

from django.conf import settings

from celery import Celery

# 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 將在每個應用目錄中尋找 tasks.py 文件來加載文件定義的異步任務。

我們需要在項目的__init__文件中導入 celery 模塊確保 Django 啟動時加載該模塊。

筆者注:

__init__.py文件的位置:

- proj/
  - manage.py
  - proj/
    - __init__.py
    - settings.py
    - urls.py

編輯 myshop/__init__.py 文件并添加以下代碼:

# import celery
from .celery_task import app as celery_app

筆者注:

原文中,與celery_task.py 的文件名為 celery.py , celery.py 與 celery 模塊的名稱相同會造成后續運行時出現以下錯誤:

File "/Users/apple/profile/django_by_example/myshop/myshop/myshop/celery.py", line 5, in <module>

from celery import Celery

ImportError: cannot import name Celery

參考:

https://stackoverflow.com/questions/19577172/celery-worker-error-importerror-no-module-named-celery

現在我們可以開始為應用編寫異步程序了。

注意:

Celery_ALWAYS_EAGER 設置允許我們按照同步方式在本地執行任務,而不是將其發送到隊列中。這對于單元測試或在本地環境不運行 Celery 的項目非常有幫助。

為應用添加異步任務


我們將創建異步任務,用戶下單后異步發送通知郵件。

我們只需將應用的異步任務放入應用目錄下的 tasks 模塊即可。在 orders 應用中新建 tasks.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_create 任務。你可以看到,一個 Celery 任務只是一個使用 task 裝飾的 Python 函數。 task 函數接收 order_id 參數。推薦只將 ID 傳入任務函數并在執行任務時執行查詢。我們使用 Django 提供的 send_mail() 函數來通知用戶已經下單。如果不希望啟動郵件設置,可以在 settings.py 文件中進行以下設置將郵件輸出到 console :

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() 方法來異步執行任務。任務將被添加到隊列并盡快執行。

打開另一個 shell 并使用下面的命令啟動 celery 工作:

celery -A myshop.celery_task:app worker -l info

筆者注:

原文命令為:

celery -A myshop worker -l info

由于前面將 celery.py 改為 celery_task ,因此執行的命令指定了文件名。

celery 現在正在運行并可以處理任務。啟動 Django 開發服務器。在瀏覽器中打開 http://127.0.0.1:8000/ ,在購物車中添加一些商品,完成訂單,在 shell 中,已經開啟了 Celery 任務并將看到以下輸出:

** -------------- celery@appledeMacBook.local v4.1.0 (latentcall)**

**---- \**** ----- **

**--- \* ***  * -- Darwin-15.6.0-x86_64-i386-64bit 2018-02-22 03:00:41**

**-- \* - **** --- **

**- \** ---------- [config]**

**- \** ---------- .> app:         myshop:0x1066daa90**

**- \** ---------- .> transport:   amqp://guest:**@localhost:5672//**

**- \** ---------- .> results:     disabled://**

**- \*** --- * --- .> concurrency: 2 (prefork)**

**-- \******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)**

**--- \***** ----- **

** -------------- [queues]**

**                .> celery           exchange=celery(direct) key=celery**

**                **

[tasks]

  . orders.tasks.order_created

[2018-02-22 03:00:41,705: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//

[2018-02-22 03:00:41,730: INFO/MainProcess] mingle: searching for neighbors

[2018-02-22 03:00:42,775: INFO/MainProcess] mingle: all alone

**[2018-02-22 03:00:42,832: WARNING/MainProcess] /Library/Python/2.7/site-packages/celery/fixups/django.py:202: UserWarning: Using settings.DEBUG leads to a memory leak, never use this setting in production environments!**

**  warnings.warn('Using settings.DEBUG leads to a memory leak, never '**

[2018-02-22 03:00:42,833: INFO/MainProcess] celery@appledeMacBook.local ready.

[2018-02-22 03:00:43,103: INFO/MainProcess] Events of group {task} enabled by remote.

任務已經執行,我們可以接收到下單通知了。

監控 Celery


我們可能希望監控正在執行的異步任務。Flower 是一個基于 web 的 Celery 監控器。可以使用 pip install flower 安裝 Flower 。

一旦安裝完畢,我們可以從項目目錄運行以下命令加載 Flower :

celery -A myshop.celery_task:app flower

筆者注:

原文命令為:

celery -A myshop flower

修改原因與上面相同。

在瀏覽器中打開 http://localhost:5555/dashboard ,你將看到激活的 celery 和異步任務:

CH7_14.png

可以從 http://flower.readthedocs.io/en/latest/ 找到 Flower 文檔。

總結


本章,我們創建了一個簡單的商店應用。創建了商品目錄并使用 sessions 創建了購物車,實現了自定義內容處理器保證模板可以獲得內容并創建表單來下單。此外,還學習了使用 Celery 加載異步任務。

下一章,我們將學習在商店中集成支付網關,在 admin 網站添加自定義動作,輸出 CSV 數據和動態生成 PDF 文件。

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

推薦閱讀更多精彩內容