HTTP本身是一个无状态的协议,本文讨论并实践几种跟踪状态的机制。
                 爱校码
HTTP本身是一个无状态的协议,每一个到达服务器的请求消息,都是独立于之前的请求,当服务器给出响应消息后,本次请求-响应的互动已经结束,不再发生关联。如果需要某个特定的状态,则要求将状态信息保存在客户端,通过一种机制将其与客户端发送的请求绑定在一起,服务器依之可判断状态;或者将状态信息保存在服务器端,服务器创建一个会话ID(会话设置期限),服务器将ID响应给客户端,在后续的客户端请求中绑定该ID,服务器亦可依之判断状态,以下讨论几种跟踪状态的机制。
URL (统一资源定位)重写 是一种会话跟踪方法,它将一个或多个令牌 标记 放置在 URL请求字符串的参数当中,令牌标记的一般格式为:
url?param1=value1¶m2=value2 ... ¶mn=valuen
每个参数的键-值对作为一个令牌标记,多个 令牌标记之间通过&符号隔开,并组成一个字符串与 URL 之间要用一个?符号隔开。
由于URL地址栏字符串的长度限制,网址重写适应于既需要保持,又不跨越太多页面的信息。 同时,令牌标记的值(value )包含特殊字符(空格、 符号、 问号或中文字符串)时必须进行编码。
设计一个Web 服务程序,展示广州和北京这两座城市中十大最受欢迎的旅游景点,展示两页信息,第一页显示所选城市前五个景点,第二页显示所选城市的后五个景点。要求服务器程序利用 URL 重写跟踪所选城市和页码 。
首先借助project_name模板利用startproject创建一个称为scenic_spot的新项目:
django-admin startproject scenic_spot --template=project_name
这里会生成一个scenic_spot/scenic_spot.py的目录和文件,如果正确地使用了项目模板,scenic_spot.py应该像下面这样:
import os
import sys
from django.conf import settings
DEBUG = os.environ.get('DEBUG', 'on') == 'on'
SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-%9buc((od1yvzxsq(_yyry^&3pwxid9&9cqb-m8t-r=9vk4%s+')
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')
settings.configure(
    DEBUG=DEBUG,
    SECRET_KEY=SECRET_KEY,
    ALLOWED_HOSTS=ALLOWED_HOSTS,
    ROOT_URLCONF=__name__,
    MIDDLEWARE=(
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ),
)
from django.urls import re_path
from django.core.wsgi import get_wsgi_application
from django.http import HttpResponse
def index(request):
    return HttpResponse('Hello World')
urlpatterns = (
    re_path(r'^$', index),
)
application = get_wsgi_application()
if __name__ == "__main__":
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)
SECRET_KEY设置会因为由于startproject的命令而随机生成的。在完成初始创建后,就可以开始编写视图和创建响应该视图所对应的页面了。
创建一个init视图方法用于建立初始化数据:
guangzhouAttractions = [] 
beijingAttractions = []
def init():
    guangzhouAttractions.append("广州白云山")
    guangzhouAttractions.append("中山纪念堂")
    guangzhouAttractions.append("广东科学中心")
    guangzhouAttractions.append("长隆旅游度假区")
    guangzhouAttractions.append("陈家祠")
    guangzhouAttractions.append("黄花岗七十二烈士陵园")
    guangzhouAttractions.append("华南植物园")
    guangzhouAttractions.append("越秀公园")
    guangzhouAttractions.append("碧水湾")
    guangzhouAttractions.append("香江野生动物世界")
    beijingAttractions.append("故宫")
    beijingAttractions.append("十三陵")
    beijingAttractions.append("北京欢乐谷")
    beijingAttractions.append("颐和园")
    beijingAttractions.append("鸟巢")
    beijingAttractions.append("恭王府")
    beijingAttractions.append("八达岭长城")
    beijingAttractions.append("雍和宫")
    beijingAttractions.append("什刹海")
    beijingAttractions.append("三里屯酒吧街")
之后,在视图顶层调用该视图函数,完成初始化任务:
init()
接下来,编写index函数,用于判断请求参数,代码如下:
def index(request):
    city = request.GET.get("city")
    if city != None and (city == "guangzhou" or city == "beijing"):
        return HttpResponse(showAttractions(request, city))
    else:
        return HttpResponse(showMainPage(request))
其中,showMainPage函数方法用于显示选择城市链接的主页面:
def showMainPage(request):
    html_str = "<html><head><title>十大旅游景点</title></head><body>"
    html_str += "请选择城市:"
    html_str += "<br/><a href='?city=guangzhou'>广州</a>"
    html_str += "<br/><a href='?city=beijing'>北京</a>"
    html_str += "</body></html>"
    return html_str
另外,showAttractions函数方法用于显示城市十大景点的分页页面:
def showAttractions(request, city):
    attractions = []
    if city == "beijing":
       attractions = beijingAttractions
    elif city == "guangzhou":
       attractions = guangzhouAttractions
    page = 1
    pageParameter = request.GET.get("page")
    if pageParameter != None:
       page = int(pageParameter)
       if page > 2:
          page = 1
    html_str = "<html><head><title>十大旅游景点</title><head><body>"
    html_str += "<a href='/'>选择城市</a>"
    if city == "beijing":
        html_str += "<hr/>当前所在城市:北京 页 " + str(page) + "<hr/>"
    elif city == "guangzhou":
        html_str += "<hr/>当前所在城市:广州 页 " + str(page) + "<hr/>"
    start = page * 5 - 5
    for i in range(start,start + 5):
         html_str += attractions[i] +"<br/>"
    html_str += "<hr style='color:blue'/>"
    html_str += "<a href='?city=" + city + "&page=1'>页 1</a>"
    html_str += " <a href='?city=" + city + "&page=2'>页 2</a>"
    html_str += "</body></html>"
    return html_str
URL模式
URL 调度器 和 URLconf (URL配置) 是 Django 应用中的基础部分。Django 需要一个 urls.py 配置文件作为起点, 这个特殊的 urls.py 被称为根 URL配置,由变量URLconf来设置, 其在 settings.py文件中定义。而urls.py文件与settting.py文件都位于由startproject命令参数所创建的blogs(之前的项目案例)文件夹内。URLconf在settting.py中的设置如下:
ROOT_URLCONF = 'blogs.urls'
它已经配置好了,因此无需在此处更改任何内容。但在当前的应用中,已经将URL模式放在了模板文件内,勿需配置ROOT_URLCONF。
在模板生成文件的urlpatterns配置项中,通过re_path函数来匹配,re_path函数的刨析如下:
def re_path(regex, view, kwargs=None, name=None):
    # ...
基本URL 的创建非常简单。 这只是匹配字符串的问题。URL路由的更高级用法是通过利用正则表达式匹配某些类型的数据并创建动态 URL 来实现的。本例中的URL配置如下:
urlpatterns = (
    re_path(r'^$', index, name='index'),
    re_path(r'^/(?P<city>[a-z]+)&(?P<page>\d+)/$', index, name='index'),       
)
在正则表达式中,通过?P语法来捕获被命名的参数,并用[a-z]来匹配任意小写字母,使用\d+将匹配任意大小的整数。
当 Django 接受一个请求(request), 它就会在URL模式中寻找匹配项。从 urlpatterns 变量的第一条开始,然后在每个re_path中去匹配请求的 URL。如果 Django 找到匹配项,它会将请求传递给视图函数,这是re_path的第二个参数。 urlpatterns 中的顺序很重要,因为 Django 将在找到匹配项后立即停止搜索。 现在,如果 Django 在urlpatterns中没有找到匹配项,它将引发 404 异常,这是 Page Not Found 的错误代码。
在scenic_spot目录下执行本案例:
python scenic_spot.py runserver
在浏览器中访问:http://localhost:8000
运行结果如图所示:

   
   
如果会话跟踪的信息需要跨越若干多次请求的页面,每一个页面的信息需要管理。Cookie 用于存储 Web 服务器发送给客户端的小段文本信息,自动地在浏览器和 Web 服务程序之间来回传递,适用于需要跨越许多页面的信息。 作为会话标识符,默认情况下,Cookie 不存储在文件中,它们存储在浏览器的内存中。如果关闭浏览器,将无法找到会话信息。Cookie 具有以下特性:
set_cookie方法进行设置,利用HttpRequest对象参数request.COOKIES['键名']来获取cookie。爱校码网站中决定推广其博文宣传,在网店的设计中,创建一个个性化的用户点击计数器,对在一月内登录访问该网站五次以上的用户,将有礼物派送,当用户第五次登录访问该网站时显示消息:礼物将会派送,请用户耐心等待。假定每位用户总是用同一台计算机访问网站,不会有两个用户用同一台计算机访问站点。
步骤一: 利用模板创建gift项目
django-admin startproject gift --template=project_name
步骤二: 编写网站计数器视图函数代码
def index(request):
    cookieFound = False
    cookies = request.COOKIES
    html_str = '<html><head><title>框架技术 · 爱校码</title></head>'
    html_str += '<body bgcolor=\"orange\">'
    html_str += '<center><h2>框架技术</h2></center>'
    html_str += '<hr><center>'
    if cookies != None:
        if 'logincount' in request.COOKIES.keys():
            cookieFound = True
    if cookieFound == True:
        temp = int(request.COOKIES['logincount']) 
        temp += 1
        if temp == 5:
            html_str += '恭喜您!!!!!,一个礼物将寄送给您,请耐心等待。'
        html_str += '您访问本网站的次数是 :'+ str(temp)+'次'
        html_str += '</center></body></html>'
        res = HttpResponse(html_str)
        res.set_cookie('logincount',str(temp),max_age=60*60*24)
        return res 
    else:
        temp = 1
        html_str += '您好,这是您第一次访问本网站'  
        html_str += '</center></body></html>'  
        res  = HttpResponse(html_str)
        res.set_cookie('logincount',str(temp),max_age=60*60*24)
        return res
步骤三: 运行项目
python gift.py runserver
在浏览器中查看:http://localhost:8000/ 运行结果如图:

利用 html 表单 form 中的 input 标签内 type 属性为 hidden,达到隐藏表单字段的目的, 并用来跟踪会话信息。它是指在用户页面中包含几个隐藏的字段,这些字段的值在提交请求时发送给服务器。对于接受请求的 Web 服务程序而言,接受的值是来自普通字段还是隐藏字段没有什么差别。大多数主流浏览器都支持隐藏的表单字段,它主要针对没有注册或没有登录的客户使用。该项会话跟踪方法仅适合当页面中包含 html form表单,并在连续动态生成的表单中使用。
隐藏表单跟踪方法胜过网址(URL)重写跟踪方法的地方在于,使用 http post 请求可以避免提交信息长度的限制,并且不需要进行字符编码。
会员管理是网站信息服务的一项基本功能,首先需要列出会员信息,然后选择某个会员进行编辑维护。因此,需要设计两项内容,其一为会员信息模型类 Member,其二为会员信息维护的视图控制函数。
步骤一: 利用模板创建member项目:
django-admin startproject member --template=project_name
进入到member目录,可以看到已经创建了一个member.py文件。
步骤二: 利用Django的startapp命令参数创建一个myme应用app:
在当前member的目录下,执行命令如下:
django-admin startapp myme
当命令执行以后,已在member路径下创建了应用myme目录,为了体现本案例的轻量级,剔除myme路径下的暂时不需要的文件。同时为了将URL模式配置独立出来,分别在member路径与myme路径下各增加一个urls.py文件。最终的文件结构如下:
member/
    | -- myme/
    |        | -- migrations/
    |        |         + -- __init__.py
    |        | -- __init__.py
    |        | -- apps.py
    |        | -- models.py
    |        | -- views.py
    |        + -- urls.py
    | -- member.py
    + -- urls.py
步骤三: 添加app配置信息与数据库配置信息,更改ROOT_URLCONF配置信息以及进行相关urls.py文件配置:
settings.configure(
    ...
    INSTALLED_APPS = [
         'myme',
    ],
    MIDDLEWARE=(
        'django.middleware.common.CommonMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ),
    ROOT_URLCONF = 'urls',
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': 'db.sqlite3',
        }
    },
)
在本案例的表单设计中,重点在于隐藏表单域的设计,暂时不考虑csrf_token的设计问题,在以上的MIDDLEWARE的中间件的配置中,先去除'corsheaders.middleware.CorsMiddleware' 的中间件配置项。
数据库配置这里使用了SQLite,因为它很方便,不需要安装其他任何东西。SQLite是一个产品级数据库,SQLite被许多公司用于成千上万的产品,如所有Android和iOS设备、主流的Web浏览器、Windows 10、MacOS等。
ROOT_URLCONF配置指向了当前路径的urls文件,同时清理member文件中的urlpatterns及所对应的视图函数index。打开当前路径下的urls文件,完成配置信息如下:
from django.urls import path, include
urlpatterns = [
     path('myme/',include(('myme.urls','myme'),namespace='myme')),
]
在该配置文件中,包含了(include)myme路径下的urls文件,其配置信息如下:
from django.urls import re_path
from . import views
urlpatterns = [   
    re_path(r'^init$', views.init,name='init'),
    re_path(r'^member$', views.member,name='member'),
    re_path(r'^editMember$', views.editMember,name='editMember'),
    re_path(r'^updateMember$', views.updateMember,name='updateMember'),
]
在myme路径下的urls文件中,from . import views其中的圆点表示与该文件处于同一个当前路径下。在配置文件的urlpatterns模式中,指向了views的四个视图函数init、member、editMember、updateMember,需要后续完成代码编写。
步骤四: 编写会员信息模型类 Member:
在这里,需要将模型类 Member映射到数据库,使用Django内置的ORM,为此,切换到myme目录路径下,打开models.py文件进行会员信息模型类 Member的设计,编写代码如下:
from django.db import models
class Member(models.Model):
    name = models.CharField(max_length=30, unique=True)
    city = models.CharField(max_length=50)
步骤五: 迁移模型
让Django创建数据库,以便可以开始使用之,运行以下命令:
python member.py makemigrations
python member.py migrate
好了!数据库已经可以使用了。
步骤六: 编写视图控制方法
打开myme路径下的view.py文件,编写视图方法:
from django.shortcuts import  get_object_or_404, redirect
from .models import Member
from django.http import HttpResponse
def init(request):
     member1 = Member()
     member1.name = '张三'
     member1.city = '广州'
     member2 = Member()
     member2.name = '李四'
     member2.city = '北京'
     member1.save()
     member2.save()
     return HttpResponse('初始化数据完成!')
def member(request):
     members = Member.objects.all()
     html_str = "<html><head><title>会员信息</title><head><body><h2>会员信息</h2>"
     html_str += "<u>"
     for item in members:
         html_str += '<li>'+item.name+'('+item.city+')(<a href="editMember?id='+str(item.id)+'">编辑</a>)'
     html_str += "</u>"
     html_str += "</body></html>"
     return HttpResponse(html_str)
def editMember(request):
     id = request.GET['id']
     member = get_object_or_404(Member, pk=id)
     if member != None:
         html_str = '<html><head><title>编辑会员信息</title><head><body>'
         html_str += '<h2>编辑会员信息</h2>'
         html_str += '<form method="POST" action="/myme/updateMember">'
         html_str += '<input type="hidden" name="id" value="'+id+'"/>'
         html_str += '<table><tr><td>姓名:</td><td>'
         html_str += '<input name="name" value="'+member.name+'"/></td></tr>'
         html_str += '<tr><td>城市:</td><td>'
         html_str += '<input name="city" value="'+member.city+'"/></td></tr>'
         html_str += '<tr><td colspan="2" style="text-align:right">'
         html_str += '<input type="submit" value="更新"/></td></tr>'
         html_str += '<tr><td colspan="2"><a href="member">会员列表</a>' 
         html_str += '</td></tr></table></form></body></html>'
         return HttpResponse(html_str)
     else:
         html_str = '无会员信息!' 
         return HttpResponse(html_str)
def updateMember(request): 
     if request.method == 'POST':
         id = request.POST['id']
         name = request.POST['name']
         city = request.POST['city']
         member = get_object_or_404(Member, pk=id)
         member.name = name
         member.city = city
         member.save()
         return redirect('/myme/member')
     else:
         return HttpResponse('更新不成功!')
在member方法中,使用了URL重写跟踪id : <a href="editMember?id='+str(item.id)+'">;而在editMember方法中,使用了隐藏表单跟踪id: <input type="hidden" name="id" value="'+id+'"/>。
步骤七: 功能验证
运行项目
python member.py runserver
在浏览器中查看:http://localhost:8000/ , URL模式的相对路径可以同时映射为/myme/member、/myme/editMember和/myme/updateMember,而/myme/init用于初始化数据。
首先,Web服务器启动后,调用初始化视图方法函数 init(),在该方法中, 针对模型Member类的两个实例对象 member1和member2添加初始化数据,并将其提交入数据库。
然后,从/member映射作为入口开始,调用视图控制方法函数member(),在方法内对模型Member类调用其所有实例对象,并构建HTML页面,响应输出会员列表信息,如图所示:

在会员列表信息中,每个会员都有一个[编辑]的超链接,锚点a标签的href属性值包含了/editMember?id=memberid的链接信息,点击此链接时将访问调用视图控制方法函数editMember(),在方法中,依据id从数据库中获取到一个模型Member类的实例对象,并构建HTML页面,发送一个会员信息编辑表单,如图所示:

在 editMember()方法构建会员信息表单的过程中,在其中包含了一项内容:
<input type="hidden" name="id" value="'+id+'"/>
这就是表单中的隐藏域,它包含了会员对象的 id,但不显示在页面上,当点击[更新]按钮提交表单的编辑信息时,隐藏域的 id 参数也同时包装到请求对象中传送到视图控制方法中,由于表单中的 method 设置为 post,将在视图控制方法函数updateMember()内判断request.method == 'POST'。在该方法中,先提取表单隐藏域的id参数值,以此值为参数调用Member类的数据库实例对象,然后再提取表单中的 name 和 city 参数值,以此值修改会员对象的 name 和 city 属性,提交到数据库完成修改,最后调用/myme/member映射输出修改后的会员列表信息。
该子任务的功能验证了利用表单隐藏域跟踪会员 id 的方法。
Django使用的Session默认都继承于SessionBase类,该类实现了一些session对象的操作方法,以及hash,decode,encode等方法。Django 是支持匿名会话的。会话框架允许基于每个站点访问者存储和检索任意数据。它在服务器端存储数据并提供cookie的发送和接收。Cookie包含会话ID - 而不是数据本身。
会话通过配置一个中间件实现的。为了打开会话,需要做下面的操作:
传统的django的请求-响应视图模式:
http请求->view->http响应
而加入中间件后,框架的视图模型则变为:
http请求 -> 中间件处理 -> view -> 中间件处理 -> http响应
SessionMiddleware对应的中间件处理分别对应process_request和process_response两个钩子函数方法。它们将会在特定的时候被触发。
class SessionMiddleware(object):
    def __init__(self):
        engine = import_module(settings.SESSION_ENGINE)
        self.SessionStore = engine.SessionStore
    def process_request(self, request):
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        request.session = self.SessionStore(session_key)
    def process_response(self, request, response):
        try:
            accessed = request.session.accessed
            modified = request.session.modified
            empty = request.session.is_empty()
        except AttributeError:
            pass
        else:
            ...
        return response
在发出请求后,SessionMiddleware的process_request方法取出session_key,并把一个新的session对象赋给request.session,而在返回响应时,process_response方法则判断session是否被修改或过期,来更新session的信息。
默认情况下,Django 在数据库里存储会话(使用 django.contrib.sessions.models.Session )。虽然这很方便,但在一些设置里,在其他地方存储会话数据速度更快,因此 Django 可以在文件系统或缓存中配置存储会话数据。
使用数据库支持的会话:
如果想使用数据库支持的会话,需要在 INSTALLED_APPS 里添加 'django.contrib.sessions' 的内置应用app。如果保存在数据库中,django会在数据库中创建一个如下的会话表:
CREATE TABLE "django_session" (
    "session_key" varchar(40) NOT NULL PRIMARY KEY,
    "session_data" text NOT NULL,
    "expire_date" datetime NOT NULL
);
session_key是放置在cookie中的id,它是唯一的,而session_data则存放序列化后的session数据字符串。通过session_key可以在数据库中取得这条session的信息。
使用缓存会话:
为了得到更好的性能,你可以使用基于缓存的会话后端。有两种办法在缓存中存储数据:
这两中会话存储会非常快,但简单缓存会更快。
使用基于文件的会话:
要使用基于文件的会话,需要设置 SESSION_ENGINE 为 "django.contrib.sessions.backends.file" 。
使用基于cookie的会话:
要使用基于cookies的会话,需要设置 SESSION_ENGINE 为 "django.contrib.sessions.backends.signed_cookies" 。这个会话数据将使用 Django 的加密工具( cryptographic signing ) 和 SECRET_KEY 工具进行保存。
爱校铺网站中的网店商品展示,允许用户将商品添加到购物车中,并查看购物,在这里进行了模拟实现,通过设计 Product模型商品类、CartItem 购物车内容项类以及 CartView 购物车视图控制类实施购物车功能应用。
步骤一: 利用模板创建cart项目:
 django-admin startproject cart --template=project_name
进入到cart目录,可以看到已经创建了一个cart.py文件。
步骤二: 利用Django的startapp命令参数创建一个myapp应用app:
在当前cart的目录下,执行命令如下:
django-admin startapp myapp
剔除myapp路径下的暂时不需要的文件。分别在cart路径与myapp路径下各增加一个urls.py文件。最终的文件结构如下:
cart/
    | -- myapp/
    |        | -- migrations/
    |        |         + -- __init__.py
    |        | -- __init__.py
    |        | -- apps.py
    |        | -- models.py
    |        | -- views.py
    |        + -- urls.py
    | -- cart.py
    + -- urls.py
步骤三: 添加app配置信息与数据库配置信息,更改ROOT_URLCONF配置信息以及进行相关urls.py文件配置:
这里的添加的app,除了myapp外,还有'django.contrib.sessions',中间件部分还要添加'django.contrib.sessions.middleware.SessionMiddleware': 
settings.configure(
    ...
    INSTALLED_APPS = [
         'django.contrib.sessions',
         'myapp',
    ],
    MIDDLEWARE=(
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ),
    ROOT_URLCONF = 'urls',
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': 'db.sqlite3',
        }
    },
    SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db',
)
本案例不用csrf_token,去除配置项'django.middleware.csrf.CsrfViewMiddleware'。本案例新增session会话管理,添加配置项'django.contrib.sessions.middleware.SessionMiddleware'。
ROOT_URLCONF配置指向了当前路径的urls文件,同时清理cart文件中的urlpatterns及所对应的视图函数index。另外,为了使用缓存会话,将设置 SESSION_ENGINE 为 "django.contrib.sessions.backends.cached_db"。
cart路径下的urls文件,完成配置信息如下:
from django.urls import path, include
urlpatterns = (
    path('myapp/',include(('myapp.urls','myapp'),namespace='myapp')),
)
myapp路径下的urls文件,完成配置信息如下:
from django.urls import re_path
from . import views
urlpatterns = (   
    re_path(r'^newproduct$', views.new_product,name='newproduct'),
    re_path(r'^products$', views.ProductsView.as_view(),name='products'),
    re_path(r'^detail$', views.ProductView.as_view(),name='detail'),
    re_path(r'^addproduct$', views.ProductView.as_view(),name='addproduct'),
    re_path(r'^show$', views.CartView.as_view(),name='show'),
    re_path(r'^addcart$', views.CartView.as_view(),name='addcart'),
)
步骤四: 编写商品模型类 Product,以及购物车内容项类 CartItem
from django.db import models
class Product(models.Model):
    name = models.CharField(max_length=30, unique=True)
    description = models.CharField(max_length=50)
    price = models.DecimalField( max_digits=8, decimal_places=2)
    def to_json(self):
        json_product = {
            'id': self.id,
            'name': self.name,
            'description': self.description,
            'price': float(self.price)
        }
        return json_product
