当前位置: 首页 > news >正文

深圳门户网站建设案例商业网站可以选择.org域名吗

深圳门户网站建设案例,商业网站可以选择.org域名吗,南宁网站建设南宁,公司网站换服务器怎么做第九章 扩展商店功能在上一章里#xff0c;为电商站点集成了支付功能#xff0c;然后可以生成PDF发票发送给用户。在本章#xff0c;我们将为商店添加优惠码功能。此外#xff0c;还会学习国际化和本地化的设置和建立一个推荐商品的系统。本章涵盖如下要点#xff1a;建立…第九章 扩展商店功能在上一章里为电商站点集成了支付功能然后可以生成PDF发票发送给用户。在本章我们将为商店添加优惠码功能。此外还会学习国际化和本地化的设置和建立一个推荐商品的系统。本章涵盖如下要点建立一个优惠券系统可以实现折扣功能给项目增加国际化功能使用Rosetta来管理翻译使用Django-parler翻译模型建立商品推荐系统1优惠码系统很多电商网站会向用户发送电子优惠码以便用户在购买时使用以折扣价进行结算。一个在线优惠码通常是一个字符串然后还规定了有效期限一次性有效或者可以反复使用。我们将为站点添加优惠码功能。我们的优惠码带有有效期但是不限制使用次数输入之后就会影响用户购物车中的总价。为了实现这个需求需要建立一个数据模型来存储优惠码有效期和对应的折扣比例。为myshop项目创建新的应用couponsCopypython manage.py startapp coupons然后在settings.py内激活该应用CopyINSTALLED_APPS [# ...coupons.apps.CouponsConfig, ]1.1创建优惠码数据模型编辑coupons应用的models.py文件创建一个Coupon模型Copyfrom django.db import models from django.core.validators import MinValueValidator, MaxValueValidatorclassCoupon(models.Model):code models.CharField(max_length50, uniqueTrue)valid_from models.DateTimeField()valid_to models.DateTimeField()discount models.IntegerField(validators[MinValueValidator(0), MaxValueValidator(100)])active models.BooleanField()def__str__(self):return self.code这是用来存储优惠码的模型Coupon模型包含以下字段code用于存放码的字符串valid_from优惠码有效期的开始时间。valid_to优惠码有效期的结束时间。discount该券对应的折扣是一个百分比所以取值为0-100我们使用了内置验证器控制该字段的取值范围。active表示该码是否有效之后执行数据迁移程序。然后将Coupon模型加入到管理后台编辑coupons应用的admin.py文件Copyfrom django.contrib import admin from .models import CouponclassCouponAdmin(admin.ModelAdmin):list_display [code, valid_from, valid_to, discount, active]list_filter [active, valid_from, valid_to]search_fields [code]admin.site.register(Coupon, CouponAdmin)现在启动站点到http://127.0.0.1:8000/admin/coupons/coupon/add/查看Coupon模型输入一个优惠码记录有效期设置为当前日期不要忘记勾上Active然后点击SAVE按钮。1.2为购物车增加优惠码功能创建数据模型之后可以查询和获得优惠码对象。现在我们必须增添使用户可以输入优惠码从而获得折扣价的功能。这个功能将按照如下逻辑进行操作用户添加商品到购物车用户能通过购物车详情页面的表单输入一个优惠码输入优惠码并提交表单之后需要来判断该码是否在数据库中存在、当前时间是否在valid_from和valid_to有效时间之间、active属性是否为True。如果优惠码通过上述检查将优惠码的信息保存在session中用折扣重新计算价格并更新购物车中的商品价格用户提交订单时将优惠码保存在订单对象中。在coupons应用里建立forms.py文件添加下列代码Copyfrom django import formsclassCouponApplyForm(forms.Form):code forms.CharField()这个表单用于用户输入优惠码。然后来编辑coupons应用的views.py文件Copyfrom django.shortcuts import render, redirect from django.utils import timezone from django.views.decorators.http import require_POST from .models import Coupon from .forms import CouponApplyFormrequire_POSTdefcoupon_apply(request):now timezone.now()form CouponApplyForm(request.POST)if form.is_valid():code form.cleaned_data[code]try:coupon Coupon.objects.get(code__iexactcode, valid_from__ltenow, valid_to__gtenow, activeTrue)request.session[coupon_id] coupon.idexcept Coupon.DoesNotExist:request.session[coupon_id] Nonereturn redirect(cart:cart_detail)这个coupon_apply视图验证优惠码并将其存储在session中使用了require_POST装饰器令该视图仅接受POST请求。这个视图的业务逻辑如下使用请求中的数据初始化CouponApplyForm如果表单通过验证从表单的cleaned_data获取code然后使用code查询数据库得到coupon对象这里使用了过滤参数iexact进行完全匹配使用activeTrue过滤出有效的优惠码使用timezone.now()获取当前时间valid_from和valid_to分别采用lte小于等于和gte大于等于过滤查询以保证当前时间位于有效期内。将优惠码ID存入当前用户的session。重定向到cart_detail URL对应的购物车详情页以显示应用了优惠码之后的金额。需要为coupon_apply视图配置URL在coupons应用中建立urls.py文件添加下列代码Copyfrom django.urls import path from . import viewsapp_name coupons urlpatterns [path(apply/, views.coupon_apply, nameapply), ]然后编辑项目的根路由增加一行Copyurlpatterns [# ...path(coupons/, include(coupons.urls, namespacecoupons)),path(, include(shop.urls, namespaceshop)), ]依然记得要把这一行放在shop.urls上方。编辑cart应用中的cart.py文件添加下列导入Copyfrom coupons.models import Coupon然后在cart类的__init__()方法的最后添加从session中获得优惠码ID的语句CopyclassCart(object):def__init__(self, request):# ...# store current applied couponself.coupon_id self.session.get(coupon_id) 在Cart类中我们需要通过coupon_id获取优惠码信息并将其保存在Cart对象内为Cart类添加如下方法CopyclassCart(object):# ... propertydefcoupon(self):if self.coupon_id:return Coupon.objects.get(idself.coupon_id)returnNonedefget_discount(self):if self.coupon:return (self.coupon.discount / Decimal(100)) * self.get_total_price()return Decimal(0)defget_total_price_after_diccount(self):return self.get_total_price() - self.get_discount()这些方法解释如下coupon()我们使用property将该方法定义为属性如果购物车包含一个coupon_id属性会返回该id对应的Coupon对象get_discount()如果包含优惠码id计算折扣价格否则返回0。get_total_price_after_discount()返回总价减去折扣价之后的折扣后价格。现在Cart类就具备了根据优惠码计算折扣价的功能。现在还需要修改购物车详情视图函数以便在页面中应用表单和展示折扣金额修改cart应用的views.py文件增加导入代码Copyfrom coupons.forms import CouponApplyForm然后修改cart_detail视图添加表单Copydefcart_detail(request):cart Cart(request)for item in cart:item[update_quantity_form] CartAddProductForm(initial{quantity: item[quantity], update: True})coupon_apply_form CouponApplyForm()return render(request, cart/detail.html, {cart: cart, coupon_apply_form: coupon_apply_form})修改cart应用的购物车模板cart/detail.html找到如下几行Copytrclasstotaltdtotal/tdtdcolspan4/tdtdclassnum${{ cart.get_total_price }}/td/tr替换成如下代码Copy{% if cart.coupon %}trclasssubtotaltdSubtotal/tdtdcolspan4/tdtdclassnum${{ cart.get_total_price_after_diccount }}/td/trtrtd{{ cart.coupon.code }} coupon ({{ cart.coupon.discount }}% off)/tdtdcolspan4/tdtdclassnum neg- ${{ cart.get_discount|floatformat:2 }}/td/tr {% endif %}trclasstotaltdTotal/tdtdcolspan4/tdtdclassnum${{ cart.get_total_price_after_diccount|floatformat:2 }}/td/tr这是新的购物车模板。如果包含一个优惠券就展示一行购物车总价再展示一行优惠券信息最后通过get_total_price_after_discount()展示折扣后价格。在同一个文件内在/table后增加下列代码Copy{# 在紧挨着/table标签之后插入 #} pApply a coupon:/p form action{% url coupons:apply %} methodpost{{ coupon_apply_form }}inputtypesubmit valueApply{% csrf_token %} /form上边这段代码展示输入优惠码的表单。在浏览器中打开http://127.0.0.1:8000/向购物车内加入一些商品然后进入购物车页面输入优惠码并提交可以看到如下所示之后来修改订单模板orders/order/create.html在其中找到如下部分Copyul{% for item in cart %}li{{ item.quantity }} x {{ item.product.name }}span${{ item.total_price }}/span/li{% endfor %} /ul替换成Copyul{% for item in cart %}li{{ item.quantity }}x {{ item.product.name }}span${{ item.total_price|floatformat:2 }}/span/li{% endfor %}{% if cart.coupon %}li{{ cart.coupon.code }} ({{ cart.coupon.discount }}% off)span- ${{ cart.get_discount|floatformat:2 }}/span/li{% endif %} /ul如果有优惠码现在的订单页面就展示优惠码信息了。继续找到下边这行CopypTotal: ${{ cart.get_total_price }}/p替换成CopypTotal: ${{ cart.get_total_price_after_diccount|floatformat:2 }}/p这样总价也变成了折扣后价格。在浏览器中打开http://127.0.0.1:8000/添加商品到购物车然后生成订单可以看到订单页面的价格现在是折扣后的价格了1.3在订单中记录优惠码信息像之前说的我们需要将优惠码信息保存至order对象中为此需要修改Order模型。编辑编辑orders应用的models.py文件增加导入部分的代码Copyfrom decimal import Decimal from django.core.validators import MinValueValidator, MaxValueValidator from coupons.models import Coupon然后为Order模型增加下列字段CopyclassOrder(models.Model):coupon models.ForeignKey(Coupon, related_nameorders, nullTrue, blankTrue, on_deletemodels.SET_NULL)discount models.IntegerField(default0, validators[MinValueValidator(0), MaxValueValidator(100)])这两个字段用于存储优惠码信息。虽然折扣信息保存在Coupon对象中但这里还是用discount字段保存了当前的折扣以免未来优惠码折扣发生变化。为coupon字段设置了on_deletemodels.SET_NULL优惠码删除时该外键字段会变成空值。增加好字段后数据迁移程序。回到models.py文件需要修改Order类中的get_total_cost()方法CopyclassOrder(models.Model):# ...defget_total_cost(self):total_cost sum(item.get_cost() for item in self.items.all())return total_cost - total_cost * (self.discount / Decimal(100))修改后的get_total_cost()方法会把折扣也考虑进去。之后还需要修改orders应用里的views.py文件中的order_create视图以便在生成订单的时候存储这两个新增的字段。找到下边这行Copyorder form.save()将其替换成如下代码Copyorder form.save(commitFalse) if cart.coupon:order.coupon cart.couponorder.discount cart.coupon.discount order.save()在修改后代码中通过调用OrderCreateForm表单对象的save()方法创建一个order对象使用commitFalse暂不存入数据库。如果购物车对象中有折扣信息就保存折扣信息。然后将order对象存入数据库。启动站点在浏览器中访问http://127.0.0.1:8000/使用一个自己创建的优惠码在完成购买之后可以到http://127.0.0.1:8000/admin/orders/order/查看包含优惠码和折扣信息的订单还可以修改管理后台的订单详情页和和PDF发票以使其包含优惠码和折扣信息。下边我们将为站点增加国际化功能。译者注这里有一个问题用户提交了订单并清空购物车后如果再向购物车内添加内容再次进入购物车详情页面可以发现自动使用了上次使用的优惠券。此种情况的原因是作者把优惠券信息附加到了session上在提交订单的时候没有清除。cart对象实例化的时候又取到了相同的优惠券信息。所以需要对程序进行一下改进。修改orders应用的order_create视图在生成OrderItem并清空购物车的代码下增加一行Copydeforder_create(request):cart Cart(request)if request.method POST:form OrderCreateForm(request.POST)# 表单验证通过就对购物车内每一条记录生成OrderItem中对应的一条记录if form.is_valid():order form.save(commitFalse)if cart.coupon:order.coupon cart.couponorder.discount cart.coupon.discountorder.save()for item in cart:OrderItem.objects.create(orderorder, productitem[product], priceitem[price],quantityitem[quantity])# 成功生成OrderItem之后清除购物车cart.clear()# 清除优惠券信息request.session[coupon_id] None# 成功完成订单后调用异步任务发送邮件order_created.delay(order.id)# 在session中加入订单idrequest.session[order_id] order.id# 重定向到支付页面return redirect(reverse(payment:process))else:form OrderCreateForm()return render(request, orders/order/create.html, {cart: cart, form: form})2国际化与本地化Django对于国际化和本地化提供了完整的支持允许开发者将站点内容翻译成多种语言而且可以处理本地化的时间日期数字和时区格式等本地化的显示内容。在开始之前先需要区分一下国际化和本地化两个概念。国际化和本地化都是一种软件开发过程。国际化Internationalization通常缩写为i18n是指一个软件可以被不同的国家和地区使用而不会局限于某种语言。本地化Localization缩写为l10n是指对国际化的软件将其进行翻译或者其他本地化适配使之变成适合某一个国家或地区使用的软件的过程。Django通过自身的国际化框架可以支持超过50种语言。2.1国际化与本地化设置Django的国际化框架可以让开发者很方便的在Python代码和模板中标注需要翻译的字符串这个框架依赖于GNU gettext开源软件来生成和管理消息文件message file)。消息文件是一个纯文本文件代表一种语言的翻译存放着在站点应用中找到的部分或者所有需要翻译的字符串以及对应的某种语言的翻译就像一个字典一样。消息文件的后缀名是.po。一旦完成翻译可以把消息文件编译以快速访问翻译内容编译后的消息文件的后缀名是.mo。2.1.1国际化与本地化设置Django提供了一些国际化和本地化的设置下边一些设置是最重要的USE_I18N布尔值是否启用国际化功能默认为TrueUSE_L10N布尔值设置本地化功能是否启用设置为True时数字和日期将采用本地化显示。默认为FalseUSE_TZ布尔值指定时间是否根据时区进行调整当使用startproject创建项目时默认为TrueLANGUAGE_CODE项目的默认语言代码采用标准的语言代码格式例如en-us表示美国英语en-gb表示英国英语。这个设置需要USE_I18N设置为True才会生效。在http://www.i18nguy.com/unicode/language-identifiers.html可以找到语言代码清单。LANGUAGES一个包含项目所有可用语言的元组其中每个元素是语言代码和语言名称构成的二元组。可以在django.conf.global_settings查看所有可用的语言。这个属性可设置的值必须是django.conf.global_settings中列出的值。LOCALE_PATHS一个目录列表目录内存放项目的翻译文件。TIME_ZONE字符串代表项目所采用的时区。如果使用startproject启动项目该值被设置为UTC。可以按照实际情况将其设置为具体时区如Europe/Madrid。中国的时区是Asia/Shanghai大小写敏感。以上是常用的国际化和本地化设置完整设置请参见https://docs.djangoproject.com/en/2.1/ref/settings/#globalization-i18n-l10n。2.1.2国际化和本地化管理命令Django包含了用于管理翻译的命令如下makemessages运行该命令会找到项目中所有标注要翻译的字符串建立或者更新locale目录下的.po文件每种语言会生成单独的.po文件。compilemessages编译所有的.po文件为.mo文件。需要使用GNU gettext工具来执行上述过程大部分linux发行版自带有该工具。如果在使用mac OSX可以通过 http://brew.sh/ 使用命令brew install gettext来安装之后使用brew link gettext --force强制链接。对于Windows下的安装参考https://docs.djangoproject.com/en/2.0/topics/i18n/translation/#gettext-on-windows中的步骤。2.1.3如何为项目增加翻译文件先来看一下增加翻译需要进行的流程在Python代码和模板中标注出需要翻译的字符串运行makemessages命令建立消息文件在消息文件中将字符串翻译成另外一种语言然后运行compilemessages命令编译消息文件2.1.4Django如何确定当前语言Django使用中间件django.middleware.locale.LocaleMiddleware来检查HTTP请求中所使用的本地语言。这个中间件做的工作如下如果使用i18_patternsdjango特殊的一种URL方式里边包含语言前缀中间件会在请求的URL中寻找特定语言的前缀如果在URL中没有发现语言前缀会在session中寻找一个键LANGUAGE_SESSION_KEY如果session中没有该键会在cookie中寻找一个键。可以通过LANGUAGE_COOKIE_NAME自定义该cookie的名称默认是django_language如果cookie中未找到找HTTP请求头的Accept-Language键如果Accept-Language头部信息未指定具体语言则使用LANGUAGE_CODE设置注意这个过程只有在开启了该中间件的时候才会得到完整执行如果未开启中间件Django直接使用LANGUAGE_CODE中的设置。2.2为项目使用国际化进行准备我们准备为电商网站增添各种语言的支持增添英语和西班牙语的支持。编辑settings.py文件加入LANGUAGES设置放在LANGUAGE_CODE的旁边CopyLANGUAGES ((en, English),(es, Spanish), )LANGUAGES设置包含两个语言代码和名称组成的元组。语言代码可以指定具体语言如en-us或en-gb也可以更模糊如en。通过这个设置我们定义了我们的网站仅支持英语和西班牙语。如果不定义LANGUAGES设置默认支持所有django支持的语言。设置LANGUAGE_CODE为如下CopyLANGUAGE_CODE en添加django.middleware.locale.LocaleMiddleware到settings.py的中间件设置中位置在SessionMiddleware中间件之后CommonMiddleware中间件之前因为LocaleMiddleware需要使用session而CommonMiddleware需要一种可用语言来解析URLMIDDLEWARE设置成如下CopyMIDDLEWARE [django.middleware.security.SecurityMiddleware,django.contrib.sessions.middleware.SessionMiddleware,django.middleware.locale.LocaleMiddleware,django.middleware.common.CommonMiddleware,# ... ]django中间件设置的顺序很重要中间件会在请求上附加额外的数据某个中间件会依赖于另外一个中间件附加的数据才能正常工作。在manage.py文件所在的项目根目录下创建如下目录Copylocale/en/es/locale目录是用来存放消息文件的目录编辑settings.py文件加入如下设置CopyLOCALE_PATH (os.path.join(BASE_DIR, locale/), )LOCALE_PATH指定了Django寻找消息文件的路径可以是一系列路径最上边的路径优先级最高。当使用makemessages命令的时候消息文件会在我们创建的locale/目录中创建如果某个应用也有locale/目录那个应用中的翻译内容会优先在那个应用的目录中创建。2.3翻译Python代码中的字符串为了翻译Python代码中的字符串字面量需要使用django.utils.translation模块中的gettext()方法来标注字符串。这个方法返回翻译后的字符串通常做法是导入该方法然后命名为一个下划线_。可以在https://docs.djangoproject.com/en/2.0/topics/i18n/translation/查看文档。2.3.1标记字符串标记字符串的方法如下Copyfrom django.utils.translation import gettext as _ output _(Text to be translated.)2.3.2惰性翻译Django对于所有的翻译函数都有惰性版本后缀为_lazy()。使用惰性翻译函数的时候字符串只有被访问的时候才会进行翻译而不是在翻译函数调用的时候。当字符串位于模块加载的时候才生成的路径中时候特别有效。使用gettext_lazy()代替gettext()方法只有在该字符串被访问的时候才会进行翻译所有的翻译函数都有惰性版本。。2.3.3包含变量的翻译被标注的字符串中还可以带有占位符以下是一个占位符的例子Copyfrom django.utils.translation import gettext as _ month _(April) day 14 output _(Today is %(month)s %(day)s) % {month: month, day: day}通过使用占位符可以使用字符串变量。例如上边这个例子的英语如果是Today is April 14翻译成的西班牙语就是Hoy es 14 de Abril。当需要翻译的文本中存在变量的时候推荐使用占位符。2.3.4复数的翻译对于复数形式的翻译可以采用ngettext()和ngettext_lazy()。这两个函数根据对象的数量来翻译单数或者复数。使用例子如下Copyoutput ngettext(there is %(count)d product, there are %(count)d products, count) % {count: count}现在我们了解了Python中翻译字面量的知识可以来为我们的项目添加翻译功能了。2.3.5为项目翻译Python字符串字面量编辑setttings.py导入gettext_lazy()然后修改LANGUAGES设置Copyfrom django.utils.translation import gettext_lazy as _LANGUAGES ((en, _(English)),(es, _(Spanish)), )这里导入了gettext_lazy()并使用了别名_来避免重复导入。将显示的名称也进行了翻译这样对于不同的语言的人来说可以看懂并选择他自己的语言。然后打开系统命令行窗口输入如下命令Copydjango-admin makemessages --all可以看到如下输出Copyprocessing locale en processing locale es然后查看项目的locale目录可以看到如下文件和目录结构Copyen/LC_MESSAGES/django.po es/LC_MESSAGES/django.po每个语言都生成了一个.po消息文件使用文本编辑器打开es/LC_MESSAGES/django.po文件在末尾可以看到如下内容Copy#: .\myshop\settings.py:107 msgid English msgstr #: .\myshop\settings.py:108 msgid Spanish msgstr 每一部分的第一行表示在那个文件的第几行发现了需翻译的内容每个翻译包含两个字符串msgid源代码中的字符串msgstr被翻译成的字符串默认为空需要手工添加。添加好翻译之后的文件如下Copy#: myshop/settings.py:117 msgid English msgstr Inglés#: myshop/settings.py:118 msgid Spanish msgstr Español保存这个文件之后执行命令编译消息文件Copydjango-admin compilemessages可以看到输出如下Copyprocessing file django.po in myshop/locale/en/LC_MESSAGES processing file django.po in myshop/locale/es/LC_MESSAGES这表明已经编译了翻译文件此时查看locale目录其结构如下Copyen/LC_MESSAGES/django.modjango.po es/LC_MESSAGES/django.modjango.po可以看到每种语言都生成了.mo文件。我们已经翻译好了语言名称本身。现在我们来试着翻译一下Order模型的所有字段修改orders应用的models.py文件Copyfrom django.utils.translation import gettext_lazy as _classOrder(models.Model):first_name models.CharField(_(frist name), max_length50)last_name models.CharField(_(last name), max_length50)email models.EmailField(_(e-mail), )address models.CharField(_(address), max_length250)postal_code models.CharField(_(postal code), max_length20)city models.CharField(_(city), max_length100)......我们为每个显示出来的字段标记了翻译内容也可以使用verbose_name属性来命名字段。在orders应用中建立如下目录Copylocale/en/es/通过创建locale目录当前应用下的翻译内容会优先保存到这个目录中而不是保存在项目根目录下的locale目录中。这样就可以为每个应用配置独立的翻译文件。在系统命令行中执行Copydjango-admin makemessages --all输出为Copyprocessing locale es processing locale en使用文本编辑器打开locale/es/LC_MESSAGES/django.po可以看到Order模型的字段翻译在msgstr中为对应的msgid字符串加上西班牙语的翻译Copy#: orders/models.py:10 msgid first name msgstr nombre#: orders/models.py:11 msgid last name msgstr apellidos#: orders/models.py:12 msgid e-mail msgstr e-mail#: orders/models.py:13 msgid address msgstr dirección#: orders/models.py:14 msgid postal code msgstr código postal#: orders/models.py:15 msgid city msgstr ciudad添加完翻译之后保存文件。除了常用的文本编辑软件还可以考虑使用Poedit编辑翻译内容该软件同样依赖gettext支持LinuxWindows和macOS X。可以在https://poedit.net/下载该软件。下边来翻译项目使用的表单。OrderCreateForm这个表单类无需翻译因为它会自动使用Order类中我们刚刚翻译的verbose_name。现在我们去翻译cart和coupons应用中的内容。在cart应用的forms.py文件中导入翻译函数为CartAddProductForm类的quantity字段增加一个参数label代码如下Copyfrom django import forms from django.utils.translation import gettext_lazy as _ PRODUCT_QUANTITY_CHOICES [(i, str(i)) for i inrange(1, 21)]classCartAddProductForm(forms.Form):quantity forms.TypedChoiceField(choicesPRODUCT_QUANTITY_CHOICES, coerceint, label_(Quantity))update forms.BooleanField(requiredFalse, initialFalse, widgetforms.HiddenInput)译者注红字部分是本书上一版的遗留无任何作用读者可以忽略。之后修改coupons应用的forms.py文件为CouponApplyForm类增加翻译Copyfrom django import forms from django.utils.translation import gettext_lazy as _classCouponApplyForm(forms.Form):code forms.CharField(label_(Coupon))我们为code字段增加了一个label标签用于展示翻译后的字段名称。2.4翻译模板Django为翻译模板内容提供了{% trans %}和{% blocktrans %}两个模板标签用于翻译内容如果要启用这两个标签需要在模板顶部加入{% load i18n %}。2.4.1使用{% trans %}模板标签{% trans %}标签用来标记一个字符串常量或者变量用于翻译。Django内部也是该文本执行gettext()等翻译函数。标记字符串的例子是Copy{% trans Text to be translated %}也可以像其他标签变量一样使用as 将 翻译后的结果放入一个变量中在其他地方使用。下面的例子使用了一个变量greetingCopy{% trans Hello! as greeting %} h1{{ greeting }}/h1这个标签用于比较简单的翻译但不能用于带占位符的文字翻译。2.4.2使用{% blocktrans %}模板标签{% blocktrans %}标签可以标记包含常量和占位符的内容用于翻译下边的例子展示了使用一个name变量的翻译Copy{% blocktrans %}Hello {{ name }}!{% endblocktrans %}可以使用with将具体的表达式设置为变量的值此时在blocktrans块内部不能够再继续访问表达式和对象的属性下面是一个使用了capfirst装饰器的例子Copy{% blocktrans with nameuser.name|capfirst %}Hello {{ name }}! {% endblocktrans %}如果翻译内容中包含变量使用{% blocktrans %}代替{% trans %}。2.4.3翻译商店模板编辑shop应用的base.html在其顶部加入i18n标签然后标注如下要翻译的部分Copy{% load i18n %} {% load static %} !DOCTYPE htmlhtmlheadmetacharsetutf-8/title{% block title %}{% trans My shop %}{% endblock %}/titlelinkhref{% static css/base2.css %} relstylesheet/headbodydividheaderahref/classlogo{% trans My shop %}/a/divdividsubheaderdivclasscart{% with total_itemsmycart|length %}{% if mycart|length 0 %}{% trans Your cart %}:ahref{% url cart:cart_detail %}{% blocktrans with total_items_pluraltotal_items|pluralize total_pricecart.get_total_price %}{{ total_items }} items{{ total_items_plural }}, ${{ total_price }}{% endblocktrans %}/a{% else %}{% trans Your cart is empty. %}{% endif %}{% endwith %}/div/divdividcontent{% block content %}{% endblock %} /div/body/html注意{% blocktrans %}展示购物车总价部分的方法在原来的模板中我们使用了Copy{{ total_items }} item{{ total_items|pluralize }}, ${{ cart.get_total_price }}现在改用{% blocktrans with ... %}来为total_items|pluralize使用了过滤器和cart.get_total_price访问对象的方法创建占位符编辑shop应用的shop/product/detail.html紧接着{% extends %}标签导入i18n标签Copy{% load i18n %}之后找到下边这一行CopyinputtypesubmitvalueAdd to cart将其替换成Copyinputtypesubmitvalue{% trans Addtocart %}现在来翻译orders应用编辑orders/order/create.html标记如下翻译内容Copy{% extends shop/base.html %} {% load i18n %} {% block title %}{% trans Checkout %} {% endblock %}{% block content %}h1{% trans Checkout %}/h1divclassorder-infoh3{% trans Your order %}/h3ul{% for item in cart %}li{{ item.quantity }}x {{ item.product.name }}span${{ item.total_price|floatformat:2 }}/span/li{% endfor %}{% if cart.coupon %}li{% blocktrans with codecart.coupon.code discountcart.coupon.discount %}{{ code }} ({{ discount }}% off){% endblocktrans %}span- ${{ cart.get_discount|floatformat:2 }}/span/li{% endif %}/ulp{% trans Total %}: ${{ cart.get_total_price_after_diccount|floatformat:2 }}/p/divformaction.methodpostclassorder-formnovalidate{{ form.as_p }}pinputtypesubmitvalue{% trans Placeorder %}/p{% csrf_token %}/form {% endblock %}到现在我们完成了如下文件的翻译shop应用的shop/product/list.html模板orders应用的orders/order/created.html模板cart应用的cart/detail.html模板之后来更新消息文件打开命令行窗口执行Copydjango-admin makemessages --all此时myshop项目下的locale目录内有了对应的.po文件而orders应用的翻译文件优先存放在应用内部的locale目录中。编辑所有.po文件在msgstr属性内添加西班牙语翻译。你也可以直接复制随书代码内对应文件的内容。执行命令编译消息文件Copydjango-admin compilemessages可以看到如下输出Copyprocessing file django.po in myshop/locale/en/LC_MESSAGES processing file django.po in myshop/locale/es/LC_MESSAGES processing file django.po in myshop/orders/locale/en/LC_MESSAGES processing file django.po in myshop/orders/locale/es/LC_MESSAGES针对每一个.po文件都会生成对应的.mo文件。2.5使用Rosetta翻译界面Rosetta是一个第三方应用通过Django管理后台编辑所有翻译内容让.po文件的管理变得更加方便先通过pip安装该模块Copypip install django-rosetta0.8.1之后在settings.py中激活该应用CopyINSTALLED_APPS [# ...rosetta, ]然后需要为Rosetta配置相应的URL其二级路由已经配置好修改项目根路由增加一行Copyurlpatterns [# ...path(rosetta/, include(rosetta.urls)),path(, include(shop.urls, namespaceshop)), ]这条路径也需要在shop.urls上边。然后启动站点使用管理员身份登录http://127.0.0.1:8000/rosetta/ 再转到http://127.0.0.1:8000/rosetta/点击右上的THIRD PARTY以列出所有的翻译文件如下图所示点开Spanish下边的Myshop链接可以看到列出了所有需要翻译的内容可以手工编辑需要翻译的地方OCCURRENCES(S)栏显示了该翻译所在的文件名和行数对于那些占位符翻译的内容显示为这样Rosetta对占位符使用了不同的背景颜色在手工输入翻译内容的时候注意不要破坏占位符的结构例如要翻译下边这一行Copy%(total_items)s item%(total_items_plural)s, $%(total_price)s应该输入Copy%(total_items)s producto%(total_items_plural)s, $%(total_price)s可以参考本章随书代码中的西班牙语翻译来录入翻译内容。结束输入的时候点击一下Save即可将当前翻译的内容保存到.po文件中当保存之后Rosetta会自动进行编译所以无需执行compilemessages命令。然而要注意Rosetta会直接读写locale目录注意要给予其相应的权限。如果需要其他用户来编辑翻译内容可以到http://127.0.0.1:8000/admin/auth/group/add/新增一个用户组叫translators然后到http://127.0.0.1:8000/admin/auth/user/编辑用户的权限以给予其修改翻译的权限将该用户加入到translators用户组内。仅限超级用户和translators用户组内的用户才能使用Rosetta。Rosetta的官方文档在https://django-rosetta.readthedocs.io/en/latest/。特别注意的是当Django已经在生产环境运行时如果修改和新增了翻译在运行了compilemessages命令之后只有重新启动Django才会让新的翻译生效。2.6待校对翻译Fuzzy translations你可能注意到了Rosetta页面上有一列叫做Fuzzy。这不是Rosetta的功能而是gettext提供的功能。如果将fuzzy设置为true则该条翻译不会包含在编译后的消息文件中。这个字段用来标记需要由用户进行检查的翻译内容。当.po文件更新了新的翻译字符串时很可能一些翻译被自动标成了fuzzy。这是因为在gettext发现一些msgid被修改过的时候gettext会将其与它认为的旧有翻译进行匹配然后标注上fuzzy。看到fuzzy出现的时候人工翻译者必须检查该条翻译然后取消fuzzy之后再行编译。2.7国际化URLDjango提供两种国际化URL的特性Language prefix in URL patterns 语言前缀URL模式在URL的前边加上不同的语言前缀构成不同的基础URLTranslated URL patterns 翻译URL模式基础URL相同把基础URL按照不同语言翻译给用户得到不同语言的URL使用翻译URL模式的优点是对搜索引擎友好。如果采用语言前缀URL则必须要为每一种语言进行索引使用翻译URL模式则一条URL就可以匹配全部语言。下边来看一下两种模式的使用2.7.1语言前缀URL模式Django可以为不同语言在URL前添加前缀例如我们的网站英语版以/en/开头而西班牙语版以/es/开头。要使用语言前缀URL模式需要启用LocaleMiddleware中间件用于从不同的URL中识别语言在之前我们已经添加过该中间件。我们来为URL模式增加前缀现在需要修改项目的根urls.py文件Copyfrom django.conf.urls.i18n import i18n_patternsurlpatterns i18n_patterns(path(admin/, admin.site.urls),path(cart/, include(cart.urls, namespacecart)),path(orders/, include(orders.urls, namespaceorders)),path(pyament/, include(payment.urls, namespacepayment)),path(coupons/, include(coupons.urls, namespacecoupons)),path(rosetta/, include(rosetta.urls)),path(, include(shop.urls, namespaceshop)), )可以混用未经翻译的标准URL与i18n_patterns类型的URL使部分URL带有语言前缀部分不带前缀。但最好只使用翻译URL以避免把翻译过的URL匹配到未经翻译过的URL模式上。现在启动站点到http://127.0.0.1:8000/ Django的语言中间件会按照之前介绍的顺序来确定本地语言然后重定向到带有语言前缀的URL。现在看一下浏览器的地址栏应该是http://127.0.0.1:8000/en/。当前语言是由请求头Accept-Language所设置或者就是LANGUAGE_CODE的设置。2.7.2翻译URL模式Django支持在URL模式中翻译字符串。针对不同的语言可以翻译出不同的URL。在urls.py中使用ugettext_lazy()来标注字符串。编辑myshop应用的根urls.py为cartorderspayment和coupons应用配置URLCopyfrom django.utils.translation import gettext_lazy as _urlpatterns i18n_patterns(path(_(admin/), admin.site.urls),path(_(cart/), include(cart.urls, namespacecart)),path(_(orders/), include(orders.urls, namespaceorders)),path(_(payment/), include(payment.urls, namespacepayment)),path(_(coupons/), include(coupons.urls, namespacecoupons)),path(rosetta/, include(rosetta.urls)),path(, include(shop.urls, namespaceshop)), )编辑orders应用的urls.py文件修改成如下Copyfrom django.utils.translation import gettext_lazy as _urlpatterns [path(_(create/), views.order_create, nameorder_create),# ... ]修改payment应用的urls.py文件修改成如下Copyfrom django.utils.translation import gettext_lazy as _urlpatterns [path(_(process/), views.payment_process, nameprocess),path(_(done/), views.payment_done, namedone),path(_(canceled/), views.payment_canceled, namecanceled), ]对于shop应用的URL不需要修改因为其URL是动态建立的。执行命令进行编译更新消息文件Copydjango-admin makemessages --all启动站点访问http://127.0.0.1:8000/en/rosetta/点击Spanish下的Myshop可以看到出现了URL对应的翻译。可以点击Untranslated查看所有尚未翻译的字符串然后输入翻译内容。2.8允许用户切换语言在之前的工作中我们配置好了英语和西班牙语的翻译应该给用户提供切换语言的选项为此准备给网站增加一个语言选择器列出所有支持的语言显示为一系列链接。编辑shop应用下的base.html找到下边这三行Copydividheaderahref/classlogo{% trans My shop %}/a/div将其替换成Copydividheaderahref/classlogo{% trans My shop %}/a{% get_current_language as LANGUAGE_CODE %}{% get_available_languages as LANGUAGES %}{% get_language_info_list for LANGUAGES as languages %}divclasslanguagesp{% trans Language %}:/pulclasslanguages{% for language in languages %}liahref/{{ language.code }}/{% iflanguage.code LANGUAGE_CODE %} classselected{% endif %}{{ language.name_local }}/a/li{% endfor %}/ul/div/div这个就是我们的语言选择器逻辑如下页面的最上方加载{% load i18n %}使用{% get_current_language %}标签用于获取当前语言使用{% get_available_languages %}标签用于从LANGUAGES里获取所有可用的支持语言使用{% get_language_info_list %}是为了快速获取语言的属性而设置的变量用循环列出了所有可支持的语言对于当前语言设置CSS类为select启动站点到http://127.0.0.1:8000/ 可以看到页面右上方出现了语言选择器如下图2.9使用django-parler翻译模型Django没有提供直接可用的模型翻译功能必须采用自己的方式实现模型翻译。有一些第三方工具可以翻译模型字段每个工具存储翻译的方式都不相同。其中一个工具叫做django-parler提供了高效的翻译管理还能够与管理后台进行集成。django-parler的工作原理是为每个模型建立一个对应的翻译数据表表内每条翻译记录通过外键连到翻译文字所在的模型表内还有一个language字段用于标记是何种语言。2.9.1安装django-parler使用pip安装django-parlerCopypip install django-parler1.9.2在settings.py内激活该应用CopyINSTALLED_APPS [# ...parler, ]继续添加下列设置CopyPARLER_LANGUAGES {None: ({code: en},{code: es},),default: {fallback: en,hide_untranslated: False,} }该配置的含义是指定了django-parler的可用语言为en和es然后指定了默认语言为en然后指定django-parler不要隐藏未翻译的内容。2.9.2翻译模型字段我们为商品品类添加翻译。django-parler提供一个TranslatableModel类此处作者原文有误写成了TranslatedModel和TranslatedFields方法来翻译模型的字段。编辑shop应用的models.py文件添加导入语句Copyfrom parler.models import TranslatableModel, TranslatedFields然后修改Category模型的name和slug字段CopyclassCategory(TranslatableModel):translations TranslatedFields(namemodels.CharField(max_length200, db_indexTrue),slugmodels.SlugField(max_length200, db_indexTrue, uniqueTrue))Category类现在继承了TranslatableModel类而不是原来的models.Modelname和slug字段被包含在了TranslatedFields包装器里。编辑Productnameslugdescription和上边一样的方式CopyclassProduct(TranslatableModel):translations TranslatedFields(namemodels.CharField(max_length200, db_indexTrue),slugmodels.SlugField(max_length200, db_indexTrue),descriptionmodels.TextField(blankTrue))category models.ForeignKey(Category, related_nameproducts)image models.ImageField(upload_toproducts/%Y/%m/%d, blankTrue)price models.DecimalField(max_digits10, decimal_places2)available models.BooleanField(defaultTrue)created models.DateTimeField(auto_now_addTrue)updated models.DateTimeField(auto_nowTrue)django-parler通过新创建模型为其他模型提供翻译在下图可以看到Product与其对应的翻译模型ProductTranslation之间的关系译者注此时如果运行站点一些IDE会提示模型的字段找不到这个对于实际运行程序没有影响该字段依然可用。django-parler生成的ProductTranslation类包含nameslugdescription和一个language_code字段还有一个外键连接到Product类针对一个Product模型会按照每种语言生成一个对应的ProductTranslation对象。由于翻译的部分和原始的类是独立的两个模型因此一些ORM的功能无法使用比如不能在Product类中根据一个翻译后的字段进行排序也不能在Meta类的ordering属性中使用翻译的字段。所以编辑shop应用的models.py文件注释掉ordering设置CopyclassCategory(TranslatableModel):# ...classMeta:# ordering (name,)verbose_name categoryverbose_name_plural categories对于Product类也要注释掉ordering还需要注释掉index_together这是因为目前的django-parler不支持联合索引的验证关系。如下图CopyclassProduct(TranslatableModel):# ...classMeta:pass# ordering (name,)# index_together ((id, slug),)译者注原书在这里遗漏了pass不要忘记加上。关于django-parler的兼容性可以在https://django-parler.readthedocs.io/en/latest/compatibility.html查看。2.9.3将django-parler集成到管理后台django-parler易于集成到django管理后台中包含一个TranslatableAdmin类代替了原来的ModelAdmin类。编辑shop应用的admin.py文件导入该类Copyfrom parler.admin import TranslatableAdmin修改CategoryAdmin和ProductAdmin类使其继承TranslatableAdmin而不是ModelAdmin类django-parler不支持prepopulated_fields属性但支持相同功能的get_prepopulated_fields()方法因此将两个类修改如下Copyfrom django.contrib import admin from .models import Category, Product from parler.admin import TranslatableAdminadmin.register(Category)classCategoryAdmin(TranslatableAdmin):list_display [name, slug]defget_prepopulated_fields(self, request, objNone):return {slug: (name,)}admin.register(Product)classProductAdmin(TranslatableAdmin):list_display [name, slug, price, available, created, updated]list_filter [available, created, updated]list_editable [price, available]defget_prepopulated_fields(self, request, objNone):return {slug: (name,)}现在在管理后台内也能进行对翻译模型的管理了。现在可以执行数据迁移程序。2.9.4迁移翻译模型数据打开shell执行下列命令Copypython manage.py makemigrations shop --name translations会看到如下输出CopyMigrations forshop:shop\migrations\0002_translations.py- Create model CategoryTranslation- Create model ProductTranslation- Change Meta options on category- Change Meta options on product- Remove field name from category- Remove field slug from category- Alter index_together for product (0 constraint(s))- Add field master to producttranslation- Add field master to categorytranslation- Remove field description from product- Remove field name from product- Remove field slug from product- Alter unique_together for producttranslation (1 constraint(s))- Alter unique_together for categorytranslation (1 constraint(s))django-parler动态地创建了CategoryTranslation和ProductTranslation。注意原模型中需要翻译的字段从原模型中删除了这意味着这几个字段的数据全都丢失了必须启动站点后重新录入。之后运行数据迁移Copypython manage.py migrate shop可以看到下列输出CopyApplying shop.0002_translations... OK现在数据已经和数据库同步好了。启动站点访问http://127.0.0.1:8000/en/admin/shop/category/可以看到已经存在的模型失去了那些需要翻译的字段。点击一个category对象进行修改可以看到包含了两个不同的表格一个对应英语一个对应西班牙语如下图所示为所有已存在category记录都添加名称和简称再为其添加西班牙语的名称和简称然后点击SAVE按钮确保在切换标签之前点击了SAVE按钮否则数据不会被保存。之后到http://127.0.0.1:8000/en/admin/shop/product/进行同样的工作补充每个商品的名称、简称、描述以及对应的西班牙语翻译。2.9.5视图中加入翻译功能为了正常使用翻译后的模型必须让shop应用的视图对翻译后的字段也能够获取QuerySet终端内输入python manage.py shell进入带Django环境的命令行模式来试验一下经过翻译后的查询操作看一下如何查询翻译后的字段。为了获取某种语言的查询结果集需要使用Django的activate()函数Copy from shop.models import Productfrom django.utils.translation import activateactivate(es)productProduct.objects.first()product.name Té verde另外一种根据不同语言查询的方式是使用django-parler提供的language()模型管理器Copy productProduct.objects.language(en).first()product.name Green tea当查询翻译字段时会根据所指定的语言返回结果。可以通过设置管理器的属性得到不同语言的结果类似这样Copy product.set_current_language(es)product.name Té verde product.get_current_language() es如果需要使用filter功能需要使用tranlations__语法例子如下Copy Product.objects.filter(translations__nameGreen tea) TranslatableQuerySet [Product: Té verde]了解了基础操作可以来修改我们自己的视图中的查询方法了修改shop应用中的views.py找到product_list视图中如下这行Copycategory get_object_or_404(Category, slugcategory_slug)替换成如下内容Copylanguage request.LANGUAGE_CODE category get_object_or_404(Category, translations__language_codelanguage, translations__slugcategory_slug)然后编辑product_detail视图找到下边这行Copyproduct get_object_or_404(Product, idid, slugslug, availableTrue)替换成如下内容Copylanguage request.LANGUAGE_CODE product get_object_or_404(Product, idid, translations__language_codelanguage, translations__slugslug,availableTrue)product_list和product_detail现在都具备了根据翻译字段查询数据库的功能。启动站点到http://127.0.0.1:8000/es/应该可以看到商品名称全部都变成了西班牙语如下图可以看到通过每个商品的slug字段生成的URL也变成了西班牙语。比如一个商品的URL在西班牙语下是http://127.0.0.1:8000/es/2/te-rojo/在英语里则是http://127.0.0.1:8000/en/2/red-tea/。如果到一个商品详情页能够看到翻译后的URL和内容如下在https://django-parler.readthedocs.io/en/latest/可以查看django-parler的文档。现在已经知道了如何翻译Python代码模板URL和模型的字段站点已经可以提供不同语言的服务了。为了完成国际化和本地化的过程还需要对本地的日期时间数字格式进行设置。2.10本地格式化根据用户的国家和地区需要以不同的格式显示日期时间和数字。本地化格式可以通过settings.py里的USE_L10N设置为True来开启。当USE_L10N设置为开启的时候Django在渲染模板的时候会尽可能的尝试使用当前本地化的方式进行输出。可以看到我们的站点的小数点是一个圆点显示的切换到西班牙语的时候小数点显示为一个逗号。这是通过对每种语言进行不同的格式设置实现的对于支持的每种语言的格式Django都有对应的配置文件例如针对西班牙语的配置文件可以查看https://github.com/django/django/blob/stable/2.0.x/django/conf/locale/es/formats.py。通常情况下只要设置USE_L10N为TrueDjango就会自动应用本地化格式。然而站点内可能有些内容并不想使用本地化格式尤其那些标准数据例如代码或者是JSON字符串的内容。Django提供了一个{% locailze %}模板标签用于控制模板或者模板片段开启或关闭本地化输出。为了使用这个标签必须在模板开头使用{% load l10n %}标签。下边是一个如何在模板中控制开启/关闭本地化输出的例子Copy{% load l10n %}{% localize on %}{{ value }} {% endlocalize %}{% localize off %}{{ value }} {% endlocalize %}Django还提供了两个模板过滤器用于控制本地化分别是localize和unlocailze用来强制让一个值开启/关闭本地化显示。用法如下Copy{{ value|localize }} {{ value|unlocalize }}除了这两个方法之外还可以采取自定义格式文件方式具体看https://docs.djangoproject.com/en/2.0/topics/i18n/formatting/#creating-custom-format-files。2.11用django-localflavor验证表单字段django-localflavor是一个第三方模块包含一系列特别针对本地化验证的工具比如为每个国家单独设计的表单和模型字段对于验证某些国家的地区电话号码身份证社会保险号码等非常方便。这个模块是按照ISO 3166国家代码标准编写的。安装django-localflavorCopypip install django-localflavor2.0在settings.py中激活该应用CopyINSTALLED_APPS [# ...localflavor, ]为了使用该模块我们给订单增加一个美国邮编字段和对应验证必须是一个有效的美国邮编才能建立订单。编辑orders应用的forms.py文件修改成如下Copyfrom localflavor.us.forms import USZipCodeFieldclassOrderCreateForm(forms.ModelForm):postal_code USZipCodeField()classMeta:model Orderfields [first_name, last_name, email, address, postal_code, city]从localflaver的us模块中导入USZipCodeField字段类型将OrderCreateForm类的postal_code字段设置为该类型。运行站点到http://127.0.0.1:8000/en/orders/create/输入一些不符合美国邮编的邮政编码可以看到表单的错误提示CopyEnter a zip code in the format XXXXX or XXXXX-XXXX.这只是一个针对给字段附加本地化验证的一个简单例子。localflavor提供的组件对于将站点快速适配到某些国家非常有用。可以在https://django-localflavor.readthedocs.io/en/latest/阅读django-flavor的官方文档。现在就结束了所有国际化和本地化配置的工作下一步是建立一个商品推荐系统。3创建商品推荐系统商品推荐系统可以预测用户对一个商品的喜好程度或者评价高低根据用户的行为和收集到的用户数据选择可能和用户相关的产品推荐给用户。在电商行业推荐系统使用的非常广泛。推荐系统可以帮助用户从浩如烟海的商品中选出自己感兴趣的商品。好的推荐系统可以增加用户粘性对电商平台则意味着销售额的提高。我们准备建立一个简单但是强大的商品推荐系统用于推荐经常被一起购买的商品这些商品基于用户过去的购买数据来给用户进行推荐。我们打算在两个页面向用户推荐商品首先是商品详情页。我们会在此展示一些与当前商品一起购买的商品。展示的文字类似Users who bought this also bought X, Y, Z. 所以我们需要一个数据结构来存放所有与该商品一同购买的次数。其次是购物车详情页。这时将不同商品与购物车中所有商品的关联购买次数进行求和再进行排名。我们将使用Redis数据库记录一起购买的商品。我们在第六章已经使用过Redis如果还没有安装Redis可以参考该章节的内容。3.1根据之前的购买记录推荐商品现在需要根据用户加入到购物车内的商品计算排名。对于我们网站每一个被售出的商品在Redis中存一个键。这个商品键对应的值是一个有序集合就为同订单的其他商品在当前商品键对应的有序集合中的分数加1。当一个订单成功支付时我们为订单每个购买的商品存储一个有序集合这个有序集合将记录一起购买的商品分数。安装redis-p模块Copypip install redis2.10.6之后在settings.py里配置RedisCopyREDIS_HOST localhost REDIS_PORT 6379 REDIS_DB 1这是用于建立和Redis服务通信的设置。在shop应用目录下新建recommender.py文件添加下列代码Copyimport redis from django.conf import settings from .models import Product# 连接到Redis r redis.StrictRedis(hostsettings.REDIS_HOST, portsettings.REDIS_PORT, dbsettings.REDIS_DB)classRecommender:defget_product_key(self, product_id):returnproduct:{}:purchased_with.format(product_id)defproducts_bought(self, products):product_ids [p.idfor p in products]# 针对订单里的每一个商品将其他商品在当前商品的有序集合中增加1for product_id in product_ids:for with_id in product_ids:if product_id ! with_id:r.zincrby(self.get_product_key(product_id), with_id, amount1)这个Recommender类用来存储订单购买时的相关信息和根据一个指定的对象获取相关的推荐。get_product_key()方法获取一个Product对象的id然后创建对应的有序集合其中的键看起来像这样product:[id]:purchased_with。product_bought()方法接受属于同一个订单的Product对象的列表然后做如下操作获取所有Product对象的ID针对每一个ID遍历一次全部的ID跳过内外循环ID相同的部分这样就针对其中每个商品都遍历了与其一同购买的商品使用get_product_id()方法得到每个商品的Redis键名。例如针对ID为33的商品返回的键名是product:33:purchased_with这个键将用于操作有序集合在该商品对应的有序序列将同一订单内的其他商品的分数增加1我们现在有了一个保存商品相关信息的方法。还需要一个方法来从Redis中获得推荐的商品继续编写Recommender类增加suggest_products_for()方法CopyclassRecommender:# ......defsuggest_products_for(self, products, max_results6):product_ids [p.idfor p in products]# 如果当前列表只有一个商品iflen(product_ids) 1:suggestions r.zrange(self.get_product_key(product_ids[0]), 0, -1, descTrue)[:max_results]else:# 生成一个临时的key用于存储临时的有序集合flat_ids .join([str(id) foridin product_ids])tmp_key tmp_{}.format(flat_ids)# 对于多个商品取所有商品的键名构成keys列表keys [self.get_product_key(id) foridin product_ids]# 合并有序集合到临时键r.zunionstore(tmp_key, keys)# 删除与当前列表内商品相同的键。r.zrem(tmp_key, *product_ids)# 获得排名结果suggestions r.zrange(tmp_key, 0, -1, descTrue)[:max_results]# 删除临时键r.delete(tmp_key)# 获取关联商品并通过相关性排序suggested_products_ids [int(id) foridin suggestions]suggested_products list(Product.objects.filter(id__insuggested_products_ids))suggested_products.sort(keylambda x: suggested_products_ids.index(x.id))return suggested_productssuggest_products_for()方法接受两个参数products表示为哪些商品进行推荐可以包含一个或多个商品max_results整数值表示最大推荐几个商品在这个方法里我们做了如下的事情获取所有Product对象的ID如果仅有一个商品直接查询这个id对应的有序集合按降序返回结果。为了实现查询使用了Redis的ZRANGE命令。我们使用max_results属性指定返回的最大数量。如果商品数量多于1个通过ID创建一个临时键名。通过Redis的ZUNIONSTORE命令合并所有商品的有序集合。ZUNIONSTORE合并所有的有序集合中相同键的分数然后将新生成的有序集合存入临时键。关于该命令可以参考https://redis.io/commands/ZUNIONSTORE。由于已经在当前购物车内的商品无需被推荐因此使用ZREM命令从临时键的有序集合中删除与当前订单内商品id相同的键。从临时键中获取商品ID使用ZRANGE命令按照分数排序通过max_results控制返回数量之后删除临时键。根据ID获取Product对象然后按照与取出的ID相同的顺序进行排列。为了更加实用再给Recommender类添加一个清除推荐商品的方法CopyclassRecommender:# ......defclear_purchases(self):foridin Product.objects.values_list(id, flatTrue):r.delete(self.get_product_key(id))我们来测试一下推荐引擎是否正常工作。确保Product数据表中有一些商品信息然后先启动RedisCopysrc/redis-server通过python manage.py shell进入带有Django项目环境的shell中Copyfrom shop.models import Product black_tea Product.objects.get(translations__nameBlack tea) red_tea Product.objects.get(translations__nameRed tea) green_tea Product.objects.get(translations__nameGreen tea) tea_powder Product.objects.get(translations__nameTea powder)之后增加一些测试购买数据Copyfrom shop.recommender import Recommender r Recommender() r.products_bought([black_tea, red_tea]) r.products_bought([black_tea, green_tea]) r.products_bought([red_tea, black_tea, tea_powder]) r.products_bought([green_tea, tea_powder]) r.products_bought([black_tea, tea_powder]) r.products_bought([red_tea, green_tea])进行完上述操作后我们实际为四个商品保存的有序集合是Copyblack_tea: red_tea (2), tea_powder (2), green_tea (1) red_tea: black_tea (2), tea_powder (1), green_tea (1) green_tea: black_tea (1), tea_powder (1), red_tea(1) tea_powder: black_tea (2), red_tea (1), green_tea (1)下边测试一下通过翻译字段获取推荐商品信息Copy from django.utils.translation import activateactivate(en)r.suggest_products_for([black_tea]) [Product: Tea powder, Product: Red tea, Product: Green tea]r.suggest_products_for([red_tea]) [Product: Black tea, Product: Tea powder, Product: Green tea]r.suggest_products_for([green_tea]) [Product: Black tea, Product: Tea powder, Product: Red tea]r.suggest_products_for([tea_powder]) [Product: Black tea, Product: Red tea, Product: Green tea]如果看到商品是按照它们的分数进行降序排列的就说明引擎工作正常了。再测试一下多个商品的推荐Copy r.suggest_products_for([black_tea, red_tea]) [Product: Tea powder, Product: Green tea]r.suggest_products_for([green_tea, red_tea]) [Product: Black tea, Product: Tea powder]r.suggest_products_for([tea_powder, black_tea]) [Product: Red tea, Product: Green tea]可以实际计算一下是否符合合并有序集合后的结果例如针对第一条程序tea_powder的分数是21green_tea的分数是11等测试之后说明我们的推荐算法正常工作下一步就是将该功能集成到站点中在商品详情页和购物车清单页进行展示。先修改shop应用的views.py文件中的product_detail视图Copyfrom .recommender import Recommenderdefproduct_detail(request, id, slug):language request.LANGUAGE_CODEproduct get_object_or_404(Product, idid, translations__language_codelanguage, translations__slugslug,availableTrue)cart_product_form CartAddProductForm()r Recommender()recommended_products r.suggest_products_for([product], 4)return render(request, shop/product/detail.html, {product: product, cart_product_form: cart_product_form,recommended_products: recommended_products})编辑shop/product/detail.html模板增加下列代码到{{ product.description|linebreaks }}之后Copy{% if recommended_products %}divclassrecommendationsh3{% trans People who bought this also bought %}/h3{% for p in recommended_products %}divclassitemahref{{ p.get_absolute_url }}imgsrc{% if p.image %}{{ p.image.url }}{% else %}{% static img/no_image.png %}{% endif %}/apahref{{ p.get_absolute_url }}{{ p.name }}/a/p/div{% endfor %}/div {% endif %}然后运行站点点击商品进入详情页可以看到类似下图的商品推荐我们还需要在购物车详情页增加推荐功能编辑cart应用的views.py文件中的cart_detail视图Copyfrom shop.recommender import Recommenderdefcart_detail(request):cart Cart(request)for item in cart:item[update_quantity_form] CartAddProductForm(initial{quantity: item[quantity], update: True})coupon_apply_form CouponApplyForm()r Recommender()cart_products [item[product] for item in cart]recommended_products r.suggest_products_for(cart_products, max_results4)return render(request, cart/detail.html,{cart: cart, coupon_apply_form: coupon_apply_form, recommended_products: recommended_products})然后修改对应的模板 cart/detail.html在 /table 之后增加下列代码Copy{% if recommended_products %}divclassrecommendations carth3{% trans People who bought this also bought %}/h3{% for p in recommended_products %}divclassitemahref{{ p.get_absolute_url }}imgsrc{% if p.image %}{{ p.image.url }}{% else %}{% static img/no_image.png %}{% endif %}/apahref{{ p.get_absolute_url }}{{ p.name }}/a/p/div{% endfor %}/div {% endif %}译者注由于上述内容使用了{% trans %}模板标签不要忘记在页面上方加入{% load i18n %}原书这里没有加会导致报错。在浏览器中打开http://127.0.0.1:8000/en/。将一些商品加入购物车然后至http://127.0.0.1:8000/en/cart/查看购物车详情可以看到出现了推荐商品现在我们就使用Redis配合Django完成了一个推荐系统。译者注原书其实并没有将功能写完。可以发现目前的购买数据调用Recommender类的products_bought()方法是在我们测试的时候通过命令行添加的而不是通过网站功能自动添加。按照一开始的分析应该在付款成功的时候更新Redis的数据。需要在payment应用的views.py文件中在payment_process视图中付款响应成功保存交易id和paid字段之后发送PDF发票之前添加如下代码Copyfrom shop.recommender import Recommenderdefpayment_process(request):......if request.method POST:......if result.is_success:order.paid Trueorder.braintree_id result.transaction.idorder.save()# 更新Redis中本次购买的商品分数r Recommender()order_items [order_item.product for order_item in order.items.all()]r.products_bought(order_items)总结在这一章学习了创建优惠码系统和国际化与本地化配置工作。还基于Redis创建了一个商品推荐系统。在下一章我们将创建一个新的项目在线教育平台里边将使用Django的CBV技术还会创建一个内容管理系统。如有不懂还要咨询下方小卡片博主也希望和志同道合的测试人员一起学习进步在适当的年龄选择适当的岗位尽量去发挥好自己的优势。我的自动化测试开发之路一路走来都离不每个阶段的计划因为自己喜欢规划和总结测试开发视频教程、学习笔记领取传送门
http://www.dnsts.com.cn/news/156145.html

