找网站建设客户,百度搜索风云榜小说,a5网站诊断,1688淘宝货源一件代发第七章 创建电商网站在上一章里#xff0c;创建了用户关注系统和行为流应用#xff0c;还学习了使用Django的信号功能与使用Redis数据库存储图片浏览次数和排名。这一章将学习如何创建一个基础的电商网站。本章将学习创建商品品类目录#xff0c;通过session实现购物车功能。…第七章 创建电商网站在上一章里创建了用户关注系统和行为流应用还学习了使用Django的信号功能与使用Redis数据库存储图片浏览次数和排名。这一章将学习如何创建一个基础的电商网站。本章将学习创建商品品类目录通过session实现购物车功能。还将学习创建自定义上下文管理器和使用Celery执行异步任务。本章的要点有创建商品品类目录使用session创建购物车管理客户订单使用Celery异步向用户发送邮件通知1创建电商网站项目我们要创建一个电商网站项目。用户能够浏览商品品类目录然后将具体商品加入购物车最后还可以通过购物车生成订单。本章电商网站的如下功能创建商品品类模型并加入管理后台创建视图展示商品品类创建购物车系统用户浏览网站的时购物车中一直保存着用户的商品创建提交订单的页面订单提交成功后异步发送邮件给用户打开系统命令行窗口为新项目配置一个新的虚拟环境并激活Copymkdirenv
virtualenv env/myshop
sourceenv/myshop/bin/activate然后在虚拟环境中安装DjangoCopypip install Django2.0.5新创建一个项目叫做myshop之后创建新应用叫shopCopydjango-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 modelsclassCategory(models.Model):name models.CharField(max_length200, db_indexTrue)slug models.SlugField(max_length200, db_indexTrue, uniqueTrue)classMeta:ordering (name,)verbose_name categoryverbose_name_plural categoriesdef__str__(self):return self.nameclassProduct(models.Model):category models.ForeignKey(Category, related_namecategory, on_deletemodels.CASCADE)name models.CharField(max_length200, db_indexTrue)slug models.SlugField(max_length200, db_indexTrue)image models.ImageField(upload_toproducts/%Y/%m/%d, blankTrue)description models.TextField(blankTrue)price models.DecimalField(max_digits10, decimal_places2)available models.BooleanField(defaultTrue)created models.DateTimeField(auto_now_addTrue)updated models.DateTimeField(auto_nowTrue)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_places2设置小数位数。availble布尔值表示商品是否可用可以用于切换该商品是否可以购买。created记录商品对象创建的时间。updated记录商品对象最后更新的时间。这里需要特别说明的是price字段使用DecimalField而不是FloatField以避免小数尾差。凡是涉及到金额相关的数值使用DecimalField字段。FloatField的后台使用Python的float类型而DecimalField字段后台使用Python的Decimal类可以避免出现浮点数的尾差。在Product模型的Meta类中使用index_together设置id和slug字段建立联合索引这样在同时使用两个字段的索引时会提高效率。由于使用了ImageField还需要安装Pillow库Copypip install Pillow5.1.0之后执行数据迁移程序创建数据表。1.2将模型注册到管理后台将我们的模型都添加到管理后台中编辑shop应用的admin.py文件Copyfrom django.contrib import admin
from .models import Category, Productadmin.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, Productdefproduct_list(request, category_slugNone):category Nonecategories Category.objects.all()products Product.objects.filter(availableTrue)if category_slug:category get_object_or_404(categories, slugcategory_slug)products products.filter(categorycategory)return render(request, shop/product/list.html,{category: category, categories: categories, products: products})这个视图逻辑较简单使用了availableTrue筛选所有可用的商品。设置了一个可选的category_slug参数用于选出特定的品类。还需要一个展示单个商品详情的视图继续编辑views.py文件Copydefproduct_detail(request, id, slug):product get_object_or_404(Product, idid, slugslug, availbableTrue)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 viewsapp_name shopurlpatterns [path(, views.product_list, nameproduct_list),path(slug:category_slug/, views.product_list, nameproduct_list_by_category),path(int:id/slug:slug/, views.product_detail, nameproduct_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, includeurlpatterns [path(admin/, admin.site.urls),path(, include(shop.urls, namespaceshop)),
]我们为shop应用配置了名为shop的二级路由。由于URL中有参数就需要配置URL反向解析编辑shop应用的models.py文件导入reverse()函数然后为Category和Product模型编写get_absolute_url()方法Copyfrom django.urls import reverseclassCategory(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.htmlproduct/list.htmldetail.html像以前的项目一样base.html是母版让其他的模板继承母版。编辑base.htmlCopy{% load static %}
!DOCTYPE htmlhtmlheadmetacharsetutf-8/title{% block title %}My shop{% endblock %}/titlelinkhref{% static css/base.css %} relstylesheet/headbodydividheaderahref/classlogoMy shop/a/divdividsubheaderdivclasscartYour cart is empty./div/divdividcontent{% block content %}{% endblock %}/div/body/html这是这个项目的母版。其中使用的CSS文件可以从随书源代码中复制到shop应用的static/目录下。然后编辑shop/product/list.htmlCopy{% extends shop/base.html %}
{% load static %}
{% block title %}{% if category %}{{ category.name }}{% else %}Products{% endif %}
{% endblock %}
{% block content %}dividsidebarh3Categories/h3ulli {% ifnotcategory %}classselected{% endif %}ahref{% url shop:product_list %}All/a/li{% for c in categories %}li {% ifcategory.slug c.slug %}classselected{% endif %}ahref{{ c.get_absolute_url }}{{ c.name }}/a/li{% endfor %}/ul/divdividmainclassproduct-listh1{% if category %}{{ category.name }}{% else %}Products{% endif %}/h1{% for product in products %}divclassitemahref{{ product.get_absolute_url }}imgsrc{% if product.image %}{{ product.image.url }}{% else %}{% static img/no_image.png %}{% endif %}/aahref{{ product.get_absolute_url }}{{ product.name }}/abr${{ 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_rootsettings.MEDIA_ROOT)注意仅在开发阶段才能如此设置。在生产环境中不能使用Django提供静态文件。使用管理后台增加一些商品然后打开http://127.0.0.1:8000/可以看到如下页面如果没有给商品上传图片则会显示no_image.png如下图然后编写商品详情页shop/product/detail.htmlCopy{% extends shop/base.html %}
{% load static %}
{% block title %}{{ product.name }}
{% endblock %}
{% block content %}divclassproduct-detailimgsrc{% if product.image %}{{ product.image.url }}{% else %}{% static img/no_image.png %}{% endif %}h1{{ product.name }}/h1h2ahref{{ product.category.get_absolute_url }}{{ product.category }}/a/h2pclassprice${{ 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 sessionssession数据存放于数据库中为默认设置即将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_AGEsession过期时间为秒数默认为1209600秒即两个星期。SESSION_COOKIE_DOMAIN默认为None设置为某个域名可以启用跨域cookie。SESSION_COOKIE_SECURE布尔值默认为False表示是否只允许HTTPS连接下使用sessionSESSION_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里都使用统一的这个名称。然后新建一个应用来管理购物车启动系统命令行并创建新应用cartCopypython 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 ProductclassCart:def__init__(self):初始化购物车对象self.session request.sessioncart 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, quantity1, update_quantityFalse):向购物车中增加商品或者更新购物车中的数量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] quantityelse:self.cart[product_id][quantity] quantityself.save()defsave(self):# 设置session.modified的值为True中间件在看到这个属性的时候就会保存sessionself.session.modified Trueadd()方法接受以下参数product要向购物车内添加或更新的product对象quantity商品数量为整数默认值为1update_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__inproduct_ids)cart self.cart.copy()for product in products:cart[str(product.id)][product] productfor 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 formsPRODUCT_QUANTITY_CHOICES [(i, str(i)) for i inrange(1, 21)]classCartAddProductForm(forms.Form):quantity forms.TypedChoiceField(choicesPRODUCT_QUANTITY_CHOICES, coerceint)update forms.BooleanField(requiredFalse, initialFalse, widgetforms.HiddenInput)使用该表单添加商品到购物车这个CartAddProductForm表单包含如下两个字段quantity限制用户选择的数量为1-20个。使用TypedChoiceField字段并且设置coerceint将输入转换为整型字段。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 CartAddProductFormrequire_POSTdefcart_add(request, product_id):cart Cart(request)product get_object_or_404(Product, idproduct_id)form CartAddProductForm(request.POST)if form.is_valid():cd form.cleaned_datacart.add(productproduct, quantitycd[quantity], update_quantitycd[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, idproduct_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.pyCopyfrom django.urls import path
from . import viewsapp_name cart
urlpatterns [path(, views.cart_detail, namecart_detail),path(add/int:product_id/, views.cart_add, namecart_add),path(remove/int:product_id/, views.cart_remove, namecart_remove),
]然后编辑项目的根urls.py配置URLCopyurlpatterns [path(admin/, admin.site.urls),path(cart/, include(cart.urls, namespacecart)),path(, include(shop.urls, namespaceshop)),
]注意这一条路由需要增加在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 %}h1Your shopping cart/h1tableclasscarttheadtrthImage/ththProduct/ththQuantity/ththRemove/ththUnit price/ththPrice/th/tr/theadtbody{% for item in cart %}{% with productitem.product %}trtdahref{{ product.get_absolute_url }}imgsrc{% if product.image %}{{ product.image.url }}{% else %}{% static img/no_image.png %}{% endif %}alt/a/tdtd{{ product.name }}/tdtd{{ item.quantity }}/tdtdahref{% url cart:cart_remove product.id %}Remove/a/tdtdclassnum${{ item.price }}/tdtdclassnum${{ item.total_price }}/td/tr{% endwith %}{% endfor %}trclasstotaltdtotal/tdtdcolspan4/tdtdclassnum${{ cart.get_total_price }}/td/tr/tbody/tablepclasstext-rightahref{% url shop:product_list %}classbutton lightContinue shopping/aahref#classbuttonCheckout/a/p
{% endblock %}这是展示购物车详情的模板包含了一个表格用于展示具体商品。用户可以通过表单修改之中的数量并将其发送至cart_add视图。还提供了一个删除链接供用户删除商品。2.5.3添加商品至购物车需要修改商品详情页增加一个Add to Cart按钮。编辑shop应用的views.py文件把CartAddProductForm添加到product_detail视图中Copyfrom cart.forms import CartAddProductFormdefproduct_detail(request, id, slug):product get_object_or_404(Product, idid, slugslug, availableTrue)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模板在展示商品价格之后添加如下内容Copypclassprice${{ product.price }}/pformaction{% url cart:cart_add product.id %}methodpost{{ cart_product_form }}{% csrf_token %}inputtypesubmitvalueAdd 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模板找到下边这行Copytd{{ item.quantity }}/td将其替换成Copytdformaction{% url cart:cart_add product.id %}methodpost{{ item.update_quantity_form.quantity }}{{ item.update_quantity_form.update }}inputtypesubmitvalueUpdate{% 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找到下面这部分Copydivclasscart
Your cart is empty.
/div将其修改成Copydivclasscart{% with total_itemscart|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 ProductclassOrder(models.Model):first_name models.CharField(max_length50)last_name models.CharField(max_length50)email models.EmailField()address models.CharField(max_length250)postal_code models.CharField(max_length20)city models.CharField(max_length100)created models.DateTimeField(auto_now_addTrue)updated models.DateTimeField(auto_nowTrue)paid models.BooleanField(defaultFalse)classMeta:ordering (-created,)def__str__(self):returnOrder {}.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_nameitems, on_deletemodels.CASCADE)product models.ForeignKey(Product, related_nameorder_items, on_deletemodels.CASCADE)price models.DecimalField(max_digits10, decimal_places2)quantity models.PositiveIntegerField(default1)def__str__(self):return{}.format(self.id)defget_cost(self):return self.price * self.quantityOrder模型包含一些存储用户基础信息的字段以及一个是否支付的布尔字段paid。稍后将在支付系统中使用该字段区分订单是否已经付款。还定义了一个获得总金额的方法get_total_cost()通过该方法可以获得当前订单的总金额。OrderItem存储了生成订单时候的价格和数量。然后定义了一个get_cost()方法返回当前商品的总价。之后执行数据迁移过程不再赘述。3.2将订单模型加入管理后台编辑orders应用的admin.py文件Copyfrom django.contrib import admin
from .models import Order, OrderItemclassOrderItemInline(admin.TabularInline):model OrderItemraw_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 OrderclassOrderCreateForm(forms.ModelForm):classMeta:model Orderfields [first_name, last_name, email, address, postal_code, city]采用内置的模型表单创建对应order对象的表单现在要建立视图来控制表单编辑orders应用中的views.pyCopyfrom django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from cart.cart import Cartdeforder_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(orderorder, productitem[product], priceitem[price],quantityitem[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 viewsapp_name ordersurlpatterns [path(create/, views.order_create, nameorder_create),
]
配置好了order_create视图的路由再配置myshop项目的根urls.py文件在shop.urls之前增加下边这条Copy path(orders/,include(orders.urls, namespaceorders)),编辑购物车详情页cart/detail.html找到下边这行Copyahref#classbuttonCheckout/a将这个结账按钮的链接修改为order_create视图的URLCopyahref{% url orders:order_create %}classbuttonCheckout/a用户现在可以通过购物车详情页来提交订单我们要为订单页制作模板在orders应用下建立如下文件和目录结构Copytemplates/orders/order/create.htmlcreated.html编辑确认订单的页面orders/order/create.html添加如下代码Copy{% extends shop/base.html %}{% block title %}
Checkout
{% endblock %}{% block content %}h1Checkout/h1divclassorder-infoh3Your order/h3ul{% for item in cart %}li{{ item.quantity }} x {{ item.product.name }}span${{ item.total_price }}/span/li{% endfor %}/ulpTotal: ${{ cart.get_total_price }}/p/divformaction.methodpostclassorder-formnovalidate{{ form.as_p }}pinputtypesubmitvaluePlace order/p{% csrf_token %}/form
{% endblock %}这个模板展示购物车内的商品和总价之后提供空白表单用于提交订单。再来编辑订单提交成功后跳转到的页面orders/order/created.htmlCopy{% extends shop/base.html %}{% block title %}
Thank you
{% endblock %}{% block content %}h1Thank you/h1pYour order has been successfully completed. Your order number is strong{{ order.id }}/strong./p
{% endblock %}这是订单成功页面。启动站点添加一些商品到购物车中然后在购物车详情页面中点击CHECKOUT按钮之后可以看到如下页面填写表单然后点击Place order按钮订单被创建然后重定向至创建成功页面现在可以到管理后台去看一看相关的信息了。4使用Celery启动异步任务在一个视图内执行的所有操作都会影响到响应时间。很多情况下尤其视图中有一些非常耗时或者可能会失败需要重试的操作我们希望尽快给用户先返回一个响应而不是等到执行结束而让服务器去继续异步执行这些任务。例如很多视频分享网站允许用户上传视频在上传成功之后服务器需花费一定时间转码这个时候会先返回一个响应告知用户视频已经成功上传正在进行转码然后异步进行转码。还一个例子是向用户发送邮件。如果站点中有一个视图的操作是发送邮件SMTP连接很可能失败或者速度比较慢这个时候采用异步的方式就能有效的避免阻塞。Celery是一个分布式任务队列采取异步的方式同时执行大量的操作支持实施操作和计划任务可以方便的批量创建异步任务并且执行也可以设定为计划执行。Celery的文档在http://docs.celeryproject.org/en/latest/index.html。4.1安装Celery通过pip安装CeleryCopypip install celery4.1.0Celery需要一个消息代理程序来处理外部的请求这个代理把要处理的请求发送到Celery worker也就是实际处理任务的模块。所以还需要安装一个消息代理程序4.2安装RabbitMQCelery的消息代理程序有很多选择Redis数据库也可以作为Celery的消息代理程序。这里我们使用RabbitMQ因为它是Celery官方推荐的消息代理程序。如果是Linux系统通过如下命令安装RabbitMQCopyapt-get install rabbitmq如果使用macOS X或者Windows可以在https://www.rabbitmq.com/download.html下载RabbitMQ。安装之后使用下列命令启动RabbitMQ服务Copyrabbitmq-server之后会看到CopyStarting broker... completed with 10 plugins.就说明RabbitMQ已经就绪等待接受消息。译者注Windows下安装RabbitMQ必须先安装Erlong OPT平台然后安装从官网下载回来的RabbitMQ windows installer。之后需要手工把Erlong安装目录下的bin目录和RabbitMQ安装目录下的sbin目录设置到PATH中。之后安装参见这里。4.3在项目中集成Celery需要为项目使用的Celery实例进行一些配置在settings.py文件的相同目录下创建celery.py文件Copyimport os
from celery import Celery# 为celery程序设置环境为当前项目的环境
os.environ.setdefault(DJANGO_SETTINGS_MODULE, myshop.settings)app Celery(myshop)app.config_from_object(django.conf:settings, namespaceCELERY)
app.autodiscover_tasks()这段程序解释如下导入DJANGO_SETTINGS_MODULE环境变量为Celery命令行程序创造运行环境。实例化一个app对象是一个Celery程序实例调用config_from_object()方法从我们项目的设置文件中读取环境设置。namespace属性指定了在我们的settings.py文件中所有和Celery相关的配置都以CELERY开头例如CELERY_BROKER_URL。调用autodiscover_tasks()让Celery自动发现所有的异步任务。Celery会在每个INSTALLED_APPS中列出的应用中寻找task.py文件在里边寻找定义好的异步任务然后执行。还需要在项目的__init__.py文件中导入celery模块以让项目启动时Celery就运行编辑myshop/__inti__.pyCopy# import celeryfrom .celery import app as celery_app现在就可以为应用启动异步任务了。CELERY_ALWAYS_EAGER设置可以让Celery在本地以同步的方式直接执行任务而不会去把任务加到队列中。这常用来进行测试或者检查Celery的配置是否正确。4.4为应用添加异步任务我们准备在用户提交订单的时候异步发送邮件。一般的做法是在应用目录下建立一个task模块专门用于编写异步任务在orders应用下建立task.py文件添加如下代码Copyfrom celery import task
from django.core.mail import send_mail
from .models import Ordertaskdeforder_created(order_id):当一个订单创建完成后发送邮件通知给用户order Order.objects.get(idorder_id)subject Order {}.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, lee0709vip.sina.com, [order.email])print(mail_sent, type(mail_sent))return mail_sent将order_created函数通过装饰器task定义为异步任务可以看到只要用task装饰就可以把一个函数变成Celery异步任务。这里我们给异步函数传入order_id推荐仅传入ID让异步任务启动的时候再去检索数据库。最后拼接好标题和正文后使用send_mail()发送邮件。在第二章已经学习过如何发送邮件如果没有SMTP服务器在settings.py里将邮件配置为打印到控制台上CopyEMAIL_BACKEND django.core.mail.backends.console.EmailBackend在实际应用中除了耗时比较大的功能之外还可以将其他容易失败需要重试的功能即使耗时较短也推荐设置为异步任务。设置好了异步任务之后还需要修改原来的视图order_created以便在订单完成的时候调用order_created异步函数。编辑orders应用的views.py文件Copyfrom .task import order_createddeforder_create(request):#......if request.method POST:#......if form.is_valid():#......cart.clear()# 启动异步任务order_created.delay(order.id)#......调用delay()方法即表示异步执行该任务任务会被加入队列然后交给执行程序执行。启动另外一个shell必须是导入了当前环境的命令行窗口比如Pycharm中启动的terminal使用如下命令启动Celery workerCopycelery -A myshop worker -l info现在Celery worker已经启动并且准备处理任务。启动站点然后添加一些商品到购物车提交订单。在启动了Celery worker的窗口应该能看到类似下边的输出Copy[2017-12-17 17:43:11,462: INFO/MainProcess] Received task:
orders.tasks.order_created[e990ddae-2e30-4e36-b0e4-78bbd4f2738e]
[2017-12-17 17:43:11,685: INFO/ForkPoolWorker-4] Task
orders.tasks.order_created[e990ddae-2e30-4e36-b0e4-78bbd4f2738e] succeeded in
0.22019841300789267s: 1表示任务已经被执行应该可以收到邮件了。译者注Windows平台下在发送邮件的时候有可能出现错误信息如下Copynot enough values to unpack (expected 3, got 0)这是因为Celery 4.x 在win10版本下运行存在问题解决方案为先安装Python的eventlet模块Copypip install eventlet然后在启动Celery worker的时候加上参数 -P eventlet命令行如下Copycelery -A myshop worker -l info -P eventlet即可解决该错误。在linux下应该不会发生该错误。参考Celery项目在 Github 上的问题Unable to run tasks under Windows #40814.5监控Celery如果想要监控异步任务的执行情况可以安装Python的FLower模块Copypip install flower0.9.2之后在新的终端窗口输入Copycelery -A myshop flower之后在浏览器中打开http://localhost:5555/dashboard即可看到图形化监控的Celery情况可以在https://flower.readthedocs.io/查看Flower的文档。总结这一章里创建了一个基础的电商网站。为网站创建了商品品类和详情展示通过session创建了购物车应用。实现了一个自定义的上下文处理器用于将购物车对象附加到所有模板上还实现了创建订单的功能。最后还学习了使用Celery启动异步任务。在下一章将学习集成支付网关为管理后台增加自定义操作将数据导出为CSV格式以及动态的生成PDF文件。如有不懂还要咨询下方小卡片博主也希望和志同道合的测试人员一起学习进步在适当的年龄选择适当的岗位尽量去发挥好自己的优势。我的自动化测试开发之路一路走来都离不每个阶段的计划因为自己喜欢规划和总结测试开发视频教程、学习笔记领取传送门