class CartItem:
    def __init__(self, product, quantity):
        self.__product = product
        self.__quantity = quantity
    @property
    def product(self):
        return self.__product
    @product.setter
    def product(self, aProduct):
        self.__product = aProduct
    @product.deleter
    def product(self):
        del self.__product
    @property
    def quantity(self):
        return self.__quantity
    @quantity.setter
    def quantity(self, aQuantity):
        self.__quantity = aQuantity
    @quantity.deleter
    def quantity(self):
        del self.__quantity
    def to_json(self):
        json_cartitem = {
            'product': self.__product.to_json(),
            'quantity': self.__quantity 
        }
        return json_cartitem
步骤五: 迁移模型
python  cart.py makemigrations
python cart.py migrate
步骤六: 编写视图控制类
from django.shortcuts import get_object_or_404, redirect
from django.views import View
from .models import Product, CartItem
from django.http import HttpResponse
import json
def new_product(request):
    html_str = '<html><head><title>添加商品信息</title><head><body><h2>添加商品信息</h2>'
    html_str += '<form method="POST" action="/myapp/addproduct">' 
    html_str += '<table><tr><td>商品名: </td>'
    html_str += '<td><input name="name" type="text" /></td></tr>'
    html_str += '<tr><td>商品描述: </td>'
    html_str += '<td><input name="description" type="text" /></td></tr>'
    html_str += '<tr><td>商品价格: </td>'
    html_str += '<td><input name="price" type="text" /></td></tr>'
    html_str += '<tr><td colspan="2" style="text-align:right">'
    html_str += '<input type="submit" value="提交"/></td></tr>'
    html_str += '</table></form></body></html>'
    return HttpResponse(html_str)