相关文章:

  • 东莞推广网站排名互动企业展厅设计公司
  • 互联网网站建设手机html编程软件app
  • 百色建设局网站网站建设架构细节
  • 医疗网站建设新闻做企业网站有效果吗
  • 做好网站维护管理做网站做地区好还是全国的好处
  • 建立电子商务网站做进行网站推广赚钱
  • wordpress英文企业网站模板h5建站网站
  • 餐饮公司网站建设的特点wordpress 管理员权限设置密码
  • 微信小程序api接口优化培训课程
  • 如何建设英文网站安吉哪里做网站好
  • 如何设计网站首页导航中文域名注册官网
  • 电商网站建设计入什么科目厚街网站建设价格
  • 网站建设服务范围c#网站开发+pdf
  • 专做外贸库存的网站销售管理软件系统
  • 河南网站建设的详细策划影视剪辑培训班
  • 北京大龙建设集团有限公司网站首页福建住房和城乡建设厅网站一体化平台
  • 芜湖做网站的公司排名温州专业网站开发网站设计
  • 公司做网站最低需用多少钱wordpress源代码在哪里
  • 住房和建设部网站共享空间网站开发公司
  • 免费php网站系统网站设计的目的和意义
  • 每月网站流量杭州制作网站个人
  • 给别人做网站挣钱吗wordpress搭建注册会员
  • 正能量软件不良网站下载什么网站广告做多
  • 三河市建设厅公示网站icp网站备案号查询
  • 有域名有空间怎么做网站公司网站制作仿站
  • 国家网站后缀网络公司开发网站
  • 藁城住房和城乡建设局网站在线文字生成图片
  • 淄博临淄建设局网站建设一个网站怎么赚钱
  • 英文外贸网站源码“一个”网站
  • 网站建设 采集网络营销推广软件