书籍出处:https://www.packtpub.com/web-development/django-example
原做者:Antonio Melécss
(译者@ucag 注:哈哈哈,第九章终于来啦。这是在线商店的最后一章,下一章将会开始一个新的项目。因此这一章的内容会偏难,最难的是推荐引擎的编写,其中的算法可能还须要各位好好的推敲,若是看了中文的看不懂,你们能够去看看英文原版的书以及相关的文档。最近我也在研究机器学习,有兴趣的你们能够一块儿交流哈~)html
(审校@夜夜月:你们好,我是来打酱油的~,粗校,主要更正了一些错字和格式,精校订进行到四章样子。)python
在上一章中,你学习了如何把支付网关整合进你的商店。你处理了支付通知,学会了如何生成 CSV 和 PDF 文件。在这一章中,你会把优惠券添加进你的商店中。你将学到国际化(internationalization)和本地化(localization)是如何工做的,你还会建立一个推荐引擎。
在这一章中将会包含一下知识点:git
不少的在线商店会送出不少优惠券,这些优惠券能够在顾客的采购中兑换为相应的折扣。在线优惠券一般是由一串给顾客的代码构成,这串代码在一个特定的时间段内是有效的。这串代码能够被兑换一次或者屡次。github
咱们将会为咱们的商店建立一个优惠券系统。优惠券将会在顾客在某一个特定的时间段内输入时生效。优惠券没有任何兑换数的限制,他们也可用于购物车的总金额中。对于这个功能,咱们将会建立一个模型(model)来储存优惠券代码,优惠券有效的时间段,以及折扣的力度。web
在 myshop
项目内使用以下命令建立一个新的应用:正则表达式
python manage.py startapp coupons
编辑 myshop
的 settings.py
文件,像下面这样把把应用添加到 INSTALLED_APPS
中:redis
INSTALLED_APPS = ( # ... 'coupons', )
新的应用已经在咱们的 Django 项目中激活了。算法
让咱们开始建立 Coupon
模型(model)。编辑 coupons
应用中的 models.py
文件,添加如下代码:shell
from django.db import models from django.core.validators import MinValueValidator,\ MaxValueValidator class Coupon(models.Model): code = models.CharField(max_length=50, unique=True) 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
咱们将会用这个模型(model)来储存优惠券。 Coupon
模型(model)包含如下几个字段:
code
:用户必需要输入的代码来将优惠券应用到他们购买的商品中valid_from
:表示优惠券会在什么时候生效的时间和日期值valid_to
:表示优惠券会在什么时候过时discount
:折扣率(这是一个百分比,因此它的值的范围是 0 到 1000)。咱们使用验证器来限制接收的最小值和最大值active
:表示优惠券是否激活的布尔值执行下面的命令来为 coupons
生成首次迁移:
python manage.py makemigrations
输出应该包含如下这几行:
Migrations for 'coupons': 0001_initial.py: - Create model Coupon
以后咱们执行下面的命令来应用迁移:
python manage.py migrate
你能够看见包含下面这一行的输出:
Applying coupons.0001_initial... OK
迁移如今已经被应用到了数据库中。让咱们把 Coupon
模型(model)添加到管理站点。编辑 coupons
应用的 admin.py
文件,添加如下代码:
from django.contrib import admin from .models improt Coupon class CouponAdmin(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)
Coupon
模型(model)如今已经注册进了管理站点中。确保你已经用命令 python manage.py runserver
打开了开发服务器。访问 http://127.0.0.1:8000/admin/coupons/add 。你能够看见下面的表单:
填写表单建立一个在当前日期有效的新优惠券,确保你点击了 Active 复选框,而后点击 Save按钮。
咱们能够保存新的优惠券以及检索目前的优惠券。如今咱们须要一个方法来让顾客能够应用他们的优惠券到他们购买的产品中。花点时间来想一想这个功能该如何实现。应用一张优惠券的流程以下:
1. 用户将产品添加进购物车 2. 用户在购物车详情页的表单中输入优惠代码 3. 当用户输入优惠代码而后提交表单时,咱们查找一张和所给优惠代码相符的有效优惠券。咱们必须检查用户输入的优惠券代码, `active` 属性为 `True` ,当前时间位于 `valid_from 和 `valid_to` 之间。 4. 若是查找到了相应的优惠券,咱们把它保存在用户会话中,而后展现包含折扣了的购物车以及更新总价。 5. 当用户下单时,咱们把优惠券保存到所给的订单中。
在 coupons
应用路径下建立一个新的文件,命名为 forms.py
文件,添加如下代码:
from django import forms class CouponApplyForm(forms.Form): code = forms.CharField()
咱们将会用这个表格来让用户输入优惠券代码。编辑 coupons
应用中的 views.py
文件,添加如下代码:
from 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 CouponApplyForm @require_POST def coupon_apply(request): now = timezone.now() form = CouponApplyForm(request.POST) if form.is_valid(): code = form.cleaned_data['code'] try: coupon = Coupon.objects.get(code__iexact=code, valid_from__lte=now, valid_to__gte=now, active=True) request.session['coupon_id'] = coupon.id except Coupon.DoesNotExist: request.session['coupon_id'] = None return redirect('cart:cart_detail')
coupon_apply
视图(view)验证优惠券而后把它保存在用户会话(session)中。咱们使用 require_POST
装饰器来限制这个视图(view)仅接受 POST 请求。在视图(view)中,咱们执行了如下几个任务:
1.咱们用上传的数据实例化了 `CouponApplyForm` 而后检查表单是否合法。 2. 若是表单是合法的,咱们就从表单的 `cleaned_data` 字典中获取 `code` 。咱们尝试用所给的代码检索 `Coupon` 对象。咱们使用 `iexact` 字段来对照查找大小写不敏感的精确匹配项。优惠券在当前必须是激活的(`active=True`)以及必须在当前日期内是有效的。咱们使用 Django 的 `timezone.now()` 函数来得到当前的时区识别时间和日期(time-zone-aware) 而后咱们把它和 `valid_from` 和 `valid_to` 字段作比较,对这两个日期分别执行 `lte` (小于等于)运算和 `gte` (大于等于)运算来进行字段查找。 3. 咱们在用户的会话中保存优惠券的 `id``。 4. 咱们把用户重定向到 `cart_detail` URL 来展现应用了优惠券的购物车。
咱们须要一个 coupon_apply
视图(view)的 URL 模式。在 coupon
应用路径下建立一个新的文件,命名为 urls.py
,添加如下代码:
from django.conf.urls import url from . import views urlpatterns = [ url(r'^apply/$', views.coupon_apply, name='apply'), ]
而后,编辑 myshop
项目的主 urls.py
文件,引入 coupons
的 URL 模式:
url(r'^coupons/', include('coupons.urls', namespace='coupons')),
记得把这个放在 shop.urls
模式以前。
如今编辑 cart
应用的 cart.py
,包含如下导入:
from coupons.models import Coupon
把下面这行代码添加进 Cart
类的 __init__()
方法中来从会话中初始化优惠券:
# store current applied coupon self.coupon_id = self.session.get('coupon_id')
在这行代码中,咱们尝试从当前会话中获得 coupon_id
会话键,而后把它保存在 Cart
对象中。把如下方法添加进 Cart
对象中:
@property def coupon(self): if self.coupon_id: return Coupon.objects.get(id=self.coupon_id) return None def get_discount(self): if self.coupon: return (self.coupon.discount / Decimal('100')) \ * self.get_total_price() return Decimal('0') def get_total_price_after_discount(self): return self.get_total_price() - self.get_discount()
下面是这几个方法:
coupon()
:咱们定义这个方法做为 property
。若是购物车包含 coupon_id
函数,就会返回一个带有给定 id
的Coupon
对象get_discount()
:若是购物车包含 coupon
,咱们就检索它的折扣比率,而后返回购物车中被扣除折扣的总和。get_total_price_after_discount()
:返回被减去折扣以后的总价。Cart
类如今已经准备好处理应用于当前会话的优惠券了,而后将它应用于相应折扣中。
让咱们在购物车详情视图(view)中引入优惠券系统。编辑 cart
应用的 views.py
,而后把下面这一行添加到顶部:
from coupons.forms import CouponApplyForm
以后,编辑 cart_detail
视图(view),而后添加新的表单:
def cart_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
应用的 acrt/detail.html
文件,找到下面这几行:
<tr class="total"> <td>Total</td> <td colspan="4"></td> <td class="num">${{ cart.get_total_price }}</td> </tr>
把它们换成如下几行:
{% if cart.coupon %} <tr class="subtotal"> <td>Subtotal</td> <td colspan="4"></td> <td class="num">${{ cart.get_total_price }}</td> </tr> <tr> <td> "{{ cart.coupon.code }}" coupon ({{ cart.coupon.discount }}% off) </td> <td colspan="4"></td> <td class="num neg"> - ${{ cart.get_discount|floatformat:"2" }} </td> </tr> {% endif %} <tr class="total"> <td>Total</td> <td colspan="4"></td> <td class="num"> ${{ cart.get_total_price_after_discount|floatformat:"2" }} </td> </tr>
这段代码用于展现可选优惠券以及折扣率。若是购物车中有优惠券,咱们就在第一行展现购物车的总价做为 小计。而后咱们在第二行展现当前可应用于购物车的优惠券。最后,咱们调用 cart
对象的 get_total_price_after_discount()
方法来展现折扣了的总价格。
在同一个文件中,在 </table>
标签以后引入如下代码:
<p>Apply a coupon:</p> <form action="{% url "coupons:apply" %}" method="post"> {{ coupon_apply_form }} <input type="submit" value="Apply"> {% csrf_token %} </form>
咱们将会展现一个表单来让用户输入优惠券代码,而后将它应用于当前的购物车当中。
访问 http://127.0.0.1:8000 ,向购物车当中添加一个商品,而后在表单中输入你建立的优惠代码来应用你的优惠券。你能够看到购物车像下面这样展现优惠券折扣:
让咱们把优惠券添加到购物流程中的下一步。编辑 orders
应用的 orders/order/create.html
模板(template),找到下面这几行:
<ul> {% for item in cart %} <li> {{ item.quantity }}x {{ item.product.name }} <span>${{ item.total_price }}</span> </li> {% endfor %} </ul>
把它替换为下面这几行:
<ul> {% for item in cart %} <li> {{ item.quantity }}x {{ item.product.name }} <span>${{ item.total_price }}</span> </li> {% endfor %} {% if cart.coupon %} <li> "{{ cart.coupon.code }}" ({{ cart.coupon.discount }}% off) <span>- ${{ cart.get_discount|floatformat:"2" }}</span> </li> {% endif %} </ul>
订单汇总如今已经包含使用了的优惠券,若是有优惠券的话。如今找到下面这一行:
<p>Total: ${{ cart.get_total_price }}</p>
把他们换成如下一行:
<p>Total: ${{ cart.get_total_price_after_discount|floatformat:"2" }}</p>
这样作以后,总价也将会在减去优惠券折扣被计算出来。
访问 http://127.0.0.1:8000/orders/create/ 。你会看到包含使用了优惠券的订单小计:
用户如今能够在购物车当中使用优惠券了。尽管,当用户结帐时,咱们依然须要在订单中储存优惠券信息。
咱们会储存每张订单中使用的优惠券。首先,咱们须要修改 Order
模型(model)来储存相关联的 Coupon
对象,若是有这个对象的话。
编辑 orders
应用的 models.py
文件,添加如下代码:
from decimal import Decimal from django.core.validators import MinValueValidator, \ MaxValueValidator from coupons.models import Coupon
而后,把下列字段添加进 Order
模型(model)中:
coupon = models.ForeignKey(Coupon, related_name='orders', null=True, blank=True) discount = models.IntegerField(default=0, validators=[MinValueValidator(0), MaxValueValidator(100)])
这些字段让用户能够在订单中储存可选的优惠券信息,以及优惠券的相应折扣。折扣被存在关联的 Coupon
对象中,可是咱们在 Order
模型(model)中引入它以便咱们在优惠券被更改或者删除时好保存它。
由于 Order
模型(model)已经被改变了,咱们须要建立迁移。执行下面的命令:
python manage.py makemigrations
你能够看到以下输出:
Migrations for 'orders': 0002_auto_20150606_1735.py: - Add field coupon to order - Add field discount to order
用下面的命令来执行迁移:
python manage.py migrate orders
你必需要确保新的迁移已经被应用了。 Order
模型(model)的字段变动如今已经同步到了数据库中。
回到 models.py
文件中,按照以下更改 Order
模型(model)的 get_total_cost()
方法:
def get_total_cost(self): total_cost = sum(item.get_cost() for item in self.items.all()) return total_cost - total_cost * \ (self.discount / Decimal('100'))
Order
模型(model)的 get_total_cost()
如今已经把使用了的折扣包含在内了,若是有折扣的话。
编辑 orders
应用的 views.py
文件,更改 order_create
视图(view)以便在建立新的订单时保存相关联的优惠券和折扣。找到下面这一行:
order = form.save()
把它替换为下面这几行:
order = form.save(commit=False) if cart.coupon: order.coupon = cart.coupon order.discount = cart.coupon.discount order.save()
在新的代码中,咱们使用 OrderCrateForm
表单的 save()
方法建立了一个 Order
对象。咱们使用 commit=False
来避免将它保存在数据库中。若是购物车当中有优惠券,咱们就会保存相关联的优惠券和折扣。而后才把 order
对象保存到数据库中。
确保用 python manage.py runserver
运行了开发服务器。使用 ./ngrok http:8000
命令来启动 Ngrok 。
在你的浏览器中打开 Ngrok 提供的 URL , 而后用用你建立的优惠券完成一次购物。当你完成一次成功的支付后,有能够访问 http://127.0.0.1:8000/admin/orders/order/ ,检查 order
对象是否包含了优惠券和折扣,以下:
你也能够更改管理界面的订单详情模板(template)和订单的 PDF 帐单,以便用展现购物车的方式来展现使用了的优惠券。
下面。咱们将会为咱们的项目添加国际化(internationalization)。
Django 提供了完整的国际化和本地化的支持。这使得你能够把你的项目翻译为多种语言以及处理地区特性的 日期,时间,数字,和时区 的格式。让咱们一块儿来搞清楚国际化和本地化的区别。国际化(一般简写为:i18n)是为让软件适应潜在的不一样语言和多种语言的使用作的处理,这样它就不是以某种特定语言或某几种语言为硬编码的软件了。本地化(简写为:l10n)是对软件的翻译以及使软件适应某种特定语言的处理。Django 自身已经使用自带的国际化框架被翻译为了50多种语言。
国际化框架让你能够容易的在 Python 代码和模板(template)中标记须要翻译的字符串。它依赖于 GNU 文本获取集来生成和管理消息文件。消息文件是一个表示一种语言的纯文本文件。它包含某一语言的一部分或者全部的翻译字符串。消息文件有 .po
扩展名。
一旦翻译完成,信息文件就会被编译一遍快速的链接到被翻译的字符串。编译后的翻译文件的扩展名为 .mo
。
Django 提供了几种国际化的设置。下面是几种最有关联的设置:
USE_I18N
:布尔值。用于设定 Django 翻译系统是否启动。默认值为 True
。USE_L10N
:布尔值。表示本地格式化是否启动。当被激活式,本地格式化被用于展现日期和数字。默认为 False
。USE_TZ
:布尔值。用于指定日期和时间是不是时区别(timezone-aware)。LANGUAGE_CODE
:项目的默认语言。在标准的语言 ID 格式中,好比,en-us
是美式英语,en-gb
是英式英语。这个设置要 USE_I18N
为 True
才能生效。你能够在这个网站找到一个合法的语言 ID 表:http://www.i18nguy.com/unicode/language-identifiers.html
。LANGUAGES
:包含项目可用语言的元组。它们由包含 语言代码 和 语言名字 的双元组构成的。你能够在 django.conf.global_settions
里看到可用语言的列表。当你选择你的网站将会使用哪种语言时,你就把 LANGUAGES
设置为那个列表的子列表。LOCAL_PATHS
:Django 寻找包含翻译的信息文件的路径列表。TIME_ZONE
:表明项目时区的字符串。当你使用 startproject
命令建立新项目时它被设置为 UTC
。你也能够把它设置为其余的时区,好比 Europe/Madrid
。这是一些可用的国际化和本地化的设置。你能够在这个网站找到所有的(设置)列表: https://docs.djangoproject.com/en/1.8/ref/settings/#globalization-i18n-l10n 。
Django 使用 manage.py
或者 django-admin.py
工具包管理翻译,包含如下命令:
makemessages
:运行于源代码树中,寻找全部被用于翻译的字符串,而后在 locale
路径中建立或更新 .po
信息文件。每一种语言建立一个单一的 .po
文件。compilemessages
: 把扩展名为 .po
的信息文件编译为用于检索翻译的 .mo
文件。你须要文本获取工具集来建立,更新,以及编译信息文件。大部分的 Linux 发行版都包含文本获取工具集。若是你在使用 Mac OS X,用 Honebrew (http://brew.sh
)是最简单的安装它的方法,使用如下命令 :brew install gettext
。你或许也须要将它和命令行强制链接 brew link gettext --force
。对于 Windows ,安装步骤以下 : https://docs.djangoproject.com/en/1.8/topics/i18n/translation/#gettext-on-windows 。
让咱们看看国际化项目的过程。咱们须要像下面这样作:
1. 咱们在 Python 代码和模板(template)中中标记须要翻译的字符串。 2. 咱们运行 `makemessages` 命令来建立或者更新包含全部翻译字符串的信息文件。 3. 咱们翻译包含在信息文件中的字符串,而后使用 `compilemessages` 编译他们。
Django 配备有一个基于请求数据的中间件,这个中间件用于决定当前语言是什么。这个中间件是位于 django.middleware.locale.localMiddleware
的 LocaleMiddleware
,它执行下面的任务:
1. 若是使用 `i18_patterns` —— 这是你使用的被翻译的 URL 模式,它在被请求的 URL 中寻找一个语言前缀来决定当前语言。 2. 若是没有找到语言前缀,就会在当前用户会话中寻找当前的 `LANGUAGE_SESSION_KEY` 。 3. 若是在会话中没有设置语言,就会寻找带有当前语言的 cookie 。这个 cookie 的定制名可在 `LANGUAGE_COOKIE_NAME` 中设置。默认地,这个 cookie 的名字是 `django_language` 。 4. 若是没有找到 cookie,就会在请求 HTTP 头中寻找 `Accept-Language` 。 5. 若是 `Accept-Language` 头中没有指定语言,Django 就使用在 `LANGUAGE_CODE` 设置中定义的语言。
默认的,Django 会使用 LANGUAGE_OCDE
中设置的语言,除非你正在使用 LocaleMiddleware
。上述操做进会在使用这个中间件时生效。
让咱们的项目准备好使用不一样的语言吧。咱们将要为咱们的商店建立英文版和西班牙语版。编辑项目中的 settings.py
文件,添加下列 LANGUAGES
设置。把它放在 LANGUAGE_OCDE
旁边:
LANGUAGES = ( ('en', 'English'), ('es', 'Spanish'), )
LANGUAGES
设置包含两个由语言代码和语言名的元组构成,好比 en-us
或 en-gb
,或者通常的设置为 en
。这样设置以后,咱们指定咱们的应用只会对英语和西班牙语可用。若是咱们不指定 LANGUAGES
设置,网站将会对全部 Django 的翻译语言有效。
确保你的 LANGUAGE_OCDE
像以下设置:
LANGUAGE_OCDE = 'en'
把 django.middleware.locale.LocaleMiddleware
添加到 MIDDLEWARE_CLASSES
设置中。确保这个设置在 SessionsMiddleware
以后,由于 LocaleMiddleware
须要使用会话数据。它一样必须放在 CommonMiddleware
以前,由于后者须要一种激活了的语言来解析请求 URL 。MIDDLEWARE_CLASSES
设置看起来应该以下:
MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', # ... )
中间件的顺序很是重要,由于每一个中间件所依赖的数据多是由其余中间件处理以后的才得到的。中间件按照
MIDDLEWARE_CLASSES
的顺序应用在每一个请求上,以及反序应用于响应中。
在主项目路径下,在 manage.py
文件同级,建立如下文件结构:
locale/ en/ es/
locale
路径是放置应用信息文件的地方。再次编辑 settings.py
,而后添加如下设置:
LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale/'), )
LOCALE_PATHS
设置指定了 Django 寻找翻译文件的路径。第一个路径有最高优先权。
当你在你的项目路径下使用 makemessages
命令时,信息文件将会在 locale
路径下生成。尽管,对于包含 locale
路径的应用,信息文件就会保存在这个应用的 locale
路径中。
咱们翻译在 Python 代码中的字母,你可使用 django.utils.translation
中的 gettext()
函数来标记须要翻译的字符串。这个函数翻译信息而后返回一个字符串。约定俗成的用法是导入这个函数后把它命名为 _
(下划线)。
你能够在这个网站找到全部关于翻译的文档 : https://docs.djangoproject.com/en/1.8/topics/i18n/translation/ 。
下面的代码展现了如何标记一个翻译字符串:
from django.utils.translation import gettext as _ output = _('Text to be translated.')
Django 对全部的翻译函数引入了惰性(lazy)版,这些惰性函数都带有后缀 _lazy()
。当使用惰性函数时,字符串在值被链接时就会被翻译,而不是在被调用时翻译(这也是它为何被惰性翻译的缘由)。惰性函数早晚会派上用场,特别是当标记字符串在模型(model)加载的执行路径中时。
使用
gettext_lazy()
而不是gettext()
,字符串就会在链接到值的时候被翻译而不会在函数调用的时候被翻译。Django 为全部的翻译都提供了惰性版本。
标记的翻译字符串能够包含占位符来引入翻译中的变量。下面就是一个带有占位符的翻译字字符串的例子:
from 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' 。当你的翻译字符串有多于一个参数时,咱们老是使用字符串插值来代替位置插值
对于复数形式,你可使用 gettext()
和 gettext_lazy()
。这些函数基于一个能够表示对象数量的参数来翻译单数和复数形式。下面这个例子展现了如何使用它们:
output = ngettext('there is %(count)d product', 'there are %(count)d products', count) % {'count': count}
如今你已经基本知道了如何在你的 Python 代码中翻译字符了,如今是把翻译应用到项目中的时候了。
编辑项目中的 settings.py
,导入 gettext_lazy()
函数,而后像下面这样修改 LANGUAGES
的设置:
from django.utils.translation import gettext_lazy as _ LANGUAGES = ( ('en', _('English')), ('es', _('Spanish')), )
咱们使用 gettext_lazy()
而不是 gettext()
来避免循环导入,这样就能够在语言名被链接时就翻译它们。
在你的项目路径下执行下面的命令:
django-admin makemessages --all
你能够看到以下输出:
processing locale es processing locale en
看下 locale/
路径。你能够看到以下文件结构:
en/ LC_MESSAGES/ django.po es/ LC_MESSAGES/ django.po
.po
文件已经为每个语言建立好了。用文本编辑器打开 es/LC_MESSAGES/django.po
。在文件的末尾,你能够看到以下几行:
#: settings.py:104 msgid "English" msgstr "" #: settings.py:105 msgid "Spanish" msgstr ""
每个翻译字符串前都有一个显示该文件详情的注释以及它在哪一行被找到。每一个翻译都包含两个字符串:
msgid
:在源代码中的翻译字符串msgstr
:对应语言的翻译,默认为空。这是你输入所给字符串翻译的地方。按照以下,根据所给的 msgid
在 msgtsr
中填写翻译:
#: settings.py:104 msgid "English" msgstr "Inglés" #: settings.py:105 msgid "Spanish" msgstr "Español"
保存修改后的信息文件,打开 shell ,运行下面的命令:
django-admin compilemessages
若是一切顺利,你能够看到像以下的输出:
processing file django.po in myshop/locale/en/LC_MESSAGES processing file django.po in myshop/locale/es/LC_MESSAGES
输出给出了被编译的信息文件的相关信息。再看一眼 locale
路径的 myshop
。你能够看到下面的文件:
en/ LC_MESSAGES/ django.mo django.po es/ LC_MESSAGES/ django.mo django.po
你能够看到每一个语言的 .mo
的编译文件已经生成了。
咱们已经翻译了语言自己的名字。如今让咱们翻译展现在网站中模型(model)字段的名字。编辑 orders
应用的 models.py
,为 Order
模型(model)添加翻译的被标记名:
from django.utils.translation import gettext_lazy as _ class Order(models.Model): first_name = models.CharField(_('first name'), max_length=50) last_name = models.CharField(_('last name'), max_length=50) email = models.EmailField(_('e-mail'),) address = models.CharField(_('address'), max_length=250) postal_code = models.CharField(_('postal code'), max_length=20) city = models.CharField(_('city'), max_length=100) #...
咱们添加为当用户下一个新订单时展现的字段添加了名字,它们分别是 first_name
,last_name
, email
, address
, postal_code
,city
。记住,你也可使用每一个字段的 verbose_name
属性。
在 orders
应用路径内建立如下路径:
locale/ en/ es/
经过建立 locale
路径,这个应用的翻译字符串就会储存在这个路径下的信息文件里,而不是在主信息文件里。这样作以后,你就能够为每一个应用生成独自的翻译文件。
在项目路径下打开 shell ,运行下面的命令:
django-admin makemessages --all
你能够看到以下输出:
processing locale es processing locale en
用文本编辑器打开 es/LC_MESSAGES/django.po
文件。你将会看到每个模型(model)的翻译字符串。为每个所给的 msgid
字符串填写 msgstr
的翻译:
#: orders/models.py:10 msgid "first name" msgstr "nombre" #: orders/models.py:12 msgid "last name" msgstr "apellidos" #: orders/models.py:14 msgid "e-mail" msgstr "e-mail" #: orders/models.py:15 msgid "address" msgstr "dirección" #: orders/models.py:17 msgid "postal code" msgstr "código postal" #: orders/models.py:19 msgid "city" msgstr "ciudad"
在你添加完翻译以后,保存文件。
在文本编辑器内,你可使用 Poedit 来编辑翻译。 Poedit 是一个用来编辑翻译的软件,它使用 gettext 。它有 Linux ,Windows,Mac OS X 版,你能够在这个网站下载 Poedit : http://poedit.net/ 。
让咱们来翻译项目中的表单吧。 orders
应用的 OrderCrateForm
尚未被翻译,由于它是一个 ModelForm
,使用 Order
模型(model)的 verbose_name
属性做为每一个字段的标签。咱们将会翻译 cart
和 coupons
应用的表单。
编辑 cart
应用路径下的 forms.py
文件,给 CartAddProductForm
的 quantity
字段添加一个 label
属性,而后按照以下标记这个须要翻译的字段:
from django import forms from django.utils.translation import gettext_lazy as _ PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)] class CartAddProductForm(forms.Form): quantity = forms.TypedChoiceField( choices=PRODUCT_QUANTITY_CHOICES, coerce=int, label=_('Quantity')) update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
编辑 coupons
应用的 forms.py
,按照以下翻译 CouponApplyForm
:
from django import forms from django.utils.translation import gettext_lazy as _ class CouponApplyForm(forms.Form): code = forms.CharField(label=_('Coupon'))
Django 提供了 {% trans %}
和 {% blocktrans %}
模板(template)标签来翻译模板(template)中的字符串。为了使用翻译模板(template)标签,你须要在你的模板(template)顶部添加 {% load i18n %}
来载入她们。
{% trans %}
模板(template)标签让你能够标记须要翻译的字符串,常量,或者是参数。在内部,Django 对所给文本执行 gettext()
。这是如何标记模板(template)中的翻译字符串:
{% trans "Text to be translated" %}
你可以使用 as
来储存你在模板(template)内使用的全局变量里的被翻译内容。下面这个例子保存了一个变量中叫作 greeting
的被翻译文本:
{% trans "Hello!" as greeting %} <h1>{{ greeting }}</h1>
{% trans %}
标签对于简单的翻译字符串是颇有用的,可是它不能包含变量的翻译内容。
{% blocktrans %}
模板(template)标签让你能够标记含有占位符的变量和字符的内容。线面这个例子展现了如何使用 {% blocktrans %}
标签来标记包含一个 name
变量的翻译内容:
{% blocktrans %}Hello {{ name }}!{% endblocktrans %}
你能够用 with
来引入模板(template)描述,好比链接对象的属性或者应用模板(template)的变量过滤器。你必须为他们用占位符。你不能在 blocktrans
内链接任何描述或者对象属性。下面的例子展现了如何使用 with
来引入一个应用了 capfirst
过滤器的对象属性:
{% blocktrans with name=user.name|capfirst %} Hello {{ name }}! {% endblocktrans %}
当你的字符串中含有变量时使用
{% blocktrans %}
代替{% trans %}
。
编辑 shop
应用的 shop/base.html
。确保你已经在顶部载入了 i18n
标签,而后按照以下标记翻译字符串:
{% load i18n %} {% load static %} <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> {% block title %}{% trans "My shop" %}{% endblock %} </title> <link href="{% static "css/base.css" %}" rel="stylesheet"> </head> <body> <div id="header"> <a href="/" class="logo">{% trans "My shop" %}</a> </div> <div id="subheader"> <div class="cart"> {% with total_items=cart|length %} {% if cart|length > 0 %} {% trans "Your cart" %}: <a href="{% url "cart:cart_detail" %}"> {% blocktrans with total_items_plural=total_ items|pluralize total_price=cart.get_total_price %} {{ total_items }} item{{ total_items_plural }}, ${{ total_price }} {% endblocktrans %} </a> {% else %} {% trans "Your cart is empty." %} {% endif %} {% endwith %} </div> </div> <div id="content"> {% block content %} {% endblock %} </div> </body> </html>
注意展现在购物车小计的 {% blocktrans %}
标签。购物车小计在以前是这样的:
{{ total_items }} item{{ total_items|pluralize }}, ${{ cart.get_total_price }}
咱们使用 {% blocktrans with ... %}
来使用 total_ items|pluralize
(模板(template)标签生效的地方)和 cart_total_price
(链接对象方法的地方)的占位符:
{% blocktrans with total_items_plural=total_items|pluralize total_price=cart.get_total_price %} {{ total_items }} item{{ total_items_plural }}, ${{ total_price }} {% endblocktrans %}
下面,编辑 shop
应用的 shop/product/detail.html
模板(template)而后在顶部载入 i18n
标签,可是它必须位于 {% extends %}
标签的下面:
{% load i18n %}
找到下面这一行:
<input type="submit" value="Add to cart">
把它替换为下面这一行:
<input type="submit" value="{% trans "Add to cart" %}">
如今翻译 orders
应用模板(template)。编辑 orders
应用的 orders/order/create.html
模板(template),而后标记翻译文本:
{% extends "shop/base.html" %} {% load i18n %} {% block title %} {% trans "Checkout" %} {% endblock %} {% block content %} <h1>{% trans "Checkout" %}</h1> <div class="order-info"> <h3>{% trans "Your order" %}</h3> <ul> {% for item in cart %} <li> {{ item.quantity }}x {{ item.product.name }} <span>${{ item.total_price }}</span> </li> {% endfor %} {% if cart.coupon %} <li> {% blocktrans with code=cart.coupon.code discount=cart.coupon.discount %} "{{ code }}" ({{ discount }}% off) {% endblocktrans %} <span>- ${{ cart.get_discount|floatformat:"2" }}</span> </li> {% endif %} </ul> <p>{% trans "Total" %}: ${{ cart.get_total_price_after_discount|floatformat:"2" }}</p> </div> <form action="." method="post" class="order-form"> {{ form.as_p }} <p><input type="submit" value="{% trans "Place order" %}"></p> {% csrf_token %} </form> {% endblock %}
看看本章中下列文件中的代码,看看字符串是如何被标记的:
shop
应用:shop/product/list.html
orders
应用:orders/order/created.html
cart
应用:cart/detail.html
让位咱们更新信息文件来引入新的翻译字符串。打开 shell ,运行下面的命令:
django-admin makemessages --all
.po
文件已经在 myshop
项目的 locale
路径下,你将看到 orders
应用如今已经包含咱们标记的全部须要翻译的字符串。
编辑项目和orderes
应用中的 .po
翻译文件,而后引入西班牙语翻译。你能够参考本章中翻译了的 .po
文件:
在项目路径下打开 shell ,而后运行下面的命令:
cd orders/ django-admin compilemessages cd ../
咱们已经编译了 orders
应用的翻译。
运行下面的命令,这样应用中不包含 locale
路径的翻译就被包含进了项目的信息文件中:
django-admin compilemessages
Rosetta 是一个让你能够编辑翻译的第三方应用,它有相似 Django 管理站点的交互界面。Rosetta 让编辑 .po
文件和更新它变得很简单,让咱们把它添加进项目中:
pip install django-rosetta==0.7.6
而后,把 rosetts
添加进项目中的setting.py
文件中的INSTALLED_APPS
设置中。
你须要把 Rosetta 的 URL 添加进主 URL 配置中。编辑项目中的主 urls.py
文件,把下列 URL 模式添加进去:
url(r'^rosetta/', include('rosetta.urls')),
确保你把它放在了 shop.urls
模式以前以免错误的匹配。
访问 http://127.0.0.1:8000/admin/ ,而后使用超级管理员帐户登陆。而后导航到 http://127.0.0.1:8000/rosetta/ 。你能够看到当前语言列表:
在 Filter 模块,点击 All 来展现全部可用的信息文件,包括属于 orders
应用的信息文件。点击Spanish模块下的 Myshop 连接来编辑西班牙语翻译。你能够看到翻译字符串的列表:
你能够在 Spanish 行下输入翻译。Occurences 行展现了代码中翻译字符串被找到的文件和行数、
包含占位符的翻译看起来像这样:
Rosetta 使用了不一样的背景色来展现占位符。当你翻译内容时,确保你没有翻译占位符。好比,用下面这个字符串举例:
%(total_items)s item%(total_items_plural)s, $%(total_price)s
它翻译为西班牙语以后长得像这样:
%(total_items)s producto%(total_items_plural)s, $%(total_price)s
你能够看看本章项目中使用相同西班牙语翻译的文件源代码。
当你完成编辑翻译后,点击Save and translate next block 按钮来保存翻译到 .po
文件中。Rosetta 在你保存翻译时编辑信息文件,因此并不须要你运行 compilemessages
命令。尽管,Rosetta 须要 locale
路径的写入权限来写入信息文件。确保路径有有效权限。
若是你想让其余用户编辑翻译,访问:http://127.0.0.1:8000/admin/auth/group/add/,而后建立一个名为 translations
的新组。当编辑一个用户时,在Permissions模块下,把 translations
组添加进ChosenGroups中。Rosetta 只对超级用户和 translations
中的用户是可用的。
你能够在这个网站阅读 Rosetta 的文档:http://django-rosetta.readthedocs.org/en/latest/ 。
当你在生产环境中添加新的翻译时,若是你的 Django 运行在一个真实服务器上,你必须在运行
compilemessages
或保存翻译以后重启你的服务器来让 Rosetta 的更改生效。
你或许已经注意到了 Rosetta 有 Fuzzy 这一行。这不是 Rosetta 的特性,它是由 gettext 提供的。若是翻译的 flag 是激活的,那么它就不会被包含进编译后的信息文件中。flag 用于须要翻译器修订的翻译字符串。当 .po
文件在更新新的翻译字符串时,一些翻译字符串可能被自动标记为 fuzzy. 当 gettext 找到一些变更不大的 msgid
时就会发生这样的状况,gettext 就会把它认为的旧的翻译和匹配在一块儿而后会在回把它标记为 fuzzy 以用于回查。翻译器以后会回查模糊翻译,会移除 fuzzy 标签而后再次编译信息文件。
Django 提供了用于国际化的 URLs。它包含两种主要用于国际化的 URLs:
翻译 URLs 的缘由是这样就能够优化你的站点,方便搜索引擎搜索。经过添加语言前缀,你就能够为每一种语言提供索引,而不是全部语言用一种索引。而且, 把 URLs 为不一样语言,你就能够提供给搜索引擎更好的搜索序列。
Django 容许你能够把语言前缀添加到 URLs 模式中。举个例子,网站的英文版能够服务于 /en/
起始路径之下,西班牙语版服务于 /es/
起始路径之下。
为了在你的 URLs 模式中使用不一样语言,你必须确保 settings.py
中的 MIDDLEWARE_CLASSES
设置中有 django.middleware.localMiddlewar
。Django 将会使用它来辨别当前请求中的语言。
让咱们把语言前缀添加到 URLs 模式中。编辑 myshop
项目的 urls.py
,添加如下库:
from django.conf.urls.i18n import i18n_patterns
而后添加 i18n_patterns()
:
urlpatterns = i18n_patterns( url(r'^admin/', include(admin.site.urls)), url(r'^cart/', include('cart.urls', namespace='cart')), url(r'^orders/', include('orders.urls', namespace='orders')), url(r'^payment/', include('payment.urls', namespace='payment')), url(r'^paypal/', include('paypal.standard.ipn.urls')), url(r'^coupons/', include('coupons.urls', namespace='coupons')), url(r'^rosetta/', include('rosetta.urls')), url(r'^', include('shop.urls', namespace='shop')), )
你能够把 i18n_patterns()
和 patterns()
URLs 模式结合起来,这样一些模式就会包含语言前缀另外一些就不会包含。尽管,最好仍是使用翻译后的 URLs 来避免 URL 匹配一个未翻译的 URL 模式的可能。
打开开发服务器,访问 http://127.0.0.1:8000/ ,由于你在 Django 中使用 LocaleMiddleware
来执行 How Django determines the current language
中描述的步骤来决定当前语言,而后它就会把你重定向到包含相同语言前缀的 URL。看看浏览器中 URL ,它看起来像这样:http://127.0.0.1:8000/en/.当前语言将会被设置在浏览器的 Accept-Language
头中,设为英语或者西班牙语或者是 LANGUAGE_OCDE
(English) 中的默认设置。
Django 支持 URL 模式中有翻译了的字符串。你能够为每一种语言使用不一样的 URL 模式。你可使用 ugettet_lazy()
函数标记 URL 模式中须要翻译的字符串。
编辑 myshop
项目中的 urls.py
文件,把翻译字符串添加进 cart
,orders
,payment
,coupons
的 URLs 模式的正则表达式中:
from django.utils.translation import gettext_lazy as _ urlpatterns = i18n_patterns( url(r'^admin/', include(admin.site.urls)), url(_(r'^cart/'), include('cart.urls', namespace='cart')), url(_(r'^orders/'), include('orders.urls', namespace='orders')), url(_(r'^payment/'), include('payment.urls', namespace='payment')), url(r'^paypal/', include('paypal.standard.ipn.urls')), url(_(r'^coupons/'), include('coupons.urls', namespace='coupons')), url(r'^rosetta/', include('rosetta.urls')), url(r'^', include('shop.urls', namespace='shop')), )
编辑 orders
应用的 urls.py
文件,标记须要翻译的 URLs 模式:
from django.conf.urls import url from .import views from django.utils.translation import gettext_lazy as _ urlpatterns = [ url(_(r'^create/$'), views.order_create, name='order_create'), # ... ]
编辑 payment
应用的 urls.py
文件,把代码改为这样:
from django.conf.urls import url from . import views from django.utils.translation import gettext_lazy as _ urlpatterns = [ url(_(r'^process/$'), views.payment_process, name='process'), url(_(r'^done/$'), views.payment_done, name='done'), url(_(r'^canceled/$'), views.payment_canceled, name='canceled'), ]
咱们不须要翻译 shop
应用的 URL 模式,由于它们是用变量建立的,并且也没有包含其余的任何字符。
打开 shell ,运行下面的命令来把新的翻译更新到信息文件:
django-admin makemessages --all
确保开发服务器正在运行中,访问:http://127.0.0.1:8000/en/rosetta/ ,点击Spanish下的Myshop 连接。你可使用显示过滤器(display filter)来查看没有被翻译的字符串。确保你的 URL 翻译有正则表达式中的特殊字符。翻译 URLs 是一个精细活儿;若是你替换了正则表达式,你可能会破坏 URL。
由于咱们正在提供多语种服务,咱们应当让用户能够选择站点的语言。咱们将会为咱们的站点添加语言选择器。语言选择器由可用的语言列表组成,咱们使用连接来展现这些语言选项:
编辑 shop/base.html
模板(template),找到下面这一行:
<div id="header"> <a href="/" class="logo">{% trans "My shop" %}</a> </div>
把他们换成如下几行::
<div id="header"> <a href="/" class="logo">{% trans "My shop" %}</a> {% get_current_language as LANGUAGE_CODE %} {% get_available_languages as LANGUAGES %} {% get_language_info_list for LANGUAGES as languages %} <div class="languages"> <p>{% trans "Language" %}:</p> <ul class="languages"> {% for language in languages %} <li> <a href="/{{ language.code }}/" {% if language.code == LANGUAGE_CODE %} class="selected"{% endif %}> {{ language.name_local }} </a> </li> {% endfor %} </ul> </div> </div>
咱们是这样建立咱们的语言选择器的:
1. 首先使用 `{% load i18n %}` 加载国际化 2. 使用 `{% get_current_language %}` 标签来检索当前语言 3. 使用 `{% get_available_languages %}` 模板(template)标签来过去 `LANGUAGES` 中定义语言 4. 使用 `{% get_language_info_list %}` 来提供简易的语言属性链接入口 5. 咱们建立了一个 HTML 列表来展现全部的可用语言列表而后咱们为当前激活语言添加了 `selected` 属性
咱们使用基于项目设置中语言变量的 i18n提供的模板(template)标签。如今访问:
http://127.0.0.1:8000/` ,你应该能在站点顶部的右边看到语言选择器:
用户如今能够轻易的切换他们的语言了。
Django 没有提供开箱即用的模型(models)翻译的解决办法。你必需要本身实现你本身的解决办法来管理不一样语言中的内容或者使用第三方模块来管理模型(model)翻译。有几种第三方应用容许你翻译模型(model)字段。每一种手采用了不一样的方法保存和链接翻译。其中一种应用是 django-parler 。这个模块提供了很是有效的办法来翻译模型(models)而且它和 Django 的管理站点整合的很是好。
django-parler 为每个模型(model)生成包含翻译的分离数据库表。这个表包含了全部的翻译的字段和源对象的外键翻译。它也包含了一个语言字段,一位内每一行都会保存一个语言的内容。
使用 pip 安装 django-parler :
pip install django-parler==1.5.1
编辑项目中 settings.py
,把 parler
添加到 INSTALLED_APPS
中,一样也把下面的配置添加到配置文件中:
PARLER_LANGUAGES = { None: ( {'code': 'en',}, {'code': 'es',}, ), 'default': { 'fallback': 'en', 'hide_untranslated': False, } }
这个设置为 django-parler 定义了可用语言 en
和 es
。咱们指定默认语言为 en
,而后咱们指定 django-parler 应该隐藏未翻译的内容。
让咱们为咱们的产品目录添加翻译。 django-parler 提供了 TranslatedModel
模型(model)类和 TranslatedFields
闭包(wrapper)来翻译模型(model)字段。编辑 shop
应用路径下的 models.py
文件,添加如下导入:
from parler.models import TranslatableModel, TranslatedFields
而后更改 Category
模型(model)来使 name
和 slug
字段能够被翻译。
咱们依然保留了每行未翻译的字段:
class Category(TranslatableModel): name = models.CharField(max_length=200, db_index=True) slug = models.SlugField(max_length=200, db_index=True, unique=True) translations = TranslatedFields( name = models.CharField(max_length=200, db_index=True), slug = models.SlugField(max_length=200, db_index=True, unique=True) )
Category
模型(model)继承了 TranslatableModel
而不是 models.Model
。而且 name
和 slug
字段都被引入到了 TranslatedFields
闭包(wrapper)中。
编辑 Product
模型(model),为 name
,slug
,description
添加翻译,一样咱们也保留了每行未翻译的字段:
class Product(TranslatableModel): name = models.CharField(max_length=200, db_index=True) slug = models.SlugField(max_length=200, db_index=True) description = models.TextField(blank=True) translations = TranslatedFields( name = models.CharField(max_length=200, db_index=True), slug = models.SlugField(max_length=200, db_index=True), description = models.TextField(blank=True) ) category = models.ForeignKey(Category, related_name='products') image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True) price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.PositiveIntegerField() available = models.BooleanField(default=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True)
django-parler 为每一个可翻译模型(model)生成了一个模型(model)。在下面的图片中,你能够看到 Product
字段和生成的 ProductTranslation
模型(model):
django-parler 生成的 ProductTranslation
模型(model)有 name
,slug
,description
可翻译字段,一个 language_code
字段,和主要的 Product
对象的 ForeignKey
字段。Product
和 ProductTranslation
之间有一个一对多关系。一个 ProductTranslation
对象会为每一个可用语言生成 Product
对象。
由于 Django 为翻译都使用了相互独立的表,因此有一些 Django 的特性咱们是用不了。使用翻译后的字段来默认排序是不可能的。你能够在查询集(QuerySets)中使用翻译字段来过滤,可是你不能够在 ordering Meta
选项中引入可翻译字段。编辑 shop
应用的 models.py
,而后注释掉 Category Meta
类的 ordering
属性:
class Meta: # ordering = ('name',) verbose_name = 'category' verbose_name_plural = 'categories'
咱们也必须注释掉 Product Meta
类的 index_together
属性,由于当前的 django-parler 的版本不支持对它的验证。编辑 Product Meta
:
class Meta: ordering = ('-created',) # index_together = (('id', 'slug'),)
你能够在这个网站阅读更多 django-parler 兼容的有关内容:
http://django-parler.readthedocs.org/en/latest/compatibility.html 。
当你建立新的翻译模型(models)时,你须要执行 makemessages
来生成模型(models)的迁移,而后把变动同步到数据库中。尽管当使已存在字段可翻译化时,你或许有想要在数据库中保留的数据。咱们将迁移当前数据到新的翻译模型(models)中。所以,咱们添加了翻译字段可是有意保存了原始字段。
翻译添加到当前字段的步骤以下:
1. 在保留原始字段的状况下,建立新翻译模型(model)的迁移 2. 建立定制化迁移,从当前已有的字段中拷贝一份数据到翻译模型(models)中 3. 从源模型(models)中删除字段
运行下面的命令来为咱们添加到 Category
和 Product
模型(model)中的翻译字段建立迁移:
python manage.py makemigrations shop --name "add_translation_model"
你能够看到以下输出:
Migrations for 'shop': 0002_add_translation_model.py: - Create model CategoryTranslation - Create model ProductTranslation - Change Meta options on category - Alter index_together for product (0 constraint(s)) - Add field master to producttranslation - Add field master to categorytranslation - Alter unique_together for producttranslation (1 constraint(s)) - Alter unique_together for categorytranslation (1 constraint(s))
如今咱们须要建立定制迁移来把已有数据拷贝到新的翻译模型(model)中。使用这个命令建立一个空的迁移:
python manage.py makemigrations --empty shop --name "migrate_translatable_fields"
你将会看到以下输出:
Migrations for 'shop': 0003_migrate_translatable_fields.py
编辑 shop/migrations/0003_migrate_translatable_fields.py
,而后添加下面的代码:
# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.apps import apps from django.conf import settings from django.core.exceptions import ObjectDoesNotExist translatable_models = { 'Category': ['name', 'slug'], 'Product': ['name', 'slug', 'description'], } def forwards_func(apps, schema_editor): for model, fields in translatable_models.items(): Model = apps.get_model('shop', model) ModelTranslation = apps.get_model('shop', '{}Translation'.format(model)) for obj in Model.objects.all(): translation_fields = {field: getattr(obj, field) for field in fields} translation = ModelTranslation.objects.create( master_id=obj.pk, language_code=settings.LANGUAGE_CODE, **translation_fields) def backwards_func(apps, schema_editor): for model, fields in translatable_models.items(): Model = apps.get_model('shop', model) ModelTranslation = apps.get_model('shop', '{}Translation'.format(model)) for obj in Model.objects.all(): translation = _get_translation(obj, ModelTranslation) for field in fields: setattr(obj, field, getattr(translation, field)) obj.save() def _get_translation(obj, MyModelTranslation): translations = MyModelTranslation.objects.filter(master_id=obj.pk) try: # Try default translation return translations.get(language_code=settings.LANGUAGE_CODE) except ObjectDoesNotExist: # Hope there is a single translation return translations.get() class Migration(migrations.Migration): dependencies = [ ('shop', '0002_add_translation_model'), ] operations = [ migrations.RunPython(forwards_func, backwards_func), ]
这段迁移包括了用于执行应用和反转迁移的 forwards_func()
和 backwards_func()
。
迁移工做流程以下:
1. 咱们在 `translatable_models` 字典中定义了模型(model)和它们的可翻译字段 2. 为了应用迁移,咱们使用 `app.get_model()` 来迭代包含翻译的模型(model)来获得这个模型(model)和其可翻译的模型(model) 3. 咱们在数据库中迭代全部的当前对象,而后为定义在项目设置中的 `LANGUAGE_CODE` 建立一个翻译对象。咱们引入了 `ForeignKey` 到源对像和一份从源字段中可翻译字段的拷贝。
backwards 函数执行的是反转操做,它检索默认的翻译对象,而后把可翻译字段的值拷贝到新的模型(model)中。
最后,咱们须要删除咱们再也不须要的源字段。编辑 shop
应用的 models.py
,而后删除 Category
模型(model)的 name
和 slug
字段:
class Category(TranslatableModel): translations = TranslatedFields( name = models.CharField(max_length=200, db_index=True), slug = models.SlugField(max_length=200, db_index=True, unique=True) )
删除 Product
模型(model)的 name
和 slug
字段:
class Product(TranslatableModel): translations = TranslatedFields( name = models.CharField(max_length=200, db_index=True), slug = models.SlugField(max_length=200, db_index=True), description = models.TextField(blank=True) ) category = models.ForeignKey(Category, related_name='products') image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True) price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.PositiveIntegerField() available = models.BooleanField(default=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True)
如今咱们必需要建立最后一次迁移来应用其中的变动。若是咱们尝试 manage.py
工具,咱们将会获得一个错误,由于咱们尚未让管理站点适应翻译模型(models)。让咱们先来修整一下管理站点。
Django-parler 很好和 Django 的管理站点相融合。它用 TranslatableAdmin
重写了 Django 的 ModelAdmin
类 来管理翻译。
编辑 shop 应用的 admin.py
文件,添加下面的库:
from parler.admin import TranslatableAdmin
把 CategoryAdmin
和 ProductAdmin
的继承类改成 TranslatableAdmin
取代原来的 ModelAdmin
。 Django-parler 如今还不支持 prepopulated_fields
属性,可是它支持 get_prepopulated_fields()
方法来提供相同的功能。让咱们据此作一些改变。 admin.py
文件看起来像这样:
from django.contrib import admin from .models import Category, Product from parler.admin import TranslatableAdmin class CategoryAdmin(TranslatableAdmin): list_display = ['name', 'slug'] def get_prepopulated_fields(self, request, obj=None): return {'slug': ('name',)} admin.site.register(Category, CategoryAdmin) class ProductAdmin(TranslatableAdmin): list_display = ['name', 'slug', 'category', 'price', 'stock', 'available', 'created', 'updated'] list_filter = ['available', 'created', 'updated', 'category'] list_editable = ['price', 'stock', 'available'] def get_prepopulated_fields(self, request, obj=None): return {'slug': ('name',)} admin.site.register(Product, ProductAdmin)
咱们已经使管理站点可以和新的翻译模型(model)工做了。如今咱们就能把变动同步到数据库中了。
咱们在变动管理站点以前已经从模型(model)中删除了旧的字段。如今咱们须要为此次变动建立迁移。打开 shell ,运行下面的命令:
python manage.py makemigrations shop --name "remove_untranslated_fields"
你将会看到以下输出:
Migrations for 'shop': 0004_remove_untranslated_fields.py: - Remove field name from category - Remove field slug from category - Remove field description from product - Remove field name from product - Remove field slug from product
迁移以后,咱们就已经删除了源字段保留了可翻译字段。
总结一下,咱们建立了如下迁移:
1. 将可翻译字段添加到模型(models)中 2. 将源字段中的数据迁移到可翻译字段中 3. 从源模型(models)中删除源字段
运行下面的命令来应用咱们建立的迁移:
python manage.py migrate shop
你将会看到以下输出:
Applying shop.0002_add_translation_model... OK Applying shop.0003_migrate_translatable_fields... OK Applying shop.0004_remove_untranslated_fields... OK
咱们的模型(models)如今已经同步到了数据库中。让咱们来翻译一个对象。
用命令 pythong manage.py runserver
运行开发服务器,在浏览器中访问:http://127.0.0.1:8000/en/admin/shop/category/add/ 。你就会看到包含两个标签的 Add category 页,一个标签是英文的,一个标签是西班牙语的。
你如今能够添加一个翻译而后点击Save按钮。确保你在切换标签以前保存了他们,否则让门就会丢失。
咱们要使咱们的 shop
视图(views)适应咱们的翻译查询集(QuerySets)。在命令行运行 python manage.py shell
,看看你是如何检索和查询翻译字段的。为了从当前激活语言中获得一个字段的内容,你只须要用和链接通常字段相同的办法链接这个字段:
>>> from shop.models import Product >>> product=Product.objects.first() >>> product.name 'Black tea'
当你链接到被翻译了字段时,他们已经被用当前语言处理过了。你能够为一个对象设置不一样的当前语言,这样你就能够得到指定的翻译了:
>>> product.set_current_language('es') >>> product.name 'Té negro' >>> product.get_current_language() 'es
当使用 filter()
执行一次查询集(QuerySets)时,你可使用 translations__
语法筛选相关联对象:
>>> Product.objects.filter(translations__name='Black tea') [<Product: Black tea>]
你也可使用 language()
管理器来为每一个检索的对象指定特定的语言:
>>> Product.objects.language('es').all() [<Product: Té negro>, <Product: Té en polvo>, <Product: Té rojo>, <Product: Té verde>]
如你所见,链接到翻译字段的方法仍是很直接的。
让咱们修改一下产品目录的视图(views)吧。编辑 shop
应用的 views.py
,在 product_list
视图(view)中找到下面这一行:
category = get_object_or_404(Category, slug=category_slug)
把它替换为下面这几行:
language = request.LANGUAGE_CODE category = get_object_or_404(Category, translations__language_code=language, translations__slug=category_slug)
而后,编辑 product_detail
视图(view),找到下面这几行:
product = get_object_or_404(Product, id=id, slug=slug, available=True)
把他们换成如下几行::
language = request.LANGUAGE_CODE get_object_or_404(Product, id=id, translations__language_code=language, translations__slug=slug, available=True)
如今 product_list
和 product_detail
已经可使用翻译字段来检索了。运行开发服务器,访问:http://127.0.0.1:8000/es/ ,你能够看到产品列表页,包含了全部被翻译为西班牙语的产品:
如今每一个产品的 URLs 已经使用 slug
字段被翻译为了的当前语言。好比,一个西班牙语产品的 URL 为:http://127.0.0.1:8000/es/2/te-rojo/ ,可是它的英文 URL 为:http://127.0.0.1:8000/en/2/red-tea/ 。若是你导航到产品详情页,你能够看到用被选语言翻译后的 URL 和内容,就像下面的例子:
若是你想要更多了解 django-parler ,你能够在这个网站找到全部的文档:http//django-parler.readthedocs.org/en/latest/ 。
你已经学习了如何翻译 Python 代码,模板(template),URL 模式,模型(model)字段。为了完成国际化和本地化的工做,咱们须要展现本地化格式的时间和日期、以及数字。
基于用户的语言,你可能想要用不一样的格式展现日期、时间和数字。本第格式化可经过修改 settings.py
中的 USE_L1ON
设置为 True
来使本地格式化生效。
当 USE_L1ON
可用时,Django 将会在模板(template)任何输出一个值时尝试使用某一语言的特定格式。你能够看到你的网站中用一个点来作分隔符的十进制英文数字,尽管在西班牙语版中他们使用逗号来作分隔符。这和 Django 里为 es
指定的语言格式。你能够在这里查看西班牙语的格式配置:https://github.com/django/django/blob/stable/1.8.x/django/conf/locale/es/formats.py 。
一般,你把 USE_L10N
的值设为 True
而后 Django 就会为每一种语言应用本地化格式。虽然在某些状况下你有不想要被格式化的值。这和输出特别相关, JavaScript 或者 JSON 必需要提供机器可读的格式。
Django 提供了 {% localize %}
模板(template)表标签来让你能够开启或者关闭模板(template)的本地化。这使得你能够控制本地格式化。你须要载入 l10n
标签来使用这个模板(template)标签。下面的例子是如何在模板(template)中开启和关闭它:
{% load l10n %} {% localize on %} {{ value }} {% endlocalize %} {% localize off %} {{ value }} {% endlocalize %}
Django 一样也提供了 localize
和 unlocalize
模板(template)过滤器来强制或取消一个值的本地化。过滤器能够像下面这样使用:
{{ value|localize }} {{ value|unlocalize }}
你也能够建立定制化的格式文件来指定特定语言的格式。你能够在这个网站找到全部关于本第格式化的信息:https://docs.djangoproject.com/en/1.8/topics/i18n/formatting/ 。
django-localflavor 是一个包含特殊工具的第三方模块,好比它的有些表单字段或者模型(model)字段对于每一个国家是不一样的。它对于验证本地地区,本地电话号码,身份证号,社保号等等都是很是有用的。这个包被集成进了一系列以 ISO 3166 国家代码命名的模块里。
使用下面的命令安装 django-localflavor :
pip install django-localflavor==1.1
编辑项目中的 settings.py
,把 localflavor
添加进 INSTALLED_APPS
设置中。
咱们将会添加美国(U.S.)邮政编码字段,这样以后建立一个新的订单就须要一个美国邮政编码了。
编辑 orders
应用的 forms.py
,让它看起来像这样:
from django import forms from .models import Order from localflavor.us.forms import USZipCodeField class OrderCreateForm(forms.ModelForm): postal_code = USZipCodeField() class Meta: model = Order fields = ['first_name', 'last_name', 'email', 'address', 'postal_code', 'city',]
咱们从 localflavor
的 us
包中引入了 USZipCodeField
而后将它应用在 OrderCreateForm
的 postal_code
字段中。访问:http://127.0.0.1:8000/en/orders/create/ 而后尝试输入一个 3 位邮政编码。你将会获得 USZipCodeField
引起的验证错误:
Enter a zip code in the format XXXXX or XXXXX-XXXX.
这只是一个在你的项目中使用定制字段来达到验证目的的简单例子,localflavor 提供的本地组件对于让你的项目适应不一样的国家是很是有用的。你能够阅读 django-localflavor 的文档,看看全部的可用地区组件:https://django-localflavor.readthedocs.org/en/latest/ 。
下面,咱们将会为咱们的店铺建立一个推荐引擎。
推荐引擎是一个能够预测用户对于某一物品偏好和比率的系统。这个系统会基于用户的行为和它对于用户的了解选出相关的商品。现在,推荐系统用于许多的在线服务。它们帮助用户从大量的相关数据中挑选他们可能感兴趣的产品。提供良好的推荐能够鼓励用户更多的消费。电商网站受益于日益增加的相关产品推荐销售中。
咱们将会建立一个简单但强大的推荐引擎来推荐常常一块儿购买的商品。咱们将基于历史销售来推荐商品,这样就能够辨别出哪些商品一般是一块儿购买的了。咱们将会在两种不一样的场景下推荐互补的产品:
咱们将会使用 Redis 来储存被一块儿购买的产品。记得你已经在第六章 跟踪用户操做 使用了 Redis 。若是你尚未安装 Redis ,你能够在那一章找到安装指导。
如今,让咱们推荐给用户一些基于他们添加到购物车的产品。咱们将会在 Redis 中为每一个产品储存一个键(key)。产品的键将会和它的评分一同储存在 Redis 的有序集中。在一次新的订单被完成时,咱们每次都会为一同购买的产品的评分加一。
当一份订单付款成功后,咱们保存每一个购买产品的键,包括赞成订单中的有序产品集。这个有序集让咱们能够为一块儿购买的产品打分。
编辑项目中的额 settings.py
,添加如下设置:
REDIS_HOST = 'localhost' REDIS_PORT = 6379 REDIS_DB = 1
这里的设置是为了和 Redis 服务器创建链接。在 shop
应用内建立一个新的文件,命名为 recommender.py
。添加如下代码:
import redis from django.conf import settings from .models import Product # connect to redis r = redis.StrictRedis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB) class Recommender(object): def get_product_key(self, id): return 'product:{}:purchased_with'.format(id) def products_bought(self, products): product_ids = [p.id for p in products] for product_id in product_ids: for with_id in product_ids: # get the other products bought with each product if product_id != with_id: # increment score for product purchased together r.zincrby(self.get_product_key(product_id), with_id, amount=1)
Recommender
类使咱们能够储存购买的产品而后检索所给产品的产品推荐。get_product_key()
方法检索 Product
对象的 id
,而后为储存产品的有序集建立一个 Redis 键(key),看起来像这样:product:[id]:purchased_with
products_bought()
方法检索被一块儿购买的产品列表(它们都属于同一个订单)。在这个方法中,咱们执行如下几个任务:
1. 获得所给 `Product` 对象的产品 ID 2. 迭代全部的产品 ID。对于每一个 `id` ,咱们迭代全部的产品 ID 而且跳过全部相同的产品,这样咱们就能够获得和每一个产品一块儿购买的产品。 3. 咱们使用 `get_product_id()` 方法来获取 Redis 产品键。对于一个 ID 为 33 的产品,这个方法返回键 `product:33:purchased_with` 。这个键用于包含和这个产品被一同购买的产品 ID 的有序集。 4. 咱们把有序集中的每一个产品 `id` 的评分加一。评分表示另外一个产品和所给产品一块儿购买的次数。
咱们有一个方法来保存和对一同购买的产品评分。如今咱们须要一个方法来检索被所给购物列表的一块儿购买的产品。把 suggest_product_for()
方法添加到 Recommender
类中:
def suggest_products_for(self, products, max_results=6): product_ids = [p.id for p in products] if len(products) == 1: # only 1 product suggestions = r.zrange( self.get_product_key(product_ids[0]), 0, -1, desc=True)[:max_results] else: # generate a temporary key flat_ids = ''.join([str(id) for id in product_ids]) tmp_key = 'tmp_{}'.format(flat_ids) # multiple products, combine scores of all products # store the resulting sorted set in a temporary key keys = [self.get_product_key(id) for id in product_ids] r.zunionstore(tmp_key, keys) # remove ids for the products the recommendation is for r.zrem(tmp_key, *product_ids) # get the product ids by their score, descendant sort suggestions = r.zrange(tmp_key, 0, -1, desc=True)[:max_results] # remove the temporary key r.delete(tmp_key) suggested_products_ids = [int(id) for id in suggestions] # get suggested products and sort by order of appearance suggested_products = list(Product.objects.filter(id__in=suggested_ products_ids)) suggested_products.sort(key=lambda x: suggested_products_ids. index(x.id)) return suggested_products
suggest_product_for()
方法接收下列参数:
products
:这是一个 Product
对象列表。它能够包含一个或者多个产品max_results
:这是一个整数,用于展现返回的推荐的最大数量在这个方法中,咱们执行如下的动做:
1. 获得所给 `Product` 对象的 ID 2. 若是只有一个产品,咱们就检索一同购买的产品的 ID,并按照他们的购买时间来排序。这样作,咱们就可使用 Redis 的 `ZRANGE` 命令。咱们经过 `max_results` 属性来限制结果的数量。 3. 若是有多于一个的产品被给予,咱们就生成一个临时的和产品 ID 一同建立的 Redis 键。 4. 咱们把包含在每一个所给产品的有序集中东西的评分组合并相加,咱们使用 Redis 的 `ZUNIONSTORE` 命令来实现这个操做。`ZUNIONSTORE` 命令执行了对有序集的所给键的求和,而后在新的 Redis 键中保存每一个元素的求和。你能够在这里阅读更多关于这个命令的信息:http://redisio/commands/ZUNIONSTORE 。咱们在临时键中保存分数的求和。 5. 由于咱们已经求和了评分,咱们或许会获取咱们推荐的重复商品。咱们就使用 `ZREM` 命令来把他们从生成的有序集中删除。 6. 咱们从临时键中检索产品 ID,使用 `ZREM` 命令来按照他们的评分排序。咱们把结果的数量限制在 `max_results` 属性指定的值内。而后咱们删除了临时键。 7. 最后,咱们用所给的 `id` 获取 `Product` 对象,而且按照一样的顺序来排序。
为了更加方便使用,让咱们添加一个方法来清除全部的推荐。
把下列方法添加进 Recommender
类中:
def clear_purchases(self): for id in Product.objects.values_list('id', flat=True): r.delete(self.get_product_key(id))
让咱们试试咱们的推荐引擎。确保你在数据库中引入了几个 Product
对象而且在 shell 中使用了下面的命令来初始化 Redis 服务器:
src/redis-server
打开另一个 shell ,执行 python manage.py shell
,输入下面的代码来检索几个产品:
from shop.models import Product black_tea = Product.objects.get(translations__name='Black tea') red_tea = Product.objects.get(translations__name='Red tea') green_tea = Product.objects.get(translations__name='Green tea') tea_powder = Product.objects.get(translations__name='Tea powder')
而后,添加一些测试购物到推荐引擎中:
from 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])
咱们已经保存了下面的评分:
black_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)
让咱们看看为单一产品的推荐吧:
>>> 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>]
正如你所看到的那样,推荐产品的顺序正式基于他们的评分。让咱们来看看为多个产品的推荐吧:
>>> 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>]
你能够看到推荐产品的顺序和评分总和的顺序是同样的。好比 black_tea
, red_tea
, tea_powder(2+1)
和 green_tea(1=1)
的产品推荐就是这样。
咱们必须保证咱们的推荐算法按照预期那样工做。让咱们在咱们的站点上展现咱们的推荐吧。
编辑 shop
应用的 views.py
,添加如下库:
from .recommender import Recommender
把下面的代码添加进 product_detail
视图(view)中的 render()
以前:
r = Recommender() recommended_products = r.suggest_products_for([product], 4)
咱们获得了四个最多产品推荐。 product_detail
视图(view)如今看起来像这样:
from .recommender import Recommender def product_detail(request, id, slug): product = get_object_or_404(Product, id=id, slug=slug, available=True) 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
应用的 shop/product/detail.html
模板(template),把如下代码添加在 {{ product.description|linebreaks }}
的后面:
{% if recommended_products %} <div class="recommendations"> <h3>{% trans "People who bought this also bought" %}</h3> {% for p in recommended_products %} <div class="item"> <a href="{{ p.get_absolute_url }}"> <img src="{% if p.image %}{{ p.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}"> </a> <p><a href="{{ p.get_absolute_url }}">{{ p.name }}</a></p> </div> {% endfor %} </div> {% endif %}
运行开发服务器,访问:http://127.0.0.1:8000/en/ 。点击一个产品来查看它的详情页。你应该看到展现在下方的推荐产品,就象这样:
咱们也将在购物车当中引入产品推荐。产品推荐将会基于用户购物车中添加的产品。编辑 cart
应用的 views.py
,添加如下库:
from shop.recommender import Recommender
编辑 cart_detail
视图(view),让它看起来像这样:
def cart_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_results=4) return render(request, 'cart/detail.html', {'cart': cart, 'coupon_apply_form': coupon_apply_form, 'recommended_products': recommendeproducts})
编辑 cart
应用的 cart/detail.html
,把下列代码添加在 <table>
HTML 标签后面:
{% if recommended_products %} <div class="recommendations cart"> <h3>{% trans "People who bought this also bought" %}</h3> {% for p in recommended_products %} <div class="item"> <a href="{{ p.get_absolute_url }}"> <img src="{% if p.image %}{{ p.image.url }}{% else %}{% static "img/no_image.png" %}{% endif %}"> </a> <p><a href="{{ p.get_absolute_url }}">{{ p.name }}</a></p> </div> {% endfor %} </div> {% endif %}
访问 http://127.0.0.1:8000/en/ ,在购物车当中添加一些商品。东航拟导航向 http://127.0.0.1:8000/en/cart/ 时,你能够在购物车下看到锐减的产品,就像下面这样:
恭喜你!你已经使用 Django 和 Redis 构建了一个完整的推荐引擎。
在这一章中,你使用会话建立了一个优惠券系统。你学习了国际化和本地化是如何工做的。你也使用 Redis 构建了一个推荐引擎。
在下一章中,你将开始一个新的项目。你将会用 Django 建立一个在线学习平台,而且使用基于类的视图(view)。你将学会建立一个定制的内容管理系统(CMS)。
(译者@夜夜月:- -下一章又轮到我了。。。。。。放出时间不固定。。。。。。)