class ProductsView(View):
    def get(self, request):
        products = Product.objects.all()
        html_str = '<html><head><title>商品列表</title><head><body><h2>商品列表</h2>'
        html_str += '<u>'
        for item in products:
           html_str += '<li>'+item.name+'('+str(item.price)+')(<a href="detail?id='+str(item.id)+'">商品明细</a>)</li>'
        html_str += '</u>'
        html_str += '<a href="/myapp/show">查看购物车</a>'
        html_str += '</body></html>'
        return HttpResponse(html_str)
class ProductView(View): 
    def get(self, request):
        id = request.GET.get("id") 
        product = get_object_or_404(Product, pk=id)
        if product != None:
           html_str = '<html><head><title>商品明细信息</title><head><body><h2>商品明细信息</h2>'
           html_str += '<form method="POST" action="/myapp/addcart">'
           html_str += '<input type="hidden" name="id" value="'+id+'"/>'
           html_str += '<table><tr><td>商品名: </td>'
           html_str += '<td>'+product.name+'</td></tr>'
           html_str += '<tr><td>商品描述: </td>'
           html_str += '<td>'+product.description+'</td></tr>'
           html_str += '<tr><td>数量: </td><td><input name="quantity" /></td></tr>'
           html_str += '<tr><td colspan="2" style="text-align:right">'
           html_str += '<input type="submit" value="立即购买"/></td></tr>'
           html_str += '<tr><td colspan="2">'
           html_str += '<a href = "/myapp/products"> 商品列表</a>'
           html_str += '</td></tr></table></form></body></html>'
           return HttpResponse(html_str) 
        else:
           html_str = '无商品信息!' 
           return HttpResponse(html_str) 
    def post(self,request):
         name = request.POST.get('name')
         description = request.POST.get('description')
         price = request.POST.get('price')
         product = Product()
         product.name = name
         product.description = description
         product.price = price
         product.save()
         return redirect('/myapp/products')
