美文网首页@IT·互联网程序员
第九章 扩展你的商店(下)

第九章 扩展你的商店(下)

作者: lakerszhy | 来源:发表于2017-05-17 17:18 被阅读164次

    9.2 添加国际化和本地化

    Django提供了完整的国际化和本地化支持。它允许你把应用翻译为多种语言,它会处理特定区域日期,时间,数字和时区。让我们弄清楚国际化和本地化的区别。国际化(通常缩写为i18n)是让软件适用于潜在的不同语言和地区的过程,让软件不会硬编码为特定语言和地区。本地化(缩写为l10n)是实际翻译软件和适应特定地区的过程。使用Django自己的国际化框架,它本身被翻译为超过50中语言。

    9.2.1 使用Django国际化

    国际化框架让你很容易的在Python代码和模板中编辑需要翻译的字符串。它依赖GNU gettext工具集生成和管理信息文件。一个信息文件是一个代表一种语言的普通文本文件。它包括你应用中的部分或全部需要翻译的字符串,以及相应的单种语言的翻译。信息文件的扩展名是.po

    一旦完成翻译,信息文件就会被编译,用来快速访问翻译后的字符串。被编译的翻译文件扩展名是.mo

    9.2.1.1 国际化和本地化设置

    Django为国际化提供了一些设置。以下是最相关的设置:

    • USE_I18N:指定Django的翻译系统是否可用的布尔值。默认为True
    • USE_L10N:表示本地格式是否可用的布尔值。可用时,用本地格式表示日期和数字。默认为False
    • USE_TZ:指定日期和时间是否时区感知的布尔值。当你用startproject创建项目时,该值设置为True
    • LANGUAGE_CODE:项目的默认语言代码。它使用标准的语言ID格式,比如en-us表示美式英语,en-gb表示英式英语。这个设置需要USE_I18N设为True才生效。你可以在这里查看有效地语言ID列表。
    • LANGUAGES:一个包括项目可用语言的元组。它由包括语言代码和语言名称的双元组构成。你可以在django.conf.global_settings中查看可用语言列表。当你选择你的网站将使用哪些语言时,你可以设置LANGUAGES为这个列表的一个子集。
    • LOCALE_PATHS:Django查找项目中包括翻译的信息文件的目录列表。
    • TIME_ZONE:表示项目时区的字符串。当你使用startproject命令创建新项目时,它设置为UTC。你可以设置它为任何时区,比如Europe/Madrid

    这是一些可用的国际化和本地化设置。你可以在这里查看完整列表。

    9.2.1.2 国际化管理命令

    使用manage.py或者django-admin工具管理翻译时,Django包括以下命令:

    • makemessages:它在源代码树上运行,查找所有标记为需要翻译的字符串,并在locale目录中创建或更新.po信息文件。每种语言创建一个.po文件。
    • compilemessages:编译存在的.po信息文件为.mo文件,用于检索翻译。

    你需要gettext工具集创建,更新和编译信息文件。大部分Linux发行版都包括了gettext工具集。如果你使用的是Mac OS X,最简单的方式是用brew install gettext命令安装。你可能还需要用brew link gettext --force强制链接到它。对于Windows安装,请参考这里的步骤。

    9.2.1.3 如果在Django项目中添加翻译

    让我们看下国际化我们项目的流程。我们需要完成以下工作:

    1. 我们标记Python代码和目录中需要编译的字符串。
    2. 我们运行makemessages命令创建或更新信息文件,其中包括了代码中所有需要翻译的字符串。
    3. 我们翻译信息文件中的字符串,然后用compilemessages管理命令编辑它们。

    9.2.1.4 Django如何决定当前语言

    Django自带一个中间件,它基于请求的数据决定当前语言。位于django.middleware.locale.LocaleMiddlewareLocaleMiddleware中间件执行以下任务:

    1. 如果你使用i18_patterns,也就是你使用翻译后的URL模式,它会在被请求的URL中查找语言前缀,来决定当前语言。
    2. 如果没有找到语言前缀,它会在当前用户会话中查询LANGUAGE_SESSION_KEY
    3. 如果没有在会话中设置语言,它会查找带当前语言的cookie。这个自定义的cookie名由LANGUAGE_COOKIE_NAME设置提供。默认情况下,该cookie名为django-language
    4. 如果没有找到cookie,它会查询请求的Accept-Language头。
    5. 如果Accept-Language头没有指定语言,Django会使用LANGUAGE_CODE设置中定义的语言。

    默认情况下,Django会使用LANGUAGE_CODE设置中定义的语言,除非你使用LocaleMiddleware。以上描述的过程只适用于使用这个中间件。

    9.2.2 为国际化我们的项目做准备

    让我们为我们的项目使用不同语言。我们将创建商店的英语和西拔牙语版本。编辑项目的settings.py文件,在LANGUAGE_CODE设置之后添加LANGUAGES设置:

    LANGUAGES = (
        ('en', 'English'),
        ('es', 'Spanish'),
    )
    

    LANGUAGES设置中包括两个元组,每个元组包括语言代码和名称。语言代码可以指定地区,比如en-usen-gb,也可以通用,比如en。在这个设置中,我们指定我们的应用只对英语和西班牙可用。如果我们没有定义LANGUAGES设置,则网站对于Django的所有翻译语言都可用。

    如下修改LANGUAGE_CODE设置:

    LANGUAGE_CODE = 'en'
    

    MIDDLEWARE设置中添加django.middleware.locale.LocaleMiddleware。确保这个中间件在SessionMiddleware之后,因为LocaleMiddleware需要使用会话数据。它还需要在CommonMiddleware之前,因为后者需要一个激活的语言解析请求的URL。MIDDLEWARE设置看起来是这样的:

    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.locale.LocaleMiddleware',
        'django.middleware.common.CommonMiddleware',
        # ...
    ]
    

    中间件的顺序很重要,因为每个中间件都依赖前面其它中间件执行后的数据集。中间件按MIDDLEWARE中出现的顺序应用在请求上,并且反序应用在响应上。

    在项目主目录中穿件以下目录结构,与manage.py同级:

    locale/
        en/
        es/
    

    locale目录是应用的信息文件存储的目录。再次编辑settings.py文件,在其中添加以下设置:

    LOCALE_PATHS = (
        os.path.join(BASE_DIR, 'locale/'),
    )
    

    LOCALE_PATHS设置指定了Django查找翻译文件的目录。最先出现的路径优先级最高。

    当你在项目目录中使用makemessages命令时,信息文件会在我们创建的locale/路径中生成。但是,对于包括locale/目录的应用来说,信息文件会在这个应用的locale/目录中生成。

    9.2.3 翻译Python代码

    要翻译Python代码中的字面量,你可以用django.utils.translation中的gettext()函数标记要翻译的字符串。这个函数翻译信息并返回一个字符串。惯例是把这个函数导入为短别名_

    你可以在这里查看所有关于翻译的文档。

    9.2.3.1 标准翻译

    以下代码展示了如何标记一个需要翻译的字符串:

    from django.utils.translation import gettext as _
    output = _('Text to be translated.')
    

    9.2.3.2 惰性翻译

    Django的所有翻译函数都包括惰性(lazy)版本,它们的后缀都是_lazy()。使用惰性函数时,当值被访问时翻译字符串,而不是惰性函数被调用时翻译(这就是为什么它们被惰性翻译)。当标记为翻译的字符串位于加载模式时执行的路径中,这些惰性翻译函数非常方便。

    使用gettext_lazy()代替gettext()时,当值被访问时翻译字符串,而不是翻译函数调用时翻译。Django为所有翻译函数提供了惰性版本。

    9.2.3.3 带变量的翻译

    标记为翻译的字符串的字符串可以包括占位符来在翻译中引入变量。以下代码是翻译带占位符的字符串:

    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 Abiril。当需要翻译的字符串中包括一个以上的参数时,总是使用字符串插值代替位置插值。这样就可以重新排列站位文本。

    9.2.3.4 翻译中的复数形式

    对于复数形式,你可以使用ngettext()ngettext_lazy()。这些函数根据一个表示对象数量的参数翻译单数和复数形式。下面的代码展示了如何使用它们:

    output = ngettext('there is %(count)d product',
                        'there are %(count)d products',
                        count) % {'count': count}
    

    现在你已经学会了翻译Python代码中字面量的基础,是时候翻译我们的项目了。

    9.2.3.5 翻译你的代码

    编辑项目的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 en
    processing locale es
    

    看一眼locale/目录,你会看这样的文件结构:

    en/
        LC_MESSAGES/
            django.po
    es/
        LC_MESSAGES/
            django.po
    

    为每种语言创建了一个.po信息文件。用文本编辑器打开es/LC_MESSAGES/django.po文件。在文件结尾,你会看到以下内容:

    #: myshop/settings.py:122
    msgid "English"
    msgstr ""
    
    #: myshop/settings.py:123
    msgid "Spanish"
    msgstr ""
    

    每个需要翻译的字符串前面都有一条注释,显示它位于的文件和行数。每个翻译包括两个字符串:

    • msgid:源代码中需要翻译的字符串。
    • msgstr:对应语言的翻译,默认为空。你需要在这里输入给定字符串的实际翻译。

    为给的msgid字符串填入msgstr翻译:

    #: myshop/settings.py:122
    msgid "English"
    msgstr "Inglés"
    
    #: myshop/settings.py:123
    msgid "Spanish"
    msgstr "Español"
    

    保存修改的信息文件,打开终端,执行以下命令:

    django-admin compilemessages
    

    如果一切顺利,你会看到类似这样的输出:

    processing file django.po in /Users/lakerszhy/Documents/GitHub/Django-By-Example/code/Chapter 9/myshop/locale/en/LC_MESSAGES
    processing file django.po in /Users/lakerszhy/Documents/GitHub/Django-By-Example/code/Chapter 9/myshop/locale/es/LC_MESSAGES
    

    输出告诉你信息文件已经编译。再看一眼myshop项目的locale目录,你会看到以下文件:

    en/
        LC_MESSAGES/
            django.mo
            django.po
    es/
        LC_MESSAGES/
            django.mo
            django.po
    

    你会看到为每种语言生成了一个编译后的.mo信息文件。

    我们已经翻译了语言名本身。现在让我们翻译在网站中显示的模型字段名。编辑orders应用的models.py文件,为Order模型字段添加需要翻译的名称标记:

    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(_('email'))
        address = models.CharField(_('address'), max_length=250)
        postal_code = models.CharField(_('postal code'), max_length=20)
        city = models.CharField(_('city'), max_length=100)
        # ...
    

    我们为用户下单时显示的字段添加了名称,分别是first_namelast_nameemailaddresspostal_codecity。记住,你也可以使用verbose_name属性为字段命名。

    orders应用中创建以下目录结构:

    locale/
        en/
        es/
    

    通过创建locale/目录,这个应用中需要翻译的字符串会存储在这个目录的信息文件中,而不是主信息文件。通过这种方式,你可以为每个应用生成独立的翻译文件。

    在项目目录打开终端,执行以下命令:

    django-admi makemessages --all
    

    你会看到以下输出:

    processing locale en
    processing locale es
    

    用文本编辑器开大es/LC_MESSAGES/django.po文件。你会看到Order模型需要翻译的字符串。为给的msgid字符串填入msgstr翻译:

    #: orders/models.py:10
    msgid "first name"
    msgstr "nombre"
    
    #: orders/models.py:11
    msgid "last name"
    msgstr "apellidos"
    
    #: orders/models.py:12
    msgid "email"
    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编辑翻译。Poedit是一个编辑翻译的软件,它使用gettext。它有Linux,Windows和Mac OS X版本。你可以在这里下载。

    让我们再翻译项目中的表单。orders应用的OrderCreateForm不需要翻译,因为它是一个ModelForm,它的表单字段标签使用了Order模型字段的verbose_name属性。我们将翻译cartcoupons应用的表单。

    编辑cart应用中的forms.py文件,为CartAddProductFormquantity字段添加一个lable属性,然后标记为需要翻译:

    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'))
    

    我们为code字段添加了label属性,并标记为需要翻译。

    9.2.4 翻译模板

    Django为翻译模板中的字符串提供了{% trans %}{% blocktrans %}模板标签。要使用翻译模板标签,你必须在模板开头添加{% load i18n %}加载它们。

    9.2.4.1 模板标签{% trans %}

    {% trans %}模板标签允许你标记需要翻译的字符串,常量或者变量。在内部,Django在给定的文本上执行gettext()。以下是在模板中标记需要翻译的字符串:

    {% trans "Text to be translated" %}
    

    你可以使用as在变量中存储翻译后的内容,然后就能在整个模板中使用这个变量。下面这个例子在greeting变量中存储翻译后的文本:

    {% trans "Hello!" as greeting %}
    <h1>{{ greeting }}</h1>
    

    {% trans %}标签对简单的翻译字符串很有用,但它不能处理包括变量的翻译内容。

    9.2.4.2 模板标签{% blocktrans %}

    {% blocktrans %}模板标签允许你标记包括字面量的内容和使用占位符的变量内容。下面这个例子展示了如何使用{% blocktrans %}标签标记一个包括name变量的翻译内容:

    {% blocktrans %}Hello {{ name }}!{% endblocktrans %}
    

    你可以使用with引入模板表达式,比如访问对象属性,或者对变量使用模板过滤器。这时,你必须总是使用占位符。你不能在blocktrans块中访问表达式或者对象属性。下面的例子展示了如何使用with,其中引入了一个对象属性,并使用capfirst过滤器:

    {% blocktrans with name=user.name|capfirst %}
        Hello {{ name }}!
    {% endblocktrans %}
    

    当需要翻译的字符串中包括变量内容时,使用{% blocktrans %}代替{% trans %}

    9.2.4.3 翻译商店的模板

    编辑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=otal_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(在这里使用模板标签)和cart.get_total_price(在这里访问对象方法)使用占位符,结果是:

    {% blocktrans with total_items_plural=otal_items|pluralize total_price=cart.get_total_price %}
        {{ total_items }} item{{ total_items_plural }},
       ${{ total_price }}
    {% endblocktrans %}
    

    接着编辑shop应用的shop/product/detai.html模板,在{% extends %}标签(它必须总是第一个标签)之后加载i18n标签:

    {% load i18n %}
    

    然后找到这一行:

    <input type="submit" value="Add to cart">
    

    替换为:

    <input type="submit" value="{% trans "Add to cart" %}">
    

    现在翻译orders应用的模板。编辑orders应用的orders/order/create.html模板,如下标记需要翻译的文本:

    {% extends "shop/base.html" %}
    {% load i18n %}
    
    {% block title %}
        {% trans "Checkout" %}
    {% endblock title %}
    
    {% 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 content %}
    

    在本章示例代码中查看以下文件是如何标记需要翻译的字符串:

    • shop应用:shop/product/list.hmtl模板
    • orders应用:orders/order/created.html模板
    • cart应用:cart/detail.html模板

    让我们更新信息文件来引入新的翻译字符串。打开终端,执行以下命令:

    django-admin makemessages --all
    

    .po翻译文件位于myshop项目的locale目录中,orders应用现在包括了所有我们标记过的翻译字符串。

    编辑项目和orders应用的.po翻译文件,并填写西班牙语翻译。你可以参考本章示例代码的.po文件。

    从项目目录中打开终端,并执行以下命令:

    cd orders/
    django-admin compilemessages
    cd ../
    

    我们已经编译了orders应用的翻译文件。

    执行以下命令,在项目的信息文件中包括没有locale目录的应用的翻译。

    django-admin compilemessage
    

    9.2.5 使用Rosetta的翻译界面

    Rosetta是一个第三方应用,让你可以用跟Django管理站点一样的界面编辑翻译。Rosetta可以很容易的编辑.po文件,并且它会更新编译后的编译文件。让我们把它添加到项目中。

    使用pip命令安装Rosetta:

    pip install django-rosetta
    

    然后把rosetta添加到项目settings.py文件中的INSTALLED_APP设置中:

    你需要把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列中输入翻译。Occurrences列显示每个需要翻译的字符串所在的文件和行数。

    包括占位符的翻译是这样的:

    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文件。保存翻译时,Rosseta会编译信息文件,所以不需要执行compilemessages命令。但是Rosetta需要写入locale目录的权限来写入信息文件。确保这些目录有合理的权利。

    如果你希望其他用户也可以编辑翻译,在浏览器中打开http://127.0.0.1:8000/admin/auth/group/add/,并创建一个translators组。然后访问http://127.0.0.1:8000/admin/auth/user/,编辑你要授予翻译权限的用户。编辑用户时,在Premissions中,把translators组添加到每个用户的Chosen Groups中。Resetta只对超级用户和属于translators组的用户可用。

    你可以在这里阅读Rosetta的文档。

    当你在生产环境添加新翻译时,如果你的Django运行在一个真实的web服务器上,你必须在执行compilemessages命令或者用Rosetta保存翻译之后重启服务器,才能让修改生效。

    9.2.6 不明确的翻译

    你可能已经注意到了,在Rosetta中有一个Fuzzy列。这不是Rosetta的特征,而是有gettext提供的。如果启用了翻译的fuzzy标记,那么它就不会包括在编译后的信息文件中。这个标记用于需要翻译者修改的翻译字符串。当用新的翻译字符串更新.po文件中,可能有些翻译字符串自动标记为fuzzy。当gettext发现某些msgid变动不大时,它会匹配为旧的翻译,并标记为fuzzy,以便复核。翻译者应该复核不明确的翻译,然后移除fuzzy标记,并在此编译信息文件。

    9.2.7 URL模式的国际化

    Django为URL提供了国际化功能。它包括两种主要的国际化URL特性:

    • URL模式中的语言前缀:把语言前缀添加到URL中,在不同的基础URL下提供每种语言的版本
    • 翻译后的URL模式:标记需要翻译的URL模式,因此同一个URL对于每种语言是不同的

    翻译URL的其中一个原因是为搜索引擎优化你的网站。通过在模式中添加语言前缀,你就可以为每种语言提供索引URL,而不是为所有语言提供一个索引URL。此外,通过翻译URL为不同语言,你可以为搜索引擎提供对每种语言排名更好的URL。

    9.2.7.1 添加语言前缀到URL模式中

    Django允许你在URL模式中添加语言前缀。例如,网站的英语版本可以/en/起始路径下,而西班牙语版本在/es/下。

    要在URL模式中使用语言,你需要确保settings.py文件的MIDDLEWARE设置中包括django.middleware.locale.LocaleMiddleware。Django将用它从请求URL中识别当前语言。

    让我们在URL模式中添加语言前缀。编辑myshop项目的urls.py文件,添加以下导入:

    from django.conf.urls.i18n import i18n_patterns
    

    然后添加i18n_patterns(),如下所示:

    urlpatterns = i18n_patterns(
        url(r'^admin/', admin.site.urls),
        url(r'^cart/', include('cart.urls', namespace='cart')),
        url(r'^orders/', include('orders.urls', namespace='orders')),
        url(r'^paypal/', include('paypal.standard.ipn.urls')),
        url(r'^payment/', include('payment.urls', namespace='payment')),
        url(r'^coupons/', include('coupons.urls', namespace='coupons')),
        url(r'^rosetta/', include('rosetta.urls')),
        url(r'^', include('shop.urls', namespace='shop')),
    )
    

    你可以在patterns()i18n_patterns()中结合URL模式,这样有些模式包括语言前缀,有些不包括。但是,最好只使用翻译后的URL,避免不小心把翻译后的URL匹配到没有翻译的URL模式。

    启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/。因为你使用了LocaleMiddleware中间件,所以Django会执行Django如何决定当前语言中描述的步骤,决定当前的语言,然后重定义到包括语言前缀的同一个URL。看一下眼浏览器中的URL,它应该是http://127.0.0.1:8000/en/。如果浏览器的Accept-Language头是西班牙语或者英语,则当前语言是它们之一;否则当前语言是设置中定义的默认LANGUAGE_CODE(英语)。

    9.2.7.2 翻译URL模式

    Django支持URL模式中有翻译后的字符串。对应单个URL模式,你可以为每种语言使用不同的翻译。你可以标记需要翻译的URL模式,方式与标记字面量一样,使用gettext_lazy()函数。

    编辑myshop项目的主urls.py文件,把翻译字符串添加到cartorderspaymentcoupons应用的URL模式的正则表达式中:

    urlpatterns = i18n_patterns(
        url(r'^admin/', admin.site.urls),
        url(_(r'^cart/'), include('cart.urls', namespace='cart')),
        url(_(r'^orders/'), include('orders.urls', namespace='orders')),
        url(r'^paypal/', include('paypal.standard.ipn.urls')),
        url(_(r'^payment/'), include('payment.urls', namespace='payment')),
        url(_(r'^coupons/'), include('coupons.urls', namespace='coupons')),
        url(r'^rosetta/', include('rosetta.urls')),
        url(r'^', include('shop.urls', namespace='shop')),
    )
    

    编辑orders应用的urls.py文件,编辑需要翻译的URL模式:

    from django.utils.translation import gettext_lazy as _
    
    urlpatterns = [
        url(_(r'^create/$'), views.order_create, name='order_create'),
        # ..
    ]
    

    编辑payment应用的urls.py文件,如下修改代码:

    from django.utils.translation import gettext 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模式,因为它们由变量构建,不包括任何字面量。

    打开终端,执行以下命令更新信息文件:

    django-admin makemessages --all
    

    确保开发服务器正在运行。在浏览器中打开http://127.0.0.1:8000/en/rosetta/,然后点击Spanish中的Myshop链接。你可以使用Display过滤器只显示没有翻译的字符串。在URL翻译中,一定要保留正则表达式中的特殊字符。翻译URL是一个精细的任务;如果你修改了正则表达式,就会破坏URL。

    9.2.8 允许用户切换语言

    因为我们现在提供了多种语言,所以我们应该让用户可以切换网站的语言。我们会在网站中添加一个语言选择器。语言选择器用链接显示可用的语言列表。

    编辑shop/base.html模板,找到以下代码:

    <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 "Languages" %}:</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 %}模板标签获得LANGUAGES设置中定义的语言。
    4. 我们用{% get_language_info_list %}标签提供访问语言属性的便捷方式。
    5. 我们构建HTML列表显示所有可用的语言,并在当前激活语言上添加selected类属性。

    我们用i18n提供的模板标签,根据项目设置提供可用的语言。现在打开http://127.0.0.1:8000/。你会看到网站右上角有语言选择器,如下图所示:

    用户现在可以很容易的切换语言。

    9.2.9 用django-parler翻译模型

    Django没有为翻译模型提供好的解决方案。你必须实现自己的解决方案来管理不同语言的内容,或者使用第三方模块翻译模型。有一些第三方应用允许你翻译模型字段。每种采用不同的方法存储和访问翻译。其中一个是django-parler。这个模块提供了一种非常高效的翻译模型的方式,并且它和Django管理站点集成的非常好。

    django-parler为每个模型生成包括翻译的独立的数据库表。这张表包括所有翻译后的字段,以及一个翻译所属的原对象的外键。因为每行存储单个语言的内容,所以它还包括一个语言字段。

    9.2.9.1 安装django-parler

    使用pip命令安装django-parler

    pip install django-parler
    

    然后编辑项目的settings.py文件,把parler添加到INSTALLED_APPS设置中。并在设置文件中添加以下代码:

    PARLER_LANGUAGES = {
        None: (
            {'code': 'en', },
            {'code': 'es', },
        ),
        'default': {
            'fallback': 'en',
            'hide_untranslated': False,
        }
    }
    

    这个设置定义了django-parler的可用语言enes。我们指定默认语言是en,并且指定django-parler不隐藏没有翻译的内容。

    9.2.9.2 翻译模型字段

    让我们为商品目录添加翻译。django-parler提供了一个TranslatableModel模型类和一个TranslatedFields包装器(wrapper)来翻译模型字段。编辑shop应用的models.py文件,添加以下导入:

    from parler.models import TranslatableModel, TranslatedFields
    

    然后修改Category模型,让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模型继承自TranslatableModel,而不是models.Model。并且nameslug字段都包括在TranslatedFields包装器中。

    编辑Product模型,为nameslugdescription字段添加翻译。同样保留非翻译字段:

    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为每个TranslatableModel模型生成另一个模型。图9.9中,你可以看到Product模型的字段和生成的ProductTranslation模型:

    django-parler生成的ProductTranslation模型包括nameslugdescription可翻译字段,一个language_code字段,以及指向Product对象的外键master字段。从ProductProductTranslation是一对多的关系。每个Product对象会为每种语言生成一个ProductTranslation对象。

    因为Django为翻译使用了单独的数据库表,所以有些Django特性不能使用了。一个翻译后的字段不能用作默认的排序。你可以在查询中用翻译后的字段过滤,但你不能再ordering元选项中包括翻译后的字段。编辑shop应用的models.py文件,注释Category类中Meta类的ordering属性:

    class Meta:
        # ordering = ('name', )
        verbose_name = 'category'
        verbose_name_plural = 'categories'
    

    我们还必须注释Product类中Meta类的index_together属性,因为当前django-parler版本不提供验证它的支持:

    class Meta:
        ordering = ('-created', )
        # index_together = (('id', 'slug'), )
    

    你可以在这里阅读更多关于django-parler和Django兼容性的信息。

    9.2.9.3 创建自定义数据库迁移

    当你为翻译创建了新模型,你需要执行makemigrations命令为模型生成数据库迁移,然后同步到数据库中。但是当你将已存在字段变为可翻译后,你的数据库中可能已经存在数据了。我们将把当前数据迁移到新的翻译模型中。因此,我们添加了翻译后的字段,但暂时保留了原来的字段。

    为已存在字段添加翻译的流程是这样的:

    1. 我们为新的可翻译模型字段创建数据库迁移,并保留原来的字段。
    2. 我们构建一个自定义数据库迁移,从已存在字段中拷贝数据到翻译模型中。
    3. 我们从原来的模型中移除已存在的字段。

    执行以下命令,为添加到CategoryProduct模型中的翻译字段创建数据库迁移:

    python manage.py makemigrations shop --name "add_translation_model"
    

    你会看到以下输出:

    Migrations for 'shop':
      shop/migrations/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))
    

    现在我们需要创建一个自定义数据库迁移,把已存在的数据拷贝到新的翻译模型中。使用以下命令创建一个空的数据库迁移:

    python manage.py makemigrations --empty shop --name "migrate_translatable_fields"
    

    你会看到以下输出:

    Migrations for 'shop':
      shop/migrations/0003_migrate_translatable_fields.py
    

    编辑shop/migrations/0003_migrate_translatable_fields.py文件,并添加以下代码:

    # -*- coding: utf-8 -*-
    # Generated by Django 1.11.1 on 2017-05-17 01:18
    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, shcema_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):
        translation = MyModelTranslation.objects.filter(master_id=obj.pk)
        try:
            # Try default translation
            return translation.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字典中定义模型和可翻译的字段。
    2. 要同步迁移,我们用app.get_model()迭代包括翻译的模型,来获得模型和它可翻译的模型类。
    3. 我们迭代数据库中所有存在的对象,并为项目设置中定义的LANGUAGE_CODE创建一个翻译对象。我们包括了一个指向原对象的ForeignKey,以及从原字段中拷贝的每个可翻译字段。

    backwards_func()函数执行相反的操作,它查询默认的翻译对象,并把可翻译字段的值拷贝回原对象。

    我们已经创建了一个数据库迁移来添加翻译字段,以及一个从已存在字段拷贝内容到新翻译模型的迁移。

    最后,我们需要删除不再需要的原字段。编辑shop应用的models.py文件,移除Category模型的nameslug字段。现在Category模型字段是这样的:

    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模型的nameslugdescription字段。它现在是这样的:

    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工具,我们会看到一个错误,因为我们还没有让管理站点适配可翻译模型。让我们先修改管理站点。

    9.2.9.4 在管理站点集成翻译

    Django管理站点可以很好的跟django-parler集成。django-parler包括一个TranslatableAdmin类,它覆写了Django提供的ModelAdmin类,来管理模型翻译。

    编辑shop应用的admin.py文件,添加以下导入:

    from parler.admin import TranslatableAdmin
    

    修改CategoryAdminProductAdmin类,让它们从TranslatableAdmin继承。django-parler还不知道prepopulated_fields属性,但它支持相同功能的get_ prepopulated_fields()方法。让我们相应的修改,如下所示:

    from django.contrib import admin
    from parler.admin import TranslatableAdmin
    from .models import Category, Product
    
    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', 'price', 'stock', 'available', 'created', 'updated')
        list_filter = ('available', 'created', 'updated')
        list_editable = ('price', 'stock', 'available')
    
        def get_prepopulated_fields(self, request, obj=None):
            return {'slug': ('name', )}
            
    admin.site.register(Product, ProductAdmin)
    

    我们已经让管理站点可以与新的翻译模型一起工作了。现在可以同步模型修改到数据库中。

    9.2.9.5 为模型翻译同步数据库迁移

    适配管理站点之前,我们已经从模型中移除了旧的字段。现在我们需要为这个修改创建一个迁移。打开终端执行以下命令:

    python manage.py makemigrations shop --name "remove_untranslated_fields"
    

    你会看到以下输出:

    Migrations for 'shop':
      shop/migrations/0004_remove_untranslated_fields.py
        - Change Meta options on product
        - 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. 添加可翻译字段到模型中
    2. 从原字段迁移已存在字段到可翻译字段
    3. 从模型中移除原字段

    执行以下命令,同步我们创建的三个迁移:

    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
    

    现在模型已经跟数据库同步了。让我们翻译一个对象。

    使用python manage.py runserver启动开发服务器,然后在浏览器中打开http://127.0.0.1:8000/en/admin/shop/category/add/。你会看到Add category页面包括两个标签页,一个英语和一个西班牙语翻译:

    现在你可以添加一个翻译,然后点击Save按钮。确保切换标签页之前保存修改,否则输入的信息会丢失。

    9.2.9.6 为翻译适配视图

    我们必须让shop的视图适配翻译的QuerySet。在命令行中执行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()执行QeurySet时,你可以在相关的翻译对象上用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>]
    

    正如你所看到的,访问和查询翻译字段非常简单。

    让我们适配商品目录视图。编辑shop应用的views.py文件,在product_list视图中找到这一行代码:

    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视图,找到这一行代码:

    product = get_object_or_404(Product, id=id, slug=slug, available=True)
    

    替换为以下代码:

    language = request.LANGUAGE_CODE
    product = 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/。你会看到商品列表页面,所有商品都已经翻译为西班牙语:

    现在用slug字段构建的每个商品的URL已经翻译为当前语言。例如,一个商品的西班牙语URL是http://127.0.0.1:8000/es/1/te-negro/,而英语的URL是http://127.0.0.1:8000/en/1/black-tea/。如果你导航到一个商品的详情页面,你会看到翻译后的URL和选中语言的内容,如下图所示:

    如果你想进一步了解django-parler,你可以在这里找到所有文档。

    你已经学习了如何翻译Python代码,模板,URL模式和模型字段。要完成国际化和本地化过程,我们还需要显示本地化格式的日期,时间和数组。

    9.2.10 格式的本地化

    根据用户的地区,你可能希望以不同格式显示日期,时间和数字。修改项目的settings.py文件中的USE_L10N设置为True,可以启动本地化格式。

    启用USE_L10N后,当Django在模板中输出值时,会尝试使用地区特定格式。你可以看到,你的英文版网站中的十进制用点号分隔小数部分,而不在西班牙版本中显示为逗号。这是因为Django为es地区指定了地区格式。你可以在这里查看西班牙格式配置。

    通常你会设置USE_L10NTrue,让Django为每个地区应用本地化格式。但是,有些情况下你可能不想使用地区化的值。当输出必须提供机器可读的JavaScript或JSON时,这一点尤其重要。

    Django提供了{% localize %}模板标签,运行你在模板块中开启或关闭本地化。这让你可以控制格式的本地化。要使用这个模板标签,你必须加载l10n标签。下面这个例子展示了如何在模板中开启或关闭本地化:

    {% load l10n %}
    
    {% localize on %}
        {{ value }}
    {% endlocalize %}
    
    {% localize off %}
        {{ value }}
    {% endlocalize %}
    

    Django还提供了localizeunlocalize模板过滤器,强制或避免本地化一个值,如下所示:

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

    你还可以创建自定义格式过滤器来指定本地格式。你可以在这里查看更多关于格式本地化的信息。

    9.2.11 用django-localflavor验证表单字段

    django-localflavor是一个第三方模板,其中包含一组特定用途的功能,比如每个国家特定的表单字段或模型字段。验证本地区域,本地电话号码,身份证,社会安全号码等非常有用。这个包由一系列以ISO 3166国家代码命名的模块组成。

    用以下命令安装django-localflavor:

    pip install django-localflavor
    

    编辑项目的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字段,并把它用于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的自定义字段进行验证。localflavor提供的本地组件对于让你的应用适应特定国家非常有用。你可以在这里阅读django-localflavor文档,查看每个国家所有可用的本地组件。

    接下来,我们将在商店中构建一个推荐引擎。

    9.3 构建推荐引擎

    推荐引擎是一个预测用户对商品的偏好或评价的系统。系统根据用户行为和对用户的了解选择商品。如今,很多在线服务都使用推荐系统。它们帮助用户从大量的可用数据中选择用户可能感兴趣的内容。提供良好的建议可以增强用户参与度。电子商务网站还可以通过推荐相关产品提高销量。

    我们将创建一个简单,但强大的推荐引擎,来推测用户通常会一起购买的商品。我们将根据历史销售确定通常一起购买的商品,来推荐商品。我们将在两个不同的场景推荐补充商品:

    • 商品详情页面:我们将显示一个通常与给定商品一起购买的商品列表。它会这样显示:购买了这个商品的用户还买了X,Y,Z。我们需要一个数据结构,存储每个商品与显示的商品一起购买的次数。
    • 购物车详情页面:根据用户添加到购物车中的商品,我们将推荐通常与这些商品一起购买的商品。这种情况下,我们计算的获得相关商品的分数必须汇总。

    我们将使用Redis存储一起购买的商品。记住,你已经在第六章中使用了Redis。如果你还没有安装Redis,请参考第六章。

    9.3.1 根据之前的购买推荐商品

    现在,我们将根据用户已经添加到购物车中的商品来推荐商品。我们将在Redis中为网站中每个出售的商品存储一个键。商品键会包括一个带评分的Redis有序集。每次完成一笔新的购买,我们为每个一起购买的商品的评分加1。

    当一个订单支付成功后,我们为购买的每个商品存储一个键,其中包括属于同一个订单的商品有序集。这个有序集让我们可以为一起购买的商品评分。

    编辑项目的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:
        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键,看起来是这样的:product:[id]:purchased_with

    product_bought()方法接收一个一起购买(也就是属于同一个订单)的Product对象列表。我们在这个方法中执行以下任务:

    1. 我们获得给定的Product对象的商品ID。
    2. 我们迭代商品ID。对于每个ID,我们迭代商品ID,并跳过同一个商品,这样我们获得了与每个商品一起购买的商品。
    3. 我们用get_product_id()方法获得每个购买的商品的Redis商品键。对于一个ID是33的商品,这个方法返回的键是product:33:purchased_with。这个键用于包括与这个商品一起购买的商品ID的有序集。
    4. 我们将ID包含在有序集中的商品评分加1。这个评分表示其它商品与给定商品一起购买的次数。

    因此这个方法可以保存一起购买的商品,并对它们评分。现在,我们需要一个方法检索与给定的商品列表一起购买的商品。在Recommender类中添加suggest_products_for()方法:

    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 sored 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_products_for()方法接收以下参数:

    • products:要获得推荐商品的商品列表。它可以包括一个或多个商品。
    • max_results:一个整数,表示返回的推荐商品的最大数量。

    在这个方法中,我们执行以下操作:

    1. 我们获得给定商品对象的商品ID。
    2. 如果只给定了一个商品,我们检索与该商品一起购买的商品ID,并按它们一起购买的总次数排序。我们用Redis的ZRANGE命令进行排序。我们限制结果数量为max_results参数指定的数量(默认是6)。
    3. 如果给定的商品多余1个,我们用商品ID生成一个临时的Redis键。
    4. 我们组合每个给定商品的有序集中包括的商品,并求和所有评分。通过Redis的ZUNIONSTORE命令实现这个操作。ZUNIONSTORE命令用给定的键执行有序集的并集,并在新的Redis键中存储元素的评分总和。你可以在这里阅读更多关于这个命令的信息。我们在一个临时键中存储评分和。
    5. 因为我们正在汇总评分,所以我们得到的有可能是正在获得推荐商品的商品。我们用ZREM命令从生成的有序集中移除它们。
    6. 我们从临时键中检索商品ID,并用ZRANGE命令根据评分排序。我们限制结果数量为max_results参数指定的数量。然后我们移除临时键。
    7. 最后,我们用给定的ID获得Product对象,并按ID同样的顺序进行排序。

    为了更实用,让我们再添加一个清除推荐的方法。在Recommender类中添加以下方法:

    def clear_purchases(self):
        for id in Product.objects.values_list('id', flat=True):
            r.delete(self.get_product_key(id))
    

    让我们试试推荐引擎。确保数据库中包括几个Product对象,并在终端使用以下命令初始化Redis服务:

    src/redis-server
    

    打开另一个终端,执行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_teared_tea的推荐商品是tea_powder(2+1)green_tea(1+1)

    我们已经确认推荐算法如期工作了。让我们为网站的商品显示推荐。

    编辑shop应用的views.py文件,并添加以下导入:

    from .recommender import Recommender
    

    product_detail()视图的render()函数之前添加以下代码:

    r = Recommender()
    recommended_products = r.suggest_products_for([product], 4)
    

    我们最多获得4个推荐商品。现在product_detail视图如下所示:

    from .recommender import Recommender
    
    def product_detail(request, id, slug):
        language = request.LANGUAGE_CODE
        product = get_object_or_404(
            Product, 
            id=id, 
            translations__language_code=language,
            translations__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模板,在{{ 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 }}">
                        ![]({% if p.image %}{{ p.image.url }}{% else %}{% static )
                    </a>
                    <p><a href="{{ p.get_absolute_url }}">{{ p.name }}</a></p>
                </div>
            {% endfor %}
        </div>
    {% endif %}
    

    使用python manage.py runserver启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/en/。点击任何一个商品显示详情页面。你会看到商品下面的推荐商品,如下图所示:

    接下来我们在购物车中包括商品推荐。基于用户添加到购物车中的商品生成推荐商品。编辑cart应用的views.py文件,添加以下导入:

    from shop.recommender import Recommender
    

    然后编辑cart_detail视图,如下所示:

    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': recommended_products
            }
        )
    

    编辑cart应用的cart/detail.html模板,在</table>标签之后添加以下代码:

    {% 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 }}">
                        ![]({% if p.image %}{{ p.image.url }}{% else %}{% static )
                    </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构建了一个完整的推荐引擎。

    9.4 总结

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

    在下一章中,你会开始一个新的项目。你会通过Django使用基于类的视图构建一个在线学习平台,你还会创建一个自定义的内容管理系统。

    相关文章

      网友评论

        本文标题:第九章 扩展你的商店(下)

        本文链接:https://www.haomeiwen.com/subject/wkndxxtx.html