Django-Web-开发秘籍第二版-全-
Django Web 开发秘籍第二版(全)
原文:
zh.annas-archive.org/md5/8d424d0e2115094b7f97110fde8851e3译者:飞龙
前言
Django 框架相对容易学习,并且解决了许多与 Web 相关的问题,例如项目结构、数据库对象关系映射、模板、表单验证、会话、认证、安全、Cookie 管理、国际化、基本管理、从脚本访问数据的接口等。Django 基于 Python 编程语言,代码清晰易读。此外,Django 有许多第三方模块可以与您的应用程序一起使用。Django 有一个建立并充满活力的社区,在那里您可以找到源代码、获得帮助并做出贡献。
使用 Django 框架的 Web 开发食谱 - 第二版将指导您通过 Django 1.8 框架完成整个 Web 开发过程。您将从虚拟环境和项目的配置开始。然后,您将学习如何使用可重用组件定义数据库结构。接下来,本书将介绍表单和视图来输入和列出数据。然后,您将继续学习响应式模板和 JavaScript 以创建最佳用户体验。之后,您将了解如何调整管理以使网站编辑者满意。您还将学习如何在 Django CMS 中集成自己的功能。下一步将是学习如何使用分层结构。您会发现从不同来源收集数据并向他人以不同格式提供数据并不像您想象的那么困难。然后,您将介绍一些编程和调试技巧。最后,您将了解如何测试并将项目部署到远程专用服务器。
与其他 Django 书籍不同,这本书不仅会处理框架本身的代码,还会涉及一些对于全面配备的 Web 开发必要的第三方模块。此外,书中还提供了使用 Bootstrap 前端框架和 jQuery JavaScript 库构建丰富用户界面的示例。
本书涵盖的内容
第一章,开始使用 Django 1.8,指导您完成启动任何 Django 项目所必需的基本配置。它将涵盖诸如虚拟环境、版本控制和项目设置等主题。
第二章,数据库结构,教授如何编写可重用代码片段以在模型中使用。当您创建一个新应用时,首先要做的是定义您的模型。此外,您还将被询问如何使用 Django 迁移来管理数据库模式更改。
第三章,表单和视图,展示了用于创建数据视图和表单的一些模式。
第四章, 模板和 JavaScript,涵盖了使用模板和 JavaScript 一起的实用示例。我们将结合模板和 JavaScript,因为信息总是通过渲染的模板呈现给用户,在现代网站上,JavaScript 对于丰富的用户体验是必不可少的。
第五章, 自定义模板过滤器和标签,解释了如何创建和使用你自己的模板过滤器和标签。正如你将看到的,默认的 Django 模板系统可以扩展以满足模板开发者的需求。
第六章, 模型管理,指导你通过扩展默认管理来添加自己的功能,因为 Django 框架自带了一个方便的预构建模型管理。
第七章, Django CMS,处理了使用 Django CMS 的最佳实践,这是用 Django 制作的最受欢迎的开源内容管理系统,并适应你项目的需求。
第八章, 分层结构,展示了每当你在 Django 中需要创建树状结构时,django-mptt 模块会派上用场。本章将向你展示如何使用它并为分层结构设置管理。
第九章, 数据导入和导出,演示了如何在不同格式之间传输数据,以及如何从不同来源检索和提供数据。本章处理数据导入的管理命令,以及数据导出的 API。
第十章, 铃铛和装饰品,展示了在日常网站开发和调试中有用的额外片段和技巧。
第十一章, 测试和部署,教你如何测试你的项目并将其部署到远程服务器。
你需要这本书的内容
要使用 Django 1.8 进行开发,你需要 Python 2.7 或 Python 3.4,Pillow 库用于图像处理,MySQL 数据库和 MySQLdb 绑定或 PostgreSQL 数据库,virtualenv 用于保持每个项目的 Python 模块分离,以及 Git 或 Subversion 用于版本控制。
所有其他具体要求都在每个菜谱中单独提及。
这本书面向的对象
如果你已经使用 Django 创建了网站,但想要深化你的知识并学习一些处理网站开发不同方面的良好方法,这本书适合你。它旨在为需要构建多语言、适应不同屏幕尺寸的设备并且能够随时间扩展的项目的中级和高级 Django 用户。
习惯用法
在这本书中,你会找到许多不同风格的文本,以区分不同类型的信息。以下是一些这些风格的示例,以及它们含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:“如果你只有一个或两个设置,你可以在你的models.py文件中使用以下模式。”
代码块设置如下:
# magazine/__init__.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
default_app_config = "magazine.apps.MagazineAppConfig"
当我们希望将你的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:
# magazine/__init__.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
default_app_config = "magazine.apps.MagazineAppConfig"
任何命令行输入或输出都如下所示:
(myproject_env)$ python
>>> import sys
>>> sys.path
新术语和重要词汇将以粗体显示。你在屏幕上看到的,例如在菜单或对话框中的文字,将以如下方式显示:“例如,我们在电话字段中添加了一个电话图标,在电子邮件字段中添加了一个@符号”。
注意
警告或重要提示将以如下框显示。
小贴士
小技巧和窍门如下所示。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大价值的标题非常重要。
要发送一般性反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件的主题中提及书名。
如果你在一个你擅长的主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从你的购买中获得最大价值。
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
错误清单
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这个问题,我们将不胜感激。这样做可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入你的勘误详情来报告。一旦你的勘误得到验证,你的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。任何现有的勘误都可以通过从 www.packtpub.com/support 选择你的标题来查看。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在网上发现任何形式的非法复制我们的作品,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 与我们联系,并提供涉嫌盗版材料的链接。
我们感谢你在保护我们的作者和为你提供有价值内容的能力方面的帮助。
询问
如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章:Django 1.8 入门
在本章中,我们将涵盖以下主题:
-
在虚拟环境中工作
-
创建项目文件结构
-
使用 pip 处理项目依赖
-
使你的代码兼容 Python 2.7 和 Python 3
-
在你的项目中包含外部依赖
-
配置开发、测试、预发布和生产环境下的设置
-
在设置中定义相对路径
-
创建和包含本地设置
-
为 Subversion 用户动态设置 STATIC_URL
-
为 Git 用户动态设置 STATIC_URL
-
将 UTF-8 设置为 MySQL 配置的默认编码
-
设置 Subversion 忽略属性
-
创建 Git 忽略文件
-
删除 Python 编译文件
-
尊重 Python 文件中的导入顺序
-
创建应用配置
-
定义可覆盖的应用设置
简介
在本章中,我们将探讨在 Python 2.7 或 Python 3 上使用 Django 1.8 开始新项目时的一些良好实践。这里介绍的一些技巧是处理项目布局、设置和配置的最佳方式。然而,对于一些技巧,你可能需要在网络上或其他关于 Django 的书籍中寻找替代方案。在深入 Django 世界的同时,请随意评估并选择最适合你的最佳部分。
我假设你已经熟悉 Django、Subversion 和 Git 版本控制、MySQL 和 PostgreSQL 数据库以及命令行使用的基础知识。我还假设你可能正在使用基于 Unix 的操作系统,如 Mac OS X 或 Linux。在 Unix 基础平台上使用 Django 进行开发更有意义,因为网站很可能会在 Linux 服务器上发布,因此,你可以在开发和部署时建立相同的常规。如果你在 Windows 上本地使用 Django,常规是相似的;然而,它们并不总是相同的。
在虚拟环境中工作
你很可能会在你的电脑上开发多个 Django 项目。一些模块,如 Python Imaging Library(或 Pillow)和 MySQLdb,可以安装一次,然后供所有项目共享。其他模块,如 Django、第三方 Python 库和 Django 应用,需要彼此隔离。虚拟环境工具是一种将所有 Python 项目分离到各自领域的实用工具。在本食谱中,我们将了解如何使用它。
准备工作
要管理 Python 包,你需要 pip。如果你使用的是 Python 2.7.9 或 Python 3.4+,那么 pip 已经包含在你的 Python 安装中了。如果你使用的是其他版本的 Python,可以通过执行 pip.readthedocs.org/en/stable/installing/ 中的安装说明来安装 pip。让我们使用以下命令安装共享的 Python 模块 Pillow 和 MySQLdb,以及虚拟环境工具:
$ sudo pip install Pillow
$ sudo pip install MySQL-python
$ sudo pip install virtualenv
如何做到这一点...
一旦你安装了所有必备条件,创建一个目录来存储你所有的 Django 项目,例如,在你的主目录下创建virtualenvs。在创建目录后执行以下步骤:
-
进入新创建的目录并创建一个使用共享系统 site 包的虚拟环境:
$ cd ~/virtualenvs $ mkdir myproject_env $ cd myproject_env $ virtualenv --system-site-packages . New python executable in ./bin/python Installing setuptools………….done. Installing pip……………done. -
要使用你新创建的虚拟环境,你需要执行当前 shell 中的激活脚本。可以使用以下命令完成:
$ source bin/activate你也可以使用以下命令(注意点与 bin 之间的空格):
$ . bin/activate -
你会看到命令行工具的提示符前面有了项目名称的前缀,如下所示:
(myproject_env)$ -
要退出虚拟环境,请输入以下命令:
$ deactivate
它是如何工作的…
当你创建一个虚拟环境时,会创建一些特定的目录(bin、build、include和lib),以便存储 Python 安装的副本,并定义一些共享的 Python 路径。当虚拟环境激活时,使用pip或easy_install安装的任何内容都将放入并用于虚拟环境的 site 包,而不是 Python 安装的全局 site 包。
要在你的虚拟环境中安装 Django 1.8,请输入以下命令:
(myproject_env)$ pip install Django==1.8
参见
-
Creating a project file structure食谱
-
在第十一章的Deploying on Apache with mod_wsgi食谱中,Testing and Deployment的Deploying on Apache with mod_wsgi食谱
创建项目文件结构
为你的项目保持一致的文件结构可以使你更有条理并提高生产效率。当你定义了基本的工作流程后,你可以更快地进入业务逻辑并创建出色的项目。
准备工作
如果你还没有这样做,创建一个virtualenvs目录,你将在这里保存所有虚拟环境(关于这一点,请参阅Working with a virtual environment食谱)。这可以在你的主目录下创建。
然后,为项目环境创建一个目录,例如,myproject_env。在其中启动虚拟环境。我建议添加commands目录以存储与项目相关的本地 bash 脚本,db_backups目录用于数据库转储,以及project目录用于你的 Django 项目。此外,在你的虚拟环境中安装 Django。
如何操作…
按照以下步骤创建项目的文件结构:
-
在激活虚拟环境后,转到项目目录并按照以下方式启动一个新的 Django 项目:
(myproject_env)$ django-admin.py startproject myproject为了清晰起见,我们将新创建的目录重命名为
django-myproject。这是你将置于版本控制下的目录,因此,它将包含.git、.svn或类似的目录。 -
在
django-myproject目录中,创建一个README.md文件来描述您的项目给新开发者。您还可以将带有 Django 版本的 pip 要求包含在内,并包括其他外部依赖(有关此内容,请参阅 使用 pip 处理项目依赖项 菜谱)。此外,此目录将包含您的项目 Python 包名为myproject;Django 应用(我建议有一个名为utils的应用,用于在整个项目中共享的不同功能);如果项目是多语言的,则包含项目翻译的locale目录;根据 创建和使用 Fabric 部署脚本 菜谱中的建议,创建一个名为fabfile.py的 Fabric 部署脚本;以及如果您决定不使用 pip 要求,则包含在此项目中的外部依赖的externals目录。 -
在您的项目 Python 包
myproject中,创建media目录用于项目上传,site_static目录用于项目特定的静态文件,static目录用于收集的静态文件,tmp目录用于上传过程,以及templates目录用于项目模板。此外,myproject目录应包含您的项目设置,settings.py和conf目录(有关此内容,请参阅 配置开发、测试、预发布和生产环境设置 菜谱),以及urls.pyURL 配置。 -
在您的
site_static目录中,创建一个site目录作为特定于站点的静态文件的命名空间。然后,将分离的静态文件分别放在其中的目录中。例如,scss用于 Sass 文件(可选),css用于生成的最小化层叠样式表,img用于样式化图像和标志,js用于 JavaScript,以及任何组合所有类型文件的第三方模块,例如 tinymce 富文本编辑器。除了site目录外,site_static目录可能还包含第三方应用的覆盖静态目录,例如cms覆盖 Django CMS 的静态文件。要使用具有图形用户界面的 CodeKit 或 Prepros 应用程序从 Sass 生成 CSS 文件并压缩 JavaScript 文件。 -
将您通过应用分离的模板放在您的模板目录中。如果一个模板文件代表一个页面(例如,
change_item.html或item_list.html),则直接将其放在应用的模板目录中。如果模板包含在其他模板中(例如,similar_items.html),则将其放在包含子目录中。此外,您的模板目录可以包含一个名为utils的目录,用于全局可重用的片段,例如分页、语言选择器等。
如何工作…
在虚拟环境中,一个完整项目的整个文件结构将类似于以下内容:

参见
-
使用 pip 处理项目依赖项 菜谱
-
将外部依赖项包含到你的项目中配方
-
配置开发、测试、预发布和生产环境的设置配方
-
在 Apache 上使用 mod_wsgi 部署的配方在测试和部署的第十一章中
-
第十一章中的创建和使用 Fabric 部署脚本配方,测试和部署
使用 pip 处理项目依赖
pip 是安装和管理 Python 包最方便的工具。除了逐个安装包之外,你还可以定义一个你想要安装的包列表,并将其传递给工具,以便它自动处理该列表。
你将需要至少有两个不同实例的项目:开发环境,在那里你创建新功能,以及通常称为托管服务器上的生产环境的公共网站环境。此外,可能还有其他开发者的开发环境。你还可以有一个测试和预发布环境,以便在本地和类似公共网站的情况下测试项目。
为了良好的可维护性,你应该能够安装开发、测试、预发布和生产环境所需的 Python 模块。其中一些模块将是共享的,而另一些将是特定的。在这个配方中,我们将了解如何组织项目依赖并使用 pip 管理它们。
准备工作
在使用此配方之前,你需要安装 pip 并激活虚拟环境。有关如何操作的更多信息,请阅读使用虚拟环境配方。
如何操作...
依次执行以下步骤,为你的 Django 项目准备 pip 需求:
-
让我们去你的 Django 项目,该项目在版本控制下,并创建
requirements目录,包含以下文本文件:base.txt用于共享模块,dev.txt用于开发环境,test.txt用于测试环境,staging.txt用于预发布环境,以及prod.txt用于生产环境。 -
编辑
base.txt并逐行添加所有环境中共享的 Python 模块,例如:# base.txt Django==1.8 djangorestframework -e git://github.com/omab/python-social-auth.git@6b1e301c79#egg=python-social-auth -
如果特定环境的需要与
base.txt中的相同,请在该环境的需求文件中添加包含base.txt的行,例如:# prod.txt -r base.txt -
如果有特定环境的特定要求,请按以下方式添加:
# dev.txt -r base.txt django-debug-toolbar selenium -
现在,你可以运行以下命令来安装开发环境所需的所有依赖项(或适用于其他环境的类似命令),如下所示:
(myproject_env)$ pip install -r requirements/dev.txt
它是如何工作的...
上述命令从您的虚拟环境中的requirements/base.txt和requirements/dev.txt下载并安装所有项目依赖项。如您所见,您可以指定 Django 框架所需的模块版本,甚至可以直接从 Git 仓库中的python-social-auth的特定提交安装。在实践中,从特定提交安装很少有用,例如,只有当您的项目中包含具有特定功能且不再受最新版本支持的第三方依赖项时。
当您的项目中有很多依赖项时,坚持使用 Python 模块的特定版本是一个好习惯,这样您可以确保在部署项目或将其提供给新开发者时,完整性不会受损,并且所有模块都能正常工作,不会发生冲突。
如果您已经手动使用 pip 逐个安装了项目requirements,您可以使用以下命令生成requirements/base.txt文件:
(myproject_env)$ pip freeze > requirements/base.txt
更多内容...
如果您想保持简单,并且确信在所有环境中都将使用相同的依赖项,您可以使用一个名为requirements.txt的文件,按定义:
(myproject_env)$ pip freeze > requirements.txt
要在新环境中安装模块,只需调用以下命令:
(myproject_env)$ pip install -r requirements.txt
注意
如果您需要从其他版本控制系统或本地路径安装 Python 库,您可以从官方文档中了解更多关于 pip 的信息:pip-python3.readthedocs.org/en/latest/reference/pip_install.html。
参见
-
使用虚拟环境工作食谱
-
将外部依赖项包含到您的项目中食谱
-
配置开发、测试、预生产和生产环境设置食谱
使您的代码兼容 Python 2.7 和 Python 3
自 1.7 版本以来,Django 可以与 Python 2.7 和 Python 3 一起使用。在本食谱中,我们将探讨使您的代码兼容这两个 Python 版本的操作。
准备工作
在创建新的 Django 项目或升级旧项目时,请考虑遵循本食谱中给出的规则。
如何操作...
使您的代码兼容 Python 的两个版本包括以下步骤:
-
在每个模块的顶部添加
from __future__ import unicode_literals,然后使用通常的引号,无需u前缀来表示 Unicode 字符串,使用b前缀来表示字节字符串。 -
要确保一个值是字节字符串,请使用
django.utils.encoding.smart_bytes函数。要确保一个值是 Unicode,请使用django.utils.encoding.smart_text或django.utils.encoding.force_text函数。 -
对于您的模型,请使用
__str__方法而不是__unicode__方法,并添加python_2_unicode_compatible装饰器,如下所示:# models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import \ python_2_unicode_compatible @python_2_unicode_compatible class NewsArticle(models.Model): title = models.CharField(_("Title"), max_length=200) content = models.TextField(_("Content")) def __str__(self): return self.title class Meta: verbose_name = _("News Article") verbose_name_plural = _("News Articles") -
要遍历字典,请使用
django.utils.six中的iteritems()、iterkeys()和itervalues()。以下是一个示例:from django.utils.six import iteritems d = {"imported": 25, "skipped": 12, "deleted": 3} for k, v in iteritems(d): print("{0}: {1}".format(k, v)) -
当你捕获异常时,请使用
as关键字,如下所示:try: article = NewsArticle.objects.get(slug="hello-world") except NewsArticle.DoesNotExist as exc: pass except NewsArticle.MultipleObjectsReturned as exc: pass -
要检查值的类型,请使用
django.utils.six,如下所示:from django.utils import six isinstance(val, six.string_types) # previously basestring isinstance(val, six.text_type) # previously unicode isinstance(val, bytes) # previously str isinstance(val, six.integer_types) # previously (int, long) -
不要使用
xrange,而应使用django.utils.six.moves中的range,如下所示:from django.utils.six.moves import range for i in range(1, 11): print(i) -
要检查当前版本是 Python 2 还是 Python 3,你可以使用以下条件:
from django.utils import six if six.PY2: print("This is Python 2") if six.PY3: print("This is Python 3")
工作原理…
Django 项目中的所有字符串都应被视为 Unicode 字符串。通常,只有HttpRequest的输入和HttpResponse的输出是以 UTF-8 编码的字节串。
Python 3 中的许多函数和方法现在返回迭代器而不是列表,这使得语言更加高效。为了使代码与两个 Python 版本兼容,你可以使用 Django 中捆绑的 six 库。
在官方 Django 文档中了解更多关于编写兼容代码的信息。docs.djangoproject.com/en/1.8/topics/python3/
小贴士
下载示例代码
你可以从你购买 Packt 书籍的账户中下载所有示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便直接将文件通过电子邮件发送给你。
在你的项目中包含外部依赖项
有时,将外部依赖项包含在你的项目中会更好。这确保了每当第三方模块升级时,所有其他开发者都会在版本控制系统(Git、Subversion 或其他)的下一个更新中收到升级版本。
此外,当从非官方来源(即除了Python Package Index(PyPI)或不同的版本控制系统之外)获取库时,最好将外部依赖项包含在你的项目中。
准备工作
以包含 Django 项目的虚拟环境开始。
如何操作…
依次执行以下步骤:
-
如果你还没有这样做,请在你的 Django 项目
django-myproject目录下创建一个外部目录。然后,在该目录下创建libs和apps目录。libs目录用于你的项目所需的 Python 模块,例如 boto、Requests、Twython、Whoosh 等。apps目录用于第三方 Django 应用,例如 django-cms、django-haystack、django-storages 等。小贴士
我强烈建议你在
libs和apps目录中创建README.txt文件,其中说明每个模块的用途、使用的版本或修订号以及来源。 -
目录结构应类似于以下内容:
![如何操作…]()
-
下一步是将外部库和应用程序添加到 Python 路径中,以便它们被识别为已安装。这可以通过在设置中添加以下代码来完成:
# settings.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import os import sys BASE_DIR = os.path.abspath(os.path.join( os.path.dirname(__file__), ".." )) EXTERNAL_LIBS_PATH = os.path.join( BASE_DIR, "externals", "libs" ) EXTERNAL_APPS_PATH = os.path.join( BASE_DIR, "externals", "apps" ) sys.path = ["", EXTERNAL_LIBS_PATH, EXTERNAL_APPS_PATH] + \ sys.path
它是如何工作的...
如果你可以运行 Python 并导入该模块,则模块应该位于 Python 路径下。将模块添加到 Python 路径的一种方法是在导入位于非常规位置的模块之前修改sys.path变量。sys.path的值是一个以空字符串开始的目录列表,表示当前目录,然后是虚拟环境中的目录,最后是 Python 安装的全局共享目录。你可以在 Python shell 中查看sys.path的值,如下所示:
(myproject_env)$ python
>>> import sys
>>> sys.path
当尝试导入一个模块时,Python 会在这个列表中搜索模块,并返回找到的第一个结果。
因此,我们首先定义BASE_DIR变量,它是settings.py文件上一级的绝对路径。然后,我们定义EXTERNAL_LIBS_PATH和EXTERNAL_APPS_PATH变量,它们相对于BASE_DIR。最后,我们修改sys.path属性,将新路径添加到列表的开头。请注意,我们还添加了一个空字符串作为第一个搜索路径,这意味着在检查其他 Python 路径之前,应该始终检查任何模块的当前目录。
小贴士
这种包含外部库的方式在具有 C 语言绑定的 Python 包(例如lxml)之间不跨平台工作。对于此类依赖项,我建议使用在使用 pip 处理项目依赖项配方中引入的 pip 需求。
参见
-
创建项目文件结构的配方
-
使用 pip 处理项目依赖项的配方
-
在设置中定义相对路径的配方
-
在第十章铃声和哨子中的使用 Django shell配方,第十章
配置开发、测试、预发布和生产环境的设置
如前所述,你将在开发环境中创建新功能,然后在测试环境中测试它们,接着将网站部署到预发布服务器,让其他人尝试新功能,最后,网站将被部署到生产服务器以供公众访问。每个环境都可以有特定的设置,你将在这个配方中看到如何组织它们。
准备工作
在 Django 项目中,我们将为每个环境创建设置:开发、测试、预发布和生产。
如何做到这一点...
按照以下步骤配置项目设置:
-
在
myproject目录中,创建一个confPython 模块,包含以下文件:__init__.py,base.py用于共享设置,dev.py用于开发设置,test.py用于测试设置,staging.py用于预发布设置,以及prod.py用于生产设置。 -
将所有共享设置放在
conf/base.py中。 -
如果环境的设置与共享设置相同,则只需从
base.py中导入所有内容,如下所示:# myproject/conf/prod.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from .base import * -
在其他文件中应用您想要附加或覆盖的特定环境的设置,例如,开发环境设置应放在
dev.py中,如下所示:# myproject/conf/dev.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from .base import * EMAIL_BACKEND = \ "django.core.mail.backends.console.EmailBackend" -
在
myproject/settings.py的开头,从环境设置之一导入配置,然后附加特定的或敏感的配置,例如DATABASES或API密钥,这些不应置于版本控制之下,如下所示:# myproject/settings.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from .conf.dev import * DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "myproject", "USER": "root", "PASSWORD": "root", } } -
创建一个
settings.py.sample文件,该文件应包含项目运行所需的所有敏感设置,但设置为空值。
*它的工作原理…
默认情况下,Django 管理命令使用myproject/settings.py中的设置。使用本食谱中定义的方法,我们可以将所有环境所需的非敏感设置都放在 conf 目录下进行版本控制。而settings.py文件本身将被版本控制忽略,并且只会包含当前开发、测试、预发布或生产环境所需的设置。
参见
-
创建和包含本地设置的食谱
-
在设置中定义相对路径的食谱
-
设置 Subversion 忽略属性的食谱
-
创建 Git 忽略文件的食谱
在设置中定义相对路径
Django 要求你在设置中定义不同的文件路径,例如媒体根目录、静态文件根目录、模板路径、翻译文件路径等。对于你的项目的每个开发者,路径可能不同,因为虚拟环境可以设置在任何地方,用户可能在 Mac OS X、Linux 或 Windows 上工作。无论如何,有一种方法可以定义这些相对于 Django 项目目录的路径。
准备工作
首先,打开settings.py。
如何操作…
根据需要修改你的路径相关设置,而不是将本地目录的路径硬编码,如下所示:
# settings.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import os
BASE_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")
)
MEDIA_ROOT = os.path.join(BASE_DIR, "myproject", "media")
STATIC_ROOT = os.path.join(BASE_DIR, "myproject", "static")
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "myproject", "site_static"),
)
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, "myproject", "templates"),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, "locale"),
)
FILE_UPLOAD_TEMP_DIR = os.path.join(
BASE_DIR, "myproject", "tmp"
)
*它的工作原理…
首先,我们定义BASE_DIR,它是相对于settings.py文件的一个更高层的绝对路径。然后,我们使用os.path.join函数将所有路径设置为相对于BASE_DIR。
参见
- 在项目中包含外部依赖的食谱
创建和包含本地设置
配置不一定是复杂的。如果你想保持简单,你可以使用两个设置文件:settings.py用于通用配置,local_settings.py用于不应置于版本控制下的敏感设置。
准备工作
不同环境的设置的大部分将共享并保存在版本控制中。然而,将会有一些设置是特定于项目实例的环境的,例如数据库或电子邮件设置。我们将它们放在local_settings.py文件中。
如何操作…
在您的项目中使用本地设置,请执行以下步骤:
-
在
settings.py的末尾添加一个local_settings.py的版本,它声称位于同一目录中,如下所示:# settings.py # … put this at the end of the file … try: execfile(os.path.join( os.path.dirname(__file__), "local_settings.py" )) except IOError: pass -
创建
local_settings.py并将您的环境特定设置放在那里,如下所示:# local_settings.py DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "myproject", "USER": "root", "PASSWORD": "root", } } EMAIL_BACKEND = \ "django.core.mail.backends.console.EmailBackend" INSTALLED_APPS += ( "debug_toolbar", )
它是如何工作的...
如您所见,本地设置通常不是导入的,而是包含并执行在settings.py文件本身中。这允许您不仅创建或覆盖现有设置,还可以调整settings.py文件中的元组或列表。例如,我们在这里添加debug_toolbar到INSTALLED_APPS中,以便能够调试 SQL 查询、模板上下文变量等。
参见
-
创建项目文件结构配方
-
在第十章铃声和哨子中的切换调试工具栏配方,铃声和哨子
为 Subversion 用户动态设置 STATIC_URL
如果您将STATIC_URL设置为静态值,那么每次您更新 CSS 文件、JavaScript 文件或图像时,您都需要清除浏览器缓存才能看到更改。有一个绕过清除浏览器缓存的方法。那就是在STATIC_URL中显示版本控制系统的修订号。每当代码更新时,访客的浏览器将强制加载所有全新的静态文件。
这个配方展示了如何为 Subversion 用户在STATIC_URL中放入修订号。
准备工作
确保您的项目处于 Subversion 版本控制之下,并在您的设置中定义了BASE_DIR,如在设置中定义相对路径配方中所示。
然后,在您的 Django 项目中创建utils模块,并在其中创建一个名为misc.py的文件。
如何做到这一点...
将修订号放入STATIC_URL设置的程序包括以下两个步骤:
-
插入以下内容:
# utils/misc.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import subprocess def get_media_svn_revision(absolute_path): repo_dir = absolute_path svn_revision = subprocess.Popen( 'svn info | grep "Revision" | awk \'{print $2}\'', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=repo_dir, universal_newlines=True) rev = svn_revision.communicate()[0].partition('\n')[0] return rev -
然后,修改
settings.py文件并添加以下行:# settings.py # … somewhere after BASE_DIR definition … from utils.misc import get_media_svn_revision STATIC_URL = "/static/%s/" % get_media_svn_revision(BASE_DIR)
它是如何工作的...
get_media_svn_revision()函数接受absolute_path目录作为参数,并在该目录中调用svn info shell 命令以找出当前修订号。我们向函数传递BASE_DIR,因为我们确信它处于版本控制之下。然后,修订号被解析、返回并包含在STATIC_URL定义中。
参见
-
为 Git 用户动态设置 STATIC_URL 的配方
-
设置 Subversion 忽略属性配方
为 Git 用户动态设置 STATIC_URL
如果您不希望在每次更改 CSS 和 JavaScript 文件或调整图像样式时刷新浏览器缓存,您需要使用具有可变路径组件的动态方式设置STATIC_URL。使用动态变化的 URL,每当代码更新时,访客的浏览器将强制加载所有全新的未缓存静态文件。在这个配方中,当您使用 Git 版本控制系统时,我们将为STATIC_URL设置一个动态路径。
准备工作
确保您的项目处于 Git 版本控制之下,并且在您的设置中已定义 BASE_DIR,如 在设置中定义相对路径 的配方中所示。
如果您还没有这样做,请在您的 Django 项目中创建 utils 模块。同样,在那里创建一个 misc.py 文件。
如何操作...
将 Git 时间戳放入 STATIC_URL 设置的步骤包括以下两个步骤:
-
将以下内容添加到位于
utils/目录下的misc.py文件中:# utils/misc.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import subprocess from datetime import datetime def get_git_changeset(absolute_path): repo_dir = absolute_path git_show = subprocess.Popen( 'git show --pretty=format:%ct --quiet HEAD', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=repo_dir, universal_newlines=True, ) timestamp = git_show.communicate()[0].partition('\n')[0] try: timestamp = \ datetime.utcfromtimestamp(int(timestamp)) except ValueError: return "" changeset = timestamp.strftime('%Y%m%d%H%M%S') return changeset -
然后,在设置中导入新创建的
get_git_changeset()函数,并用于STATIC_URL路径,如下所示:# settings.py # … somewhere after BASE_DIR definition … from utils.misc import get_git_changeset STATIC_URL = "/static/%s/" % get_git_changeset(BASE_DIR)
它是如何工作的...
get_git_changeset() 函数接受 absolute_path 目录作为参数,并使用参数调用 git show 命令以显示目录中 HEAD 修订版本的 Unix 时间戳。如前一个配方中所述,我们向函数传递 BASE_DIR,因为我们确信它处于版本控制之下。时间戳被解析;转换为包含年、月、日、小时、分钟和秒的字符串;返回;并包含在 STATIC_URL 的定义中。
参见
-
为 Subversion 用户动态设置 STATIC_URL 配方
-
创建 Git 忽略文件 配方
将 UTF-8 设置为 MySQL 配置的默认编码
MySQL 是最受欢迎的开源数据库。在这个配方中,我将告诉您如何将其设置为默认编码。请注意,如果您不在数据库配置中设置此编码,您可能会遇到默认使用 LATIN1 编码的 UTF-8 编码数据的情况。这会导致使用符号如 € 时出现数据库错误。此外,这个配方将帮助您避免将数据库数据从 LATIN1 转换为 UTF-8 的困难,尤其是当您有一些表使用 LATIN1 编码,而其他表使用 UTF-8 编码时。
准备工作
确保已安装 MySQL 数据库管理系统和 MySQLdb Python 模块,并且您在项目设置中使用 MySQL 引擎。
如何操作...
在您最喜欢的编辑器中打开 /etc/mysql/my.cnf MySQL 配置文件,并确保以下设置在 [client]、[mysql] 和 [mysqld] 部分中设置,如下所示:
# /etc/mysql/my.cnf
[client]
default-character-set = utf8
[mysql]
default-character-set = utf8
[mysqld]
collation-server = utf8_unicode_ci
init-connect = 'SET NAMES utf8'
character-set-server = utf8
如果任何部分不存在,请在文件中创建它们。然后,在您的命令行工具中重新启动 MySQL,如下所示:
$ /etc/init.d/mysql restart
它是如何工作的...
现在,每次您创建新的 MySQL 数据库时,数据库及其所有表都将默认设置为 UTF-8 编码。
不要忘记在所有开发或发布项目的计算机上设置此选项。
设置 Subversion 忽略属性
如果您使用 Subversion 进行版本控制,您将需要将大多数项目保留在存储库中;然而,一些文件和目录应仅保留在本地,而不被跟踪。
准备工作
确保您的 Django 项目处于 Subversion 版本控制之下。
如何操作...
打开你的命令行工具并将默认编辑器设置为 nano、vi、vim 或你喜欢的任何其他编辑器,如下所示:
$ export EDITOR=nano
小贴士
如果你没有偏好,我推荐使用 nano,这是一个非常直观且简单的终端文本编辑器。
然后,转到你的项目目录并输入以下命令:
$ svn propedit svn:ignore myproject
这将在编辑器中打开一个临时文件,你需要在这里输入以下文件和目录模式,以便 Subversion 忽略:
# Project files and directories
local_settings.py
static
media
tmp
# Byte-compiled / optimized / DLL files
__pycache__
*.py[cod]
*$py.class
# C extensions
*.so
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov
.tox
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
# Translations
*.pot
# Django stuff:
*.log
# PyBuilder
target
保存文件并退出编辑器。对于你项目中的每个其他 Python 包,你也需要忽略几个文件和目录。只需进入一个目录并输入以下命令:
$ svn propedit svn:ignore .
然后,将以下内容放入临时文件中,保存并关闭编辑器,如下所示:
# Byte-compiled / optimized / DLL files
__pycache__
*.py[cod]
*$py.class
# C extensions
*.so
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov
.tox
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
# Translations
*.pot
# Django stuff:
*.log
# PyBuilder
target
工作原理…
在 Subversion 中,你需要为你的项目中的每个目录定义忽略属性。主要来说,我们不希望跟踪 Python 编译文件,例如 *.pyc。我们也不想忽略特定于每个环境的 local_settings.py,static 目录,它复制了来自不同应用的收集静态文件,media 目录,它包含上传的文件和与数据库一起更改的内容,以及 tmp 目录,它是临时用于文件上传的。
小贴士
如果你将所有设置都保存在一个 conf Python 包中,如 配置开发、测试、预生产和生产环境设置 菜谱中所述,请也将 settings.py 添加到忽略文件中。
相关内容
-
创建和包含本地设置 菜谱
-
创建 Git 忽略文件 菜谱
创建 Git 忽略文件
如果你使用的是 Git——最受欢迎的分布式版本控制系统——忽略一些文件和文件夹比使用 Subversion 更容易。
准备工作
确保你的 Django 项目处于 Git 版本控制之下。
如何操作…
使用你喜欢的文本编辑器,在你的 Django 项目根目录下创建一个 .gitignore 文件,并将以下文件和目录放入其中,如下所示:
# .gitignore
# Project files and directories
/myproject/local_settings.py
/myproject/static/
/myproject/tmp/
/myproject/media/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
# Translations
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
工作原理…
.gitignore 文件指定了应该由 Git 版本控制系统有意不跟踪的路径。我们在这个菜谱中创建的 .gitignore 文件将忽略 Python 编译文件、本地设置、收集的静态文件、上传的临时目录以及包含上传文件的媒体目录。
小贴士
如果你将所有设置都保存在一个 conf Python 包中,如 配置开发、测试、预生产和生产环境设置 菜谱中所述,请也将 settings.py 添加到忽略文件中。
相关内容
- 设置 Subversion 忽略属性 菜谱
删除 Python 编译文件
当你第一次运行你的项目时,Python 会将所有的 *.py 代码编译成字节码文件,即 *.pyc,这些文件随后用于执行。
通常,当你更改 *.py 文件时,*.pyc 会重新编译;然而,有时在切换分支或移动目录时,你需要手动清理编译文件。
准备工作
使用您喜欢的编辑器,并在您的家目录中编辑或创建一个 .bash_profile 文件。
如何做…
将此别名添加到 .bash_profile 的末尾,如下所示:
# ~/.bash_profile
alias delpyc="find . -name \"*.pyc\" -delete"
现在,为了清理 Python 编译文件,请转到您的项目目录,并在命令行中输入以下命令:
$ delpyc
它是如何工作的…
首先,我们创建一个 Unix 别名,用于搜索当前目录及其子目录中的 *.pyc 文件并将它们删除。当你在命令行工具中启动新会话时,会执行 .bash_profile 文件。
参见
-
设置 Subversion 忽略属性 的配方
-
创建 Git 忽略文件 的配方
尊重 Python 文件中的导入顺序
当你创建 Python 模块时,保持与文件结构的一致性是一种良好的做法。这使得其他开发者和你自己更容易阅读代码。本配方将向您展示如何结构化您的导入。
准备工作
创建一个虚拟环境和其中的 Django 项目。
如何做…
在您创建的 Python 文件中使用以下结构。在定义 UTF-8 为默认 Python 文件编码的第一行之后,将按类别分节的导入放在那里,如下所示:
# -*- coding: UTF-8 -*-
# System libraries
from __future__ import unicode_literals
import os
import re
from datetime import datetime
# Third-party libraries
import boto
from PIL import Image
# Django modules
from django.db import models
from django.conf import settings
# Django apps
from cms.models import Page
# Current-app modules
from . import app_settings
它是如何工作的…
我们有五个主要类别用于导入,如下所示:
-
Python 默认安装中的包的系统库
-
为额外安装的 Python 包提供的第三方库
-
Django 模块用于 Django 框架的不同模块
-
Django 应用程序用于第三方和本地应用程序
-
当前-app 模块用于当前应用的相对导入
更多内容…
在 Python 和 Django 编码时,请使用 Python 代码的官方风格指南 PEP 8。您可以在 www.python.org/dev/peps/pep-0008/ 找到它。
参见
-
使用 pip 处理项目依赖项 的配方
-
在项目中包含外部依赖项 的配方
创建应用配置
当使用 Django 开发网站时,你为项目本身创建一个模块,然后创建多个名为应用程序或 apps 的 Python 模块,这些模块结合了不同的模块功能,通常包括模型、视图、表单、URL 配置、管理命令、迁移、信号、测试等。Django 框架有一个应用程序注册表,其中收集了所有应用程序和模型,稍后用于配置和内省。自 Django 1.7 以来,应用程序的元信息可以保存在每个使用的 AppConfig 实例中。让我们创建一个示例 magazine 应用程序,看看如何使用那里的应用程序配置。
准备工作
您可以手动创建 Django 应用程序或使用以下命令在虚拟环境中创建(在 使用虚拟环境 配方中学习如何使用虚拟环境),如下所示:
(myproject_env)$ django-admin.py startapp magazine
在 models.py 中添加一些 NewsArticle 模型,创建 admin.py 中的模型管理,并在设置中的 INSTALLED_APPS 中放入 "magazine"。如果你还不熟悉这些任务,请学习官方 Django 教程,链接为 docs.djangoproject.com/en/1.8/intro/tutorial01/。
如何做到这一点...
按照以下步骤创建和使用应用配置:
-
首先,创建
apps.py文件,并将以下内容放入其中,如下所示:# magazine/apps.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.apps import AppConfig from django.utils.translation import ugettext_lazy as _ class MagazineAppConfig(AppConfig): name = "magazine" verbose_name = _("Magazine") def ready(self): from . import signals -
然后,编辑应用中的
__init__.py文件,并放入以下内容:# magazine/__init__.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals default_app_config = "magazine.apps.MagazineAppConfig" -
最后,让我们创建一个
signals.py文件,并在其中添加一些信号处理器:# magazine/signals.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.conf import settings from .models import NewsArticle @receiver(post_save, sender=NewsArticle) def news_save_handler(sender, **kwargs): if settings.DEBUG: print("%s saved." % kwargs['instance']) @receiver(post_delete, sender=NewsArticle) def news_delete_handler(sender, **kwargs): if settings.DEBUG: print("%s deleted." % kwargs['instance'])
它是如何工作的...
当你运行 HTTP 服务器或调用管理命令时,会调用 django.setup()。它会加载设置,设置日志,并初始化应用注册表。应用注册表初始化分为三个步骤,如下所示:
-
Django 从设置中的
INSTALLED_APPS为每个项目项导入配置。这些项可以指向应用名或直接指向配置,例如"magazine"或"magazine.apps.NewsAppConfig"。 -
Django 尝试从
INSTALLED_APPS中的每个应用导入models.py并收集所有模型。 -
最后,Django 为每个应用配置运行
ready()方法。如果你有任何信号处理器,这个方法是一个正确的地方来注册它们。ready()方法是可选的。 -
在我们的例子中,
MagazineAppConfig类设置了magazine应用的配置。name参数定义了当前应用的名字。verbose_name参数在 Django 模型管理中被使用,其中模型根据应用进行展示和分组。ready()方法导入并激活了信号处理器,当处于 DEBUG 模式时,会在终端打印出NewsArticle被保存或删除的信息。
还有更多...
在调用 django.setup() 之后,你可以按照以下方式从注册表中加载应用配置和模型:
>>> from django.apps import apps as django_apps
>>> magazine_app_config = django_apps.get_app_config("magazine")
>>> magazine_app_config
<MagazineAppConfig: magazine>
>>> magazine_app_config.models_module
<module 'magazine.models' from 'magazine/models.pyc'>
NewsArticle = django_apps.get_model("magazine", "NewsArticle")
你可以在官方 Django 文档中了解更多关于应用配置的信息,链接为 docs.djangoproject.com/en/1.8/ref/applications/
参见
-
使用虚拟环境工作 的配方
-
定义可覆盖的应用设置 的配方
-
第六章, 模型管理
定义可覆盖的应用设置
本配方将向你展示如何为你的应用定义设置,然后在项目中的 settings.py 或 local_settings.py 文件中覆盖这些设置。这对于可重用应用特别有用。
准备工作
你可以手动创建 Django 应用,或者使用以下命令:
(myproject_env)$ django-admin.py startapp myapp1
如何做到这一点...
如果你只有一个或两个设置,你可以在你的 models.py 文件中使用以下模式。如果设置很多,并且你希望它们组织得更好,可以在应用中创建一个 app_settings.py 文件,并按照以下方式放置设置:
# models.py or app_settings.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
SETTING1 = getattr(settings, "MYAPP1_SETTING1", u"default value")
MEANING_OF_LIFE = getattr(settings, "MYAPP1_MEANING_OF_LIFE", 42)
STATUS_CHOICES = getattr(settings, "MYAPP1_STATUS_CHOICES", (
("draft", _("Draft")),
("published", _("Published")),
("not_listed", _("Not Listed")),
))
然后,你可以在 models.py 中使用应用设置,如下所示:
# models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .app_settings import STATUS_CHOICES
class NewsArticle(models.Model):
# …
status = models.CharField(_("Status"),
max_length=20, choices=STATUS_CHOICES
)
如果你只想为单个项目覆盖STATUS_CHOICES设置,你只需打开settings.py文件并添加以下内容:
# settings.py
# …
from django.utils.translation import ugettext_lazy as _
MYAPP1_STATUS_CHOICES = (
("imported", _("Imported")),
("draft", _("Draft")),
("published", _("Published")),
("not_listed", _("Not Listed")),
("expired", _("Expired")),
)
它是如何工作的...
Python 函数getattr(object, attribute_name[, default_value])试图从对象中获取attribute_name属性,如果未找到则返回default_value。在这种情况下,会尝试不同的设置,以便从 Django 项目设置模块中获取,如果未找到,则分配默认值。
第二章:数据库结构
本章将涵盖以下主题:
-
使用模型混入
-
创建一个包含与 URL 相关方法的模型混入
-
创建一个用于处理创建和修改日期的模型混入
-
创建一个用于处理元标签的模型混入
-
创建一个用于处理通用关系的模型混入
-
处理多语言字段
-
使用迁移
-
从 South 迁移切换到 Django 迁移
-
将外键更改为多对多字段
简介
当你启动一个新应用时,首先要做的是创建代表你的数据库结构的模型。我们假设你之前已经创建了 Django 应用,或者至少你已经阅读并理解了官方的 Django 教程。在本章中,我们将看到一些使你的项目中的不同应用保持数据库结构一致性的有趣技术。然后,我们将看到如何创建自定义模型字段,以便在数据库中处理数据的国际化。本章结束时,我们将看到如何使用迁移在开发过程中更改你的数据库结构。
使用模型混入
在面向对象的语言,如 Python 中,混入类可以被视为一个具有实现功能的接口。当一个模型扩展混入时,它实现了该接口,并包括所有其字段、属性和方法。在 Django 模型中,当你想要在不同模型中多次重用通用功能时,可以使用混入。
准备工作
首先,你需要创建可重用的混入。本章后面将给出一些典型的混入示例。一个存放模型混入的好地方是在utils模块中。
小贴士
如果你创建了一个将与他人共享的可重用应用,请将模型混入放在可重用应用中,例如在base.py文件中。
如何操作...
打开你想要使用混入的任何 Django 应用的models.py文件,并输入以下代码:
# demo_app/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from utils.models import UrlMixin
from utils.models import CreationModificationMixin
from utils.models import MetaTagsMixin
@python_2_unicode_compatible
class Idea(UrlMixin, CreationModificationMixin, MetaTagsMixin):
title = models.CharField(_("Title"), max_length=200)
content = models.TextField(_("Content"))
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
它是如何工作的...
Django 模型继承支持三种继承类型:抽象基类、多表继承和代理模型。模型混入是具有指定字段、属性和方法的抽象模型类。当你创建一个如Idea这样的模型时,如前例所示,它会继承来自UrlMixin、CreationModificationMixin和MetaTagsMixin的所有特性。所有抽象类的字段都保存在与扩展模型相同的数据库表中。在接下来的食谱中,你将学习如何定义你的模型混入。
注意,我们为Idea模型使用了@python_2_unicode_compatible装饰器。你可能还记得在第一章中“使你的代码兼容 Python 2.7 和 Python 3”食谱中的内容,它的目的是使__str__()方法与 Unicode 兼容,适用于以下两个 Python 版本:2.7 和 3。
更多内容...
要了解不同类型的模型继承类型,请参考在docs.djangoproject.com/en/1.8/topics/db/models/#model-inheritance提供的官方 Django 文档。
参见
-
在 第一章 的 使代码兼容 Python 2.7 和 Python 3 菜谱中,开始使用 Django 1.8
-
创建一个具有 URL 相关方法的模型混入 菜谱
-
创建一个处理创建和修改日期的模型混入 菜谱
-
创建一个处理元标签的模型混入 菜谱
创建一个具有 URL 相关方法的模型混入
对于每个有自己的页面的模型,定义 get_absolute_url() 方法是一个好的实践。这个方法可以在模板中使用,也可以在 Django 管理站点中预览保存的对象。然而,get_absolute_url() 是模糊的,因为它返回的是 URL 路径而不是完整的 URL。在这个菜谱中,我们将看到如何创建一个模型混入,允许你默认定义 URL 路径或完整 URL,并自动生成另一个,并处理正在设置的 get_absolute_url() 方法。
准备工作
如果你还没有这样做,创建 utils 包以保存你的混入。然后,在 utils 包中创建 models.py 文件(或者,如果你创建了一个可重用的应用,将混入放在你的应用中的 base.py 文件中)。
如何操作...
依次执行以下步骤:
-
将以下内容添加到你的
utils包的models.py文件中:# utils/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import urlparse from django.db import models from django.contrib.sites.models import Site from django.conf import settings class UrlMixin(models.Model): """ A replacement for get_absolute_url() Models extending this mixin should have either get_url or get_url_path implemented. """ class Meta: abstract = True def get_url(self): if hasattr(self.get_url_path, "dont_recurse"): raise NotImplementedError try: path = self.get_url_path() except NotImplementedError: raise website_url = getattr( settings, "DEFAULT_WEBSITE_URL", "http://127.0.0.1:8000" ) return website_url + path get_url.dont_recurse = True def get_url_path(self): if hasattr(self.get_url, "dont_recurse"): raise NotImplementedError try: url = self.get_url() except NotImplementedError: raise bits = urlparse.urlparse(url) return urlparse.urlunparse(("", "") + bits[2:]) get_url_path.dont_recurse = True def get_absolute_url(self): return self.get_url_path() -
要在你的应用中使用混入,从
utils包中导入它,在你的模型类中继承混入,并定义get_url_path()方法如下:# demo_app/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.utils.encoding import \ python_2_unicode_compatible from utils.models import UrlMixin @python_2_unicode_compatible class Idea(UrlMixin): title = models.CharField(_("Title"), max_length=200) # … get_url_path(self): return reverse("idea_details", kwargs={ "idea_id": str(self.pk), }) -
如果你在这个代码的预发布或生产环境中进行检查,或者运行一个与默认 IP 或端口不同的本地服务器,请在你的本地设置中设置
DEFAULT_WEBSITE_URL(不带尾随斜杠),如下所示:# settings.py # … DEFAULT_WEBSITE_URL = "http://www.example.com"
它是如何工作的…
UrlMixin 类是一个具有三个方法:get_url()、get_url_path() 和 get_absolute_url() 的抽象模型。期望在扩展模型类(例如,Idea)中覆盖 get_url() 或 get_url_path() 方法。你可以定义 get_url(),这是对象的完整 URL,然后 get_url_path() 将将其剥离为路径。你也可以定义 get_url_path(),这是对象的绝对路径,然后 get_url() 将在路径的开头添加网站 URL。get_absolute_url() 方法将模仿 get_url_path() 方法。
小贴士
常规做法是始终覆盖 get_url_path() 方法。
在模板中,当你需要同一网站中对象的链接时,使用 <a href="{{ idea.get_url_path }}">{{ idea.title }}</a>。对于电子邮件、RSS 源或 API 中的链接,使用 <a href="{{ idea.get_url }}">{{ idea.title }}</a>。
默认的get_absolute_url()方法将在 Django 模型管理中用于网站视图功能,也可能被一些第三方 Django 应用程序使用。
相关内容
-
使用模型混入器配方
-
创建一个处理创建和修改日期的模型混入器配方
-
创建一个处理元标签的模型混入器配方
-
创建一个处理通用关系的模型混入器配方
创建一个处理创建和修改日期的模型混入器
在模型实例的创建和修改中包含时间戳是一种常见的行为。在这个配方中,我们将看到如何创建一个简单的模型混入器,用于保存模型的创建和修改日期和时间。使用这样的混入器将确保所有模型使用相同的字段名来存储时间戳,并且具有相同的行为。
准备工作
如果你还没有这样做,创建utils包以保存你的混入器。然后,在utils包中创建models.py文件。
如何做到这一点...
打开你的utils包中的models.py文件,并在其中插入以下内容:
# utils/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import now as timezone_now
class CreationModificationDateMixin(models.Model):
"""
Abstract base class with a creation and modification
date and time
"""
created = models.DateTimeField(
_("creation date and time"),
editable=False,
)
modified = models.DateTimeField(
_("modification date and time"),
null=True,
editable=False,
)
def save(self, *args, **kwargs):
if not self.pk:
self.created = timezone_now()
else:
# To ensure that we have a creation data always,
# we add this one
if not self.created:
self.created = timezone_now()
self.modified = timezone_now()
super(CreationModificationDateMixin, self).\
save(*args, **kwargs)
save.alters_data = True
class Meta:
abstract = True
它是如何工作的...
CreationModificationDateMixin类是一个抽象模型,这意味着扩展模型类将在同一个数据库表中创建所有字段,也就是说,不会有导致表难以处理的一对一关系。这个混入器有两个日期时间字段和一个在保存扩展模型时将被调用的save()方法。save()方法检查模型是否有主键,这是新未保存实例的情况。在这种情况下,它将创建日期设置为当前日期和时间。如果存在主键,则将修改日期设置为当前日期和时间。
作为替代,除了save()方法之外,你还可以为创建和修改字段使用auto_now_add和auto_now属性,这将自动添加创建和修改时间戳。
相关内容
-
使用模型混入器配方
-
创建一个处理元标签的模型混入器配方
-
创建一个处理通用关系的模型混入器配方
创建一个处理元标签的模型混入器
如果你想要优化你的网站以适应搜索引擎,你不仅需要为每个页面设置语义标记,还需要设置适当的元标签。为了获得最大的灵活性,你需要有一种方法来为每个对象定义特定的元标签,每个对象在你的网站上都有自己的页面。在这个配方中,我们将看到如何创建与元标签相关的字段和方法模型混入器。
准备工作
如前几个配方所示,确保你有用于混入器的utils包。在你的首选编辑器中打开此包的models.py文件。
如何做到这一点...
将以下内容放入models.py文件中:
# utils/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import escape
from django.utils.safestring import mark_safe
class MetaTagsMixin(models.Model):
"""
Abstract base class for meta tags in the <head> section
"""
meta_keywords = models.CharField(
_("Keywords"),
max_length=255,
blank=True,
help_text=_("Separate keywords by comma."),
)
meta_description = models.CharField(
_("Description"),
max_length=255,
blank=True,
)
meta_author = models.CharField(
_("Author"),
max_length=255,
blank=True,
)
meta_copyright = models.CharField(
_("Copyright"),
max_length=255,
blank=True,
)
class Meta:
abstract = True
def get_meta_keywords(self):
tag = ""
if self.meta_keywords:
tag = '<meta name="keywords" content="%s" />\n' %\
escape(self.meta_keywords)
return mark_safe(tag)
def get_meta_description(self):
tag = ""
if self.meta_description:
tag = '<meta name="description" content="%s" />\n' %\
escape(self.meta_description)
return mark_safe(tag)
def get_meta_author(self):
tag = ""
if self.meta_author:
tag = '<meta name="author" content="%s" />\n' %\
escape(self.meta_author)
return mark_safe(tag)
def get_meta_copyright(self):
tag = ""
if self.meta_copyright:
tag = '<meta name="copyright" content="%s" />\n' %\
escape(self.meta_copyright)
return mark_safe(tag)
def get_meta_tags(self):
return mark_safe("".join((
self.get_meta_keywords(),
self.get_meta_description(),
self.get_meta_author(),
self.get_meta_copyright(),
)))
它是如何工作的...
此混入为其扩展的模型添加了四个字段:meta_keywords、meta_description、meta_author和meta_copyright。还添加了在 HTML 中渲染元标签的方法。
如果您在如Idea这样的模型中使用此混入,该模型在本章的第一个菜谱中展示,那么您可以在您的详情页模板的HEAD部分放入以下内容以渲染所有元标签:
{{ idea.get_meta_tags }}
您还可以使用以下行来渲染特定的元标签:
{{ idea.get_meta_description }}
如您从代码片段中注意到的,渲染的元标签被标记为安全,即它们没有被转义,我们不需要使用安全模板过滤器。只有来自数据库的值被转义,以确保最终的 HTML 格式正确。
参见
-
使用模型混入 菜谱
-
创建一个处理创建和修改日期的模型混入 菜谱
-
创建一个处理通用关系的模型混入 菜谱
创建一个处理通用关系的模型混入
除了外键关系或多对多关系等常规数据库关系之外,Django 还有一个将模型与任何其他模型的实例相关联的机制。这个概念被称为通用关系。对于每个通用关系,都有一个保存的相关模型的类型以及该模型实例的 ID。
在这个菜谱中,我们将看到如何在模型混入中泛化通用关系的创建。
准备工作
为了使这个菜谱工作,您需要安装contenttypes应用。默认情况下,它应该在INSTALLED_APPS目录中,如下所示:
# settings.py
INSTALLED_APPS = (
# …
"django.contrib.contenttypes",
)
再次确保您已经为您模型混入创建了utils包。
如何做到...
-
在文本编辑器中打开
utils包中的models.py文件,并插入以下内容:# utils/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic from django.core.exceptions import FieldError def object_relation_mixin_factory( prefix=None, prefix_verbose=None, add_related_name=False, limit_content_type_choices_to={}, limit_object_choices_to={}, is_required=False, ): """ returns a mixin class for generic foreign keys using "Content type - object Id" with dynamic field names. This function is just a class generator Parameters: prefix : a prefix, which is added in front of the fields prefix_verbose : a verbose name of the prefix, used to generate a title for the field column of the content object in the Admin. add_related_name : a boolean value indicating, that a related name for the generated content type foreign key should be added. This value should be true, if you use more than one ObjectRelationMixin in your model. The model fields are created like this: <<prefix>>_content_type : Field name for the "content type" <<prefix>>_object_id : Field name for the "object Id" <<prefix>>_content_object : Field name for the "content object" """ p = "" if prefix: p = "%s_" % prefix content_type_field = "%scontent_type" % p object_id_field = "%sobject_id" % p content_object_field = "%scontent_object" % p class TheClass(models.Model): class Meta: abstract = True if add_related_name: if not prefix: raise FieldError("if add_related_name is set to True," "a prefix must be given") related_name = prefix else: related_name = None optional = not is_required ct_verbose_name = ( _("%s's type (model)") % prefix_verbose if prefix_verbose else _("Related object's type (model)") ) content_type = models.ForeignKey( ContentType, verbose_name=ct_verbose_name, related_name=related_name, blank=optional, null=optional, help_text=_("Please select the type (model) for the relation, you want to build."), limit_choices_to=limit_content_type_choices_to, ) fk_verbose_name = (prefix_verbose or _("Related object")) object_id = models.CharField( fk_verbose_name, blank=optional, null=False, help_text=_("Please enter the ID of the related object."), max_length=255, default="", # for south migrations ) object_id.limit_choices_to = limit_object_choices_to # can be retrieved by # MyModel._meta.get_field("object_id").limit_choices_to content_object = generic.GenericForeignKey( ct_field=content_type_field, fk_field=object_id_field, ) TheClass.add_to_class(content_type_field, content_type) TheClass.add_to_class(object_id_field, object_id) TheClass.add_to_class(content_object_field, content_object) return TheClass -
以下是如何在您的应用中使用两个通用关系的一个示例(将此代码放入
demo_app/models.py),如下所示:# demo_app/models.py # -*- coding: UTF-8 -*- from __future__ import nicode_literals from django.db import models from utils.models import object_relation_mixin_factory from django.utils.encoding import python_2_unicode_compatible FavoriteObjectMixin = object_relation_mixin_factory( is_required=True, ) OwnerMixin = object_relation_mixin_factory( prefix="owner", prefix_verbose=_("Owner"), add_related_name=True, limit_content_type_choices_to={ 'model__in': ('user', 'institution') }, is_required=True, ) @python_2_unicode_compatible class Like(FavoriteObjectMixin, OwnerMixin): class Meta: verbose_name = _("Like") verbose_name_plural = _("Likes") def __str__(self): return _("%(owner)s likes %(obj)s") % { "owner": self.owner_content_object, "obj": self.content_object, }
它是如何工作的...
如您所见,这个片段比之前的更复杂。object_relation_mixin_factory对象本身不是一个混入;它是一个生成模型混入的函数,即一个可以从中扩展的抽象模型类。动态创建的混入添加了content_type和object_id字段以及指向相关实例的content_object通用外键。
为什么我们不能只定义一个包含这三个属性的简单模型混入呢?一个动态生成的抽象类允许我们为每个字段名设置前缀;因此,我们可以在同一个模型中拥有多个通用关系。例如,之前展示的Like模型将为喜欢的对象添加content_type、object_id和content_object字段,以及为喜欢该对象的(用户或机构)添加owner_content_type、owner_object_id和owner_content_object字段。
object_relation_mixin_factory() 函数通过 limit_content_type_choices_to 参数添加了限制内容类型选择的可能性。前面的例子仅将 owner_content_type 的选择限制为 User 和 Institution 模型的内容类型。此外,还有一个 limit_object_choices_to 参数,可以用于自定义表单验证,仅将通用关系限制为特定对象,例如具有发布状态的对象。
参见
-
创建一个包含与 URL 相关方法的模型混入器 的配方
-
创建一个用于处理创建和修改日期的模型混入器 的配方
-
创建一个用于处理元标签的模型混入器 的配方
-
在第四章 模板和 JavaScript 中实现 实现 Like 小部件 的配方
处理多语言字段
Django 使用国际化机制来翻译代码和模板中的冗长字符串。然而,开发者需要决定如何在模型中实现多语言内容。有几个第三方模块可以处理可翻译的模型字段;然而,我更喜欢在本配方中向您介绍的这个简单解决方案。
您将了解的方法的优点如下:
-
在数据库中定义多语言字段非常直接
-
在数据库查询中使用多语言字段非常简单
-
您可以使用贡献的行政功能编辑具有多语言字段的模型,而无需额外修改
-
如果需要,您可以在同一模板中轻松显示一个对象的全部翻译
-
您可以使用数据库迁移来添加或删除语言
准备工作
您已经创建了 utils 包吗?您现在需要为那里的自定义模型字段创建一个新的 fields.py 文件。
如何操作…
执行以下步骤以定义多语言字符字段和多语言文本字段:
-
打开
fields.py文件,并按照以下方式创建多语言字符字段:# utils/fields.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf import settings from django.db import models from django.utils.translation import get_language from django.utils.translation import string_concat class MultilingualCharField(models.CharField): def __init__(self, verbose_name=None, **kwargs): self._blank = kwargs.get("blank", False) self._editable = kwargs.get("editable", True) super(MultilingualCharField, self).\ __init__(verbose_name, **kwargs) def contribute_to_class(self, cls, name, virtual_only=False): # generate language specific fields dynamically if not cls._meta.abstract: for lang_code, lang_name in settings.LANGUAGES: if lang_code == settings.LANGUAGE_CODE: _blank = self._blank else: _blank = True localized_field = models.CharField( string_concat(self.verbose_name, " (%s)" % lang_code), name=self.name, primary_key=self.primary_key, max_length=self.max_length, unique=self.unique, blank=_blank, null=False, # we ignore the null argument! db_index=self.db_index, rel=self.rel, default=self.default or "", editable=self._editable, serialize=self.serialize, choices=self.choices, help_text=self.help_text, db_column=None, db_tablespace=self.db_tablespace ) localized_field.contribute_to_class( cls, "%s_%s" % (name, lang_code), ) def translated_value(self): language = get_language() val = self.__dict__["%s_%s" % (name, language)] if not val: val = self.__dict__["%s_%s" % \ (name, settings.LANGUAGE_CODE)] return val setattr(cls, name, property(translated_value)) -
在同一文件中,添加一个类似的多语言文本字段。以下代码中突出显示了不同的部分:
class MultilingualTextField(models.TextField): def __init__(self, verbose_name=None, **kwargs): self._blank = kwargs.get("blank", False) self._editable = kwargs.get("editable", True) super(MultilingualTextField, self).\ __init__(verbose_name, **kwargs) def contribute_to_class(self, cls, name, virtual_only=False): # generate language specific fields dynamically if not cls._meta.abstract: for lang_code, lang_name in settings.LANGUAGES: if lang_code == settings.LANGUAGE_CODE: _blank = self._blank else: _blank = True localized_field = models.TextField( string_concat(self.verbose_name, " (%s)" % lang_code), name=self.name, primary_key=self.primary_key, max_length=self.max_length, unique=self.unique, blank=_blank, null=False, # we ignore the null argument! db_index=self.db_index, rel=self.rel, default=self.default or "", editable=self._editable, serialize=self.serialize, choices=self.choices, help_text=self.help_text, db_column=None, db_tablespace=self.db_tablespace ) localized_field.contribute_to_class( cls, "%s_%s" % (name, lang_code), ) def translated_value(self): language = get_language() val = self.__dict__["%s_%s" % (name, language)] if not val: val = self.__dict__["%s_%s" % \ (name, settings.LANGUAGE_CODE)] return val setattr(cls, name, property(translated_value))
现在,我们将考虑一个如何在您的应用程序中使用多语言字段的示例,如下所示:
-
首先,在您的设置中设置多种语言:
# myproject/settings.py # -*- coding: UTF-8 -*- # … LANGUAGE_CODE = "en" LANGUAGES = ( ("en", "English"), ("de", "Deutsch"), ("fr", "Français"), ("lt", "Lietuvi kalba"), ) -
然后,按照以下方式为您的模型创建多语言字段:
# demo_app/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import \ python_2_unicode_compatible from utils.fields import MultilingualCharField from utils.fields import MultilingualTextField @python_2_unicode_compatible class Idea(models.Model): title = MultilingualCharField( _("Title"), max_length=200, ) description = MultilingualTextField( _("Description"), blank=True, ) class Meta: verbose_name = _("Idea") verbose_name_plural = _("Ideas") def __str__(self): return self.title
它是如何工作的…
Idea 的示例将创建一个类似于以下模型的模型:
class Idea(models.Model):
title_en = models.CharField(
_("Title (en)"),
max_length=200,
)
title_de = models.CharField(
_("Title (de)"),
max_length=200,
blank=True,
)
title_fr = models.CharField(
_("Title (fr)"),
max_length=200,
blank=True,
)
title_lt = models.CharField(
_("Title (lt)"),
max_length=200,
blank=True,
)
description_en = models.TextField(
_("Description (en)"),
blank=True,
)
description_de = models.TextField(
_("Description (de)"),
blank=True,
)
description_fr = models.TextField(
_("Description (fr)"),
blank=True,
)
description_lt = models.TextField(
_("Description (lt)"),
blank=True,
)
此外,还将有两个属性:title 和 description,它们将返回当前活动语言中的标题和描述。
MultilingualCharField和MultilingualTextField字段会根据你的LANGUAGES设置动态处理模型字段。它们将覆盖 Django 框架创建模型类时使用的contribute_to_class()方法。多语言字段会为项目的每种语言动态添加字符或文本字段。此外,还会创建属性以返回当前活动语言或默认的主语言的翻译值。
例如,你可以在模板中有以下内容:
<h1>{{ idea.title }}</h1>
<div>{{ idea.description|urlize|linebreaks }}</div>
这将根据当前选定的语言显示英文、德语、法语或立陶宛语文本。然而,如果翻译不存在,它将回退到英文。
这里是另一个例子。如果你想在视图中按翻译标题对QuerySet进行排序,你可以定义如下:
qs = Idea.objects.order_by("title_%s" % request.LANGUAGE_CODE)
使用迁移
并非一旦创建了你的数据库结构,它就不会在未来改变。随着开发的迭代进行,你可以在开发过程中获取业务需求更新,并且你将需要在过程中执行数据库模式更改。使用 Django 迁移,你不需要手动更改数据库表和字段,因为大部分操作都是通过命令行界面自动完成的。
准备工作
在命令行工具中激活你的虚拟环境。
如何操作…
要创建数据库迁移,请查看以下步骤:
-
当你在新的
demo_app应用中创建模型时,你需要创建一个初始迁移,这将为你应用创建数据库表。这可以通过以下命令完成:(myproject_env)$ python manage.py makemigrations demo_app -
第一次创建项目所有表时,运行以下命令:
(myproject_env)$ python manage.py migrate它执行所有没有数据库迁移的应用的常规数据库同步,并且除了这个之外,它还会迁移所有设置了迁移的应用。此外,当你想要执行所有应用的新的迁移时,也要运行此命令。
-
如果你想要执行特定应用的迁移,运行以下命令:
(myproject_env)$ python manage.py migrate demo_app -
如果你修改了数据库模式,你必须为该模式创建一个迁移。例如,如果我们向
Idea模型添加一个新的子标题字段,我们可以使用以下命令创建迁移:(myproject_env)$ python manage.py makemigrations --name \ subtitle_added demo_app -
要创建一个修改数据库表中数据的迁移,我们可以使用以下命令:
(myproject_env)$ python manage.py makemigrations --empty \ --name populate_subtitle demo_app这将创建一个数据迁移框架,你需要修改并添加数据操作到它之前应用。
-
要列出所有可用已应用和未应用的迁移,运行以下命令:
(myproject_env)$ python manage.py migrate --list已应用的迁移将带有
[X]前缀。 -
要列出特定应用的可用迁移,运行以下命令:
(myproject_env)$ python manage.py migrate --list demo_app
它是如何工作的…
Django 迁移是数据库迁移机制的指令文件。指令文件告诉我们哪些数据库表需要创建或删除;哪些字段需要添加或删除;以及哪些数据需要插入、更新或删除。
Django 中有两种类型的迁移。一种是模式迁移,另一种是数据迁移。在添加新模型或添加或删除字段时应该创建模式迁移。当你想向数据库中填充一些值或从数据库中大量删除值时,应该使用数据迁移。数据迁移应该使用命令行工具中的命令创建,然后在迁移文件中编程。每个应用的迁移都保存在它们的migrations目录中。第一个迁移通常被称为0001_initial.py,我们示例应用中的其他迁移将被称为0002_subtitle_added.py和0003_populate_subtitle.py。每个迁移都有一个自动递增的数字前缀。对于每个执行的迁移,都会在django_migrations数据库表中保存一个条目。
可以通过指定我们想要迁移到的迁移编号来回迁移,如下所示:
(myproject_env)$ python manage.py migrate demo_app 0002
如果你想要撤销特定应用的全部迁移,可以使用以下命令:
(myproject_env)$ python manage.py migrate demo_app zero
小贴士
在测试了前向和反向迁移过程并且确定它们将在其他开发和公共网站环境中良好工作之前,不要将迁移提交到版本控制。
参见
-
第一章中的使用 pip 处理项目依赖和将外部依赖包含在你的项目中食谱,Django 1.8 入门
-
将外键更改为多对多字段食谱
从 South 迁移切换到 Django 迁移
如果你像我一样,自从 Django 核心功能中存在数据库迁移之前(即,在 Django 1.7 之前)就开始使用 Django,那么你很可能之前已经使用过第三方 South 迁移。在这个食谱中,你将学习如何将你的项目从 South 迁移切换到 Django 迁移。
准备工作
确保所有应用及其 South 迁移都是最新的。
如何操作…
执行以下步骤:
-
将所有应用迁移到最新的 South 迁移,如下所示:
(myproject_env)$ python manage.py migrate -
在设置中将
south从INSTALLED_APPS中移除。 -
对于每个有 South 迁移的应用,删除迁移文件,只留下
migrations目录。 -
使用以下命令创建新的迁移文件:
(my_project)$ python manage.py makemigrations -
由于数据库模式已经正确设置,可以伪造初始 Django 迁移:
(my_project)$ python manage.py migrate --fake-initial -
如果你的应用中存在任何循环外键(即,不同应用中的两个模型通过外键或多对多关系相互指向),请分别对这些应用应用假初始迁移:
(my_project)$ python manage.py migrate --fake-initial demo_app
工作原理…
在切换到处理数据库模式更改的新方式时,数据库中没有冲突,因为 South 迁移历史保存在 south_migrationhistory 数据库表中;而 Django 迁移历史保存在 django_migrations 数据库表中。唯一的问题是具有不同语法的迁移文件,因此需要将 South 迁移完全替换为 Django 迁移。
因此,首先,我们删除 South 迁移文件。然后,makemigrations 命令识别空的 migrations 目录并为每个应用创建新的初始 Django 迁移。一旦这些迁移被伪造,就可以创建并应用进一步的 Django 迁移。
参见
-
使用迁移 配方
-
将外键更改为多对多字段 的配方
将外键更改为多对多字段
这个配方是一个实际示例,说明如何在保留已存在数据的情况下将多对一关系更改为多对多关系。我们将为此情况使用模式和数据迁移。
准备工作
假设您有一个 Idea 模型,其中有一个指向 Category 模型的外键,如下所示:
# demo_app/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class Category(models.Model):
title = models.CharField(_("Title"), max_length=200)
def __str__(self):
return self.title
@python_2_unicode_compatible
class Idea(models.Model):
title = model.CharField(_("Title"), max_length=200)
category = models.ForeignKey(Category,
verbose_name=_("Category"), null=True, blank=True)
def __str__(self):
return self.title
应使用以下命令创建和执行初始迁移:
(myproject_env)$ python manage.py makemigrations demo_app
(myproject_env)$ python manage.py migrate demo_app
如何做到这一点…
以下步骤将指导您如何从外键关系到多对多关系进行切换,同时保留已存在的数据:
-
添加一个名为
categories的新多对多字段,如下所示:# demo_app/models.py @python_2_unicode_compatible class Idea(models.Model): title = model.CharField(_("Title"), max_length=200) category = models.ForeignKey(Category, verbose_name=_("Category"), null=True, blank=True, ) categories = models.ManyToManyField(Category, verbose_name=_("Categories"), blank=True, related_name="ideas", ) -
为了将新字段添加到数据库中,请创建并运行一个模式迁移,如下所示:
(myproject_env)$ python manage.py makemigrations demo_app \ --name categories_added (myproject_env)$ python manage.py migrate demo_app -
创建一个数据迁移,将类别从外键复制到多对多字段,如下所示:
(myproject_env)$ python manage.py makemigrations --empty \ --name copy_categories demo_app -
打开新创建的迁移文件 (
demo_app/migrations/0003_copy_categories.py) 并定义正向迁移指令,如下所示:# demo_app/migrations/0003_copy_categories.py # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations def copy_categories(apps, schema_editor): Idea = apps.get_model("demo_app", "Idea") for idea in Idea.objects.all(): if idea.category: idea.categories.add(idea.category) class Migration(migrations.Migration): dependencies = [ ('demo_app', '0002_categories_added'), ] operations = [ migrations.RunPython(copy_categories), ] -
运行以下数据迁移:
(myproject_env)$ python manage.py migrate demo_app -
在
models.py文件中删除外键字段category:# demo_app/models.py @python_2_unicode_compatible class Idea(models.Model): title = model.CharField(_("Title"), max_length=200) categories = models.ManyToManyField(Category, verbose_name=_("Categories"), blank=True, related_name="ideas", ) -
创建并运行一个模式迁移,以从数据库表中删除
categories字段,如下所示:(myproject_env)$ python manage.py schemamigration \ --name delete_category demo_app (myproject_env)$ python manage.py migrate demo_app
它是如何工作的…
首先,我们在 Idea 模型中添加一个新的多对多字段。然后,我们将现有关系从外键关系到多对多关系复制。最后,我们移除外键关系。
参见
-
使用迁移 配方
-
从 South 迁移切换到 Django 迁移 配方
第三章。表单和视图
在本章中,我们将涵盖以下主题:
-
将 HttpRequest 传递给表单
-
利用表单的 save 方法
-
上传图片
-
使用 django-crispy-forms 创建表单布局
-
下载授权文件
-
过滤对象列表
-
管理分页列表
-
编写基于类的视图
-
生成 PDF 文档
-
使用 Haystack 实现多语言搜索
简介
当在模型中定义数据库结构时,我们需要一些视图让用户输入数据或向他人展示数据。在本章中,我们将重点关注管理表单、列表视图以及生成 HTML 之外输出的视图。对于最简单的示例,我们将把 URL 规则和模板的创建留给你。
将 HttpRequest 传递给表单
每个 Django 视图的第一个参数是HttpRequest对象,通常命名为request。它包含有关请求的元数据。例如,当前语言代码、当前用户、当前 cookie 和当前会话。默认情况下,在视图中使用的表单接受GET或POST参数、文件、初始数据和其他参数;然而,不接受HttpRequest对象。在某些情况下,将HttpRequest传递给表单很有用,特别是当你想使用请求数据过滤表单字段的选项或处理保存某些内容,如当前用户或 IP 在表单中时。
在这个菜谱中,我们将看到一个示例表单,人们可以选择一个用户并向他们发送消息。我们将把HttpRequest对象传递给表单,以便排除当前用户从收件人选项中;我们不希望任何人给自己发消息。
准备工作
让我们创建一个名为email_messages的新应用,并将其放入设置中的INSTALLED_APPS。此应用将没有模型,只有表单和视图。
如何做到这一点...
要完成这个菜谱,执行以下步骤:
-
在
forms.py文件中添加一个新的包含两个字段的表单:收件人选择和消息文本。此外,此表单将有一个初始化方法,该方法将接受请求对象,然后修改收件人选择字段的QuerySet:# email_messages/forms.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User class MessageForm(forms.Form): recipient = forms.ModelChoiceField( label=_("Recipient"), queryset=User.objects.all(), required=True, ) message = forms.CharField( label=_("Message"), widget=forms.Textarea, required=True, ) def __init__(self, request, *args, **kwargs): super(MessageForm, self).__init__(*args, **kwargs) self.request = request self.fields["recipient"].queryset = \ self.fields["recipient"].queryset.\ exclude(pk=request.user.pk) -
然后,创建包含
message_to_user()视图的views.py文件以处理表单。正如你所看到的,请求对象作为第一个参数传递给表单,如下所示:# email_messages/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect from .forms import MessageForm @login_required def message_to_user(request): if request.method == "POST": form = MessageForm(request, data=request.POST) if form.is_valid(): # do something with the form return redirect("message_to_user_done") else: form = MessageForm(request) return render(request, "email_messages/message_to_user.html", {"form": form} )
它是如何工作的...
在初始化方法中,我们有self变量,它代表表单本身的实例,我们还有新添加的request变量,然后我们有剩余的位置参数(*args)和命名参数(**kwargs)。我们调用super()初始化方法,将所有位置和命名参数传递给它,以便正确初始化表单。然后,我们将request变量分配给表单的新request属性,以便在表单的其他方法中稍后访问。然后,我们修改收件人选择字段的queryset属性,排除请求中的当前用户。
在视图中,我们将在这两种情况下将HttpRequest对象作为第一个参数传递:当表单提交时,以及当它第一次加载时。
参见
- 使用表单的保存方法
使用表单的保存方法
为了使视图简洁明了,在可能且合理的情况下,将表单数据的处理移动到表单本身是一个好的做法。常见的做法是有一个save()方法,它将保存数据,执行搜索或执行其他智能操作。我们将扩展之前菜谱中定义的表单,添加save()方法,这将向选定的收件人发送电子邮件。
准备工作
我们将基于将 HttpRequest 传递给表单菜谱中定义的示例进行构建。
如何操作…
要完成这个菜谱,执行以下两个步骤:
-
从 Django 导入函数以发送电子邮件。然后,将
save()方法添加到MessageForm中。它将尝试向选定的收件人发送电子邮件,如果发生任何错误,它将静默失败:# email_messages/forms.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext,\ ugettext_lazy as _ from django.core.mail import send_mail from django.contrib.auth.models import User class MessageForm(forms.Form): recipient = forms.ModelChoiceField( label=_("Recipient"), queryset=User.objects.all(), required=True, ) message = forms.CharField( label=_("Message"), widget=forms.Textarea, required=True, ) def __init__(self, request, *args, **kwargs): super(MessageForm, self).__init__(*args, **kwargs) self.request = request self.fields["recipient"].queryset = \ self.fields["recipient"].queryset.\ exclude(pk=request.user.pk) def save(self): cleaned_data = self.cleaned_data send_mail( subject=ugettext("A message from %s") % \ self.request.user, message=cleaned_data["message"], from_email=self.request.user.email, recipient_list=[ cleaned_data["recipient"].email ], fail_silently=True, ) -
然后,如果提交的数据有效,从视图中调用表单的
save()方法:# email_messages/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect from .forms import MessageForm @login_required def message_to_user(request): if request.method == "POST": form = MessageForm(request, data=request.POST) if form.is_valid(): form.save() return redirect("message_to_user_done") else: form = MessageForm(request) return render(request, "email_messages/message_to_user.html", {"form": form} )
它是如何工作的…
让我们来看看表单。save()方法使用表单的清理数据来读取收件人的电子邮件地址和消息。电子邮件的发送者是请求中的当前用户。如果由于邮件服务器配置错误或其他原因无法发送电子邮件,它将静默失败;也就是说,不会引发错误。
现在,让我们看看视图。当提交的表单有效时,表单的save()方法将被调用,用户将被重定向到成功页面。
参见
-
将 HttpRequest 传递给表单
-
下载授权文件菜谱
上传图片
在这个菜谱中,我们将查看处理图像上传的最简单方法。您将看到一个示例应用,访客可以上传带有励志名言的图片。
准备工作
确保在您的虚拟环境或全局范围内安装了 Pillow 或 PIL。
然后,让我们创建一个quotes应用,并将其放入设置中的INSTALLED_APPS。然后,我们将添加一个具有三个字段(author、quote文本和picture)的InspirationalQuote模型,如下所示:
# quotes/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import os
from django.db import models
from django.utils.timezone import now as timezone_now
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
def upload_to(instance, filename):
now = timezone_now()
filename_base, filename_ext = os.path.splitext(filename)
return "quotes/%s%s" % (
now.strftime("%Y/%m/%Y%m%d%H%M%S"),
filename_ext.lower(),
)
@python_2_unicode_compatible
class InspirationalQuote(models.Model):
author = models.CharField(_("Author"), max_length=200)
quote = models.TextField(_("Quote"))
picture = models.ImageField(_("Picture"),
upload_to=upload_to,
blank=True,
null=True,
)
class Meta:
verbose_name = _("Inspirational Quote")
verbose_name_plural = _("Inspirational Quotes")
def __str__(self):
return self.quote
此外,我们创建了一个 upload_to() 函数,它将上传图片的路径设置为类似于 quotes/2015/04/20150424140000.png 的东西。如您所见,我们使用日期时间戳作为文件名以确保其唯一性。我们将此函数传递给 picture 图像字段。
如何做到这一点...
执行以下步骤以完成配方:
-
创建
forms.py文件并将一个简单的模型表单放在那里:# quotes/forms.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import forms from .models import InspirationalQuote class InspirationalQuoteForm(forms.ModelForm): class Meta: model = InspirationalQuote fields = ["author", "quote", "picture", "language"] -
在
views.py文件中,放置一个处理表单的视图。别忘了将FILES字典对象传递给表单。当表单有效时,按照以下方式触发保存方法:# quotes/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.shortcuts import redirect from django.shortcuts import render from .forms import InspirationalQuoteForm def add_quote(request): if request.method == "POST": form = InspirationalQuoteForm( data=request.POST, files=request.FILES, ) if form.is_valid(): quote = form.save() return redirect("add_quote_done") else: form = InspirationalQuoteForm() return render(request, "quotes/change_quote.html", {"form": form} ) -
最后,在
templates/quotes/change_quote.html中创建视图的模板。对于 HTML 表单,设置enctype属性为multipart/form-data非常重要,否则文件上传将不会工作:{# templates/quotes/change_quote.html #} {% extends "base.html" %} {% load i18n %} {% block content %} <form method="post" action="" enctype="multipart/form-data"> {% csrf_token %} {{ form.as_p }} <button type="submit">{% trans "Save" %}</button> </form> {% endblock %}
它是如何工作的...
Django 模型表单是从模型创建的表单。它们提供了模型中的所有字段,因此您不需要再次定义它们。在前面的例子中,我们为 InspirationalQuote 模型创建了一个模型表单。当我们保存表单时,表单知道如何将每个字段保存到数据库中,以及如何上传文件并将它们保存到媒体目录中。
还有更多
作为额外奖励,我们将看到一个如何从上传的图像生成缩略图的例子。使用这种技术,您还可以生成图像的几个其他特定版本,例如列表版本、移动版本和桌面计算机版本。
我们将向 InspirationalQuote 模型(quotes/models.py)添加三个方法。它们是 save()、create_thumbnail() 和 get_thumbnail_picture_url()。当模型正在保存时,我们将触发缩略图的创建。当我们需要在模板中显示缩略图时,我们可以使用 {{ quote.get_thumbnail_picture_url }} 获取其 URL。方法定义如下:
# quotes/models.py
# …
from PIL import Image
from django.conf import settings
from django.core.files.storage import default_storage as storage
THUMBNAIL_SIZE = getattr(
settings,
"QUOTES_THUMBNAIL_SIZE",
(50, 50)
)
class InspirationalQuote(models.Model):
# …
def save(self, *args, **kwargs):
super(InspirationalQuote, self).save(*args, **kwargs)
# generate thumbnail picture version
self.create_thumbnail()
def create_thumbnail(self):
if not self.picture:
return ""
file_path = self.picture.name
filename_base, filename_ext = os.path.splitext(file_path)
thumbnail_file_path = "%s_thumbnail.jpg" % filename_base
if storage.exists(thumbnail_file_path):
# if thumbnail version exists, return its url path
return "exists"
try:
# resize the original image and
# return URL path of the thumbnail version
f = storage.open(file_path, 'r')
image = Image.open(f)
width, height = image.size
if width > height:
delta = width - height
left = int(delta/2)
upper = 0
right = height + left
lower = height
else:
delta = height - width
left = 0
upper = int(delta/2)
right = width
lower = width + upper
image = image.crop((left, upper, right, lower))
image = image.resize(THUMBNAIL_SIZE, Image.ANTIALIAS)
f_mob = storage.open(thumbnail_file_path, "w")
image.save(f_mob, "JPEG")
f_mob.close()
return "success"
except:
return "error"
def get_thumbnail_picture_url(self):
if not self.picture:
return ""
file_path = self.picture.name
filename_base, filename_ext = os.path.splitext(file_path)
thumbnail_file_path = "%s_thumbnail.jpg" % filename_base
if storage.exists(thumbnail_file_path):
# if thumbnail version exists, return its URL path
return storage.url(thumbnail_file_path)
# return original as a fallback
return self.picture.url
在前面的方法中,我们使用文件存储 API 而不是直接操作文件系统,这样我们就可以用 Amazon S3 存储桶或其他存储服务替换默认存储,而方法仍然有效。
缩略图的创建是如何工作的?如果我们把原始文件保存为 quotes/2014/04/20140424140000.png,我们将检查 quotes/2014/04/20140424140000_thumbnail.jpg 文件是否存在,如果不存在,我们将打开原始图像,从中裁剪出中心部分,将其调整到 50 x 50 像素,并将其保存到存储中。
get_thumbnail_picture_url() 方法检查缩略图版本是否存储中存在,并返回其 URL。如果缩略图版本不存在,则返回原始图像的 URL 作为后备。
参见
- 使用 django-crispy-forms 创建表单布局 的配方
使用 django-crispy-forms 创建表单布局
django-crispy-forms Django 应用允许你使用以下 CSS 框架之一:Uni-Form、Bootstrap 或 Foundation 来构建、自定义和重用表单。django-crispy-forms的使用与 Django 贡献的行政中的字段集类似;然而,它更高级且可自定义。你在 Python 代码中定义表单布局,你不需要担心每个字段在 HTML 中的呈现方式。然而,如果你需要添加特定的 HTML 属性或包装,你也可以轻松做到。此外,django-crispy-forms使用的所有标记都位于模板中,可以根据特定需求进行覆盖。
在这个配方中,我们将看到一个如何使用 Bootstrap 3 与django-crispy-forms结合的示例,Bootstrap 3 是最受欢迎的前端框架,用于开发响应式、以移动端优先的 Web 项目。
准备工作
首先,依次执行以下任务:
从getbootstrap.com/下载 Bootstrap 前端框架,并在模板中集成 CSS 和 JavaScript。更多关于这方面的内容,请参考第四章中的安排 base.html 模板配方,模板和 JavaScript。
使用以下命令在你的虚拟环境中安装django-crispy-forms:
(myproject_env)$ pip install django-crispy-forms
确保将crispy_forms添加到INSTALLED_APPS,然后设置bootstrap3为在此项目中使用的模板包:
# conf/base.py or settings.py
INSTALLED_APPS = (
# …
"crispy_forms",
)
# …
CRISPY_TEMPLATE_PACK = "bootstrap3"
让我们创建一个bulletin_board应用来展示django-crispy-forms的使用,并将其放入设置中的INSTALLED_APPS。我们将有一个Bulletin模型,包含以下字段:bulletin_type、title、description、contact_person、phone、email和image,如下所示:
# bulletin_board/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
TYPE_CHOICES = (
('searching', _("Searching")),
('offering', _("Offering")),
)
@python_2_unicode_compatible
class Bulletin(models.Model):
bulletin_type = models.CharField(_("Type"), max_length=20, choices=TYPE_CHOICES)
title = models.CharField(_("Title"), max_length=255)
description = models.TextField(_("Description"),
max_length=300)
contact_person = models.CharField(_("Contact person"),
max_length=255)
phone = models.CharField(_("Phone"), max_length=200,
blank=True)
email = models.EmailField(_("Email"), blank=True)
image = models.ImageField(_("Image"), max_length=255,
upload_to="bulletin_board/", blank=True)
class Meta:
verbose_name = _("Bulletin")
verbose_name_plural = _("Bulletins")
ordering = ("title",)
def __str__(self):
return self.title
如何做…
按照以下步骤进行:
-
让我们在新创建的应用中添加一个公告的模型表单。我们将在初始化方法本身中附加一个表单助手。表单助手将具有布局属性,这将定义表单的布局,如下所示:
# bulletin_board/forms.py # -*- coding: UTF-8 -*- from django import forms from django.utils.translation import ugettext_lazy as _,\ ugettext from crispy_forms.helper import FormHelper from crispy_forms import layout, bootstrap from .models import Bulletin class BulletinForm(forms.ModelForm): class Meta: model = Bulletin fields = ["bulletin_type", "title", "description", "contact_person", "phone", "email", "image"] def __init__(self, *args, **kwargs): super(BulletinForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_action = "" self.helper.form_method = "POST" self.fields["bulletin_type"].widget = \ forms.RadioSelect() # delete empty choice for the type del self.fields["bulletin_type"].choices[0] self.helper.layout = layout.Layout( layout.Fieldset( _("Main data"), layout.Field("bulletin_type"), layout.Field("title", css_class="input-block-level"), layout.Field("description", css_class="input-blocklevel", rows="3"), ), layout.Fieldset( _("Image"), layout.Field("image", css_class="input-block-level"), layout.HTML(u"""{% load i18n %} <p class="help-block">{% trans "Available formats are JPG, GIF, and PNG. Minimal size is 800 × 800 px." %}</p> """), title=_("Image upload"), css_id="image_fieldset", ), layout.Fieldset( _("Contact"), layout.Field("contact_person", css_class="input-blocklevel"), layout.Div( bootstrap.PrependedText("phone", """<span class="glyphicon glyphicon-earphone"> </span>""", css_class="inputblock-level"), bootstrap.PrependedText("email", "@", css_class="input-block-level", placeholder="contact@example.com"), css_id="contact_info", ), ), bootstrap.FormActions( layout.Submit("submit", _("Save")), ) ) -
要在模板中渲染表单,我们只需要加载
crispy_forms_tags模板标签库,并使用如下所示的{% crispy %}模板标签:{# templates/bulletin_board/change_form.html #} {% extends "base.html" %} {% load crispy_forms_tags %} {% block content %} {% crispy form %} {% endblock %} -
创建
base.html模板。你可以根据第四章中的安排 base.html 模板配方中的示例来做这件事,模板和 JavaScript。
如何工作…
包含公告表单的页面将类似于以下所示:

如你所见,字段是按字段集分组的。Fieldset对象的第一个参数定义了图例,其他位置参数定义了字段。你也可以传递命名参数来定义字段集的 HTML 属性;例如,对于第二个字段集,我们传递了title和css_id来设置title和idHTML 属性。
字段也可以通过命名参数传递额外的属性;例如,对于description字段,我们正在传递css_class和rows来设置 HTML 的class和rows属性。
除了正常字段外,你还可以传递 HTML 片段,因为这是通过图像字段的帮助块完成的。你还可以在布局中添加前置文本字段。例如,我们在电话字段中添加了一个电话图标,在电子邮件字段中添加了一个@符号。正如你从联系字段的示例中看到的,我们可以很容易地使用Div对象将字段包裹在 HTML <div>元素中。当需要将特定的 JavaScript 应用于某些表单字段时,这很有用。
HTML 表单的action属性由表单辅助器的form_action属性定义。如果你使用空字符串作为 action,表单将被提交到包含表单的同一视图。HTML 表单的method属性由表单辅助器的form_method属性定义。正如你所知,HTML 表单允许 GET 和 POST 方法。最后,有一个Submit对象用于渲染提交按钮,它将按钮的名称作为第一个位置参数,将按钮的值作为第二个参数。
还有更多…
对于基本用法,给定的示例已经足够了。然而,如果你需要为你的项目中的表单指定特定的标记,你仍然可以覆盖和修改django-crispy-forms应用的模板,因为这些标记不是硬编码在 Python 文件中的,而是通过模板渲染生成的。只需将django-crispy-forms应用的模板复制到你的项目模板目录,并按需更改它们。
参见
-
过滤对象列表菜谱
-
管理分页列表菜谱
-
下载授权文件菜谱
下载授权文件
有时,你可能需要只允许特定的人从你的网站上下载知识产权。例如,音乐、视频、文学或其他艺术作品应该只对付费会员开放。在这个菜谱中,你将学习如何仅通过贡献的 Django 认证应用限制认证用户下载图片。
准备工作
首先,创建quotes应用,就像在上传图片菜谱中做的那样。
如何做到这一点…
依次执行以下步骤:
-
创建一个需要认证才能下载文件的视图,如下所示:
# quotes/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import os from django.shortcuts import get_object_or_404 from django.http import FileResponse from django.utils.text import slugify from django.contrib.auth.decorators import login_required from .models import InspirationalQuote @login_required(login_url="my_login_page") def download_quote_picture(request, quote_id): quote = get_object_or_404(InspirationalQuote, pk=quote_id) file_name, file_extension = os.path.splitext( quote.picture.file.name) file_extension = file_extension[1:] # remove the dot response = FileResponse( quote.picture.file, content_type="image/%s" % file_extension ) response["Content-Disposition"] = "attachment;" \ " filename=%s---%s.%s" % ( slugify(quote.author)[:100], slugify(quote.quote)[:100], file_extension ) return response -
将视图添加到 URL 配置中:
# quotes/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import patterns, url urlpatterns = patterns("", # … url(r'^(?P<quote_id>\d+)/download/$', "quotes.views.download_quote_picture", name="download_quote_picture" ), ) -
然后,我们需要在项目 URL 配置中设置登录视图。注意我们是如何也为
django-crispy-forms添加login_helper的:# myproject/urls.py # -*- coding: UTF-8 -*- from django.conf.urls import patterns, include, url from django.conf import settings from django.contrib import admin from django.core.urlresolvers import reverse_lazy from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ from django.conf.urls.i18n import i18n_patterns from crispy_forms.helper import FormHelper from crispy_forms import layout, bootstrap login_helper = FormHelper() login_helper.form_action = reverse_lazy("my_login_page") login_helper.form_method = "POST" login_helper.form_class = "form-signin" login_helper.html5_required = True login_helper.layout = layout.Layout( layout.HTML(string_concat("""<h2 class="form-signin-heading">""", _("Please Sign In"), """</h2>""")), layout.Field("username", placeholder=_("username")), layout.Field("password", placeholder=_("password")), layout.HTML("""<input type="hidden" name="next" value="{{ next }}" />"""), layout.Submit("submit", _("Login"), css_class="btn-lg"), ) urlpatterns = i18n_patterns("", # … url(r'login/$', "django.contrib.auth.views.login", {"extra_context": {"login_helper": login_helper}}, name="my_login_page" ), url(r'^quotes/', include("quotes.urls")), ) -
让我们为登录表单创建一个模板,如下所示:
{# templates/registration/login.html #} {% extends "base.html" %} {% load crispy_forms_tags %} {% block stylesheet %} {{ block.super }}<link rel="stylesheet" href="{{ STATIC_URL }}site/css/login.css"> {% endblock %} {% block content %} <div class="container"> {% crispy form login_helper %} </div> {% endblock %} -
创建
login.css文件以给登录表单添加一些样式。最后,你应该限制用户绕过 Django 直接下载受限制的文件。如果你使用的是 Apache 网络服务器,可以在使用 Apache 2.2 时,将.htaccess文件放在media/quotes目录中,并包含以下内容:# media/quotes/.htaccess Order deny,allow Deny from all如果你使用的是 Apache 2.4,可以放置以下内容:
# media/quotes/.htaccess Require all denied
它是如何工作的…
download_quote_picture() 视图从特定的励志引语中流式传输图片。将 Content-Disposition 标头设置为 attachment 使得文件可下载而不是立即在浏览器中显示。文件的名称将类似于 walt-disney---if-you-can-dream-it-you-can-do-it.png。如果访客未登录而尝试访问可下载文件,@login_required 装饰器将重定向访客到登录页面。
由于我们想要有一个漂亮的 Bootstrap 风格的登录表单,我们再次使用 django-crispy-forms 并为 login_helper 表单定义一个辅助器。辅助器作为额外的上下文变量传递给授权表单,然后作为 {% crispy %} 模板标签的第二个参数使用。
根据应用的 CSS,登录表单可能看起来类似于以下内容:

相关内容
-
上传图片 配方
-
使用 django-crispy-forms 创建表单布局 配方
过滤对象列表
在网络开发中,除了带有表单的视图外,通常还会有对象列表视图和详细视图。列表视图可以简单地列出按顺序排列的对象,例如按字母顺序或创建日期排序;然而,对于大量数据来说,这并不非常用户友好。为了最佳的可访问性和便利性,你应该能够通过所有可能的类别过滤内容。在这个配方中,我们将看到用于通过任意数量的类别过滤列表视图的模式。
我们将要创建的是一个可以按类型、导演、演员或评分过滤的电影列表视图。应用 Bootstrap 3 后,它看起来将类似于以下内容:

准备工作
对于过滤示例,我们将使用与类型、导演和演员有关系的 Movie 模型来过滤。也可以按评分过滤,这是一个带有选项的 PositiveIntegerField。让我们创建 movies 应用程序,将其放入设置中的 INSTALLED_APPS,并在新应用中定义所提到的模型,如下所示:
# movies/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
RATING_CHOICES = (
(1, ""),
(2, ""),
(3, ""),
(4, ""),
(5, ""),
)
@python_2_unicode_compatible
class Genre(models.Model):
title = models.CharField(_("Title"), max_length=100)
def __str__(self):
return self.title
@python_2_unicode_compatible
class Director(models.Model):
first_name = models.CharField(_("First name"), max_length=40)
last_name = models.CharField(_("Last name"), max_length=40)
def __str__(self):
return self.first_name + " " + self.last_name
@python_2_unicode_compatible
class Actor(models.Model):
first_name = models.CharField(_("First name"), max_length=40)
last_name = models.CharField(_("Last name"), max_length=40)
def __str__(self):
return self.first_name + " " + self.last_name
@python_2_unicode_compatible
class Movie(models.Model):
title = models.CharField(_("Title"), max_length=255)
genres = models.ManyToManyField(Genre, blank=True)
directors = models.ManyToManyField(Director, blank=True)
actors = models.ManyToManyField(Actor, blank=True)
rating = models.PositiveIntegerField(choices=RATING_CHOICES)
def __str__(self):
return self.title
如何做到这一点…
要完成这个配方,请按照以下步骤操作:
-
首先,我们创建包含所有可能的过滤类别的
MovieFilterForm:# movies/forms.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext_lazy as _ from .models import Genre, Director, Actor, RATING_CHOICES class MovieFilterForm(forms.Form): genre = forms.ModelChoiceField( label=_("Genre"), required=False, queryset=Genre.objects.all(), ) director = forms.ModelChoiceField( label=_("Director"), required=False, queryset=Director.objects.all(), ) actor = forms.ModelChoiceField( label=_("Actor"), required=False, queryset=Actor.objects.all(), ) rating = forms.ChoiceField( label=_("Rating"), required=False, choices=RATING_CHOICES, ) -
然后,我们创建一个
movie_list视图,它将使用MovieFilterForm验证请求查询参数并执行所选类别的过滤。注意这里使用的facets字典,它用于列出类别和当前选定的选项:# movies/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.shortcuts import render from .models import Genre, Director, Actor from .models import Movie, RATING_CHOICES from .forms import MovieFilterForm def movie_list(request): qs = Movie.objects.order_by("title") form = MovieFilterForm(data=request.GET) facets = { "selected": {}, "categories": { "genres": Genre.objects.all(), "directors": Director.objects.all(), "actors": Actor.objects.all(), "ratings": RATING_CHOICES, }, } if form.is_valid(): genre = form.cleaned_data["genre"] if genre: facets["selected"]["genre"] = genre qs = qs.filter(genres=genre).distinct() director = form.cleaned_data["director"] if director: facets["selected"]["director"] = director qs = qs.filter(directors=director).distinct() actor = form.cleaned_data["actor"] if actor: facets["selected"]["actor"] = actor qs = qs.filter(actors=actor).distinct() rating = form.cleaned_data["rating"] if rating: rating = int(rating) facets["selected"]["rating"] = (rating, dict(RATING_CHOICES)[rating]) qs = qs.filter(rating=rating).distinct() # Let's inspect the facets in the console if settings.DEBUG: from pprint import pprint pprint(facets) context = { "form": form, "facets": facets, "object_list": qs, } return render(request, "movies/movie_list.html", context) -
最后,我们创建列表视图的模板。在这里,我们将使用
facets字典来列出分类并知道哪个分类当前被选中。为了生成过滤器的 URL,我们将使用{% modify_query %}模板标签,该标签将在第五章的创建一个用于修改请求查询参数的模板标签食谱中进行描述,自定义模板过滤器和标签。请在templates/movies/movie_list.html目录中复制以下代码:{# templates/movies/movie_list.html #} {% extends "base_two_columns.html" %} {% load i18n utility_tags %} {% block sidebar %} <div class="filters panel-group" id="accordion"> <div class="panel panel-default"> <div class="panel-heading"> <h6 class="panel-title"> <a data-toggle="collapse" data-parent="#accordion" href="#collapseGenres"> {% trans "Filter by Genre" %} </a> </h6> </div> <div id="collapseGenres" class="panel-collapse collapse in"> <div class="panel-body"> <div class="list-group"> <a class="list-group-item{% if not facets.selected.genre %} active{% endif %}" href="{% modify_query "genre" "page" %}">{% trans "All" %}</a> {% for cat in facets.categories.genres %} <a class="list-group-item{% if facets.selected.genre == cat %} active{% endif %}" href="{% modify_query "page" genre=cat.pk %}">{{ cat }}</a> {% endfor %} </div> </div> </div> </div> <div class="panel panel-default"> <div class="panel-heading"> <h6 class="panel-title"> <a data-toggle="collapse" data-parent="#accordion" href="#collapseDirectors"> {% trans "Filter by Director" %} </a> </h6> </div> <div id="collapseDirectors" class="panel-collapse collapse"> <div class="panel-body"> <div class="list-group"> <a class="list-group-item{% if not facets.selected.director %} active{% endif %}" href="{% modify_query "director" "page" %}">{% trans "All" %}</a> {% for cat in facets.categories.directors %} <a class="list-group-item{% if facets.selected.director == cat %} active{% endif %}" href="{% modify_query "page" director=cat.pk %}">{{ cat }}</a> {% endfor %} </div> </div> </div> </div> {# Analogously by the examples of genres and directors above, add a filter for actors here… #} <div class="panel panel-default"> <div class="panel-heading"> <h6 class="panel-title"> <a data-toggle="collapse" data-parent="#accordion" href="#collapseRatings"> {% trans "Filter by Rating" %} </a> </h6> </div> <div id="collapseRatings" class="panel-collapse collapse"> <div class="panel-body"> <div class="list-group"> <a class="list-group-item{% if not facets.selected.rating %} active{% endif %}" href="{% modify_query "rating" "page" %}">{% trans "All" %}</a> {% for r_val, r_display in facets.categories.ratings %} <a class="list-group-item{% if facets.selected.rating.0 == r_val %} active{% endif %}" href="{% modify_query "page" rating=r_val %}">{{ r_display }}</a> {% endfor %} </div> </div> </div> </div> </div> {% endblock %} {% block content %} <div class="movie_list"> {% for movie in object_list %} <div class="movie alert alert-info"> <p>{{ movie.title }}</p> </div> {% endfor %} </div> {% endblock %} -
添加一个简单的两列布局的基本模板,如下所示:
{# base_two_columns.html #} {% extends "base.html" %} {% block container %} <div class="container"> <div class="row"> <div id="sidebar" class="col-md-4"> {% block sidebar %} {% endblock %} </div> <div id="content" class="col-md-8"> {% block content %} {% endblock %} </div> </div> </div> {% endblock %} -
创建
base.html模板。你可以根据第四章中提供的安排 base.html 模板食谱进行操作,模板和 JavaScript。
它是如何工作的…
我们正在使用传递给模板上下文的facets字典来了解我们有哪些过滤器以及哪些过滤器被选中。为了深入了解,facets字典由两部分组成:categories字典和selected字典。categories字典包含所有可过滤分类的QuerySets或选择。selected字典包含每个分类当前选中的值。
在视图中,我们检查查询参数是否以表单的形式有效,然后从选定的分类中钻取对象的QuerySet。此外,我们将选定的值设置到facets字典中,该字典将被传递到模板。
在模板中,对于facets字典中的每个分类,我们列出所有分类并将当前选中的分类标记为活动状态。
如此简单。
参见
-
管理分页列表食谱
-
组合基于类的视图食谱
-
第五章中的创建一个用于修改请求查询参数的模板标签食谱,自定义模板过滤器和标签
管理分页列表
如果你有一个动态变化的对象列表或者它们的数量超过 30,你肯定需要分页列表。有了分页,你提供的数据集的一部分而不是完整的QuerySet,限制每页的数量,你还会显示链接以访问列表的其他页面。Django 有管理分页数据的类,我们将在本食谱中看到如何做到这一点,该食谱提供了上一个食谱中的示例。
准备工作
让我们从过滤对象列表食谱中的movies应用的表单和视图开始。
如何做到这一点…
要将分页添加到电影的列表视图,请按照以下步骤操作:
-
首先,从 Django 导入必要的分页类。我们将在过滤后立即将分页管理添加到
movie_list视图。此外,我们将稍微修改上下文字典,将page对象而不是电影QuerySet分配给object_list键:# movies/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.shortcuts import render from django.core.paginator import Paginator, EmptyPage,\ PageNotAnInteger from .models import Movie from .forms import MovieFilterForm def movie_list(request): paginate_by = 15 qs = Movie.objects.order_by("title") # … filtering goes here… paginator = Paginator(qs, paginate_by) page_number = request.GET.get("page") try: page = paginator.page(page_number) except PageNotAnInteger: # If page is not an integer, show first page. page = paginator.page(1) except EmptyPage: # If page is out of range, show last existing page. page = paginator.page(paginator.num_pages) context = { # … "object_list": page, } return render(request, "movies/movie_list.html", context) -
在模板中,我们将在电影列表之后添加分页控件,如下所示:
{# templates/movies/movie_list.html #} {% extends "base.html" %} {% load i18n utility_tags %} {% block sidebar %} {# … filters go here… #} {% endblock %} {% block content %} <div class="movie_list"> {% for movie in object_list %} <div class="movie alert alert-info"> <p>{{ movie.title }}</p> </div> {% endfor %} </div> {% if object_list.has_other_pages %} <ul class="pagination"> {% if object_list.has_previous %} <li><a href="{% modify_query page=object_list.previous_page_number %}">«</a></li> {% else %} <li class="disabled"><span>«</span></li> {% endif %} {% for page_number in object_list.paginator.page_range %} {% if page_number == object_list.number %} <li class="active"> <span>{{ page_number }} <span class="sr-only">(current)</span></span> </li> {% else %} <li> <a href="{% modify_query page=page_number %}">{{ page_number }}</a> </li> {% endif %} {% endfor %} {% if object_list.has_next %} <li><a href="{% modify_query page=object_list.next_page_number %}">»</a></li> {% else %} <li class="disabled"><span>»</span></li> {% endif %} </ul> {% endif %} {% endblock %}
它是如何工作的…
当你在浏览器中查看结果时,你会看到类似以下分页控件,添加在电影列表之后:

我们是如何实现这一点的?当QuerySet被过滤时,我们将创建一个paginator对象,传递QuerySet和每页显示的最大项目数,这里为 15。然后,我们将从查询参数page中读取当前页码。下一步是从paginator中检索当前页对象。如果页码不是整数,我们获取第一页。如果数字超过可能的页数,则检索最后一页。页对象具有显示在前面截图中的分页小部件所需的方法和属性。此外,页对象像QuerySet一样工作,这样我们就可以遍历它并从页面的部分中获取项目。
模板中标记的片段使用 Bootstrap 3 前端框架的标记创建分页小部件。我们仅在存在多于当前页面的页面时显示分页控件。小部件中包含上一页和下一页的链接,以及所有页码的列表。当前页码被标记为活动状态。为了生成链接的 URL,我们使用{% modify_query %}模板标签,这将在第五章的创建用于修改请求查询参数的模板标签配方中稍后描述,自定义模板过滤器和标签。
相关内容
-
过滤对象列表配方
-
编写基于类的视图配方
-
第五章中的创建用于修改请求查询参数的模板标签配方,自定义模板过滤器和标签
编写基于类的视图
Django 视图是可调用的,它接受请求并返回响应。除了基于函数的视图外,Django 还提供了一种将视图定义为类的方法。当您想要创建可重用的模块化视图或组合通用混合视图时,这种方法很有用。在这个配方中,我们将之前显示的基于函数的movie_list视图转换为基于类的MovieListView视图。
准备工作
创建模型、表单和模板,类似于之前的配方,过滤对象列表和管理分页列表。
如何做…
-
我们需要在 URL 配置中创建一个 URL 规则并添加一个基于类的视图。要将基于类的视图包含在 URL 规则中,使用
as_view()方法,如下所示:# movies/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import patterns, url from .views import MovieListView urlpatterns = patterns("", url(r'^$', MovieListView.as_view(), name="movie_list"), ) -
我们的基于类的视图
MovieListView将继承 Django 的View类并重写get()和post()方法,这些方法用于区分 GET 和 POST 请求。我们还将添加get_queryset_and_facets()和get_page()方法,使类更加模块化:# movies/views.py # -*- coding: UTF-8 -*- from django.shortcuts import render from django.core.paginator import Paginator, EmptyPage,\ PageNotAnInteger from django.views.generic import View from .models import Genre from .models import Director from .models import Actor from .models import Movie, RATING_CHOICES from .forms import MovieFilterForm class MovieListView(View): form_class = MovieFilterForm template_name = "movies/movie_list.html" paginate_by = 15 def get(self, request, *args, **kwargs): form = self.form_class(data=request.GET) qs, facets = self.get_queryset_and_facets(form) page = self.get_page(request, qs) context = { "form": form, "facets": facets, "object_list": page, } return render(request, self.template_name, context) def post(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def get_queryset_and_facets(self, form): qs = Movie.objects.order_by("title") facets = { "selected": {}, "categories": { "genres": Genre.objects.all(), "directors": Director.objects.all(), "actors": Actor.objects.all(), "ratings": RATING_CHOICES, }, } if form.is_valid(): genre = form.cleaned_data["genre"] if genre: facets["selected"]["genre"] = genre qs = qs.filter(genres=genre).distinct() director = form.cleaned_data["director"] if director: facets["selected"]["director"] = director qs = qs.filter( directors=director, ).distinct() actor = form.cleaned_data["actor"] if actor: facets["selected"]["actor"] = actor qs = qs.filter(actors=actor).distinct() rating = form.cleaned_data["rating"] if rating: facets["selected"]["rating"] = ( int(rating), dict(RATING_CHOICES)[int(rating)] ) qs = qs.filter(rating=rating).distinct() return qs, facets def get_page(self, request, qs): paginator = Paginator(qs, self.paginate_by) page_number = request.GET.get("page") try: page = paginator.page(page_number) except PageNotAnInteger: # If page is not an integer, show first page. page = paginator.page(1) except EmptyPage: # If page is out of range, # show last existing page. page = paginator.page(paginator.num_pages) return page
它是如何工作的…
以下是在 get() 方法中发生的事情:
首先,我们创建 form 对象,将其传递给 GET 字典样式的对象。GET 对象包含所有使用 GET 方法传递的查询变量。
然后,将 form 传递给 get_queryset_and_facets() 方法,该方法返回一个包含以下两个元素的元组:QuerySet 和 facets 字典。
然后,当前 request 对象和 QuerySet 被传递给 get_page() 方法,该方法返回当前页面对象。
最后,我们创建一个上下文字典并渲染响应。
还有更多…
如您所见,get()、post() 和 get_page() 方法是通用的,因此我们可以在 utils 应用程序中创建一个具有这些方法的通用 FilterableListView 类。然后,在需要可过滤列表的任何应用程序中,我们可以创建一个基于类的视图,该视图扩展 FilterableListView 并仅定义 form_class 和 template_name 属性以及 get_queryset_and_facets() 方法。这就是基于类的视图是如何工作的。
参见
-
过滤对象列表 菜谱
-
管理分页列表 菜谱
生成 PDF 文档
Django 视图允许您创建不仅仅是 HTML 页面。您可以生成任何类型的文件。例如,您可以创建用于发票、票务、预订确认等的 PDF 文档。在这个菜谱中,我们将向您展示如何从数据库中的数据生成简历(履历)的 PDF 格式。我们将使用 Pisa xhtml2pdf 库,它非常实用,因为它允许您使用 HTML 模板来制作 PDF 文档。
准备工作
首先,我们需要在您的虚拟环境中安装 xhtml2pdf Python 库:
(myproject_env)$ pip install xhtml2pdf
然后,让我们创建一个包含一个简单的 CV 模型以及通过外键附加的 Experience 模型的 cv 应用程序。CV 模型将包含以下字段:名字、姓氏和电子邮件。Experience 模型将包含以下字段:工作的开始日期、工作的结束日期、公司、在该公司的职位以及获得的技能:
# cv/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class CV(models.Model):
first_name = models.CharField(_("First name"), max_length=40)
last_name = models.CharField(_("Last name"), max_length=40)
email = models.EmailField(_("Email"))
def __str__(self):
return self.first_name + " " + self.last_name
@python_2_unicode_compatible
class Experience(models.Model):
cv = models.ForeignKey(CV)
from_date = models.DateField(_("From"))
till_date = models.DateField(_("Till"), null=True, blank=True)
company = models.CharField(_("Company"), max_length=100)
position = models.CharField(_("Position"), max_length=100)
skills = models.TextField(_("Skills gained"), blank=True)
def __str__(self):
till = _("present")
if self.till_date:
till = self.till_date.strftime("%m/%Y")
return _("%(from)s-%(till)s %(pos)s at %(company)s") % {
"from": self.from_date.strftime("%m/%Y"),
"till": till,
"pos": self.position,
"company": self.company,
}
class Meta:
ordering = ("-from_date",)
如何操作…
执行以下步骤以完成菜谱:
-
在 URL 规则中,让我们为以下视图创建一个规则,该规则将根据
CV模型的 ID 下载简历的 PDF 文档:# cv/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import patterns, url urlpatterns = patterns('cv.views', url(r'^(?P<cv_id>\d+)/pdf/$', "download_cv_pdf", name="download_cv_pdf"), ) -
现在,让我们创建
download_cv_pdf()视图。这个视图渲染一个 HTML 模板,然后将渲染后的字符串传递给pisaDocumentPDF 创建器:# cv/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals try: from cStringIO import StringIO except ImportError: from StringIO import StringIO from xhtml2pdf import pisa from django.conf import settings from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.http import HttpResponse from .models import CV def download_cv_pdf(request, cv_id): cv = get_object_or_404(CV, pk=cv_id) response = HttpResponse(content_type="application/pdf") response["Content-Disposition"] = "attachment; "\ "filename=%s_%s.pdf" % ( cv.first_name, cv.last_name ) html = render_to_string("cv/cv_pdf.html", { "cv": cv, "MEDIA_ROOT": settings.MEDIA_ROOT, "STATIC_ROOT": settings.STATIC_ROOT, }) pdf = pisa.pisaDocument( StringIO(html.encode("UTF-8")), response, encoding="UTF-8", ) return response -
最后,我们将创建用于渲染文档的模板,如下所示:
{# templates/cv/cv_pdf.html #} <!DOCTYPE HTML> <html> <head> <meta charset="utf-8" /> <title>My Title</title> <style type="text/css"> @page { size: "A4"; margin: 2.5cm 1.5cm 2.5cm 1.5cm; @frame footer { -pdf-frame-content: footerContent; bottom: 0cm; margin-left: 0cm; margin-right: 0cm; height: 1cm; } } #footerContent { color: #666; font-size: 10pt; text-align: center; } /* … Other CSS Rules go here … */ </style> </head> <body> <div> <h1>Curriculum Vitae</h1> <table> <tr> <td><p><b>{{ cv.first_name }} {{ cv.last_name }}</b><br /> Contact: {{ cv.email }}</p> </td> <td align="right"> <img src="img/smiley.jpg" width="100" height="100" /> </td> </tr> </table> <h2>Experience</h2> <table> {% for experience in cv.experience_set.all %} <tr> <td valign="top"><p>{{ experience.from_date|date:"F Y" }} - {% if experience.till_date %} {{ experience.till_date|date:"F Y" }} {% else %} present {% endif %}<br /> {{ experience.position }} at {{ experience.company }}</p> </td> <td valign="top"><p><b>Skills gained</b><br> {{ experience.skills|linebreaksbr }} <br> <br> </p> </td> </tr> {% endfor %} </table> </div> <pdf:nextpage> <div> This is an empty page to make a paper plane. </div> <div id="footerContent"> Document generated at {% now "Y-m-d" %} | Page <pdf:pagenumber> of <pdf:pagecount> </div> </body> </html>
它是如何工作的…
前往模型管理并输入一份 CV 文档。然后,如果您访问文档的 URL http://127.0.0.1:8000/en/cv/1/pdf/,您将被要求下载一个看起来类似于以下内容的 PDF 文档:

视图是如何工作的呢?首先,我们通过 ID 加载一份简历,如果存在,否则抛出页面未找到错误。然后,我们创建响应对象,并设置 PDF 文档的内容类型。我们将Content-Disposition头设置为attachment,并指定文件名。这将强制浏览器打开一个对话框,提示我们保存 PDF 文档,并建议指定文件名。然后,我们将 HTML 模板作为字符串渲染,传递简历对象和MEDIA_ROOT以及STATIC_ROOT路径。
注意
注意,用于 PDF 创建的<img>标签的src属性需要指向文件系统中的文件或在线图像的完整 URL。Pisa xhtml2pdf 将下载图像并将其包含在 PDF 文档中。
然后,我们创建一个pisaDocument文件,以 UTF-8 编码的 HTML 作为源文件,以文件对象作为目标。响应对象是一个类似文件的对象,pisaDocument将文档内容写入其中。响应对象按预期由视图返回。
让我们看看用于创建此文档的 HTML 模板。该模板包含一些不寻常的标记标签和 CSS 规则。如果我们想在文档的每一页上放置一些元素,我们可以为这些元素创建 CSS 框架。在前面的示例中,带有footerContent ID 的<div>标签被标记为框架,它将在每一页的底部重复。以类似的方式,我们还可以为每一页设置页眉或背景图像。
以下是在此文档中使用的特定标记标签:
-
<pdf:nextpage>标签设置手动分页符 -
<pdf:pagenumber>标签返回当前页码 -
<pdf:pagecount>标签返回总页数
Pisa xhtml2pdf 库的当前版本 0.0.6 并不完全支持所有 HTML 标签和 CSS 规则。没有公开可访问的基准来查看具体支持哪些标签和规则以及支持的水平。因此,您需要通过实验来使 PDF 文档看起来符合设计要求。然而,这个库仍然足够强大,可以用于自定义布局,这基本上只需要 HTML 和 CSS 的知识就可以创建。
参见
-
管理分页列表配方
-
下载授权文件配方
使用 Haystack 实现多语言搜索
内容驱动网站的主要功能之一是全文搜索。Haystack 是一个模块化搜索 API,支持 Solr、Elasticsearch、Whoosh 和 Xapian 搜索引擎。对于您项目中需要可搜索的每个模型,您需要定义一个索引,该索引将从模型中读取文本信息并将其放置到后端。在此配方中,您将学习如何使用基于 Python 的 Whoosh 搜索引擎和 Haystack 设置一个多语言网站的搜索。
准备工作
在开始时,让我们创建一些包含将被索引的模型的几个应用。让我们创建一个包含 Category 和 Idea 模型的 ideas 应用,如下所示:
# ideas/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.core.urlresolvers import NoReverseMatch
from django.utils.encoding import python_2_unicode_compatible
from utils.models import UrlMixin
from utils.fields import MultilingualCharField, MultilingualTextField
@python_2_unicode_compatible
class Category(models.Model):
title = MultilingualCharField(_("Title"), max_length=200)
class Meta:
verbose_name = _("Idea Category")
verbose_name_plural = _("Idea Categories")
def __str__(self):
return self.title
@python_2_unicode_compatible
class Idea(UrlMixin):
title = MultilingualCharField(_("Title"), max_length=200)
subtitle = MultilingualCharField(_("Subtitle"), max_length=200, blank=True)
description = MultilingualTextField(_("Description"),
blank=True)
is_original = models.BooleanField(_("Original"))
categories = models.ManyToManyField(Category,
verbose_name=_("Categories"), blank=True,
related_name="ideas")
class Meta:
verbose_name = _("Idea")
verbose_name_plural = _("Ideas")
def __str__(self):
return self.title
def get_url_path(self):
try:
return reverse("idea_detail", kwargs={"id": self.pk})
except NoReverseMatch:
return ""
Idea 模型具有多语言字段,这意味着应该为每种语言的内容提供翻译。
另一个应用将是来自 上传图片 菜单的 quotes,其中包含 InspirationalQuote 模型,每个引言可以只使用在 settings.LANGUAGES 中定义的任何一种语言,并且每个引言不一定有翻译:
# quotes/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import os
from django.db import models
from django.utils.timezone import now as timezone_now
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.urlresolvers import NoReverseMatch
from utils.models import UrlMixin
def upload_to(instance, filename):
now = timezone_now()
filename_base, filename_ext = os.path.splitext(filename)
return 'quotes/%s%s' % (
now.strftime("%Y/%m/%Y%m%d%H%M%S"),
filename_ext.lower(),
)
@python_2_unicode_compatible
class InspirationalQuote(UrlMixin):
author = models.CharField(_("Author"), max_length=200)
quote = models.TextField(_("Quote"))
picture = models.ImageField(_("Picture"), upload_to=upload_to,
blank=True, null=True)
language = models.CharField(_("Language"), max_length=2,
blank=True, choices=settings.LANGUAGES)
class Meta:
verbose_name = _("Inspirational Quote")
verbose_name_plural = _("Inspirational Quotes")
def __str__(self):
return self.quote
def get_url_path(self):
try:
return reverse("quote_detail", kwargs={"id": self.pk})
except NoReverseMatch:
return ""
# …
def title(self):
return self.quote
将这两个应用放入设置中的 INSTALLED_APPS,创建并应用数据库迁移,并为这些模型创建模型管理以添加一些数据。此外,为这些模型创建列表和详情视图,并将它们连接到 URL 规则。如果你在这些任务中遇到任何困难,请再次熟悉官方 Django 教程中的概念:docs.djangoproject.com/en/1.8/intro/tutorial01/。
确保你在虚拟环境中安装了 django-haystack、whoosh 和 django-crispy-forms:
(myproject_env)$ pip install django-crispy-forms
(myproject_env)$ pip install django-haystack
(myproject_env)$ pip install whoosh
如何做到这一点...
让我们通过以下步骤使用 Haystack 和 Whoosh 设置多语言搜索:
-
创建一个包含
MultilingualWhooshEngine和我们想法和引言的搜索索引的search应用。搜索引擎将位于multilingual_whoosh_backend.py文件中:# search/multilingual_whoosh_backend.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf import settings from django.utils import translation from haystack.backends.whoosh_backend import \ WhooshSearchBackend, WhooshSearchQuery, WhooshEngine from haystack import connections from haystack.constants import DEFAULT_ALIAS class MultilingualWhooshSearchBackend(WhooshSearchBackend): def update(self, index, iterable, commit=True, language_specific=False): if not language_specific and \ self.connection_alias == "default": current_language = (translation.get_language() or settings.LANGUAGE_CODE)[:2] for lang_code, lang_name in settings.LANGUAGES: using = "default_%s" % lang_code translation.activate(lang_code) backend = connections[using].get_backend() backend.update(index, iterable, commit, language_specific=True) translation.activate(current_language) elif language_specific: super(MultilingualWhooshSearchBackend, self).\ update(index, iterable, commit) class MultilingualWhooshSearchQuery(WhooshSearchQuery): def __init__(self, using=DEFAULT_ALIAS): lang_code = translation.get_language()[:2] using = "default_%s" % lang_code super(MultilingualWhooshSearchQuery, self).\ __init__(using) class MultilingualWhooshEngine(WhooshEngine): backend = MultilingualWhooshSearchBackend query = MultilingualWhooshSearchQuery -
然后,让我们按照以下步骤创建搜索索引:
# search/search_indexes.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf import settings from django.utils.translation import get_language from haystack import indexes from ideas.models import Idea from quotes.models import InspirationalQuote class IdeaIndex(indexes.SearchIndex, indexes.Indexable): text = indexes.CharField(document=True) def get_model(self): return Idea def index_queryset(self, using=None): """Used when the entire index for model is updated.""" return self.get_model().objects.all() def prepare_text(self, obj): # this will be called for each language / backend return "\n".join(( obj.title, obj.subtitle, obj.description, "\n".join([cat.title for cat in obj.categories.all() ]), )) class InspirationalQuoteIndex(indexes.SearchIndex, indexes.Indexable): text = indexes.CharField(document=True) def get_model(self): return InspirationalQuote def index_queryset(self, using=None): """Used when the entire index for model is updated.""" if using and using != "default": lang_code = using.replace("default_", "") else: lang_code = settings.LANGUAGE_CODE[:2] return self.get_model().objects.filter(language=lang_code) def prepare_text(self, obj): # this will be called for each language / backend return "\n".join(( obj.author, obj.quote, )) -
之后,配置设置以使用我们的
MultilingualWhooshEngine:INSTALLED_APPS = ( # … # third party "crispy_forms", "haystack", # project-specific "quotes", "utils", "ideas", "search", ) LANGUAGE_CODE = "en" LANGUAGES = ( ("en", "English"), ("de", "Deutsch"), ("fr", "Français"), ("lt", "Lietuvių kalba"), ) CRISPY_TEMPLATE_PACK = "bootstrap3" HAYSTACK_CONNECTIONS = { "default": { "ENGINE": "search.multilingual_whoosh_backend."\ "MultilingualWhooshEngine", "PATH": os.path.join(PROJECT_PATH, "myproject", "tmp", "whoosh_index_en"), }, "default_en": { "ENGINE": "search.multilingual_whoosh_backend."\ "MultilingualWhooshEngine", "PATH": os.path.join(PROJECT_PATH, "myproject", "tmp", "whoosh_index_en"), }, "default_de": { "ENGINE": "search.multilingual_whoosh_backend."\ "MultilingualWhooshEngine", "PATH": os.path.join(PROJECT_PATH, "myproject", "tmp", "whoosh_index_de"), }, "default_fr": { "ENGINE": "search.multilingual_whoosh_backend."\ "MultilingualWhooshEngine", "PATH": os.path.join(PROJECT_PATH, "myproject", "tmp", "whoosh_index_fr"), }, "default_lt": { "ENGINE": "search.multilingual_whoosh_backend."\ "MultilingualWhooshEngine", "PATH": os.path.join(PROJECT_PATH, "myproject", "tmp", "whoosh_index_lt"), }, } -
现在,我们需要定义搜索视图的 URL 规则:
# myproject/urls.py # -*- coding: UTF-8 -*- from django.conf.urls import patterns, include, url from django.core.urlresolvers import reverse_lazy from django.utils.translation import string_concat from django.utils.translation import ugettext_lazy as _ from django.conf.urls.i18n import i18n_patterns from crispy_forms.helper import FormHelper from crispy_forms import layout, bootstrap from haystack.views import SearchView class CrispySearchView(SearchView): def extra_context(self): helper = FormHelper() helper.form_tag = False helper.disable_csrf = True return {"search_helper": helper} urlpatterns = i18n_patterns('', # … url(r'^search/$', CrispySearchView(), name='haystack_search'), # … ) -
然后,以下是搜索表单和搜索结果的模板,如下所示:
{# templates/search/search.html #} {% extends "base.html" %} {% load i18n crispy_forms_tags utility_tags %} {% block content %} <h2>{% trans "Search" %}</h2> <form method="get" action="{{ request.path }}"> <div class="well clearfix"> {% crispy form search_helper %} <p class="pull-right"> <input class="btn btn-primary" type="submit" value="Search"> </p> </div> </form> {% if query %} <h3>{% trans "Results" %}</h3> {% for result in page.object_list %} <p> <a href="{{ result.object.get_url_path }}"> {{ result.object.title }} </a> </p> {% empty %} <p>{% trans "No results found." %}</p> {% endfor %} {% if page.has_previous or page.has_next %} <nav> <ul class="pager"> <li class="previous"> {% if page.has_previous %}<a href="{% modify_query page=page.previous_page_number %}">{% endif %} <span aria-hidden="true">«</span> {% if page.has_previous %}</a>{% endif %} </li> <li class="next"> {% if page.has_next %}<a href="{% modify_query page=page.next_page_number %}">{% endif %} <span aria-hidden="true">»</span> {% if page.has_next %}</a>{% endif %} </li> </ul> </nav> {% endif %} {% endif %} {% endblock %} -
调用
rebuild_index管理命令以索引数据库数据并准备全文搜索:(myproject_env)$ python manage.py rebuild_index --noinput
如何工作…
MultilingualWhooshEngine 指定了两个自定义属性:后端和查询。自定义的 MultilingualWhooshSearchBackend 后端确保对于每种语言,项目将只在该语言中索引,并放置在 HAYSTACK_CONNECTIONS 设置中定义的特定 Haystack 索引位置下。自定义的 MultilingualWhooshSearchQuery 查询确保在搜索关键字时,将使用当前语言的特定 Haystack 连接。
每个索引都有一个 text 字段,其中将存储模型特定语言的全文。索引的模型由 get_model() 方法定义,要索引的 QuerySet 由 index_queryset() 方法定义,要搜索的文本在 prepare_text() 方法中收集。
由于我们想要一个漂亮的 Bootstrap 3 表单,我们将通过覆盖 SearchView 的 extra_context() 方法将 FormHelper 从 django-crispy-forms 传递给搜索视图。我们可以这样做,如下所示:

定期更新搜索索引的最简单方法是通过 cron 作业每晚调用 rebuild_index 管理命令。要了解相关信息,请查看第十一章中的 设置 cron 作业以执行常规任务 菜谱,测试和部署。
参见
-
使用 django-crispy-forms 创建表单布局 菜谱
-
下载授权文件 菜谱
-
第十一章中的 设置 cron 作业以执行常规任务 菜谱,测试和部署
第四章:模板和 JavaScript
在本章中,我们将涵盖以下主题:
-
安排
base.html模板 -
包含 JavaScript 设置
-
使用 HTML5 数据属性
-
在模态对话框中打开对象详情
-
实现连续滚动
-
实现点赞小部件
-
通过 Ajax 上传图片
简介
我们生活在一个 Web2.0 的世界,在这个世界里,社交网络应用和智能网站通过 Ajax 在服务器和客户端之间进行通信,只有当上下文发生变化时才会刷新整个页面。在本章中,你将学习处理模板中 JavaScript 的最佳实践,以创建丰富的用户体验。对于响应式布局,我们将使用 Bootstrap 3 前端框架。对于高效的脚本编写,我们将使用 jQuery JavaScript 框架。
安排base.html模板
当你开始处理模板时,首先要做的动作之一是创建base.html模板,这个模板将被项目中大多数页面模板扩展。在本食谱中,我们将演示如何创建这样一个模板,考虑到多语言 HTML5 网站的响应式设计。
小贴士
响应式网站是指能够适应设备视口的网站,无论访问者使用的是桌面浏览器、平板电脑还是手机。
准备工作
在你的项目中创建templates目录,并在设置中设置TEMPLATE_DIRS。
如何操作...
执行以下步骤:
-
在你的
templates根目录下,创建一个包含以下内容的base.html文件:{# templates/base.html #} <!DOCTYPE html> {% load i18n %} <html lang="{{ LANGUAGE_CODE }}"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>{% block title %}{% endblock %}{% trans "My Website" %}</title> <link rel="icon" href="{{ STATIC_URL }}site/img/favicon.ico" type="image/png" /> {% block meta_tags %}{% endblock %} {% block base_stylesheet %} <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" /> <link href="{{ STATIC_URL }}site/css/style.css" rel="stylesheet" media="screen" type="text/css" /> {% endblock %} {% block stylesheet %}{% endblock %} {% block base_js %} <script src="img/"></script> <script src="img/"></script> <script src="img/bootstrap.min.js"></script> <script src="img/{% url "js_settings" %}"></script> {% endblock %} {% block js %}{% endblock %} {% block extrahead %}{% endblock %} </head> <body class="{% block bodyclass %}{% endblock %}"> {% block page %} <section class="wrapper"> <header class="clearfix container"> <h1>{% trans "My Website" %}</h1> {% block header_navigation %} {% include "utils/header_navigation.html" %} {% endblock %} {% block language_chooser %} {% include "utils/language_chooser.html" %} {% endblock %} </header> <div id="content" class="clearfix container"> {% block content %} {% endblock %} </div> <footer class="clearfix container"> {% block footer_navigation %} {% include "utils/footer_navigation.html" %} {% endblock %} </footer> </section> {% endblock %} {% block extrabody %}{% endblock %} </body> </html> -
在同一目录下,创建另一个名为
base_simple.html的文件,用于特定情况,如下所示:{# templates/base_simple.html #} {% extends "base.html" %} {% block page %} <section class="wrapper"> <div id="content" class="clearfix"> {% block content %} {% endblock %} </div> </section> {% endblock %}
它是如何工作的...
基础模板包含了 HTML 文档的<head>和<body>部分,以及在每个网站页面上重复使用的所有详细信息。根据网页设计要求,你可以为不同的布局拥有额外的基础模板。例如,我们添加了base_simple.html文件,它具有相同的 HTML <head>部分和一个非常简约的<body>部分;它可以用于登录屏幕、密码重置或其他简单页面。你可以为单列、双列和三列布局拥有单独的基础模板,其中每个模板都扩展base.html并覆盖<body>部分的内容。
让我们来看看我们之前定义的base.html模板的细节。
在<head>部分,我们定义 UTF-8 为默认编码以支持多语言内容。然后,我们有 viewport 定义,它将在浏览器中缩放网站以使用全宽。这对于使用 Bootstrap 前端框架创建的特定屏幕布局的小屏幕设备是必要的。当然,有一个可定制的网站标题,favicon 将在浏览器标签中显示。我们为元标签、样式表、JavaScript 以及可能需要在<head>部分中使用的任何其他内容提供了可扩展的块。请注意,我们在模板中加载 Bootstrap CSS 和 JavaScript,因为我们希望所有元素都有响应式布局和基本坚固的预定义样式。然后,我们加载 JavaScript jQuery 库,该库高效且灵活,允许我们创建丰富的用户体验。我们还加载了从 Django 视图渲染的 JavaScript 设置。你将在下一个菜谱中了解这一点。
在<body>部分,我们有可覆盖的导航和语言选择器的页眉。我们还有内容块和页脚。在最底部,有一个可扩展的块用于额外的标记或 JavaScript。
我们创建的基本模板绝不是一种静态不可更改的模板。你可以添加你需要的内容,例如,Google Analytics 代码、常见的 JavaScript 文件、iPhone 书签的 Apple 触摸图标、Open Graph 元标签、Twitter Card 标签、schema.org 属性等等。
参见
- 包含 JavaScript 设置 菜谱
包含 JavaScript 设置
每个 Django 项目都在conf/base.py或settings.py设置文件中设置了其配置。其中一些配置值还需要在 JavaScript 中设置。由于我们希望在一个位置定义我们的项目设置,并且我们不希望在设置 JavaScript 值时重复这个过程,因此将一个动态生成的配置文件包含在基本模板中是一种良好的实践。在这个菜谱中,我们将看到如何做到这一点。
准备工作
确保你已经在TEMPLATE_CONTEXT_PROCESSORS设置中设置了媒体、静态和请求上下文处理器,如下所示:
# conf/base.py or settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.auth.context_processors.auth",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
"django.core.context_processors.media",
"django.core.context_processors.static",
"django.core.context_processors.tz",
"django.contrib.messages.context_processors.messages",
"django.core.context_processors.request",
)
此外,如果你还没有这样做,请创建utils应用并将其放置在设置中的INSTALLED_APPS下。
如何做到…
按照以下步骤创建和包含 JavaScript 设置:
-
创建一个 URL 规则来调用一个渲染 JavaScript 设置的视图,如下所示:
# urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import patterns, include, url from django.conf.urls.i18n import i18n_patterns urlpatterns = i18n_patterns("", # … url(r"^js-settings/$", "utils.views.render_js", {"template_name": "settings.js"}, name="js_settings", ), ) -
在你的
utils应用的视图中,创建一个render_js()视图,该视图返回 JavaScript 内容类型的响应,如下所示:# utils/views.py # -*- coding: utf-8 -*- from __future__ import unicode_literals from datetime import datetime, timedelta from django.shortcuts import render from django.views.decorators.cache import cache_control @cache_control(public=True) def render_js(request, cache=True, *args, **kwargs): response = render(request, *args, **kwargs) response["Content-Type"] = \ "application/javascript; charset=UTF-8" if cache: now = datetime.utcnow() response["Last-Modified"] = \ now.strftime("%a, %d %b %Y %H:%M:%S GMT") # cache in the browser for 1 month expires = now + timedelta(days=31) response["Expires"] = \ expires.strftime("%a, %d %b %Y %H:%M:%S GMT") else: response["Pragma"] = "No-Cache" return response -
创建一个
settings.js模板,该模板返回包含全局设置变量的 JavaScript,如下所示:# templates/settings.js window.settings = { MEDIA_URL: '{{ MEDIA_URL|escapejs }}', STATIC_URL: '{{ STATIC_URL|escapejs }}', lang: '{{ LANGUAGE_CODE|escapejs }}', languages: { {% for lang_code, lang_name in LANGUAGES %}'{{ lang_code|escapejs }}': '{{ lang_name|escapejs }}'{% if not forloop.last %},{% endif %} {% endfor %} } }; -
最后,如果你还没有这样做,请将渲染的 JavaScript 设置文件包含在基本模板中,如下所示:
# templates/base.html <script src="img/{% url "js_settings" %}"></script>
它是如何工作的…
Django 模板系统非常灵活;你不仅限于仅使用模板来创建 HTML。在这个例子中,我们将动态创建 JavaScript 文件。你可以在开发 Web 服务器http://127.0.0.1:8000/en/js-settings/中访问它,其内容将类似于以下内容:
window.settings = {
MEDIA_URL: '/media/',
STATIC_URL: '/static/20140424140000/',
lang: 'en',
languages: { 'en': 'English', 'de': 'Deutsch', 'fr': 'Français', 'lt': 'Lietuvi kalba' }
};
视图将在服务器和浏览器中都可缓存。
如果你想向 JavaScript 设置传递更多变量,可以创建一个自定义视图并将所有值传递到上下文,或者创建一个自定义上下文处理器并将所有值传递到那里。在后一种情况下,这些变量也将被访问到你的项目的所有模板中。例如,你可能在模板中有{{ is_mobile }}、{{ is_tablet }}和{{ is_desktop }}这样的指示器,用户代理字符串会告诉访问者是否使用移动、平板或桌面浏览器。
参见
-
安排 base.html 模板的食谱
-
使用 HTML5 数据属性的食谱
使用 HTML5 数据属性
当你与 DOM 元素相关的动态数据时,你需要一种更有效的方法来从 Django 传递值到 JavaScript。在这个食谱中,我们将看到一种将 Django 中的数据附加到自定义 HTML5 数据属性的方法,然后描述如何使用两个实际示例从 JavaScript 中读取数据。第一个示例将是一个根据视口改变源图片的图片,这样在移动设备上显示最小版本,在平板上显示中等大小的版本,在网站桌面版本上显示最大高质量的图片。第二个示例将是一个带有指定地理位置标记的谷歌地图。
准备工作
要开始,请执行以下步骤:
-
创建一个带有
Location模型的locations应用,该模型至少包含标题字符字段、用于 URL 的 slug 字段、small_image、medium_image和large_image图像字段,以及纬度和经度浮点字段。小贴士
术语slug来自报纸编辑,它意味着一个没有特殊字符的短字符串;只有字母、数字、下划线和连字符。Slugs 通常用于创建唯一的 URL。
-
为此模型创建一个管理界面并输入一个示例位置。
-
最后,为位置创建一个详细视图并设置其 URL 规则。
如何做到这一点…
执行以下步骤:
-
由于我们已经创建了应用,我们现在需要位置详情的模板:
{# templates/locations/location_detail.html #} {% extends "base.html" %} {% block content %} <h2>{{ location.title }}</h2> <img class="img-full-width" src="img/{{ location.small_image.url }}" data-small-src="img/{{ location.small_image.url }}" data-medium-src="img/{{ location.medium_image.url }}" data-large-src="img/{{ location.large_image.url }}" alt="{{ location.title|escape }}" /> <div id="map" data-latitude="{{ location.latitude|stringformat:"f" }}" data-longitude="{{ location.longitude|stringformat:"f" }}" ></div> {% endblock %} {% block extrabody %} <script src="img/js?v=3"></script> <script src="img/location_detail.js"></script> {% endblock %} -
除了模板,我们还需要一个 JavaScript 文件,该文件将读取 HTML5 数据属性并相应地使用它们,如下所示:
//site_static/site/js/location_detail.js jQuery(function($) { function show_best_images() { $('img.img-full-width').each(function() { var $img = $(this); if ($img.width() > 1024) { $img.attr('src', $img.data('large-src')); } else if ($img.width() > 468) { $img.attr('src', $img.data('medium-src')); } else { $img.attr('src', $img.data('small-src')); } }); } function show_map() { var $map = $('#map'); var latitude = parseFloat($map.data('latitude')); var longitude = parseFloat($map.data('longitude')); var latlng = new google.maps.LatLng(latitude, longitude); var map = new google.maps.Map($map.get(0), { zoom: 15, center: latlng }); var marker = new google.maps.Marker({ position: latlng, map: map }); }show_best_images();show_map(); $(window).on('resize', show_best_images); }); -
最后,我们需要设置一些 CSS,如下所示:
/* site_static/site/css/style.css */ img.img-full-width { width: 100%; } #map { height: 300px; }
它是如何工作的…
如果你在一个浏览器中打开你的位置详情视图,你将在大窗口中看到类似以下的内容:

如果你将浏览器窗口调整到 468 像素或更小,图片将变为其最小版本,如下所示:

让我们来看看代码。在模板中,我们有一个带有img-full-width CSS 类的图像标签,其默认源是最小的图像。这个image标签还具有data-small-src、data-medium-src和data-large-src自定义属性。在 JavaScript 中,当页面加载或窗口大小调整时调用show_best_images()函数。该函数遍历所有带有img-full-width CSS 类的图像,并根据当前图像宽度从自定义数据属性中设置适当图像源。
然后,在模板中有一个带有地图 ID 和data-latitude和data-longitude自定义属性的<div>元素。在 JavaScript 中,当页面加载时调用show_map()函数。这个函数将在<div>元素中创建一个 Google 地图。最初,读取并从字符串转换为浮点数的自定义属性。然后,创建一个LatLng对象,在接下来的步骤中,它将成为地图的中心和在此地图上显示的标记的地理位置。
参见
-
包含 JavaScript 设置 菜谱
-
在模态对话框中打开对象详情 菜谱
-
在第六章 插入地图到更改表单 菜谱中,模型管理
在模态对话框中打开对象详情
在这个菜谱中,我们将创建一个位置链接列表,当点击时,会打开一个包含有关位置信息和更多…链接的 Bootstrap 3 模态对话框(在这个菜谱中我们将称之为弹出窗口),该链接指向位置详情页面。对话框的内容将通过 Ajax 加载。对于没有 JavaScript 的访客,详情页面将立即打开,而不经过这个中间步骤。
准备工作
让我们从之前菜谱中创建的locations应用开始。
在urls.py文件中,我们将有三个 URL 规则;一个用于位置列表,另一个用于位置详情,第三个用于对话框,如下所示:
# locations/urls.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.conf.urls import patterns, url
urlpatterns = patterns("locations.views",
url(r"^$", "location_list", name="location_list"),
url(r"^(?P<slug>[^/]+)/$", "location_detail",
name="location_detail"),
url(r"^(?P<slug>[^/]+)/popup/$", "location_detail_popup",
name="location_detail_popup"),
)
因此,将会有三个简单的视图,如下所示:
# locations/views.py
from __future__ import unicode_literals
# -*- coding: UTF-8 -*-
from django.shortcuts import render, get_object_or_404
from .models import Location
def location_list(request):
location_list = Location.objects.all()
return render(request, "locations/location_list.html",
{"location_list": location_list})
def location_detail(request, slug):
location = get_object_or_404(Location, slug=slug)
return render(request, "locations/location_detail.html",
{"location": location})
def location_detail_popup(request, slug):
location = get_object_or_404(Location, slug=slug)
return render(request, "locations/location_detail_popup.html",
{"location": location})
如何做到这一点...
依次执行以下步骤:
-
为位置列表视图创建一个模板,其中包含一个隐藏的空模态对话框。每个列表位置都将有处理弹出信息的自定义 HTML5 数据属性,如下所示:
{# templates/locations/location_list.html #} {% extends "base.html" %} {% load i18n %} {% block content %} <h2>{% trans "Locations" %}</h2> <ul> {% for location in location_list %} <li class="item"> <a href="{% url "location_detail" slug=location.slug %}" data-popup-url="{% url "location_detail_popup" slug=location.slug %}" data-popup-title="{{ location.title|escape }}"> {{ location.title }} </a> </li> {% endfor %} </ul> {% endblock %} {% block extrabody %} <div id="popup" class="modal fade"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title">Modal title</h4> </div> <div class="modal-body"> </div> </div> </div> </div> <script src="img/location_list.js"></script> {% endblock %} -
我们需要 JavaScript 来处理对话框的打开和内容的动态加载:
// site_static/site/js/location_list.js jQuery(function($) { var $popup = $('#popup'); $('body').on('click', '.item a', function(e) { e.preventDefault(); var $link = $(this); var popup_url = $link.data('popup-url'); var popup_title = $link.data('popup-title'); if (!popup_url) { return true; } $('.modal-title', $popup).html(popup_title); $('.modal-body', $popup).load(popup_url, function() { $popup.on('shown.bs.modal', function () { // do something when dialog is shown }).modal("show"); }); $('.close', $popup).click(function() { // do something when dialog is closing }); }); }); -
最后,我们将创建一个模板,用于在模态对话框中加载的内容,如下所示:
{# templates/locations/location_detail_popup.html #} {% load i18n %} <p><img src="img/{{ location.small_image.url }}" alt="{{ location.title|escape }}" /></p> <p class="clearfix"> <a href="{% url "location_detail" slug=location.slug %}" class="btn btn-default pull-right"> {% trans "More" %} <span class="glyphicon glyphicon-chevron-right"></span> </a> </p>
如何工作…
如果我们在浏览器中转到位置的列表视图并点击其中一个位置,我们将看到一个类似于以下模态对话框:

这是如何工作的?在模板中,有一个具有item CSS 类的<div>元素和每个位置的链接。链接具有data-popup-url和data-popup-title自定义属性。在 JavaScript 中,当页面加载时,我们为<body>标签分配一个onclick处理程序。处理程序检查是否有任何具有item CSS 类的链接被点击。对于每个这样的点击链接,自定义属性被读取为popup_url和popup_title,新的标题被设置为隐藏的对话框框,内容使用 Ajax 加载到模态对话框中,然后显示给访客。
参见
-
使用 HTML5 数据属性教程
-
实现连续滚动教程
-
实现点赞小部件教程
实现连续滚动
社交网站通常具有连续滚动的功能,这也被称为无限滚动。有长长的项目列表,当你向下滚动页面时,新项目会自动加载并附加到底部。在本教程中,我们将看到如何使用 Django 和 jScroll jQuery 插件实现这种效果。我们将使用一个示例视图来展示来自互联网电影数据库(www.imdb.com/)的所有时间最顶部的 250 部电影。
准备工作
首先,从以下链接下载 jScroll 插件:github.com/pklauzinski/jscroll。
将包中的jquery.jscroll.js和jquery.jscroll.min.js文件放入myproject/site_static/site/js/目录。
接下来,对于本例,你将创建一个movies应用,该应用具有电影的分页列表视图。你可以创建一个Movie模型或包含电影数据的字典列表。每部电影都将有排名、标题、发行年份和评分字段。
如何做到这一点…
执行以下步骤以创建一个连续滚动的页面:
-
第一步是为列表视图创建一个模板,该模板还将显示指向下一页的链接,如下所示:
{# templates/movies/movie_list.html #} {% extends "base.html" %} {% load i18n utility_tags %} {% block content %} <h2>{% trans "Top Movies" %}</h2> <div class="object_list"> {% for movie in object_list %} <div class="item"> <p>{{ movie.rank }}. <strong>{{ movie.title }}</strong> ({{ movie.year }}) <span class="badge">{% trans "IMDB rating" %}: {{ movie.rating }}</span> </p> </div> {% endfor %} {% if object_list.has_next %} <p class="pagination"><a class="next_page" href="{% modify_query page=object_list.next_page_number %}">{% trans "More…" %}</a></p> {% endif %} </div> {% endblock %} {% block extrabody %} <script src="img/jquery.jscroll.min.js"></script> <script src="img/list.js"></script> {% endblock %} -
第二步是添加 JavaScript,如下所示:
// site_static/site/js/list.js jQuery(function($) { $('.object_list').jscroll({ loadingHtml: '<img src="img/loading.gif" alt="Loading" />', padding: 100, pagingSelector: '.pagination', nextSelector: 'a.next_page:last', contentSelector: '.item,.pagination' }); });
它是如何工作的…
当你在浏览器中打开电影列表视图时;页面上会显示预定义数量的项目,例如,25 个。当你向下滚动时,会加载并附加到项目容器中的额外 25 个项目和下一个分页链接。然后,加载并附加到项目底部的第三页,这个过程会一直持续到没有更多页面可以显示。
页面加载时,具有object_list CSS 类的<div>标签,包含项目和分页链接,将变成一个 jScroll 对象。以下参数定义了其功能:
-
loadingHtml:这将在新页面加载时在列表末尾显示一个动画加载指示器。 -
padding:这将定义当滚动位置和滚动区域末尾之间有 100 像素时,需要加载新页面。 -
pagingSelector:这个 CSS 选择器用于找到在启用 JavaScript 的浏览器中将被隐藏的 HTML 元素 -
nextSelector:这个 CSS 选择器用于找到将用于读取下一页 URL 的 HTML 元素 -
contentSelector:这个 CSS 选择器定义了要从加载的内容中提取并放入容器的 HTML 元素
参见
-
第三章中的管理分页列表配方,表单和视图
-
第三章中的编写基于类的视图配方,表单和视图
-
包括 JavaScript 设置配方
实现 Like 小部件
现在,社交网站通常已经集成了 Facebook、Twitter 和 Google+小部件来喜欢和分享页面。在这个配方中,我将指导您通过一个类似的内部喜欢 Django 应用,该应用将所有喜欢保存到您的数据库中,以便您可以根据您网站上喜欢的事物创建特定的视图。我们将创建一个具有两种状态按钮和显示总喜欢数量的徽章的 Like 小部件。以下是其状态:
-
非激活状态,您可以点击按钮来激活它:
![实现 Like 小部件]()
-
激活状态,您可以点击按钮来取消激活它:
![实现 Like 小部件]()
小部件的状态将由 Ajax 调用处理。
准备工作
首先,创建一个包含Like模型的likes应用,该模型与喜欢某物的用户具有外键关系,与数据库中的任何对象具有通用关系。我们将使用在第二章中定义的ObjectRelationMixin,该定义位于创建用于处理通用关系的模型混合配方中,数据库结构。如果您不想使用混合,也可以在以下模型中自己定义一个通用关系:
# likes/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.utils.encoding import python_2_unicode_compatible
from utils.models import CreationModificationDateMixin
from utils.models import object_relation_mixin_factory
@python_2_unicode_compatible
class Like(CreationModificationDateMixin,
object_relation_mixin_factory(is_required=True)):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
class Meta:
verbose_name = _("like")
verbose_name_plural = _("likes")
ordering = ("-created",)
def __str__(self):
return _(u"%(user)s likes %(obj)s") % {
"user": self.user,
"obj": self.content_object,
}
同时,确保请求上下文处理器已在设置中设置。我们还需要在设置中为当前登录用户附加请求的认证中间件:
# conf/base.py or settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
# …
"django.core.context_processors.request",
)
MIDDLEWARE_CLASSES = (
# …
"django.contrib.auth.middleware.AuthenticationMiddleware",
)
如何做到这一点…
依次执行以下步骤:
-
在
likes应用中,创建一个包含空__init__.py文件的templatetags目录,以便将其作为一个 Python 模块。然后,添加likes_tags.py文件,我们将在此文件中定义{% like_widget %}模板标签,如下所示:# likes/templatetags/likes_tags.py # -*- coding: UTF-8 -*- from django import template from django.contrib.contenttypes.models import ContentType from django.template import loader from likes.models import Like register = template.Library() ### TAGS ### @register.tag def like_widget(parser, token): try: tag_name, for_str, obj = token.split_contents() except ValueError: raise template.TemplateSyntaxError, \ "%r tag requires a following syntax: " \ "{%% %r for <object> %%}" % ( token.contents[0], token.contents[0]) return ObjectLikeWidget(obj) class ObjectLikeWidget(template.Node): def __init__(self, obj): self.obj = obj def render(self, context): obj = template.resolve_variable(self.obj, context) ct = ContentType.objects.get_for_model(obj) is_liked_by_user = bool(Like.objects.filter( user=context["request"].user, content_type=ct, object_id=obj.pk, )) context.push() context["object"] = obj context["content_type_id"] = ct.pk context["is_liked_by_user"] = is_liked_by_user context["count"] = get_likes_count(obj) output = loader.render_to_string( "likes/includes/like.html", context) context.pop() return output -
同时,我们将在同一文件中添加一个过滤器,以获取指定对象的喜欢数量:
### FILTERS ### @register.filter def get_likes_count(obj): ct = ContentType.objects.get_for_model(obj) return Like.objects.filter( content_type=ct, object_id=obj.pk, ).count() -
在 URL 规则中,我们需要一个用于视图的规则,该视图将使用 Ajax 处理喜欢和取消喜欢操作:
# likes/urls.py # -*- coding: UTF-8 -*- from django.conf.urls import patterns, url urlpatterns = patterns("likes.views", url(r"^(?P<content_type_id>[^/]+)/(?P<object_id>[^/]+)/$", "json_set_like", name="json_set_like"), ) -
然后,我们需要定义视图,如下所示:
# likes/views.py # -*- coding: UTF-8 -*- import json from django.http import HttpResponse from django.views.decorators.cache import never_cache from django.contrib.contenttypes.models import ContentType from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from .models import Like from .templatetags.likes_tags import get_likes_count @never_cache @csrf_exempt def json_set_like(request, content_type_id, object_id): """ Sets the object as a favorite for the current user """ result = { "success": False, } if request.user.is_authenticated() and \ request.method == "POST": content_type = ContentType.objects.get(id=content_type_id) obj = content_type.get_object_for_this_type(pk=object_id) like, is_created = Like.objects.get_or_create( content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, user=request.user, ) if not is_created: like.delete() result = { "success": True, "obj": unicode(obj), "action": is_created and "added" or "removed", "count": get_likes_count(obj), } json_str = json.dumps(result, ensure_ascii=False, encoding="utf8") return HttpResponse(json_str, mimetype="application/json; charset=utf-8") -
在任何对象的列表或详细视图的模板中,我们可以添加小部件的模板标签。让我们将小部件添加到之前配方中创建的位置详细中,如下所示:
{# templates/locations/location_detail.html #} {% extends "base.html" %} {% load likes_tags %} {% block content %} {% if request.user.is_authenticated %} {% like_widget for location %} {% endif %} {# the details of the object go here… #} {% endblock %} {% block extrabody %} <script src="img/likes.js"></script> {% endblock %} -
然后,我们需要为小部件创建一个模板,如下所示:
{# templates/likes/includes/like.html #} {% load i18n %} <div class="like-widget"> <button type="button" class="like-button btn btn-default {% if is_liked_by_user %} active{% endif %}" data-href="{% url "json_set_like" content_type_id=content_type_id object_id=object.pk %}" data-like-text="{% trans "Like" %}" data-unlike-text="{% trans "Unlike" %}" > {% if is_liked_by_user %} <span class="glyphicon glyphicon-star"></span> {% trans "Unlike" %} {% else %} <span class="glyphicon glyphicon-star-empty"></span> {% trans "Like" %} {% endif %} </button> <span class="like-badge badge">{{ count }}</span> </div> -
最后,我们创建 JavaScript 来处理浏览器中的喜欢和取消喜欢操作,如下所示:
// site_static/site/js/likes.js (function($) { $(document).on('click', '.like-button', function() { var $button = $(this); var $badge = $button.closest('.like-widget') .find('.like-badge'); $.post($button.data('href'), function(data) { if (data['action'] == 'added') { $button.addClass('active').html( '<span class="glyphicon glyphicon-star"></span> ' + $button.data('unlike-text') ); } else { $button.removeClass('active').html( '<span class="glyphicon glyphicon-star-empty"></span> ' + $button.data('like-text') ); } $badge.html(data['count']); }, 'json'); }); })(jQuery);
如何工作…
对于你网站中的任何对象,你可以放置 {% like_widget for object %} 模板标签,该标签将检查对象是否已被喜欢,并显示适当的状态。小部件模板中的 data-href、data-like-text 和 data-unlike-text 自定义 HTML5 属性。第一个属性持有唯一的对象特定 URL,用于更改小部件的当前状态。其他两个属性持有小部件的翻译文本。在 JavaScript 中,喜欢按钮通过喜欢按钮 CSS 类识别。文档上附加的点击事件监听器监视来自每个此类按钮的 onClick 事件,然后向由 data-href 属性指定的 URL 发送 Ajax 请求。指定的视图接受被喜欢对象的两个参数,即内容类型和对象 ID。视图检查指定对象的 Like 是否存在,如果存在,则视图将其删除;否则,添加 Like 对象。因此,视图返回一个包含成功状态、被喜欢对象的文本表示、操作(Like 对象是添加还是删除)以及总喜欢数的 JSON 响应。根据返回的操作,JavaScript 将显示按钮的适当状态。
你可以在 Chrome 开发者工具或 Firefox Firebug 插件中调试 Ajax 响应。如果在开发过程中出现任何服务器错误,你将在响应预览中看到错误跟踪,否则你将看到如下截图所示的返回 JSON:

相关内容
-
在模态对话框中打开对象详情 的菜谱
-
实现连续滚动 的菜谱
-
通过 Ajax 上传图像 的菜谱
-
第二章中的 创建用于处理通用关系的模型混入 菜谱,数据库结构
-
第五章,自定义模板过滤器和标签
通过 Ajax 上传图像
使用 Ajax 进行文件上传已成为网络上的事实标准。人们希望在选择文件后立即看到他们选择的内容,而不是在提交表单后看到。此外,如果表单有验证错误,没有人愿意再次选择文件;带有验证错误的文件仍然应该保留在表单中。
有一个第三方应用程序 django-ajax-uploader,可以用来使用 Ajax 上传图像。在这个菜谱中,我们将看到如何做到这一点。
准备工作
让我们从为 第三章中的 上传图像 菜谱创建的 quotes 应用程序开始。我们将重用模型和视图;然而,我们将创建不同的表单和模板,并添加 JavaScript。
使用以下命令在你的本地环境中安装 django-crispy-forms 和 django-ajax-uploader:
(myproject)$ pip install django-crispy-forms
(myproject)$ pip install ajaxuploader
不要忘记将这些应用程序放入 INSTALLED_APPS 中,如下所示:
# conf/base.py or settings.py
INSTALLED_APPS = (
# …
"quotes",
"crispy_forms",
"ajaxuploader",
)
如何做…
让我们按照以下步骤重新定义励志名言的表单:
-
首先,我们为 Bootstrap 3 标记创建一个布局。请注意,我们使用隐藏的
picture_path和delete_picture字段以及一些文件上传小部件的标记,而不是picture图像字段:# quotes/forms.py # -*- coding: UTF-8 -*- import os from django import forms from django.utils.translation import ugettext_lazy as _ from django.core.files import File from django.conf import settings from crispy_forms.helper import FormHelper from crispy_forms import layout, bootstrap from .models import InspirationQuote class InspirationQuoteForm(forms.ModelForm): picture_path = forms.CharField( max_length=255, widget=forms.HiddenInput(), required=False, ) delete_picture = forms.BooleanField( widget=forms.HiddenInput(), required=False, ) class Meta: model = InspirationQuote fields = ["author", "quote"] def __init__(self, *args, **kwargs): super(InspirationQuoteForm, self).\ __init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_action = "" self.helper.form_method = "POST" self.helper.layout = layout.Layout( layout.Fieldset( _("Quote"), layout.Field("author"), layout.Field("quote", rows=3), layout.HTML(""" {% include "quotes/includes/image_upload_widget.html" %} """), layout.Field("picture_path"), # hidden layout.Field("delete_picture"), # hidden ), bootstrap.FormActions( layout.Submit("submit", _("Save"), css_class="btn btn-primary"), ) ) -
然后,我们将覆盖保存方法以处理励志名言的保存,如下所示:
def save(self, commit=True): instance = super(InspirationQuoteForm, self).\ save(commit=True) if self.cleaned_data['delete_picture'] and \ instance.picture: instance.picture.delete() if self.cleaned_data['picture_path']: tmp_path = self.cleaned_data['picture_path'] abs_tmp_path = os.path.join( settings.MEDIA_ROOT, tmp_path) filename = InspirationQuote._meta.\ get_field('picture').upload_to( instance, tmp_path) instance.picture.save( filename, File(open(abs_tmp_path, "rb")), False ) os.remove(abs_tmp_path) instance.save() return instance -
除了在 quotes 应用中定义的先前视图外,我们添加了
ajax_uploader视图,该视图将处理使用 Ajax 的上传,如下所示:# quotes/views.py # … from ajaxuploader.views import AjaxFileUploader ajax_uploader = AjaxFileUploader() -
然后,我们为视图设置 URL 规则,如下所示:
# quotes/urls.py # -*- coding: UTF-8 -*- from django.conf.urls import patterns, url urlpatterns = patterns("", # … url(r"^ajax-upload/$", "quotes.views.ajax_uploader", name="ajax_uploader"), ) -
接下来,创建一个
image_upload_widget.html模板,该模板将被包含在 crispy 表单中:{# templates/quotes/includes/image_upload_widget.html #} {% load i18n %} <div id="image_upload_widget"> <div class="preview"> {% if instance.picture %} <img src="img/{{ instance.picture.url }}" alt="" /> {% endif %} </div> <div class="uploader"> <noscript> <p>{% trans "Please enable JavaScript to use file uploader." %}</p> </noscript> </div> <p class="help_text" class="help-block">{% trans "Available formats are JPG, GIF, and PNG." %}</p> <div class="messages"></div> </div> -
然后,是时候创建表单页面的模板了。在 extrabody 块中,我们将设置一个
translatable_file_uploader_options变量,该变量将处理文件上传的所有可翻译选项,例如小部件模板标记、错误消息和通知:{# templates/quotes/change_quote.html #} {% extends "base.html" %} {% load i18n crispy_forms_tags %} {% block stylesheet %} {{ block.super }} <link rel="stylesheet" href="{{ STATIC_URL }}ajaxuploader/css/fileuploader.css" /> {% endblock %} {% block content %} {% crispy form %} {% endblock %} {% block extrabody %} <script src="img/fileuploader.js"></script> <script> var translatable_file_uploader_options = { template: '<div class="qq-upload-drop-area"><span>{% trans "Drop image here" %}</span></div>' + '<div class="qq-uploader">' + '<div class="qq-upload-button btn"><span class="glyphicon glyphicon-upload"></span> {% trans "Upload Image" %}</div>' + ' <button class="btn btn-danger qq-delete-button"><span class="glyphicon glyphicon-trash"></span> {% trans "Delete" %}</button>' + '<ul class="qq-upload-list"></ul>' + '</div>', // template for one item in file list fileTemplate: '<li>' + '<span class="qq-upload-file"></span>' + '<span class="qq-upload-spinner"></span>' + '<span class="qq-upload-size"></span>' + '<a class="qq-upload-cancel" href="#">{% trans "Cancel" %}</a>' + '<span class="qq-upload-failed-text">{% trans "Failed" %}</span>' + '</li>', messages: { typeError: '{% trans "{file} has invalid extension. Only {extensions} are allowed." %}', sizeError: '{% trans "{file} is too large, maximum file size is {sizeLimit}." %}', minSizeError: '{% trans "{file} is too small, minimum file size is {minSizeLimit}." %}', emptyError: '{% trans "{file} is empty, please select files again without it." %}', filesLimitError: '{% trans "No more than {filesLimit} files are allowed to be uploaded." %}', onLeave: '{% trans "The files are being uploaded, if you leave now the upload will be cancelled." %}' } }; var ajax_uploader_path = '{% url "ajax_uploader" %}'; </script> <script src="img/change_quote.js"></script> {% endblock %} -
最后,我们创建一个 JavaScript 文件,该文件将初始化文件上传小部件并处理图片预览和删除,如下所示:
// site_static/site/js/change_quote.js $(function() { var csrfmiddlewaretoken = $('input[name="csrfmiddlewaretoken"]').val(); var $image_upload_widget = $('#image_upload_widget'); var current_image_path = $('#id_picture_path').val(); if (current_image_path) { $('.preview', $image_upload_widget).html( '<img src="img/' + window.settings.MEDIA_URL + current_image_path + '" alt="" />' ); } var options = $.extend(window.translatable_file_uploader_options, { allowedExtensions: ['jpg', 'jpeg', 'gif', 'png'], action: window.ajax_uploader_path, element: $('.uploader', $image_upload_widget)[0], multiple: false, onComplete: function(id, fileName, responseJSON) { if(responseJSON.success) { $('.messages', $image_upload_widget).html(""); // set the original to media_file_path $('#id_picture_path').val('uploads/' + fileName); // show preview link $('.preview', $image_upload_widget).html( '<img src="img/' + fileName + '" alt="" />' ); } }, onAllComplete: function(uploads) { // uploads is an array of maps // the maps look like this: {file: FileObject, response: JSONServerResponse} $('.qq-upload-success').fadeOut("slow", function() { $(this).remove(); }); }, params: { 'csrf_token': csrfmiddlewaretoken, 'csrf_name': 'csrfmiddlewaretoken', 'csrf_xname': 'X-CSRFToken' }, showMessage: function(message) { $('.messages', $image_upload_widget).html( '<div class="alert alert-danger">' + message + '</div>' ); } }); var uploader = new qq.FileUploader(options); $('.qq-delete-button', $image_upload_widget).click(function() { $('.messages', $image_upload_widget).html(""); $('.preview', $image_upload_widget).html(""); $('#id_delete_picture').val(1); return false; }); });
如何工作…
当在上传小部件中选择图片时,浏览器中的结果将类似于以下截图:

同一个表单可以用来创建励志名言和更改现有的励志名言。让我们深入了解这个过程,看看它是如何工作的。在表单中,我们有一个上传机制,由以下基本部分组成:
-
定义为具有预览 CSS 类的
<div>标签的图片预览区域。最初,如果我们处于对象更改视图并且InspirationQuote对象作为{{ instance }}传递给模板,它可能会显示图片。 -
用于 Ajax 上传小部件的区域,该小部件定义为具有
uploaderCSS 类的<div>标签。它将被动态创建的上传和删除按钮以及上传进度指示器填充。 -
上传的帮助文本。
-
定义为具有
messagesCSS 类的<div>标签的错误消息区域。 -
用于设置上传文件路径的隐藏
picture_path字符字段。 -
用于标记文件删除的隐藏
delete_picture布尔字段。
在页面加载时,JavaScript 将检查picture_path是否已设置;如果是,它将显示图片预览。只有在选择图片并提交表单的情况下才会发生这种情况;然而,存在验证错误。
此外,我们正在 JavaScript 中定义上传小部件的选项。这些选项由全局translatable_file_uploader_options变量与模板中设置的翻译字符串以及其他在 JavaScript 文件中设置的配置选项组合而成。Ajax 上传小部件使用这些选项进行初始化。一些重要的设置需要注意,包括onComplete回调,它会在上传图片时显示图片预览并填写picture_path字段,以及showMessage回调,它定义了如何在指定区域显示错误消息。
最后,还有一个 JavaScript 中的删除按钮处理程序,当点击时,将隐藏的delete_picture字段设置为1并移除预览图片。
Ajax 上传小部件动态创建一个包含文件上传字段和隐藏的<iframe>标签的表单,用于提交表单数据。当选择一个文件时,它立即上传到MEDIA_URL下的uploads目录,并将文件的路径设置为隐藏的picture_path字段。此目录是上传文件的临时位置。当用户提交灵感名言表单且输入有效时,会调用save()方法。如果delete_picture设置为1,则模型实例的图片将被删除。如果picture_path字段已定义,则从临时位置复制图像到最终目的地,并删除原始文件。
参见
-
第三章中的上传图片菜谱
-
在模态对话框中打开对象详情菜谱
-
实现连续滚动菜谱
-
实现点赞小部件菜谱
第五章:自定义模板过滤器和标签
在本章中,我们将涵盖以下主题:
-
遵循你自己的模板过滤器或标签的约定
-
创建一个模板过滤器以显示自文章发布以来过去了多少天
-
创建一个模板过滤器以提取第一个媒体对象
-
创建一个模板过滤器以使 URL 人性化
-
创建一个模板标签以包含一个模板(如果存在)
-
创建一个模板标签以在模板中加载 QuerySet
-
创建一个模板标签以将内容解析为模板
-
创建一个模板标签以修改请求查询参数
简介
如你所知,Django 有一个功能丰富的模板系统,具有模板继承、用于更改值表示的过滤器以及用于表现逻辑的标签等功能。此外,Django 允许你向你的应用程序添加自己的模板过滤器和标签。自定义过滤器或标签应位于应用程序templatetags Python 包下的模板标签库文件中。然后,你可以使用{% load %}模板标签在任何模板中加载你的模板标签库。在本章中,我们将创建几个有用的过滤器和标签,这将赋予模板编辑器更多的控制权。
要查看本章的模板标签在实际中的应用,请创建一个虚拟环境,将本章提供的代码提取到其中,运行开发服务器,并在浏览器中访问http://127.0.0.1:8000/en/。
遵循你自己的模板过滤器或标签的约定
如果你没有持续遵循的指导方针,自定义模板过滤器或标签可能会变得一团糟。模板过滤器或标签应该尽可能地为模板编辑器提供服务。它们应该既方便又灵活。在本食谱中,我们将探讨一些在增强 Django 模板系统功能时应遵循的约定。
如何做到这一点...
在扩展 Django 模板系统时,遵循以下约定:
-
当页面的逻辑更适合视图、上下文处理器或模型方法时,不要创建或使用自定义模板过滤器或标签。当你的内容是上下文特定的,例如对象列表或对象详情视图,请在视图中加载对象。如果你需要在每个页面上显示一些内容,请创建一个上下文处理器。当你需要获取与模板上下文无关的对象属性时,使用模型的自定义方法而不是模板过滤器。
-
使用
_tags后缀命名模板标签库。如果你的应用程序名称与你的模板标签库不同,你可以避免模糊的包导入问题。 -
在新创建的库中,将过滤器与标签分开,例如,使用以下代码中的注释进行说明:
# utils/templatetags/utility_tags.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import template register = template.Library() ### FILTERS ### # .. your filters go here.. ### TAGS ### # .. your tags go here.. -
在创建高级自定义模板标签时,确保它们的语法易于记忆,包括以下结构:
-
for [app_name.model_name]:包含此结构以使用特定的模型 -
using [template_name]:包含此结构以使用模板输出模板标签 -
limit [count]:包含此结构以将结果限制在特定数量 -
as [context_variable]:包含此结构以将结果保存到可以多次重用的上下文变量
-
-
尽量避免在模板标签中定义多个位置值,除非它们是自我解释的。否则,这可能会让模板开发者感到困惑。
-
尽可能多地创建可解析的参数。没有引号的单个字符串应被视为需要解析的上下文变量或提醒你模板标签组件结构的简短单词。
创建一个模板过滤器以显示自帖子发布以来已过去多少天
并非所有人都会跟踪日期,在谈论前沿信息的创建或修改日期时;对我们中的许多人来说,读取时间差更方便。例如,博客条目是三天前发布的,新闻文章是今天发布的,用户最后一次登录是昨天。在这个菜谱中,我们将创建一个名为 days_since 的模板过滤器,它将日期转换为人性化的时间差。
准备工作
如果你还没有这样做,请创建一个 utils 应用程序并将其放在设置中的 INSTALLED_APPS 下。然后,在这个应用程序中创建一个 templatetags Python 包(Python 包是包含空 __init__.py 文件的目录)。
如何操作...
创建一个包含以下内容的 utility_tags.py 文件:
# utils/templatetags/utility_tags.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from django import template
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import now as tz_now
register = template.Library()
### FILTERS ###
@register.filter
def days_since(value):
""" Returns number of days between today and value."""
today = tz_now().date()
if isinstance(value, datetime.datetime):
value = value.date()
diff = today - value
if diff.days > 1:
return _("%s days ago") % diff.days
elif diff.days == 1:
return _("yesterday")
elif diff.days == 0:
return _("today")
else:
# Date is in the future; return formatted date.
return value.strftime("%B %d, %Y")
它是如何工作的...
如果你像以下代码所示在模板中使用此过滤器,它将渲染类似 昨天 或 5 天前 的内容:
{% load utility_tags %}
{{ object.published|days_since }}
你可以将此过滤器应用于 date 和 datetime 类型的值。
每个模板标签库都有一个注册表,其中收集了过滤器和标签。Django 过滤器是由 @register.filter 装饰器注册的函数。默认情况下,模板系统中的过滤器将命名为与函数或其它可调用对象相同的名称。如果你想,你可以通过将名称传递给装饰器来为过滤器设置不同的名称,如下所示:
@register.filter(name="humanized_days_since")
def days_since(value):
...
过滤器本身相当直观。最初,读取当前日期。如果过滤器的给定值是 datetime 类型,则提取 date。然后,计算今天与提取值之间的差异。根据天数,返回不同的字符串结果。
更多...
此过滤器也很容易扩展以显示时间差异,例如 刚刚、7 分钟前 和 3 小时前。只需对 datetime 值而不是日期值进行操作。
参见
-
创建一个用于提取第一个媒体对象的模板过滤器 菜谱
-
创建一个用于人性化 URL 的模板过滤器 菜谱
创建一个用于提取第一个媒体对象的模板过滤器
假设你正在开发一个博客概览页面,并且对于每篇帖子,你想要在该页面上显示从内容中获取的图片、音乐或视频。在这种情况下,你需要从帖子的 HTML 内容中提取<figure>、<img>、<object>、<embed>、<video>、<audio>和<iframe>标签。在这个配方中,我们将看到如何使用正则表达式在first_media过滤器中执行此操作。
准备工作
我们将从utils应用开始,这个应用应该在设置中的INSTALLED_APPS中设置,并且在这个应用中设置templatetags包。
如何做到这一点...
在utility_tags.py文件中,添加以下内容:
# utils/templatetags/utility_tags.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import re
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
### FILTERS ###
media_tags_regex = re.compile(
r"<figure[\S\s]+?</figure>|"
r"<object[\S\s]+?</object>|"
r"<video[\S\s]+?</video>|"
r"<audio[\S\s]+?</audio>|"
r"<iframe[\S\s]+?</iframe>|"
r"<(img|embed)[^>]+>",
re.MULTILINE
)
@register.filter
def first_media(content):
""" Returns the first image or flash file from the html
content """
m = media_tags_regex.search(content)
media_tag = ""
if m:
media_tag = m.group()
return mark_safe(media_tag)
它是如何工作的...
如果数据库中的 HTML 内容有效,当你将以下代码放入模板中时,它将从对象的content字段中检索媒体标签;如果没有找到媒体,将返回一个空字符串。
{% load utility_tags %}
{{ object.content|first_media }}
正则表达式是搜索/替换文本模式的一个强大功能。首先,我们将定义编译后的正则表达式为media_file_regex。在我们的例子中,我们将搜索所有可能出现在多行中的媒体标签。
小贴士
Python 字符串可以不使用加号(+)进行连接。
让我们看看这个正则表达式是如何工作的,如下所示:
-
交替模式由竖线(
|)符号分隔。 -
对于可能的多行标签,我们将使用
[\S\s]+?模式,该模式至少匹配一次任何符号;然而,尽可能少地匹配,直到我们找到它后面的字符串。因此,<figure[\S\s]+?</figure>搜索一个<figure>标签以及它之后的所有内容,直到找到关闭的</figure>标签。 -
类似地,使用
[^>]+模式,我们搜索至少一次且尽可能多次的任何符号,除了大于(>)符号。
re.MULTILINE标志确保搜索将在多行中发生。然后,在过滤器中,我们将对这个正则表达式模式进行搜索。默认情况下,过滤器的结果将显示为<、>和&符号,它们被转义为<、>和&实体。然而,我们使用mark_safe()函数将结果标记为安全且 HTML 就绪,以便在模板中显示而不进行转义。
更多...
如果你对正则表达式感兴趣,你可以在官方 Python 文档中了解更多信息,链接为docs.python.org/2/library/re.html。
参见
-
创建一个用于显示自帖子发布以来已过去多少天的模板过滤器配方
-
创建一个用于使 URL 人性化的模板过滤器配方
创建一个用于使 URL 人性化的模板过滤器
通常,普通网络用户在地址字段中输入 URL 时没有协议和尾随斜杠。在这个配方中,我们将创建一个humanize_url过滤器,用于以更短的形式向用户展示 URL,截断非常长的地址,类似于 Twitter 对推文中的链接所做的那样。
准备工作
与之前的食谱类似,我们将从 utils 应用程序开始,该应用程序应在设置中的 INSTALLED_APPS 中设置,并包含 templatetags 包。
如何操作...
在 utils 应用中的 utility_tags.py 模板库的 FILTERS 部分,让我们添加一个 humanize_url 过滤器并将其注册,如下面的代码所示:
# utils/templatetags/utility_tags.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import re
from django import template
register = template.Library()
### FILTERS ###
@register.filter
def humanize_url(url, letter_count):
""" Returns a shortened human-readable URL """
letter_count = int(letter_count)
re_start = re.compile(r"^https?://")
re_end = re.compile(r"/$")
url = re_end.sub("", re_start.sub("", url))
if len(url) > letter_count:
url = "%s…" % url[:letter_count - 1]
return url
它是如何工作的...
我们可以在任何模板中使用 humanize_url 过滤器,如下所示:
{% load utility_tags %}
<a href="{{ object.website }}" target="_blank">
{{ object.website|humanize_url:30 }}
</a>
该过滤器使用正则表达式删除前缀协议和尾随斜杠,将 URL 缩短到指定的字母数,如果 URL 不适合指定的字母数,则在末尾添加省略号。
参见
-
创建一个模板过滤器以显示自帖子发布以来已过去多少天 的食谱
-
创建一个模板过滤器以提取第一个媒体对象 的食谱
-
创建一个模板标签以包含存在的模板 的食谱
创建一个模板标签以包含存在的模板
Django 有 {% include %} 模板标签,它渲染并包含另一个模板。然而,在某些情况下存在一个问题,如果模板不存在,则会引发错误。在这个食谱中,我们将看到如何创建一个 {% try_to_include %} 模板标签,该标签包含另一个模板,如果不存在该模板,则静默失败。
准备工作
我们将再次从已安装并准备好自定义模板标签的 utils 应用程序开始。
如何操作...
高级自定义模板标签由两部分组成:解析模板标签参数的函数以及负责模板标签逻辑和输出的 Node 类。按照以下步骤创建 {% try_to_include %} 模板标签:
-
首先,让我们创建一个解析模板标签参数的函数,如下所示:
# utils/templatetags/utility_tags.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import template from django.template.loader import get_template register = template.Library() ### TAGS ### @register.tag def try_to_include(parser, token): """Usage: {% try_to_include "sometemplate.html" %} This will fail silently if the template doesn't exist. If it does exist, it will be rendered with the current context.""" try: tag_name, template_name = token.split_contents() except ValueError: raise template.TemplateSyntaxError, \ "%r tag requires a single argument" % \ token.contents.split()[0] return IncludeNode(template_name) -
然后,我们需要在同一个文件中的
Node类,如下所示:class IncludeNode(template.Node): def __init__(self, template_name): self.template_name = template_name def render(self, context): try: # Loading the template and rendering it template_name = template.resolve_variable( self. template_name, context) included_template = get_template( template_name ).render(context) except template.TemplateDoesNotExist: included_template = "" return included_template
它是如何工作的...
{% try_to_include %} 模板标签期望一个参数,即 template_name。因此,在 try_to_include() 函数中,我们尝试将标记的分隔内容仅分配给 tag_name 变量(即 try_to_include)和 template_name 变量。如果这不起作用,则引发模板语法错误。该函数返回 IncludeNode 对象,该对象获取 template_name 字段以供以后使用。
在 IncludeNode 的 render() 方法中,我们解析 template_name 变量。如果向模板标签传递了上下文变量,则其值将在这里用于 template_name。如果向模板标签传递了引号字符串,则引号内的内容将用于 template_name。
最后,我们将尝试加载模板并使用当前模板上下文进行渲染。如果不起作用,则返回空字符串。
至少有两种情况我们可以使用这个模板标签:
-
当在模型中定义路径时包含模板,如下所示:
{% load utility_tags %} {% try_to_include object.template_path %} -
它用于在模板上下文变量作用域中定义路径的模板中包含模板时。这在需要为 Django CMS 中的占位符创建自定义布局的插件时特别有用:
{# templates/cms/start_page.html #} {% with editorial_content_template_path="cms/plugins/editorial_content/start_page.html" %} {% placeholder "main_content" %} {% endwith %} {# templates/cms/plugins/editorial_content.html #} {% load utility_tags %} {% if editorial_content_template_path %} {% try_to_include editorial_content_template_path %} {% else %} <div> <!-- Some default presentation of editorial content plugin --> </div> {% endif %}
更多...
你可以使用{% try_to_include %}标签以及默认的{% include %}标签来包含扩展其他模板的模板。这对于大型门户来说是有益的,在这些门户中,你有不同种类的列表,其中复杂的项目与小部件具有相同的结构,但数据来源不同。
例如,在艺术家列表模板中,你可以包含艺术家项目模板,如下所示:
{% load utility_tags %}
{% for object in object_list %}
{% try_to_include "artists/includes/artist_item.html" %}
{% endfor %}
此模板将从项目基类扩展,如下所示:
{# templates/artists/includes/artist_item.html #}
{% extends "utils/includes/item_base.html" %}
{% block item_title %}
{{ object.first_name }} {{ object.last_name }}
{% endblock %}
项目基类定义了任何项目的标记,并包括一个 Like 小部件,如下所示:
{# templates/utils/includes/item_base.html #}
{% load likes_tags %}
<h3>{% block item_title %}{% endblock %}</h3>
{% if request.user.is_authenticated %}
{% like_widget for object %}
{% endif %}
参见
-
在第七章的为 Django CMS 创建模板菜谱中,Django CMS
-
在第七章的编写自己的 CMS 插件菜谱中,Django CMS
-
在第四章的实现 Like 小部件菜谱中,模板和 JavaScript
-
在模板中创建一个加载 QuerySet 的模板标签菜谱
-
创建一个将内容解析为模板的模板标签菜谱
-
创建一个修改请求查询参数的模板标签菜谱
在模板中创建一个加载 QuerySet 的模板标签
通常,应该在视图中定义要在网页上显示的内容。如果这是要在每个页面上显示的内容,那么创建一个上下文处理器是合理的。另一种情况是,你需要在某些页面上显示额外的内容,例如最新新闻或随机引言;例如,对象的起始页面或详细信息页面。在这种情况下,你可以使用{% get_objects %}模板标签加载必要的内容,我们将在本菜谱中实现它。
准备工作
再次,我们将从应该安装并准备好自定义模板标签的utils应用开始。
如何做到...
一个高级自定义模板标签由一个解析传递给标签的参数的函数和一个Node类组成,该类渲染标签的输出或修改模板上下文。执行以下步骤以创建{% get_objects %}模板标签:
-
首先,让我们创建一个解析模板标签参数的函数,如下所示:
# utils/templatetags/utility_tags.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db import models from django import template register = template.Library() ### TAGS ### @register.tag def get_objects(parser, token): """ Gets a queryset of objects of the model specified by app and model names Usage: {% get_objects [<manager>.]<method> from <app_name>.<model_name> [limit <amount>] as <var_name> %} Example: {% get_objects latest_published from people.Person limit 3 as people %} {% get_objects site_objects.all from news.Article limit 3 as articles %} {% get_objects site_objects.all from news.Article as articles %} """ amount = None try: tag_name, manager_method, str_from, appmodel, \ str_limit, amount, str_as, var_name = \ token.split_contents() except ValueError: try: tag_name, manager_method, str_from, appmodel, \ str_as, var_name = token.split_contents() except ValueError: raise template.TemplateSyntaxError, \ "get_objects tag requires a following "\ "syntax: "\ "{% get_objects [<manager>.]<method> "\ "from <app_ name>.<model_name> "\ "[limit <amount>] as <var_name> %}" try: app_name, model_name = appmodel.split(".") except ValueError: raise template.TemplateSyntaxError, \ "get_objects tag requires application name "\ "and model name separated by a dot" model = models.get_model(app_name, model_name) return ObjectsNode( model, manager_method, amount, var_name ) -
然后,我们将在同一文件中创建
Node类,如下面的代码所示:class ObjectsNode(template.Node): def __init__( self, model, manager_method, amount, var_name ): self.model = model self.manager_method = manager_method self.amount = amount self.var_name = var_name def render(self, context): if "." in self.manager_method: manager, method = \ self.manager_method.split(".") else: manager = "_default_manager" method = self.manager_method qs = getattr( getattr(self.model, manager), method, self.model._default_manager.none, )() if self.amount: amount = template.resolve_variable( self.amount, context ) context[self.var_name] = qs[:amount] else: context[self.var_name] = qs return ""
它是如何工作的...
{% get_objects %}模板标签从指定的应用和模型的方法中加载定义的 QuerySet,将结果限制到指定的数量,并将结果保存到上下文变量中。
以下代码是使用我们刚刚创建的模板标签的最简单示例。它将在任何模板中使用以下片段加载所有新闻文章:
{% load utility_tags %}
{% get_objects all from news.Article as all_articles %}
{% for article in all_articles %}
<a href="{{ article.get_url_path }}">{{ article.title }}</a>
{% endfor %}
这是在使用Article模型的默认objects管理器的all()方法,并且它将根据模型Meta类中定义的ordering属性对文章进行排序。
创建一个需要自定义管理器和自定义方法来从数据库查询对象的更高级的示例。管理器是一个提供数据库查询操作给模型的接口。每个模型默认至少有一个名为objects的管理器。作为一个例子,让我们创建一个具有草稿或发布状态以及允许选择随机发布艺术家的custom_manager的Artist模型:
# artists/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
STATUS_CHOICES = (
("draft", _("Draft"),
("published", _("Published"),
)
class ArtistManager(models.Manager):
def random_published(self):
return self.filter(status="published").order_by("?")
class Artist(models.Model):
# ...
status = models.CharField(_("Status"), max_length=20,
choices=STATUS_CHOICES)
custom_manager = ArtistManager()
要加载一个随机发布的艺术家,您可以将以下片段添加到任何模板中:
{% load utility_tags %}
{% get_objects custom_manager.random_published from artists.Artist limit 1 as random_artists %}
{% for artist in random_artists %}
{{ artist.first_name }} {{ artist.last_name }}
{% endfor %}
让我们看看{% get_objects %}模板标签的代码。在解析函数中,有两种预期的格式之一;带有限制和不带限制。字符串将被解析,模型将被识别,然后模板标签的组件传递给ObjectNode类。
在Node类的render()方法中,我们将检查管理器的名称及其方法名称。如果没有定义,将使用_default_manager,这是 Django 注入的任何模型的一个自动属性,指向第一个可用的models.Manager()实例。在大多数情况下,_default_manager将与objects相同。之后,我们将调用管理器的方法,如果方法不存在,则回退到空的QuerySet。如果定义了限制,我们将解析其值并限制QuerySet。最后,我们将QuerySet保存到上下文变量中。
参见
-
创建一个模板标签以包含一个模板(如果存在)的配方
-
创建一个模板标签以将内容解析为模板的配方
-
创建一个模板标签以修改请求查询参数的配方
创建一个模板标签以将内容解析为模板
在这个配方中,我们将创建一个{% parse %}模板标签,这将允许您将模板片段放入数据库。当您想为认证用户和非认证用户提供不同的内容,或者想包含个性化的问候语或不想在数据库中硬编码媒体路径时,这非常有用。
准备工作
如同往常,我们将从应该安装并准备好自定义模板标签的utils应用开始。
如何操作...
一个高级的自定义模板标签由一个解析传递给标签的参数的函数和一个渲染标签输出或修改模板上下文的Node类组成。按照以下步骤创建它们:
-
首先,让我们创建一个解析模板标签参数的函数,如下所示:
# utils/templatetags/utility_tags.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import template register = template.Library() ### TAGS ### @register.tag def parse(parser, token): """ Parses the value as a template and prints it or saves to a variable Usage: {% parse <template_value> [as <variable>] %} Examples: {% parse object.description %} {% parse header as header %} {% parse "{{ MEDIA_URL }}js/" as js_url %} """ bits = token.split_contents() tag_name = bits.pop(0) try: template_value = bits.pop(0) var_name = None if len(bits) == 2: bits.pop(0) # remove the word "as" var_name = bits.pop(0) except ValueError: raise template.TemplateSyntaxError, \ "parse tag requires a following syntax: "\ "{% parse <template_value> [as <variable>] %}" return ParseNode(template_value, var_name) -
然后,我们将在同一文件中创建
Node类,如下所示:class ParseNode(template.Node): def __init__(self, template_value, var_name): self.template_value = template_value self.var_name = var_name def render(self, context): template_value = template.resolve_variable( self.template_value, context) t = template.Template(template_value) context_vars = {} for d in list(context): for var, val in d.items(): context_vars[var] = val result = t.render(template.RequestContext( context["request"], context_vars)) if self.var_name: context[self.var_name] = result return "" return result
它是如何工作的...
{% parse %}模板标签允许您将值解析为模板并立即渲染或将其保存为上下文变量。
如果我们有一个包含描述字段的对象,该字段可以包含模板变量或逻辑,我们可以使用以下代码进行解析和渲染:
{% load utility_tags %}
{% parse object.description %}
也可以定义一个值以便使用引号字符串进行解析,如下面的代码所示:
{% load utility_tags %}
{% parse "{{ STATIC_URL }}site/img/" as img_path %}
<img src="img/{{ img_path }}someimage.png" alt="" />
让我们看看{% parse %}模板标签的代码。解析函数逐个检查模板标签的参数。首先,我们期望parse名称,然后是模板值,最后我们期望可选的as词后跟上下文变量名称。模板值和变量名称传递给ParseNode类。该类的render()方法首先解析模板变量的值,并从中创建一个模板对象。然后,它使用所有上下文变量渲染模板。如果变量名称已定义,结果被保存到它;否则,结果立即显示。
参见
-
创建一个模板标签以包含模板(如果存在)菜谱
-
在模板中创建加载 QuerySet 的模板标签菜谱
-
创建一个模板标签来修改请求查询参数菜谱
创建一个模板标签来修改请求查询参数
Django 通过向 URL 配置文件添加正则表达式规则,提供了一个方便且灵活的系统来创建规范和干净的 URL。然而,在管理查询参数方面,缺乏内置机制。例如,搜索或可筛选对象列表视图需要接受查询参数,以便通过另一个参数深入筛选结果或转到另一页。在这个菜谱中,我们将创建{% modify_query %}、{% add_to_query %}和{% remove_from_query %}模板标签,这些标签允许您添加、更改或删除当前查询的参数。
准备工作
再次强调,我们从utils应用开始,它应该在INSTALLED_APPS中设置,并包含templatetags包。
此外,请确保您已为TEMPLATE_CONTEXT_PROCESSORS设置配置了request上下文处理器,如下所示:
# conf/base.py or settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
"django.contrib.auth.context_processors.auth",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
"django.core.context_processors.media",
"django.core.context_processors.static",
"django.core.context_processors.tz",
"django.contrib.messages.context_processors.messages",
"django.core.context_processors.request",
)
如何去做...
对于这些模板标签,我们将使用simple_tag装饰器来解析组件,并要求您只需定义渲染函数,如下所示:
-
首先,我们将创建
{% modify_query %}模板标签:# utils/templatetags/utility_tags.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import urllib from django import template from django.utils.encoding import force_str register = template.Library() ### TAGS ### @register.simple_tag(takes_context=True) def modify_query( context, *params_to_remove, **params_to_change ): """ Renders a link with modified current query parameters """ query_params = [] for key, value_list in \ context["request"].GET._iterlists(): if not key in params_to_remove: # don't add key-value pairs for # params_to_change if key in params_to_change: query_params.append( (key, params_to_change[key]) ) params_to_change.pop(key) else: # leave existing parameters as they were # if not mentioned in the params_to_change for value in value_list: query_params.append((key, value)) # attach new params for key, value in params_to_change.items(): query_params.append((key, value)) query_string = context["request"].path if len(query_params): query_string += "?%s" % urllib.urlencode([ (key, force_str(value)) for (key, value) in query_params if value ]).replace("&", "&") return query_string -
然后,让我们创建
{% add_to_query %}模板标签:@register.simple_tag(takes_context=True) def add_to_query( context, *params_to_remove, **params_to_add ): """ Renders a link with modified current query parameters """ query_params = [] # go through current query params.. for key, value_list in \ context["request"].GET._iterlists(): if not key in params_to_remove: # don't add key-value pairs which already # exist in the query if key in params_to_add and \ unicode(params_to_add[key]) in value_list: params_to_add.pop(key) for value in value_list: query_params.append((key, value)) # add the rest key-value pairs for key, value in params_to_add.items(): query_params.append((key, value)) # empty values will be removed query_string = context["request"].path if len(query_params): query_string += "?%s" % urllib.urlencode([ (key, force_str(value)) for (key, value) in query_params if value ]).replace("&", "&") return query_string -
最后,让我们创建
{% remove_from_query %}模板标签:@register.simple_tag(takes_context=True) def remove_from_query(context, *args, **kwargs): """ Renders a link with modified current query parameters """ query_params = [] # go through current query params.. for key, value_list in \ context["request"].GET._iterlists(): # skip keys mentioned in the args if not key in args: for value in value_list: # skip key-value pairs mentioned in kwargs if not (key in kwargs and unicode(value) == unicode(kwargs[key])): query_params.append((key, value)) # empty values will be removed query_string = context["request"].path if len(query_params): query_string = "?%s" % urllib.urlencode([ (key, force_str(value)) for (key, value) in query_params if value ]).replace("&", "&") return query_string
它是如何工作的...
所有的三个创建的模板标签表现相似。首先,它们从request.GET字典样式的QueryDict对象中读取当前查询参数到一个新的键值query_params元组列表中。然后,根据位置参数和关键字参数更新值。最后,形成新的查询字符串,所有空格和特殊字符都进行 URL 编码,并且连接查询参数的与号被转义。这个新的查询字符串被返回到模板中。
小贴士
要了解更多关于 QueryDict 对象的信息,请参阅官方 Django 文档,docs.djangoproject.com/en/1.8/ref/request-response/#querydict-objects。
让我们看看如何使用 {% modify_query %} 模板标签的示例。模板标签中的位置参数定义了要删除哪些查询参数,而关键字参数定义了在当前查询中要修改哪些查询参数。如果当前 URL 是 http://127.0.0.1:8000/artists/?category=fine-art&page=5,我们可以使用以下模板标签来渲染一个链接,该链接跳转到下一页:
{% load utility_tags %}
<a href="{% modify_query page=6 %}">6</a>
以下片段是使用前面的模板标签渲染的输出:
<a href="/artists/?category=fine-art&page=6">6</a>
我们也可以使用以下示例来渲染一个链接,该链接重置分页并跳转到另一个分类,雕塑,如下所示:
{% load utility_tags i18n %}
<a href="{% modify_query "page" category="sculpture" %}">{% trans "Sculpture" %}</a>
以下片段是使用前面的模板标签渲染的输出:
<a href="/artists/?category=sculpture">Sculpture</a>
使用 {% add_to_query %} 模板标签,您可以逐步添加具有相同名称的参数。例如,如果当前 URL 是 http://127.0.0.1:8000/artists/?category=fine-art,您可以使用以下链接添加另一个分类,雕塑:
{% load utility_tags i18n %}
<a href="{% add_to_query "page" category="sculpture" %}">{% trans "Sculpture" %}</a>
这将在模板中渲染成如下片段:
<a href="/artists/?category=fine-art&category=sculpture">Sculpture</a>
最后,借助 {% remove_from_query %} 模板标签,您可以逐步删除具有相同名称的参数。例如,如果当前 URL 是 http://127.0.0.1:8000/artists/?category=fine-art&category=sculpture,您可以使用以下链接帮助删除 雕塑 分类:
{% load utility_tags i18n %}
<a href="{% remove_from_query "page" category="sculpture" %}"><span class="glyphicon glyphicon-remove"></span> {% trans "Sculpture" %}</a>
这将在模板中渲染如下:
<a href="/artists/?category=fine-art"><span class="glyphicon glyphicon-remove"></span> Sculpture</a>
参见
-
第三章中的 过滤对象列表 配方,表单和视图
-
创建一个模板标签以包含存在的模板 的配方
-
创建一个模板标签以在模板中加载 QuerySet 的配方
-
创建一个模板标签以将内容解析为模板 的配方
第六章。模型管理
在本章中,我们将涵盖以下主题:
-
自定义更改列表页面上的列
-
创建管理操作
-
开发更改列表过滤器
-
自定义默认管理设置
-
在更改表单中插入地图
简介
Django 框架自带了一个用于模型的内置管理系统。只需付出很少的努力,你就可以为浏览模型设置可筛选、可搜索和可排序的列表,并配置表单以添加和编辑数据。在本章中,我们将通过开发一些实际案例来介绍自定义管理的先进技术。
自定义更改列表页面上的列
修改默认 Django 管理系统的列表视图让你可以查看特定模型的所有实例的概览。默认情况下,list_display 模型管理属性控制着显示在不同列中的字段。此外,你还可以在那里设置自定义函数,这些函数返回关系中的数据或显示自定义 HTML。在本配方中,我们将为 list_display 属性创建一个特殊函数,该函数在列表视图的一列中显示一个图像。作为额外奖励,我们将通过添加 list_editable 设置,使一个字段在列表视图中直接可编辑。
准备工作
首先,请确保在设置中的 INSTALLED_APPS 中包含了 django.contrib.admin,并且 AdminSite 已在 URL 配置中连接。然后,创建一个新的 products 应用程序并将其添加到 INSTALLED_APPS 中。这个应用程序将包含 Product 和 ProductPhoto 模型,其中一种产品可能有多个照片。在这个例子中,我们还将使用 UrlMixin,它是在 第二章 中定义的,与 URL 相关的方法的配方中定义的。创建一个具有 URL 相关方法的模型混入。
让我们在 models.py 文件中创建 Product 和 ProductPhoto 模型,如下所示:
# products/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import os
from django.db import models
from django.utils.timezone import now as timezone_now
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.core.urlresolvers import NoReverseMatch
from django.utils.encoding import python_2_unicode_compatible
from utils.models import UrlMixin
def upload_to(instance, filename):
now = timezone_now()
filename_base, filename_ext = os.path.splitext(filename)
return "products/%s/%s%s" % (
instance.product.slug,
now.strftime("%Y%m%d%H%M%S"),
filename_ext.lower(),
)
@python_2_unicode_compatible
class Product(UrlMixin):
title = models.CharField(_("title"), max_length=200)
slug = models.SlugField(_("slug"), max_length=200)
description = models.TextField(_("description"), blank=True)
price = models.DecimalField(_("price (€)"), max_digits=8,
decimal_places=2, blank=True, null=True)
class Meta:
verbose_name = _("Product")
verbose_name_plural = _("Products")
def __str__(self):
return self.title
def get_url_path(self):
try:
return reverse("product_detail", kwargs={
"slug": self.slug
})
except NoReverseMatch:
return ""
@python_2_unicode_compatible
class ProductPhoto(models.Model):
product = models.ForeignKey(Product)
photo = models.ImageField(_("photo"), upload_to=upload_to)
class Meta:
verbose_name = _("Photo")
verbose_name_plural = _("Photos")
def __str__(self):
return self.photo.name
如何操作...
我们将为 Product 模型创建一个简单的管理,该模型将具有附加到产品上的 ProductPhoto 模型的实例,作为内联。
在 list_display 属性中,我们将列出模型管理中使用的 get_photo() 方法,该方法将用于显示多对一关系中的第一张照片。
让我们创建一个包含以下内容的 admin.py 文件:
# products/admin.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponse
from .models import Product, ProductPhoto
class ProductPhotoInline(admin.StackedInline):
model = ProductPhoto
extra = 0
class ProductAdmin(admin.ModelAdmin):
list_display = ["title", "get_photo", "price"]
list_editable = ["price"]
fieldsets = (
(_("Product"), {
"fields": ("title", "slug", "description", "price"),
}),
)
prepopulated_fields = {"slug": ("title",)}
inlines = [ProductPhotoInline]
def get_photo(self, obj):
project_photos = obj.productphoto_set.all()[:1]
if project_photos.count() > 0:
return """<a href="%(product_url)s" target="_blank">
<img src="img/%(photo_url)s" alt="" width="100" />
</a>""" % {
"product_url": obj.get_url_path(),
"photo_url": project_photos[0].photo.url,
}
return ""
get_photo.short_description = _("Preview")
get_photo.allow_tags = True
admin.site.register(Product, ProductAdmin)
工作原理...
如果你查看浏览器中的产品管理列表,它将类似于以下截图:

通常,list_display 属性定义了在管理列表视图中要列出的字段;例如,title 和 price 是 Product 模型的字段。
除了正常的字段名称外,list_display 属性还接受一个函数或另一个可调用对象,管理模型的属性名称,或模型的属性名称。
小贴士
在 Python 中,可调用对象是一个函数、方法或实现了 __call__() 方法的类。你可以使用 callable() 函数检查一个变量是否可调用。
在list_display中使用的每个可调用函数都会接收到一个作为第一个参数传递的模型实例。因此,在我们的例子中,我们有模型管理器的get_photo()方法,它检索Product实例作为obj。该方法尝试从多对一关系中获得第一个ProductPhoto,如果存在,则返回带有链接到Product详情页的<img>标签的 HTML。
您可以为在list_display中使用的可调用函数设置多个属性。可调用函数的short_description属性定义了列显示的标题。allow_tags属性通知管理器不要转义 HTML 值。
此外,通过list_editable设置使价格字段可编辑,底部有一个保存按钮来保存整个产品列表。
还有更多...
理想情况下,get_photo()方法中不应包含任何硬编码的 HTML;然而,它应该从文件中加载并渲染一个模板。为此,您可以使用django.template.loader中的render_to_string()函数。然后,您的展示逻辑将与业务逻辑分离。我将这留作您的练习。
相关内容
-
在第二章的使用与 URL 相关的方法创建模型混入配方中,数据库结构
-
创建管理操作配方
-
开发变更列表过滤器配方
创建管理操作
Django 管理系统为我们提供了可以执行列表中选定项目的操作。默认情况下有一个操作,用于删除选定的实例。在本配方中,我们将为Product模型的列表创建一个额外的操作,允许管理员将选定的产品导出到 Excel 电子表格。
准备工作
我们将从上一个配方中创建的products应用开始。
确保您的虚拟环境中已安装xlwt模块以创建 Excel 电子表格:
(myproject_env)$ pip install xlwt
如何操作...
管理操作是接受三个参数的函数:当前的ModelAdmin值、当前的HttpRequest值以及包含所选项目的QuerySet值。按照以下步骤创建自定义管理操作:
-
让我们在产品应用的
admin.py文件中创建一个export_xls()函数,如下所示:# products/admin.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import xlwt # ... other imports ... def export_xls(modeladmin, request, queryset): response = HttpResponse( content_type="application/ms-excel" ) response["Content-Disposition"] = "attachment; "\ "filename=products.xls" wb = xlwt.Workbook(encoding="utf-8") ws = wb.add_sheet("Products") row_num = 0 ### Print Title Row ### columns = [ # column name, column width ("ID", 2000), ("Title", 6000), ("Description", 8000), ("Price (€)", 3000), ] header_style = xlwt.XFStyle() header_style.font.bold = True for col_num, (item, width) in enumerate(columns): ws.write(row_num, col_num, item, header_style) # set column width ws.col(col_num).width = width text_style = xlwt.XFStyle() text_style.alignment.wrap = 1 price_style = xlwt.XFStyle() price_style.num_format_str = "0.00" styles = [ text_style, text_style, text_style, price_style, text_style ] for obj in queryset.order_by("pk"): row_num += 1 project_photos = obj.productphoto_set.all()[:1] url = "" if project_photos: url = "http://{0}{1}".format( request.META['HTTP_HOST'], project_photos[0].photo.url, ) row = [ obj.pk, obj.title, obj.description, obj.price, url, ] for col_num, item in enumerate(row): ws.write( row_num, col_num, item, styles[col_num] ) wb.save(response) return response export_xls.short_description = _("Export XLS") -
然后,将
actions设置添加到ProductAdmin中,如下所示:class ProductAdmin(admin.ModelAdmin): # ... actions = [export_xls]
它是如何工作的...
如果您在浏览器中查看产品管理列表页面,您将看到一个名为导出 XLS的新操作,以及默认的删除选定的产品操作,如下面的截图所示:

默认情况下,管理员操作会对 QuerySet 执行某些操作,并将管理员重定向回更改列表页面。然而,对于这些更复杂的行为,可以返回 HttpResponse。export_xls() 函数返回具有 Excel 电子表格内容类型的 HttpResponse。使用 Content-Disposition 标头,我们将响应设置为可下载的 products.xls 文件。
然后,我们使用 xlwt Python 模块创建 Excel 文件。
首先,创建一个使用 UTF-8 编码的工作簿。然后,我们向其中添加一个名为 Products 的工作表。我们将使用工作表的 write() 方法来设置每个单元格的内容和样式,以及使用 col() 方法来获取列并设置其宽度。
要查看工作表中所有列的概览,我们将创建一个包含列名和宽度的元组列表。Excel 使用一些神奇的单位来表示列宽。它们是默认字体中零字符宽度的 1/256。接下来,我们将定义标题样式为粗体。因为我们已经定义了列,我们将遍历它们并在第一行中填充列名,同时将粗体样式分配给它们。
然后,我们将创建一个用于普通单元格和价格的样式。普通单元格中的文本将换行。价格将具有特殊的数字样式,小数点后有两位数字。
最后,我们将遍历按 ID 排序的选定产品的 QuerySet,并在相应的单元格中打印指定的字段,同时应用特定的样式。
工作簿被保存到类似文件的 HttpResponse 对象中,生成的 Excel 表格看起来类似于以下内容:
| ID | 标题 | 描述 | 价格 (€) | 预览 |
|---|---|---|---|---|
| 1 | Ryno | 使用 Ryno 微型循环,你不仅限于街道或自行车道。它是一种过渡性车辆——它可以去任何一个人可以步行或骑自行车的地方。 | 3865.00 | http://127.0.0.1:8000/media/products/ryno/20140523044813.jpg |
| 2 | Mercury Skate | 设计这款 Mercury Skate 的主要目的是减少滑板者的疲劳,并为他们提供在人行道上更容易、更顺畅的骑行体验。 | http://127.0.0.1:8000/media/products/mercury-skate/20140521030128.png |
|
| 4 | Detroit Electric Car | Detroit Electric SP:01 是一款限量版、两座、纯电动跑车,为电动汽车的性能和操控设定了新的标准。 | http://127.0.0.1:8000/media/products/detroit-electric-car/20140521033122.jpg |
参见
-
第九章,数据导入和导出
-
定制更改列表页面上的列 菜单
-
开发更改列表过滤器 菜单
开发更改列表过滤器
如果你想让管理员能够通过日期、关系或字段选择来过滤更改列表,你需要使用管理模型的list_filter属性。此外,还有可能使用定制过滤器。在这个菜谱中,我们将添加一个允许你通过附加照片数量选择产品的过滤器。
准备工作
让我们从上一个菜谱中创建的products应用开始。
如何操作...
执行以下两个步骤:
-
在
admin.py文件中,创建一个继承自SimpleListFilter的PhotoFilter类,如下所示:# products/admin.py # -*- coding: UTF-8 -*- # ... all previous imports go here ... from django.db import models class PhotoFilter(admin.SimpleListFilter): # Human-readable title which will be displayed in the # right admin sidebar just above the filter options. title = _("photos") # Parameter for the filter that will be used in the # URL query. parameter_name = "photos" def lookups(self, request, model_admin): """ Returns a list of tuples. The first element in each tuple is the coded value for the option that will appear in the URL query. The second element is the human-readable name for the option that will appear in the right sidebar. """ return ( ("zero", _("Has no photos")), ("one", _("Has one photo")), ("many", _("Has more than one photo")), ) def queryset(self, request, queryset): """ Returns the filtered queryset based on the value provided in the query string and retrievable via `self.value()`. """ qs = queryset.annotate( num_photos=models.Count("productphoto") ) if self.value() == "zero": qs = qs.filter(num_photos=0) elif self.value() == "one": qs = qs.filter(num_photos=1) elif self.value() == "many": qs = qs.filter(num_photos__gte=2) return qs -
然后,将列表过滤器添加到
ProductAdmin中,如下所示:class ProductAdmin(admin.ModelAdmin): # ... list_filter = [PhotoFilter]
如何工作...
我们刚刚创建的列表过滤器将显示在产品列表的侧边栏中,如下所示:

PhotoFilter类具有可翻译的标题和查询参数名称作为属性。它还有两个方法:定义过滤器选项的lookups()方法和定义在选中特定值时如何过滤QuerySet对象的queryset()方法。
在lookups()方法中,我们定义了三个选项:没有照片、有一张照片和有多张照片附加。在queryset()方法中,我们使用QuerySet的annotate()方法来选择每个产品的照片数量。然后根据所选选项过滤这些照片的数量。
要了解更多关于聚合函数,如annotate()的信息,请参考官方 Django 文档,链接为docs.djangoproject.com/en/1.8/topics/db/aggregation/.
参见
-
自定义更改列表页面上的列菜谱
-
创建管理操作菜谱
-
自定义默认管理设置菜谱
自定义默认管理设置
Django 应用以及第三方应用都带有自己的管理设置;然而,有一个机制可以关闭这些设置并使用你自己的更好的管理设置。在这个菜谱中,你将学习如何用自定义管理设置替换django.contrib.auth应用的管理设置。
准备工作
创建一个custom_admin应用,并将此应用放在设置中的INSTALLED_APPS下。
如何操作...
在custom_admin应用中的新admin.py文件中插入以下内容:
# custom_admin/admin.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.admin import User, Group
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.contrib.contenttypes.models import ContentType
class UserAdminExtended(UserAdmin):
list_display = ("username", "email", "first_name",
"last_name", "is_active", "is_staff", "date_joined",
"last_login")
list_filter = ("is_active", "is_staff", "is_superuser",
"date_joined", "last_login")
ordering = ("last_name", "first_name", "username")
save_on_top = True
class GroupAdminExtended(GroupAdmin):
list_display = ("__unicode__", "display_users")
save_on_top = True
def display_users(self, obj):
links = []
for user in obj.user_set.all():
ct = ContentType.objects.get_for_model(user)
url = reverse(
"admin:{}_{}_change".format(
ct.app_label, ct.model
),
args=(user.id,)
)
links.append(
"""<a href="{}" target="_blank">{}</a>""".format(
url,
"{} {}".format(
user.first_name, user.last_name
).strip() or user.username,
)
)
return u"<br />".join(links)
display_users.allow_tags = True
display_users.short_description = _("Users")
admin.site.unregister(User)
admin.site.unregister(Group)
admin.site.register(User, UserAdminExtended)
admin.site.register(Group, GroupAdminExtended)
如何工作...
默认用户管理列表看起来类似于以下截图:

默认的用户管理列表看起来类似于以下截图:

在这个菜谱中,我们创建了两个模型管理类,UserAdminExtended和GroupAdminExtended,分别扩展了贡献的UserAdmin和GroupAdmin类,并覆盖了一些属性。然后,我们注销了现有的User和Group模型的管理类,并注册了新的修改后的类。
以下截图显示了用户管理现在的样子:

修改后的用户管理设置在列表视图中显示的字段比默认设置更多,添加了额外的过滤和排序选项,并在编辑表单的顶部显示提交按钮。
在新的组管理设置更改列表中,我们将显示分配给特定组的用户。这看起来与浏览器中的以下截图类似:

更多...
在我们的 Python 代码中,我们使用了一种新的字符串格式化方法。要了解更多关于字符串的format()方法与旧风格用法的信息,请参考以下 URL:pyformat.info/。
参见
-
定制更改列表页面上的列菜谱
-
在更改表单中插入地图菜谱
在更改表单中插入地图
Google Maps 提供了一个 JavaScript API,可以将地图插入到您的网站中。在这个菜谱中,我们将创建一个带有Location模型的locations应用,并扩展更改表单的模板,以便添加一个地图,管理员可以在其中找到并标记位置的地理坐标。
准备工作
我们将从locations应用开始,这个应用应该在设置中的INSTALLED_APPS下。在那里创建一个Location模型,包含标题、描述、地址和地理坐标,如下所示:
# locations/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
COUNTRY_CHOICES = (
("UK", _("United Kingdom")),
("DE", _("Germany")),
("FR", _("France")),
("LT", _("Lithuania")),
)
@python_2_unicode_compatible
class Location(models.Model):
title = models.CharField(_("title"), max_length=255,
unique=True)
description = models.TextField(_("description"), blank=True)
street_address = models.CharField(_("street address"),
max_length=255, blank=True)
street_address2 = models.CharField(
_("street address (2nd line)"), max_length=255,
blank=True)
postal_code = models.CharField(_("postal code"),
max_length=10, blank=True)
city = models.CharField(_("city"), max_length=255, blank=True)
country = models.CharField(_("country"), max_length=2,
blank=True, choices=COUNTRY_CHOICES)
latitude = models.FloatField(_("latitude"), blank=True,
null=True,
help_text=_("Latitude (Lat.) is the angle between "
"any point and the equator "
"(north pole is at 90; south pole is at -90)."))
longitude = models.FloatField(_("longitude"), blank=True,
null=True,
help_text=_("Longitude (Long.) is the angle "
"east or west of "
"an arbitrary point on Earth from Greenwich (UK), "
"which is the international zero-longitude point "
"(longitude=0 degrees). "
"The anti-meridian of Greenwich is both 180 "
"(direction to east) and -180 (direction to west)."))
class Meta:
verbose_name = _("Location")
verbose_name_plural = _("Locations")
def __str__(self):
return self.title
如何操作...
Location模型的管理就像它可能的那样简单。执行以下步骤:
-
让我们为
Location模型创建管理设置。请注意,我们正在使用get_fieldsets()方法来定义字段集,并从模板中渲染描述,如下所示:# locations/admin.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ from django.contrib import admin from django.template.loader import render_to_string from .models import Location class LocationAdmin(admin.ModelAdmin): save_on_top = True list_display = ("title", "street_address", "description") search_fields = ("title", "street_address", "description") def get_fieldsets(self, request, obj=None): map_html = render_to_string( "admin/includes/map.html" ) fieldsets = [ (_("Main Data"), {"fields": ("title", "description")}), (_("Address"), {"fields": ("street_address", "street_address2", "postal_code", "city", "country", "latitude", "longitude")}), (_("Map"), {"description": map_html, "fields": []}), ] return fieldsets admin.site.register(Location, LocationAdmin) -
要创建自定义更改表单模板,在您的
templates目录下admin/locations/location/中添加一个新的change_form.html文件。此模板将从默认的admin/change_form.html模板扩展,并覆盖extrastyle和field_sets块,如下所示:{# myproject/templates/admin/locations/location/change_form.html #} {% extends "admin/change_form.html" %} {% load i18n admin_static admin_modify %} {% load url from future %} {% load admin_urls %} {% block extrastyle %} {{ block.super }} <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}site/css/locating.css" /> {% endblock %} {% block field_sets %} {% for fieldset in adminform %} {% include "admin/includes/fieldset.html" %} {% endfor %} <script type="text/javascript" src="img/js?language=en"></script> <script type="text/javascript" src="img/locating.js"></script> {% endblock %} -
然后,我们需要为将插入到
Map字段集中的地图创建模板:{# myproject/templates/admin/includes/map.html #} {% load i18n %} <div class="form-row"> <div id="map_canvas"> <!-- THE GMAPS WILL BE INSERTED HERE DYNAMICALLY --> </div> <ul id="map_locations"></ul> <div class="buttonHolder"> <button id="locate_address" type="button" class="secondaryAction"> {% trans "Locate address" %} </button> <button id="remove_geo" type="button" class="secondaryAction"> {% trans "Remove from map" %} </button> </div> </div> -
当然,地图默认情况下不会被样式化。因此,我们必须添加一些 CSS,如下面的代码所示:
/* site_static/site/css/locating.css */ #map_canvas { width:722px; height:300px; margin-bottom: 8px; } #map_locations { width:722px; margin: 0; padding: 0; margin-bottom: 8px; } #map_locations li { border-bottom: 1px solid #ccc; list-style: none; } #map_locations li:first-child { border-top: 1px solid #ccc; } .buttonHolder { width:722px; } #remove_geo { float: right; } -
然后,让我们创建一个
locating.jsJavaScript 文件。在这个文件中,我们将使用 jQuery,因为 jQuery 附带了贡献的行政系统,这使得工作变得简单且跨浏览器。我们不希望污染环境中的全局变量,因此,我们将从一个闭包开始,为变量和函数创建一个私有作用域(闭包是函数返回后仍保持活跃的局部变量),如下所示:// site_static/site/js/locating.js (function ($, undefined) { var gMap; var gettext = window.gettext || function (val) { return val; }; var gMarker; // ... this is where all the further JavaScript // functions go ... }(django.jQuery)); -
我们将逐个创建 JavaScript 函数。
getAddress4search()函数将从地址字段收集address字符串,稍后可用于地理编码,如下所示:function getAddress4search() { var address = []; var sStreetAddress2 = $('#id_street_address2').val(); if (sStreetAddress2) { sStreetAddress2 = ' ' + sStreetAddress2; } address.push($('#id_street_address').val() + sStreetAddress2); address.push($('#id_city').val()); address.push($('#id_country').val()); address.push($('#id_postal_code').val()); return address.join(', '); } -
updateMarker()函数将接受纬度和经度参数,并在地图上绘制或移动标记。它还使标记可拖动:function updateMarker(lat, lng) { var point = new google.maps.LatLng(lat, lng); if (gMarker) { gMarker.setPosition(point); } else { gMarker = new google.maps.Marker({ position: point, map: gMap }); } gMap.panTo(point, 15); gMarker.setDraggable(true); google.maps.event.addListener(gMarker, 'dragend', function() { var point = gMarker.getPosition(); updateLatitudeAndLongitude(point.lat(), point.lng()); }); } -
updateLatitudeAndLongitude()函数接受纬度和经度参数,并更新具有id_latitude和id_longitudeID 的字段的值,如下所示:function updateLatitudeAndLongitude(lat, lng) { lat = Math.round(lat * 1000000) / 1000000; lng = Math.round(lng * 1000000) / 1000000; $('#id_latitude').val(lat); $('#id_longitude').val(lng); } -
autocompleteAddress()函数从 Google Maps 地理编码获取结果,并在地图下方列出以供选择正确的选项,或者如果只有一个结果,它将更新地理位置和地址字段,如下所示:function autocompleteAddress(results) { var $foundLocations = $('#map_locations').html(''); var i, len = results.length; // console.log(JSON.stringify(results, null, 4)); if (results) { if (len > 1) { for (i=0; i<len; i++) { $('<a href="">' + results[i].formatted_address + '</a>').data('gmap_index', i).click(function (e) { e.preventDefault(); var result = results[$(this).data('gmap_index')]; updateAddressFields(result.address_components); var point = result.geometry.location; updateLatitudeAndLongitude(point.lat(), point.lng()); updateMarker(point.lat(), point.lng()); $foundLocations.hide(); }).appendTo($('<li>').appendTo($foundLocations)); } $('<a href="">' + gettext('None of the listed') + '</a>').click(function (e) { e.preventDefault(); $foundLocations.hide(); }).appendTo($('<li>').appendTo($foundLocations)); $foundLocations.show(); } else { $foundLocations.hide(); var result = results[0]; updateAddressFields(result.address_components); var point = result.geometry.location; updateLatitudeAndLongitude(point.lat(), point.lng()); updateMarker(point.lat(), point.lng()); } } } -
updateAddressFields()函数接受一个嵌套字典作为参数,其中包含地址组件,并填写所有地址字段:function updateAddressFields(addressComponents) { var i, len=addressComponents.length; var streetName, streetNumber; for (i=0; i<len; i++) { var obj = addressComponents[i]; var obj_type = obj.types[0]; if (obj_type == 'locality') { $('#id_city').val(obj.long_name); } if (obj_type == 'street_number') { streetNumber = obj.long_name; } if (obj_type == 'route') { streetName = obj.long_name; } if (obj_type == 'postal_code') { $('#id_postal_code').val(obj.long_name); } if (obj_type == 'country') { $('#id_country').val(obj.short_name); } } if (streetName) { var streetAddress = streetName; if (streetNumber) { streetAddress += ' ' + streetNumber; } $('#id_street_address').val(streetAddress); } } -
最后,我们有在页面加载时调用的初始化函数。它将
onclick事件处理程序附加到按钮上,创建一个 Google Map,并在latitude和longitude字段中定义的初始地理位置上标记,如下所示:$(function (){ $('#locate_address').click(function() { var oGeocoder = new google.maps.Geocoder(); oGeocoder.geocode( {address: getAddress4search()}, function (results, status) { if (status === google.maps.GeocoderStatus.OK) { autocompleteAddress(results); } else { autocompleteAddress(false); } } ); }); $('#remove_geo').click(function() { $('#id_latitude').val(''); $('#id_longitude').val(''); gMarker.setMap(null); gMarker = null; }); gMap = new google.maps.Map($('#map_canvas').get(0), { scrollwheel: false, zoom: 16, center: new google.maps.LatLng(51.511214, -0.119824), disableDoubleClickZoom: true }); google.maps.event.addListener(gMap, 'dblclick', function(event) { var lat = event.latLng.lat(); var lng = event.latLng.lng(); updateLatitudeAndLongitude(lat, lng); updateMarker(lat, lng); }); $('#map_locations').hide(); var $lat = $('#id_latitude'); var $lng = $('#id_longitude'); if ($lat.val() && $lng.val()) { updateMarker($lat.val(), $lng.val()); } });
它是如何工作的...
如果你查看浏览器中的位置更改表单,你将看到一个在字段集中显示的地图,后面跟着包含地址字段的字段集,如下面的截图所示:

在地图下方有两个按钮:定位地址和从地图中移除。
当你点击定位地址按钮时,将调用地理编码以搜索输入地址的地理坐标。地理编码的结果是一个或多个地址,它们以嵌套字典格式包含纬度和经度。要在开发者工具的控制台中查看嵌套字典的结构,请在 autocompleteAddress() 函数的开始处放置以下行:
console.log(JSON.stringify(results, null, 4));
如果只有一个结果,缺失的邮政编码或其他缺失的地址字段将被填充,纬度和经度将被填写,并在地图上的一个特定位置放置一个标记。如果有更多结果,整个列表将在地图下方显示,并提供选择正确结果的选项,如下面的截图所示:

然后,管理员可以通过拖放在地图上移动标记。此外,在地图上的任何地方双击都将更新地理坐标和标记位置。
最后,如果点击了从地图中移除按钮,地理坐标将被清除,并移除标记。
参见
- 参见第四章中的使用 HTML5 数据属性配方,模板和 JavaScript
第七章:Django CMS
在本章中,我们将介绍以下菜谱:
-
为 Django CMS 创建模板
-
构建页面菜单
-
将应用转换为 CMS 应用
-
添加自己的导航
-
编写自己的 CMS 插件
-
向 CMS 页面添加新字段
简介
Django CMS 是一个基于 Django 的开源内容管理系统,由瑞士的 Divio AG 创建。Django CMS 负责网站的架构,提供导航菜单,使在前端编辑页面内容变得容易,并支持网站的多语言。你还可以使用提供的钩子根据需要扩展它。要创建网站,你需要创建页面的层次结构,其中每个页面都有一个模板。模板有占位符,可以分配不同的插件来包含内容。使用特殊的模板标签,可以从层次页面结构生成菜单。CMS 负责将 URL 映射到特定页面。
在本章中,我们将从开发者的角度查看 Django CMS 3.1。我们将了解模板正常运行所必需的内容,并查看头部和尾部导航的可能页面结构。你还将学习如何将应用的 URL 规则附加到 CMS 页面树节点。然后,我们将自定义导航附加到页面菜单并创建我们自己的 CMS 内容插件。最后,你将学习如何向 CMS 页面添加新字段。
尽管在这本书中,我不会引导你了解使用 Django CMS 的所有细节;但到本章结束时,你将了解其目的和使用方法。其余内容可以通过官方文档在 docs.django-cms.org/en/develop/ 学习,也可以通过尝试 CMS 的前端用户界面来学习。
为 Django CMS 创建模板
对于你页面结构中的每一页,你需要从在设置中定义的模板列表中选择一个模板。在这个菜谱中,我们将查看这些模板的最小要求。
准备工作
如果你想要启动一个新的 Django CMS 项目,请在虚拟环境中执行以下命令并回答所有提示的问题:
(myproject_env)$ pip install djangocms-installer
(myproject_env)$ djangocms -p project/myproject myproject
在这里,project/myproject 是项目将被创建的路径,而 myproject 是项目名称。
另一方面,如果你想在现有项目中集成 Django CMS,请查看官方文档 docs.django-cms.org/en/latest/how_to/install.html。
如何操作...
我们将更新由 Bootstrap 驱动的 base.html 模板,使其包含 Django CMS 所需的所有内容。然后,我们将创建并注册两个模板,default.html 和 start.html,供 CMS 页面选择:
-
首先,我们将更新在第四章 安排 base.html 模板食谱中创建的基本模板,如下所示:
{# templates/base.html #} <!DOCTYPE html> {% load i18n cms_tags sekizai_tags menu_tags %} <html lang="{{ LANGUAGE_CODE }}"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>{% block title %}{% endblock %}{% trans "My Website" %}</title> <link rel="icon" href="{{ STATIC_URL }}site/img/favicon.ico" type="image/png" /> {% block meta_tags %}{% endblock %} {% render_block "css" %} {% block base_stylesheet %} <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" /> <link href="{{ STATIC_URL }}site/css/style.css" rel="stylesheet" media="screen" type="text/css" /> {% endblock %} {% block stylesheet %}{% endblock %} {% block base_js %} <script src="img/"></script> <script src="img/"></script> <script src="img/bootstrap.min.js"></script> {% endblock %} {% block js %}{% endblock %} {% block extrahead %}{% endblock %} </head> <body class="{% block bodyclass %}{% endblock %} {{ request.current_page.cssextension.body_css_class }}"> {% cms_toolbar %} {% block page %} <div class="wrapper"> <div id="header" class="clearfix container"> <h1>{% trans "My Website" %}</h1> <nav class="navbar navbar-default" role="navigation"> {% block header_navigation %} <ul class="nav navbar-nav"> {% show_menu_below_id "start_page" 0 1 1 1 %} </ul> {% endblock %} {% block language_chooser %} <ul class="nav navbar-nav pull-right"> {% language_chooser %} </ul> {% endblock %} </nav> </div> <div id="content" class="clearfix container"> {% block content %} {% endblock %} </div> <div id="footer" class="clearfix container"> {% block footer_navigation %} <nav class="navbar navbar-default" role="navigation"> <ul class="nav navbar-nav"> {% show_menu_below_id "footer_navigation" 0 1 1 1 %} </ul> </nav> {% endblock %} </div> </div> {% endblock %} {% block extrabody %}{% endblock %} {% render_block "js" %} </body> </html> -
然后,我们将在
templates目录下创建一个cms目录,并为 CMS 页面添加两个模板:default.html用于普通页面,start.html用于主页,如下所示:{# templates/cms/default.html #} {% extends "base.html" %} {% load cms_tags %} {% block title %}{% page_attribute "page_title" %} - {% endblock %} {% block meta_tags %} <meta name="description" content="{% page_attribute meta_description %}"/> {% endblock %} {% block content %} <h1>{% page_attribute "page_title" %}</h1> <div class="row"> <div class="col-md-8"> {% placeholder main_content %} </div> <div class="col-md-4"> {% placeholder sidebar %} </div> </div> {% endblock %} {# templates/cms/start.html #} {% extends "base.html" %} {% load cms_tags %} {% block meta_tags %} <meta name="description" content="{% page_attribute meta_description %}"/> {% endblock %} {% block content %} <!-- Here goes very customized website-specific content like slideshows, latest tweets, latest news, latest profiles, etc. --> {% endblock %} -
最后,我们将设置这两个模板的路径,如下所示:
# conf/base.py or settings.py CMS_TEMPLATES = ( ("cms/default.html", gettext("Default")), ("cms/start.html", gettext("Homepage")), )
它是如何工作的...
如往常一样,base.html模板是所有其他模板扩展的主要模板。在这个模板中,Django CMS 使用来自django-sekizai模块的{% render_block %}模板标签在创建前端工具栏和其他管理小部件的模板中注入 CSS 和 JavaScript。我们将在<body>部分的开始处插入{% cms_toolbar %}模板标签——这就是工具栏将被放置的位置。我们将使用{% show_menu_below_id %}模板标签从特定的页面菜单树渲染头部和底部菜单。此外,我们还将使用{% language_chooser %}模板标签渲染语言选择器,该选择器可以在不同语言中切换到同一页面。
在CMS_TEMPLATES设置中定义的default.html和start.html模板,在创建 CMS 页面时将作为选择项可用。在这些模板中,对于需要动态输入内容的每个区域,当需要页面特定内容时,添加{% placeholder %}模板标签;当需要在不同页面间共享的内容时,添加{% static_placeholder %}模板标签。登录管理员可以在 CMS 工具栏从实时模式切换到草稿模式,并切换到结构部分时,向占位符添加内容插件。
相关内容
-
第四章 安排 base.html 模板食谱
-
页面菜单结构化食谱
页面菜单结构化
在本食谱中,我们将讨论一些关于定义您网站页面树结构的指南。
准备工作
在创建您页面结构之前设置网站可用的语言是一种良好的做法(尽管 Django CMS 数据库结构也允许您稍后添加新语言)。除了LANGUAGES之外,请确保您在设置中已设置CMS_LANGUAGES。CMS_LANGUAGES设置定义了每个 Django 站点应激活哪些语言,如下所示:
# conf/base.py or settings.py
# ...
from __future__ import unicode_literals
gettext = lambda s: s
LANGUAGES = (
("en", "English"),
("de", "Deutsch"),
("fr", "Français"),
("lt", "Lietuvių kalba"),
)
CMS_LANGUAGES = {
"default": {
"public": True,
"hide_untranslated": False,
"redirect_on_fallback": True,
},
1: [
{
"public": True,
"code": "en",
"hide_untranslated": False,
"name": gettext("en"),
"redirect_on_fallback": True,
},
{
"public": True,
"code": "de",
"hide_untranslated": False,
"name": gettext("de"),
"redirect_on_fallback": True,
},
{
"public": True,
"code": "fr",
"hide_untranslated": False,
"name": gettext("fr"),
"redirect_on_fallback": True,
},
{
"public": True,
"code": "lt",
"hide_untranslated": False,
"name": gettext("lt"),
"redirect_on_fallback": True,
},
],
}
如何操作...
页面导航是在树结构中设置的。第一棵树是主树,与其他树不同,主树的根节点不会反映在 URL 结构中。这个树的根节点是网站的首页。通常,这个页面有一个特定的模板,你在其中添加从不同应用程序聚合的内容;例如,幻灯片、实际新闻、新注册用户、最新推文或其他最新或特色对象。为了方便地从不同的应用程序渲染项目,请查看第五章 在模板中创建一个模板标签到 QuerySet 菜单中的 自定义模板过滤器和标签。
如果你的网站有多个导航,如顶部、元和页脚导航,请在页面的 高级 设置中为每个树的根节点分配一个 ID。这个 ID 将在基础模板中通过 {% show_menu_below_id %} 模板标签使用。你可以在官方文档中了解更多关于此和其他与菜单相关的模板标签的信息,请参阅 docs.django-cms.org/en/latest/reference/navigation.html。
第一棵树定义了网站的主结构。如果你想将页面放在根级 URL 下,例如,/en/search/ 但不是 /en/meta/search/,请将此页面放在主页下。如果你不希望页面在菜单中显示,因为它将通过图标或小部件链接,只需将其从菜单中隐藏。
页脚导航通常显示与顶部导航不同的项目,其中一些项目被重复,例如,开发者页面仅在页脚中显示;而新闻页面将在页眉和页脚中显示。对于所有重复的项目,只需在页面的高级设置中创建一个带有 重定向 设置的页面,并将其设置为在主树中的原始页面。默认情况下,当你创建一个二级树结构时,该树根下的所有页面都将包括根页面的 slug 在它们的 URL 路径中。如果你想跳过 URL 路径中的根页面的 slug,你需要在页面的高级设置中设置 覆盖 URL 设置。例如,开发者页面应该在 /en/developers/ 下,而不是 /en/secondary/developers/。
如何工作...
最后,你的页面结构将类似于以下图像(当然,页面结构也可以更复杂):

参见
-
在第五章 自定义模板过滤器和标签 的 在模板中创建一个模板标签来加载 QuerySet 菜单中,自定义模板过滤器和标签
-
为 Django CMS 创建模板 菜单
-
附加您自己的导航 菜单
将应用程序转换为 CMS 应用程序
简单的 Django CMS 网站将使用管理界面创建整个页面树。然而,对于现实世界的案例,您可能需要在某些页面节点下显示表单或对象列表。如果您已经创建了一个负责您网站中某些类型对象的应用,例如movies,您可以轻松地将它转换为 Django CMS 应用并将其附加到一个页面上。这将确保应用的根 URL 是可翻译的,并且在选择菜单项时菜单项会被突出显示。在本教程中,我们将把movies应用转换为 CMS 应用。
准备工作
让我们从在第三章的过滤对象列表教程中创建的movies应用开始,表单和视图。
如何操作...
按照以下步骤将常规moviesDjango 应用转换为 Django CMS 应用:
-
首先,删除或注释掉应用的 URL 配置的包含,因为它将由 Django CMS 中的 apphook 包含,如下所示:
# myproject/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import patterns, include, url from django.conf import settings from django.conf.urls.static import static from django.contrib.staticfiles.urls import \ staticfiles_urlpatterns from django.conf.urls.i18n import i18n_patterns from django.contrib import admin admin.autodiscover() urlpatterns = i18n_patterns("", # remove or comment out the inclusion of app's urls # url(r"^movies/", include("movies.urls")), url(r"^admin/", include(admin.site.urls)), url(r"^", include("cms.urls")), ) urlpatterns += staticfiles_urlpatterns() urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -
在
movies目录下创建一个cms_app.py文件,并在其中创建MoviesApphook,如下所示:# movies/cms_app.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ from cms.app_base import CMSApp from cms.apphook_pool import apphook_pool class MoviesApphook(CMSApp): name = _("Movies") urls = ["movies.urls"] apphook_pool.register(MoviesApphook) -
在设置中设置新创建的 apphook,如下所示:
# settings.py CMS_APPHOOKS = ( # ... "movies.cms_app.MoviesApphook", ) -
最后,在所有电影模板中,将第一行改为从当前 CMS 页面的模板扩展,而不是
base.html,如下所示:{# templates/movies/movies_list.html #} Change {% extends "base.html" %} to {% extends CMS_TEMPLATE %}
它是如何工作的...
Apphooks 是连接应用 URL 配置到 CMS 页面的接口。Apphooks 需要从CMSApp扩展。为了定义将在页面高级设置下的应用选择列表中显示的名称,将 apphook 的路径放入CMS_APPHOOKS项目设置中,并重新启动 Web 服务器;apphook 将作为高级页面设置中的一个应用出现。在选择页面应用后,您需要重新启动服务器以使 URL 生效。
如果您希望应用的模板包含页面的占位符或属性,例如title或description元标签,则应用的模板应该扩展页面模板。
参见
-
在第三章的过滤对象列表教程中,表单和视图的过滤对象列表教程
-
附加自己的导航教程
附加自己的导航
一旦您的应用被钩接到 CMS 页面,该页面节点下的所有 URL 路径将由该应用的urls.py文件控制。要在该页面下添加一些菜单项,您需要向页面树中添加一个动态的导航分支。在本教程中,我们将改进movies应用,并在电影页面下添加新的导航项。
准备工作
假设我们有一个针对不同电影列表的 URL 配置:编辑精选、商业电影和独立电影,如下面的代码所示:
# movies/urls.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.conf.urls import url, patterns
from django.shortcuts import redirect
urlpatterns = patterns("movies.views",
url(r"^$", lambda request: redirect("featured_movie_list")),
url(r"^editors-picks/$", "movie_list", {"featured": True},
name='featured_movie_list'),
url(r"^commercial/$", "movie_list", {"commercial": True},
name="commercial_movie_list"),
url(r"^independent/$", "movie_list", {"independent": True},
name="independent_movie_list"),
url(r"^(?P<slug>[^/]+)/$", "movie_detail",
name="movie_detail"),
)
如何操作...
按照以下两个步骤将编辑精选、商业电影和独立电影菜单选项附加到电影页面下的导航菜单:
-
在
movies应用中创建一个menu.py文件,并添加以下MoviesMenu类,如下所示:# movies/menu.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from menus.base import NavigationNode from menus.menu_pool import menu_pool from cms.menu_bases import CMSAttachMenu class MoviesMenu(CMSAttachMenu): name = _("Movies Menu") def get_nodes(self, request): nodes = [ NavigationNode( _("Editor's Picks"), reverse("featured_movie_list"), 1, ), NavigationNode( _("Commercial Movies"), reverse("commercial_movie_list"), 2, ), NavigationNode( _("Independent Movies"), reverse("independent_movie_list"), 3, ), ] return nodes menu_pool.register_menu(MoviesMenu) -
重新启动 Web 服务器,然后编辑电影页面的高级设置,并选择附加菜单设置中的电影菜单。
工作原理...
在前端,您将看到附加到电影页面的新菜单项,如下面的图片所示:

可附加到页面的动态菜单需要扩展CMSAttachMenu,定义它们将被选中的名称,并定义返回NavigationNode对象列表的get_nodes()方法。NavigationNode类至少需要三个参数:菜单项的标题、菜单项的 URL 路径和节点的 ID。ID 可以自由选择,唯一的要求是它们必须在这个附加菜单中是唯一的。其他可选参数如下:
-
parent_id:如果您想创建一个层次动态菜单,这是父节点的 ID -
parent_namespace:如果这个节点要附加到不同的菜单树,这是另一个菜单的名称,例如,这个菜单的名称是"MoviesMenu" -
attr:这是一个字典,包含可以在模板或菜单修改器中使用的附加属性 -
visible:这设置菜单项是否可见
对于其他可附加菜单的示例,请参考官方文档中的django-cms.readthedocs.org/en/latest/how_to/menus.html。
参见
-
结构化页面菜单菜谱
-
将应用转换为 CMS 应用菜谱
编写自己的 CMS 插件
Django CMS 自带许多内容插件,可以在模板占位符中使用,例如文本、Flash、图片和谷歌地图插件。然而,为了获得更结构化和更好的样式内容,你需要自己的自定义插件,这并不太难实现。在这个菜谱中,我们将看到如何创建一个新的插件,并为其数据创建一个自定义布局,这取决于页面选择的模板。
准备工作
让我们创建一个editorial应用,并在INSTALLED_APPS设置中提及它。此外,我们还需要cms/magazine.html模板,该模板已在CMS_TEMPLATES设置中创建和提及;您可以简单地复制cms/default.html模板来完成此操作。
如何做到...
要创建EditorialContent插件,请按照以下步骤操作:
-
在新创建的应用的
models.py文件中,添加一个继承自CMSPlugin的EditorialContent模型。EditorialContent模型将包含以下字段:标题、副标题、描述、网站、图片、图片标题以及一个 CSS 类:# editorial/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import os from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now as tz_now from cms.models import CMSPlugin from cms.utils.compat.dj import python_2_unicode_compatible def upload_to(instance, filename): now = tz_now() filename_base, filename_ext = \ os.path.splitext(filename) return "editorial/%s%s" % ( now.strftime("%Y/%m/%Y%m%d%H%M%S"), filename_ext.lower(), ) @python_2_unicode_compatible class EditorialContent(CMSPlugin): title = models.CharField(_("Title"), max_length=255) subtitle = models.CharField(_("Subtitle"), max_length=255, blank=True) description = models.TextField(_("Description"), blank=True) website = models.CharField(_("Website"), max_length=255, blank=True) image = models.ImageField(_("Image"), max_length=255, upload_to=upload_to, blank=True) image_caption = models.TextField(_("Image Caption"), blank=True) css_class = models.CharField(_("CSS Class"), max_length=255, blank=True) def __str__(self): return self.title class Meta: ordering = ["title"] verbose_name = _("Editorial content") verbose_name_plural = _("Editorial contents") -
在同一个应用中,创建一个
cms_plugins.py文件,并添加一个继承自CMSPluginBase的EditorialContentPlugin类。这个类有点像ModelAdmin——它定义了插件的行政设置的外观:# editorial/cms_plugins.py # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.utils.translation import ugettext as _ from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool from .models import EditorialContent class EditorialContentPlugin(CMSPluginBase): model = EditorialContent name = _("Editorial Content") render_template = "cms/plugins/editorial_content.html" fieldsets = ( (_("Main Content"), { "fields": ( "title", "subtitle", "description", "website"), "classes": ["collapse open"] }), (_("Image"), { "fields": ("image", "image_caption"), "classes": ["collapse open"] }), (_("Presentation"), { "fields": ("css_class",), "classes": ["collapse closed"] }), ) def render(self, context, instance, placeholder): context.update({ "object": instance, "placeholder": placeholder, }) return context plugin_pool.register_plugin(EditorialContentPlugin) -
要指定哪些插件放入哪些占位符,你必须定义
CMS_PLACEHOLDER_CONF设置。你还可以为在特定占位符中渲染的插件的模板定义额外的上下文。让我们允许EditorialContentPlugin用于main_content占位符,并在cms/magazine.html模板中为main_content占位符设置editorial_content_template上下文变量,如下所示:# settings.py CMS_PLACEHOLDER_CONF = { "main_content": { "name": gettext("Main Content"), "plugins": ( "EditorialContentPlugin", "TextPlugin", ), }, "cms/magazine.html main_content": { "name": gettext("Magazine Main Content"), "plugins": ( "EditorialContentPlugin", "TextPlugin" ), "extra_context": { "editorial_content_template": \ "cms/plugins/editorial_content/magazine.html", } }, } -
然后,我们将创建两个模板。其中一个将是
editorial_content.html模板。它检查editorial_content_template上下文变量是否存在。如果变量存在,则包含它。否则,显示编辑内容的默认布局:{# templates/cms/plugins/editorial_content.html #} {% load i18n %} {% if editorial_content_template %} {% include editorial_content_template %} {% else %} <div class="item{% if object.css_class %} {{ object.css_class }}{% endif %}"> <!-- editorial content for non-specific placeholders --> <div class="img"> {% if object.image %} <img class="img-responsive" alt="{{ object.image_caption|striptags }}" src="img/{{ object.image.url }}" /> {% endif %} {% if object.image_caption %}<p class="caption">{{ object.image_caption|removetags:"p" }}</p> {% endif %} </div> <h3><a href="{{ object.website }}">{{ object.title }}</a></h3> <h4>{{ object.subtitle }}</h4> <div class="description">{{ object.description|safe }}</div> </div> {% endif %} -
第二个模板是
cms/magazine.html模板中EditorialContent插件的特定模板。这里没有什么特别之处,只是为容器添加了一个额外的 Bootstrap 特定的wellCSS 类,使插件更加突出:{# templates/cms/plugins/editorial_content/magazine.html #} {% load i18n %} <div class="well item{% if object.css_class %} {{ object.css_class }}{% endif %}"> <!-- editorial content for non-specific placeholders --> <div class="img"> {% if object.image %} <img class="img-responsive" alt="{{ object.image_caption|striptags }}" src="img/{{ object.image.url }}" /> {% endif %} {% if object.image_caption %}<p class="caption">{{ object.image_caption|removetags:"p" }}</p> {% endif %} </div> <h3><a href="{{ object.website }}">{{ object.title }}</a></h3> <h4>{{ object.subtitle }}</h4> <div class="description">{{ object.description|safe }}</div> </div>
它是如何工作的...
如果你进入任何 CMS 页面的草稿模式并切换到结构部分,你可以在占位符中添加编辑内容插件。此插件的内容将使用指定的模板进行渲染,并且可以根据插件选择的页面模板进行自定义。例如,为新闻页面选择cms/magazine.html模板,然后添加编辑内容插件。新闻页面将类似于以下截图:

在这里,带有图片和描述的测试标题是插入到magazine.html页面模板中的main_content占位符中的自定义插件。如果页面模板不同,插件将不会渲染具有 Bootstrap 特定的well CSS 类;因此,它不会有灰色背景。
参见
-
为 Django CMS 创建模板 食谱
-
结构化页面菜单 食谱
向 CMS 页面添加新字段
CMS 页面有多个多语言字段,如标题、别名、菜单标题、页面标题、描述元标签和覆盖 URL。它们还有几个常见的非语言特定字段,如模板、在模板标签中使用的 ID、附加应用和附加菜单。然而,这可能对于更复杂的网站来说还不够。幸运的是,Django CMS 提供了一种可管理的机制来为 CMS 页面添加新的数据库字段。在本食谱中,你将了解如何为导航菜单项和页面主体的 CSS 类添加字段。
准备工作
让我们创建cms_extensions应用并将其放在设置中的INSTALLED_APPS下。
如何操作...
要创建具有导航菜单项和页面主体 CSS 类字段的 CMS 页面扩展,请按照以下步骤操作:
-
在
models.py文件中,创建一个扩展PageExtension的CSSExtension类,并为菜单项的 CSS 类和<body>CSS 类添加字段,如下所示:# cms_extensions/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from cms.extensions import PageExtension from cms.extensions.extension_pool import extension_pool MENU_ITEM_CSS_CLASS_CHOICES = ( ("featured", ".featured"), ) BODY_CSS_CLASS_CHOICES = ( ("serious", ".serious"), ("playful", ".playful"), ) class CSSExtension(PageExtension): menu_item_css_class = models.CharField( _("Menu Item CSS Class"), max_length=200, blank=True, choices=MENU_ITEM_CSS_CLASS_CHOICES, ) body_css_class = models.CharField( _("Body CSS Class"), max_length=200, blank=True, choices=BODY_CSS_CLASS_CHOICES, ) extension_pool.register(CSSExtension) -
在
admin.py文件中,让我们为刚刚创建的CSSExtension模型添加管理选项:# cms_extensions/admin.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.contrib import admin from cms.extensions import PageExtensionAdmin from .models import CSSExtension class CSSExtensionAdmin(PageExtensionAdmin): pass admin.site.register(CSSExtension, CSSExtensionAdmin) -
然后,我们需要在每个页面的工具栏中显示 CSS 扩展。这可以通过在应用的
cms_toolbar.py文件中放置以下代码来完成:# cms_extensions/cms_toolbar.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from cms.api import get_page_draft from cms.toolbar_pool import toolbar_pool from cms.toolbar_base import CMSToolbar from cms.utils import get_cms_setting from cms.utils.permissions import has_page_change_permission from django.core.urlresolvers import reverse, NoReverseMatch from django.utils.translation import ugettext_lazy as _ from .models import CSSExtension @toolbar_pool.register class CSSExtensionToolbar(CMSToolbar): def populate(self): # always use draft if we have a page self.page = get_page_draft( self.request.current_page) if not self.page: # Nothing to do return # check global permissions # if CMS_PERMISSIONS is active if get_cms_setting("PERMISSION"): has_global_current_page_change_permission = \ has_page_change_permission(self.request) else: has_global_current_page_change_permission = \ False # check if user has page edit permission can_change = self.request.current_page and \ self.request.current_page.\ has_change_permission(self.request) if has_global_current_page_change_permission or \ can_change: try: extension = CSSExtension.objects.get( extended_object_id=self.page.id) except CSSExtension.DoesNotExist: extension = None try: if extension: url = reverse( "admin:cms_extensions_cssextension_change", args=(extension.pk,) ) else: url = reverse( "admin:cms_extensions_cssextension_add") + \ "?extended_object=%s" % self.page.pk except NoReverseMatch: # not in urls pass else: not_edit_mode = not self.toolbar.edit_mode current_page_menu = self.toolbar.\ get_or_create_menu("page") current_page_menu.add_modal_item( _("CSS"), url=url, disabled=not_edit_mode )此代码检查用户是否有更改当前页面的权限,如果有,它将从当前工具栏加载页面菜单,并添加一个新的菜单项,CSS,带有创建或编辑
CSSExtension的链接。 -
由于我们想在导航菜单中访问 CSS 扩展以附加 CSS 类,我们需要在相同应用的
menu.py文件中创建一个菜单修改器:# cms_extensions/menu.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from cms.models import Page from menus.base import Modifier from menus.menu_pool import menu_pool class CSSModifier(Modifier): def modify(self, request, nodes, namespace, root_id, post_cut, breadcrumb): if post_cut: return nodes for node in nodes: try: page = Page.objects.get(pk=node.id) except: continue try: page.cssextension except: pass else: node.cssextension = page.cssextension return nodes menu_pool.register_modifier(CSSModifier) -
然后,我们将把主体 CSS 类添加到
base.html模板中的<body>元素,如下所示:{# templates/base.html #} <body class="{% block bodyclass %}{% endblock %}{% if request.current_page.cssextension %}{{ request.current_page.cssextension.body_css_class }}{% endif %}"> -
最后,我们将修改
menu.html文件,这是导航菜单的默认模板,并添加菜单项的 CSS 类,如下所示:{# templates/menu/menu.html #} {% load i18n menu_tags cache %} {% for child in children %} <li class="{% if child.ancestor %}ancestor{% endif %}{% if child.selected %} active{% endif %}{% if child.children %} dropdown{% endif %}{% if child.cssextension %} {{ child.cssextension.menu_item_css_class }}{% endif %}"> {% if child.children %}<a class="dropdown-toggle" data-toggle="dropdown" href="#">{{ child.get_menu_title }} <span class="caret"></span></a> <ul class="dropdown-menu"> {% show_menu from_level to_level extra_inactive extra_active template "" "" child %} </ul> {% else %} <a href="{{ child.get_absolute_url }}"><span>{{ child.get_menu_title }}</span></a> {% endif %} </li> {% endfor %}
它是如何工作的...
PageExtension 类是一个与 Page 模型具有一对一关系的模型混入。为了能够在 Django CMS 中管理自定义扩展模型,有一个特定的 PageExtensionAdmin 类可以扩展。然后,在 cms_toolbar.py 文件中,我们将创建 CSSExtensionToolbar 类,继承自 CMSToolbar 类,以在 Django CMS 工具栏中创建一个项。在 populate() 方法中,我们将执行常规的检查页面权限的流程,然后我们将向工具栏中添加一个 CSS 菜单项。
如果管理员有编辑页面的权限,那么他们将在 页面 菜单项下看到工具栏中的 CSS 选项,如下面的截图所示:

当管理员点击新的 CSS 菜单项时,会弹出一个窗口,他们可以从中选择导航菜单项和主体的 CSS 类,如下面的截图所示:

要在导航菜单中显示来自 Page 扩展的特定 CSS 类,我们需要相应地将 CSSExtension 对象附加到导航项上。然后,这些对象可以在 menu.html 模板中以 {{ child.cssextension }} 的形式访问。最后,你将有一些导航菜单项被突出显示,例如这里显示的 音乐 项(取决于你的 CSS):

显示当前页面 <body> 的特定 CSS 类要简单得多。我们可以立即使用 {{ request.current_page.cssextension.body_css_class }}。
参见
- 为 Django CMS 创建模板 的食谱
第八章。层次结构
在本章中,我们将介绍以下食谱:
-
创建层次类别
-
使用 django-mptt-admin 创建类别管理界面
-
使用 django-mptt-tree-editor 创建类别管理界面
-
在模板中渲染类别
-
在表单中使用单个选择字段选择类别
-
在表单中使用复选框列表选择多个类别
简介
无论你构建自己的论坛、线程评论还是分类系统,总会有需要将层次结构保存到数据库中的时刻。尽管关系数据库(如 MySQL 和 PostgreSQL)的表是扁平的,但有一种快速有效的方法来存储层次结构。它被称为修改后的前序树遍历(MPTT)。MPTT 允许你读取树结构,而无需对数据库进行递归调用。
首先,让我们熟悉树结构的术语。树数据结构是一个以根节点为起点的递归节点集合,具有对子节点的引用。有一个限制,即没有节点会回引用以创建循环,也没有重复的引用。以下是一些其他需要学习的术语:
-
父节点是指向子节点的任何节点。
-
后代是通过递归从父节点遍历到其子节点可以到达的节点。因此,节点的后代将是其子节点、子节点的子节点,依此类推。
-
祖先是通过递归从子节点遍历到其父节点可以到达的节点。因此,节点的祖先将是其父节点、父节点的父节点,依此类推,直到根节点。
-
兄弟是指具有相同父节点的节点。
-
叶是指没有子节点的节点。
现在,我将解释 MPTT 是如何工作的。想象一下,你将树水平展开,根节点在最上方。树中的每个节点都有左值和右值。想象它们作为节点左右两侧的小左右手柄。然后,你逆时针绕树行走(遍历),从根节点开始,并用数字标记你找到的每个左值或右值:1、2、3,依此类推。它看起来会类似于以下图表:

在这个层次结构的数据库表中,你将为每个节点有一个标题、左值和右值。
现在,如果你想获取具有左值为2和右值为11的B节点的子树,你必须选择所有左值在2和11之间的节点。它们是C、D、E和F。
要获取具有左值为5和右值为10的D节点的所有祖先,你必须选择所有左值小于5且右值大于10的节点。这些将是B和A。
要获取节点的后代数量,你可以使用以下公式:descendants = (right - left - 1) / 2
因此,B节点的后代数量可以按照以下方式计算:(11 - 2 - 1) / 2 = 4
如果我们要将E节点连接到C节点,我们只需要更新它们第一个共同祖先B节点的左右值。然后,C节点的左值仍然是3;E节点的左值将变为4,右值变为5;C节点的右值变为6;D节点的左值变为7;F节点的左值保持为8;其他节点也将保持不变。
类似地,在 MPTT 中还有其他与节点相关的树操作。对于项目中每个层次结构,自己管理所有这些可能过于复杂。幸运的是,有一个名为django-mptt的 Django 应用可以处理这些算法,并提供一个简单的 API 来处理树结构。在本章中,你将学习如何使用这个辅助应用。
创建层次化类别
为了说明如何处理 MPTT,我们将创建一个movies应用,它将有一个层次化的Category模型和一个与类别具有多对多关系的Movie模型。
准备工作
要开始,执行以下步骤:
-
使用以下命令在你的虚拟环境中安装
django-mptt:(myproject_env)$ pip install django-mptt -
然后,创建一个
movies应用。在设置中将movies应用以及mptt添加到INSTALLED_APPS中,如下所示:# conf/base.py or settings.py INSTALLED_APPS = ( # ... "mptt", "movies", )
如何操作...
我们将创建一个层次化的Category模型和一个Movie模型,这些模型将与类别具有多对多关系,如下所示:
-
打开
models.py文件,添加一个Category模型,它扩展了mptt.models.MPTTModel和CreationModificationDateMixin,这些我们在第二章中定义了,数据库结构。除了来自混入器的字段外,Category模型还需要一个parent字段,字段类型为TreeForeignKey,以及一个title字段:# movies/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import \ python_2_unicode_compatible from utils.models import CreationModificationDateMixin from mptt.models import MPTTModel from mptt.fields import TreeForeignKey, TreeManyToManyField @python_2_unicode_compatible class Category(MPTTModel, CreationModificationDateMixin): parent = TreeForeignKey("self", blank=True, null=True) title = models.CharField(_("Title"), max_length=200) def __str__(self): return self.title class Meta: ordering = ["tree_id", "lft"] verbose_name = _("Category") verbose_name_plural = _("Categories") -
然后,创建一个扩展
CreationModificationDateMixin的Movie模型。还包括一个title字段和一个categories字段,字段类型为TreeManyToManyField:@python_2_unicode_compatible class Movie(CreationModificationDateMixin): title = models.CharField(_("Title"), max_length=255) categories = TreeManyToManyField(Category, verbose_name=_("Categories")) def __str__(self): return self.title class Meta: verbose_name = _("Movie") verbose_name_plural = _("Movies")
它是如何工作的...
MPTTModel混入器将为Category模型添加tree_id、lft、rght和level字段。tree_id字段用于你可以有多个树在数据库表中。实际上,每个根类别都保存在一个单独的树中。lft和rght字段存储 MPTT 算法中使用的左右值。level字段存储节点在树中的深度。根节点的级别为0。
除了新字段外,MPTTModel混入器还添加了用于在树结构中导航的方法,类似于使用 JavaScript 通过 DOM 元素导航。以下列出了这些方法:
-
如果你想要获取一个分类的祖先节点,请使用以下代码:
ancestor_categories = category.get_ancestors( ascending=False, include_self=False, )升序参数定义了从哪个方向读取节点(默认为
False)。include_self参数定义是否将分类本身包含在QuerySet中(默认为False)。 -
要仅获取根分类,请使用以下代码:
root = category.get_root() -
如果你想要获取一个分类的直接子节点,请使用以下代码:
children = category.get_children() -
要获取一个分类的所有子节点,请使用以下代码:
descendants = category.get_descendants(include_self=False)在这里,
include_self参数再次定义是否将分类本身包含在QuerySet中。 -
如果你想在不查询数据库的情况下获取子节点数量,请使用以下代码:
descendants_count = category.get_descendant_count() -
要获取所有兄弟节点,请调用以下方法:
siblings = category.get_siblings(include_self=False)根分类被视为其他根分类的兄弟。
-
要仅获取前一个和后一个兄弟节点,请调用以下方法:
previous_sibling = category.get_previous_sibling() next_sibling = category.get_next_sibling() -
此外,还有方法检查分类是否为根节点、子节点或叶节点,如下所示:
category.is_root_node() category.is_child_node() category.is_leaf_node()
所有这些方法都可以在视图、模板或管理命令中使用。如果你想操作树结构,你也可以使用 insert_at() 和 move_to() 方法。在这种情况下,你可以阅读有关它们和树管理器方法的文档,请参阅django-mptt.github.io/django-mptt/models.html。
在前面的模型中,我们使用了 TreeForeignKey 和 TreeManyToManyField。这些与 ForeignKey 和 ManyToManyField 类似,不同之处在于它们在管理界面中以缩进的形式显示选择项。
此外,请注意,在 Category 模型的 Meta 类中,我们按 tree_id 和 lft 值的顺序对分类进行排序,以便在树结构中自然显示分类。
参见
-
在第二章的创建用于处理创建和修改日期的模型混入食谱中,数据库结构
-
在第七章的结构化页面菜单食谱中,Django CMS
-
使用 django-mptt-admin 创建分类管理界面 食谱
使用 django-mptt-admin 创建分类管理界面
django-mptt 应用程序附带一个简单的模型管理混入,允许你创建树结构并以缩进形式列出。要重新排序树,你需要自己创建此功能或使用第三方解决方案。目前,有两个应用程序可以帮助你为层次模型创建可拖拽的管理界面。其中之一是 django-mptt-admin。让我们在本食谱中看看它。
准备工作
首先,我们需要通过以下步骤安装 django-mptt-admin 应用程序:
-
首先,使用以下命令在你的虚拟环境中安装应用程序:
(myproject_env)$ pip install django-mptt-admin -
然后,将其放入设置中的
INSTALLED_APPS,如下所示:# conf/base.py or settings.py INSTALLED_APPS = ( # ... "django_mptt_admin" )
如何操作...
创建一个扩展 DjangoMpttAdmin 而不是 admin.ModelAdmin 的 Category 模型的管理界面,如下所示:
# movies/admin.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from django_mptt_admin.admin import DjangoMpttAdmin
from .models import Category
class CategoryAdmin(DjangoMpttAdmin):
list_display = ["title", "created", "modified"]
list_filter = ["created"]
admin.site.register(Category, CategoryAdmin)
它是如何工作的...
分类的管理界面将有两种模式:树视图和网格视图。树视图看起来类似于以下截图:

树视图使用 jqTree jQuery 库进行节点操作。你可以展开和折叠分类以获得更好的概览。要重新排序或更改依赖关系,你可以在这个列表视图中拖放标题。在重新排序过程中,用户界面看起来类似于以下截图:

注意
注意,任何常规的列表相关设置,如 list_display 或 list_filter,都将被忽略。
如果你想要过滤分类、按特定字段排序或过滤,或应用管理操作,你可以切换到网格视图,它显示默认的分类变更列表。
参见
-
创建分层分类 的配方
-
使用 django-mptt-tree-editor 创建分类管理界面 的配方
使用 django-mptt-tree-editor 创建分类管理界面
如果你想在你的管理界面中使用变更列表的常用功能,例如列、管理操作、可编辑字段或过滤器,以及在同一视图中操作树结构,你需要使用另一个名为 django-mptt-tree-editor 的第三方应用程序。让我们看看如何做到这一点。
准备工作
首先,我们需要安装 django-mptt-tree-editor 应用程序。执行以下步骤:
-
首先,使用以下命令在你的虚拟环境中安装应用程序:
(myproject_env)$ pip install django-mptt-tree-editor -
然后,将其放入设置中的
INSTALLED_APPS,如下所示:# conf/base.py or settings.py INSTALLED_APPS = ( # ... "mptt_tree_editor" )
如何做到这一点...
创建一个扩展 TreeEditor 而不是 admin.ModelAdmin 的 Category 模型的管理界面。确保你在 list_display 设置的开始处添加 indented_short_title 和 actions_column,如下所示:
# movies/admin.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from mptt_tree_editor.admin import TreeEditor
from .models import Category
class CategoryAdmin(TreeEditor):
list_display = ["indented_short_title", "actions_column", "created", "modified"]
list_filter = ["created"]
admin.site.register(Category, CategoryAdmin)
它是如何工作的...
你的分类管理界面现在看起来类似于以下截图:

分类管理界面允许你展开或折叠分类。indented_short_title 列将返回分类的缩进短标题(如果有的话)或分类的缩进 Unicode 表示。定义为 actions_column 的列将被渲染为拖放以重新排序或重构分类的手柄。由于拖动手柄位于不同于分类标题的列中,使用它可能会感觉有些奇怪。在重新排序过程中,用户界面看起来类似于以下截图:

如你所见,你可以在同一视图中使用默认 Django 管理界面的所有列表相关功能。
在django-mptt-tree-editor中,树编辑功能是从另一个使用 Django 制作的 CMS(内容管理系统)FeinCMS 移植过来的。
参见
-
创建层次类别的配方
-
使用 django-mptt-admin 创建类别管理界面的配方
在模板中渲染类别
一旦你在你的应用中创建了类别,你需要在模板中按层次结构显示它们。最简单的方法是使用django-mptt应用中的{% recursetree %}模板标签。我将在本配方中向你展示如何做到这一点。
准备工作
确保你已经创建了Category模型,并在数据库中输入了一些类别。
如何做...
将你的层次类别QuerySet传递给模板,然后使用以下方式使用{% recursetree %}模板标签:
-
创建一个视图,加载所有类别并将它们传递给一个模板:
# movies/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.shortcuts import render from .models import Category def movie_category_list(request): context = { "categories": Category.objects.all(), } return render( request, "movies/movie_category_list.html", context ) -
创建一个包含以下内容的模板:
{# templates/movies/movie_category_list.html #} {% extends "base_single_column.html" %} {% load i18n utility_tags mptt_tags %} {% block sidebar %} {% endblock %} {% block content %} <ul class="root"> {% recursetree categories %} <li> {{ node.title }} {% if not node.is_leaf_node %} <ul class="children"> {{ children }} </ul> {% endif %} </li> {% endrecursetree %} </ul> {% endblock %} -
创建一个 URL 规则来显示视图。
它是如何工作的...
模板将被渲染为嵌套列表,如下面的截图所示:

{% recursetree %}模板标签块模板标签接受类别的QuerySet,并使用标签中的模板内容渲染列表。这里使用了两个特殊变量:node和children。node变量是Category模型的一个实例。你可以使用它的字段或方法,如{{ node.get_descendant_count }}、{{ node.level }}或{{ node.is_root }}来添加特定的 CSS 类或 HTML5 data-*属性以供 JavaScript 使用。第二个变量children定义了当前类别子类的放置位置。
更多内容...
如果你的层次结构非常复杂,有超过 20 个深度级别,建议使用非递归模板过滤器tree_info。有关如何做到这一点的更多信息,请参阅官方文档django-mptt.github.io/django-mptt/templates.html#tree-info-filter。
参见
-
在第四章 模板和 JavaScript 中的使用 HTML5 数据属性配方
-
创建层次类别的配方
-
在表单中使用单选字段选择类别的配方
在表单中使用单选字段选择类别
如果你想在表单中显示类别选择会发生什么?层次结构将如何呈现?在django-mptt中有一个特殊的TreeNodeChoiceField表单字段,你可以用它来在选择的字段中显示层次结构。让我们看看如何做到这一点。
准备工作
我们将从之前配方中定义的movies应用开始。
如何做...
让我们创建一个包含类别字段的表单,并在视图中显示它:
-
在应用的
forms.py文件中,创建一个包含类别字段的表单,如下所示:# movies/forms.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext_lazy as _ from django.utils.html import mark_safe from mptt.forms import TreeNodeChoiceField from .models import Category class MovieFilterForm(forms.Form): category = TreeNodeChoiceField( label=_("Category"), queryset=Category.objects.all(), required=False, level_indicator=mark_safe( " " ), ) -
然后,创建一个 URL 规则、视图和模板来显示这个表单。
它是如何工作的...
类别选择将类似于以下:

TreeNodeChoiceField 的行为类似于 ModelChoiceField;然而,它以缩进的形式显示层次选择。默认情况下,TreeNodeChoiceField 使用三个连字符 --- 作为每个更深层级的前缀。在我们的例子中,我们将通过将 level_indicator 参数传递给字段来更改级别指示符为四个非换行空格( HTML 实体)。为了确保非换行空格不会被转义,我们使用了 mark_safe() 函数。
参见
- 在表单中使用复选框列表来选择多个类别 的食谱
在表单中使用复选框列表来选择多个类别
当在表单中需要选择多个类别时,你可以使用由 django-mptt 提供的 TreeNodeMultipleChoiceField 多选字段。然而,从 GUI 的角度来看,多选字段并不非常用户友好,因为用户需要滚动并按住控制键同时点击才能进行多选。这真的很糟糕。一个更好的方法将是提供一个复选框列表来选择类别。在这个食谱中,我们将创建一个允许你在表单中显示缩进复选框的字段。
准备工作
我们将从之前食谱中定义的 movies 应用程序开始,以及你应该在你的项目中拥有的 utils 应用程序。
如何操作...
要渲染带有复选框的缩进类别列表,创建并使用一个新的 MultipleChoiceTreeField 表单字段,并为此字段创建一个 HTML 模板。具体的模板将传递到表单的 crispy 布局中。为此,执行以下步骤:
-
在
utils应用中添加一个fields.py文件,并创建一个扩展ModelMultipleChoiceField的MultipleChoiceTreeField表单字段,如下所示:# utils/fields.py # -*- coding: utf-8 -*- from __future__ import unicode_literals from django import forms class MultipleChoiceTreeField( forms.ModelMultipleChoiceField ): widget = forms.CheckboxSelectMultiple def label_from_instance(self, obj): return obj -
使用新的带有类别选择的字段来在电影创建表单中选择。此外,在表单布局中,将自定义模板传递给类别字段,如下所示:
# movies/forms.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django import forms from django.utils.translation import ugettext_lazy as _ from crispy_forms.helper import FormHelper from crispy_forms import layout, bootstrap from utils.fields import MultipleChoiceTreeField from .models import Movie, Category class MovieForm(forms.ModelForm): categories = MultipleChoiceTreeField( label=_("Categories"), required=False, queryset=Category.objects.all(), ) class Meta: model = Movie def __init__(self, *args, **kwargs): super(MovieForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_action = "" self.helper.form_method = "POST" self.helper.layout = layout.Layout( layout.Field("title"), layout.Field( "categories", template="utils/"\ "checkbox_select_multiple_tree.html" ), bootstrap.FormActions( layout.Submit("submit", _("Save")), ) ) -
创建一个类似以下所示的 Bootstrap 风格的复选框列表模板:
{# templates/utils/checkbox_select_multiple_tree.html #} {% load crispy_forms_filters %} {% load l10n %} <div id="div_{{ field.auto_id }}" class="form-group{% if wrapper_class %} {{ wrapper_class }}{% endif %}{% if form_show_errors%}{% if field.errors %} has-error{% endif %}{% endif %}{% if field.css_classes %} {{ field.css_classes }}{% endif %}"> {% if field.label and form_show_labels %} <label for="{{ field.id_for_label }}" class="control-label {{ label_class }}{% if field.field.required %} requiredField{% endif %}"> {{ field.label|safe }}{% if field.field.required %}<span class="asteriskField">*</span>{% endif %} </label> {% endif %} <div class="controls {{ field_class }}"{% if flat_attrs %} {{ flat_attrs|safe }}{% endif %}> {% include 'bootstrap3/layout/field_errors_block.html' %} {% for choice_value, choice_instance in field.field.choices %} <label class="checkbox{% if inline_class %}-{{ inline_class }}{% endif %} level-{{ choice_instance.level }}"> <input type="checkbox"{% if choice_value in field.value or choice_value|stringformat:"s" in field.value or choice_value|stringformat:"s" == field.value|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}"id="id_{{ field.html_name }}_{{ forloop.counter }}"value="{{ choice_value|unlocalize }}"{{ field.field.widget.attrs|flatatt }}> {{ choice_instance }} </label> {% endfor %} {% include "bootstrap3/layout/help_text.html" %} </div> </div> -
创建一个 URL 规则、视图和模板,使用
{% crispy %}模板标签来显示表单。要了解如何使用此模板标签,请参考第三章中的 使用 django-crispy-forms 创建表单布局 食谱。 -
最后,在你的 CSS 文件中添加一个规则,通过设置 margin-left 参数来缩进具有类名的标签,例如
.level-0、.level-1、.level-2等。确保你有足够多的这些 CSS 类,以适应你上下文中可能的树的最大深度,如下所示:/* style.css */ .level-0 { margin-left: 0; } .level-1 { margin-left: 20px; } .level-2 { margin-left: 40px; }
它是如何工作的...
因此,我们得到以下表单:

与 Django 的默认行为相反,Django 在 Python 代码中硬编码了字段生成,而django-crispy-forms应用使用模板来渲染字段。您可以在crispy_forms/templates/bootstrap3下浏览它们,并将其中一些复制到您项目模板目录中的类似路径,并在必要时覆盖它们。
在我们的电影创建表单中,我们为类别字段传递了一个自定义模板,该模板将为<label>标签添加.level-* CSS 类,并包裹复选框。CheckboxSelectMultiple小部件的一个问题是,当渲染时,它只使用选择值和选择文本,而在我们的情况下,我们还需要其他属性,如类别深度级别。为了解决这个问题,我们将创建一个自定义的MultipleChoiceTreeField表单字段,它扩展了ModelMultipleChoiceField并重写了label_from_instance方法,以返回类别本身而不是其 Unicode 表示。字段的模板看起来很复杂;然而,它只是普通字段模板(crispy_forms/templates/bootstrap3/field.html)和多个复选框字段模板(crispy_forms/templates/bootstrap3/layout/checkboxselectmultiple.html)的组合,包含所有必要的 Bootstrap 3 标记。我们只是稍作修改,添加了.level-* CSS 类。
参见
-
在第三章 表单和视图 中的
Creating a form layout with django-crispy-forms配方 -
使用单个选择字段在表单中选择类别 的配方
第九章:数据导入和导出
在这一章中,我们将介绍以下菜谱:
-
从本地 CSV 文件导入数据
-
从本地 Excel 文件导入数据
-
从外部 JSON 文件导入数据
-
从外部 XML 文件导入数据
-
创建可筛选的 RSS 源
-
使用 Tastypie 创建 API
-
使用 Django REST 框架创建 API
简介
有时候,您的数据需要从本地格式传输到数据库,从外部资源导入,或提供给第三方。在这一章中,我们将探讨一些编写管理命令和 API 的实际示例,以实现这些操作。
从本地 CSV 文件导入数据
逗号分隔值(CSV)格式可能是将表格数据存储在文本文件中最简单的方式。在这个菜谱中,我们将创建一个管理命令,用于从 CSV 导入数据到 Django 数据库。我们需要一个包含电影标题、URL 和发行年份的 CSV 电影列表。你可以使用 Excel、Calc 或其他电子表格应用程序轻松创建此类文件。
准备工作
创建一个包含以下字段的 Movie 模型的 movies 应用:title、url 和 release_year。将应用放置在设置中的 INSTALLED_APPS 下。
如何操作...
按照以下步骤创建和使用一个管理命令,用于从本地 CSV 文件导入电影:
-
在
movies应用中,创建一个management目录,然后在新的management目录中创建一个commands目录。在这两个新目录中放置空的__init__.py文件,以使它们成为 Python 包。 -
在那里添加一个
import_movies_from_csv.py文件,内容如下:# movies/management/commands/import_movies_from_csv.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import csv from django.core.management.base import BaseCommand from movies.models import Movie SILENT, NORMAL, VERBOSE, VERY_VERBOSE = 0, 1, 2, 3 class Command(BaseCommand): help = ( "Imports movies from a local CSV file. " "Expects title, URL, and release year." ) def add_arguments(self, parser): # Positional arguments parser.add_argument( "file_path", nargs=1, type=unicode, ) def handle(self, *args, **options): verbosity = options.get("verbosity", NORMAL) file_path = options["file_path"][0] if verbosity >= NORMAL: self.stdout.write("=== Movies imported ===") with open(file_path) as f: reader = csv.reader(f) for rownum, (title, url, release_year) in \ enumerate(reader): if rownum == 0: # let's skip the column captions continue movie, created = \ Movie.objects.get_or_create( title=title, url=url, release_year=release_year, ) if verbosity >= NORMAL: self.stdout.write("{}. {}".format( rownum, movie.title )) -
要运行导入,请在命令行中调用以下内容:
(myproject_env)$ python manage.py import_movies_from_csv \ data/movies.csv
它是如何工作的...
对于一个管理命令,我们需要创建一个从 BaseCommand 继承并重写 add_arguments() 和 handle() 方法的 Command 类。help 属性定义了管理命令的帮助文本。当你输入以下内容到命令行时,可以看到它:
(myproject_env)$ python manage.py help import_movies_from_csv
Django 管理命令使用内置的 argparse 模块来解析传递的参数。add_arguments() 方法定义了应该传递给管理命令的位置参数或命名参数。在我们的情况下,我们将添加一个 Unicode 类型的位置参数 file_path。通过将 nargs 设置为 1 属性,我们允许只有一个值。要了解您可以定义的其他参数以及如何进行此操作,请参阅官方 argparse 文档:docs.python.org/2/library/argparse.html#the-add-argument-method。
在 handle() 方法的开始处,检查 verbosity 参数。详细程度定义了命令的详细程度,从 0 不向命令行工具输出任何内容到 3 非常详细。你可以按如下方式将此参数传递给命令:
(myproject_env)$ python manage.py import_movies_from_csv \
data/movies.csv --verbosity=0
然后,我们还期望文件名作为第一个位置参数。options["file_path"]返回 nargs 中定义的值的列表,因此在这种情况下它是一个值。
我们打开给定的文件,并将其指针传递给csv.reader。然后,对于文件中的每一行,如果不存在匹配的电影,我们将创建一个新的Movie对象。管理命令将打印出导入的电影标题到控制台,除非你将详细程度设置为0。
小贴士
如果你想在开发过程中调试管理命令的错误,为它传递--traceback参数。如果发生错误,你将看到问题的完整堆栈跟踪。
还有更多...
你可以从官方文档docs.python.org/2/library/csv.html了解更多关于 CSV 库的信息。
相关链接
- 从本地 Excel 文件导入数据的菜谱
从本地 Excel 文件导入数据
另一个流行的存储表格数据的格式是 Excel 电子表格。在这个菜谱中,我们将从这种格式的文件中导入电影。
准备工作
让我们从之前菜谱中创建的movies应用开始。按照以下步骤安装xlrd包以读取 Excel 文件:
(project_env)$ pip install xlrd
如何操作...
按照以下步骤创建和使用一个导入本地 XLS 文件的管理命令:
-
如果你还没有这样做,在
movies应用中,创建一个management目录,然后在新的management目录中创建一个commands目录。在这两个新目录中放入空的__init__.py文件,使它们成为 Python 包。 -
将以下内容的
import_movies_from_xls.py文件添加到你的项目中:# movies/management/commands/import_movies_from_xls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import xlrd from django.utils.six.moves import range from django.core.management.base import BaseCommand from movies.models import Movie SILENT, NORMAL, VERBOSE, VERY_VERBOSE = 0, 1, 2, 3 class Command(BaseCommand): help = ( "Imports movies from a local XLS file. " "Expects title, URL, and release year." ) def add_arguments(self, parser): # Positional arguments parser.add_argument( "file_path", nargs=1, type=unicode, ) def handle(self, *args, **options): verbosity = options.get("verbosity", NORMAL) file_path = options["file_path"][0] wb = xlrd.open_workbook(file_path) sh = wb.sheet_by_index(0) if verbosity >= NORMAL: self.stdout.write("=== Movies imported ===") for rownum in range(sh.nrows): if rownum == 0: # let's skip the column captions continue (title, url, release_year) = \ sh.row_values(rownum) movie, created = Movie.objects.get_or_create( title=title, url=url, release_year=release_year, ) if verbosity >= NORMAL: self.stdout.write("{}. {}".format( rownum, movie.title )) -
要运行导入,请在命令行中调用以下命令:
(myproject_env)$ python manage.py import_movies_from_xls \ data/movies.xls
工作原理...
从 XLS 文件导入的原则与 CSV 相同。我们打开文件,逐行读取,并从提供的数据中创建Movie对象。详细解释如下。
-
Excel 文件是包含不同标签页的工作簿。
-
我们使用
xlrd库打开命令中传递的文件。然后,我们将从工作簿中读取第一张工作表。 -
之后,我们将逐行读取(除了带有列标题的第一行)并从这些行中创建
Movie对象。再次提醒,管理命令将打印出导入的电影标题到控制台,除非你将详细程度设置为0。
还有更多...
你可以在www.python-excel.org/了解更多关于如何处理 Excel 文件的信息。
相关链接
- 从本地 CSV 文件导入数据的菜谱
从外部 JSON 文件导入数据
Last.fm音乐网站在ws.audioscrobbler.com/域名下有一个 API,你可以使用它来读取专辑、艺术家、曲目、事件等。该 API 允许你使用 JSON 或 XML 格式。在这个菜谱中,我们将使用 JSON 格式导入被标记为 disco 的顶级曲目。
准备工作
按照以下步骤从 Last.fm 导入 JSON 格式的数据:
-
要使用
Last.fm,你需要注册并获取一个 API 密钥。API 密钥可以在www.last.fm/api/account/create创建。 -
API 密钥必须在设置中设置为
LAST_FM_API_KEY。 -
此外,使用以下命令在你的虚拟环境中安装
requests库:(myproject_env)$ pip install requests -
让我们检查 JSON 端点的结构 (
ws.audioscrobbler.com/2.0/?method=tag.gettoptracks&tag=disco&api_key=xxx&format=json):{ "tracks":{ "track":[ { "name":"Billie Jean", "duration":"293", "mbid":"f980fc14-e29b-481d-ad3a-5ed9b4ab6340", "url":"http://www.last.fm/music/Michael+Jackson/_/Billie+Jean", "streamable":{ "#text":"0", "fulltrack":"0" }, "artist":{ "name":"Michael Jackson", "mbid":"f27ec8db-af05-4f36-916e-3d57f91ecf5e", "url":"http://www.last.fm/music/Michael+Jackson" }, "image":[ { "#text":"http://img2-ak.lst.fm/i/u/34s/114a4599f3bd451ca915f482345bc70f.png", "size":"small" }, { "#text":"http://img2-ak.lst.fm/i/u/64s/114a4599f3bd451ca915f482345bc70f.png", "size":"medium" }, { "#text":"http://img2-ak.lst.fm/i/u/174s/114a4599f3bd451ca915f482345bc70f.png", "size":"large" }, { "#text":"http://img2-ak.lst.fm/i/u/300x300/114a4599f3bd451ca915f482345bc70f.png", "size":"extralarge" } ], "@attr":{ "rank":"1" } }, ... ], "@attr":{ "tag":"disco", "page":"1", "perPage":"50", "totalPages":"26205", "total":"1310249" } } }
我们想读取曲目名称、艺术家、URL 和中等大小的图片。
如何操作...
按照以下步骤创建 Track 模型和管理命令,该命令从 Last.fm 导入顶级曲目到数据库:
-
让我们创建一个具有简单
Track模型的music应用程序,如下所示:# music/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import os from django.utils.translation import ugettext_lazy as _ from django.db import models from django.utils.text import slugify from django.utils.encoding import \ python_2_unicode_compatible def upload_to(instance, filename): filename_base, filename_ext = \ os.path.splitext(filename) return "tracks/%s--%s%s" % ( slugify(instance.artist), slugify(instance.name), filename_ext.lower(), ) @python_2_unicode_compatible class Track(models.Model): name = models.CharField(_("Name"), max_length=250) artist = models.CharField(_("Artist"), max_length=250) url = models.URLField(_("URL")) image = models.ImageField(_("Image"), upload_to=upload_to, blank=True, null=True) class Meta: verbose_name = _("Track") verbose_name_plural = _("Tracks") def __str__(self): return "%s - %s" % (self.artist, self.name) -
然后,创建如下所示的管理命令:
# music/management/commands/import_music_from_lastfm_as_json.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import os import requests from StringIO import StringIO from django.utils.six.moves import range from django.core.management.base import BaseCommand from django.utils.encoding import force_text from django.conf import settings from django.core.files import File from music.models import Track SILENT, NORMAL, VERBOSE, VERY_VERBOSE = 0, 1, 2, 3 class Command(BaseCommand): help = "Imports top tracks from last.fm as XML." def add_arguments(self, parser): # Named (optional) arguments parser.add_argument( "--max_pages", type=int, default=0, ) def handle(self, *args, **options): self.verbosity = options.get("verbosity", NORMAL) max_pages = options["max_pages"] params = { "method": "tag.gettoptracks", "tag": "disco", "api_key": settings.LAST_FM_API_KEY, "format": "json", } r = requests.get( "http://ws.audioscrobbler.com/2.0/", params=params ) response_dict = r.json() total_pages = int( response_dict["tracks"]["@attr"]["totalPages"] ) if max_pages > 0: total_pages = max_pages if self.verbosity >= NORMAL: self.stdout.write("=== Tracks imported ===") self.save_page(response_dict) for page_number in range(2, total_pages + 1): params["page"] = page_number r = requests.get( "http://ws.audioscrobbler.com/2.0/", params=params ) response_dict = r.json() self.save_page(response_dict) -
由于列表是分页的,我们将向
Command类添加save_page()方法以保存单个页面的曲目。此方法接受一个参数,即包含单个页面顶级曲目的字典,如下所示:def save_page(self, d): for track_dict in d["tracks"]["track"]: track, created = Track.objects.get_or_create( name=force_text(track_dict["name"]), artist=force_text( track_dict["artist"]["name"] ), url=force_text(track_dict["url"]), ) image_dict = track_dict.get("image", None) if created and image_dict: image_url = image_dict[1]["#text"] image_response = requests.get(image_url) track.image.save( os.path.basename(image_url), File(StringIO(image_response.content)) ) if self.verbosity >= NORMAL: self.stdout.write(" - {} - {}".format( track.artist, track.name )) -
要运行导入,请在命令行中调用以下命令:
(myproject_env)$ python manage.py \ import_music_from_lastfm_as_json --max_pages=3
它是如何工作的...
可选的命名参数 max_pages 限制导入的数据为三页。如果你想下载所有可用的顶级曲目,请跳过它;然而,请注意,有超过 26,000 页,如 totalPages 值中详细说明,这将花费一些时间。
使用 requests.get() 方法,我们从 Last.fm 读取数据,传递 params 查询参数。响应对象有一个内置方法称为 json(),它将 JSON 字符串转换为解析后的字典。
我们从这个字典中读取总页数值,然后保存第一页的结果。然后,我们逐页获取第二页及以后的页面并保存它们。导入过程中一个有趣的部分是下载和保存图片。在这里,我们也使用 request.get() 来检索图片数据,然后通过 StringIO 将其传递给 File,这相应地用于 image.save() 方法。image.save() 的第一个参数是一个文件名,它将被 upload_to 函数的值覆盖,仅用于文件扩展名。
参见
- 从外部 XML 文件导入数据 的配方
从外部 XML 文件导入数据
Last.fm 文件还允许你从他们的服务中以 XML 格式获取数据。在这个配方中,我将向你展示如何做到这一点。
准备就绪
要准备从 Last.fm 以 XML 格式导入顶级曲目,请按照以下步骤操作:
-
从 准备就绪 部分的第一个三个步骤开始,在 从外部 JSON 文件导入数据 配方中。
-
然后,让我们检查 XML 端点的结构(
ws.audioscrobbler.com/2.0/?method=tag.gettoptracks&tag=disco&api_key=xxx&format=xml),如下所示:<?xml version="1.0" encoding="UTF-8"?> <lfm status="ok"> <tracks tag="disco" page="1" perPage="50" totalPages="26205" total="1310249"> <track rank="1"> <name>Billie Jean</name> <duration>293</duration> <mbid>f980fc14-e29b-481d-ad3a-5ed9b4ab6340</mbid> <url>http://www.last.fm/music/Michael+Jackson/_/Billie+Jean</url> <streamable fulltrack="0">0</streamable> <artist> <name>Michael Jackson</name> <mbid>f27ec8db-af05-4f36-916e-3d57f91ecf5e</mbid> <url>http://www.last.fm/music/Michael+Jackson</url> </artist> <image size="small">http://img2-ak.lst.fm/i/u/34s/114a4599f3bd451ca915f482345bc70f.png</image> <image size="medium">http://img2-ak.lst.fm/i/u/64s/114a4599f3bd451ca915f482345bc70f.png</image> <image size="large">http://img2-ak.lst.fm/i/u/174s/114a4599f3bd451ca915f482345bc70f.png</image> <image size="extralarge">http://img2-ak.lst.fm/i/u/300x300/114a4599f3bd451ca915f482345bc70f.png</image> </track> ... </tracks> </lfm>
如何做到这一点...
按顺序执行以下步骤以导入Last.fm的 XML 格式的顶级曲目:
-
如果您还没有这样做,创建一个具有类似之前食谱的
Track模型的music应用。 -
然后,创建一个
import_music_from_lastfm_as_xml.py管理命令。我们将使用 Python 附带的ElementTreeXML API 来解析 XML 节点,如下所示:# music/management/commands/import_music_from_lastfm_as_xml.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import os import requests from xml.etree import ElementTree from StringIO import StringIO from django.utils.six.moves import range from django.core.management.base import BaseCommand from django.utils.encoding import force_text from django.conf import settings from django.core.files import File from music.models import Track SILENT, NORMAL, VERBOSE, VERY_VERBOSE = 0, 1, 2, 3 class Command(BaseCommand): help = "Imports top tracks from last.fm as XML." def add_arguments(self, parser): # Named (optional) arguments parser.add_argument( "--max_pages", type=int, default=0, ) def handle(self, *args, **options): self.verbosity = options.get("verbosity", NORMAL) max_pages = options["max_pages"] params = { "method": "tag.gettoptracks", "tag": "disco", "api_key": settings.LAST_FM_API_KEY, "format": "xml", } r = requests.get( "http://ws.audioscrobbler.com/2.0/", params=params ) root = ElementTree.fromstring(r.content) total_pages = int( root.find("tracks").attrib["totalPages"] ) if max_pages > 0: total_pages = max_pages if self.verbosity >= NORMAL: self.stdout.write("=== Tracks imported ===") self.save_page(root) for page_number in range(2, total_pages + 1): params["page"] = page_number r = requests.get( "http://ws.audioscrobbler.com/2.0/", params=params ) root = ElementTree.fromstring(r.content) self.save_page(root) -
由于列表是分页的,我们将在
Command类中添加一个save_page()方法来保存单个页面的曲目。此方法以 XML 的根节点作为参数,如下所示:def save_page(self, root): for track_node in root.findall("tracks/track"): track, created = Track.objects.get_or_create( name=force_text( track_node.find("name").text ), artist=force_text( track_node.find("artist/name").text ), url=force_text( track_node.find("url").text ), ) image_node = track_node.find( "image[@size='medium']" ) if created and image_node is not None: image_response = \ requests.get(image_node.text) track.image.save( os.path.basename(image_node.text), File(StringIO(image_response.content)) ) if self.verbosity >= NORMAL: self.stdout.write(" - {} - {}".format( track.artist, track.name )) -
要运行导入,请在命令行中调用以下命令:
(myproject_env)$ python manage.py \ import_music_from_lastfm_as_xml --max_pages=3
它是如何工作的...
该过程类似于 JSON 方法。使用requests.get()方法,我们从Last.fm读取数据,将查询参数作为params传递。响应的 XML 内容传递给ElementTree解析器,并返回根节点。
ElementTree节点有find()和findall()方法,您可以通过传递 XPath 查询来过滤出特定的子节点。
以下是一个表格,列出了ElementTree支持的可用 XPath 语法:
| XPath 语法组件 | 含义 |
|---|---|
tag |
这选择具有给定标签的所有子元素。 |
* |
这选择所有子元素。 |
. |
这选择当前节点。 |
// |
这选择当前元素所有级别下的所有子元素。 |
.. |
这选择父元素。 |
[@attrib] |
这选择具有给定属性的所有元素。 |
[@attrib='value'] |
这选择具有给定属性值的所有元素。 |
[tag] |
这选择所有具有名为 tag 的子元素的元素。仅支持直接子元素。 |
[position] |
这选择位于给定位置的元素。位置可以是整数(1是第一个位置),last()表达式(对于最后一个位置),或者相对于最后一个位置的位置(例如,last()-1)。 |
因此,使用root.find("tracks").attrib["totalPages"],我们读取总页数。我们将保存第一页,然后逐页保存其他页面。
在save_page()方法中,root.findall("tracks/track")返回一个通过<tracks>节点下的<track>节点进行迭代的迭代器。使用track_node.find("image[@size='medium']"),我们获取中等大小的图像。
还有更多...
您可以在en.wikipedia.org/wiki/XPath上了解更多关于 XPath 的信息。
ElementTree的完整文档可以在docs.python.org/2/library/xml.etree.elementtree.html找到。
参见
- 从外部 JSON 文件导入数据的配方
创建可过滤的 RSS 订阅源
Django 自带了一个内容聚合框架,允许你轻松创建 RSS 和 Atom 订阅源。RSS 和 Atom 订阅源是具有特定语义的 XML 文档。它们可以在 RSS 阅读器如 Feedly 中订阅,也可以在其他网站、移动应用程序或桌面应用程序中聚合。在这个配方中,我们将创建BulletinFeed,它提供了一个带有图像的公告板。此外,结果可以通过 URL 查询参数进行过滤。
准备工作
创建一个新的bulletin_board应用,并将其放在设置中的INSTALLED_APPS下。
如何操作...
我们将创建一个Bulletin模型和相应的 RSS 订阅源。我们将能够通过类型或类别过滤 RSS 订阅源,以便只订阅例如提供二手书的公告:
-
在此应用的
models.py文件中,添加Category和Bulletin模型,并在这两者之间建立外键关系,如下所示:# bulletin_board/models.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.utils.encoding import \ python_2_unicode_compatible from utils.models import CreationModificationDateMixin from utils.models import UrlMixin TYPE_CHOICES = ( ("searching", _("Searching")), ("offering", _("Offering")), ) @python_2_unicode_compatible class Category(models.Model): title = models.CharField(_("Title"), max_length=200) def __str__(self): return self.title class Meta: verbose_name = _("Category") verbose_name_plural = _("Categories") @python_2_unicode_compatible class Bulletin(CreationModificationDateMixin, UrlMixin): bulletin_type = models.CharField(_("Type"), max_length=20, choices=TYPE_CHOICES) category = models.ForeignKey(Category, verbose_name=_("Category")) title = models.CharField(_("Title"), max_length=255) description = models.TextField(_("Description"), max_length=300) contact_person = models.CharField(_("Contact person"), max_length=255) phone = models.CharField(_("Phone"), max_length=50, blank=True) email = models.CharField(_("Email"), max_length=254, blank=True) image = models.ImageField(_("Image"), max_length=255, upload_to="bulletin_board/", blank=True) class Meta: verbose_name = _("Bulletin") verbose_name_plural = _("Bulletins") ordering = ("-created",) def __str__(self): return self.title def get_url_path(self): try: path = reverse( "bulletin_detail", kwargs={"pk": self.pk} ) except: # the apphook is not attached yet return "" else: return path -
然后,创建
BulletinFilterForm,允许访客通过类型和类别过滤公告,如下所示:# bulletin_board/forms.py # -*- coding: UTF-8 -*- from django import forms from django.utils.translation import ugettext_lazy as _ from models import Category, TYPE_CHOICES class BulletinFilterForm(forms.Form): bulletin_type = forms.ChoiceField( label=_("Bulletin Type"), required=False, choices=(("", "---------"),) + TYPE_CHOICES, ) category = forms.ModelChoiceField( label=_("Category"), required=False, queryset=Category.objects.all(), ) -
添加一个包含
BulletinFeed类的feeds.py文件,如下所示:# bulletin_board/feeds.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.contrib.syndication.views import Feed from django.core.urlresolvers import reverse from .models import Bulletin, TYPE_CHOICES from .forms import BulletinFilterForm class BulletinFeed(Feed): description_template = \ "bulletin_board/feeds/bulletin_description.html" def get_object(self, request, *args, **kwargs): form = BulletinFilterForm(data=request.REQUEST) obj = {} if form.is_valid(): obj = { "bulletin_type": \ form.cleaned_data["bulletin_type"], "category": form.cleaned_data["category"], "query_string": \ request.META["QUERY_STRING"], } return obj def title(self, obj): t = "My Website - Bulletin Board" # add type "Searching" or "Offering" if obj.get("bulletin_type", False): tp = obj["bulletin_type"] t += " - %s" % dict(TYPE_CHOICES)[tp] # add category if obj.get("category", False): t += " - %s" % obj["category"].title return t def link(self, obj): if obj.get("query_string", False): return reverse("bulletin_list") + "?" + \ obj["query_string"] return reverse("bulletin_list") def feed_url(self, obj): if obj.get("query_string", False): return reverse("bulletin_rss") + "?" + \ obj["query_string"] return reverse("bulletin_rss") def item_pubdate(self, item): return item.created def items(self, obj): qs = Bulletin.objects.order_by("-created") if obj.get("bulletin_type", False): qs = qs.filter( bulletin_type=obj["bulletin_type"], ).distinct() if obj.get("category", False): qs = qs.filter( category=obj["category"], ).distinct() return qs[:30] -
为将在订阅源中提供的公告描述创建一个模板,如下所示:
{# templates/bulletin_board/feeds/bulletin_description.html #} {% if obj.image %} <p><a href="{{ obj.get_url }}"><img src="img/{{ request.META.HTTP_HOST }}{{ obj.image.url }}" alt="" /></a></p> {% endif %} <p>{{ obj.description }}</p> -
为公告板应用创建一个 URL 配置,并将其包含在根 URL 配置中,如下所示:
# templates/bulletin_board/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import * from .feeds import BulletinFeed urlpatterns = patterns("bulletin_board.views", url(r"^$", "bulletin_list", name="bulletin_list"), url(r"^(?P<bulletin_id>[0-9]+)/$", "bulletin_detail", name="bulletin_detail"), url(r"^rss/$", BulletinFeed(), name="bulletin_rss"), ) -
你还需要为可过滤的公告列表和详情提供视图和模板。在
Bulletin列表页面模板中,添加以下链接:<a href="{% url "bulletin_rss" %}?{{ request.META.QUERY_STRING }}">RSS Feed</a>
工作原理...
因此,如果你在数据库中有一些数据,并在浏览器中打开http://127.0.0.1:8000/bulletin-board/rss/?bulletin_type=offering&category=4,你将获得具有Offering类型和4类别 ID 的公告 RSS 订阅源。
BulletinFeed类有一个get_objects()方法,它接受当前的HttpRequest并定义了该类其他方法中使用的obj字典。obj字典包含公告类型、类别和当前查询字符串。
title()方法返回订阅源的标题。它可以是通用的,也可以与所选的公告类型或类别相关。link()方法返回经过过滤的原始公告列表的链接。feed_url()方法返回当前订阅源的 URL。items()方法执行过滤并返回过滤后的公告QuerySet。最后,item_pubdate()方法返回公告的创建日期。
要查看我们正在扩展的Feed类的所有可用方法和属性,请参阅以下文档:docs.djangoproject.com/en/1.8/ref/contrib/syndication/#feed-class-reference。
代码的其他部分是自解释的。
参见
-
在第二章(ch02.html "第二章. 数据库结构")中,数据库结构的创建具有与 URL 相关方法的模型混入器食谱
-
在第二章(ch02.html "第二章. 数据库结构")中,数据库结构的创建用于处理创建和修改日期的模型混入器食谱
-
使用 Tastypie 创建 API 食谱
使用 Tastypie 创建 API
Tastypie 是一个用于 Django 创建网络服务 应用程序编程接口(API)的框架。它支持完整的 GET/POST/PUT/DELETE/PATCH HTTP 方法来处理在线资源。它还支持不同类型的身份验证和授权、序列化、缓存、节流等功能。在本教程中,你将学习如何为第三方提供公告以供阅读,也就是说,我们只实现 GET HTTP 方法。
准备工作
首先,使用以下命令在你的虚拟环境中安装 Tastypie:
(myproject_env)$ pip install django-tastypie
在设置中将 Tastypie 添加到 INSTALLED_APPS。然后,增强我们在 创建可过滤的 RSS 源 食谱中定义的 bulletin_board 应用。
如何操作...
我们将为公告创建一个 API 并将其注入到 URL 配置中,如下所示:
-
在
bulletin_board应用中,创建一个api.py文件,包含两个资源,CategoryResource和BulletinResource,如下所示:# bulletin_board/api.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from tastypie.resources import ModelResource from tastypie.resources import ALL, ALL_WITH_RELATIONS from tastypie.authentication import ApiKeyAuthentication from tastypie.authorization import DjangoAuthorization from tastypie import fields from .models import Category, Bulletin class CategoryResource(ModelResource): class Meta: queryset = Category.objects.all() resource_name = "categories" fields = ["title"] allowed_methods = ["get"] authentication = ApiKeyAuthentication() authorization = DjangoAuthorization() filtering = { "title": ALL, } class BulletinResource(ModelResource): category = fields.ForeignKey(CategoryResource, "category", full=True) class Meta: queryset = Bulletin.objects.all() resource_name = "bulletins" fields = [ "bulletin_type", "category", "title", "description", "contact_person", "phone", "email", "image" ] allowed_methods = ["get"] authentication = ApiKeyAuthentication() authorization = DjangoAuthorization() filtering = { "bulletin_type": ALL, "title": ALL, "category": ALL_WITH_RELATIONS, } -
在主 URL 配置中,包含 API URL,如下所示:
# myproject/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import patterns, include, url from django.conf import settings from django.conf.urls.static import static from django.contrib.staticfiles.urls import \ staticfiles_urlpatterns from django.contrib import admin admin.autodiscover() from tastypie.api import Api from bulletin_board.api import CategoryResource from bulletin_board.api import BulletinResource v1_api = Api(api_name="v1") v1_api.register(CategoryResource()) v1_api.register(BulletinResource()) urlpatterns = patterns('', url(r"^admin/", include(admin.site.urls)), url(r"^api/", include(v1_api.urls)), ) urlpatterns += staticfiles_urlpatterns() urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -
在模型管理中为管理员用户创建一个 Tastypie API 密钥。为此,导航到 Tastypie | Api key | 添加 Api key,选择管理员用户,并保存条目。这将生成一个随机 API 密钥,如下面的截图所示:
![如何操作...]()
-
然后,你可以打开此 URL 来查看 JSON 响应在操作中的效果(只需将 xxx 替换为你的 API 密钥):
http://127.0.0.1:8000/api/v1/bulletins/?format=json&username=admin&api_key=xxx。
它是如何工作的...
Tastypie 的每个端点都应该有一个扩展 ModelResource 的类定义。类似于 Django 模型,资源的配置设置在 Meta 类中:
-
queryset参数定义了要列出的对象的QuerySet。 -
resource_name参数定义了 URL 端点的名称。 -
fields参数列出了在 API 中应显示的模型字段。 -
allowed_methods参数列出了请求方法,例如get、post、put、delete和patch。 -
authentication参数定义了第三方在连接到 API 时如何进行身份验证。可用的选项有Authentication、BasicAuthentication、ApiKeyAuthentication、SessionAuthentication、DigestAuthentication、OAuthAuthentication、MultiAuthentication或你自己的自定义身份验证。在我们的案例中,我们使用ApiKeyAuthentication,因为我们希望每个用户使用username和api_key。 -
authorization参数回答了授权问题:是否允许此用户执行所声明的操作?可能的选项有Authorization、ReadOnlyAuthorization、DjangoAuthorization或你自己的自定义授权。在我们的案例中,我们使用ReadOnlyAuthorization,因为我们只想允许用户进行读取访问。 -
filtering参数定义了可以通过哪些字段在 URL 查询参数中过滤列表。例如,根据当前配置,你可以通过包含单词"movie"的标题来过滤项目:http://127.0.0.1:8000/api/v1/bulletins/?format=json&username=admin&api_key=xxx&title__contains=movie。
此外,还有一个在BulletinResource中定义的category外键,使用full=True参数,这意味着在公告资源中会显示完整的类别字段列表,而不是一个端点链接。
除了 JSON 之外,Tastypie还允许你使用其他格式,如 XML、YAML 和 bplist。
使用 Tastypie 可以与 API 做更多的事情。要了解更多详细信息,请查看官方文档django-tastypie.readthedocs.org/en/latest/。
参见
-
创建可过滤的 RSS 源 配方
-
使用 Django REST 框架创建 API 的配方
使用 Django REST 框架创建 API
除了 Tastypie 之外,还有一个更新、更鲜活的框架可以用来为你的数据传输到第三方创建 API。那就是 Django REST Framework。这个框架有更广泛的文档和类似 Django 的实现,它也更容易维护。因此,如果你必须在 Tastypie 和 Django REST Framework 之间选择,我会推荐后者。在这个配方中,你将学习如何使用 Django REST Framework 来允许你的项目合作伙伴、移动客户端或基于 Ajax 的网站访问你网站上的数据,以创建、读取、更新和删除。
准备工作
首先,使用以下命令在你的虚拟环境中安装 Django REST Framework 及其可选依赖项:
(myproject_env)$ pip install djangorestframework
(myproject_env)$ pip install markdown
(myproject_env)$ pip install django-filter
将rest_framework添加到设置中的INSTALLED_APPS。然后,增强我们在创建可过滤的 RSS 源配方中定义的bulletin_board应用。
如何操作...
要在我们的bulletin_board应用中集成一个新的 REST API,请执行以下步骤:
-
将特定的配置添加到设置中:
# conf/base.py or settings.py REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions." "DjangoModelPermissionsOrAnonReadOnly" ], "DEFAULT_PAGINATION_CLASS": \ "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 100, } -
在
bulletin_board应用中,创建一个名为serializers.py的文件,内容如下:# bulletin_board/serializers.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from rest_framework import serializers from .models import Category, Bulletin class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = ["id", "title"] class BulletinSerializer(serializers.ModelSerializer): category = CategorySerializer() class Meta: model = Bulletin fields = [ "id", "bulletin_type", "category", "title", "description", "contact_person", "phone", "email", "image" ] def create(self, validated_data): category_data = validated_data.pop('category') category, created = Category.objects.\ get_or_create(title=category_data['title']) bulletin = Bulletin.objects.create( category=category, **validated_data ) return bulletin def update(self, instance, validated_data): category_data = validated_data.pop('category') category, created = Category.objects.get_or_create( title=category_data['title'], ) for fname, fvalue in validated_data.items(): setattr(instance, fname, fvalue) instance.category = category instance.save() return instance -
在
bulletin_board应用中的views.py文件中添加两个新的基于类的视图:# bulletin_board/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from rest_framework import generics from .models import Bulletin from .serializers import BulletinSerializer class RESTBulletinList(generics.ListCreateAPIView): queryset = Bulletin.objects.all() serializer_class = BulletinSerializer class RESTBulletinDetail( generics.RetrieveUpdateDestroyAPIView ): queryset = Bulletin.objects.all() serializer_class = BulletinSerializer -
最后,将新视图连接到 URL 配置:
# myproject/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import patterns, include, url from bulletin_board.views import RESTBulletinList from bulletin_board.views import RESTBulletinDetail urlpatterns = [ # ... url( r"^api-auth/", include("rest_framework.urls", namespace="rest_framework") ), url( r"^rest-api/bulletin-board/$", RESTBulletinList.as_view(), name="rest_bulletin_list" ), url( r"^rest-api/bulletin-board/(?P<pk>[0-9]+)/$", RESTBulletinDetail.as_view(), name="rest_bulletin_detail" ), ]
如何工作...
我们在这里创建的是一个公告板的 API,用户可以读取分页的公告列表;创建一个新的公告;通过 ID 读取、更改或删除单个公告。读取不需要认证;而添加、更改或删除公告则需要拥有具有适当权限的用户账户。
这是你可以如何接近创建的 API:
| URL | HTTP 方法 | 描述 |
|---|---|---|
http://127.0.0.1:8000/rest-api/bulletin-board/ |
GET |
以分页方式列出 100 条公告 |
http://127.0.0.1:8000/rest-api/bulletin-board/ |
POST |
如果请求用户已认证并有权创建公告,则创建一个新的公告 |
http://127.0.0.1:8000/rest-api/bulletin-board/1/ |
GET |
获取 ID 为1的公告 |
http://127.0.0.1:8000/rest-api/bulletin-board/1/ |
PUT |
如果用户已认证并有权更改公告,则更新 ID 为1的公告 |
http://127.0.0.1:8000/rest-api/bulletin-board/1/ |
DELETE |
如果用户已认证并有权删除公告,则删除 ID 为1的公告 |
如何实际使用 API?例如,如果你已经安装了requests库,你可以在 Django shell 中按照以下方式创建一个新的公告:
(myproject_env)$ python manage.py shell
>>> import requests
>>> response = requests.post("http://127.0.0.1:8000/rest-api/bulletin-board/", auth=("admin", "admin"), data={"title": "TEST", "category.title": "TEST", "contact_person": "TEST", "bulletin_type": "searching", "description": "TEST"})
>>> response.status_code
201
>>> response.json()
{u'category': {u'id': 6, u'title': u'TEST'}, u'description': u'TEST', u'title': u'TEST', u'image': None, u'email': u'', u'phone': u'', u'bulletin_type': u'searching', u'contact_person': u'TEST', u'id': 3}
此外,Django REST Framework 为你提供了一个基于 Web 的 API 文档,当你通过浏览器访问 API 端点时,它会显示出来。在那里,你也可以通过集成表单尝试 API,如下面的截图所示:

让我们快速看一下我们编写的代码是如何工作的。在设置中,我们已将访问设置为依赖于 Django 系统的权限。对于匿名请求,只允许读取。其他访问选项包括允许任何权限给所有人,允许任何权限仅给认证用户,允许任何权限给工作人员用户,等等。完整的列表可以在www.django-rest-framework.org/api-guide/permissions/找到。
然后,在设置中,分页被设置为。当前选项是使用类似于 SQL 查询的limit和offset参数。其他选项包括使用页码分页来处理相对静态的内容,或者使用游标分页来处理实时数据。我们将默认分页设置为每页 100 项。
之后,我们为类别和公告定义了序列化器。它们处理将在输出中显示或由输入验证的数据。为了处理类别检索或保存,我们必须重写BulletinSerializer的create()和update()方法。在 Django REST Framework 中处理关系有多种方式,我们在示例中选择了最详细的一种。要了解更多关于如何序列化关系的信息,请参阅www.django-rest-framework.org/api-guide/relations/的文档。
在定义序列化器之后,我们创建了两个基于类的视图来处理 API 端点,并将它们插入到 URL 配置中。在 URL 配置中,我们有一个规则(/api-auth/)用于可浏览的 API 页面、登录和注销。
参见
-
创建可过滤的 RSS 源菜谱
-
使用 Tastypie 创建 API菜谱
-
在第十一章测试和部署中,使用 Django REST framework 创建的 API 测试菜谱
第十章。铃铛和装饰
在本章中,我们将介绍以下菜谱:
-
使用 Django shell
-
使用数据库查询表达式
-
为更好的国际化支持对 slugify()函数进行猴子补丁
-
切换调试工具栏
-
使用 ThreadLocalMiddleware
-
缓存方法返回值
-
使用 Memcached 来缓存 Django 视图
-
使用信号通知管理员关于新条目的信息
-
检查缺失的设置
简介
在本章中,我们将介绍其他几个重要的小技巧,这将帮助你更好地理解和利用 Django。你将了解如何使用 Django shell 在将代码写入文件之前进行实验。你将介绍猴子补丁,也称为游击式补丁,这是 Python 和 Ruby 等动态语言的一个强大功能。你将学习如何调试代码和检查其性能。你将了解如何从任何模块访问当前登录用户和其他请求参数。此外,你还将学习如何缓存值、处理信号和创建系统检查。准备好一个有趣的编程体验!
使用 Django shell
在虚拟环境激活并且将你的项目目录选为当前目录后,在你的命令行工具中输入以下命令:
(myproject_env)$ python manage shell
通过执行前面的命令,你将进入一个为你的 Django 项目配置的交互式 Python shell,在那里你可以玩转代码,检查类,尝试方法,或即时执行脚本。在本菜谱中,我们将介绍你需要了解的最重要的函数,以便与 Django shell 一起工作。
准备工作
你可以使用以下命令之一安装 IPython 或 bpython,这将突出显示 Django shell 输出的语法,并添加一些其他辅助工具:
(myproject_env)$ pip install ipython
(myproject_env)$ pip install bpython
如何做到...
通过遵循以下说明来学习使用 Django shell 的基本知识:
-
通过输入以下命令来运行 Django shell:
(myproject_env)$ python manage.py shell提示符将更改为
In [1]:或>>>,具体取决于你是否使用 IPython。如果你使用 bpython,shell 将显示在完整的终端窗口中,底部有可用的快捷键(类似于 nano 编辑器),并且你还可以在输入时获得代码高亮和文本自动完成。 -
现在,你可以导入类、函数或变量,并与之互动。例如,要查看已安装模块的版本,你可以导入该模块,然后尝试读取其
__version__、VERSION或version变量,如下所示:>>> import re >>> re.__version__ '2.2.1' -
要获取模块、类、函数、方法、关键字或文档主题的全面描述,请使用
help()函数。你可以传递一个包含特定实体路径的字符串,或者直接传递实体本身,如下所示:>>> help("django.forms")这将打开
django.forms模块的帮助页面。使用箭头键上下滚动页面。按Q键返回 shell。小贴士
如果你不带参数运行
help(),它将打开交互式帮助。在这里,你可以输入模块、类、函数等的任何路径,并获取有关其功能和使用方法的信息。要退出交互式帮助,请按Ctrl + D。 -
这是一个将实体传递给
help()函数的示例。这将打开ModelForm类的帮助页面,如下所示:>>> from django.forms import ModelForm >>> help(ModelForm) -
要快速查看模型实例可用的字段和值,请使用
__dict__属性。此外,使用pprint()函数以更可读的格式(不仅仅是长行)打印字典,如下所示:>>> from pprint import pprint >>> from django.contrib.contenttypes.models import ContentType >>> pprint(ContentType.objects.all()[0].__dict__) {'_state': <django.db.models.base.ModelState object at 0x10756d250>, 'app_label': u'bulletin_board', 'id': 11, 'model': u'bulletin', 'name': u'Bulletin'}注意,使用
__dict__,我们不会得到多对多关系。然而,这可能足以快速了解字段和值。 -
要获取对象的全部可用属性和方法,你可以使用
dir()函数,如下所示:>>> dir(ContentType()) ['DoesNotExist', 'MultipleObjectsReturned', '__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__hash__', '__init__', u'__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', '_base_manager', '_default_manager', '_deferred', '_do_insert', '_do_update', '_get_FIELD_display', '_get_next_or_previous_by_FIELD', '_get_next_or_previous_in_order', '_get_pk_val', '_get_unique_checks', '_meta', '_perform_date_checks', '_perform_unique_checks', '_save_parents', '_save_table', '_set_pk_val', '_state', 'app_label', 'clean', 'clean_fields', 'content_type_set_for_comment', 'date_error_message', 'delete', 'full_clean', 'get_all_objects_for_this_type', 'get_object_for_this_type', 'id', 'logentry_set', 'model', 'model_class', 'name', 'natural_key', 'objects', 'permission_set', 'pk', 'prepare_database_save', 'save', 'save_base', 'serializable_value', 'unique_error_message', 'validate_unique']要按行打印这些属性,你可以使用以下方法:
>>> pprint(dir(ContentType())) -
Django shell 在将它们放入你的模型方法、视图或管理命令之前,用于实验
QuerySets或正则表达式。例如,要检查电子邮件验证正则表达式,你可以在 Django shell 中键入以下内容:>>> import re >>> email_pattern = re.compile(r"[^@]+@[^@]+\.[^@]+") >>> email_pattern.match("aidas@bendoraitis.lt") <_sre.SRE_Match object at 0x1075681d0> -
如果你想要尝试不同的
QuerySets,你需要执行项目中模型和应用的设置,如下所示:>>> import django >>> django.setup() >>> from django.contrib.auth.models import User >>> User.objects.filter(groups__name="Editors") [<User: admin>] -
要退出 Django shell,请按Ctrl + D或键入以下命令:
>>> exit()
它是如何工作的...
正常 Python shell 和 Django shell 之间的区别在于,当你运行 Django shell 时,manage.py会将DJANGO_SETTINGS_MODULE环境变量设置为项目的设置路径,然后 Django shell 中的所有代码都在你的项目上下文中处理。
参见
-
Using database query expressions 食谱
-
Monkey-patching the slugify() function for better internationalization support 食谱
使用数据库查询表达式
Django 对象关系映射(ORM)包含特殊抽象构造,可用于构建复杂的数据库查询。它们被称为查询表达式,允许你过滤数据、排序它、注解新列和聚合关系。在本食谱中,我们将看到如何在实践中使用它。我们将创建一个显示病毒视频并计算每个视频在移动设备和桌面设备上被观看次数的应用。
准备工作
首先,将django-mobile安装到你的虚拟环境中。此模块将用于区分桌面设备和移动设备:
(myproject_env)$ pip install django-mobile
要配置它,你需要修改几个项目设置,如下所示。除此之外,让我们创建viral_videos应用。将它们都放在INSTALLED_APPS下:
# conf/base.py or settings.py
INSTALLED_APPS = (
# ...
# third party
"django_mobile",
# project-specific
"utils",
"viral_videos",
)
TEMPLATE_CONTEXT_PROCESSORS = (
# ...
"django_mobile.context_processors.flavour",
)
TEMPLATE_LOADERS = (
# ...
"django_mobile.loader.Loader",
)
MIDDLEWARE_CLASSES = (
# ...
"django_mobile.middleware.MobileDetectionMiddleware",
"django_mobile.middleware.SetFlavourMiddleware",
)
接下来,创建一个包含创建和修改时间戳、标题、嵌入代码、桌面设备上的印象和移动设备上的印象的病毒视频模型,如下所示:
# viral_videos/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from utils.models import CreationModificationDateMixin, UrlMixin
@python_2_unicode_compatible
class ViralVideo(CreationModificationDateMixin, UrlMixin):
title = models.CharField(
_("Title"), max_length=200, blank=True)
embed_code = models.TextField(_("YouTube embed code"), blank=True)
desktop_impressions = models.PositiveIntegerField(
_("Desktop impressions"), default=0)
mobile_impressions = models.PositiveIntegerField(
_("Mobile impressions"), default=0)
class Meta:
verbose_name = _("Viral video")
verbose_name_plural = _("Viral videos")
def __str__(self):
return self.title
def get_url_path(self):
from django.core.urlresolvers import reverse
return reverse(
"viral_video_detail",
kwargs={"id": str(self.id)}
)
如何做到这一点...
为了说明查询表达式,让我们创建病毒视频详情视图并将其插入到 URL 配置中,如下所示:
-
在
views.py中创建viral_video_detail()视图,如下所示:# viral_videos/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import datetime from django.shortcuts import render, get_object_or_404 from django.db import models from django.conf import settings from .models import ViralVideo POPULAR_FROM = getattr( settings, "VIRAL_VIDEOS_POPULAR_FROM", 500 ) def viral_video_detail(request, id): yesterday = datetime.date.today() - \ datetime.timedelta(days=1) qs = ViralVideo.objects.annotate( total_impressions=\ models.F("desktop_impressions") + \ models.F("mobile_impressions"), label=models.Case( models.When( total_impressions__gt=OPULAR_FROM, then=models.Value("popular") ), models.When( created__gt=yesterday, then=models.Value("new") ), default=models.Value("cool"), output_field=models.CharField(), ), ) # DEBUG: check the SQL query that Django ORM generates print(qs.query) qs = qs.filter(pk=id) if request.flavour == "mobile": qs.update( mobile_impressions=\ models.F("mobile_impressions") + 1 ) else: qs.update( desktop_impressions=\ models.F("desktop_impressions") + 1 ) video = get_object_or_404(qs) return render( request, "viral_videos/viral_video_detail.html", {'video': video} ) -
定义应用的 URL 配置,如下所示:
# viral_videos/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import * urlpatterns = [ url( r"^(?P<id>\d+)/", "viral_videos.views.viral_video_detail", name="viral_video_detail" ), ] -
将应用的 URL 配置包含到项目的根 URL 配置中,如下所示:
# myproject/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import include, url from django.conf import settings from django.conf.urls.i18n import i18n_patterns urlpatterns = i18n_patterns("", # ... url(r"^viral-videos/", include("viral_videos.urls")), ) -
为
viral_video_detail()视图创建一个模板,如下所示:{# templates/viral_videos/viral_video_detail.html #} {% extends "base.html" %} {% load i18n %} {% block content %} <h1>{{ video.title }} <span class="badge">{{ video.label }}</span> </h1> <div>{{ video.embed_code|safe }}</div> <div> <h2>{% trans "Impressions" %}</h2> <ul> <li>{% trans "Desktop impressions" %}: {{ video.desktop_impressions }}</li> <li>{% trans "Mobile impressions" %}: {{ video.mobile_impressions }}</li> <li>{% trans "Total impressions" %}: {{ video.total_impressions }}</li> </ul> </div> {% endblock %} -
为
viral_videos应用设置管理权限并向数据库中添加一些视频。
如何工作...
你可能已经注意到了视图中的print()语句。它是临时用于调试目的的。如果你运行本地开发服务器并在浏览器中访问http://127.0.0.1:8000/en/viral-videos/1/中的第一个视频,你将在控制台看到以下 SQL 查询被打印出来:
SELECT "viral_videos_viralvideo"."id", "viral_videos_viralvideo"."created", "viral_videos_viralvideo"."modified", "viral_videos_viralvideo"."title", "viral_videos_viralvideo"."embed_code", "viral_videos_viralvideo"."desktop_impressions", "viral_videos_viralvideo"."mobile_impressions", ("viral_videos_viralvideo"."desktop_impressions" + "viral_videos_viralvideo"."mobile_impressions") AS "total_impressions", CASE WHEN ("viral_videos_viralvideo"."desktop_impressions" + "viral_videos_viralvideo"."mobile_impressions") > 500 THEN popular WHEN "viral_videos_viralvideo"."created" > 2015-11-06 00:00:00 THEN new ELSE cool END AS "label" FROM "viral_videos_viralvideo"
然后,在浏览器中,你会看到一个类似于以下图像的简单页面,显示视频标题、视频标签、嵌入的视频以及桌面设备、移动设备和总印象数:

Django QuerySets中的annotate()方法允许你在SELECT SQL语句中添加额外的列,以及从QuerySets检索到的对象的即时创建属性。使用models.F(),我们可以引用所选数据库表中的不同字段值。在本例中,我们将创建total_impressions属性,它是桌面设备和移动设备上的印象总和。
使用models.Case()和models.When(),我们可以根据不同的条件返回值。为了标记值,我们使用models.Value()。在我们的例子中,我们将为 SQL 查询创建label列,并为QuerySet返回的对象创建属性。如果印象数超过 500,则将其设置为popular,如果它是今天创建的,则设置为new,否则设置为cool。
在视图的末尾,我们调用了qs.update()方法。根据访客使用的设备,它们会增加当前视频的mobile_impressions或desktop_impressions。增加操作在 SQL 级别发生。这解决了所谓的竞争条件,即当两个或更多访客同时访问视图并尝试同时增加印象计数时。
参见
-
使用 Django shell配方
-
在第二章的创建与 URL 相关的方法的模型混入配方中,数据库结构
-
在第二章的创建用于处理创建和修改日期的模型混入配方中,数据库结构
为更好的国际化支持对 slugify()函数进行猴子补丁
Monkey patch 或 guerrilla patch 是一段在运行时扩展或修改另一段代码的代码。通常不建议经常使用 monkey patch;然而,有时,这是在不创建模块的单独分支的情况下修复第三方模块中错误的唯一可能方法。此外,monkey patching 可以用来准备功能或单元测试,而不需要使用复杂的数据库或文件操作。在这个菜谱中,您将学习如何用来自第三方 awesome-slugify 模块的默认 slugify() 函数替换,该模块更智能地处理德语、希腊语和俄语单词,并允许为其他语言创建定制的 slug。作为一个快速提醒,我们使用 slugify() 函数来创建对象的标题或上传的文件名的 URL 友好版本;它删除前后空白字符,将文本转换为小写,删除非单词字符,并将空格转换为连字符。
准备工作
要开始,请执行以下步骤:
-
在您的虚拟环境中安装
awesome-slugify,如下所示:(myproject_env)$ pip install awesome-slugify -
在您的项目中创建一个
guerrilla_patches应用程序,并将其放在设置中的INSTALLED_APPS下。
如何操作...
在 guerrilla_patches 应用程序的 models.py 文件中,添加以下内容:
# guerrilla_patches/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.utils import text
from slugify import slugify_de as awesome_slugify
awesome_slugify.to_lower = True
text.slugify = awesome_slugify
它是如何工作的...
默认的 Django slugify() 函数处理德语变音符号不正确。要亲自查看这一点,请在 Django shell 中运行以下代码而不使用 monkey patch:
(myproject_env)$ python manage.py shell
>>> from django.utils.text import slugify
>>> slugify("Heizölrückstoßabdämpfung")
u'heizolruckstoabdampfung'
在德语中,这是不正确的,因为字母 ß 完全被移除,而不是用 ss 替换,而字母 ä、ö 和 ü 被改为 a、o 和 u;而它们应该被替换为 ae、oe 和 ue。
我们所做的 monkey patch 在初始化时加载 django.utils.text 模块,并将 Slugify 类的可调用实例分配给 slugify() 函数。现在,如果您在 Django shell 中运行相同的代码,您将得到不同但正确的结果,如下所示:
(myproject_env)$ python manage.py shell
>>> from django.utils.text import slugify
>>> slugify("Heizölrückstoßabdämpfung")
u'heizoelrueckstossabdaempfung'
想了解更多关于如何使用 awesome-slugify 模块的信息,请参考以下链接:pypi.python.org/pypi/awesome-slugify。
还有更多...
在创建任何 monkey patch 之前,我们需要完全理解我们想要修改的代码是如何工作的。这可以通过分析现有代码和检查不同变量的值来完成。为此,有一个有用的内置 Python 调试器 pdb 模块,可以临时添加到 Django 代码或任何第三方模块中,以便在任何断点处停止开发服务器的执行。使用以下代码来调试 Python 模块的未知部分:
import pdb
pdb.set_trace()
这将启动交互式 shell,您可以在其中输入变量以查看它们的值。如果您输入 c 或 continue,代码执行将继续到下一个断点。如果您输入 q 或 quit,管理命令将被终止。您可以了解更多关于 Python 调试器的命令以及如何检查代码的跟踪回溯,请参阅docs.python.org/2/library/pdb.html。
另一种快速查看开发服务器中变量值的方法是使用变量作为消息发出警告,如下所示:
raise Warning, some_variable
当您处于 DEBUG 模式时,Django 记录器将为您提供跟踪回溯和其他局部变量。
小贴士
在将代码提交到仓库之前,不要忘记移除调试函数。
相关内容
- 使用 Django shell 的食谱
切换调试工具栏
在使用 Django 进行开发时,您可能想要检查请求头和参数,检查当前的模板上下文,或测量 SQL 查询的性能。所有这些以及更多都可以通过 Django 调试工具栏实现。它是一组可配置的面板,显示有关当前请求和响应的各种调试信息。在本食谱中,我将指导您如何根据由书签工具设置的 cookie 切换调试工具栏的可见性。书签工具是一个包含一小段 JavaScript 代码的书签,您可以在浏览器中的任何页面上运行它。
准备工作
要开始切换调试工具栏的可见性,请查看以下步骤:
-
将 Django 调试工具栏安装到您的虚拟环境中:
(myproject_env)$ pip install django-debug-toolbar==1.4 -
在设置中将
debug_toolbar放入INSTALLED_APPS。
如何做...
按照以下步骤设置 Django 调试工具栏,可以使用浏览器中的书签工具切换其开启或关闭:
-
添加以下项目设置:
MIDDLEWARE_CLASSES = ( # ... "debug_toolbar.middleware.DebugToolbarMiddleware", ) DEBUG_TOOLBAR_CONFIG = { "DISABLE_PANELS": [], "SHOW_TOOLBAR_CALLBACK": \ "utils.misc.custom_show_toolbar", "SHOW_TEMPLATE_CONTEXT": True, } DEBUG_TOOLBAR_PANELS = [ "debug_toolbar.panels.versions.VersionsPanel", "debug_toolbar.panels.timer.TimerPanel", "debug_toolbar.panels.settings.SettingsPanel", "debug_toolbar.panels.headers.HeadersPanel", "debug_toolbar.panels.request.RequestPanel", "debug_toolbar.panels.sql.SQLPanel", "debug_toolbar.panels.templates.TemplatesPanel", "debug_toolbar.panels.staticfiles.StaticFilesPanel", "debug_toolbar.panels.cache.CachePanel", "debug_toolbar.panels.signals.SignalsPanel", "debug_toolbar.panels.logging.LoggingPanel", "debug_toolbar.panels.redirects.RedirectsPanel", ] -
在
utils模块中,创建一个misc.py文件,包含custom_show_toolbar()函数,如下所示:# utils/misc.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals def custom_show_toolbar(request): return "1" == request.COOKIES.get("DebugToolbar", False) -
打开 Chrome 或 Firefox 浏览器,转到 书签管理器。然后,创建两个新的 JavaScript 链接。第一个链接显示工具栏。它看起来类似于以下内容:
Name: Debug Toolbar On URL: javascript:(function(){document.cookie="DebugToolbar=1; path=/";location.reload();})(); -
第二个 JavaScript 链接隐藏工具栏,看起来类似于以下内容:
Name: Debug Toolbar Off URL: javascript:(function(){document.cookie="DebugToolbar=0; path=/";location.reload();})();
如何工作...
DEBUG_TOOLBAR_PANELS 设置定义了工具栏中要显示的面板。DEBUG_TOOLBAR_CONFIG 字典定义了工具栏的配置,包括用于检查是否显示工具栏的功能的路径。
默认情况下,当您浏览项目时,Django 调试工具栏不会显示。然而,当您点击您的书签工具时,调试工具栏开启,DebugToolbar cookie 将被设置为 1,页面将被刷新,您将看到带有调试面板的工具栏。例如,您将能够检查 SQL 语句的性能以进行优化,如下面的截图所示:

你还将能够检查当前视图的模板上下文变量,如下面的截图所示:

参见
- 在第十一章的通过电子邮件获取详细错误报告配方中,测试和部署
使用 ThreadLocalMiddleware
HttpRequest对象包含有关当前用户、语言、服务器变量、cookies、会话等信息。实际上,HttpRequest在视图和中间件中提供,然后你可以将其或其属性值传递给表单、模型方法、模型管理器、模板等。为了使生活更简单,你可以使用存储当前HttpRequest对象的ThreadLocalMiddleware中间件,在全局可访问的 Python 线程中。因此,你可以从模型方法、表单、信号处理器以及任何之前没有直接访问HttpRequest对象的任何地方访问它。在本配方中,我们将定义此中间件。
准备工作
创建utils应用程序并将其放在设置中的INSTALLED_APPS下。
如何操作...
执行以下两个步骤:
-
在
utils应用程序中添加一个middleware.py文件,内容如下:# utils/middleware.py # -*- coding: UTF-8 -*- from threading import local _thread_locals = local() def get_current_request(): """ returns the HttpRequest object for this thread """ return getattr(_thread_locals, "request", None) def get_current_user(): """ returns the current user if it exists or None otherwise """ request = get_current_request() if request: return getattr(request, "user", None) class ThreadLocalMiddleware(object): """ Middleware that adds the HttpRequest object to thread local storage """ def process_request(self, request): _thread_locals.request = request -
将此中间件添加到设置中的
MIDDLEWARE_CLASSES:MIDDLEWARE_CLASSES = ( # ... "utils.middleware.ThreadLocalMiddleware", )
如何工作...
ThreadLocalMiddleware处理每个请求并将当前的HttpRequest对象存储在当前线程中。Django 中的每个请求-响应周期都是单线程的。有两个函数:get_current_request()和get_current_user()。这些函数可以从任何地方使用,以获取当前的HttpRequest对象或当前用户。
例如,你可以创建并使用CreatorMixin,将当前用户保存为模型的创建者,如下所示:
# utils/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
class CreatorMixin(models.Model):
"""
Abstract base class with a creator
"""
creator = models.ForeignKey(
"auth.User",
verbose_name=_("creator"),
editable=False,
blank=True,
null=True,
)
def save(self, *args, **kwargs):
from utils.middleware import get_current_user
if not self.creator:
self.creator = get_current_user()
super(CreatorMixin, self).save(*args, **kwargs)
save.alters_data = True
class Meta:
abstract = True
参见
-
在第二章的创建与 URL 相关的模型混入配方中,数据库结构
-
在第二章的创建用于处理创建和修改日期的模型混入配方中,数据库结构
-
在第二章的创建用于处理元标签的模型混入配方中,数据库结构
-
在第二章的创建用于处理通用关系的模型混入配方中,数据库结构
缓存方法返回值
如果你多次在请求-响应周期中调用相同的模型方法,该方法具有重量级计算或数据库查询,那么视图的性能可能会非常慢。在本配方中,你将了解一个可以用于缓存方法返回值以供以后重复使用的模式。请注意,我们在这里不使用 Django 缓存框架,我们只是使用 Python 默认提供的。
准备工作
选择一个具有在相同的请求-响应周期中将被重复使用的耗时方法的模型的应用程序。
如何操作...
这是一个你可以用来缓存模型方法返回值的模式,以便在视图、表单或模板中重复使用,如下所示:
class SomeModel(models.Model):
# ...
def some_expensive_function(self):
if not hasattr(self, "_expensive_value_cached"):
# do some heavy calculations...
# ... and save the result to result variable
self._expensive_value_cached = result
return self._expensive_value_cached
例如,让我们在本章“使用数据库查询表达式”食谱中创建的 ViralVideo 模型中创建一个 get_thumbnail_url() 方法:
# viral_videos/models.py
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import re
# ... other imports ...
@python_2_unicode_compatible
class ViralVideo(CreationModificationDateMixin, UrlMixin):
# ...
def get_thumbnail_url(self):
if not hasattr(self, "_thumbnail_url_cached"):
url_pattern = re.compile(
r'src="img/([^"]+)"'
)
match = url_pattern.search(self.embed_code)
self._thumbnail_url_cached = ""
if match:
video_id = match.groups()[0]
self._thumbnail_url_cached = \
"http://img.youtube.com/vi/{}/0.jpg".format(
video_id
)
return self._thumbnail_url_cached
它是如何工作的...
该方法检查模型实例是否存在 _expensive_value_cached 属性。如果不存在,则进行耗时计算并将结果分配给这个新属性。在方法结束时,返回缓存的值。当然,如果你有多个重量级方法,你需要使用不同的属性名来保存每个计算值。
现在,你可以在模板的页眉和页脚中使用类似 {{ object.some_expensive_function }} 的内容,耗时计算将只进行一次。
在模板中,你可以在 {% if %} 条件和值的输出中使用该函数,如下所示:
{% if object.some_expensive_function %}
<span class="special">
{{ object.some_expensive_function }}
</span>
{% endif %}
在本例中,我们通过解析视频嵌入代码的 URL,获取其 ID,然后组合缩略图图像的 URL 来检查 YouTube 视频的缩略图。然后,你可以在模板中如下使用它:
{% if video.get_thumbnail_url %}
<figure>
<img src="img/{{ video.get_thumbnail_url }}"
alt="{{ video.title }}" />
<figcaption>{{ video.title }}</figcaption>
</figure>
{% endif %}
参见
- 有关更多详细信息,请参阅第四章,“模板和 JavaScript”。
使用 Memcached 缓存 Django 视图
Django 提供了一种通过缓存最昂贵的部分(如数据库查询或模板渲染)来加速请求-响应周期的可能性。Django 原生支持的最快、最可靠的缓存服务器是基于内存的缓存服务器 Memcached。在本食谱中,你将学习如何使用 Memcached 缓存我们之前在本章“使用数据库查询表达式”食谱中创建的 viral_videos 应用程序中的视图。
准备工作
为了为你的 Django 项目准备缓存,你需要做几件事情:
-
安装 Memcached 服务器,如下所示:
$ wget http://memcached.org/files/memcached-1.4.23.tar.gz $ tar -zxvf memcached-1.4.23.tar.gz $ cd memcached-1.4.23 $ ./configure && make && make test && sudo make install -
启动 Memcached 服务器,如下所示:
$ memcached -d -
在你的虚拟环境中安装 Memcached Python 绑定,如下所示:
(myproject_env)$ pip install python-memcached
如何操作...
要为特定的视图集成缓存,请执行以下步骤:
-
在项目设置中设置
CACHES,如下所示:CACHES = { "default": { "BACKEND": "django.core.cache.backends." "memcached.MemcachedCache", "LOCATION": "127.0.0.1:11211", "TIMEOUT": 60, # 1 minute "KEY_PREFIX": "myproject_production", } } -
修改
viral_videos应用的视图,如下所示:# viral_videos/views.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.views.decorators.vary import vary_on_cookie from django.views.decorators.cache import cache_page @vary_on_cookie @cache_page(60) def viral_video_detail(request, id): # ...
它是如何工作的...
现在,如果你访问第一个病毒视频 http://127.0.0.1:8000/en/viral-videos/1/ 并刷新页面几次,你会看到点击次数每分钟只改变一次。这是因为对于每个访问者,缓存被启用 60 秒。使用 @cache_page 装饰器为视图设置了缓存。
Memcached 是一个键值存储,默认情况下,对于每个缓存的页面,使用完整的 URL 生成键。当两个访问者同时访问同一页面时,第一个访问者将获得由 Python 代码生成的页面,而第二个访问者将获得来自 Memcached 服务器的 HTML 代码。
在我们的示例中,为了确保即使访问相同的 URL,每个访客也能得到单独的处理,我们使用了@vary_on_cookie装饰器。这个装饰器检查 HTTP 请求的Cookie头部的唯一性。
从官方文档中了解更多关于 Django 缓存框架的信息,请访问docs.djangoproject.com/en/1.8/topics/cache/。
参见
-
使用数据库查询表达式菜谱
-
缓存方法返回值菜谱
使用信号通知管理员关于新条目的信息
Django 框架有一个名为信号的概念,类似于 JavaScript 中的事件。有几个内置的信号可以在模型初始化前后、保存或删除实例、迁移数据库模式、处理请求等操作时触发。此外,你可以在你的可重用应用程序中创建自己的信号,并在其他应用程序中处理它们。在这个菜谱中,你将学习如何使用信号在特定模型保存时向管理员发送电子邮件。
准备工作
让我们从在使用数据库查询表达式菜谱中创建的viral_videos应用程序开始。
如何做...
按照以下步骤创建通知管理员:
-
创建包含以下内容的
signals.py文件:# viral_videos/signals.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.db.models.signals import post_save from django.dispatch import receiver from .models import ViralVideo @receiver(post_save, sender=ViralVideo) def inform_administrators(sender, **kwargs): from django.core.mail import mail_admins instance = kwargs["instance"] created = kwargs["created"] if created: context = { "title": instance.title, "link": instance.get_url(), } plain_text_message = """ A new viral video called "%(title)s" has been created. You can preview it at %(link)s.""" % context html_message = """ <p>A new viral video called "%(title)s" has been created.</p> <p>You can preview it <a href="%(link)s">here</a>.</p>""" % context mail_admins( subject="New Viral Video Added at example.com", message=plain_text_message, html_message=html_message, fail_silently=True, ) -
创建包含以下内容的
apps.py文件:# viral_videos/apps.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.apps import AppConfig from django.utils.translation import ugettext_lazy as _ class ViralVideosAppConfig(AppConfig): name = "viral_videos" verbose_name = _("Viral Videos") def ready(self): from .signals import inform_administrators -
更新包含以下内容的
__init__.py文件:# viral_videos/__init__.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals default_app_config = \ "viral_videos.apps.ViralVideosAppConfig" -
确保在项目设置中已设置
ADMINS,如下所示:ADMINS = ( ("Aidas Bendoraitis", "aidas.bendoraitis@example.com"), )
它是如何工作的...
ViralVideosAppConfig应用程序配置类有一个ready()方法,当所有项目模型被加载到内存中时会被调用。根据 Django 文档,信号允许某些发送者通知一组接收者某个动作已经发生。在ready()方法中,我们将导入,因此注册了inform_administrators()信号接收器,并将其限制为仅处理ViralVideo模型作为发送者的信号。因此,每次我们保存ViralVideo模型时,inform_administrators()函数都会被调用。该函数检查视频是否是新创建的。如果是这样,它将向在设置中列出的系统管理员发送电子邮件。
从官方文档中了解更多关于 Django 信号的信息,请访问docs.djangoproject.com/en/1.8/topics/signals/。
参见
-
使用数据库查询表达式菜谱
-
第一章中的创建应用程序配置菜谱,使用 Django 1.8 入门
-
检查缺失的设置菜谱
检查缺失的设置
自 Django 1.7 以来,您可以使用可扩展的 系统检查框架,它取代了旧版的 validate 管理命令。在本配方中,您将学习如何创建一个检查 ADMINS 设置是否已设置的检查。同样,您将能够检查您使用的 API 是否设置了不同的密钥或访问令牌。
准备工作
让我们从在 使用数据库查询表达式 配方中创建并扩展的 viral_videos 应用程序开始。
如何操作...
要使用系统检查框架,请按照以下简单步骤操作:
-
创建包含以下内容的
checks.py文件:# viral_videos/checks.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.core.checks import Warning, register, Tags @register(Tags.compatibility) def settings_check(app_configs, **kwargs): from django.conf import settings errors = [] if not settings.ADMINS: errors.append( Warning( """The system admins are not set in the project settings""", hint="""In order to receive notifications when new videos are created, define system admins like ADMINS=(("Admin", "admin@example.com"),) in your settings""", id="viral_videos.W001", ) ) return errors -
在应用配置的
ready()方法中导入检查,如下所示:# viral_videos/apps.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.apps import AppConfig from django.utils.translation import ugettext_lazy as _ class ViralVideosAppConfig(AppConfig): name = "viral_videos" verbose_name = _("Viral Videos") def ready(self): from .signals import inform_administrators from .checks import settings_check -
要尝试您刚刚创建的检查,请删除或注释掉
ADMINS设置,并在您的虚拟环境中运行check管理命令,如下所示:(myproject_env)$ python manage.py check System check identified some issues: WARNINGS: ?: (viral_videos.W001) The system admins are not set in the project settings HINT: define system admins like ADMINS=(("Admin", "admin@example.com"),) in your settings System check identified 1 issue (0 silenced).
它是如何工作的...
系统检查框架在模型、字段、数据库、管理、认证、内容类型和安全方面有一系列检查,如果项目中某些设置不正确,它会引发错误或警告。此外,您还可以创建类似于本配方中我们所做的自己的检查。
我们已注册了 settings_check() 函数,该函数在没有为项目定义 ADMINS 设置时返回一个包含警告的列表。
除了来自 django.core.checks 模块的 Warning 实例外,返回的列表还可以包含 Debug、Info、Error 和 Critical 类的实例或任何继承自 django.core.checks.CheckMessage 的其他类的实例。调试、信息和警告会静默失败;而错误和临界错误会阻止项目运行。
在此示例中,检查被标记为 compatibility 检查。其他选项包括:models、signals、admin 和 security。
从官方文档中了解更多关于系统检查框架的信息,请访问 docs.djangoproject.com/en/1.8/topics/checks/。
参见
-
使用数据库查询表达式 的配方
-
使用信号通知管理员关于新条目 的配方
-
在 第一章 的 创建应用配置 配方中,使用 Django 1.8 入门
第十一章:测试和部署
在本章中,我们将介绍以下食谱:
-
使用 Selenium 测试页面
-
使用 mock 测试视图
-
测试使用 Django REST 框架创建的 API
-
发布可重用 Django 应用
-
通过电子邮件获取详细的错误报告
-
在 Apache 上使用 mod_wsgi 部署
-
设置 cron 作业以执行常规任务
-
创建和使用 Fabric 部署脚本
简介
到目前为止,我期望您已经开发了一个或多个 Django 项目或可重用应用,并准备好向公众展示。对于开发周期的最后一步,我们将探讨如何测试您的项目,将可重用应用分发给他人,并在远程服务器上发布您的网站。请继续关注最终的部分!
使用 Selenium 测试页面
Django 为您的网站提供了编写测试套件的可能性。测试套件会自动检查您的网站或其组件,以查看是否一切正常工作。当您修改代码时,您可以运行测试来检查更改是否没有以错误的方式影响应用程序的行为。自动化软件测试的世界可以分为五个级别:单元测试、集成测试、组件接口测试、系统测试和操作验收测试。验收测试检查业务逻辑,以了解项目是否按预期工作。在本食谱中,您将学习如何使用 Selenium 编写验收测试,这允许您在浏览器中模拟填写表单或点击特定 DOM 元素等活动。
准备工作
让我们从第四章中实现点赞小部件食谱的locations和likes应用开始,模板和 JavaScript。
如果您还没有,请从getfirefox.com安装 Firefox 浏览器。
然后,按照以下步骤在您的虚拟环境中安装 Selenium:
(myproject_env)$ pip install selenium
如何操作...
我们将通过以下步骤使用 Selenium 测试基于 Ajax 的点赞功能:
-
在您的
locations应用中创建名为tests.py的文件,并包含以下内容:# locations/tests.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from time import sleep from django.test import LiveServerTestCase from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User from selenium import webdriver from selenium.webdriver.support.ui import WebDriverWait from likes.models import Like from .models import Location class LiveLocationTest(LiveServerTestCase): @classmethod def setUpClass(cls): super(LiveLocationTest, cls).setUpClass() cls.browser = webdriver.Firefox() cls.browser.delete_all_cookies() cls.location = Location.objects.create( title="Haus der Kulturen der Welt", slug="hkw", small_image="locations/2015/11/" "20151116013056_small.jpg", medium_image="locations/2015/11/" "20151116013056_medium.jpg", large_image="locations/2015/11/" "20151116013056_large.jpg", ) cls.username = "test-admin" cls.password = "test-admin" cls.superuser = User.objects.create_superuser( username=cls.username, password=cls.password, email="", ) @classmethod def tearDownClass(cls): super(LiveLocationTest, cls).tearDownClass() cls.browser.quit() cls.location.delete() cls.superuser.delete() def test_login_and_like(self): # login self.browser.get("%(website)s/admin/login/" "?next=/locations/%(slug)s/" % { "website": self.live_server_url, "slug": self.location.slug, }) username_field = \ self.browser.find_element_by_id("id_username") username_field.send_keys(self.username) password_field = \ self.browser.find_element_by_id("id_password") password_field.send_keys(self.password) self.browser.find_element_by_css_selector( 'input[type="submit"]' ).click() WebDriverWait(self.browser, 10).until( lambda x: self.browser.\ find_element_by_css_selector( ".like-button" ) ) # click on the "like" button like_button = self.browser.\ find_element_by_css_selector('.like-button') is_initially_active = \ "active" in like_button.get_attribute("class") initial_likes = int(self.browser.\ find_element_by_css_selector( ".like-badge" ).text) sleep(2) # remove this after the first run like_button.click() WebDriverWait(self.browser, 10).until( lambda x: int( self.browser.find_element_by_css_selector( ".like-badge" ).text ) != initial_likes ) likes_in_html = int( self.browser.find_element_by_css_selector( ".like-badge" ).text ) likes_in_db = Like.objects.filter( content_type=ContentType.objects.\ get_for_model(Location), object_id=self.location.pk, ).count() sleep(2) # remove this after the first run self.assertEqual(likes_in_html, likes_in_db) if is_initially_active: self.assertLess(likes_in_html, initial_likes) else: self.assertGreater( likes_in_html, initial_likes ) # click on the "like" button again to switch back # to the previous state like_button.click() WebDriverWait(self.browser, 10).until( lambda x: int( self.browser.find_element_by_css_selector( ".like-badge" ).text ) == initial_likes ) sleep(2) # remove this after the first run -
测试将在
DEBUG = False模式下运行;因此,您必须确保所有静态文件在您的开发环境中都是可访问的。请确保您将以下行添加到项目 URL 配置中:# myproject/urls.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.conf.urls import patterns, include, url from django.conf import settings from django.conf.urls.static import static from django.contrib.staticfiles.urls import \ staticfiles_urlpatterns urlpatterns = patterns("",# … ) urlpatterns += staticfiles_urlpatterns() urlpatterns += static( settings.STATIC_URL, document_root=settings.STATIC_ROOT ) urlpatterns += static( settings.MEDIA_URL, document_root=settings.MEDIA_ROOT ) -
收集静态文件,以便测试服务器可以访问,如下所示:
(myproject_env)$ python manage.py collectstatic --noinput -
按照以下所示运行
locations应用的测试:(myproject_env)$ python manage.py test locations Creating test database for alias 'default'... . -------------------------------------------------------- Ran 1 test in 19.158s OK Destroying test database for alias 'default'...
它是如何工作的...
当我们运行这些测试时,Firefox 浏览器将打开并转到http://localhost:8081/admin/login/?next=/locations/hkw/的登录管理页面。
然后,用户名和密码字段将填写为test-admin,您将被重定向到Haus der Kulturen der Welt位置的详细页面,如下所示:http://localhost:8081/locations/hkw/。
在那里,您将看到点赞按钮被点击两次,导致点赞和取消点赞操作。
让我们看看这个测试套件是如何工作的。我们定义一个继承自LiveServerTestCase的类。这创建了一个测试套件,将在8081端口下运行本地服务器。setUpClass()类方法将在所有测试开始时执行,tearDownClass()类方法将在测试运行后执行。在中间,测试将执行套件中以test开头名称的所有方法。对于每个通过测试,你将在命令行工具中看到一个点(.),对于每个失败的测试,将有一个字母F,对于测试中的每个错误,你将看到一个字母E。最后,你将看到有关失败和错误测试的提示。由于我们目前在locations应用的套件中只有一个测试,所以你将只在那里看到一个点。
当我们开始测试时,会创建一个新的测试数据库。在setUpClass()中,我们创建一个浏览器对象、一个位置和一个超级用户。然后执行test_login_and_like()方法,该方法打开管理登录页面,找到用户名字段,输入管理员的用户名,找到密码字段,输入管理员的密码,找到提交按钮,并点击它。然后,它最多等待十秒钟,直到页面上可以找到具有.like-button CSS 类的 DOM 元素。
如你从第四章中实现点赞小部件的配方中可能记得,我们的小部件由两个元素组成:一个点赞按钮和一个显示总点赞数的徽章。如果按钮被点击,你的点赞将通过 Ajax 调用添加或从数据库中移除。此外,徽章计数将更新以反映数据库中的点赞数,如图所示:

在测试的进一步过程中,我们检查按钮的初始状态(是否有.active CSS 类),检查初始的点赞数,并模拟点击按钮。我们最多等待 10 秒钟,直到徽章中的计数发生变化。然后,我们检查徽章中的计数是否与数据库中该位置的点赞总数匹配。我们还将检查徽章中的计数是如何变化的(增加或减少)。最后,我们将再次模拟点击按钮以切换回之前的状态。
sleep()函数仅在测试中用于让你能够看到整个工作流程。你可以安全地删除它们,以使测试运行更快。
最后,调用tearDownClass()方法,该方法关闭浏览器并从测试数据库中删除位置和超级用户。
参见
-
在第四章中实现点赞小部件的配方,模板和 JavaScript
-
使用 mock 测试视图的配方
-
使用 Django REST Framework 创建的测试 API配方
使用 mock 测试视图
在本配方中,我们将探讨如何编写单元测试。单元测试是检查函数或方法是否返回正确结果的那种测试。我们再次以likes应用为例,编写测试以检查向json_set_like()视图发送请求时,对于未认证用户返回{"success": false},对于认证用户返回{"action": "added", "count": 1, "obj": "Haus der Kulturen der Welt", "success": true}。我们将使用Mock对象来模拟HttpRequest和AnonymousUser对象。
准备工作
让我们从实现 Like 小部件配方中的locations和likes应用开始,该配方位于第四章,模板和 JavaScript。
在你的虚拟环境中安装mock模块,如下所示:
(myproject_env)$ pip install mock
如何做...
我们将通过以下步骤使用模拟来测试点赞动作:
-
在你的
likes应用中创建名为tests.py的文件,内容如下:# likes/tests.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals import mock import json from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User from django.test import SimpleTestCase from locations.models import Location class JSSetLikeViewTest(SimpleTestCase): @classmethod def setUpClass(cls): super(JSSetLikeViewTest, cls).setUpClass() cls.location = Location.objects.create( title="Haus der Kulturen der Welt", slug="hkw", small_image="locations/2015/11/" "20151116013056_small.jpg", medium_image="locations/2015/11/" "20151116013056_medium.jpg", large_image="locations/2015/11/" "20151116013056_large.jpg", ) cls.content_type = \ ContentType.objects.get_for_model(Location) cls.username = "test-admin" cls.password = "test-admin" cls.superuser = User.objects.create_superuser( username=cls.username, password=cls.password, email="", ) @classmethod def tearDownClass(cls): super(JSSetLikeViewTest, cls).tearDownClass() cls.location.delete() cls.superuser.delete() def test_authenticated_json_set_like(self): from .views import json_set_like mock_request = mock.Mock() mock_request.user = self.superuser mock_request.method = "POST" response = json_set_like( mock_request, self.content_type.pk, self.location.pk ) expected_result = json.dumps({ "success": True, "action": "added", "obj": self.location.title, "count": Location.objects.count(), }) self.assertJSONEqual( response.content, expected_result ) def test_anonymous_json_set_like(self): from .views import json_set_like mock_request = mock.Mock() mock_request.user.is_authenticated.return_value = \ False mock_request.method = "POST" response = json_set_like( mock_request, self.content_type.pk, self.location.pk ) expected_result = json.dumps({ "success": False, }) self.assertJSONEqual( response.content, expected_result ) -
按如下方式运行
likes应用的测试:(myproject_env)$ python manage.py test likes Creating test database for alias 'default'... .. -------------------------------------------------------- Ran 2 tests in 0.093s OK Destroying test database for alias 'default'...
它是如何工作的...
就像在前一个配方中一样,当你为likes应用运行测试时,首先创建一个临时测试数据库。然后调用setUpClass()方法。稍后,执行以test开头的方法,最后调用tearDownClass()方法。
单元测试继承自SimpleTestCase类。在setUpClass()中,我们创建一个位置和一个超级用户。同时,我们找到Location模型的ContentType对象——我们将需要它在设置或删除不同对象的点赞视图中使用。作为提醒,视图看起来类似于以下内容,并返回 JSON 字符串作为结果:
def json_set_like(request, content_type_id, object_id):
# ...all the view logic goes here...
return HttpResponse(
json_str,
content_type="text/javascript; charset=utf-8"
)
在test_authenticated_json_set_like()和test_anonymous_json_set_like()方法中,我们使用了Mock对象。它们是具有任何属性或方法的对象。Mock对象的每个未定义属性或方法都是另一个Mock对象。因此,在 shell 中,你可以尝试如下链式属性:
>>> import mock
>>> m = mock.Mock()
>>> m.whatever.anything().whatsoever
<Mock name='mock.whatever.anything().whatsoever' id='4464778896'>
在我们的测试中,我们使用Mock对象来模拟HttpRequest和AnonymousUser对象。对于认证用户,我们仍然需要真实的User对象,因为视图需要用户的 ID 来保存到数据库中的Like对象。
因此,我们调用json_set_like()函数,查看返回的 JSON 响应是否正确:如果访问者未认证,则响应返回{"success": false};对于认证用户,返回类似{"action": "added", "count": 1, "obj": "Haus der Kulturen der Welt", "success": true}的内容。
最后,调用tearDownClass()类方法,从测试数据库中删除位置和超级用户。
参见
-
在第四章的实现 Like 小部件配方中,模板和 JavaScript的实现 Like 小部件配方
-
使用 Selenium 测试页面配方
-
使用 Django REST Framework 创建的 API 测试配方
测试使用 Django REST 框架创建的 API
我们已经了解了如何编写操作验收和单元测试。在这个配方中,我们将对本书早期创建的 REST API 进行组件接口测试。
小贴士
如果你不太熟悉 REST API 是什么以及如何使用它,你可以在 www.restapitutorial.com/ 上了解相关信息。
准备工作
让我们从第九章 数据导入和导出 中的 使用 Django REST 框架创建 API 配方中的 bulletin_board 应用开始。
如何做到这一点...
要测试 REST API,请执行以下步骤:
-
在你的
bulletin_board应用中创建一个tests.py文件,如下所示:# bulletin_board/tests.py # -*- coding: UTF-8 -*- from __future__ import unicode_literals from django.contrib.auth.models import User from django.core.urlresolvers import reverse from rest_framework import status from rest_framework.test import APITestCase from .models import Category, Bulletin class BulletinTests(APITestCase): @classmethod def setUpClass(cls): super(BulletinTests, cls).setUpClass() cls.superuser, created = User.objects.\ get_or_create( username="test-admin", ) cls.superuser.is_active = True cls.superuser.is_superuser = True cls.superuser.save() cls.category = Category.objects.create( title="Movies" ) cls.bulletin = Bulletin.objects.create( bulletin_type="searching", category=cls.category, title="The Matrix", description="There is no Spoon.", contact_person="Aidas Bendoraitis", ) cls.bulletin_to_delete = Bulletin.objects.create( bulletin_type="searching", category=cls.category, title="Animatrix", description="Trinity: " "There's a difference, Mr. Ash, " "between a trap and a test.", contact_person="Aidas Bendoraitis", ) @classmethod def tearDownClass(cls): super(BulletinTests, cls).tearDownClass() cls.category.delete() cls.bulletin.delete() cls.superuser.delete() -
添加一个方法来测试如下所示列出公告的 API 调用:
def test_list_bulletins(self): url = reverse("rest_bulletin_list") data = {} response = self.client.get(url, data, format="json") self.assertEqual( response.status_code, status.HTTP_200_OK ) self.assertEqual( response.data["count"], Bulletin.objects.count() ) -
添加一个方法来测试如下显示单个公告的 API 调用:
def test_get_bulletin(self): url = reverse("rest_bulletin_detail", kwargs={ "pk": self.bulletin.pk }) data = {} response = self.client.get(url, data, format="json") self.assertEqual( response.status_code, status.HTTP_200_OK ) self.assertEqual(response.data["id"], self.bulletin.pk) self.assertEqual( response.data["bulletin_type"], self.bulletin.bulletin_type ) self.assertEqual( response.data["category"]["id"], self.category.pk ) self.assertEqual( response.data["title"], self.bulletin.title ) self.assertEqual( response.data["description"], self.bulletin.description ) self.assertEqual( response.data["contact_person"], self.bulletin.contact_person ) -
添加一个方法来测试如果当前用户已认证,则创建公告的 API 调用,如下所示:
def test_create_bulletin_allowed(self): # login self.client.force_authenticate(user=self.superuser) url = reverse("rest_bulletin_list") data = { "bulletin_type": "offering", "category": {"title": self.category.title}, "title": "Back to the Future", "description": "Roads? Where we're going, " "we don't need roads.", "contact_person": "Aidas Bendoraitis", } response = self.client.post(url, data, format="json") self.assertEqual( response.status_code, status.HTTP_201_CREATED ) self.assertTrue(Bulletin.objects.filter( pk=response.data["id"] ).count() == 1) # logout self.client.force_authenticate(user=None) -
添加一个方法来测试尝试创建公告的 API 调用;然而,由于当前访客是匿名用户,操作失败,如下所示:
def test_create_bulletin_restricted(self): # make sure the user is logged out self.client.force_authenticate(user=None) url = reverse("rest_bulletin_list") data = { "bulletin_type": "offering", "category": {"title": self.category.title}, "title": "Back to the Future", "description": "Roads? Where we're going, " "we don't need roads.", "contact_person": "Aidas Bendoraitis", } response = self.client.post(url, data, format="json") self.assertEqual( response.status_code, status.HTTP_403_FORBIDDEN ) -
添加一个方法来测试如果当前用户已认证,则更改公告的 API 调用,如下所示:
def test_change_bulletin_allowed(self): # login self.client.force_authenticate(user=self.superuser) url = reverse("rest_bulletin_detail", kwargs={ "pk": self.bulletin.pk }) # change only title data = { "bulletin_type": self.bulletin.bulletin_type, "category": { "title": self.bulletin.category.title }, "title": "Matrix Resurrection", "description": self.bulletin.description, "contact_person": self.bulletin.contact_person, } response = self.client.put(url, data, format="json") self.assertEqual( response.status_code, status.HTTP_200_OK ) self.assertEqual(response.data["id"], self.bulletin.pk) self.assertEqual( response.data["bulletin_type"], "searching" ) # logout self.client.force_authenticate(user=None) -
添加一个方法来测试尝试更改公告的 API 调用;然而,由于当前访客是匿名用户,操作失败:
def test_change_bulletin_restricted(self): # make sure the user is logged out self.client.force_authenticate(user=None) url = reverse("rest_bulletin_detail", kwargs={ "pk": self.bulletin.pk }) # change only title data = { "bulletin_type": self.bulletin.bulletin_type, "category": { "title": self.bulletin.category.title }, "title": "Matrix Resurrection", "description": self.bulletin.description, "contact_person": self.bulletin.contact_person, } response = self.client.put(url, data, format="json") self.assertEqual( response.status_code, status.HTTP_403_FORBIDDEN ) -
添加一个方法来测试如果当前用户已认证,则删除公告的 API 调用,如下所示:
def test_delete_bulletin_allowed(self): # login self.client.force_authenticate(user=self.superuser) url = reverse("rest_bulletin_detail", kwargs={ "pk": self.bulletin_to_delete.pk }) data = {} response = self.client.delete(url, data, format="json") self.assertEqual( response.status_code, status.HTTP_204_NO_CONTENT ) # logout self.client.force_authenticate(user=None) -
添加一个方法来测试尝试删除公告的 API 调用;然而,由于当前访客是匿名用户,操作失败:
def test_delete_bulletin_restricted(self): # make sure the user is logged out self.client.force_authenticate(user=None) url = reverse("rest_bulletin_detail", kwargs={ "pk": self.bulletin_to_delete.pk }) data = {} response = self.client.delete(url, data, format="json") self.assertEqual( response.status_code, status.HTTP_403_FORBIDDEN ) -
按如下所示运行
bulletin_board应用的测试:(myproject_env)$ python manage.py test bulletin_board Creating test database for alias 'default'... ........ -------------------------------------------------------- Ran 8 tests in 0.081s OK Destroying test database for alias 'default'...
它是如何工作的...
REST API 测试套件扩展了 APITestCase 类。再次强调,我们有 setUpClass() 和 tearDownClass() 类方法,这些方法将在不同的测试之前和之后执行。此外,测试套件有一个 client 属性,它是 APIClient 类型的,可以用来模拟 API 调用。它有所有标准 HTTP 调用的方法:get()、post()、put()、patch()、delete()、head() 和 options();而在我们的测试中,我们使用的是 GET、POST 和 DELETE 请求。此外,client 有方法通过登录凭证、令牌或直接传递 User 对象来认证用户。在我们的测试中,我们通过第三种方式认证,即直接将用户传递给 force_authenticate() 方法。
代码的其余部分是自我解释的。
参见
-
在第九章 数据导入和导出 中的 使用 Django REST 框架创建 API 配方 Chapter 9,数据导入和导出
-
使用 Selenium 测试页面 的配方
-
使用模拟测试视图 的配方
发布可重用 Django 应用
Django 文档有一个教程,介绍如何打包你的可重用应用,以便可以在任何虚拟环境中使用 pip 安装:
docs.djangoproject.com/en/1.8/intro/reusable-apps/
然而,还有更好的方法来使用 Cookiecutter 工具打包和发布可重用的 Django 应用,它为不同的编码项目创建模板,例如新的 Django CMS 网站、Flask 网站,或 jQuery 插件。可用的项目模板之一是 cookiecutter-djangopackage。在这个食谱中,您将学习如何使用它来分发可重用的 likes 应用。
准备工作
在您的虚拟环境中安装 Cookiecutter:
(myproject_env)$ pip install cookiecutter
如何操作...
要发布您的 likes 应用,请按照以下步骤操作:
-
按照以下步骤开始一个新的 Django 应用项目:
(myapp_env)$ cookiecutter \ https://github.com/pydanny/cookiecutter-djangopackage.git -
回答问题以创建应用模板:
full_name [Your full name here]: Aidas Bendoraitis email [you@example.com]: aidas@bendoraitis.lt github_username [yourname]: archatas project_name [dj-package]: django-likes repo_name [dj-package]: django-likes app_name [djpackage]: likes project_short_description [Your project description goes here]: Django-likes allows your website users to like any object. release_date [2015-10-02]: year [2015]: version [0.1.0]: -
这将创建一个文件结构,如下面的图像所示:
![如何操作...]()
-
将
likes应用的文件从您正在使用的 Django 项目中复制到django-likes/likes目录。 -
将可重用应用项目添加到 GitHub 下的 Git 仓库。
-
探索不同的文件并完成许可、README、文档、配置和其他文件。
-
确保应用通过测试:
(myapp_env)$ pip install -r requirements-test.txt (myapp_env)$ python runtests.py -
如果您的包是封闭源代码,创建一个可共享的 ZIP 存档作为发布:
(myapp_env)$ python setup.py sdist这将创建一个
django-likes/dist/django-likes-0.1.0.tar.gz文件,可以使用 pip 安装或卸载,如下所示:(myproject_env)$ pip install django-likes-0.1.0.tar.gz (myproject_env)$ pip uninstall django-likes -
如果您的包是开源的,请在 Python 包索引(PyPI)上注册并发布您的应用。
(myapp_env)$ python setup.py register (myapp_env)$ python setup.py publish -
此外,为了传播信息,通过在
www.djangopackages.com/packages/add/提交表格,将您的应用添加到 Django 包中。
它是如何工作的...
Cookiecutter 填充了 Django 应用项目模板的不同部分中输入的请求数据。因此,您将获得一个准备分发的 setup.py 文件,用于 Python 包索引、Sphinx 文档、BSD 作为默认许可、项目的通用文本编辑配置、包含在您的应用中的静态文件和模板,以及其他好东西。
参考也
-
在第一章的创建项目文件结构食谱中,使用 Django 1.8 入门
-
在第一章的使用 pip 处理项目依赖食谱中,使用 Django 1.8 入门
-
在第四章的实现点赞小部件食谱中,模板和 JavaScript中
通过电子邮件获取详细的错误报告
为了执行系统日志,Django 使用 Python 内置的日志模块。默认的 Django 配置似乎相当复杂。在这个食谱中,您将学习如何调整它,以便在发生错误时发送包含完整 HTML 的错误电子邮件,类似于 Django 在 DEBUG 模式下提供的。
准备工作
在您的虚拟环境中定位 Django 项目。
如何操作...
以下步骤将帮助您发送关于错误的详细电子邮件:
-
在文本编辑器中打开
myproject_env/lib/python2.7/site-packages/django/utils/log.py文件,并将DEFAULT_LOGGING字典复制到您的项目设置中的LOGGING字典。 -
将
include_html设置添加到mail_admins处理器中,如下所示:# myproject/conf/base.py or myproject/settings.py LOGGING = { "version": 1, "disable_existing_loggers": False, "filters": { "require_debug_false": { "()": "django.utils.log.RequireDebugFalse", }, "require_debug_true": { "()": "django.utils.log.RequireDebugTrue", }, }, "handlers": { "console": { "level": "INFO", "filters": ["require_debug_true"], "class": "logging.StreamHandler", }, "null": { "class": "django.utils.log.NullHandler", }, "mail_admins": { "level": "ERROR", "filters": ["require_debug_false"], "class": "django.utils.log.AdminEmailHandler", "include_html": True, } }, "loggers": { "django": { "handlers": ["console"], }, "django.request": { "handlers": ["mail_admins"], "level": "ERROR", "propagate": False, }, "django.security": { "handlers": ["mail_admins"], "level": "ERROR", "propagate": False, }, "py.warnings": { "handlers": ["console"], }, } }
工作原理...
日志配置包括四个部分:记录器、处理器、过滤器和格式化器。以下是如何描述它们:
-
记录器是日志系统中的入口点。每个记录器都可以有一个日志级别:
DEBUG、INFO、WARNING、ERROR或CRITICAL。当消息写入记录器时,消息的日志级别将与记录器的级别进行比较。如果它符合或超过记录器的日志级别,它将被进一步处理。否则,消息将被忽略。 -
处理器是定义记录器中每个消息如何处理的引擎。它们可以写入控制台、通过电子邮件发送给管理员、保存到日志文件、发送到 Sentry 错误日志服务等等。在我们的案例中,我们为
mail_admins处理器设置了include_html参数,因为我们希望包含完整的 HTML、跟踪信息和局部变量,以便于我们 Django 项目中发生的错误消息。 -
过滤器提供了对从记录器传递到处理器的消息的额外控制。例如,在我们的案例中,只有当 DEBUG 模式设置为
False时,才会发送电子邮件。 -
格式化器用于定义如何将日志消息渲染为字符串。在本例中未使用;然而,有关日志的更多信息,您可以参考官方文档
docs.djangoproject.com/en/1.8/topics/logging/。
相关内容
- 使用 mod_wsgi 在 Apache 上部署教程
使用 mod_wsgi 在 Apache 上部署
关于如何部署您的 Django 项目,有许多选择。在本教程中,我将指导您如何在配备 Virtualmin 的专用 Linux 服务器上部署 Django 项目。
专用服务器是一种互联网托管类型,您租赁的是整个服务器,不会与其他人共享。Virtualmin 是一个网络托管控制面板,允许您管理虚拟域名、邮箱、数据库和整个服务器,而无需深入了解服务器管理的命令行程序。
要运行 Django 项目,我们将使用带有mod_wsgi模块的 Apache 网络服务器和 MySQL 数据库。
准备工作
确保您已在您的专用 Linux 服务器上安装了 Virtualmin。有关说明,请参阅www.virtualmin.com/download.html。
如何操作...
按照以下步骤在配备 Virtualmin 的 Linux 服务器上部署 Django 项目:
-
以 root 用户登录到 Virtualmin 并将服务器的用户默认 shell 从
sh更改为bash。这可以通过导航到 Virtualmin | 系统定制 | 自定义 Shell 来完成,如下面的截图所示:![如何操作...]()
-
通过导航到 Virtualmin | 创建虚拟服务器 为你的项目创建一个虚拟服务器。启用以下功能:为域名设置网站? 和 创建 MySQL 数据库?。你为域名设置的用户名和密码也将用于 SSH 连接、FTP 和 MySQL 数据库访问,如下所示:
![如何操作...]()
-
登录到你的域名管理面板,并将你的域名的
A记录设置为专用服务器的 IP 地址。 -
以 root 用户通过 Secure Shell 连接到专用服务器,并在系统范围内安装 Python 库,
pip,virtualenv,MySQLdb和Pillow。 -
确保默认的 MySQL 数据库编码为 UTF-8:
-
在远程服务器上编辑 MySQL 配置文件,例如,使用 nano 编辑器:
$ ssh root@myproject.com root@myproject.com's password: $ nano /etc/mysql/my.cnf添加或编辑以下配置:
[client] default-character-set=utf8 [mysql] default-character-set=utf8 [mysqld] collation-server=utf8_unicode_ci init-connect='SET NAMES utf8' character-set-server=utf8 -
按 Ctrl + O 保存更改,并按 Ctrl + X 退出 nano 编辑器。
-
然后,按照以下步骤重新启动 MySQL 服务器:
$ /etc/init.d/mysql restart -
按 Ctrl + D 退出 Secure Shell。
-
-
当你在 Virtualmin 中创建一个域名时,该域的用户会自动创建。以你的 Django 项目的用户身份通过 Secure Shell 连接到专用服务器,并按照以下步骤为你的项目创建一个虚拟环境:
$ ssh myproject@myproject.com myproject@myproject.com's password: $ virtualenv . --system-site-packages $ echo source ~/bin/activate >> .bashrc $ source ~/bin/activate (myproject)myproject@server$提示
每次你通过 Secure Shell 以与域名相关的用户连接到你的 Django 项目时,
.bashrc脚本都会被调用。该脚本将自动激活此项目的虚拟环境。 -
如果你将项目代码托管在 Bitbucket 上,你需要设置 SSH 密钥以避免从或向 Git 仓库拉取或推送时出现密码提示:
-
逐个执行以下命令:
(myproject)myproject@server$ ssh-keygen (myproject)myproject@server$ ssh-agent /bin/bash (myproject)myproject@server$ ssh-add ~/.ssh/id_rsa (myproject)myproject@server$ cat ~/.ssh/id_rsa.pub -
最后一条命令将打印出你需要复制并粘贴到 Bitbucket 网站上的 管理账户 | SSH 密钥 | 添加密钥 的 SSH 公钥。
-
-
创建一个
project目录,进入它,并按照以下步骤克隆你的项目代码:(myproject)myproject@server$ git clone \ git@bitbucket.org:somebitbucketuser/myproject.git myproject提示
现在,你的项目路径应该类似于以下:
/home/myproject/project/myproject -
按照以下步骤安装你项目的 Python 需求,包括指定的 Django 版本:
(myproject)myproject@server$ pip install -r requirements.txt -
在你的项目目录下创建
media,tmp和static目录。 -
此外,创建一个与以下设置类似的
local_settings.py:# /home/myproject/project/myproject/myproject/local_settings.py DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "myproject", "USER": "myproject", "PASSWORD": "mypassword", } } PREPEND_WWW = True DEBUG = False ALLOWED_HOSTS = ["myproject.com"] -
导入你本地创建的数据库转储。如果你使用的是 Mac,你可以使用一个应用程序,Sequel Pro (
www.sequelpro.com/),通过 SSH 连接来完成。你也可以通过 FTP 将数据库转储上传到服务器,然后在 Secure Shell 中运行以下命令:(myproject)myproject@server$ python manage.py dbshell < \ ~/db_backups/db.sql -
按照以下步骤收集静态文件:
(myproject)myproject@server$ python manage.py collectstatic \ --noinput -
进入
~/public_html目录,并使用 nano 编辑器(或您选择的任何编辑器)创建一个wsgi文件:# /home/myproject/public_html/my.wsgi #!/home/myproject/bin/python # -*- coding: utf-8 -*- import os, sys, site django_path = os.path.abspath( os.path.join(os.path.dirname(__file__), "../lib/python2.6/site-packages/"), ) site.addsitedir(django_path) project_path = os.path.abspath( os.path.join(os.path.dirname(__file__), "../project/myproject"), ) sys.path += [project_path] os.environ["DJANGO_SETTINGS_MODULE"] = "myproject.settings" from django.core.wsgi import get_wsgi_application application = get_wsgi_application() -
然后,在同一目录中创建
.htaccess文件。.htaccess文件将重定向所有请求到在wsgi文件中设置的 Django 项目,如下所示:# /home/myproject/public_html/.htaccess AddHandler wsgi-script .wsgi DirectoryIndex index.html RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME}/index.html !-f RewriteCond %{REQUEST_URI} !^/media/ RewriteCond %{REQUEST_URI} !^/static/ RewriteRule ^(.*)$ /my.wsgi/$1 [QSA,L] -
将
.htaccess复制为.htaccess_live。 -
然后,也为维护情况创建
.htaccess_maintenance。这个新的 Apache 配置文件将向除了您之外的所有用户显示temporarily-offline.html,这些用户通过您的局域网或计算机的 IP 地址被识别。您可以通过谷歌搜索what's my ip来检查您的 IP。以下是如何.htaccess_maintenance看起来:# /home/myproject/public_html/.htaccess_maintenance AddHandler wsgi-script .wsgi DirectoryIndex index.html RewriteEngine On RewriteBase / RewriteCond %{REMOTE_HOST} !¹\.2\.3\.4$ RewriteCond %{REQUEST_URI} !/temporarily-offline\.html RewriteCond %{REQUEST_URI} !^/media/ RewriteCond %{REQUEST_URI} !^/static/ RewriteRule .* /temporarily-offline.html [R=302,L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME}/index.html !-f RewriteCond %{REQUEST_URI} !^/media/ RewriteCond %{REQUEST_URI} !^/static/ RewriteRule ^(.*)$ /my.wsgi/$1 [QSA,L]小贴士
将此文件中的 IP 数字替换为您的 IP 地址。
-
然后,创建一个当您的网站宕机时将显示的 HTML 文件:
<!-- /home/myproject/public_html/temporarily-offline.html --> The site is being updated... Please come back later. -
通过 Secure Shell 以 root 用户身份登录到服务器并编辑 Apache 配置:
-
打开域名配置文件,如下所示:
$ nano /etc/apache2/sites-available/myproject.mydomain.conf -
在
</VirtualHost>之前添加以下行:Options -Indexes AliasMatch ^/static/\d+/(.*) \ "/home/myproject/project/myproject/static/$1" AliasMatch ^/media/(.*) \ "/home/myproject/project/myproject/media/$1" <FilesMatch "\.(ico|pdf|flv|jpe?g|png|gif|js|css|swf)$"> ExpiresActive On ExpiresDefault "access plus 1 year" </FilesMatch> -
重启 Apache 以使更改生效:
$ /etc/init.d/apache2 restart
-
-
设置默认的预定 cron 作业。有关如何操作的更多信息,请参阅设置 cron 作业以执行常规任务配方。
它是如何工作的...
使用此配置,media和static目录中的文件将直接由 Apache 提供服务;而所有其他 URL 都由 Django 项目通过my.wsgi文件处理。
使用 Apache 站点配置中的<FilesMatch>指令,所有媒体文件都被设置为缓存一年。静态 URL 路径有一个编号的前缀,每次您从 Git 仓库更新代码时都会更改。
当您需要更新网站并希望将其关闭进行维护时,您必须将.htaccess_maintenance复制到.htaccess。当您想要再次设置网站时,您必须将.htaccess_live复制到.htaccess。
更多内容...
要查找托管您的 Django 项目的其他选项,请参阅:djangofriendly.com/hosts/。
参见
-
第一章中的创建项目文件结构配方,Django 1.8 入门
-
第一章中的使用 pip 处理项目依赖项配方,Django 1.8 入门
-
第一章中的为 Git 用户动态设置 STATIC_URL 配方,Django 1.8 入门
-
第一章中的将 UTF-8 设置为 MySQL 配置的默认编码配方,Django 1.8 入门
-
创建和使用 Fabric 部署脚本配方
-
设置 cron 作业以执行常规任务配方
设置 cron 作业以执行常规任务
通常网站每周、每天或每小时都需要在后台执行一些管理任务。这可以通过 cron 作业实现,也称为计划任务。这些是在指定时间段内在服务器上运行的脚本。在这个配方中,我们将创建两个 cron 作业:一个用于清除数据库中的会话,另一个用于备份数据库数据。两者都将每晚运行。
准备工作
首先,将你的 Django 项目部署到远程服务器上。然后,通过 SSH 连接到服务器。
如何操作...
让我们按照以下步骤创建两个脚本并使它们定期运行:
-
在你的项目主目录中创建
commands、db_backups和logs目录:(myproject)myproject@server$ mkdir commands (myproject)myproject@server$ mkdir db_backups (myproject)myproject@server$ mkdir logs -
在
commands目录中,创建一个包含以下内容的cleanup.sh文件:# /home/myproject/commands/cleanup.sh #! /usr/bin/env bash PROJECT_PATH=/home/myproject CRON_LOG_FILE=${PROJECT_PATH}/logs/cleanup.log echo "Cleaning up the database" > ${CRON_LOG_FILE} date >> ${CRON_LOG_FILE} cd ${PROJECT_PATH} . bin/activate cd project/myproject python manage.py cleanup --traceback >> \ ${CRON_LOG_FILE} 2>&1 -
使以下文件可执行:
(myproject)myproject@server$ chmod +x cleanup.sh -
然后,在同一个目录中,创建一个包含以下内容的
backup_db.sh文件:# /home/myproject/commands/cleanup.sh #! /usr/bin/env bash PROJECT_PATH=/home/myproject CRON_LOG_FILE=${PROJECT_PATH}/logs/backup_db.log WEEK_DATE=$(LC_ALL=en_US.UTF-8 date +"%w-%A") BACKUP_PATH=${PROJECT_PATH}/db_backups/${WEEK_DATE}.sql DATABASE=myproject USER=my_db_user PASS=my_db_password EXCLUDED_TABLES=( django_session ) IGNORED_TABLES_STRING='' for TABLE in "${EXCLUDED_TABLES[@]}" do : IGNORED_TABLES_STRING+=\ " --ignore-table=${DATABASE}.${TABLE}" done echo "Creating DB Backup" > ${CRON_LOG_FILE} date >> ${CRON_LOG_FILE} cd ${PROJECT_PATH} mkdir -p db_backups echo "Dump structure" >> ${CRON_LOG_FILE} mysqldump -u ${USER} -p${PASS} --single-transaction \ --no-data ${DATABASE} > ${BACKUP_PATH} 2>> ${CRON_LOG_FILE} echo "Dump content" >> ${CRON_LOG_FILE} mysqldump -u ${USER} -p${PASS} ${DATABASE} \ ${IGNORED_TABLES_STRING} >> ${BACKUP_PATH} 2>> \ ${CRON_LOG_FILE} -
使以下文件也可执行:
(myproject)myproject@server$ chmod +x backup_db.sh -
通过运行脚本并检查
logs目录中的*.log文件来测试脚本是否正确执行,如下所示:(myproject)myproject@server$ ./cleanup.sh (myproject)myproject@server$ ./backup_db.sh -
在你的项目主目录中创建一个包含以下任务的
crontab.txt文件:00 01 * * * /home/myproject/commands/cleanup.sh 00 02 * * * /home/myproject/commands/backup_db.sh -
按照以下方式安装 crontab 任务:
(myproject)myproject@server$ crontab -e crontab.txt
它是如何工作的...
在当前设置下,每晚 1 点 cleanup.sh 将被执行,2 点 backup_db.sh 将被执行。执行日志将保存在 cleanup.log 和 backup_db.log 文件中。如果你遇到任何错误,你应该检查这些文件以查找错误跟踪。
数据库备份脚本稍微复杂一些。每周的每一天,它都会为该天创建一个名为 0-Sunday.sql、1-Monday.sql 等的备份文件。因此,你将能够恢复七天前或更晚的数据。首先,备份脚本导出所有表的数据库模式,然后导出所有表的数据,除了在 EXCLUDED_TABLES(目前是,即 django_session)中并列在一起的那些表。
crontab 语法如下:每行包含一个特定的时间段和一个要运行的任务。时间由五个部分组成,由空格分隔,如下所示:
-
从 0 到 59 分钟
-
从 0 到 23 小时
-
一个月中的天从 1 到 31
-
从 1 到 12 个月
-
一周中的天从 0 到 7,其中 0 是星期日,1 是星期一,以此类推。7 又是星期日。
星号(*)表示将使用每个时间段。因此,以下任务定义了每天每月每天都要执行的 cleanup.sh 在凌晨 1:00。
00 01 * * * /home/myproject/commands/cleanup.sh
你可以在 en.wikipedia.org/wiki/Cron 上了解更多关于 crontab 的详细信息。
参见
-
使用 mod_wsgi 在 Apache 上部署 的配方
-
创建和使用 Fabric 部署脚本 的配方
创建和使用 Fabric 部署脚本
通常,要更新你的网站,你必须执行诸如设置维护页面、停止 cron 作业、创建数据库备份、从仓库拉取新代码、迁移数据库、收集静态文件、测试、再次启动 cron 作业以及取消维护页面等重复性任务。这是一项相当繁琐的工作,其中可能会出错。此外,你还需要记住预发布网站(可以测试新功能的地方)和生产网站(向公众展示的地方)的不同程序。幸运的是,有一个名为 Fabric 的 Python 库允许你自动化这些任务。在这个菜谱中,你将学习如何创建 fabfile.py,Fabric 的脚本,以及如何在预发布和生产环境中部署你的项目。
可以从包含它的目录中调用 Fabric 脚本,如下所示:
(myproject_env)$ fab staging deploy
这将在预发布服务器上部署项目。
准备工作
使用 在 Apache 上使用 mod_wsgi 部署 的说明设置类似的预发布和生产网站。全局或在你的项目虚拟环境中安装 Fabric,如下所示:
$ pip install fabric
如何操作...
我们将首先在 Django 项目目录中创建一个 fabfile.py 文件,包含几个函数,如下所示:
# fabfile.py
# -*- coding: UTF-8 -*-
from fabric.api import env, run, prompt, local, get, sudo
from fabric.colors import red, green
from fabric.state import output
env.environment = ""
env.full = False
output['running'] = False
PRODUCTION_HOST = "myproject.com"
PRODUCTION_USER = "myproject"
def dev():
""" chooses development environment """
env.environment = "dev"
env.hosts = [PRODUCTION_HOST]
env.user = PRODUCTION_USER
print("LOCAL DEVELOPMENT ENVIRONMENT\n")
def staging():
""" chooses testing environment """
env.environment = "staging"
env.hosts = ["staging.myproject.com"]
env.user = "myproject"
print("STAGING WEBSITE\n")
def production():
""" chooses production environment """
env.environment = "production"
env.hosts = [PRODUCTION_HOST]
env.user = PRODUCTION_USER
print("PRODUCTION WEBSITE\n")
def full():
""" all commands should be executed without questioning """
env.full = True
def deploy():
""" updates the chosen environment """
if not env.environment:
while env.environment not in ("dev", "staging",
"production"):
env.environment = prompt(red('Please specify target'
'environment ("dev", "staging", or '
'"production"): '))
print
globals()["_update_%s" % env.environment]()
dev()、staging() 和 production() 函数为当前任务设置适当的环境。然后,deploy() 函数分别调用 _update_dev()、_update_staging() 或 _update_production() 私有函数。让我们在同一个文件中定义这些私有函数,如下所示:
-
在开发环境中部署的函数将可选执行以下任务:
-
使用生产数据库中的数据更新本地数据库
-
从生产服务器下载媒体文件
-
从 Git 仓库更新代码
-
迁移本地数据库
让我们在 Fabric 脚本文件中创建这个函数,如下所示:
def _update_dev(): """ updates development environment """ run("") # password request print if env.full or "y" == prompt(red("Get latest " "production database (y/n)?"), default="y"): print(green(" * creating production-database " "dump...")) run("cd ~/db_backups/ && ./backup_db.sh --latest") print(green(" * downloading dump...")) get("~/db_backups/db_latest.sql", "tmp/db_latest.sql") print(green(" * importing the dump locally...")) local("python manage.py dbshell < " "tmp/db_latest.sql && rm tmp/db_latest.sql") print if env.full or "y" == prompt("Call prepare_dev " "command (y/n)?", default="y"): print(green(" * preparing data for " "development...")) local("python manage.py prepare_dev") print if env.full or "y" == prompt(red("Download media " "uploads (y/n)?"), default="y"): print(green(" * creating an archive of media " "uploads...")) run("cd ~/project/myproject/media/ " "&& tar -cz -f " "~/project/myproject/tmp/media.tar.gz *") print(green(" * downloading archive...")) get("~/project/myproject/tmp/media.tar.gz", "tmp/media.tar.gz") print(green(" * extracting and removing archive " "locally...")) for host in env.hosts: local("cd media/ " "&& tar -xzf ../tmp/media.tar.gz " "&& rm tmp/media.tar.gz") print(green(" * removing archive from the " "server...")) run("rm ~/project/myproject/tmp/media.tar.gz") print if env.full or "y" == prompt(red("Update code (y/n)?"), default="y"): print(green(" * updating code...")) local("git pull") print if env.full or "y" == prompt(red("Migrate database " "schema (y/n)?"), default="y"): print(green(" * migrating database schema...")) local("python manage.py migrate --no-initial-data") local("python manage.py syncdb") print -
-
在预发布环境中部署的函数将可选执行以下任务:
-
设置维护页面,说明网站正在更新,访客应等待或稍后回来
-
停止计划中的 cron 作业
-
从生产数据库获取最新数据
-
从生产数据库获取最新的媒体文件
-
从 Git 仓库拉取代码
-
收集静态文件
-
迁移数据库模式
-
重启 Apache 网络服务器
-
启动计划中的 cron 作业
-
取消维护页面
让我们在 Fabric 脚本中创建这个函数,如下所示:
def _update_staging(): """ updates testing environment """ run("") # password request print if env.full or "y" == prompt(red("Set under-" "construction screen (y/n)?"), default="y"): print(green(" * Setting maintenance screen")) run("cd ~/public_html/ " "&& cp .htaccess_under_construction .htaccess") print if env.full or "y" == prompt(red("Stop cron jobs " " (y/n)?"), default="y"): print(green(" * Stopping cron jobs")) sudo("/etc/init.d/cron stop") print if env.full or "y" == prompt(red("Get latest " "production database (y/n)?"), default="y"): print(green(" * creating production-database " "dump...")) run("cd ~/db_backups/ && ./backup_db.sh --latest") print(green(" * downloading dump...")) run("scp %(user)s@%(host)s:" "~/db_backups/db_latest.sql " "~/db_backups/db_latest.sql" % { "user": PRODUCTION_USER, "host": PRODUCTION_HOST, } ) print(green(" * importing the dump locally...")) run("cd ~/project/myproject/ && python manage.py " "dbshell < ~/db_backups/db_latest.sql") print if env.full or "y" == prompt(red("Call " " prepare_staging command (y/n)?"), default="y"): print(green(" * preparing data for " " testing...")) run("cd ~/project/myproject/ " "&& python manage.py prepare_staging") print if env.full or "y" == prompt(red("Get latest media " " (y/n)?"), default="y"): print(green(" * updating media...")) run("scp -r %(user)s@%(host)s:" "~/project/myproject/media/* " " ~/project/myproject/media/" % { "user": PRODUCTION_USER, "host": PRODUCTION_HOST, } ) print if env.full or "y" == prompt(red("Update code (y/n)?"), default="y"): print(green(" * updating code...")) run("cd ~/project/myproject " "&& git pull") print if env.full or "y" == prompt(red("Collect static " "files (y/n)?"), default="y"): print(green(" * collecting static files...")) run("cd ~/project/myproject " "&& python manage.py collectstatic --noinput") print if env.full or "y" == prompt(red('Migrate database " " schema (y/n)?'), default="y"): print(green(" * migrating database schema...")) run("cd ~/project/myproject " "&& python manage.py migrate " "--no-initial-data") run("cd ~/project/myproject " "&& python manage.py syncdb") print if env.full or "y" == prompt(red("Restart webserver " "(y/n)?"), default="y"): print(green(" * Restarting Apache")) sudo("/etc/init.d/apache2 graceful") print if env.full or "y" == prompt(red("Start cron jobs " "(y/n)?"), default="y"): print(green(" * Starting cron jobs")) sudo("/etc/init.d/cron start") print if env.full or "y" == prompt(red("Unset under-" "construction screen (y/n)?"), default="y"): print(green(" * Unsetting maintenance screen")) run("cd ~/public_html/ " "&& cp .htaccess_live .htaccess") print -
-
在生产环境中部署的函数将可选执行以下任务:
-
设置维护页面,说明网站正在更新,访客应等待或稍后回来
-
停止计划中的 cron 作业
-
备份数据库
-
从 Git 仓库拉取代码
-
收集静态文件
-
迁移数据库模式
-
重启 Apache 网络服务器
-
启动计划中的 cron 作业
-
取消维护页面
让我们在 Fabric 脚本中创建这个函数,如下所示:
def _update_production(): """ updates production environment """ if "y" != prompt(red("Are you sure you want to " "update " + red("production", bold=True) + \ " website (y/n)?"), default="n"): return run("") # password request print if env.full or "y" == prompt(red("Set under-" "construction screen (y/n)?"), default="y"): print(green(" * Setting maintenance screen")) run("cd ~/public_html/ " "&& cp .htaccess_under_construction .htaccess") print if env.full or "y" == prompt(red("Stop cron jobs" " (y/n)?"), default="y"): print(green(" * Stopping cron jobs")) sudo("/etc/init.d/cron stop") print if env.full or "y" == prompt(red("Backup database " "(y/n)?"), default="y"): print(green(" * creating a database dump...")) run("cd ~/db_backups/ " "&& ./backup_db.sh") print if env.full or "y" == prompt(red("Update code (y/n)?"), default="y"): print(green(" * updating code...")) run("cd ~/project/myproject/ " "&& git pull") print if env.full or "y" == prompt(red("Collect static " "files (y/n)?"), default="y"): print(green(" * collecting static files...")) run("cd ~/project/myproject " "&& python manage.py collectstatic --noinput") print if env.full or "y" == prompt(red("Migrate database " "schema (y/n)?"), default="y"): print(green(" * migrating database schema...")) run("cd ~/project/myproject " "&& python manage.py migrate " "--no-initial-data") run("cd ~/project/myproject " "&& python manage.py syncdb") print if env.full or "y" == prompt(red("Restart webserver " "(y/n)?"), default="y"): print(green(" * Restarting Apache")) sudo("/etc/init.d/apache2 graceful") print if env.full or "y" == prompt(red("Start cron jobs " "(y/n)?"), default="y"): print(green(" * Starting cron jobs")) sudo("/etc/init.d/cron start") print if env.full or "y" == prompt(red("Unset under-" "construction screen (y/n)?"), default="y"): print(green(" * Unsetting maintenance screen")) run("cd ~/public_html/ " "&& cp .htaccess_live .htaccess") print -
它是如何工作的...
在fabfile.py文件中的每个非私有函数都成为从命令行工具调用可能的参数。要查看所有可用的函数,请运行以下命令:
(myproject_env)$ fab --list
Available commands:
deploy updates the chosen environment
dev chooses development environment
full all commands should be executed without questioning
production chooses production environment
staging chooses testing environment
这些函数的调用顺序与它们传递给 Fabric 脚本的顺序相同,因此你在部署到不同环境时需要小心参数的顺序:
-
要在开发环境中部署,请运行以下命令:
(myproject_env)$ fab dev deploy这将提出类似于以下的问题:
Get latest production database (y/n)? [y] _当回答是时,将执行特定的步骤。
-
要在预发布环境中部署,请运行以下命令:
(myproject_env)$ fab staging deploy -
最后,要在生产环境中部署,请运行以下命令:
(myproject_env)$ fab production deploy
对于部署的每一步,你都会被询问是否要执行它或跳过它。如果你想要在不进行任何提示(除了密码请求)的情况下执行所有步骤,请在部署脚本中添加一个full参数,如下所示:
(myproject_env)$ fab dev full deploy
Fabric 脚本使用几个基本函数,可以描述如下:
-
local(): 这个函数用于在当前计算机上本地运行命令 -
run(): 这个函数用于在远程服务器上以指定用户运行命令 -
prompt(): 这个函数用于提问 -
get(): 这个函数用于从远程服务器下载文件到本地计算机 -
sudo(): 这个函数用于以 root(或其他)用户运行命令
Fabric 使用安全外壳连接在远程服务器上执行任务。每个run()或sudo()命令作为一个单独的连接执行;因此,当你想要一次性执行多个命令时,你必须要么在服务器上创建一个bash脚本并通过 Fabric 调用它,要么使用&&shell 运算符来分隔命令,该运算符仅在上一条命令成功后才会执行下一条命令。
我们还使用scp命令从生产服务器复制文件到预发布服务器。递归复制指定目录下所有文件的scp语法类似于以下:
scp -r myproject_user@myproject.com:/path/on/production/server/* \
/path/on/staging/server/
为了使输出更友好,我们使用了颜色,如下所示:
print(green(" * migrating database schema..."))
部署脚本期望你拥有两个管理命令:prepare_dev和prepare_staging。由你自己决定将这些命令放在哪里。基本上,你可以更改超级用户密码为更简单的一个,并更改那里的站点域名。如果你不需要这样的功能,只需从 Fabric 脚本中删除它即可。
一般原则是不要在 Git 仓库中保存任何敏感数据,因此,例如,为了备份数据库,我们在远程生产服务器上调用backup_db.sh脚本。这样的文件内容可能类似于以下:
# ~/db_backups/backup_db.sh
#!/bin/bash
if [[ $1 = '--latest' ]]
then
today="latest"
else
today=$(date +%Y-%m-%d-%H%M)
fi
mysqldump --opt -u my_db_user -pmy_db_password myproject > \
db_$today.sql
你可以使用以下方法使其可执行:
$ chmod +x backup_db.sh
当运行前面的命令而不带参数时,它将在文件名中包含日期和时间来创建数据库备份,例如,db_2014-04-24-1400.sql,如下所示:
$ ./backup_db.sh
当传递 --latest 参数时,备份文件的名称将是 db_latest.sql:
$ ./backup_db.sh --latest
还有更多...
Fabric 脚本不仅可以用于部署,还可以用于在远程服务器上执行任何常规操作,例如,当你使用 Rosetta 工具在线翻译 *.po 文件时收集可翻译字符串,当你使用 Haystack 进行全文搜索时重建搜索索引,按需创建备份,调用自定义管理命令,等等。
要了解更多关于 Fabric 的信息,请参考以下网址:docs.fabfile.org/en/1.10/。
参见
- 使用 mod_wsgi 在 Apache 上部署 菜单









浙公网安备 33010602011771号