class CartView(View):
    def get(self,request):
        if request.session.get('cart'):
            cart = json.loads(request.session.get('cart'))
            html_str = '<html><head><title>购物车</title><head>'
            html_str += '<body><a href = "/myapp/products"> 商品列表</a>'
            html_str += '<table><tr><td style="width:150px">数量</td>'
            html_str += '<td style="width:150px">商品</td>'
            html_str += '<td style="width:150px">价格</td>'
            html_str += '<td style="width:150px">小计</td></tr>'
            total = 0
            for item in cart:
                product = item['product']
                quantity = item['quantity']
                if quantity!=0:
                    price = product['price']
                    summary = quantity*price
                    html_str += '<tr><td>'+str(quantity)+'</td>'
                    html_str += '<td>'+product["name"]+'</td>'
                    html_str += '<td>'+str(price)+'</td>'
                    html_str += '<td>'+str(summary)+'</td></tr>'
                    total = total + float(summary)
           html_str += '<tr><td colspan="4" style="text-align:left">'
           html_str += '总计:'+str(total)+'</td></tr>'
           html_str += '</table></body></html>'
           return HttpResponse(html_str) 
        else:
            html_str = '无购物车信息!' 
            return HttpResponse(html_str)
    def post(self, request):
        quantity = int(request.POST.get('quantity'))
        id = request.POST.get('id')
        product = get_object_or_404(Product, pk=id)
        if product != None and quantity>0:
           cartItem = CartItem(product,quantity)
           cartItem.product = product
           cartItem.quantity = quantity  
           cart = []  
           if request.session.get('cart'):
              cart = json.loads(request.session.get('cart'))
           cart.append(cartItem.to_json()) 
           request.session['cart'] = json.dumps(cart)
           return redirect('/myapp/products')
        else:
           return HttpResponse("不满足添加购物车条件!")
