《Django By Example》第九章 下 中文 翻译 (个人学习,渣翻)

全文链接

第一章 创建一个blog应用
第二章 使用高级特性来增强你的blog
第三章 扩展你的blog应用
第四章上 创建一个社交网站
第四章下 创建一个社交网站
第五章 在你的网站中分享内容
第六章 跟踪用户动作
第七章 建立一个在线商店
第八章 管理付款和订单
第九章上 扩展你的商店
第九章下 扩展你的商店
第十章上 创建一个在线学习平台
第十章下 创建一个在线学习平台
第十一章 缓存内容
第十二章 构建一个API

书籍出处:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé

(审校@夜夜月:本章分上下两部分,这里是下半部。)

(译者@ucag 注:哈哈哈,第九章终于来啦。这是在线商店的最后一章,下一章将会开始一个新的项目。所以这一章的内容会偏难,最难的是推荐引擎的编写,其中的算法可能还需要各位好好的推敲,如果看了中文的看不懂,大家可以去看看英文原版的书以及相关的文档。最近我也在研究机器学习,有兴趣的大家可以一起交流哈~)

(审校@夜夜月:大家好,我是来打酱油的~,粗校,主要更正了一些错字和格式,精校正进行到四章样子。)

第九章(下)

使用 Rosetta 翻译交互界面

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/ 。你可以看到当前语言列表:

django-9-5

Filter 模块,点击 All 来展示所有可用的信息文件,包括属于 orders 应用的信息文件。点击Spanish模块下的 Myshop 链接来编辑西班牙语翻译。你可以看到翻译字符串的列表:

django-9-6

你可以在 Spanish 行下输入翻译。Occurences 行展示了代码中翻译字符串被找到的文件和行数、

包含占位符的翻译看起来像这样:

django-9-7

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 标签然后再次编译信息文件。

国际化的 URL 模式

Django 提供了用于国际化的 URLs。它包含两种主要用于国际化的 URLs:

  • URL 模式中的语言前缀:把语言的前缀添加到 URLs 当中,以便在不同的基本 URL 下提供每一种语言的版本。
  • 翻译后的 URL 模式:标记要翻译的 URL 模式,这样同样的 URL 就可以服务于不同的语言。

翻译 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) 中的默认设置。

翻译 URL 模式

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-9-8

用户现在可以轻易的切换他们的语言了。

使用 django-parler 翻译模型(models)

Django 没有提供开箱即用的模型(models)翻译的解决办法。你必须要自己实现你自己的解决办法来管理不同语言中的内容或者使用第三方模块来管理模型(model)翻译。有几种第三方应用允许你翻译模型(model)字段。每一种手采用了不同的方法保存和连接翻译。其中一种应用是 django-parler 。这个模块提供了非常有效的办法来翻译模型(models)并且它和 Django 的管理站点整合的非常好。

django-parler 为每一个模型(model)生成包含翻译的分离数据库表。这个表包含了所有的翻译的字段和源对象的外键翻译。它也包含了一个语言字段,一位内每一行都会保存一个语言的内容。

**安装 django-parler **

使用 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 定义了可用语言 enes 。我们指定默认语言为 en ,然后我们指定 django-parler 应该隐藏未翻译的内容。

翻译模型(model)字段

让我们为我们的产品目录添加翻译。 django-parler 提供了 TranslatedModel 模型(model)类和 TranslatedFields 闭包(wrapper)来翻译模型(model)字段。编辑 shop 应用路径下的 models.py 文件,添加以下导入:

from parler.models import TranslatableModel, TranslatedFields

然后更改 Category 模型(model)来使 nameslug 字段可以被翻译。
我们依然保留了每行未翻译的字段:

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。并且 nameslug 字段都被引入到了 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-9-9

django-parler 生成的 ProductTranslation 模型(model)有 name,slug,description 可翻译字段,一个 language_code 字段,和主要的 Product 对象的 ForeignKey 字段。ProductProductTranslation 之间有一个一对多关系。一个 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)中删除字段

运行下面的命令来为我们添加到 CategoryProduct 模型(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)的 nameslug 字段:

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)的 nameslug 字段:

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

CategoryAdminProductAdmin 的继承类改为 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)迁移

我们在变更管理站点之前已经从模型(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 页,一个标签是英文的,一个标签是西班牙语的。

django-9-10

你现在可以添加一个翻译然后点击Save按钮。确保你在切换标签之前保存了他们,不然让门就会丢失。

使视图(views)适应翻译

我们要使我们的 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_listproduct_detail 已经可以使用翻译字段来检索了。运行开发服务器,访问:http://127.0.0.1:8000/es/ ,你可以看到产品列表页,包含了所有被翻译为西班牙语的产品:

django-9-11

现在每个产品的 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-9-12

如果你想要更多了解 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 同样也提供了 localizeunlocalize 模板(template)过滤器来强制或取消一个值的本地化。过滤器可以像下面这样使用:

{{ value|localize }}
{{ value|unlocalize }}

你也可以创建定制化的格式文件来指定特定语言的格式。你可以在这个网站找到所有关于本第格式化的信息:https://docs.djangoproject.com/en/1.8/topics/i18n/formatting/

使用 django-localflavor 来验证表单字段

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',]

我们从 localflavorus 包中引入了 USZipCodeField 然后将它应用在 OrderCreateFormpostal_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/

下面,我们将会为我们的店铺创建一个推荐引擎。

创建一个推荐引擎

推荐引擎是一个可以预测用户对于某一物品偏好和比率的系统。这个系统会基于用户的行为和它对于用户的了解选出相关的商品。如今,推荐系统用于许多的在线服务。它们帮助用户从大量的相关数据中挑选他们可能感兴趣的产品。提供良好的推荐可以鼓励用户更多的消费。电商网站受益于日益增长的相关产品推荐销售中。

我们将会创建一个简单但强大的推荐引擎来推荐经常一起购买的商品。我们将基于历史销售来推荐商品,这样就可以辨别出哪些商品通常是一起购买的了。我们将会在两种不同的场景下推荐互补的产品:

  • 产品详情页:我们将会展示和所给商品经常一起购买的产品列表。比如像这样展示:Users who bought this also bought X, Y, Z(购买了这个产品的用户也购买了X, Y,Z)。我们需要一个数据结构来让我们能储被展示产品中和每个产品一起购买的次数。
  • 购物车详情页:基于用户添加到购物车当中的产品,我们将会推荐经常和他们一起购买的产品。在这样的情况下,我们计算的包含相关产品的评分一定要相加。

我们将会使用 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/ 。点击一个产品来查看它的详情页。你应该看到展示在下方的推荐产品,就象这样:

django-9-13

我们也将在购物车当中引入产品推荐。产品推荐将会基于用户购物车中添加的产品。编辑 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-9-14

恭喜你!你已经使用 Django 和 Redis 构建了一个完整的推荐引擎。

总结

在这一章中,你使用会话创建了一个优惠券系统。你学习了国际化和本地化是如何工作的。你也使用 Redis 构建了一个推荐引擎。

在下一章中,你将开始一个新的项目。你将会用 Django 创建一个在线学习平台,并且使用基于类的视图(view)。你将学会创建一个定制的内容管理系统(CMS)。

(译者@夜夜月:- -下一章又轮到我了。。。。。。放出时间不固定。。。。。。)

推荐阅读更多精彩内容