创建电商网站
Posted 一个处女座的测试
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了创建电商网站相关的知识,希望对你有一定的参考价值。
第七章 创建电商网站
在上一章里,创建了用户关注系统和行为流应用,还学习了使用Django的信号功能与使用Redis数据库存储图片浏览次数和排名。这一章将学习如何创建一个基础的电商网站。本章将学习创建商品品类目录,通过session实现购物车功能。还将学习创建自定义上下文管理器和使用Celery执行异步任务。
本章的要点有:
创建商品品类目录
使用session创建购物车
管理客户订单
使用Celery异步向用户发送邮件通知
1创建电商网站项目
我们要创建一个电商网站项目。用户能够浏览商品品类目录,然后将具体商品加入购物车,最后还可以通过购物车生成订单。本章电商网站的如下功能:
创建商品品类模型并加入管理后台,创建视图展示商品品类
创建购物车系统,用户浏览网站的时购物车中一直保存着用户的商品
创建提交订单的页面
订单提交成功后异步发送邮件给用户
打开系统命令行窗口,为新项目配置一个新的虚拟环境并激活:
Copymkdirenv
virtualenv env/myshop
sourceenv/myshop/bin/activate
然后在虚拟环境中安装Django:
Copypip install Django==2.0.5
新创建一个项目叫做myshop,之后创建新应用叫shop:
Copydjango-admin startproject myshop
cd myshop/
django-admin startapp shop
编辑settings.py文件,激活shop应用:
CopyINSTALLED_APPS = [
# ...'shop.apps.ShopConfig',
]
现在应用已经激活,下一步是设计数据模型。
1.1创建商品品类模型
我们的商品品类模型包含一系列商品大类,每个商品大类中包含一系列商品。每一个商品都有一个名称,可选的描述,可选的图片,价格和是否可用属性。编辑shop应用的models.py文件:
Copyfrom django.db import models
classCategory(models.Model):
name = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(max_length=200, db_index=True, unique=True)
classMeta:
ordering = ('name',)
verbose_name = 'category'
verbose_name_plural = 'categories'def__str__(self):
return self.name
classProduct(models.Model):
category = models.ForeignKey(Category, related_name='category', on_delete=models.CASCADE)
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)
available = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
classMeta:
ordering = ('name',)
index_together = (('id', 'slug'),)
def__str__(self):
return self.name
这是我们的Category和Product模型。Category包含name字段和设置为不可重复的slug字段(unique同时也意味着创建索引)。Product模型的字段如下:
category:关联到Category模型的外键。这是一个多对一关系,一个商品必定属于一个品类,一个品类包含多个商品。
name:商品名称。
slug:商品简称,用于创建规范化URL。
image:可选的商品图片。
description:可选的商品图片。
price:该字段使用了Python的decimal.Decimal类,用于存储商品的金额,通过max_digits设置总位数,decimal_places=2设置小数位数。
availble:布尔值,表示商品是否可用,可以用于切换该商品是否可以购买。
created:记录商品对象创建的时间。
updated:记录商品对象最后更新的时间。
这里需要特别说明的是price字段,使用DecimalField,而不是FloatField,以避免小数尾差。
凡是涉及到金额相关的数值,使用DecimalField字段。FloatField的后台使用Python的float类型,而DecimalField字段后台使用Python的Decimal类,可以避免出现浮点数的尾差。
在Product模型的Meta类中,使用index_together设置id和slug字段建立联合索引,这样在同时使用两个字段的索引时会提高效率。
由于使用了ImageField,还需要安装Pillow库:
Copypip install Pillow==5.1.0
之后执行数据迁移程序,创建数据表。
1.2将模型注册到管理后台
将我们的模型都添加到管理后台中,编辑shop应用的admin.py文件:
Copyfrom django.contrib import admin
from .models import Category, Product
@admin.register(Category)classCategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = 'slug': ('name',)
@admin.register(Product)classProductAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'price', 'available', 'created', 'updated']
list_filter = ['available', 'created', 'updated']
list_editable = ['price', 'available']
prepopulated_fields = 'slug': ('name',)
我们使用了prepopulated_fields用于让slug字段通过name字段自动生成,在之前的项目中可以看到这么做很简便。在ProductAdmin中使用list_editable设置了可以编辑的字段,这样可以一次性编辑多行而不用点开每一个对象。注意所有在list_editable中的字段必须出现在list_display中。
之后创建超级用户。打开http://127.0.0.1:8000/admin/shop/product/add/,使用管理后台添加一个新的商品品类和该品类中的一些商品,页面如下:
译者注:这里图片上有一个stock字段,这是上一版的程序使用的字段。在本书内程序已经修改,但图片依然使用了上一版的图片。本项目中后续并没有使用stock字段。
1.3创建商品品类视图
为了展示商品,我们创建一个视图,用于列出所有商品,或者根据品类显示某一品类商品,编辑shop应用的views.py文件:
Copyfrom django.shortcuts import render, get_object_or_404
from .models import Category, Product
defproduct_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(categories, 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参数用于选出特定的品类。
还需要一个展示单个商品详情的视图,继续编辑views.py文件:
Copydefproduct_detail(request, id, slug):
product = get_object_or_404(Product, id=id, slug=slug, availbable=True)
return render(request, 'shop/product/detail.html', 'product': product)
product_detail视图需要id和slug两个参数来获取商品对象。只通过ID可以获得商品对象,因为ID是唯一的,这里增加了slug字段是为了对搜索引擎优化。
在创建了上述视图之后,需要为其配置URL,在shop应用内创建urls.py文件并添加如下内容:
Copyfrom django.urls import path
from . import views
app_name = 'shop'
urlpatterns = [
path('', views.product_list, name='product_list'),
path('<slug:category_slug>/', views.product_list, name='product_list_by_category'),
path('<int:id>/<slug:slug>/', views.product_detail, name='product_detail'),
]
我们为product_list视图定义了两个不同的URL,一个名称是product_list,不带任何参数,表示展示全部品类的全部商品;一个名称是product_list_by_category,带参数,用于显示指定品类的商品。还为product_detail视图配置了传入id和slug参数的URL。
这里要解释的就是product_list视图带一个默认值参数,所以默认路径进来后就是展示全部品类的页面。加上了具体某个品类,就展示那个品类的商品。详情页的URL使用id和slug来进行参数传递。
还需要编写项目的一级路由,编辑myshop项目的根urls.py文件:
Copyfrom django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('shop.urls', namespace='shop')),
]
我们为shop应用配置了名为shop的二级路由。
由于URL中有参数,就需要配置URL反向解析,编辑shop应用的models.py文件,导入reverse()函数,然后为Category和Product模型编写get_absolute_url()方法:
Copyfrom django.urls import reverse
classCategory(models.Model):
# ......defget_absolute_url(self):
return reverse('shop:product_list_by_category',args=[self.slug])
classProduct(models.Model):
# ......defget_absolute_url(self):
return reverse('shop:product_detail',args=[self.id,self.slug])
这样就为模型的对象配置好了用于反向解析URL的方法,我们已经知道,get_absolute_url()是很好的获取具体对象规范化URL的方法。
1.4创建商品品类模板
现在需要创建模板,在shop应用下建立如下目录和文件结构:
Copytemplates/
shop/
base.html
product/
list.html
detail.html
像以前的项目一样,base.html是母版,让其他的模板继承母版。编辑base.html:
Copy% load static %
<!DOCTYPE html><html><head><metacharset="utf-8"/><title>% block title %My shop% endblock %</title><linkhref="% static "css/base.css" %" rel="stylesheet"></head><body><divid="header"><ahref="/"class="logo">My shop</a></div><divid="subheader"><divclass="cart">Your cart is empty.</div></div><divid="content">
% block content %
% endblock %
</div></body></html>
这是这个项目的母版。其中使用的CSS文件可以从随书源代码中复制到shop应用的static/目录下。
然后编辑shop/product/list.html:
Copy% extends "shop/base.html" %
% load static %
% block title %
% if category % category.name % else %Products% endif %
% endblock %
% block content %
<divid="sidebar"><h3>Categories</h3><ul><li % ifnotcategory %class="selected"% endif %><ahref="% url "shop:product_list" %">All</a></li>
% for c in categories %
<li % ifcategory.slug == c.slug %class="selected"
% endif %><ahref=" c.get_absolute_url "> c.name </a></li>
% endfor %
</ul></div><divid="main"class="product-list"><h1>% if category % category.name % else %Products
% endif %</h1>
% for product in products %
<divclass="item"><ahref=" product.get_absolute_url "><imgsrc="
% if product.image % product.image.url % else %% static "img/no_image.png" %% endif %"></a><ahref=" product.get_absolute_url "> product.name </a><br>
$ product.price
</div>
% endfor %
</div>
% endblock %
这是展示商品列表的模板,继承了base.html,使用categories变量在侧边栏显示品类的列表,在页面主体部分通过products变量展示商品清单。展示所有商品和具体某一类商品都采用这个模板。如果Product对象的image字段为空,我们显示一张默认的图片,可以在随书源码中找到img/no_image.png,将其拷贝到对应的目录。
由于使用了Imagefield,还需要对媒体文件进行一些设置,编辑settings.py文件加入下列内容:
CopyMEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL是保存用户上传的媒体文件的目录,MEDIA_ROOT是存放媒体文件的目录,通过BASE_DIR变量动态建立该目录。
为了让Django提供静态文件服务,还必须修改shop应用的urls.py文件:
Copyfrom 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/,可以看到如下页面:
如果没有给商品上传图片,则会显示no_image.png,如下图:
然后编写商品详情页shop/product/detail.html:
Copy% extends "shop/base.html" %
% load static %
% block title %
product.name
% endblock %
% block content %
<divclass="product-detail"><imgsrc="% if product.image % product.image.url % else %
% static "img/no_image.png" %% endif %"><h1> product.name </h1><h2><ahref=" product.category.get_absolute_url "> product.category </a></h2><pclass="price">$ product.price </p>
product.description|linebreaks
</div>
% endblock %
在模板中调用get_absolute_url()方法用于展示对应类的商品,打开http://127.0.0.1:8000/,然后点击任意一个商品,详情页如下:
现在已经将商品品类和展示功能创建完毕。
2创建购物车功能
在建立商品品类之后,下一步是创建一个购物车,让用户可以将指定的商品及数量加入购物车,而且在浏览整个网站并且下订单之前,购物车都会维持其中的信息。为此,我们需要将购物车数据存储在当前用户的session中。
由于session通用翻译成会话,而在本章中很多时候session指的是Django的session模块或者session对象,所以不再进行翻译。
我们将使用Django的session框架来存储购物车数据。直到用户生成订单,商品信息都存储在购session中,为此我们还需要为购物车和其中的商品创建一个模型。
2.1使用Django的session模块
Django 提供了一个session模块,用于进行匿名或登录用户会话,可以为每个用户保存独立的数据。session数据存储在服务端,通过在cookie中包含session ID就可以获取到session,除非将session存储在cookie中。session中间件管理具体的cookie信息,默认的session引擎将session保存在数据库内,也可以切换不同的session引擎。
要使用session,需要在settings.py文件的MIDDLEWARE设置中启用'django.contrib.sessions.middleware.SessionMiddleware',这个管理session中间件在使用startproject命令创建项目时默认已经被启用。
这个中间件在request对象中设置了session属性用于访问session数据,类似于一个字典一样,可以存储任何可以被序列化为JSON的Python数据类型。可以像这样存入数据:
Copyrequest.session['foo'] = 'bar'
获取键对应的值:
Copyrequest.session.get('foo')
删除一个键值对:
Copydel request.session['foo']
可以将request.session当成字典来操作。
当用户登录到一个网站的时候,服务器会创建一个新的用于登录用户的session信息替代原来的匿名用户session信息,这意味着原session信息会丢失。如果想保存原session信息,需要在登录的时候将原session信息存为一个新的session数据。
2.2session设置
Django中可以配置session模块的一些参数,其中最重要的是SESSION_ENGINE设置,即设置session数据具体存储在何处。默认情况下,Django通过django.contrib.session应用的Session模型,将session数据保存在数据库中的django_session数据表中。
Django提供了如下几种存储session数据的方法:
Database sessions:session数据存放于数据库中,为默认设置,即将session数据存放到settings.py中的DATABASES设置中的数据库内。
File-based sessions:保存在一个具体的文件中
Cached sessions:基于缓存的session存储,使用Django的缓存系统,可以通过CACHES设置缓存后端。这种情况下效率最高。
Cached database sessions:先存到缓存再持久化到数据库中。取数据时如果缓存内无数据,再从数据库中取。
Cookie-based sessions:基于cookie的方式,session数据存放在cookie中。
为了提高性能,使用基于缓存的session是好的选择。Django直接支持基于Memcached的缓存和如Redis的第三方缓存后端。
还有其他一系列的session设置,以下是一些主要的设置:
SESSION_COOKIE_AGE:session过期时间,为秒数,默认为1209600秒,即两个星期。
SESSION_COOKIE_DOMAIN:默认为None,设置为某个域名可以启用跨域cookie。
SESSION_COOKIE_SECURE:布尔值,默认为False,表示是否只允许HTTPS连接下使用session
SESSION_EXPIRE_AT_BROWSER_CLOSE:布尔值,默认为False,表示是否一旦浏览器关闭,session就失效
SESSION_SAVE_EVERY_REQUEST:布尔值,默认为False,设置为True表示每次HTTP请求都会更新session,其中的过期时间相关设置也会一起更新。
可以在https://docs.djangoproject.com/en/2.0/ref/settings/#sessions查看所有的session设置和默认值。
2.3session过期
特别需要提的是SESSION_EXPIRE_AT_BROWSER_CLOSE设置。该设置默认为False,此时session有效时间采用SESSION_COOKIE_AGE中的设置。
如果将SESSION_EXPIRE_AT_BROWSER_CLOSE设置为True,则session在浏览器关闭后就失效,SESSION_COOKIE_AGE设置不起作用。
还可以使用request.session.set_expiry()方法设置过期时间。
2.4在session中存储购物车数据
我们需要创建一个简单的数据结构,可以被JSON序列化,用于存放购物车数据。购物车中必须包含如下内容:
Product对象的ID
商品的数量
商品的单位价格
由于商品的价格会变化,我们在将商品加入购物车的同时存储当时商品的价格,如果商品价格之后再变动,也不进行处理。
现在需要实现创建购物车和为session添加购物车的功能,购物车按照如下方式工作:
当需要创建一个购物车的时候,先检查session中是否存在自定义的购物车键,如果存在说明当前用户已经使用了购物车,如果不存在,就新建一个购物车键。
对于接下来的HTTP请求,都要重复第一步,并且从购物车中保存的商品ID到数据库中取得对应的Product对象数据。
编辑settings.py里新增一行:
CopyCART_SESSION_ID = 'cart'
这就是我们的购物车键名称,由于session对于每个用户都通过中间件管理,所以可以在所有用户的session里都使用统一的这个名称。
然后新建一个应用来管理购物车,启动系统命令行并创建新应用cart:
Copypython manage.py startapp cart
然后在settings.py中激活该应用:
CopyINSTALLED_APPS = [
# ...'shop.apps.ShopConfig',
'cart.apps.CartConfig',
]
在cart应用中创建cart.py,添加如下代码:
Copyfrom decimal import Decimal
from django.conf import settings
from shop.models import Product
classCart:
def__init__(self):
"""
初始化购物车对象
"""
self.session = request.session
cart = self.session.get(settings.CART_SESSION_ID)
ifnot cart:
# 向session中存入空白购物车数据
cart = self.session[settings.CART_SESSION_ID] =
self.cart =cart
这是我们用于管理购物车的Cart类,使用request对象进行初始化,使用self.session = request.session让类中的其他方法可以访问session数据。首先,使用self.session.get(settings.CART_SESSION_ID)尝试获取购物车对象。如果不存在购物车对象,通过为购物车键设置一个空白字段对象从而新建一个购物车对象。我们将使用商品ID作为字典中的键,其值又是一个由数量和价格构成的字典,这样可以保证不会重复生成同一个商品的购物车数据,也简化了取出购物车数据的方式。
创建将商品添加到购物车和更新数量的方法,为Cart类添加add()和save()方法:
CopyclassCart:
# ......defadd(self, product, quantity=1, update_quantity=False):
"""
向购物车中增加商品或者更新购物车中的数量
"""
product_id = str(product.id)
if product_id notin 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()
defsave(self):
# 设置session.modified的值为True,中间件在看到这个属性的时候,就会保存session
self.session.modified = True
add()方法接受以下参数:
product:要向购物车内添加或更新的product对象
quantity:商品数量,为整数,默认值为1
update_quantity:布尔值,为True表示要将商品数量更新为quantity参数的值,为False表示将当前数量增加quantity参数的值。
我们把商品的ID转换成字符串形式然后作为购物车中商品键名,这是因为Django使用JSON序列化session数据,而JSON只允许字符串作为键名。商品价格也被从decimal类型转换为字符串,同样是为了序列化。最后,使用save()方法把购物车数据保存进session。
save()方法中修改了session.modified = True,中间件通过这个判断session已经改变然后存储session数据。
我们还需要从购物车中删除商品的方法,为Cart类添加以下方法:
CopyclassCart:
# ......defremove(self, product):
"""
从购物车中删除商品
"""
product_id = str(product.id)
if product_id in self.cart:
del self.cart[product_id]
self.save()
remove()根据id从购物车中移除对应的商品,然后调用save()方法保存session数据。
为了使用方便,我们会需要遍历购物车内的所有商品,用于展示等操作。为此需要在Cart类内定义__iter()__方法,生成迭代器,供将for循环使用。
CopyclassCart:
# ......def__iter__(self):
"""
遍历所有购物车中的商品并从数据库中取得商品对象
"""
product_ids = self.cart.keys()
# 获取购物车内的所有商品对象
products = Product.objects.filter(id__in=product_ids)
cart = self.cart.copy()
for product in products:
cart[str(product.id)]['product'] = product
for item in cart.values():
item['price'] = Decimal(item['price'])
item['total_price'] = item['price'] * item['quantity']
yield item
在__iter()__方法中,获取了当前购物车中所有商品的Product对象。然后浅拷贝了一份cart购物车数据,并为其中的每个商品添加了键为product,值为商品对象的键值对。最后迭代所有的值,为把其中的价格转换为decimal类,增加一个total_price键来保存总价。这样我们就可以迭代购物车对象了。
还需要显示购物车中有几件商品。当执行len()方法的时候,Python会调用对象的__len__()方法,为Cart类添加如下的__len__()方法:
CopyclassCart:
# ......def__len__(self):
"""
购物车内一共有几种商品
"""returnsum(item['quantity'] for item in self.cart.values())
这个方法返回所有商品的数量的合计。
再编写一个计算购物车商品总价的方法:
CopyclassCart:
# ......defget_total_price(self):
returnsum(Decimal(item['price']*item['quantity']) for item in self.cart.values())
最后,再编写一个清空购物车的方法:
CopyclassCart:
# ......defclear(self):
del self.session[settings.CART_SESSION_ID]
self.save()
现在就编写完了用于管理购物车的Cart类。
译者注,原书的代码采用class Cart(object)的写法,译者将其修改为Python 3的新式类编写方法。
2.5创建购物车视图
现在我们拥有了管理购物车的Cart类,需要创建如下的视图来添加、更新和删除购物车中的商品
添加商品的视图,可以控制增加或者更新商品数量
删除商品的视图
详情视图,显示购物车中的商品和总金额等信息
2.5.1购物车相关视图
为了向购物车内增加商品,显然需要一个表单让用户选择数量并按下添加到购物车的按钮。在cart应用中创建forms.py文件并添加如下内容:
Copyfrom django import forms
PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i inrange(1, 21)]
classCartAddProductForm(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)上还是替代原有数量(True),把这个字段设置为HiddenInput,因为我们不需要用户看到这个字段。
创建向购物车中添加商品的视图,编写cart应用中的views.py文件,添加如下代码:
Copyfrom 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 .form import CartAddProductForm
@require_POSTdefcart_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取得商品对象之后验证表单。表单验证通过后,将商品添加到购物车,然后跳转到购物车详情页面对应的cart_detail URL,稍后我们会来编写cart_detail URL。
再来编写删除商品的视图,在cart应用的views.py中添加如下代码:
Copydefcart_remove(request, product_id):
cart = Cart(request)
product = get_object_or_404(Product, id=product_id)
cart.remove(product)
return redirect('cart:cart_detail')
删除商品视图同样接受商品ID作为参数,通过ID获取Product对象,删除成功之后跳转到cart_detail URL。
还需要一个展示购物车详情的视图,继续在cart应用的views.py文件中添加下列代码:
Copydefcart_detail(request):
cart = Cart(request)
return render(request, 'cart/detail.html', 'cart': cart)
cart_detail视图用来展示当前购物车中的详情。现在已经创建了添加、更新、删除及展示的视图,需要配置URL,在cart应用里新建urls.py:
Copyfrom django.urls import path
from . import views
app_name = 'cart'
urlpatterns = [
path('', views.cart_detail, name='cart_detail'),
path('add/<int:product_id>/', views.cart_add, name='cart_add'),
path('remove/<int:product_id>/', views.cart_remove, name='cart_remove'),
]
然后编辑项目的根urls.py,配置URL:
Copyurlpatterns = [
path('admin/', admin.site.urls),
path('cart/', include('cart.urls', namespace='cart')),
path('', include('shop.urls', namespace='shop')),
]
注意这一条路由需要增加在shop.urls路径之前,因为这一条比下一条的匹配路径更加严格。
2.5.2创建展示购物车的模板
cart_add和cart_remove视图并未渲染模板,而是重定向到cart_detail视图,我们需要为编写展示购物车详情的模板。
在cart应用内创建如下文件目录结构:
Copytemplates/
cart/
detail.html
编辑cart/detail.html,添加下列代码:
Copy% extends 'shop/base.html' %
% load static %
% block title %
Your shopping cart
% endblock %
% block content %
<h1>Your shopping cart</h1><tableclass="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><ahref=" product.get_absolute_url "><imgsrc="
% if product.image % product.image.url % else %% static 'img/no_image.png' %% endif %"alt=""></a></td><td> product.name </td><td> item.quantity </td><td><ahref="% url 'cart:cart_remove' product.id %">Remove</a></td><tdclass="num">$ item.price </td><tdclass="num">$ item.total_price </td></tr>
% endwith %
% endfor %
<trclass="total"><td>total</td><tdcolspan="4"></td><tdclass="num">$ cart.get_total_price </td></tr></tbody></table><pclass="text-right"><ahref="% url 'shop:product_list' %"class="button light">Continue shopping</a><ahref="#"class="button">Checkout</a></p>
% endblock %
这是展示购物车详情的模板,包含了一个表格用于展示具体商品。用户可以通过表单修改之中的数量,并将其发送至cart_add视图。还提供了一个删除链接供用户删除商品。
2.5.3添加商品至购物车
需要修改商品详情页,增加一个Add to Cart按钮。编辑shop应用的views.py文件,把CartAddProductForm添加到product_detail视图中:
Copyfrom cart.forms import CartAddProductForm
defproduct_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/templates/shop/product/detail.html模板,在展示商品价格之后添加如下内容:
Copy<pclass="price">$ product.price </p><formaction="% url 'cart:cart_add' product.id %"method="post">
cart_product_form
% csrf_token %
<inputtype="submit"value="Add to cart"></form>
product.description|linebreaks
启动站点,到http://127.0.0.1:8000/,进入任意一个商品的详情页,可以看到商品详情页内增加了按钮,如下图:
选择一个数量,然后点击Add to cart按钮,即可购物车详情界面,如下图:
2.5.4更新商品数量
当用户在浏览购物车详情时,在下订单前很可能会修改购物车的中商品的数量,我们必须允许用户在购物车详情页修改数量。
编辑cart应用中的views.py文件,修改其中的cart_detail视图:
Copydefcart_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应用的cart/detail.html模板,找到下边这行
Copy<td> item.quantity </td>
将其替换成:
Copy<td><formaction="% url 'cart:cart_add' product.id %"method="post">
item.update_quantity_form.quantity
item.update_quantity_form.update
<inputtype="submit"value="Update">
% csrf_token %
</form></td>
之后启动站点,到http://127.0.0.1:8000/cart/,可以看到如下所示:
修改数量然后点击Update按钮来测试新的功能,还可以尝试从购物车中删除商品。
2.6创建购物车上下文处理器
你可能在实际的电商网站中会注意到,购物车的详细情况一直显示在页面上方的导航部分,在购物车为空的时候显示特殊的为空的字样,如果购物车中有商品,则会显示数量或者其他内容。这种展示购物车的方法与之前编写的处理购物车的视图没有关系,因此我们可以通过创建一个上下文处理器,将购物车对象作为request对象的一个属性,而不用去管是不是通过视图操作。
2.6.1上下文处理器
Django中的上下文管理器,就是能够接受一个request请求对象作为参数,返回一个要添加到request上下文的字典的Python函数。
当默认通过startproject启动一个项目的时候,settings.py中的TEMPLATES设置中的conetext_processors部分,就是给模板附加上下文的上下文处理器,有这么几个:
django.template.context_processors.debug:这个上下文处理器附加了布尔类型的debug变量,以及sql_queries变量,表示请求中执行的SQL查询
django.template.context_processors.request:这个上下文处理器设置了request变量
django.contrib.auth.context_processors.auth:这个上下文处理器设置了user变量
django.contrib.messages.context_processors.messages:这个上下文处理器设置了messages变量,用于使用消息框架
除此之外,django还启用了django.template.context_processors.csrf来防止跨站请求攻击。这个组件没有写在settings.py里,强制启用,无法进行设置和关闭。有关所有上下文管理器的详情请参见https://docs.djangoproject.com/en/2.0/ref/templates/api/#built-in-template-context-processors。
2.6.2将购物车设置到request上下文中
现在我们就来设置一个自定义上下文处理器,以在所有模板内访问购物车对象。
在cart应用内新建一个context_processors.py文件,同视图,模板以及其他内容一样,django内的程序可以写在应用内的任何地方,但为了结构良好,将其单独写成一个文件:
Copyfrom .cart import Cart
defcart(request):
return 'cart': Cart(request)
Django规定的上下文处理器,就是一个函数,接受request请求作为参数,然后返回一个字典。这个字典的键值对被RequestContext设置为所有模板都可以使用的变量及对应的值。在我们的上下文处理器中,我们使用request对象初始化了cart对象
之后在settings.py里将我们的自定义上下文处理器加到TEMPLATES设置中:
CopyTEMPLATES = [
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'APP_DIRS': True,
'OPTIONS':
'context_processors': [
......
'cart.context_processors.cart'
],
,
,
]
定义了上下文管理器之后,只要一个模板被RequestContext渲染,上下文处理器就会被执行然后附加上变量名cart。
所有使用RequestContext的请求过程中都会执行上下文处理器。对于不是每个模板都需要的变量,一般情况下首先考虑的是使用自定义模板标签,特别是涉及到数据库查询的变量,否则会极大的影响网站的效率。
修改base.html,找到下面这部分:
Copy<divclass="cart">
Your cart is empty.
</div>
将其修改成:
Copy<divclass="cart">
% with total_items=cart|length %
% if cart|length > 0 %
Your cart:
<ahref="% url 'cart:cart_detail' %"> total_items item total_items|pluralize ,
$ cart.get_total_price
</a>
% else %
Your cart is empty.
% endif %
% endwith %
</div>
启动站点,到http://127.0.0.1:8000/,添加一些商品到购物车,在网站的标题部分可以显示出购物车的信息:
3生成客户订单
当用户准备对一个购物车内的商品进行结账的时候,需要生成一个订单数据保存到数据库中。订单必须保存用户信息和用户所购买的商品信息。
为了实现订单功能,新创建一个订单应用:
Copypython manage.py startapp orders
然后在settings.py中的INSTALLED_APPS中进行激活:
CopyINSTALLED_APPS = [
# ...'orders.apps.OrdersConfig',
]
3.1创建订单模型
我们用一个模型存储订单的详情,然后再用一个模型保存订单内的商品信息,包括价格和数量。编辑orders应用的models.py文件:
Copyfrom django.db import models
from shop.models import Product
classOrder(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)
classMeta:
ordering = ('-created',)
def__str__(self):
return'Order '.format(self.id)
defget_total_cost(self):
returnsum(item.get_cost() for item in self.items.all())
classOrderItem(models.Model):
order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
product = models.ForeignKey(Product, related_name='order_items', on_delete=models.CASCADE)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=1)
def__str__(self):
return''.format(self.id)
defget_cost(self):
return self.price * self.quantity
Order模型包含一些存储用户基础信息的字段,以及一个是否支付的布尔字段paid。稍后将在支付系统中使用该字段区分订单是否已经付款。还定义了一个获得总金额的方法get_total_cost(),通过该方法可以获得当前订单的总金额。
OrderItem存储了生成订单时候的价格和数量。然后定义了一个get_cost()方法,返回当前商品的总价。
之后执行数据迁移,过程不再赘述。
3.2将订单模型加入管理后台
编辑orders应用的admin.py文件:
Copyfrom django.contrib import admin
from .models import Order, OrderItem
classOrderItemInline(admin.TabularInline):
model = OrderItem
raw_id_fields = ['product']
@admin.register(Order)classOrderAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name', 'email',
'address', 'postal_code', 'city', 'paid',
'created', 'updated']
list_filter = ['paid', 'created', 'updated']
inlines = [OrderItemInline]
我们让OrderItem类继承了admin.TabularInline类,然后在OrderAdmin类中使用了inlines参数指定OrderItemInline,通过该设置,可以将一个模型显示在相关联的另外一个模型的编辑页面中。
启动站点到http://127.0.0.1:8000/admin/orders/order/add/,可以看到如下的页面:
3.3创建客户订单视图和模板
在用户提交订单的时候,我们需要用刚创建的订单模型来保存用户当时购物车内的信息。创建一个新的订单的步骤如下:
提供一个表单供用户填写
根据用户填写的内容生成一个新Order类实例,然后将购物车中的商品放入OrderItem实例中并与Order实例建立外键关系
清理全部购物车内容,然后重定向用户到一个操作成功页面。
首先利用内置表单功能建立订单表单,在orders应用中新建forms.py文件并添加如下代码:
Copyfrom django import forms
from .models import Order
classOrderCreateForm(forms.ModelForm):
classMeta:
model = Order
fields = ['first_name', 'last_name', 'email', 'address', 'postal_code', 'city']
采用内置的模型表单创建对应order对象的表单,现在要建立视图来控制表单,编辑orders应用中的views.py:
Copyfrom django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from cart.cart import Cart
deforder_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'])
# 成功生成OrderItem之后清除购物车
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)获取当前购物车对象;之后根据HTTP请求种类的不同,视图进行以下工作:
GET请求:初始化空白的OrderCreateForm,并且渲染orders/order/created.html页面。
POST请求:通过POST请求中的数据生成表单并且验证,验证通过之后执行order = form.save()创建新订单对象并写入数据库;然后遍历购物车的所有商品,对每一种商品创建一个OrderItem对象并存入数据库。最后清空购物车,渲染orders/order/created.html页面。
在orders应用里建立urls.py作为二级路由:
Copyfrom django.urls import path
from . import views
app_name = 'orders'
urlpatterns = [
path('create/', views.order_create, name='order_create'),
]
配置好了order_create视图的路由,再配置myshop项目的根urls.py文件,在shop.urls之前增加下边这条:
Copy path('orders/',include('orders.urls', namespace='orders')),
编辑购物车详情页cart/detail.html,找到下边这行:
Copy<ahref="#"class="button">Checkout</a>
将这个结账按钮的链接修改为order_create视图的URL:
Copy<ahref="% url 'orders:order_create' %"class="button">Checkout</a>
用户现在可以通过购物车详情页来提交订单,我们要为订单页制作模板,在orders应用下建立如下文件和目录结构:
Copytemplates/
orders/
order/
create.html
created.html
编辑确认订单的页面orders/order/create.html,添加如下代码:
Copy% extends 'shop/base.html' %
% block title %
Checkout
% endblock %
% block content %
<h1>Checkout</h1><divclass="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><formaction="."method="post"class="order-form"novalidate>
form.as_p
<p><inputtype="submit"value="Place order"></p>
% csrf_token %
</form>
% endblock %
这个模板,展示购物车内的商品和总价,之后提供空白表单用于提交订单。
再来编辑订单提交成功后跳转到的页面orders/order/created.html:
Copy% 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 %
这是订单成功页面。启动站点,添加一些商品到购物车中,然后在购物车详情页面中点击CHECKOUT按钮,之后可以看到如下页面:
填写表单然后点击Place order按钮,订单被创建,然后重定向至创建成功页面:
测试 Django 电子邮件后端