一、定义 Django 独立应用的范围
每个软件项目都是由边界定义的,不管你是否有意选择了它们。在这一章中,我们将通过探索开发——和共享——你的独立应用的好处,开始我们的 Django 独立应用的冒险,以及如何考虑带来什么其他依赖,甚至你的 Django 应用是否应该是一个 Django 应用。
创建独立应用的优势
创建一个 Django 独立应用的好处是多种多样的,有利他的,利己的,也有纯粹实用的。
分享您的作品
假设你计划将你的应用作为开源包发布,大多数人想到的第一个好处是与他人分享你的工作的价值。在软件社区中,共享代码已经有很长的历史了(只要还有囤积的历史!)因为在大多数情况下,这是一种其他人可以从你已经做的事情中受益,而不会让你失去任何机会的方式。除非你认为你能够授权和销售你的 Django 应用,否则它本身不太可能是一个盈利的企业。既然你已经从别人免费分享的作品中获益,为什么不分享回来呢?
提高代码质量
一旦你开始分享你的工作,一个连锁反应往往是你的应用本身质量的提高。大多数人第一次公开分享他们的作品时会感到有点紧张,因为现在每个人都可以仔细检查。但是知道其他人会阅读和/或使用你的代码会让你更加小心你如何设计和开发它。一旦有足够多的人使用它,你将开始吸引其他人的贡献,无论是以错误报告的形式(是的,识别错误是一种贡献!)、错误修复、功能建议和新功能。
不要重复你自己
如果你正在寻找更实际的原因,是时候开始思考应用“可重用”的含义了作为开发人员,拥有可重用的代码可以节省时间和降低风险。您可以使用已经创建的内容和您知道已经有效的内容。
想象一下,如果每次创建一个新的 Django 项目,您都必须从头开始编写自己的认证系统,包括视图、中间件和模型。不可避免的是,您可能只是将代码从一个项目复制并粘贴到另一个项目,希望留下错误,并且从来没有真正的最佳实践集合。谢天谢地,你不必这么做,因为 Django 的 contrib 模块中就有这样一个可重用的应用。
这就谈到了风险的问题。代码中的风险可以从 bug 到你没有想到的重要特性,并且,所有的事情都是平等的,当你从零开始独自工作时,这些风险会增加。我们从可重用应用中寻求的不仅仅是从从头开始编写代码中节省的时间,还有最佳实践的积累,来自真实世界使用的功能,以及有机会被消除的错误。
整个公司的通用性
对于支持各种已部署的 Django 应用的开发人员和团队来说,支持一个公司的一个产品或多个产品,例如,将通用功能提取到可重用的应用中,这是一种确保功能在任何地方都能按预期工作的方式,并使您能够修复 bug 或添加关键的新功能,以便可以在任何使用它的地方部署版本更新。
示例包括定制的用户应用、特定领域的模型(例如,健康科学、金融)、资产管理和通用工具。
客户项目的共性
对于自由职业者或代理角色的开发人员和团队来说,与各种客户和项目一起工作,可重用的应用是跨项目携带有用功能的一种方式。
Django 中大多数流行的内容管理和电子商务解决方案都是这样产生的。Django CMS 和 Wagtail 是两个流行的基于 Django 的内容管理系统,也是独立的 Django 应用,由创意机构开发,作为客户项目的解决方案。
声望的流通
最后,虽然通常不言而喻,但有一个受欢迎的或至少有用的独立应用,这是一种可感知的声望——我们不会用“虚荣”这个词。对于产品团队、代理团队和自由职业者来说,一个共享的独立应用是能力、观点和视野的展示。它可以用来吸引潜在的雇员和潜在的客户。
前面提到的内容管理系统 Django CMS 和 Wagtail 分别由 Divio 和 Torchbox 创建。他们做了伟大的工作,但是有多少 Django 开发者会知道他们的名字不是为了他们各自 CMS 的贡献?他们在招聘中的知名度现在肯定有所提高。我自己不能说出任何特别的恶名,但是由于我自己发布的 Django 应用,一些客户直接或间接地发现了我。
现在,如果这是你想创建和发布 Django 独立应用的唯一原因,我建议你把注意力转移到别的地方。拥有一个被大量人使用的应用的连锁效应可能会慢慢积累,你也不太可能喜欢构建和维护应用的过程,你的用户群也可能会受到影响。
但是,如果你的目标包括与社区共享、创建不断改进的代码库、减少开发时间和风险,那么不管你的 GitHub 档案为你赢得多少虚假的互联网积分,努力都是值得的。
有没有 Django?
在定义一个潜在的 Django 独立应用的范围时,你需要问的第一个问题是,你心目中的包是否需要是一个 Django 应用,或者仅仅是一个使用 Django 的 Python 包,第二个问题是,即使它不是一个应用,你的包也应该是特定于 Django 的。
第一个问题是一个有点小的问题,但是涉及到你的应用应该如何集成到其他 Django 项目和开发工作流中。很容易在 Python 文件中看到 Django 导入,并认为“啊,这是一个应用”,而实际上它是不可安装的库代码(不可安装是指 INSTALLED_APPS,而不是指已安装的包)。例如,如果您想要分发一些额外的表单字段,这些字段可以作为“常规”Python 库分发和使用,而无需将模块添加到 INSTALLED_APPS 列表中。你仍然需要考虑后续章节中讨论的问题,比如测试(3,21),文档(22),和打包(8),但是在你如何设置你的测试和文档使用上有一些小的不同。
另一方面,您预期的包在 Django 项目中可能非常有用,但对 Django 没有硬性要求,不知道 web 框架甚至根本不知道用于 web 应用。如果是这样,那么你应该沿着这条路走下去。
为什么要保证这种分离?首先,如果你要与世界其他地方分享,并且核心功能实际上不依赖于 Django,那么你已经扩大了受众。您还减少了您的包中的另一个依赖项,即使您在 Django 项目中使用它,这也是另一个可能中断的依赖项。在有意义的地方,参考标准库而不是 Django 实用程序。如果有东西在新的 Django 版本中移动,你现在就与这种变化绝缘了。
值得记住的是,您可以向 Django 项目添加功能,而无需特定于 Django 的模块,或者不一定需要 Django。在关于混合依赖支持的第十六章中,我们将研究如何区分哪些是 Django 特有的,哪些不是。
选择依赖关系
像您正在创建的这样的包的伟大之处在于,它们为您提供了免费的功能——也许不是免费的,但是不需要您自己编写代码和找出边缘情况的成本。这些好处假设您正在使用的依赖项已经过测试,并且如宣传的那样工作,并且它们继续受到新版本的 Python、Django 及其各自的依赖项的支持。
您添加的每个依赖项都增加了您需要测试的表面积,以及中断交互的机会。在 Django 项目中是这样,在你自己的独立应用中也是这样。现在,自己重写一切当然是不明智的!但是要仔细考虑是否真的需要在项目中添加每个依赖项。
在引导性问题中,您应该考虑
-
依赖关系是否为您的应用提供了必要的功能?
-
你将支持的 Django 版本是最新的吗?
-
它有什么样的测试和文档覆盖?
-
维护者看起来有多忠诚?
必要的功能
向项目和库添加依赖项是很容易的。在大多数 Django 项目中,项目只有一个“消费者”,即项目团队。这可能是一个拥有多个不同环境部署和数百万用户的大型团队,但该团队和该应用仍然是唯一的消费者。有了像 Django 独立应用这样的可重用库,您可以预期它将被用于数十个,甚至数千个其他项目。应用的每个依赖项都是使用应用的项目的依赖项。如果你添加到应用中的每个依赖项包含错误或不兼容的代码,并使你面临风险,那么它就是一个潜在的拦截器。
这与您在 Django 项目中添加依赖项的方式没有太大的不同;然而,当您控制最终部署并且其他人不再依赖您自己的代码时,退出这些更改会更容易。
所以问一下是否有必要增加这一点,不是从严格的逻辑意义上,而是是否增加了更多的便利。
这是我犯的一个“错误”。我自己案例中的一个例子是,我决定使用 django-extensions 应用作为我自己的 django-organizations 的依赖项。我想要一个带时间戳的模型——这是一个好东西,当它丢失时,你会注意到它——此外,我还想要一个会自己处理的 slug 字段。为此,我想要 AutoslugField。与其说这是一个限制性的决定,不如说是一个糟糕的决定。然而,我可以使用一个典型的 Django slug 字段,但是出于我自己的需要,Autoslug 就在那里。后来我意识到,有些人,包括我自己,可能需要能够配置 slugs 是如何制造的,这不应该如此固定。
版本兼容性
由于 Python 2 的寿终正寝,与两年前甚至一年前相比,现在对 Python 版本兼容性的考虑已经不那么重要了。然而,Django 兼容性仍然是一个问题。正在讨论的依赖项是否支持您经常使用的 Django 的当前版本?这是一个明显的开始问题。
但是你也想知道它是否支持旧版本,是否有努力支持未来的版本。重要的是,您添加的任何依赖项都支持您的应用将支持的 Python 和 Django 的相同版本。
一个很好的经验法则是寻找对 Django 长期支持(LTS)版本的支持。
测试和记录
当在依赖关系中寻找测试和文档时,我们寻找几样东西:
-
验证代码是否有效,是否可以测试 bug
-
解释如何安装和使用软件包,以及它存在的原因
-
表示有人关心
当然,您应该在依赖项中找到测试,以及广泛覆盖软件包功能的测试。您还应该使用持续集成(CI)系统来寻找自动化测试运行,这样您就可以看到当有人将代码推送到存储库时,测试总是在运行。很难相信有人只是在自己运行测试,如果没有与存储库相关的 CI 系统,那么您将不得不相信贡献者正在运行测试,并且他们通过了测试。
基本文档对于确保您知道如何设置依赖关系以及可能的边缘情况非常重要。例如,它做了什么意想不到的事情或者它与什么相关的包不兼容?对于较大的依赖关系,大量的文档是必须的,但是对于较小的依赖关系,文档不仅是一个帮助,也是一个信号,表明这个包有一个特定的使命(用例),并且提供了足够的上下文,您可以判断作者和/或维护者是否已经考虑和支持了超出他们自己的初始问题的各种用例和开发人员。
第三点直接导致维护节奏。
维护节奏
这种依赖看起来会得到积极的支持吗?是否有大量未解决的问题,特别是 bug,没有被解决?是否存在未完成的旧的拉请求,尤其是针对 bug 的拉请求?许多人忽略的一个更重要的问题是,即使报告了问题并合并了拉请求,这些会导致新的发布吗?如果在过去的一年中合并了拉式请求,但是在此期间没有发布新的版本,那么它可能没有得到充分的维护。
特定与一般
作为程序员,我们倾向于构建一个解决方案,然后看看如何将这个解决方案抽象出来以解决更多的问题。可以说,这是一件非常好的事情。然而,我们很容易忘乎所以,试图覆盖太多的领域,陷入一个漂亮的抽象的兔子洞,而不是“仅仅”解决你面前的具体问题。
例如,您可能有一个用 Stripe 构建的 SaaS 应用订阅管理库。一个更一般化的方法将考虑不同的订阅和支付后端。然而,除非你自己积极地利用这些不同的场景,否则试图处理它们很可能导致半途而废的解决方案。当你开始使用应用时,创建一个能够处理用户定制场景的更通用的系统将会使你花费更多的时间在抽象上,这些时间可以更好地用于完善你的应用。
编写你的应用来涵盖你能想到的每一个用例几乎肯定是一个错误,不管更一般的用例有多有用。你很可能花时间预测非问题,却没有预测到实际需求。比起构建一个解决不了任何明确问题的过于抽象的解决方案,添加更多的用例,或者制定更通用的、可行的解决方案要好得多。
摘要
在本章中,您了解了创建独立应用的好处,包括分享常见问题的解决方案的机会、通过开源审查帮助提高代码质量,以及通过标准化常见问题来改进开发过程。您还了解了 Django 相关的 Python 项目和 Django 应用之间的区别,以及如何权衡附加依赖项的包含。在下一章,我们将研究一个独立应用的结构。
二、构建独立的 Django 应用
除了包含在您的独立 Django 应用中的功能之外,在更实际的层面上,您还需要构建代码以供重用。一个独立的 Django 应用的结构和一个直接嵌入到你的 Django 项目代码库中的 Django 应用的结构基本上没有什么不同。然而,有几个你想要遵循的实践来最大化你的应用的可用性。我们将在本章中讨论这些问题。
作为 Python 模块的 Django 应用
让我们重申,Django 应用,不管是独立的还是其他的,都是一个 Python 包。也就是说,它由多个 Python 模块(即 Python 文件)组成,可以从其他包导入,也可以从其他包导入。让 Python 包成为 Django 应用的具体原因是,它具有只能在 Django 项目中使用的功能、类和函数,方法是将它们显式包含在项目的 INSTALLED_APPS 列表中。包存在于 Python 路径中是不够的。
你可以把任何你想添加的 Python 包添加到 INSTALLED_APPS 中,但是如果它不是 Django 应用,它绝对不会为你做任何事情。我们可以把 Django 应用想象成一个接口,或者我们有一些抽象的东西,比如基类(Python 的 abc 模块),但是对于模块,它可能看起来像这样:
-
该包包括具有一个或多个具体模型的模型包。
-
该包包括一个模板标签模块,其中包含标签模块库。
-
这个包包括一个 HTML 模板的模板目录。
-
这个包包含一个静态资产目录,其中包含图像、CSS 文件、JavaScript 文件等等。
-
该包包括管理命令,即应用(myapp)的 myapp.management.commands 模块中的模块,定义了从 django . core . management . base Command 继承的命令类
-
该包定义了一个默认的 app.config 类
前五个中的任何一个都足以提供需要安装 Django 应用的功能。最后,定义一个默认的 AppConfig 类,这是一个最佳实践,但是就其本身而言,它并没有提供太多的功能。它确实允许您对基本的应用配置和名称空间进行更改(稍后将详细介绍)。
匹配预期的接口就足够了,可以从你的 Django 应用提供可安装的内容,不管是独立的还是其他的。知道了这一点,即使在你自己的项目中运行良好,也要避免单独依赖你的独立应用的模块接口。创建独立应用的目的是允许在所有 Django 项目中重用,所以你应该尽可能的清晰。
从历史上看,确保一个包被识别为 Django 应用的唯一步骤是包含一个模型模块。如果你在假设这种情况仍然存在的情况下工作,那它就不是。除非您在应用中包含模型类,否则没有必要包含模型模块。如果你的应用只包含模板标签,你可以只包含以下内容,包括用于确保目录是一个包的__init__.py
文件,用于包含任何和所有模板标签库的templatetags
模块,以及用于定义标签库的boo_tags.py
文件,该标签库包含模板标签和/或可以使用{% load boo_tags %}
加载到模板中的过滤器:
boo
|── __init__.py
|── templatetags
│ |── __init__.py
│ |── boo_tags.py
现在,如果您想使用 boo_tags 中的模板标记 boo,您需要做的就是将 boo 添加到 INSTALLED_APPS 中,并且您可以在项目中的任何位置加载标记库。
- 包含自定义标签的应用必须位于 INSTALLED_APPS 中,以便{% load %}标签能够正常工作。这是一个安全特性:它允许您在一台主机上托管许多模板库的 Python 代码,而无需为每个 Django 安装启用对所有模板库的访问。 1
中间件、URL 和视图呢?
许多 Django 应用包含了额外的 Django 相关特性,比如中间件、视图、URL 定义和上下文处理器。它甚至在 Django 文档中如此正确地建议:
- 应用包括模型、视图、模板、模板标签、静态文件、URL、中间件等的某种组合。它们通常通过 INSTALLED_APPS 设置和其他机制(如 URLconfs、中间件设置或模板继承)连接到项目中。
这些功能对一些 Django 应用是有益的,甚至是必要的,但严格来说,它们不需要 Django 应用来使用。您可以包含任何 Python 包中的 URL、中间件类、表单,甚至视图,无论它是 INSTALLED_APPS 中的 Django 应用,还是路径上可用的 Python 包。
Django 库并没有因为它不是一个可安装的应用而变得不那么有用。例如,表单、中间件和视图是 Django 项目的核心组件。您的库如何集成到其他 Django 项目中会略有不同,但是计划、测试、开发和维护您的库的步骤不会有很大的不同。
示例应用:货币
我们将从一个非常基本的示例应用开始。这是一个让处理货币更容易的应用。在它们的基础上,货币值只是数值,特别是十进制值,指的是特定面额的金额,通常在特定的时间点。大约 10 美元不同于€10 美元,2015 年的 10 美元也不同于 1990 年的 10 美元。
我们想做的是让切换货币金额的显示更容易,并轻松地格式化它们。首先,我们只是想改变某些数字的格式,所以我们只是添加了几个模板过滤器。
摆在我们面前的问题是这样的:它一定是 Django app 吗?随着我们的构建,越来越多的功能可能是非 Django 特有的,但是如果我们要添加模板标签,它们必须是 Django 应用的一部分。否则,我们无法加载标签库。由于这包括一个必须从 INSTALLED_APPS 中已安装的应用访问的功能,这将是一个 Django 应用。
我们将启动名为 currency 的应用,首先只需要一些必要的文件。文件结构将如下所示:
currency
|── __init__.py
|── apps.py
|── templatetags
│ |── __init__.py
│ |── currency_tags.py
|── tests.py
包含一个__init__.py
文件的 currency 文件夹定义了我们的模块。我们的核心功能现在只是模板标签和过滤器,所以我们只是有一个模板标签模块,再加上__
init__
.py
文件,然后是标签库名称。
我们的测试有一个 tests.py 文件,然后是一个 apps.py 文件。为了满足 Django 应用的要求,我们的包必须定义一个 models.py 文件或一个 apps.py 文件,最好是包含后者,甚至包含 models.py 文件。
所以现在我们来看内容。我们的 init。py 文件是空的(暂时)。
这是我们的 apps.py 文件:
from django.apps import AppConfig
class CurrencyConfig(AppConfig):
name = "currency"
verbose_name = "Currency"
这是我们在currency_tags.py
中的标签库:
from django import template
register = template.Library()
@register.filter
def accounting(value):
return "({0})".format(value) if value < 0 else "{0}".format(value)
这是我们的 tests.py 文件:
import unittest
from currency.templatetags.currency_tags import accounting
class TestTemplateFilters(unittest.TestCase):
def test_positive_value(self):
self.assertEqual("10", accounting(10))
def test_zero_value(self):
self.assertEqual("0", accounting(0))
def test_negative_value(self):
self.assertEqual("(10)", accounting(-10))
摘要
在这一章中,你学习了 Django 应用的组成和结构,以及如何区分包含对 Django 项目有用的功能的 Python 包和必须是 Django 应用的 Python 包。在下一章,我们将看看你的 Django 应用的测试,包括它们的价值和如何包含它们。
Django 文件: https://docs.djangoproject.com/en/2.2/howto/custom-template-tags/#custom-template-tags-and-filters
三、测试
测试确保我们的代码做我们期望它做的事情。他们还确保对代码库的更改不会破坏一些意想不到的东西,并且他们向我们应用的其他用户发出信号,他们可以依赖代码。
在这一章中,你将确切地了解测试如何在一个独立的 Django 应用中提供价值,如何在 Django 项目之外运行你的应用的测试,如何测试更复杂的多应用关系,如何包含你的测试,以及你是否需要配置 Django 来测试你的应用。
为什么要测试?
每个人都说你应该测试。这听起来很明显——如果测试是好的,我们应该去做。但是这回避了关于测试的好处的问题。
测试你的 Django 应用有几个目的。测试与应用代码一起编写,或者在应用代码之前编写,有助于提供一个可以用来验证代码的工作规范。在这种情况下,他们还可以帮助塑造代码和界面,就像你从零开始添加一些功能一样,测试会给你第一次使用它的机会。
一旦就位,即使是无关紧要的测试也可以防止代码库看似无关紧要的变化所带来的倒退。
虽然不是它们的主要用途,测试也可以提供一个如何使用你的代码的例子。在这种情况下,它们当然不能代替适当的文档,但是作为代码示例的测试——特别是当测试自动运行时——是一种可以验证是否是最新的文档形式。
所有这一切的基础是这样一个事实,计算机程序是由人类编写的,而我们人类在自己编写可靠的代码时是非常不可靠的(如果这不适用于你,请原谅)。有各种各样我们无法预测的事情,我们不擅长马上看到的边缘情况,以及在代码表面不明显的交互。
测试并不能解决所有这些问题,但是测试提供了一个有效的工具来消除我们代码的不确定性。最终,测试为你和你的应用的其他用户提供了信心——不要忘记“未来的你”很可能是这些用户中的一员!
测试来自 Django 项目的应用
Django 项目提供了一种使用测试管理命令运行测试的方法:
python manage.py test
这个命令将运行 Django 项目中的所有测试。通过结合使用测试管理命令和应用名称,可以将范围缩小到只运行单独命名的应用,如下所示:
python manage.py test myapp
如果非常简单的 myapp 看起来像这样
myapp/
__init__.py
models.py
tests.py
使用一个简单的 tests.py 文件,如下所示
from django.test import TestCase
from myapp.models import SomeModel
class TestSomeModel(TestCase):
def test_str_method(self):
instance = SomeModel()
self.assertEqual(f"{instance}", "<Unnamed Instance>")
然后,命令 python manage.py test myapp 将使用 Django 的默认测试运行程序运行 myapp.tests 中的所有测试,例如,对于给定的示例测试文件,该命令将运行 TestSomeModel.test_str_method。
例如,如果你在一个工作项目的上下文中开发你的应用,当你在一个更大的 Django 项目中工作时,这就很好了。如果你的应用是一个独立的库,其中的代码是从项目的之外的管理的,那么它的帮助就小得多。对于一个独立的应用来说,能够像任何其他 Python 包一样运行测试会更好。
测试应用
如果您以前使用过其他 Python 包,您会注意到它们是以一种简单的方式进行测试的。在某个地方会有一个测试模块,通常 setup.py 文件会定义一个测试脚本来使用 python setup.py 测试命令运行。这也适用于使用 Django 的包,但需要注意的是,Django 的许多功能必须在 Django 项目的上下文中运行,这是 Python 的 unittest 不会为您处理的。
为了激发一些测试独立应用的合理方法,让我们考虑一下测试应用最直接可用的策略:从您正在使用应用的任何项目中进行测试(假设您正在提取它)。
这意味着,要测试 myapp 应用,它需要安装在与您的工作项目相同的路径上,即相同的虚拟环境中,并且它需要位于您的工作项目的 INSTALLED_APPS 中。当需要测试对 myapp 的更改时,您需要回到工作项目来运行它们,也就是说,运行。/manage.py 测试 myapp。
如果这听起来不太合理,那么你就在正确的轨道上。然而,这种策略不允许测试一个独立的应用,这意味着它对于不参与您的项目的任何人来说都是不可重复的。如果你打算打包应用以供重用,你将无法求助于你的原始项目。谢天谢地,有更好的方法。
项目之外的测试
为了激励我们的后续解决方案,我们将尽可能设置最明显的解决方案。这将需要建立一个虚拟的,或持有人,项目,并从那里运行测试。为此,我们将在应用的根文件夹中创建新的 Django 项目,与应用源文件夹本身并行。该项目将把我们的应用包含在 INSTALLED_APPS 列表中。
然后,运行测试和任何其他命令就像调用 holder 项目的 manage.py 文件一样简单,就像任何其他项目一样。
下一步是在包根中创建一个示例项目,这将是一个只包含我们的应用的精简项目。现在,我们可以直接在包中运行 manage.py 命令并测试应用。您甚至可以在项目根目录下添加一个 bash 脚本,无论测试位于何处,它都会执行测试。
#!/bin/bash
cd sample_project
python manage.py test myapp
这是布局的样子:
sample_project
__init__.py
settings.py
url.spy
wsgi.py
__init__.py
manage.py
myapp/
__init__.py
models.py
tests.py
然后,为了运行应用的测试,您可以从示例项目中运行它们,就像它是一个生产就绪的 Django 项目一样:
python manage.py test myapp
这是可行的,并且是对原始示例的改进,但是它仍然增加了运行我们的测试所必需的内容。
使用测试脚本
当然,Django 不要求我们有项目支架,只要求配置 Django 设置。因此,更好的解决方案是使用 Python 脚本来配置这些最低限度的设置,然后运行测试。
该脚本需要做三件事:
-
定义或配置 Django 设置
-
触发 Django 初始化(即使用 django.setup())
-
执行测试运行程序
有两种方法可以提供 Django 设置。一种是用 settings.configure()的关键字参数在测试脚本中直接配置它们。另一种方法是指向一个仅供测试的 settings.py 模块,就像您运行生产应用一样。以下是前者的一个小例子:
#!/usr/bin/env python
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
settings.configure(
DATABASES={"default": {
"ENGINE": "django.db.backends.sqlite3"
}},
ROOT_URLCONF="tests.urls",
INSTALLED_APPS=[
"django.contrib.auth",
"django.contrib.contenttypes",
"myapp",
],
) # Minimal Django settings required for our tests
django.setup() # configures Django
TestRunner = get_runner(settings) # Gets the test runner class
test_runner = TestRunner() # Creates an instance of the test runner
failures = test_runner.run_tests(["tests"]) # Run tests and gather failures
sys.exit(bool(failures)) # Exits script with error code 1 if any failures
而是使用一个设置模块(来自下面的 Django 文档)。这在功能上与前面的代码相同,只是它将设置分解到一个更典型的设置文件中,在本例中为tests/test_settings.py
#!/usr/bin/env python
import os
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings'
django.setup()
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(["tests"])
sys.exit(bool(failures))
为什么要选择一个而不是另一个?如果您对设置有其他需求,使用单独的设置模块将更加灵活。脚本内配置风格可以满足更简单的应用,例如,没有模型的应用。
在第二十二章中,我们将研究一种更符合人体工程学的方法来管理你的测试和测试配置。
测试应用关系
但是,当你的 Django 应用被设计成与其他应用一起使用,或者与它们一起使用时,会发生什么呢?仅仅孤立地测试你的应用是不够的。在这种情况下,您需要创建示例应用,并将它们包含在您的测试设置中。
假设您的应用提供了基本模型。对于我们的例子来说,这是一个非常基本的电子商务模块,让人们可以根据他们想要的任何模型制作产品,并添加一些基本字段,如价格、SKU 以及是否活跃销售。该应用还包括一个 queryset 类,其中定义了一些有用的方法。由于我们的模型类是抽象的,queryset 类必须与用户应用中的具体模型相关联。
class ProductsQuerySet(models.QuerySet):
def in_stock(self):
return self.filter(is_in_stock=True)
class ProductBase(models.Model):
sku = models.CharField()
price = models.DecimalField()
is_in_stock = models.BooleanField()
class Meta:
abstract = True
现在,为了测试这一点,我们需要一个具体的模型(无论如何,实际使用基本模型进行测试是有帮助的)。为此,我们需要另一个应用,它定义了一个从抽象模型继承的具体模型,并使用提供的 queryset。
这样的应用只需要提供成为应用的最低要求,特别是 models.py 模块:
test_app/
migrations/ ...
__init__.py
models.py
在模型文件中,使用应用的抽象基础模型定义一个模型:
from myapp.models import ProductBase, ProductQuerySet
class Pen(ProductBase):
"""Testing app model"""
name = models.CharField()
pen_type = models.CharField()
objects = ProductQuerySet.as_manager()
定义模型后,确保测试应用包含在您的测试设置 INSTALLED_APPS 中:
INSTALLED_APPS = [
'myapp',
'test_app',
]
注意,如果 Django 包需要任何级别的集成测试,这也适用于不可安装的应用。
在哪里包含测试
当您向 Django 项目中的应用添加测试时,您可能会在每个应用中包含测试模块,或者是一个文件,或者是一个目录:
myapp/
__init__.py
models.py
tests.py
这个将也适用于独立的应用,但通常应该避免。在这种情况下,您的测试应该位于应用之外的一个单独的顶级模块中。如果你使用额外的模块进行测试,比如测试应用,那么这可以确保你的应用不依赖于代码中未安装的模块。它还保持安装的包更干净(尽管值得注意的是,这不是一致的意见)。
myapp/
__init__.py
models.py
test_app/
__init__.py
models.py
tests/
__init__.py
test_models.py
没有 Django 的测试
这里的重点是 Django 应用,即可以安装并包含在 Django 项目中的 Python 模块,以使用模型、模板标签、管理命令等。但在许多情况下,应用提供的功能可以作为普通的旧 Python 代码来测试。
这将是你的应用中需要设置的任何东西的情况,比如模型。然而,这并不适用于 Django 的每个部分或你的应用的每个部分。事实上,如果您的应用没有任何模型,并且您没有任何与请求相关的功能要测试——特别是在集成测试级别——那么您可以放弃设置或使用 Django 的测试模块,坚持使用标准库的 unittest,或者您选择的任何其他测试框架。
如果您正在加载 Django 项目,例如,任何涉及模型、设置或完整的请求/响应周期的东西,您只需要通过 Django 调用一个测试运行程序。在大多数情况下,测试表单、模板标签和过滤器中的逻辑等特性并不依赖于 Django 中任何需要项目设置的部分。
你为什么要这么做?使用 unittest 而不是 django.test 所带来的性能提升是非常值得怀疑的,更不用说有影响了。然而,如果这些是您唯一需要的测试,那么您的测试环境将更容易设置和运行。
摘要
在这一章中,你学习了为什么对你的独立应用进行测试是重要的,以及当 Django 应用不再是父 Django 项目的一部分时,如何开始测试它。您还了解了如何使用处理 Django 设置的 Python 脚本来简化测试执行,以及如何测试基于您自己的应用之外的其他应用定义的关系的应用特性。最后,您了解了将独立应用的测试包含在顶级测试目录中的位置,并且,对于某些不依赖于数据库或模板引擎的应用类型,使用 Python 的 unittest 库而不使用 Django 设置可能就足够了。
在下一章中,您将学习如何在没有 Django 项目的情况下为您的应用管理数据库迁移。
四、模型迁移
数据库迁移允许您跟踪数据库模型中的变化,并将它们传播到底层数据库模式。如果你的独立应用包含*具体模型,*那么你需要在你的应用中包含迁移。与运行测试一样,这在独立应用中与在您自己项目中的应用中没有本质上的不同;然而,有几个陷阱需要注意。
在这一章中,你将学习如何在你的 Django 项目之外管理你的应用的数据库迁移,以及一些使这些迁移更加安全和清晰的实践。
项目之外的迁移
当您为项目中的应用创建迁移时,只需从项目根目录运行管理命令即可:
./manage.py makemigrations app
对于迁移,我们会遇到与运行测试相同的问题——我们没有一个项目来运行迁移命令。
用于运行测试的 runtests.py 脚本可以适用于运行迁移命令;然而,采用现有的模式会更简单:每个 Django 项目附带的 manage.py 脚本。
在项目根目录中,创建一个 manage.py 文件。名字本身并不重要,但有了这个名字,它的目的对你和其他任何人来说都是显而易见的。就在 runtests.py 示例中,您可以直接从调用 settings.configure 或指向单独的设置模块的文件中配置 Django。最终结果看起来与标准 manage.py 脚本几乎没有区别。
import sys
import django
from django.conf import settings
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.admin",
"django.contrib.contenttypes",
"django.contrib.sites",
"myapp",
]
settings.configure(
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",
}
},
INSTALLED_APPS=INSTALLED_APPS,
ROOT_URLCONF="tests.urls",
)
django.setup()
if __name__ == '__main__':
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
测试迁移
有时候,开发人员发布更新只是为了发现令人惊讶的失败构建,或者更糟糕的是,失败的部署,这都是因为没有添加或包含一个或多个迁移。这可以简单到改变一个字段的值,或者添加一个全新的模型和数据库表。
或者,更新可能工作得很好,但是所涉及的更改在当前模型的状态和迁移定义的状态之间产生了差距。更改模型字段的属性就足以做到这一点,即使它不需要对数据库模式进行任何更改。这种情况下的问题是,最终用户可能会自己创建缺失的迁移,这在应用时可能会与您添加到包中的后续迁移相冲突。
通过仔细检查没有可供迁移的更改,可以避免这两种情况。比双重检查更好的是,这可以添加到您的自动化测试套件中。
from django.test import TestCase
from django.core.management import call_command
class TestMigrations(TestCase):
def test_no_missing_migrations(self):
call_command("makemigrations", check=True, dry_run=True)
前面的测试所做的就是使用两个命令行选项运行 makemigrations 命令,- check 和- dry-run。如果检测到任何更改,check 标志会使命令以失败的非零状态退出,而预演标志只是确保不会创建其他输出。如果您缺少迁移,此测试将会失败。
附加迁移指南
如果您没有描述性地命名迁移的习惯,创建一个独立的应用是一个养成这个习惯的好机会。如果迁移足够简单,Django 将尝试提供一个描述性的名称,但这并不总是可能的,相反,您将只能进行带时间戳的迁移。虽然您确实可以阅读源代码,但从名称中了解其内容和用途会很有帮助。您可以使用-n 选项指定文件的名称:
./manage.py makemigrations myapp -n add_missing_choices
对于迁移名称,一个好的指导原则是将它们视为更简洁的 Git commit 消息主题:(I)进行了何种更改(例如,添加、更新、删除)以及(ii)迁移本身的主题。这将有助于您以后添加新功能,并且有助于参与者了解数据库更改的进度。
摘要
在本章中,您了解了如何利用用于项目外部测试的相同基本策略,为您的独立应用创建数据库迁移,如何将缺失迁移的测试添加到您的测试套件中,以及为什么建设性的迁移命名是有价值的。在下一章中,我们将会看到在你的独立应用中包含 HTML 模板,包括如何包含它们,但同样重要的是包含什么来优化对用户的有用性。
五、模板
在独立应用中包含 HTML 模板的机制与在 Django 项目的应用中包含模板没有什么不同。但是,您确实需要仔细考虑命名以及包含在模板中的内容。在本章中,您将学习如何为最终用户命名模板,以及如何为开发人员用户优化模板内容。
三个基本策略
如果您的应用包含返回呈现的 HTML 响应的视图,它将通过呈现 HTML 模板来做到这一点。由于 Django 加载模板的方式,您有三种选择来处理初始模板内容:
-
不要包括模板——毕竟,这是用户的网站。
-
包括基本的 HTML 模板。
-
包括详细的甚至是风格化的模板。
在大多数情况下,你应该在你的应用中包含基本的 HTML 模板,这些模板显示了开发过程中的渲染结果和你的视图中包含的模板上下文的结构。
包括什么
第一个选择,发布你的独立应用,但不包含你的视图所引用的 HTML 模板,应该被认为是不可行的。排除模板的主要好处是,它使你的应用的用户在哪里需要添加他们自己的模板变得很明显。然而,虽然应用的最终用户能够提供他们自己的模板,并且您可以记录应该包括哪些模板,但这增加了使用应用的摩擦。这使得探索性的使用变得更加困难,并使得在最终的模板中应该期望什么变得不那么明显。
第二个和第三个选项之间没有明显的区别,但是除了样式之外,我们可以确定一个更详细的模板,它引入了一些元素的组合,这些元素对于交付应用的功能并不是严格必需的。这个简短的模板,我们可能会认为是一个简单的应用视图,引入了一些不必要的假设。
{% extends "base.html" %}
{% block content %}
<h3>Here is a list of other fruits reported by the app</h3>
<ul>
{% for fruit in fruit_list %}
<li class="fruit-{{fruit.category }}">{{ fruit }}</li>
{% endfor %}
</ul>
{% endblock content %}
我们期望页面呈现如图 5-1 所示。
图 5-1
呈现的网页
虽然这是一个好的、流行的约定,但是没有什么要求要求任何人将基础模板命名为 base.html,也没有任何要求,如果这样的模板存在,它应该是这个特定级别的直接基础模板。同样,不要求任何项目模板都包含名为 content 的模板块。这可能是有意义的,并且可能是你在 Django 项目中见过的最一致的东西,但是这仍然是一个方便的约定。因此,虽然这个模板将为拥有 base.html 和内容块的人创造更丰富的初始体验,但对于那些没有的人来说,它将失败。
更好的策略——在大多数情况下——是包含基本模板,用最少的结构和样式向用户显示模板上下文的全部范围。下面是之前的例子:
<h3>Here is a list of other fruits reported by the app.</h3>
<ul>
{% for fruit in fruit_list %}
<li class="fruit-{{fruit.category }}">{{ fruit }}</li>
{% endfor %}
</ul>
图 5-2 中的结果并不令人兴奋,但是预期用户将会覆盖我们的
图 5-2
最少的结构和样式
模板最终会使它们在发布的应用中看起来不适合生产。
有些情况下,提供更详细的模板是有用的,例如,应用的业务目标是提供风格化的结果,定制的管理皮肤。
在你的模板中包含翻译字符串是一个好主意,但是对于简单的例子模板来说没有必要。考虑到您的开发人员用户将会覆盖这些,他们可以包含这个或他们选择的语言。参见第十三章了解如何解决这个问题。
电子邮件和杂项模板
从视图中呈现 HTML 响应同样适用于任何其他模板化的内容,包括电子邮件。电子邮件模板是应用中的一个常见功能,包括用户注册、邀请和任何其他类型的出站通知。
关于电子邮件和通知模板的一个新增功能是将它们包含在自己的 templates 子文件夹中,例如 email/。
Flat is Better than Nested: The Zen of Python
"使用额外的目录不是不必要的嵌套吗?"记住实用性胜过纯粹性,目录“仅仅”是名称空间。myapp/email/welcome_body.html 和 myapp/email_welcome_body.html 的命名空间深度差异为零;他们只是性格不同而已。查看文件夹系统的不同之处在于它们总是显而易见的,不仅在文件系统中,更重要的是在调用源代码中。
摘要
在这一章中,你学习了如何最好地在你的独立应用中包含 HTML 模板,以及在其中包含什么。您了解到,与其包含依赖于特定基础模板的大量样式的模板,不如只包含模板的核心结构来演示模板中的可用内容。在下一章中,您将学习如何包含像 CSS 和 JavaScript 这样的静态资产来提供基本样式和前端功能。
六、使用静态文件
在您自己的 Django 项目中,您可能有静态文件,包括样式表、JavaScript、字体和图像在内的静态资产,所有这些都打算直接提供给最终用户的浏览器。这些静态文件或静态资产允许您控制呈现的 HTML 的布局和样式,并引入客户端(浏览器)交互性。
虽然不太常见,但一些独立的应用可能会包含自己的静态文件,这意味着可以通过最终用户项目的名称直接引用,也可以通过您自己的应用模板引用。各种类型的独立应用可能使用静态文件,通常包括提供自定义管理功能的应用和捆绑前端框架组件的应用。在本章中,我们将逐步介绍在独立应用中包含静态资产的意义以及如何包含它们。
独立应用中的静态文件
历史上,在可重用的应用中包含静态文件有两个主要原因:
-
添加应用提供的基于界面的功能,例如,一个更大整体的核心组件。
-
将静态文件包含在项目生成过程中,例如,包含 JavaScript 框架,以便在运行 collectstatic 时,所需的框架文件自动可用于项目。
后一个原因,包括项目中的 JS 框架这样的静态文件,在很大程度上已经过时了。随着前端应用代码和构建管理系统(如 Gulp、Webpack 和 package)的流行,开发人员和团队从这些构建系统中包含和构建这些文件,与 Django 静态管道并行,这种情况要常见得多。
例如,如果应用的目的是将 Vue.js 框架包含在项目模板中,那么避免创建 Django 应用并配置项目 JavaScript 构建系统(如 Webpack)来直接包含框架将是一个更好的主意。除此之外,这使得对 JavaScript 依赖版本的细粒度和可移植控制成为可能。
这并不是说没有创建可重用应用来提供这种功能的用例。对于不保证 JavaScript 构建过程或非常狭窄的用例的较小的内部项目,这仍然是有益的。
相反,在独立的 Django 应用中包含静态文件的主要用例是包含基于接口的功能或样式。包含静态文件的机制很简单:
-
在应用目录中添加一个静态/目录。
-
将静态文件添加到新的静态/目录中。
Note
我们还有一个步骤来确保这些文件包含在最终的包中进行分发,但是这足以填充项目。如果最终用户启用了基于应用目录的静态文件收集(默认情况下包含),则在安装了应用的项目中和 INSTALLED_APPS 中运行 collectstatic 会将这些文件复制到项目的 STATIC_ROOT 目录中。
STATICFILES_FINDERS = [
...
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
值得注意的是,当您运行 collectstatic 命令来收集项目的所有静态文件时,所有文件都将被聚合到项目的 STATIC_ROOT 目录中,包括它们相对于应用静态目录的路径和。这意味着,如果您包含 static/style.css,它将作为/static/style.css 包含在最终项目中。这不仅模糊了文件的来源,还会导致命名冲突。
与模板一样,解决方案是包含一个命名子目录来命名文件:
myapp/
static/
myapp/
style.css
templates/
myapp/
list.html
现在,您的文件将作为/static/myapp/style.css 提供。如果最终用户聚集并缩小这些文件,您的资产的最终名称可能并不重要,但您总是希望避免命名冲突。
在 Django admin
如果您的 Django 应用包含任何类型的 Django admin 可视化定制,从全面的样式替换到较小的 JavaScript 小部件,您可以像前面描述的那样包含必要的文件。这些文件可以包含在同类的基于目录的名称空间中,或者使用 admin/子目录。这是分割较大文件集合的好方法,尽管为了避免名称冲突,您应该确保文件具有特定于应用的名称和/或使用额外的基于目录的命名空间。
此布局将包括 myapp.css 作为/static/admin/myapp.css:
myapp/
static/
admin/
myapp.css
myapp/
style.css
然而,没有必要为这些文件使用 admin/namespace,尤其是如果您的应用只包含特定于管理员的静态文件。另一方面,如果你的应用的目标是覆盖或者取代现有的管理文件,那么你应该命名它们,使得文件路径与你想要覆盖的文件完全相同。
myapp/
static/
admin/
css/
login.css
现在假设 myapp 包含在项目的 INSTALLED_APPS 中的 django.contrib.admin 之后,那么它的 login.css 文件将被用来代替 django 的文件。
最后,如果您在 Django admin 中包含了基于 jQuery 的 JavaScript,请确保您的插件或函数与您想要支持的 Django 版本兼容。如果您这样做,并使用 Django 管理员使用的特殊命名空间——Django 的管理员为其包含的 jQuery 使用django.jQuery
命名空间以避免与其他引入的版本冲突——您就不需要自己包含 jQuery。另一方面,如果你的定制功能由于某种原因依赖于一个非常特定的 jQuery 版本,你会希望在你的应用中包含那个版本。在这种情况下,您可以效仿 Django,使用“普通的”jQuery 名称空间或您自己的名称空间,以避免与任何其他引入的 JavaScript 发生任何后续冲突。
摘要
在本章中,您已经学习了如何在您的独立应用中包含静态资产,在您的独立应用中包含什么样的静态资产是有意义的,以及如何为特定于 Django admin 的功能包含 JavaScript 资产。在下一章,我们将进一步探讨命名空间的问题,激发挑战,并建立一套创建合理和一致的命名系统的策略。
七、应用中的命名空间
名称空间是一个非常棒的想法——让我们做更多这样的事情吧!
—蟒蛇之禅
在前几章中,我们介绍了如何向您的应用添加一些功能,包括 HTML 模板和静态文件,它们是通过使用应用命名空间来组织和访问的。
在这一章中,我们将了解命名空间的决定是如何渗透到你的独立应用的其余部分,以及如何利用命名空间来使你的应用的集成和使用更加容易。
名称空间一览
名称空间是一种组织命名的对象的方式——在 Python 和其他语言中——这样它们既有一个父标识符,更重要的是,它们的名称不会冲突。我们已经讨论过几次名称空间,主要是在模板和静态文件目录的上下文中,但是它们的使用远远超出了配置目录以收集文件的范围。
带命名空间
-
两个不同的模块可以各自定义一个名称相同的函数,这样 my_module.does_a_thing 和 your_module.does_a_thing 就不会冲突。
-
两个不同的类可以各自定义一个名称相同的方法,这样 MyClass.does_a_thing 就不会与 YourClass.does_a_thing 冲突。
-
两个不同的字典可以各自包含映射到不同值的相同键,例如 my_dict[“key”] = 4,而 your_dict[“key”] = 1。
在您自己的项目中,使用不同的名称空间很容易被认为是理所当然的,但是在将代码引入不同的 Django 项目时,您应该花时间确保它有合理的名称空间。
应用本身
我们命名一个独立的 Django 应用的切入点是应用本身,更确切地说,是它的模块名和它在 AppConfig 中的命名方式。这是最简单也是最重要的一步。为了避免 Django 项目代码库中的名称冲突,更重要的是,为了使区别更明显,应用名称应该是描述性的,并且不能明显地与已知的现有应用名称重叠,无论是 Django 附带的名称还是为共享而发布的名称。
例如,如果您构建了一个独立的应用来与 stripe 计费服务进行交互,您可能会忍不住将其命名为 Stripe。但是如果这样做,就会与权威的 Stripe Python SDK 发生冲突。相反,你可能会决定把它命名为与 Django 相关的东西,比如 django_stripe 或 djstripe,只是现在后者至少与现有的已发布的独立应用有冲突!如果你的应用是 djstripe 功能的替代品,那么它们不太可能在同一个项目中使用,因此会发生冲突;然而,除非你的应用是 djstripe 的一个分支,否则它很可能会给使用该应用的开发人员带来困惑。在这种情况下,请选择不同的名称。
当一个应用的描述性名称因为冲突而不可用或不明智时,选择一个具有额外上下文的适应名称,如 stripe_billing,或使用同义词或典故,也可以,如 zebra。斑马应用现在是一个无人维护的应用,用于在 Django 项目中集成条纹支付,这样命名是因为斑马有条纹。
资源定位符
有多种方法可以将名称空间添加到 URL 中,以使它们在项目中易于识别,并避免命名冲突。在命名冲突的情况下,将使用第一个匹配的命名的 URL 。这可能会令人困惑,尤其是在没有引发异常的情况下。
完全可以使用基本 URL 名称本身来建立名称空间。myapp_list 和 myapp:list 之间基本上没有什么差别。后者更清楚名称空间在哪里“中断”,但两者都确保了与列表相关的视图对于 myapp 名称是唯一的。
设置
如果你的独立应用允许通过 django.conf.settings 进行配置,那么这些也需要一致的命名空间。在您自己的 Django 项目中可能适用的东西并不能保证在所有其他项目中都适用。
例如,对于一个名为 organizations 的应用,它管理多个用户的帐户,您可能有几个设置来控制要使用的用户模型、每个群组允许的成员数量以及管理员用户是否可以邀请新成员:
GROUP_USER_MODEL = AUTH_USER_MODEL
GROUPS_LIMIT = 8
ADMINS_CAN_INVITE = True
虽然在最初的 Django 项目中已经足够混乱,但是影响的范围非常有限。然而,在一个可重用的应用中跨项目传递这些混杂的名字解决了这个问题。因此,请确保项目设置中命名的每个设置都有一个一致的前言,例如:
ORGANIZATIONS_USER_MODEL = AUTH_USER_MODEL
ORGANIZATIONS_USER_LIMIT = 8
ORGANIZATIONS_ADMINS_CAN_INVITE = True
有关在应用中构建设置和处理默认值的更多信息,请参见第七章。
管理命令
Django 的管理命令作为 Django 项目的基于命令行的接口。考虑它们的一个好方法是视图,但是用于终端处理而不是 HTTP 请求。独立应用包含管理命令有很多原因:同步数据、导入和导出数据,或者提供创建默认数据的方法。
快速回顾一下,管理命令名来自模块(文件)名。Django 会将定义 BaseCommand 子类的管理/命令中的模块视为命名管理命令。
myapp/
__init__.py
management/
__init__.py
commands/
__init__.py
migrate_user_data.py
然而,与 URL 名称不同,管理命令名称是全局的。如果您想包含一个管理命令,该命令将跨系统迁移用户数据,使用名称 migrate 将与 Django ORM 的 migrate 命令冲突,覆盖基本的 migrate 命令会引起很大的麻烦。
为了避免名称冲突和明确命令的目的,有两种解决方案:
-
在命令名(模块名)前加上应用标识符
-
使每个命令名尽可能具有描述性和唯一性
使用应用名作为管理命令的名称空间并不是一种常见的做法,但这并不意味着这不是一种好的做法。如果你的应用有几个管理命令或者你的应用有一个简单的名字,这是一个好的策略。django-jet 和 dj-stripe 都遵循这种做法,分别在管理命令前面加上 jet_ 和 dj_stripe。在 dj-stripe 的情况下,这意味着 sync_models 的命令被明确定义为与 dj-stripe 相关,而不是全局模糊的命令。
在管理命令的情况下,即使没有名称前缀,使命令名称显式通常也就足够了。这可能包括在其他地方包含应用名称,或者引用应用特有的内容,如一类数据或服务。django-cachalot 提供了 invalidate_cachalot 管理命令,这显然是一个特定于应用的名称,而且它的功能也很清楚。django-reversions 提供 createinitialrevisions 和 deleterevisions。
如果没有类似 URL 的管理命令命名空间方案,您选择哪种策略将取决于上下文。
模板标签
在 templatetags 包中放置多少模块没有限制。请记住,{% load %}语句将加载给定 Python 模块名称的标签/过滤器,而不是应用的名称。
—Django docs
模板标签和过滤器在渲染时向模板添加逻辑和格式功能。添加新的模板标签非常简单,只需在应用中包含一个 template tags 模块,然后将一个或多个标签库作为子模块。
myapp/
templatetags/
__init__.py
myapp_tags.py
模板标签和过滤器提出了两个命名空间挑战:
-
标记库名称是全局的,也就是说,不是与应用相关的名称空间。
-
单个标签和过滤器被类似地加载到单个 namesapce 中,尽管只是在加载库的模板的上下文中。
这意味着您应该使用名称前缀来保持您的模板标签模块的唯一命名,并命名各个标签和过滤器,以便它们隐式地应用命名空间,至少在它们提供某种特定于应用的功能时。如果标签或过滤器的用途或用途超出了应用中数据的上下文,那么更通用地命名它们可能更有意义。
模型和数据库表
由于应用名称本身的原因,应用模型及其各自的数据库表名称都有默认的名称空间。一个项目可能有 15 个不同的应用,每个应用都有自己的模型类 User,这不会造成任何特殊的冲突,只要它们是用别名导入的,它们可能会发生冲突。
from app1.models import User as UserApp1
from app2.models import User as UserApp2
from app3.models import User as UserApp3
尽管如此,导入冲突并不是我们在命名和描述性方面寻求避免的唯一问题。如果一个模型既以特定于应用的方式提供服务(即,很难解释如何在应用之外使用它),又希望在应用之外与其他模型一起使用,则应使用特定于应用的命名。
class MyAppUser(models.User):
"""A user model specific to the myapp app"""
数据库表命名在开发 Django 项目时基本上是事后才想到的,因为 ORM 会生成默认的表名。虽然关于创建描述性的和人类友好的表名还有一些要说的,但是对于一个可重用的应用来说,主要关心的只是表名中的应用前言应该是唯一的。
假设你有一个应用,它提供了一种面向用户的日志记录,你把这个应用命名为 logs 对于一个独立的应用来说,这不是一个很好的名字,但让我们把它当作一个既定的。数据库表名将以 logs_ 开头,例如,对于名为 LogEntry 的模型,表名将是 logs_logentry。
如果您在 Django 应用之外编写任何 SQL,那么表名缺少源代码范围内模型类所具有的上下文。因此,在本例中,如果您必须维护应用名称日志,那么在您的模型元类中指定 db_table 值是明智的:
class LogEntry(models.Model):
...
class Meta:
db_table = "activitylogs_logentry"
现在,任何编写 SQL 查询或检查数据库的人都会更清楚地了解这个表代表什么以及它包含什么类型的数据。
八、创建基本包
获取 Django 应用并使其独立的最后一项工作是将它变成一个可安装包。这本身是一个丰富的话题,我们将在第十八章中更深入地讨论,但目前我们的目标是满足最低要求,使一个简单的 Django 应用可以从 Django 项目之外安装。
示例博客应用
这个简单的博客应用已经被用于无数的教程和示例,否则它会变得陈旧,在这里它让我们专注于作为一个工作的独立的应用的新功能。
我们将在第四部分中更详细地介绍如何设置一个包,但是这将足以创建一个可测试、可部署和可发布的包。我们的博客应用非常简单,只包括一个模型,Post,两个视图 post_list 和 post_detail 及其各自的 URL,一个用于呈现阅读次数的模板过滤器,两个视图的基本模板,以及一个用于初始博客样式的 CSS 文件。
blog
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── __init__.py
├── models.py
├── static
│ └── blog
│ └── blog.css
├── templates
│ └── blog
│ ├── post_detail.html
│ └── post_list.html
├── templatetags
│ ├── __init__.py
│ └── blog_tags.py
├── urls.py
└── views.py
除了用于运行测试和创建迁移的文件之外,我们将在根 blog_app 目录中包含 blog app 目录。
blog_app
├── blog
├── manage.py
├── runtests.py
├── setup.py
|-- tests
我们在这里包括的一个东西是 setup.py 文件。
基本的 setup.py 文件
为了对此进行打包,我们需要一种定义包的方式:它叫什么,它是什么版本,代码在哪里。如果您熟悉 Ruby Gemspec 或 Node package.json 文件,setup.py 文件有类似的作用。而且只是 Python。我们来看看文件。
from setuptools import setup, find_packages
setup(
name="blog",
version="0.1.0",
author="Ben Lopatin",
author_email="ben@benlopatin.com",
url="http://www.django-standalone-apps.com",
packages=find_packages(exclude=["tests"]),
)
这是我们能得到的最基本和最简单的东西。这不足以发布我们的包,但应该足以构建它,以便我们可以将它作为独立的 Python 包安装在本地。
setup 函数的参数顺序没有意义,因为它们是关键字参数,这里对它们进行分组和分隔只是为了便于解释:
-
第一个参数是包名。如果忽略这一点,您仍然可以为您的包创建一个构建目录,但是任何构建工件,从 wheel 文件到压缩的源代码,都将被命名为 UNTITLED,从而避免在其他地方发布和安装。
-
第二个参数指定了版本号。这对于在发布错误修复或新功能时替换旧版本至关重要。
-
作者的名字,也就是你。
-
作者电子邮件,这是你的电子邮件地址。
-
项目 URL,表示某人可以在哪里找到关于项目的更多信息(例如,文档站点、源存储库)。
-
最后一行指定了在哪里可以找到包。这是一个关键的参数,通过依赖 find_packages 函数,我们可以避免指定单独的路径名。
使用该文件,我们可以运行 python setup.py 检查并查看我们没有遗漏任何内容,然后运行 python setup.py build 来生成包的副本,因为它将以分布式形式出现在 build/目录中。
添加模板和静态文件
如果使用 python setup.py build 命令构建项目,并在新创建(或更新)的 build/目录中列出文件,您会发现缺少模板和静态资产文件:
build/
lib/
blog/
__init__.py
admin.py
apps.py
migrations/
0001_initial.py
__init__.py
models.py
templatetags/
__init__.py
blog_tags.py
urls.py
views.py
这是因为 setuptools 只寻找包含在您的包中的 Python 文件(以及一些特定的非 Python 文件)。为了包含这些,我们需要包含一个清单文件,即 MANIFEST.in。
MANIFEST.in 文件允许您使用非常简单的命名或通配符格式来指定应该包含在您的包中的文件。在我们的例子中,我们不希望必须分别指定每个模板和静态资产,所以我们希望使用通配符。为了举例,因为我们只有一个 CSS 文件,我们将使用两个。
include blog/static/blog/blog.css
recursive-include blog/templates ∗.html
这两行根据相对于根目录的位置来指定文件,即父目录中的 MANIFEST.in。第一行包含一个文件的路径名,而第二行将包含位于 blog/templates 目录中的所有 HTML 文件,包括子目录。匹配这些行的文件将与 Python 文件一起被复制到构建产品中。
现在,如果您使用 python setup.py build 构建您的应用,您会发现您的static
和templates
目录包含您所有的 CSS、JavaScript 和 HTML 模板:
build/
lib/
blog/
__init__.py
admin.py
apps.py
migrations/
0001_initial.py
__init__.py
models.py
static/
blog/
blog.css
templates/
blog/
post_detail.html
post_list.html
templatetags/
__init__.py
blog_tags.py
urls.py
views.py
安装和使用
现在有两种方法可以安装一个可用的本地应用。您可以运行 python setup.py install,它会将应用的副本安装到与当前 python 路径相关的 site-packages 目录中(例如,system Python site-packages 或 virtualenv site-packages)。或者可以运行 python setup.py develop。这将安装一个从 site-packages 到您的项目根目录的链接,链接的形式是一个名为 blog.egg-info 的文件,其中包含您的包的路径。
使用 develop 命令的好处是,由于你的包目录被有效地用符号链接到你的站点包中,所以对你的包的每一个修改都可以在你使用这个包的任何地方立即得到。因此,与另一个项目并行开发一个包(这里是您的独立应用)变得更加容易,而不必每次更改都重新安装。
不利的一面是,你最终可能会开发另一个独立应用的未标记版本(即特定版本)。那么风险就是你的另一个包不能准确地捕捉你的独立应用的特性或发布的 API。如果您尝试使用 python setup.py develop 在 Python 环境中安装应用,然后使用 pip 进行安装,也可能会遇到冲突。因此,这应该用于探索性工作,而不是作为安装独立应用的全职策略。
在第十八章中,我们将深入研究一些改进的策略来构建一个使维护更容易、发布共享更简单的包。
摘要
在本章中,您了解了为独立应用创建可安装 Python 包的基本结构和要求。这为在 Django 项目环境之外使用您的独立应用提供了最基本的第一步,并为您在包索引上发布提供了基础。在下一章,我们将开始研究如何从你现有的 Django 项目中评估和提取一个独立的应用。
九、范围界定和绘制边界
在第一章中,我们简要讨论了界定 Django 独立应用的范围意味着什么,包括你创建独立应用的目标,对第三方依赖的考虑,以及应用预期执行的工作。
在这一章中,我们将更深入地回顾这些相同的考虑,特别是关于在现有代码库的上下文中寻找一个可能的 Django 独立应用。
问题的范围和性质
定义软件项目的范围是项目的首要挑战之一,也是最重要的挑战之一。范围定义了它将做什么,边界在哪里,并且不仅影响项目的代码行大小,还影响它的复杂程度,它可以支持多少功能,以及测试和维护软件的挑战性。
在您自己的项目中编写的应用可能有一个明确的范围,但是当它是项目源代码的一个集成部分时,边界很容易变得模糊。这可能是因为功能蠕变或某些即时便利——在应用中包含一些新的小功能更简单或更方便,即使该功能的目的与应用的核心工作正交,或过于特定于您的特定项目。
应用的范围也将影响需要什么依赖关系。随着“依赖表面积”的增加,应用在维护和版本支持方面的脆弱性也在增加。如果你的独立应用依赖于 Django,那么它只需绑定到 Django 版本。然而,如果它需要 Django 和一个或多个附加的独立应用,那么它很可能会受到所有依赖项在支持 Django 和 Python 版本时重叠程度的限制。从你自己的项目中减少你的独立应用的范围,预期的效果是使它更容易维护。
独立应用的工作
- 你说你在这里做什么?
如果你在寻找一种启发式的方法来界定应用的范围,你可以做得比简单地写一份简短的工作描述更糟。作为一名员工,你可能有一个不合适的职称和众多不同的职责,但如果你把它剥离,那么很可能你可以在你所做的事情之外为你的工作定义一个目标。作为一名软件开发人员,你的工作是将业务需求转化为工作软件。作为首席执行官,你的工作是领导公司实现增长和盈利(至少在大多数地方是这样)。可能会有更多的工作职责,这些职责可能很重要,但是几乎每份工作都有一个单一的目的。同样,你的独立应用应该有一个指导性的目的,可以简单明了地描述。
设身处地为营销人员着想(不,真的)。你会如何描述你的应用?它解决什么问题?它是如何解决这个问题的?对于许多其他应用,它是如何解决这个问题的?
这些问题的答案将有助于开源软件包的营销,但这不是我们问这些问题的原因。
以下是几个流行的独立 Django 应用,用我自己的话来说,是它们的简短工作描述:
-
Wagtail 的工作是以一种用户友好的方式使用户可编辑的内容更容易建模和服务。
-
Django REST 框架的工作是为创建 RESTful APIs 和将现有的 Django 应用连接到 API 提供类似 Django 的体验。
-
django-taggit 的工作是在分类学的意义上,使向 django 项目中的任何类型的对象添加标签变得容易
-
Easy Thumbnails 的工作是为单个上传的图像创建调整大小的图像。
这些应用的大小和它们提供的功能范围各不相同,但在每种情况下,所有这些功能都可以追溯到一个非常明确的目的。
创建和提取的维度
你可以从几个方面来看待一个独立的应用;在这里,我想把重点放在我称之为垂直和水平细分的轴上,或者换句话说,业务功能与技术基础。在下图中,我们看到一个特性(黄色条)是通过任何应用或项目的水平组件的垂直切片(图 9-1 )。
图 9-1
垂直和水平分割
不要被说服这允许某种科学分类;相反,这是一种评估模块如何分解和组织的便捷方式,在独立的 Django 应用环境中,如何定义它们的工作。
水平分割的模块——应用或其他——提供了某种通用的“基础设施”,可以在支持功能开发的项目中使用。这是一段只有开发人员才能体验到的代码(总的来说)。
例子包括
-
django-model-utils
-
django-扩展
-
Django-克里西弗斯
这些主要解决开发团队面临的问题,比如使复杂表单的呈现变得更容易,提供公共基类以避免重复的样板代码。
项目中水平组织的代码看起来像这样,强调通过代码首先做什么以及它其次解决什么业务领域来组织:
app/
forms/
models/
...
image_models.py
user_models.py
subscription_models.py
views/
这看起来很熟悉,因为这是在 Django 应用中组织代码的惯例,但也是在其他框架中组织项目代码的惯例。
另一方面,垂直分割的模块首先是围绕业务需求组织的,也就是说,通常是用户将会经历的事情。
例子包括
-
决哥,请回答
-
干草堆
-
Django-沙阿
显然,这不是一个“非此即彼”的区别。但 Django 以应用为中心的架构鼓励垂直或基于功能的组织。Django 应用包括从模型到 URL 路由到表单和视图的所有内容。这必然是独立应用发布的方式,但这也是 Django 项目中应用的默认模式。
如果你的目标功能看起来像是与功能无关的东西,它可能需要包含在一个独立于功能的应用中。
确定应用的范围
一个独立的 Django 应用应该足够大以完成它的工作,而不是更大。但是那有多大呢?错误地确定独立应用的范围会有什么后果?你如何解决一个看起来太小或太大的应用?
首先,一个应用不应该比它的工作大。但也要足够大。如果它太大,要么它做得太多,要么它本身就是另一个框架。太小,它可能不保证创建一个独立的应用,甚至是一个可安装包。
当应用太大时
一个 app 太大意味着什么?有一些相当大的独立应用不一定太大。答案一清二楚:看情况。让一个应用变得太大的第一件事是包含无关紧要的功能,这些功能要么对应用不重要,要么在他们的自己的应用中足够有用。一个容易处理的例子是一个包含自己的工具基类的应用,就像你在 django-model-utils 中找到的那种。这些额外的功能可能会在维护应用和添加功能时分散注意力。它们也增加了需要测试的代码量。如果有必要,不要抛弃它们,但要确保它们是必要的。
对于太大的应用,有几个突出的解决方案:
-
把它分成单独的包裹。
-
组织成子应用。
如果至少有一个潜在的包本身就足够有用的话,那么分解成单独的包是一个好主意。在这种情况下,独立包的好处是可以为更多的用例提供更广泛的服务。然而,无论是否是独立的应用,每个额外的包都会增加维护成本,并可能使确保两个组件继续良好合作变得更具挑战性。
如果它们是相互依赖的,那么创建单独的包没有好处,在一个包中使用子应用或迷你应用是更好的选择。
将独立应用组织成子应用需要将每个应用单独添加到目标项目的 INSTALLED_APPS 中,如下所示:
INSTALLED_APPS = [
...
"cms",
"cms.pages",
"cms.photos",
...
]
这种策略包括一个主要的顶级应用,以及附属和组成功能应用,作为它们自己的可安装应用。虽然比只添加一个“核心”应用要笨拙,但这可能允许您更好地逻辑构建组件,并允许开发人员用户排除他们不需要的功能。这不会减少你的应用的源代码,但它确实减少了任何使用应用的人必须考虑的范围。
当应用太小时
制作一个太小的应用的问题与制作一个太大的应用的问题是不同的,可以说没有那么重要。主要的问题是,它有导致大量微小依赖的风险,这些依赖都需要被包含和维护。当独立包的数量超出了你容易记住的数量时,使用独立包的好处就减少了。我们希望尽量减少我们的应用对其他人的项目的依赖性。
如果你打算创建多个小应用,首先考虑它们是否真的有意义,或者它们是否在功能或业务领域有足够的通用性来捆绑在一起。如果您发现自己在不止一个项目中同时使用这些特性,很可能就是这种情况。
不过,创建一个小小的独立应用不应该被视为禁止。在光谱的两端有明显的张力。
摘要
在本章中,您学习了如何确定应用的范围,包括定义应用要完成的工作、评估应用可能的功能和组件维度,以及确定如何评估和调整范围的大小。在下一章,我们将学习如何从现有的 Django 项目开始重构和提取你的应用。
十、分离您的应用
虽然有些软件包完全以独立的可安装库的形式出现,但更典型的是以一种或另一种形式作为现有项目中的功能出现。这可能分布在整个项目中,也可能在项目的应用中。
本章的目标是从一个或多个应用中提取功能,要么移除一个整体,要么整合多个应用中的功能,并将其放入项目中一个独特的、与项目无关的应用中。这不仅意味着它有自己的应用,还意味着它“不知道”也不依赖于你最初的 Django 项目的细节。
入门指南
例如,这可能意味着从软件即服务(SaaS)项目中提取和删除特定的订阅计划信息。这意味着只使用 Django 的核心设置或特定于应用的设置。这通常意味着放弃特定后端服务的假设,如电子邮件提供商或云文件存储提供商,这种假设对于应用的核心功能是不必要的。
所有这一切的关键是理解你的应用(??)目前在你的项目的依赖层次中的位置,以及它们应该在那个层次中的位置。依赖层次描述了 Django 项目中各种模块之间的相互关系(它可以用于任何软件项目,甚至是一个单独的 Python 包),以及相互之间的依赖关系。它包括您即将独立的应用、特定于项目的 Django 应用、其他第三方独立的 Django 应用、Django 本身、与 Django 无关的第三方 Python 包,甚至 Python 标准库。
- 这里是建筑图。我们想展示你在拉基础应用,或者从上面看,这取决于什么。
因此,我们的目标是,如果有必要的话,将 app 提升到依赖关系的层次上,这样它就可以在外部 Django 项目中使用。
首先重构
任何时候,当你回到甚至开始写代码的时候,都会有重写或编辑源代码的诱惑,或者通常所说的重构。然而,就其原始含义而言,重构意味着只修改代码,而不影响其工作方式,或者用马丁·福勒的话说(着重号后加):
重构是改变 软件系统的过程,其方式是不改变代码的外部行为,而 改进其内部结构 。
—重构,第 1 版。
马丁·福勒
这个术语的口语含义要宽松得多,通常用来描述对源代码的任何类型的编辑。然而,这里我们关注重构的更严格的定义。这包括从重新格式化源代码到重命名变量,直到分解或移动函数、方法、类甚至模块。它不涉及添加新功能或替换算法。这些可能是好的,甚至是必要的步骤,但它们不是重构。
尽管在重构方面使用了诱惑这个词,但重构是一件好事。它往往使代码更容易理解,也更容易重用。这些都是提取独立应用的好处。然而,在某些情况下,如果它掩盖了代码中进行的其他更改,它可能会成为一种干扰,所以当前面有估计的重构级别时,首先解决它是有意义的。这就解决了这个问题,并使代码更容易处理更重要的工作。
代码格式化——从标准化的缩进到包导入的组织——是一个很好的起点。代码中的差异有助于跟踪您的进展,如果它们充满了虚假的变化,就没那么有用了。如果你决定使用像 black 这样的自动套用格式软件,而你以前从未使用过,那么它的第一次运行可能会产生很大的差异。首先将它隔离为一个提交,然后继续前进,这样您就知道您做了哪些更改,哪些只是清除了空白。
接下来,您可能想要重命名一些变量或函数,特别是如果它们的命名方式过度反映了更大的 Django 项目。将此重构作为第一步的第二个原因是,对函数和类名、函数签名或类初始化签名的更改可能会级联到您现有的 Django 项目中。尽可能多的预先加载这些工作是有好处的,这样后续的更新可以尽可能的集中在应用本身。
模型重命名和迁移
重命名模型和/或数据库表是一项重构工作,但是它伴随着一些特定于 Django 的警告。这是因为对 Django 模型的类名甚至模块名的更改会影响迁移状态,当没有显式指定表名时,还会影响默认的数据库表名。以这种方式不小心更改表名可能是破坏性的。
第一步是确保你的应用模型使用一个公共的、合理的数据库表名称空间。您现有的应用名称可能不适合独立的应用,也有可能您的模型是在项目中的另一个应用中诞生的。因此,表名可能是不一致的,并且它们可能是无意义的描述。
因此,对于应用中的每个模型,确保其当前表名在模型的元类中明确命名:
class LogCategory(models.Model):
class Meta:
db_table = "tracking_logcategory"
class Entry(models.Model):
class Meta:
db_table = "someapp_entry"
此时,您应该创建一个迁移文件来捕获这种状态更改,尽管此时它对数据库没有任何影响(因为名称实际上没有改变):
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AlterModelTable(
name='entry',
table='someapp_entry',
),
]
第二步是根据需要使用 Meta.db_table 属性重命名这些表:
class LogCategory(models.Model):
class Meta:
db_table = "tracking_logcategory"
class Entry(models.Model):
class Meta:
db_table = "tracking_entry"
同样,为这个变更创建一个单独的迁移文件。运行时产生的迁移将改变底层数据库表名,但不会影响数据库的结构。
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AlterModelTable(
name='entry',
table='tracking_entry',
),
]
此时,您可以在应用中自由地重命名模型类,而不会影响数据库。
- 一点:如果您在应用的任何模型上定义了多对多字段,您应该确保您有一个显式定义的 through 模型,其表名在前面的示例中定义。您还需要从现有的表名开始。
如果您在应用之间移动模型类——例如,提取一个特定于项目的模型或者合并几个模型——您可能会遇到的另一个难题是,当模型改变应用时,Django 迁移是具有破坏性的。如果我们想将一个模型移出一个应用,下面是结果迁移:
class Migration(migrations.Migration):
dependencies = [
('myappp', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='Entry',
),
]
该迁移操作是一个删除操作,如果运行,将导致删除底层数据库表。当然,有各种方法可以解决这个问题。你可以假装迁移,跑步。/manage . py migrate myapp 0001-fake,例如,在不影响数据库的情况下推进迁移状态。这随后需要为目标或接收者应用执行,可以说在本地开发中有点麻烦。在生产部署中尝试编排是非常讨厌的。
您还可以子类化 migration operation 类,使其 database_forwards 方法不做任何事情,从而不会影响数据库中的任何更改:
class DeleteNothing(migrations.DeleteModel):
def database_forwards(self, *args, **kwargs):
"""Do nothing"""
pass
这无疑优于伪造迁移,但同样麻烦,可能令人困惑,而且谢天谢地是不必要的。这是迁移的一个用例。SeparateDatabaseAndState 操作类。
像我们的 DeleteModel 这样的数据库迁移有两个影响:一个是对数据库的影响,这是我们试图防止发生的,另一个是对应用模型的累积状态的影响。我们确实需要后者。迁徙。SeparateDatabaseAndState 类允许您将这两者分开,以便可以运行影响迁移的状态。结果是更新的迁移状态“知道”表名是什么,并且不会对底层数据库产生任何影响。
实现这一点很简单;我们在顶级 Migration.operations 中插入类初始化调用,然后将删除操作移到 state_operations 关键字参数中,以分隔 DatabaseAndState。先前的迁移变成了这样:
class Migration(migrations.Migration):
dependencies = [
('myappp', '0001_initial'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.DeleteModel(
name='Entry',
),
]
)
]
当运行时,这将推进 myapp 迁移历史的状态,以便入口模型不再是应用的一部分,但是它不会对数据库进行任何更改。
允许定制
很可能你的应用基于你对它的使用做出了一些假设。这些可能包括特定的支持服务或者甚至是工作流,它们接近你在另一个项目中所需要的,但是仍然足够具体,可以作为共享功能使用。有几种方法可以解决这个问题,虽然现在没有必要去做,但是在你的工作项目中进行这些改变是有利的。
后端类
您可能使用过提供定制功能选项的独立应用,包括
-
django-anymail(电子邮件)
-
django-allauth(认证)
-
干草堆(搜索)
其中的每一个都解决了一个特定的业务问题,这个问题有多个解决方案。在上述应用的情况下,这是通过允许开发人员用户选择特定的后端或提供商来实现的。与 Django ORM 中的数据库支持一样,每个定义的后端或提供者处理集成所需的细节,但是向开发人员用户公开一个公共接口以获得无缝体验。
使用这个选项的关键是每个后端或提供者类都从基类继承或者匹配一个基本接口。如果您的应用应该管理工作流,但当前有一个非常特定于项目的工作流,则可以将此工作流移动到项目中的一个单独模块中,并通过应用中的导入来引用。
在项目设置中,您可能需要指向类或模块的点状路径:
MYAPP_WORKFLOW = "core.workflows.CustomerWorkflow"
然后,在您的应用中,当您需要启动此工作流时,您可以简单地通过路径导入正确的类或模块:
from django.conf import settings
from django.utils.module_loading import import_string
def get_myapp_workflow():
"""Returns the class by dotted path from settings"""
import_string(backend or MYAPP_WORKFLOW)
def run_workflow():
"""Calls the user/project defined class"""
workflow_class = get_myapp_workflow()
customer_workflow = workflow_class().start()
...
信号
Django 的信号提供了一种分派命名事件的方法,并使用同步回调函数来处理它们。原型例子包括 Django 在 ORM 对象的生命周期里程碑发出的内置信号,包括 pre_save、post_save、pre_delete 和 post_delete。
信号允许您从特定的类对这些事件做出响应,并修改正在讨论的对象,或者作为特定操作或参数的结果触发一些其他工作流。当过度使用时,信号可能会令人困惑,使程序的流程变得混乱,使调试、测试、管理性能以及整体维护变得更加困难。*也就是说,*它们确实解决了当你知道无法访问原始代码时,如何改变某个对象的处理方式或者某个函数应该如何工作的问题。例如,在您自己的项目中,向模型类的 save 方法添加一些语句是微不足道的;当使用独立的应用时,这变得不可行。
class Entry(models.Model):
def save(self, **kwargs):
some_webhook(self)
return super().save(**kwargs)
特别是如果自定义功能看起来像是一次性的,或者是一种将一个模型中的更改绑定到另一个模型的方式,以便它遵循 Django 自己标记的相当标准的模式,signals 可以帮助您从应用中解开特定于项目的逻辑,并将其保留在您的项目中:
from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import Entry
@receiver(pre_save, sender=MyModel)
def webhook_sender(sender, ∗∗kwargs):
some_webhook(kwargs["instance"])
如果您正在寻找可以自由使用调试日志的地方,信号处理程序是一个很好的起点。
完成并从项目中移除
作为删除代码前的最后一步,借此机会清理和压缩应用迁移,以便当您从项目中删除代码时,它在没有任何项目引用的情况下开始运行。没有人想进行虚假的迁移。
一旦提取了代码,您就可以将它移出项目。现在还没有必要急着发布 Python 包索引(PyPI)。
从这里,您可以使用 Git 将代码作为子模块包含在您的项目中,或者设置可安装包的基础并包含来自远程源代码库的需求。这允许您开始在多个项目中使用单个代码库。
摘要
在本章中,我们回顾了从项目中移除应用的一些策略,包括如何保留现有的数据库结构,以及当应用代码不再是项目的一部分时,如何启用项目级定制。
在下一章,我们将看看一些将你的应用添加回你的项目的策略。
十一、重新添加你的应用
从项目中提取应用需要几个步骤:
-
重构应用
-
将其移动到顶级名称空间(如果需要)
-
从项目中移除它
第三步给你留下了一个难题,如果你想在你原来的项目中继续使用应用。除非您不需要新的独立应用,或者在某些情况下,您需要在项目中保留原始的、未重构的源代码,否则您需要将新的独立应用包含在衍生它的一个或多个项目中。
本地验证
第一步是使用项目外部本地安装的源代码来验证集成。为此,您可以使用 python setup.py develop 在项目的 Python 环境中进行安装,如第八章所述。注意,如果您的项目只能通过一个虚拟机(例如,使用 vagger)或一个容器(例如,Docker)获得,那么跳到下一个部署步骤会更快。
我们在这一点上的假设是,您已经从项目中移除了应用,无论是从项目根还是从项目存储库中,这样它就不在项目路径中了。至少,这意味着如果您试图在没有以某种明确的方式安装应用的情况下运行您的项目,您可能会遇到一两个导入错误。
然而,简单地从项目根目录之外的应用自己的根目录安装意味着您可以验证项目是否仍然如预期的那样运行,其中的代码已经从项目本身删除。
基于源代码管理的包
从本地安装的包中安装和使用您的应用对于测试和开发新功能来说很好,但在部署项目时就不行了。为此,我们需要远程可用的应用,一个简单的方法是通过源代码控制提供包。作为起点,或者对于没有私有包索引的私有可重用应用,这是一个简单的起点。
Python 包安装程序 pip 允许您以多种方式安装包。当然,您可以只提供包名,pip 将在 Python 包索引中查找指定的包。但是,pip 也可以从远程源代码控制链接安装软件包,也就是 Git 存储库。
完整的细节记录在 pip 网站上,但是简单地说,它通过提供(I)版本控制协议,(ii)到存储库的路径,以及(iii)目标包名来工作。在以下示例中,软件包 myapp 是从 Git repo 安装的,并带有 Git 存储库的完整路径:
pip install git://githost.org/myapp.git@v1.0#egg=myapp
此示例还通过使用 Git 标记安装了一个特定的版本。片段@v1.0 由两部分组成:@表示后面是存储库中已命名的头,即分支名、标记名或提交 SHA,其余部分是该名称本身。然后,行尾 egg=myapp 指定了目标包的名称。
这可以像包名一样添加到 requirements.txt 文件中。
如果你是从你的 GitHub 库安装的,假设你的用户名是 me,同样的一行应该是这样的:
pip install git://github.com/me/myapp.git@v1.0#egg=myapp
使用标记或提交的源代码管理版本规范在技术上是可选的,但是为了使用这种策略实际安装包,您应该总是使用通过标记名或提交 SHA 的提交版本。分支名称可能很吸引人,但却是移动的目标,不允许您有效地声明版本。
从 Git 而不是从 Python 包索引(PyPI)使用和安装包的优势主要与发布包的控制和开销有关。您不需要在 PyPI 上注册名称,不需要构建任何东西,也不需要认真担心 changelog 或者您的包元数据是否正确。这些都不是很重要的问题,但是如果你只是想开始重用你的应用,你可以先跳过这些准备工作。
这些限制不仅包括可见性的降低,这是通过 PyPI 和通过一个简短的 pip install myapp 安装提供的,还包括可重用性的降低。可见性对此有影响,但最大的障碍是缺乏连续可用的版本。使用 Git 标签和提交 sha 可以让您锁定特定的版本,这对于单个项目来说是一个很好的策略,但是对于其他包来说却是不可行的,因为您失去了基于范围(例如django>=3.0
)选择版本的能力。当包固定它们需求的版本时,它们将在环境中强制使用那个版本,即使其他包可能需要相同的包。
图 11-1
阴影区域是相互兼容版本的范围
如果两个包都通过提供最小和/或最大支持版本号来要求相同的依赖性(如图 11-1 所示),那么我们通常可以期望找到一些相互兼容的版本。相反,如果这些包中的一个固定了一个特定的版本,,那么它可能会安装这个特定的版本,这个版本不在另一个包指定的兼容范围内,即使这个特定的固定版本是不必要的。
基于源代码控制的包安装排除了像这样的版本范围的使用,并将要求版本锁定。
已发布打包
为了易于使用和安装的一致性,以及在更大的 Django 生态系统中的可见性和访问,您需要将您的应用发布到一个包索引中。发布还允许您充分利用需求规范中的版本号,甚至是私有索引。
发布包的最基本的工作包括(I)在索引上注册包名,(ii)构建包,以及(iii)上传构建文件。
你需要两个额外的包裹,轮子和绳子。wheel 包用于构建 Python wheels,这是预构建的 Python 包,使用。whl 分机。对于开发人员来说,安装 Wheel 文件比安装普通的源代码包要快得多。twine 包用于将您的包构建上传到包索引。您可以在没有 twine 的情况下做到这一点,但 twine 将确保使用 HTTPS,并简化注册和上传多种包格式的步骤。
pip install wheel twine
注册名称是一个一次性的步骤,它会保留包的名称。这可以防止名称冲突,所以您需要确保您选择的包名称还没有被使用。然而,这是我们不会明确采取的一个步骤。可以使用 setup.py 的“注册”命令;然而,这不一定是安全的,twine 会安全地做到这一点,并且不需要添加显式的用户步骤。尽管如此,这是值得注意的一步!
为了利用基于版本号的安装,您的软件包将需要一个版本号。我们希望在两个地方包含版本号,setup.py 文件和您的模块根目录。如果是一个独立的 Django 应用,后者应该是 myapp/init.py。作为传播版本号的更复杂方法的替代,您可以从在两个地方对其进行硬编码开始。
# setup.py
from setuptools import setup, find_packages
setup(
name="blog",
version="0.1.0",
author="Ben Lopatin",
author_email="ben@benlopatin.com",
url="http://www.django-standalone-apps.com",
packages=find_packages(exclude=["tests"]),
)
# __init__.py
__version__ = "0.1.0"
setup.py 文件中的版本号用于在软件包索引中注册版本号,并在安装时管理版本。您的软件包中包含的版本号,即 init。py 文件是非常有用的元数据,可以用来验证安装和使用的是哪个版本的软件包。这并不意味着它是多余的,但这意味着只改变 init 中的版本。py 文件本身不会对作为新版本发布到包索引的内容产生任何实质性影响。这些更改必须在 setup.py 文件中或通过该文件进行。
- 在第四部分中,我们将会看到一些处理版本号更新的改进方法。
在构建和上传之前,最好快速检查一下您的包元数据是否有效。您可以通过运行 python setup.py check:
python setup.py check -r -s
check 命令将对您的包元数据执行最少的验证。您应该始终运行此步骤,以确保其格式正确。如果检查失败,则-s 选项将导致脚本退出并显示错误代码,而-r 选项检查所有字符串是否都符合 reStructuredText。如果您打算在自述文件中使用 Markdown 并读入 setup.py 文件,则应跳过此选项;否则,这可以防止包索引的格式被破坏。
设置了版本号并验证了元数据后,接下来您需要构建一个发行版,即人们在安装您的应用时实际下载的文件。大致来说,有两种方式来构建这个包:使用一个源发行版和使用一个轮发行版。它们并不相互排斥,所以我们将构建两者(记住,您需要安装 wheel 包来构建包含以下内容的 wheel):
python setup.py sdist bdist_wheel
这将在您的软件包的 dist/目录中创建一个扩展名为. whl 的归档文件和一个归档文件,两者都根据您的软件包名称和版本进行命名(sdist 创建的特定扩展名因系统而异,并且是可配置的)。
然后是上传发行版的时候了,安装了 twine 之后,命令看起来像这样:
twine upload dist/*
如果您还没有注册包名,twine 上传步骤将在上传前首先注册包名。如果成功,您将看到您的新版本——或新软件包——安装在软件包索引上。如果由于某种原因上传失败,例如,只有一个分发选项,您可以修复问题(如果有的话),并尝试重新上传失败的分发。不可能重新上传相同版本的发行版,但是如果发行版没有成功上传,那么这个限制就不适用。
最后一步是标记您的发布版本。使用 Git,您可以使用 tag 命令,就像这样:
git tag -a v0.1.0 -m "Initial version"
标记的目的是确保您可以跟踪每个版本中到底部署了什么。出于这个原因,您应该在成功发布新版本之后的在您的存储库中标记您的提交。如果您必须在上传软件包版本之前进行最终更改,这可以防止标记错误的版本。
摘要
在本章中,您学习了一些将提取的应用作为独立应用添加回项目的策略。您学习了如何在本地完成这项工作,方法是在项目路径中安装应用,使用源代码控制和版本标签远程完成这项工作,最后作为已发布的可安装包发布到 PyPI。我们将在随后的章节中进一步探讨改进打包过程的方法。
十二、处理应用设置
每个 Django 项目都可以通过它的 settings.py 文件的设置模块进行配置。这就是你如何指定你正在使用的数据库和如何连接到它,如何配置你的模板系统,当然还有安装什么应用。一个典型的设置文件包含了 Django 的一般设置(比如数据库和密钥),项目应用设置,当然还有独立应用的设置。
并非每个独立应用都需要自己的用户可配置设置。但是独立应用需要自己的设置有各种各样的原因,包括
- 第三方 API 集成
-
特定于应用的缓存行为
-
功能切换
-
指定依赖关系
-
限制允许的文件类型
-
将设置添加到你自己的项目中非常简单,将它们添加到一个独立的应用中也不是什么难事。但是,由于应用将集成到其他项目中,并且可能包含具有各种值和类型约束的设置,因此需要预先考虑在应用中命名、构建和包含这些内容。
设置命名
首先考虑的是命名。不仅要清楚地命名设置值,还应命名为易于与应用关联的名称。实际上,这意味着它们应该用一个特定于应用的前缀来命名。
可以在 Django 本身的 contrib.auth 应用中找到这样的例子。auth 应用允许您指定自定义用户模型,如下所示:
AUTH_USER_MODEL = "custom_users.User"
这可以用 USER_MODEL 简单而简洁地命名,但是 AUTH _ preamble 确保它显然对应于 AUTH 应用。
因此,如果你的应用公开了一些这样的设置
MAX_API_TIMEOUT = 10
SERVICE_API_KEY = "helloworld123"
确保它们的命名空间与您的应用相对应:
MYAPP_MAX_API_TIMEOUT = 10
MYAPP_SERVICE_API_KEY = "helloworld123"
设置格式
设置最终是可以通过 django 项目中的 django.conf.settings 访问的 Python 对象。因此,尽管我们认为 DEBUG 的设置为布尔值,SECRET_KEY 的设置为字符串值,但它们并不局限于简单类型,甚至不局限于内置类型。DATABASES 设置是一个字典,TEMPLATES 是字典列表,INSTALLED_APPS 和 MIDDLEWARE 是字符串列表。
- 平的比嵌套的好。
在显示应用的配置设置时,显示的值越简单越好。在“什么格式?”根本问题主要是使用多个顶级设置还是一个或多个嵌套设置的字典。
例如,对应用的所有设置值使用一个字典可能很有诱惑力,这样就只有一个“设置”这种方法的好处是保证了最终用户设置文件的简单性,但在许多情况下,它会混淆这些设置的来源。例如,如果最终用户使用 12 因子应用风格运行他们的 Django 项目,并使用环境变量来填充设置值,那么理想情况下,它们应该与顶级设置值具有 1:1 的关系。
- 虽然实用性胜过纯粹性。
这应该是一个很好的默认设置,而不是硬性规定。使用字典公开设置的主要优点是,当设置组相互关联时,它会变得更加明显。在这里的设置片段中,很明显缓存设置是紧密相关的(特别是如果有其他应用设置的话)。
MYAPP_CACHE_TTL = 10
MYAPP_CACHE_KEY_PREFIX = "myapp"
MYAPP_CACHE = {
"TTL": 10,
"KEY_PREFIX": "myapp",
}
然而,使用字典的一个缺点是它可能不太清楚默认值是如何被覆盖的。提供的整个字典是否算作导入的设置?还是使用最终用户的设置来更新现有的默认值?至少当一个顶级 app 设置是而不是添加在最终用户的设置中时,很明显会使用默认。
关于环境变量的最后一点:我应该强调,在 Django 项目中使用环境变量是最终用户的特权,而不是独立的应用开发人员的特权。避免在流程环境中期待值的诱惑,始终依赖设置模块。否则会不必要地限制最终用户提供设置的方式,并且还会强制实施环境变量命名约定,尽管这些约定看起来很合理,但并不适合最终用户自己的情况。
采购应用设置
最后要考虑的是,如何将这些设置放到你的应用中需要的地方。这主要会影响您对应用的使用,因为这些设置通常会在应用内部使用。
下面是 views.py 摘录的一个简短示例,其中几个特定于应用的设置来自 django.conf.settings:
# myapp/views.py
from django.conf import settings
from myapp.client import ApiClient
USE_CACHING = settings.MYAPP_CACHE_SETTINGS["USE_CACHING"]
CACHE_PREFIX = settings.MYAPP_CACHE_SETTINGS["CACHE_PREFIX"]
CACHE_TTL = settings.MYAPP_CACHE_SETTINGS["CACHE_TTL"]
def list_api_resources(request):
client = ApiClient(settings.MYAPP_API_KEY)
api_results = cache.get(f"{CACHE_PREFIX}:results")
if not api_results:
api_results = client.list()
cache.set(f"{CACHE_PREFIX}:results", api_results, CACHE_TTL)
return render(request, "myapp/api_resources.html", {
"api_results": api_results,
})
首先,这里有许多可能出错的地方:
-
设置中可能未定义 MYAPP_CACHE_SETTINGS 名称,或者可能为其分配了错误的类型,从而导致 AttributeError。
-
MY_API_KEY 可能丢失,也会导致 AttributeError。
-
类似地,任何单独的 MYAPP_CACHE_SETTINGS 值都可能丢失,从而导致令人困惑的 KeyError。
-
并且任何单独提供的高速缓存设置可能具有错误的类型,或者错误的*值,*如果对于 a 设置有合理的值界限。
在您自己的项目中,您可以在设置模块中检查和绑定您的设置值,但这不是您可以委托给应用最终用户的事情。相反,应该检查这些错误,并尽早在您的独立应用中捕获错误。实际上,这意味着检查缺失或格式错误的值,并尽快引发配置错误。
# myapp/views.py
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from myapp.client import ApiClient
if not getattr(settings, "MYAPP_API_KEY"):
raise ImproperlyConfigured("MYAPP_API_KEY must be set")
try:
USE_CACHING = settings.MYAPP_CACHE_SETTINGS["USE_CACHING"]
except (AttributeError, KeyError):
USE_CACHING = False
try:
CACHE_PREFIX = settings.MYAPP_CACHE_SETTINGS["CACHE_PREFIX"]
except (AttributeError, KeyError):
USE_CACHING = "myappp"
try:
CACHE_TTL = int(settings.MYAPP_CACHE_SETTINGS["CACHE_TTL"])
except (AttributeError, KeyError):
CACHE_TTL = 3600
except (TypeError, ValueError):
raise ImproperlyConfigured("MYAPP cache TTL must be a number")
def list_api_resources(request):
""""""
client = ApiClient(settings.MYAPP_API_KEY)
api_results = cache.get(f"{CACHE_PREFIX}:results")
if not api_results:
api_results = client.list()
cache.set(f"{CACHE_PREFIX}:results", api_results, CACHE_TTL)
return render(request, "myapp/api_resources.html", {
"api_results": api_results,
}
现在,至少如果最终用户忘记提供 MYAPP_API_KEY 或者不小心将缓存 TTL 设置为“helloworld ”,您可以用易于理解且有帮助的错误消息来捕捉这些错误。如果缺少一个可能缺少的值,就会提供一个合理的默认值。
然而,包含在具有不同目的的模块中的代码是混乱的,并且如果这些值中的任何一个在其他模块中是必需的,那么要么需要重复这个操作,要么这些其他模块将需要有选择地从 views.py 文件中导入这些清除的值。相反,让我们将所有这些特定于应用的设置移到它们自己的模块中。这将允许您在一个地方封装所有的值检查,并且没有其他模块需要知道这些设置是如何获得或给出的。
虽然 conf.py 和 app_settings.py 也是常见的选择,但这种模块的一个明显的名称是 settings.py。我个人更喜欢 conf.py。第一种是最流行的方法,虽然明智,但这意味着它更容易引起混乱,尤其是在应用中的任何其他模块都导入 django.conf.settings 的情况下;当然,一个解决方案是简单地将那些单独需要的全局设置导入到你的应用设置模块中。
现在有了一个特定于应用的设置模块,可以从中导入这些设置,views.py 和其他模块只需要导入它,就可以避免任何类型的额外错误和默认值处理:
# myapp/conf.py
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
# Required values
MYAPP_API_KEY = getattr(settings, "MYAPP_API_KEY")
if not MYAPPP_API_KEY:
raise ImproperlyConfigured("MYAPP_API_KEY is missing")
# Values with defaults
USE_CACHING = True
CACHE_PREFIX = "myapp"
CACHE_TTL = 60 ∗ 60
try:
USE_CACHING = settings.MYAPP_CACHE_SETTINGS["USE_CACHING"]
except (AttributeError, KeyError):
pass
try:
CACHE_PREFIX = settings.MYAPP_CACHE_SETTINGS["CACHE_PREFIX"]
except (AttributeError, KeyError):
pass
try:
CACHE_TTL = int(settings.MYAPP_CACHE_SETTINGS["CACHE_TTL"])
except (AttributeError, KeyError):
pass
except (TypeError, ValueError):
raise ImproperlyConfigured("MYAPP cache TTL must be a number")
摘要
在本章中,我们介绍了处理特定于应用的设置的策略,包括如何命名空间和构建特定于应用的设置,如何在应用中获取这些设置,以及如何最好地处理缺失值和错误值。
在下一章中,我们将看看如何让你的独立应用可以在你自己语言之外的语言中使用。
十三、国际化
国际化和本地化允许不同语言的人使用不同的书写上下文(例如,日期格式)来使用应用。概念上很简单,这是让更多人可以使用软件的一个强有力的方法。
作为一个以英语为母语的国家的人,我认为公平地说,在英语国家,大多数以英语为母语的人很少考虑他们的软件将如何被说其他语言的人或其他国家的人使用。然而,只为一种语言的“市场”编写软件不利于你的利益,因为你用其他语言编写软件的成本相当低,结果是更大的用户群,包括最终用户和潜在贡献者。
有几个步骤可以让你的独立 Django 应用对使用其他语言的人有用,这些步骤同时按顺序和优先级排列。
为什么翻译
举个简单的例子,假设您的应用包含一个执行一些基本验证的表单类。在我们的例子中,它检查优惠券字段中提供的值是否与当前活动的优惠券相匹配。如果不匹配,那么数据就不会被验证,并返回一个错误字符串,表单将显示给用户。
class CouponForm(forms.Form):
coupon = forms.CharField(required=False)
def clean_coupon(self):
data = self.cleaned_data.get('coupon', '')
if data and not Coupon.objects.active().filter(code=data).exists():
raise forms.ValidationError(
"Invalid coupon code"
)
return data
现在,对于您的应用的每个用户,显示的验证消息将始终是“您输入了无效的优惠券代码”,无论他们的网站配置了什么语言。如果您想用西班牙语提供它,那么您需要检查 Django 项目中的字段或特定消息,然后返回一个自定义消息。
class CouponView(FormView):
def form_invalid(self, form):
context = super().get_context_data(form=form)
spanish_errors = {}
if (
form.errors.get("coupon", [""])[0] ==
"Invalid coupon code"
):
spanish_errors["coupon"] = "Cupón inválido"
context["spanish_errors"] = spanish_errors
return self.render_to_response(context)
这显然是一个人为的例子,你可能已经看到了一些简化的方法。但如果这种习俗的改变完全没有必要,那就好了。
稍加修改,它们就没有必要了。
可翻译字符串和翻译如何工作
该解决方案可以完全在 form 类中实现,只需一次导入,并将字符串“包装”在对gettext
的函数调用中:
from django.utils.translation import gettext as _
class CouponForm(forms.Form):
coupon = forms.CharField(required=False)
def clean_coupon(self):
data = self.cleaned_data.get('coupon', '')
if data and not Coupon.objects.active().filter(code=data).exists():
raise forms.ValidationError(
_("Invalid coupon code")
)
return data
我称之为“包装”字符串,因为使用 common _ alias 看起来就是这样,但不要搞错了,这是一个函数调用。执行时,将根据调用上下文中设置的语言环境使用外部程序gettext
获取返回的字符串,该语言环境可以是默认语言环境,也可以是最终用户在会话中选择的语言环境。
以这种方式,国际化只不过是简单的字典查找。然而,与英语-西班牙语词典不同的是,对于所选的单词或短语没有微妙的选项;相反,这种查找的行为更像一个 Python 字典,其中每个字符串都是返回另一个字符串的精确键。
对国际化的一个常见反对意见是,最初的开发人员不知道潜在的语言是什么,或者对它们的了解不足以提供翻译,因此付出努力没有什么意义。幸运的是,让独立的应用可翻译,甚至被翻译,没有这样的要求!
确定翻译步骤的优先顺序
启用翻译的第一步是使字符串可翻译。最简单地说,这意味着如前所述,在对一个gettext
函数的调用中“包装”您的字符串。django.utils.translation 模块中有几个gettext
函数,以及一个模板标记库;它们的详细用法在 Django 的官方文档中有所记载,在此不必赘述。你的应用的首要任务是确保 Python 代码中面向用户的字符串用 gettext
和可翻译的包装。如果你除此之外什么都不做,你就已经完成了关键的 80%。
这是单一优先级的原因有两个:第一,如果字符串可供查找,完全有可能为任何语言创建必要的语言文件;第二,这是最终开发人员唯一不能更改的面向用户的内容。
- 对于面向用户的字符串,我在这里指的是任何期望显示给应用用户并在他们的浏览器中看到的字符串。除非使用异常向最终用户发出消息(例如,通过验证错误),否则您可能不希望翻译异常消息。
使模板可翻译是第二要务,这是第二要务还是第三要务完全取决于 Django 独立应用中模板的性质。这里的原因是模板完全可以被开发者用户扩展和覆盖。如果你的模板是稀疏的,并且完全打算被开发人员替换,那么使这些可翻译的价值是微不足道的。另一方面,如果您的应用中的模板具有丰富的结构,并且旨在成为面向用户的体验的一部分,那么确保这些模板是可翻译的——通过使用来自 i18n 标记库的模板标记——应该是优先考虑的事情。
随着这两项任务的完成,进一步努力的必要性和价值急剧下降,除非您知道特定语言的用例,并准备好创建翻译的资源。这些额外的步骤包括生成和添加 po 文件,即用于gettext
翻译的基于文本的源文件,与翻译服务集成,以及编译和包含 mo 文件,即gettext
使用的二进制查找文件。
生成和添加 po 文件非常简单,完全不需要目标语言知识。然而,它不涉及选择语言!这有点像没有测量就优化。除非你知道对特定语言的需求是什么,否则你无法做出选择。这可能会让你的应用更明显地准备好翻译*贡献,*但即使这是一个可疑的策略。在西半球使用最广泛的三种语言中,有许多国家特有的变体;而在使用翻译的地方,这些原本很小的差异往往是显著的。
模型内容和翻译
Django 应用中面向用户的内容有几个来源:模板、Python 代码本身以及用户控制的基于模型的内容。在大多数包含大量内容的网站和 web 应用中,基于模型的内容构成了大部分内容。虽然您作为独立的应用作者不提供这些内容,但您可以为开发人员用户提供启示来添加翻译。当然,如何做到这一点,以及这是否必要或有价值,取决于你的独立应用的性质。
在您自己的 Django 项目中,使用您控制的模型,除了下面描述的那些之外,还有几个可用的解决方案。像 django-model translation)这样的第三方独立应用允许您将特定于地区的字段添加到现有模型中,并在最少干预的情况下从您的应用无缝访问这些字段。然而,这涉及到修改数据库表,这意味着数据库迁移,而在第三方应用的情况下,这意味着试图管理不受您控制的库的迁移,而且如果您使用任何类型的临时部署系统,会失去对这些迁移的跟踪,所有这些都意味着对于管理 Django 项目的开发人员用户来说,试图在第三方应用中为模型添加翻译支持是不可行的。谢天谢地,作为 Django 独立应用的开发者,你可以提供一些启示。
对于内容繁重的应用,其中模型有几个或许多表示面向用户内容的字段,一个优秀且灵活的策略是包含一个 locale 字段,并允许翻译随实例而变,或者更具体地说,随数据库行而变。这意味着,例如,对于一个电子邮件模型,您可能允许多个实例具有相同的基础:
from django.conf import settings
from django.db import models
class EmailType:
confirmation = "confirmation"
notification = "notification"
@classmethod
def choices(cls):
return [(cls.notification, cls.notification),
(cls.confirmation, cls.confirmation)]
class EmailMessage(models.Model):
email_type = models.CharField(
max_length=20,
choices=EmailType.choices(),
)
locale = models.CharField(
max_length=10,
choices=settings.LANGUAGES,
default="",
)
message = models.TextField()
class Meta:
unique_together = ("email_type", "locale")
现在有一种内置的方法可以将翻译的内容包含在数据库中,而无需对数据库做任何进一步的修改。不过,这种策略对于“内容密集型”模型来说最有意义,因为这种模型要么代表大量内容,要么代表大量需要一起翻译的字段。
对于只有几个字段需要翻译的模型,另一种选择是利用内置的查找字段,这种选择在撰写本文时还没有被广泛采用。如果您愿意将开发人员用户提交给 PostgreSQL 数据库,那么可以选择使用 HStoreField 或 JSONField。两者都可以用来表示字典;HStoreField 更简单,仅限于字符串,但 JSONField 使用默认的数据库功能(HStoreField 要求您安装数据库扩展)。
对于读者来说,最大限度地发挥这种策略的潜力是一项值得鼓励的工作,但最简单的做法是将核心字段数据存储在字典中:
from django.contrib.postgres.fields import JSONField
from django.db import models
class Product(models.Model):
title_source = JSONField()
price = models.IntegerField()
def title(self, locale=""):
if locale:
try:
return self.title_source[locale]
except KeyError:
pass
return self.title_source[""]
这巧妙地解决了数据存储问题,以及显式检索。这样一个接口的可用性保证了巨大的改进,包括更新数据,特别是像 django-translation 这样的东西所提供的简化查询。也许这是你的第一个 Django 独立应用!
摘要
在这一章中,我们回顾了什么是国际化,以及为什么在你的独立应用中适应国际化很重要。您了解了如何优先为应用添加翻译支持,何时为应用添加特定的语言翻译,以及如何翻译基于模型的内容。
在下一章,我们将学习管理不同 Python 和 Django 版本兼容性的问题以及解决这些问题的一些策略。