Django by Example:在线商店e-shop项目(上)

原作Antonio Melé,简书翻译https://www.jianshu.com/p/4a45e4988835。

本章我们将学习如何创建基本的在线商店,创建商品目录和使用 Django sessions 将商品放入购物车。我们还将学习如何创建自定义内容处理器和使用 Celery 加载异步任务。

本章将包含以下内容:

  • 创建产品目录

  • 使用 Django session 创建购物车


创建一个在线商店项目


我们将创建一个在线商店项目。我们的用户将能够通过商品目录浏览商品并将商品放入购物车。最后,检查购物车并下单。本章将包括在线商店的以下功能:

  • 创建商品目录模型,将其添加到 admin网站,并且创建展示商品目录的基本视图;

  • 使用 Django session 创建购物车系统帮助用户浏览网站时保存选择的商品;

  • 创建表单和下单功能;

  • 用户下单成功后为用户发送同步邮件。

首先,打开 teminal 并使用以下命令来为新项目创建虚拟环境并激活:

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

在虚拟环境中使用以下命令安装 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 中。

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网站的商品更改列表页面看起来是这样的:

创建产品目录视图


为了展示产品目录,我们需要创建一个视图列出所有产品或者通过给定类别对产品进行过滤。编辑 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 应用目录下创建下面的目录和文件结构:

我们需要定义基础模板,然后在产品列表和详情模板中对其进行扩展。编辑 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/,点击任意产品来查看产品详情。看起来是这样的:

创建购物车


创建完商品目录之后,下一步是创建保存用户选择的商品的购物车。购物车帮助用户选择想要的商品并在用户浏览网站时暂时保存选中的商品直到用户下单。购物车应该放在 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_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/ 并点击某个商品到商品详情页面。现在页面在添加到购物车前面有一个选择数量的选项。页面看起来是这样的:

选择数量并点击 Add to cart 按钮。表单通过 POST 方法提交到 cart_add 视图。视图将商品(包括商品的价格和选择的数量)添加到 session 的购物车中。然后,重定向到购物车详情页面,看起来是这样的:

在购物车中更新产品数量

用户查看购物车时,他们在下单之前可能需要更改产品数量。下面将实现购物车详细页面更改商品数量的功能。

编辑 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/,可以看到每个商品都包含编辑数量的表单:

更改某个商品的数量,并点击 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/并在购物车中添加一些商品,在页面的头部,你将看到商品的总数和总价格:

下章我们将介绍订单管理和使用Celery异步发送邮件。

更多阅读

Django实战: Python爬取链家上海二手房信息,存入数据库并在前端显示

Django实战教程: 开发企业级应用智能文档管理系统smartdoc(1)

Django实战教程: 开发餐厅在线点评网站(1)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值