创建电商网站

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添加购物车的功能,购物车按照如下方式工作:

  1. 当需要创建一个购物车的时候,先检查session中是否存在自定义的购物车键,如果存在说明当前用户已经使用了购物车,如果不存在,就新建一个购物车键。

  1. 对于接下来的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创建客户订单视图和模板

在用户提交订单的时候,我们需要用刚创建的订单模型来保存用户当时购物车内的信息。创建一个新的订单的步骤如下:

  1. 提供一个表单供用户填写

  1. 根据用户填写的内容生成一个新Order类实例,然后将购物车中的商品放入OrderItem实例中并与Order实例建立外键关系

  1. 清理全部购物车内容,然后重定向用户到一个操作成功页面。

首先利用内置表单功能建立订单表单,在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 电子邮件后端

创建电商网站

创建电商网站

django是前端还是后端

Django框架项目-电商web(未前后分离)

Django电商网站--登录功能