步骤七: 功能验证
在urlpatterns配置中,URL模式的相对路径可以同时映射为/myapp/newproduct、/myapp/products、/myapp/detail、 /myapp/addproduct、/myapp/show和/myapp/addcart。分别调用视图函数new_product;视图类ProductsView的get方法;视图类ProductView的get方法与post方法;以及视图类CartView的get方法与post方法。
当通过/myapp/newproduct映射访问视图函数/myapp/newproduct时,生成填加新商品的表单页面,填写商品数据后,提交访问ProductView的post方法,完成新商品输入任务。再通过/myapp/products映射访问视图类ProductsView的get方法,建立商品列表的页面 html发送到浏览器,展示所有商品,如图所示:

页面中针对每一项商品,包含了一个[商品明细]的超链接,锚点a标签的href属性值包含了/myapp/detail?id=id的链接信息。点击此链接时将访问视图类ProductView的get方法,该方法创建输出被选商品的明细信息,如图所示:

在明细页面中,包含了一个超链接[商品列表]和一个按钮[立即购买]。点击超链接将回到初始/myapp/products映射。当添加商品到购物车时,在数量输入框内填写数字值,并单击 [立即购买]按钮,将提交商品明细信息表单,调用视图类CartView的post方法,提取表单中的商品id参数,取出数据库中对应的商品对象,以该对象和提交表单中的数量参数quantity,创建一个新的购物车内容项CartItem类的对象:
cartItem = CartItem(product,quantity)
然后,获取当前的会话对象request.session,查看会话对象中是否存在键名为cart的列表对象(购物车)。若不存在,则创建一个新的列表对象,将其以键名为cart设置会话对象的属性值。而列表对象的元素项即序列化后(CartItem对象转为json)购物车内容项,可以继续添加到列表对象中:
    cart = []  
    if request.session.get('cart'):
          cart = json.loads(request.session.get('cart'))
    cart.append(cartItem.to_json()) 
    request.session['cart'] = json.dumps(cart)
当客户点击图中所示的[查看购物车]超链接时,将切换到视图类CartView的get方法。在其中由会话对象获取购物车列表对象:
cart = json.loads(request.session.get('cart'))
然后遍历该购物车,取出每一内容项展示所购商品数量、价格、小计和总计。如图所示:

该子任务的功能验证了利用request.session类型的会话对象跟踪购物车列表对象的方法。
博文最后更新时间: