如果你在 web 开发领域工作了足够长的时间,你最终会得出这样的结论:你可以用任何 web 框架和编程语言产生相同的结果。但是,虽然您实际上可以产生相同的结果,但是差异很大的是您创建解决方案所花费的时间:创建原型的时间、添加新功能的时间、进行测试的时间、进行调试的时间、部署到规模的时间等等。
从这个意义上来说,Django 框架使用了一套设计原则,与许多其他 web 框架相比,它产生了最有生产力的 web 开发过程之一。注意,我并不是说 Django 是银弹(例如,最好的原型,最具伸缩性);我是说,最终,Django 框架包含了一组设计原则和权衡,使其成为构建大多数大中型 web 应用所需特性的最有效的框架之一。
显式导致 web 应用容易被更多的人理解和维护。对于最初没有编写 web 应用的人来说,添加新功能或理解 web 应用背后的逻辑可能已经够难了,但是如果您加入了具有隐式行为的 mix 构造,用户在试图弄清楚隐式地做了什么时只会面临更大的挫折。Explicit 确实需要更多的输入工作,但是当您将它与您可能面临的调试或解决问题的潜在努力相比较时,这是非常值得的。
为了更好地理解这种明确性,我将给出一个 Django 视图方法和一个等价的 Ruby on Rails 视图方法,这两个方法执行相同的逻辑,即通过给定的 id 获取存储并将响应路由到模板。以下代码片段是 Ruby on Rails 版本;请注意带有#的行,它们是注释,表示正在发生的事情。
class StoresController < ApplicationController
def show
# Automatic access to params, a ruby hash with request parameters and view parameters
@store = Store.find(params[:id])
# Instance variables like @store are automatically passed on to view template
# Automatically uses template views/stores/show.html.erb
end
end
# Explicit request variable contains request parameters
# Other view parameters must be explicitly passed to views
def detail(request, store_id):
store = Store.objects.get(id=store_id)
# Instance variables must be explicitly passed on to a view template
# Explicit template must be assigned
return render(request, 'stores/detail.html', {'store': store})
Ruby on Rails 视图方法的隐含性通常被称为“魔力”,甚至被许多人认为是一种特性。之所以称之为‘魔法’,是因为某些行为是在幕后提供的。然而,除非你对框架和应用了如指掌,否则很难确定为什么会发生某些事情,这使得修复或更新变得更加困难。因此,尽管“魔术”可能在开始时为您节省几分钟或几个小时的开发时间,但它最终会花费您几个小时或几天的维护时间。
所以就像在 Python 中一样,Django 框架总是偏爱显式方法而不是隐式技术。
需要指出的是,显式不等于冗长或多余。虽然与隐式驱动的 web 框架(例如 Rails)相比,您最终肯定会在 Django 中键入更多的代码,但正如在前面的 DRY principle 部分中所描述的那样,Django 框架尽力避免在 web 应用中引入不必要的代码。
[user@∼]$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
Listing 1-1.Python interactive session
[user@∼]$ virtualenv --python=python3 mydjangosandbox
Already using interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /mydjangosandbox/bin/python3
Also creating executable in /mydjangosandbox/bin/python
Installing setuptools, pkg_resources, pip, wheel...done.
Listing 1-4.Create virtual Python environment
with virtualenv
[user@∼]$ source ./bin/activate
[(mydjangosandbox)user@∼] $
# NOTE: source is a Unix/Linux specific command, for other OS just execute activate
Listing 1-6.
Activate
virtual Python environment
[user@coffeehouse ∼]$ python manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
You have 13 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
May 23, 2017 - 22:41:20
Django version 1.11, using settings 'coffeehouse.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Listing 1-12.Start Django development web server
provided by manage.py
有时更改 Django 开发 web 服务器的默认地址和端口很方便。这可能是因为默认端口正被另一个服务占用,或者需要将 web 服务器绑定到非本地地址,以便远程计算机上的某个人可以查看开发服务器。这很容易通过将端口或完整地址:端口字符串附加到python manage.py runserver命令来实现,如列表 1-13 中的各种示例所示。
# Run the development server on the local address and port 4345 (http://127.0.0.1:4345/)
python manage.py runserver 4345
# Run the dev server on the 96.126.104.88 address and port 80 (http://96.126.104.88/)
python manage.py runserver 96.126.104.88:80
# Run the dev server on the 192.168.0.2 address and port 8888 (http://192.168.0.2:8888/)
python manage.py runserver 192.168.0.2:8888
Listing 1-13.Start Django development web server on different address and port
[user@coffeehouse ∼]$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying sessions.0001_initial... OK
Listing 1-15.Run first Django migrate operation to create base database tables
from django.conf.urls import url
from django.contrib import admin
from django.views.generic import TemplateView
...
...
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$',TemplateView.as_view(template_name='homepage.html')),
]
Listing 1-16.Django url for home page to template
from django.shortcuts import render
def contact(request):
# Content from request or database extracted here
# and passed to the template for display
return render(request,'about/contact.html')
Listing 1-19.
Handler view method
in views.py
清单 1-19 中的contact方法——像views.py文件中的所有其他方法一样——是一个访问用户 web 请求的控制器方法。注意,联系方法的输入名为request。在这种类型的方法中,您可以使用request引用访问来自 web 请求的内容(例如,IP 地址、会话),或者访问来自数据库的信息,以便最终将这些信息传递给模板。如果您查看 contact 方法的最后一行,它以 Django helper 方法render的返回语句结束。在这种情况下,render 方法将控制权返回给about/contact.html模板。
[user@coffeehouse ∼]$ python manage.py createsuperuser
Username (leave blank to use 'admin'):
Email address: admin@coffeehouse.com
Password:
Password (again):
The password is too similar to the email address.
This password is too short. It must contain at least 8 characters.
This password is too common.
Password:
Password (again):
Superuser created successfully.
Listing 1-22.Create Django superuser for admin interface
Caution
如果您收到错误OperationalError - no such table: auth_user,这意味着 Django 项目的数据库仍然没有正确设置。您需要在项目的 BASE_DIR 中运行python manage.py migrate,这样 Django 就会创建必要的表来跟踪用户。更多细节请参见上一节“为 Django 项目设置数据库”。
# Project main urls.py
from coffeehouse.stores import views as stores_views
urlpatterns = patterns[
url(r'^stores/(?P<store_id>\d+)/',stores_views.detail),
]
Listing 2-5.Django url parameter definition for access in view methods in main urls.py file
from coffeehouse.stores import views as stores_views
urlpatterns = patterns[
url(r'^stores/',stores_views.detail),
url(r'^stores/(?P<store_id>\d+)/',stores_views.detail),
]
Listing 2-7.Django urls with optional parameters leveraging the same view method
from django.shortcuts import render
def detail(request,store_id='1'):
# Access store_id with 'store_id' variable
return render(request,'stores/detail.html')
Listing 2-8.Django view method
in views.py with default value
from django.shortcuts import render
def detail(request,store_id='1',location=None):
# Access store_id param with 'store_id' variable and location param with 'location' variable
# Extract 'hours' or 'map' value appended to url as
# ?hours=sunday&map=flash
hours = request.GET.get('hours', '')
map = request.GET.get('map', '')
# 'hours' has value 'sunday' or '' if hours not in url
# 'map' has value 'flash' or '' if map not in url
return render(request,'stores/detail.html')
Listing 2-9.Django view method extracting url parameters with request.GET
from django.conf.urls import url
from django.views.generic import TemplateView
from coffeehouse.about import views as about_views
from coffeehouse.stores import views as stores_views
urlpatterns = [
url(r'^$',TemplateView.as_view(template_name='homepage.html')),
url(r'^about/',about_views.index),
url(r'^about/contact/',about_views.contact),
url(r'^stores/',stores_views.index),
url(r'^stores/(?P<store_id>\d+)/',stores_views.detail,{'location':'headquarters'}),
]
Listing 2-10.Django urls.py with no url consolidation
from django.conf.urls import include, url
from django.views.generic import TemplateView
urlpatterns = [
url(r'^$',TemplateView.as_view(template_name='homepage.html')),
url(r'^about/',include('coffeehouse.about.urls')),
url(r'^stores/',include('coffeehouse.stores.urls'),{'location':'headquarters'}),
]
Listing 2-11.Django urls.py with include to consolidate urls
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$',views.index),
url(r'^contact/$',views.contact),
]
Listing 2-12.Django /coffeehouse/about/urls.py loaded via include
# Definition in urls.py
url(r'^$',TemplateView.as_view(template_name='homepage.html'),name="homepage")
# Definition in view method
from django.http import HttpResponsePermanentRedirect
from django.core.urlresolvers import reverse
def method(request):
....
return HttpResponsePermanentRedirect(reverse('homepage'))
# Definition in template
<a href="{% url 'homepage' %}">Back to home page</a>
Listing 2-14.Django url using name
这种情况的误差是NoReverseMatch at....Reverse for 'urlname' with arguments '()' and keyword arguments '{}' not found. X pattern(s) tried。如果您查看错误堆栈,您将能够指出这是在哪里发生的,并纠正它。请注意,这是一个致命的错误,如果它没有被隔离到发生它的视图或页面,它将在启动时停止整个应用。
正如您可以从这个简短的列表中证明的那样,request引用包含许多可操作的信息来满足业务逻辑(例如,您可以基于来自用户'的 IP 地址的地理位置信息来响应某些内容)。在django.http.request.HttpRequest和django.http.request.QueryDict属性和方法之间有超过 50 个request选项可用,所有这些都在书中相关的部分进行了解释——但是你可以在上一页的脚注链接中查看request选项的完整范围。
from django.shortcuts import render
def detail(request,store_id='1',location=None):
# Create fixed data structures to pass to template
# data could equally come from database queries
# web services or social APIs
STORE_NAME = 'Downtown'
store_address = {'street':'Main #385','city':'San Diego','state':'CA'}
store_amenities = ['WiFi','A/C']
store_menu = ((0,''),(1,'Drinks'),(2,'Food'))
values_for_template = {'store_name':STORE_NAME, 'store_address':store_address, 'store_amenities':store_amenities, 'store_menu':store_menu}
return render(request,'stores/detail.html', values_for_template)
Listing 2-21.Set up dictionary in Django view method for access in template
from django.shortcuts import render
# No method body(s) and only render() example provided for simplicity
# Returns content type text/plain, with default HTTP 200
return render(request,'stores/menu.csv', values_for_template, content_type='text/plain')
# Returns HTTP 404, wtih default text/html
# NOTE: Django has a built-in shortcut & template 404 response, described in the next section
return render(request,'custom/notfound.html',status=404)
# Returns HTTP 500, wtih default text/html
# NOTE: Django has a built-in shortcut & template 500 response, described in the next section
return render(request,'custom/internalerror.html',status=500)
# Returns content type application/json, with default HTTP 200
# NOTE: Django has a built-in shortcut JSON response, described in the next section
return render(request,'stores/menu.json', values_for_template, content_type='application/json')
Listing 2-23.HTTP Content-type and HTTP Status for Django view method responses
清单 2-23 中的第一个例子旨在返回一个包含纯文本内容的响应。注意render方法的content_type参数。清单 2-23 中的第二个和第三个例子将 HTTP Status代码设置为404和500。因为 HTTP Status 404代码用于未找到的资源,所以render方法为此使用了一个特殊的模板。类似地,因为 HTTP Status 500代码用于指示错误,render方法也为此使用了一个特殊的模板。
如果 Django 项目在settings.py中有DEBUG=True,HTTP 404(未找到)生成一个带有可用 URL 的页面——如图 2-1 所示——HTTP500(内部服务器错误)生成一个带有详细错误的页面——如图 2-2 所示。如果 Django 项目在settings.py中有DEBUG=False,HTTP 404(未找到)会生成一个单行 HTML 页面,显示为“Not Found. The requested URL <url_location> was not found on this server.”,HTTP 500(内部服务器错误)会生成一个单行 HTML 页面,显示为“A server error occurred. Please contact the administrator"。
from django.shortcuts import render
def page_not_found(request):
# Dict to pass to template, data could come from DB query
values_for_template = {}
return render(request,'404.html',values_for_template,status=404)
def server_error(request):
# Dict to pass to template, data could come from DB query
values_for_template = {}
return render(request,'500.html',values_for_template,status=500)
def bad_request(request):
# Dict to pass to template, data could come from DB query
values_for_template = {}
return render(request,'400.html',values_for_template,status=400)
def permission_denied(request):
# Dict to pass to template, data could come from DB query
values_for_template = {}
return render(request,'403.html',values_for_template,status=403)
Listing 2-25.Custom views to override built-in Django HTTP view methods
The HTTP 304 status code indicates a “Not Modified” response, so you can't send content in the response, it should always be empty.
正如您在表 2-5 的示例中所看到的,有多种快捷方式可以生成带有内嵌内容的不同 HTTP 状态响应,并且完全不需要使用模板。另外,你可以看到表 2-5 中的快捷键也可以接受content_type参数,如果内容是 HTML 以外的东西(即content_type=text/html)。
由于非 HTML 响应在 web 应用中变得非常普遍,您可以看到表 2-5 还显示了三个 Django 内置的响应快捷方式来输出非 HTML 内容。JsonResponse类用于将内联响应转换成 JavaScript 对象符号(JSON)。因为这个响应将有效负载转换为 JSON 数据结构,所以它会自动将内容类型设置为application/json。StreamingHttpResponse类的设计目的是在不需要将整个有效负载放在内存中的情况下传输响应,这种情况有助于大型有效负载响应。FileResponse类是StreamingHttpResponse的子类,旨在传输二进制数据(如 PDF 或图像文件)。
接下来,使用loader模块,我们使用get_template方法加载模板users_csvexport.html,该模板将具有类似 CSV 的结构,带有数据占位符。然后我们创建一个Context对象来保存将填充模板的数据,在本例中,它只是一个名为 u sers的变量。接下来,我们用context对象调用模板的render方法,以便用数据填充模板的数据占位符。最后,通过write方法将渲染后的模板写入response对象,并返回response对象。
class CoffeehouseMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization on start-up
def __call__(self, request):
# Logic executed on a request before the view (and other middleware) is called.
# get_response call triggers next phase
response = self.get_response(request)
# Logic executed on response after the view is called.
# Return response to finish middleware sequence
return response
def process_view(self, request, view_func, view_args, view_kwargs):
# Logic executed before a call to view
# Gives access to the view itself & arguments
def process_exception(self,request, exception):
# Logic executed if an exception/error occurs in the view
def process_template_response(self,request, response):
# Logic executed after the view is called,
# ONLY IF view response is TemplateResponse, see listing 2-22
Listing 2-28.Django middleware class structure
Server start-up
__init__ on django.middleware.security.SecurityMiddleware called
__init__ on django.contrib.sessions.middleware.SessionMiddleware called
__init__ on django.middleware.common.CommonMiddleware called
__init__ on django.middleware.csrf.CsrfViewMiddleware called
__init__ on django.contrib.auth.middleware.AuthenticationMiddleware called
__init__ on django.contrib.messages.middleware.MessageMiddleware called
__init__ on django.middleware.clickjacking.XframeOptionsMiddleware called
request for index() view method
__call__ on django.middleware.security.SecurityMiddleware called
process_view on django.middleware.security.SecurityMiddleware called (if declared)
__call__ on django.contrib.sessions.middleware.SessionMiddleware called
process_view on django.contrib.sessions.middleware.SessionMiddleware called (if declared)
__call__ on django.middleware.common.CommonMiddleware called
process_view on django.middleware.common.CommonMiddleware called (if declared)
__call__ on django.middleware.csrf.CsrfViewMiddleware called
process_view on django.middleware.csrf.CsrfViewMiddleware called (if declared)
__call__ on django.contrib.auth.middleware.AuthenticationMiddleware called
process_view on django.contrib.auth.middleware.AuthenticationMiddleware called (if declared)
__call__ on django.contrib.messages.middleware.MessageMiddleware called
process_view on django.contrib.messages.middleware.MessageMiddleware called (if declared)
__call__ on django.middleware.clickjacking.XframeOptionsMiddleware called
process_view on django.middleware.clickjacking.XframeOptionsMiddleware called (if declared)
start index() view method logic
if an exception occurs in index() view
process_exception on django.middleware.clickjacking.XframeOptionsMiddleware called (if declared)
process_exception on django.contrib.messages.middleware.MessageMiddleware called (if declared)
process_exception on django.contrib.auth.middleware.AuthenticationMiddleware called(if declared)
process_exception on django.middleware.csrf.CsrfViewMiddleware called (if declared)
process_exception on django.middleware.common.CommonMiddleware called (if declared)
process_exception on django.contrib.sessions.middleware.SessionMiddleware called (if declared)
process_exception on django.middleware.security.SecurityMiddleware called (if declared)
if index() view returns TemplateResponse
process_template_response on django.middleware.clickjacking.XframeOptionsMiddleware called (if declared)
process_template_response on django.contrib.messages.middleware.MessageMiddleware called (if declared)
process_template_response on django.contrib.auth.middleware.AuthenticationMiddleware called(if declared)
process_template_response on django.middleware.csrf.CsrfViewMiddleware called (if declared)
process_template_response on django.middleware.common.CommonMiddleware called (if declared)
process_template_response on django.contrib.sessions.middleware.SessionMiddleware called (if declared)
process_template_response on django.middleware.security.SecurityMiddleware called (if declared)
from django.contrib import messages
# Generic add_message method
messages.add_message(request, messages.DEBUG, 'The following SQL statements were executed: %s' % sqlqueries) # Debug messages ignored by default
messages.add_message(request, messages.INFO, 'All items on this page have free shipping.')
messages.add_message(request, messages.SUCCESS, 'Email sent successfully.')
messages.add_message(request, messages.WARNING, 'You will need to change your password in one week.')
messages.add_message(request, messages.ERROR, 'We could not process your request at this time.')
# Shortcut level methods
messages.debug(request, 'The following SQL statements were executed: %s' % sqlqueries) # Debug messages ignored by default
messages.info(request, 'All items on this page have free shipping.')
messages.success(request, 'Email sent successfully.')
messages.warning(request, 'You will need to change your password in one week.')
messages.error(request, 'We could not process your request at this time.')
Listing 2-29.Techniques to add Django flash messages
# Reduce threshold to DEBUG level in settings.py
from django.contrib.messages import constants as message_constants
MESSAGE_LEVEL = message_constants.DEBUG
# Increase threshold to WARNING level in setting.py
from django.contrib.messages import constants as message_constants
MESSAGE_LEVEL = message_constants.WARNING
Listing 2-30.Set default Django message level globally in settings.py
# Reduce threshold to DEBUG level per request
from django.contrib import messages
messages.set_level(request, messages.DEBUG)
# Increase threshold to WARNING level per request
from django.contrib import messages
messages.set_level(request, messages.WARNING)
Listing 2-31.Set default Django message level on a per request basis
from django.contrib import messages
# Generic add_message method, with fail_silently=True
messages.add_message(request, messages.INFO, 'All items on this page have free shipping.',fail_silently=True)
# Shortcut level method, with fail_silently=True
messages.info(request, 'All items on this page have free shipping.',fail_silently=True)
Listing 2-32.Use of the fail_silently=True attribute to ignore errors in case Django messages framework not installed
from django.contrib import messages
the_req_messages = messages.get_messages(request)
for msg in the_req_messages:
do_something_with_the_flash_message(msg)
Listing 2-34.Use of get_messages() method
to access Django flash messages
get方法用于处理视图上的 HTTP GET 请求,而post方法用于视图上的 HTTP POST 请求。这提供了一种更加模块化的方法来处理不同的 HTTP 操作,而标准视图方法需要显式地检查请求并创建条件来处理不同的 HTTP 操作。目前,不要担心 HTTP GET 和 HTTP POST 视图处理;Django 表格中对此进行了更详细的探讨,其中主题更具相关性。
虽然自动转义对于 HTML 输出来说是一个很好的安全预防措施,但这将我们带到假设 Django 总是生成 HTML 的第二点。如果 Django 模板必须输出 CSV、JSON 或 XML 内容,而像<、>、'(单引号)、"(双引号)和&这样的字符对内容消费者有特殊的意义,并且不能使用等效的视觉表示,那么会发生什么呢?在这种情况下,您还需要显式禁用 Django 强制的默认自动转义行为。
{% extends "base.html" %}
{% block title %}Coffeehouse home page{% endblock title %}
Listing 3-11.Django template with {% extends %} and {% block %} tag
注意清单 3-11 中第一个模板语句是如何{% extends "base.html" %}的。此外,注意清单 3-11 是如何用内容Coffeehouse home page定义{% block title %}标签的。清单 3-11 中的块覆盖了base.html模板中的标题栏。那么清单 3-11 中的 HTML <title>标签在哪里呢?没有,你也不需要。Django 自动重用来自base.html模板的布局,并在必要的地方替换块内容。
重用其他模板的 Django 模板倾向于使用有限的布局元素(例如 HTML 标签)和更多的 Django block语句来覆盖内容。这是有益的,因为正如我前面所概述的,它允许您一次建立整体布局,并在逐页的基础上定义内容。
Django 模板的可重用性可以多次出现。例如,您可以拥有模板 A、B 和 C,其中 B 要求重用 A,但是 C 要求重用 B 的一部分,唯一的区别是模板 C 需要使用{% extends "B" %}标签而不是{% extends "A"%}标签。但是由于模板 B 重用了 A,模板 C 也可以访问模板 A 中的相同元素。
Django 模板中的另一个可重用功能是将一个 Django 模板包含在另一个 Django 模板中。Django 通过{% include %}标签支持这个功能。
{% include %}标签需要一个模板参数——类似于{% extend %}标签——它可以是硬编码的字符串引用(如{% include "footer.html" %})、模板的相对路径(如{% include "../header.html" %}),或者是视图传递的变量,可以是视图中加载的字符串或模板对象。
声明为{% include %}标签一部分的模板知道声明它们的模板中的上下文变量。这意味着如果模板 A 使用了{% include "footer.html" %}标签,模板 A 的变量就会自动地为footer.html模板所用。
包含性地,可以使用with关键字显式地为{% include %}语句提供上下文变量。例如,{% include "footer.html" with year="2013" %}语句使得year变量可以在footer.html模板中访问。{% include %}标签还支持使用with符号传递多个变量的能力(例如{% include "footer.html" with year="2013" copyright="Creative Commons" %})。
最后,如果您希望声明为{% include %}标签一部分的模板能够限制声明它们的模板对上下文变量的访问,那么您可以使用only关键字。例如,如果模板 B 使用{% include "footer.html" with year="2013" only %}语句,那么footer.html模板只能访问year变量,而不考虑模板 B 中可用的变量。类似地,{% include "footer.html" only %}语句将footer.html模板限制为没有变量,而不考虑使用该语句的模板中可用的变量。
def onsale(request):
# Create fixed data structures to pass to template
# data could equally come from database queries
# web services or social APIs
sale_items = {'Monday':'Mocha 2x1','Tuesday':'Latte 2x1'}
return {'SALE_ITEMS': sale_items}
Listing 3-13.Custom Django context processor method
| 基于标准的字符 | 描述 |
| --- | --- |
| c | 输出 ISO 8601 格式(例如,2015-01-02T10:30:00.000123+02:00 或 2015-01-02t 10:30:00.000123,如果日期时间没有时区[即,简单日期时间]) |
| r | 输出 RFC 2822 格式的日期(例如,“星期四,2000 年 12 月 21 日 16:01:07 +0200”) |
| U | 输出自 Unix 纪元日期-1970 年 1 月 1 日 00:00:00 UTC 以来的秒数 |
| I(大写的 I) | 输出夏令时是否有效(例如,“1”或“0”) |
| 基于小时的字符 | 描述 |
| a | 输出'上午'或'下午' |
| A | 输出' AM '或' PM ' |
| f | 输出时间,12 小时制的小时和分钟,如果分钟为零,则不输出分钟(例如,“1”,“1:30”) |
| g | 输出不带前导零的小时、12 小时格式(例如“1”到“12”) |
| G | 输出不带前导零的 24 小时制小时格式(例如,0 到 23) |
| h | 输出小时,12 小时格式(例如,“01”到“12”) |
| H | 输出小时,24 小时格式(例如,“00”到“23”) |
| 我 | 输出分钟数(例如“00”到“59”) |
| P | 输出时间,12 小时制的小时、分钟和' a.m.'/'p.m . ',如果分钟为零,则不输出分钟,如果合适,则输出特殊情况字符串' midnight '和' noon '(例如,' a . m . 1 ',' 1:30 p.m . ',' midnight ',' noon ',' 12:30 p.m . ') |
| s | 输出秒,带前导零的两位数(例如,“00”到“59”) |
| u | 输出微秒数(例如,000000 到 999999) |
| 时区字符 | 描述 |
| e | 输出时区名称。可以是任何格式,或者可能返回空字符串,具体取决于日期时间的定义(例如,“”、“GMT”、“-500”、“美国/东部”) |
| O | 以小时为单位输出时区与格林威治时间的差异(例如,“+0200”) |
| T | 输出日期时间时区(例如' EST ',' MDT ') |
| Z | 以秒为单位输出时区偏移量。UTC 以西的时区偏移量始终为负,而 UTC 以东的时区偏移量始终为正(例如-43200 到 43200) |
| 日和周字符 | 描述 |
| D | 输出星期几,文本,3 个字母(例如,“Thu”,“Fri”) |
| L(小写 L) | 输出星期几,文本,长型(例如,“星期四”,“星期五”) |
| S | 输出一个月中某一天的英文序号后缀,2 个字符(例如,“st”、“nd”、“rd”或“th”) |
| w | 输出星期几,不带前导零的数字(例如,“0”代表星期日,“6”代表星期六) |
| z | 输出一年中的某一天(例如,0 到 365) |
| W | 输出一年中的周数,基于 ISO-8601 从星期一开始(例如,1,53) |
| o | 输出周编号年份,对应于 ISO-8601 周编号(W)(例如,“1999”) |
| 月份字符 | 描述 |
| b | 输出文本月份,3 个字母,小写(例如'一月','二月') |
| d | 输出一个月中的某一天,2 位数,带前导零(例如,“01”到“31”) |
| j | 输出不带前导零的一个月中的某一天(例如,“1”到“31”) |
| E | 输出月份,区域特定的替代表示,通常用于长日期表示(例如,波兰语区域的“listopada”,与“Listopad”相对) |
| F | 输出月份,文本,长型(例如,“一月”,“二月”) |
| m | 输出月份,带前导零的两位数(例如,“01”到“12”) |
| M | 输出月份,文本,3 个字母(例如'一月','二月') |
| n | 输出不带前导零的月份(例如“1”到“12”) |
| 普通 | 以美联社风格输出月份缩写(例如,“一月”、“二月”、“三月”、“五月”) |
| t | 输出给定月份的天数(例如,28 到 31) |
| 年份字符 | 描述 |
| L | 输出布尔值来判断是否是闰年(例如,真或假) |
| y | 输出年份,两位数(例如“99”) |
| Y | 输出年份,4 位数字(例如,“1999”) |
To literally output a date character in a string statement you can use the backslash character (e.g., {{variable|date:"jS \o\f F o"}} outputs 1st of January 2018, note the escaped \o\f)
add。-add过滤器增加数值。add过滤器可以添加两个变量或一个硬编码值和一个变量。例如,如果一个变量包含 5,那么过滤语句{{variable|add:"3"}}输出 8。如果值可以被强制转换成整数——就像上一个例子一样——add过滤器执行一次求和,如果不能,加法过滤器进行连接。对于包含“Hello”的字符串变量,过滤器语句{{variable|add:" World"}}输出 Hello World。对于包含['a ',' e ',' i']的列表变量和包含['o ',' u']的列表变量,过滤器语句{{variable|add:othervariable}}输出['a ',' e ',' I ',' o ',' u']。
length_is。-length_is过滤器用于评估值的长度是否是给定参数的大小。例如,如果一个变量包含latte,那么标签和过滤器语句{% if variable|length_is:"7" %}的计算结果为假。对于包含['a','e','i']的列表变量,标签和过滤器语句{% if variable|length_is:"3" %}评估为真。
make_list。-make_list过滤器从一个字符串或数字创建一个列表。例如,对于过滤器和标签语句{% with mycharlist="mocha"|make_list %},mycharlist 变量被赋予列表['m ',' o ',' c ',' h ',' a']。对于包含 724 个过滤器和标签语句{% with myintlist=variable|make_list %}的整数变量,myintlist 被分配列表['7 ',' 2 ',' 4']。
pluralize。-pluralize过滤器根据参数值返回复数后缀。例如,如果变量drink_count包含 1,过滤语句"You have {{drink_count}} drink{{pluralize|drink_count}}"输出"You have 1 drink",如果变量包含 2,相同的过滤语句输出"You have 2 drinks"。默认情况下,复数过滤器使用字母 s,这是最常见的复数后缀。但是,您可以使用附加参数指定不同的单数和复数后缀。例如,如果store_count为 1,则过滤语句"We have {{store_count}} business{{store_count|pluralize:"es"}}"输出"We have 1 business",如果store_count为 5,则输出"We have 5 businesses"。另一个例子是过滤语句"We have {{resp_number}} responsibilit{{resp_number|pluralize:"y","ies"}}",如果resp_number为 1,则输出"We have 1 responsibility",如果resp_number为 3,则输出"We have 3 responsibilities"。
slugify。-slugify过滤器将字符串转换成 ASCII 类型的字符串。这意味着字符串被转换为小写,删除非单词字符(字母数字和下划线),去除前导和尾随空格,以及将空格转换为连字符。例如,如果一个变量包含Welcome to the #1 Coffeehouse!,过滤语句{{variable|slugify}}输出welcome-to-the-1-coffeehouse。slugify过滤器通常用于规范 URL 和文件路径的字符串。
truncatechars。-truncatechars过滤器将字符串截断成给定数量的字符,并附加一个省略号序列。例如,如果变量包含Coffeehouse started as a small store,过滤语句{{variable|truncatechars:20}}输出Coffeehouse started...。
truncatechars_html。-truncatechars_html过滤器类似于truncatechars过滤器,但是能够识别 HTML 标签。这个过滤器是为 HTML 内容设计的,所以内容不会留下打开的 HTML 标签。例如,如果变量包含<b>Coffeehouse started as a small store</b>,过滤语句{{variable|truncachars_html:20}}输出<b>Coffeehouse start...</b>。
truncatewords。-truncatewords过滤器将字符串截断成给定数量的单词,并附加一个省略号序列。例如,如果一个变量包含Coffeehouse started as a small store,过滤语句{{variable|truncatwords:3}}输出Coffeehouse started as...。
truncatewords_html。-truncatewords_html过滤器类似于truncatewords过滤器,但是能够识别 HTML 标签。这个过滤器是为 HTML 内容设计的,所以内容不会留下打开的 HTML 标签。例如,如果变量包含<b>Coffeehouse started as a small store</b>,过滤语句{{variable|truncatwords_html:3}}输出<b>Coffeehouse started as...</b>。
force_escape。-force_escape过滤器从一个值中转义 HTML 字符,就像escape过滤器一样。不同的是force_escape被立即应用并返回一个新的转义字符串。当您需要多次转义或想要对转义结果应用其他过滤器时,这很有用。通常,你会使用escape滤镜。
linebreaks。-linebreaks过滤器用 HTML 标签替换纯文本换行符,单个换行符变成 HTML 换行符(<br/>),新的一行后面跟一个空行变成段落换行符(</p>)。例如,如果变量包含385 Main\nSan Diego, CA,过滤语句{{variable|linebreaks}}输出<p>385 Main<br/>San Diego, CA</p>。
linebreaksbr。-linebreaksbr过滤器将所有文本变量换行符转换成 HTML 换行符(<br/>)。例如,如果变量包含385 Main\nSan Diego, CA,过滤语句{{variable|linebreaksbr}}输出385 Main<br/>San Diego, CA。
striptags。-striptags过滤器从一个值中删除所有 HTML 标签。例如,如果一个变量包含<b>Coffee</b>house, the <i>best</i> <span>drinks</span>,过滤语句{{variable|striptags}}输出Coffeehouse, the best drinks。
Caution
striptags 过滤器使用非常基本的逻辑来剥离 HTML 标签。这意味着一段复杂的 HTML 有可能没有完全去掉标签。这就是为什么通过 striptags 过滤器传递的变量中的内容会被自动转义,并且永远不应该被标记为安全的。
# Variable definition
Coffeehouse started as a small store
# Template definition with wordwrap filter for every 12 characters
{{variable|wordwrap:12}}
# Output
Coffeehouse
started as a
small store
Listing 3-17.Django wordwrap filter
{% csrf_token %}。-{% csrf_token %}标签提供了一个字符串来防止跨站脚本。{% csrf_token %}标签仅用于 HTML <form>标签中。{% csrf_token %}标签的数据输出允许 Django 防止表单数据提交中的伪造请求(例如 HTTP POST 请求)。Django 表单一章中提供了关于{% csrf_token %}标签的更多细节。
比较操作
{% if %}同{% elif %} {% else %}。-{% if %}标签通常与{% elif %}和{% else %}标签结合使用,以评估多个条件。如果变量存在且不为空,或者如果变量持有一个True布尔值,则带有自变量变量的{% if %}标签评估为真。清单 3-18 展示了一系列{% if %}标签示例。
{% if drinks %} {% if drinks %} {% if drinks %}
We have drinks! We have drinks We have drinks
{% endif %} {% else %} {% elif drinks_on_sale %}
No drinks,sorry We have drinks on sale!
{% endif %} {% else %}
No drinks, sorry
{% endif %}
Listing 3-18.Django {% if %} tag with {% elif %} and {% else %}
Note
变量必须既存在又不为空才能计算为 true。仅存在且为空的变量的计算结果为 false。
{% if %}带and、or和not操作符。-{% if %}标签还支持and、or和not操作符来创建更复杂的条件。这些运算符允许您比较是否有多个变量不为空(如{% if drinks and drinks_on_sale %}),是否有一个或另一个变量不为空(如{% if drinks or drinks_on_sale %}),或者是否有一个变量为空(如{% if not drinks %})。
{% if %}带==、!=、<、>、<=和>=操作符。-{% if %}标签还支持等于、不等于、大于和小于运算符,以创建将变量与固定字符串或数字进行比较的条件。这些运算符允许您比较变量是否等于字符串或数字(如{% if drink == "mocha" %})、变量是否不等于变量或数字(如{% if store.id != 2 %})或变量是否大于或小于数字(如{% if store.id > 5 %})。
{% firstof %}。-{% firstof %}标记是一个简写标记,用于输出一组非空变量中的第一个变量。通过嵌套{% if %}标签可以实现{% firstof %}标签的相同功能。清单 3-19 展示了{ % firstof %}标签的一个示例,以及一组等价的嵌套{% if %}标签。
# Firstof example
{% firstof var1 var2 var3 %}
# Equivalent of firstof example
{% if var1 %}
{{var1|safe}}
{% elif var2 %}
{{var2|safe}}
{% elif var3 %}
{{var3|safe}}
{% endif %}
# Firstof example with a default value in case of no match (i.e, all variables are empty)
{% firstof var1 var2 var3 "All vars are empty" %}
# Assign the firstof result to another variable
{% firstof var1 var2 var3 as resultof %}
# resultof now contains result of firstof statement
Listing 3-19.Django {% firstof %} tag and equivalent {% if %}{% elif %}{% else %} tags
{% if <value> in %}和{% if <value> not in %}。-{% if %}标签还支持in和not in操作符来验证常量或变量的存在。例如,{% if "mocha" in drinks %}测试值"mocha"是否在drinks列表变量中,或者{% if 2 not in stores %}测试值2是否不在stores列表变量中。虽然in和not in操作符通常用于测试列表变量,但是也可以测试字符串中是否存在字符(例如{% if "m" in drink %})。此外,还可以比较一个变量的值是否出现在另一个变量中(如{% if order_drink in drinks %})。
{% if <value> is <value> %}和{% if <value> is not %}。-{% if %}标签还支持is和is not操作符进行对象级比较。例如,{% if target_drink is None %}测试值target_drink是否是一个None对象,或者{% if daily_special is not True %}测试值daily_special是否不是True。
{% if value|<filter> <condition> <value> %}。-{% if %}标签还支持直接对一个值应用过滤器,然后执行评估。例如,{% if target_drink_list|random == user_drink %}Congratulations your drink just got selected!{% endif %}在一个条件中直接使用random过滤器。
Parentheses are Not Allowed in If Tags: Operator Precedence Governs, Use Nested If Tags to Alter Precedence
比较运算符通常被聚合到单个语句中(例如,if...<...or...>...和...==...)并遵循一定的执行优先级。Django 遵循与 Python 相同的操作符优先级。 5 例如,语句{ % if drink in specials or drink == drink _ of _ the _ day % }的计算结果为((drink in specials)or(drink = = drink _ of _ the _ day)),其中首先运行内部括号运算,因为 in 和= =的优先级高于 or。
在 Python 中,可以通过在比较语句中使用显式括号来改变这种优先级。但是,Django 不支持在{% if %}标记中使用括号,您必须依赖操作符优先级或者使用嵌套的{% if %}语句来声明由显式括号产生的相同逻辑。
环
{% for %}和{% for %}同{% empty %}。-{% for %}标签遍历字典、列表、元组或字符串变量上的项目。{% for %}标签语法是{% for <reference> in <variable> %},其中reference在每次迭代中被赋予一个来自变量的新值。
根据变量的性质,可以有一个或多个引用(例如,对于列表一个引用{% for item in list %},对于字典两个引用{% for key,value in dict.items %})。此外,也可以用reversed关键字(例如{ % for item in list reversed %})反转循环顺序。{% for %}标签还支持{% empty %}标签,在循环中没有迭代的情况下(即主变量为空)会处理该标签。清单 3-20 展示了一个{% for %}和一个{% for %}和{% empty %}循环示例。
<ul> <ul>
{% for drink in drinks %} {% for storeid,store in stores %}
<li>{{ drink.name }}</li> <li><a href="/stores{{storeid}}/">{{store.name}}</a></li>
{% empty %} {% endfor %}
<li>No drinks, sorry</li> </ul>
{% endfor %}
</ul>
Listing 3-20.Django {% for %} tag and {% for %} with {% empty %}
{% for %}标签还生成一系列变量来管理迭代过程,例如迭代计数器、第一次迭代标志和最后一次迭代标志。当您想要在给定的迭代中创建行为(例如,格式化、附加处理)时,这些变量会很有用。表 3-4 说明了{% for %}标签变量。
{% ifchanged %}。-{% ifchanged %}标签是在{% for %}标签中使用的特殊逻辑标签。有时,了解循环引用是否从一个迭代更改到另一个迭代(例如,插入新标题)会很有帮助。{% ifchanged %}标签的参数是循环引用本身(如{% ifchanged drink %}{{drink}} section{% endifchanged %})或引用的一部分(如{% ifchanged store.name %}Available in {{store.name}}{% endifchanged %})。{% ifchanged %}标签也支持使用{% else %}标签(例如{% ifchanged drink %}{{drink.name}}{% else %}Same old {{drink.name}} as before{% endifchanged %})。
{% regroup %}。-{% regroup %}标签用于将字典变量的内容重新排列到不同的组中。{% regroup %}标签避免了在{% for %}标签内创建复杂条件来实现所需显示的需要。{% regroup %}标签预先安排了字典的内容,使得{% for %}标签的逻辑更加简单。清单 3-22 展示了一个使用{% regroup %}标签及其输出的字典。
# Dictionary definition
stores = [
{'name': 'Downtown', 'street': '385 Main Street', 'city': 'San Diego'},
{'name': 'Uptown', 'street': '231 Highland Avenue', 'city': 'San Diego'},
{'name': 'Midtown', 'street': '85 Balboa Street', 'city': 'San Diego'},
{'name': 'Downtown', 'street': '639 Spring Street', 'city': 'Los Angeles'},
{'name': 'Midtown', 'street': '1407 Broadway Street', 'city': 'Los Angeles'},
{'name': 'Downton', 'street': '50 1st Street', 'city': 'San Francisco'},
]
# Template definition with regroup and for tags
{% regroup stores by city as city_list %}
<ul>
{% for city in city_list %}
<li>{{ city.grouper }}
<ul>
{% for item in city.list %}
<li>{{ item.name }}: {{ item.street }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
# Output
San Diego
Downtown : 385 Main Street
Uptown : 231 Highland Avenue
Midtown : 85 Balboa Street
Los Angeles
Downtown: 639 Spring Street
Midtown: 1407 Broadway Street
San Francisco
Downtown: 50 1st Street
Listing 3-22.Django {% for %} tag and {% regroup %}
Tip
{% regroup %}标记也可以使用筛选器或属性来获得分组结果。例如, 3-22 中的商店列表可以方便地按城市预先排序,从而自动按城市进行分组,但是如果商店列表没有预先排序,您需要先按城市对列表进行排序,以避免分组不完整,您可以直接使用 dictsort 过滤器(例如,{ % regroup stores | dict sort:' city ' by city as city _ list % })。{% regroup %}标记的另一种可能性是使用嵌套属性,如果分组对象有嵌套属性(例如,如果 city 有一个 state 属性{ % regroup stores by city . state as state _ list % })。
Python 和过滤器操作
{% filter %}。-{% filter %}标签用于将 Django 过滤器应用于模板部分。如果你声明了{% filter lower %},那么lower过滤器将应用于这个标签和{% endfilter %}标签之间的所有变量——注意过滤器lower将所有内容转换成小写。也可以使用相同的管道技术将多个过滤器应用到同一个部分,以将过滤器链接到变量(例如{% filter lower|center:"50" %}...variables to convert to lower case and center...{% endfilter %})。
{% with %}。-{% with %}标签允许您在 Django 模板的上下文中定义变量。当您需要为 Django view 方法没有公开的值创建变量时,或者当一个变量与一个重量级操作相关联时,这很有用。也可以在同一个{% with %}标签中定义多个变量(例如{% with drinkwithtax=drink.cost*1.07 drinkpromo=drink.cost*0.85 %})。在到达{% endwith %}标签之前,{% with %}标签中定义的每个变量都可供模板使用。
Python Logic Only Allowed Behind the Scenes in Custom Django Tags or Filters
Django 模板不允许包含内联 Python 逻辑。事实上,Django 模板允许内联 Python 逻辑的最接近的方式是通过{% with %}标记,这并不复杂。
比如一个 url 指向/drinks/index/,命名为drinks_main,可以用{% url %}引用这个 url(如<a href="{% url drinks_main %}"> Go to drinks home page </a>);如果一个 url 指向/stores/1/并且被命名为stores_detail,你可以使用{% url %}和一个参数来引用这个 url(例如<a href="{% url stores_detail store.id %}"> Go to {{store.name}} page </a>)。
{% url %}标签还支持as关键字,将结果定义为一个变量。这允许结果被多次使用,或者在声明了{% url %}标签以外的地方使用(例如{% url drink_detail drink.name as drink_on_the_day%}...后来在模板<a href="{{drink_of_the_day}}> Drink of the day </a>。第二章详细描述了命名 Django url 的过程,以便于管理和反向匹配。
from django import template
register = template.Library()
@register.filter()
def boldcoffee(value):
'''Returns input wrapped in HTML tags'''
return '<b>%s</b>' % value
Listing 3-23.Django custom filter with no arguments
默认情况下,筛选器接收与修饰方法相同的名称。所以在这种情况下,boldcoffee方法创建了一个名为boldcoffee的过滤器。方法输入value代表过滤器调用者的输入。在这种情况下,该方法只是返回包装在 HTML <b>标记中的输入值,其中 return 语句中使用的语法是标准的 Python 字符串格式操作。
要在 Django 模板中应用这个定制过滤器,可以使用语法{{byline|boldcoffee}}。byline变量作为value参数传递给过滤器方法,所以如果byline变量包含文本Open since 1965!,过滤器输出就是<b>Open since 1965!</b>。
Django 定制过滤器也支持包含参数,如清单 3-24 所示。
@register.filter()
def coffee(value,arg="muted"):
'''Returns input wrapped in HTML tags with a CSS class'''
'''Defaults to CSS class 'muted' from Bootstrap'''
return '<span class="%s">%s</span>' % (arg,value)
Listing 3-24.Django custom filter with arguments
这个默认设置使得来自包含 HTML <b>或<span>标签的清单 3-23 和 3-24 的定制过滤器创建逐字输出(也就是说,您不会看到以粗体显示的文本,而是逐字显示的<b>Open since 1965!</b>)。有时这是想要的行为,但有时不是。
Tip
要使 Django 模板在使用默认设置应用自定义过滤器后呈现 HTML 字符,您可以使用内置的safe过滤器(例如{{byline|coffee|safe}})或使用内置的{% autoescape %}标签(例如{% autoescape off %} {{byline|coffee}} {% endautoescape %}标签)包围过滤器声明。然而,Django filters 也可以将 filter is_safe 选项设置为 True,以使该过程自动化,并避免使用额外的过滤器或标记。
您可以将自定义过滤器中的is_safe选项设置为True,以确保自定义过滤器输出“按原样”呈现(例如,<b>标签以粗体显示),并且 HTML 元素不会被转义。
这种过滤器设计方法做了一个很大的假设:自定义过滤器总是用包含安全内容的变量来调用。如果byline变量包含文本Open since 1965 & serving > 1000 coffees day!会发生什么?变量现在包含了不安全的字符&和>,为什么它们是不安全的?因为它们在 HTML 中有特殊的含义,如果不转义,就有可能破坏页面布局(例如,>在这个上下文中可能意味着“不止”,但在 HTML 中它也意味着标签打开,浏览器可以将其解释为标记,从而破坏页面,因为它从未关闭)。
from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter(needs_autoescape=True)
def smartcoffee(value, autoescape=True):
'''Returns input wrapped in HTML tags'''
'''and also detects surrounding autoescape on filter (if any) and escapes '''
if autoescape:
value = escape(value)
result = '<b>%s</b>' % value
return mark_safe(result)
Listing 3-25.Django custom filter that detects autoescape setting
filter 方法的needs_autoescape参数和autoescape关键字参数允许过滤器知道在调用过滤器时转义是否有效。如果自动转义开启,那么value通过escape方法来转义所有字符。无论 value 的内容是否转义,过滤器都通过mark_safe方法传递最终结果,因此 HTML <b>标签在模板中被解释为粗体。
尽管 Jinja 使用相同的管道符号|将过滤器应用于变量,但 Jinja 过滤器在技术上分为过滤器和测试。在 Django 模板中,只有执行测试的过滤器(例如,divisibleby),但在 Jinja 中,这些类型构造被称为测试,并使用条件语法{% if variable is test %}而不是标准管道符号|。
Jinja 还支持一系列全局函数。例如,Jinja 提供了range函数,它的工作方式就像 Python 中在循环中有用的标准函数一样(例如{% for number in range(50 - coffeeshops|count) %})。此外,Jinja 还提供了全局函数lipsum来生成虚拟占位符内容、dict来生成字典、cycler来生成元素循环、joiner来连接节。
灵活的标签嵌套、条件和引用
Jinja 在嵌套标签方面非常灵活,尤其是与 Django 模板相比。例如,在 Jinja 中,你甚至可以有条件地应用{% extends %}标签(例如{% if user %}{% extends "base.html" %}{% else %}{% extends "signup_base.html" %}{% endif %})或者使用带有内嵌条件的变量引用名称(例如{% extends layout_template if layout_template is defined else 'master.html' %})——这在 Django 模板中是不可能的。
在 Jinja 中,您可以使用{% set %}标签来定义变量,使其在模板结束之前都具有有效的作用域。尽管 Jinja 也支持{% with %}标签——就像 Django 模板版本一样——{% with %}标签对于多个变量定义来说可能会变得很麻烦,因为它需要每次都用{% endwith %}来结束作用域。对于全局模板变量来说,{% set %}是一个很好的选择,因为您只需要初始定义,作用域传播到模板的末尾,而不必担心关闭作用域。
行语句
Jinja 在其所谓的行语句中支持逻辑语句的定义。默认情况下,line 语句前面有一个#符号,可以作为标记语法的替代。例如,{% for %}标记语句{% for item in items %}可以使用等价的行语句# for item in items,正如标记语句{% endfor %}可以使用等价的行语句# endfor.行语句,更重要的是,与使用需要{% %}语法的标记语句相比,给模板一种 Python 的感觉,可以使复杂的逻辑更容易破译。
{% if user %}{% extends "base.html" %}{% else %}{% extends "signup_base.html" %}{% endif %}
{% block title %}Coffeehouse home page{% endblock %}
Listing 4-6.Jinja template with {% extends %} and {% block %} tag
看看清单 4-6 如何使用包裹在{% if user %}语句周围的{% extends "base.html" %}。如果定义了user变量,Jinja 扩展了base.html模板;否则它扩展了signup_base.html模板。这种条件语法在 Django 模板中是不可能的。
此外,注意清单 4-6 是如何用内容Coffeehouse home page定义{% block title %}标签的。清单 4-6 中的块覆盖了base.html模板中的title块。那么清单 4-6 中的 HTML <title>标签在哪里呢?没有,你也不需要。Jinja 自动重用来自base.html或signup_base.html模板的布局,并在必要的地方替换块的内容。
重用其他模板的 Jinja 模板倾向于使用有限的布局元素(例如 HTML 标签)和更多的 Jinja 块语句来覆盖内容。这是有益的,因为正如我前面所概述的,它允许您一次建立整体布局,并在逐页的基础上定义内容。
Jinja 模板的可重用性可以多次出现。例如,你可以有模板 A、B 和 C,其中 B 需要重用 A,但是 C 需要重用 B 的一部分,唯一的区别是模板 C 需要使用{% extends "B" %}标签而不是{% extends "A"%}标签。但是由于模板 B 重用了 A,模板 C 也可以访问模板 A 中的相同元素。
Jinja 模板支持的另一个可重用功能是将一个 Jinja 模板包含在另一个 Jinja 模板中。Jinja 通过{% include %}标签支持这一功能。
默认情况下,{% include %}标签需要一个模板的名称。例如,{% include "footer.html" %}在声明模板的位置插入footer.html模板的内容。{% include %}标签也让底层模板知道变量。这意味着footer.html模板可以有变量定义(例如{{year}},如果调用模板有这些变量定义,{% include %}标签会自动替换这些值。
此外,还可以提供一个模板列表作为后备机制。例如,{% include ['special_sidebar.html', 'sidebar.html'] ignore missing %}告诉 Jinja 首先尝试定位special_sidebar.html模板,如果没有找到就尝试定位sidebar.html模板;如果两个模板都没有找到,最后一个参数ignore missing告诉 Jinja 不要渲染任何东西。注意ignore missing参数也可以用在单独的语句中(例如{% include "footer.html" ignore missing %},以及列表)。此外,如果没有使用ignore missing语句,并且 Jinja 找不到在{% include %}中声明的匹配模板,Jinja 会引发一个异常。
{% if %}同{% elif %} {% else %}。-{% if %}语句是评估条件的主要构件。{% if %}语句通常与{% elif %}和{% else %}语句结合使用,以评估多个条件。如果变量存在且不为空,或者变量包含真布尔值,则带有自变量变量的{% if %}语句计算为真。清单 4-14 展示了一系列{% if %}语句示例。
{% if drinks %} {% if drinks %} {% if drinks %}
We have drinks! We have drinks We have drinks
{% endif %} {% else %} {% elif drinks_on_sale %}
No drinks,sorry We have drinks on sale!
{% endif %} {% else %}
No drinks, sorry
{% endif %}
Listing 4-14.Jinja {% if %} statement with {% elif %} and {% else %}
Note
变量必须既存在又不为空才能匹配条件。只存在且为空的变量不符合条件。
{% if %}带and、or和not操作符。-{% if %}语句还支持and、or和not操作符来创建更复杂的条件。这些运算符允许您比较多个变量是否为非空(如{% if drinks and drinks_on_sale %})、一个或另一个变量是否为非空(如{% if drinks or drinks_on_sale %})或一个变量是否为空(如{% if not drinks %})。
{% if %}带==、!=、<、>、<=和>=操作符。-{% if %}语句还支持等于、不等于、大于和小于运算符,以创建将变量与固定字符串或数字进行比较的条件。这些运算符允许您比较变量是否等于字符串或数字(如{% if drink == "mocha" %})、变量是否不等于变量或数字(如{% if store_id != 2 %})或变量是否大于或小于数字(如{% if store_id > 5 %})。
{% if <value> in %}和{% if <value> not in %}。-{% if %}语句还支持in和not in操作符来验证常量或变量的存在。例如,{% if "mocha" in drinks %}测试值"mocha"是否在drinks列表变量中,或者{% if 2 not in stores %}测试值2是否不在stores列表变量中。虽然in和not in操作符通常用于测试列表变量,但是也可以测试字符串中是否存在字符(例如{% if "m" in drink %})。此外,还可以比较一个变量的值是否出现在另一个变量中(如{% if order_drink in drinks %})。
环
{% for %}和{% for %}同{% else %}。-{% for %}语句迭代字典、列表、元组或字符串变量上的项目。{% for %}语句语法是{% for <reference> in <variable> %},其中<reference>在每次迭代中被赋予一个来自<variable>的新值。根据变量的性质,可以有一个或多个引用(例如,对于列表有一个引用,对于字典有两个引用)。{% for %}语句还支持{% else %}语句,该语句在循环中没有迭代的情况下被处理(即主变量为空)。清单 4-15 展示了一个{% for %}和一个{% for %}和{% else %}循环示例。
<ul> <ul>
{% for drink in drinks %} {% for storeid,store in stores %}
<li>{{ drink.name }}</li> <li><a href="/stores/{{storeid}}/">{{store.name}}</a></li>
{% else %} {% endfor %}
<li>No drinks, sorry</li> </ul>
{% endfor %}
</ul>
Listing 4-15.Jinja {% for %} statement and {% for %} with {% else %}
{% for %}语句还生成一系列变量来管理迭代过程,例如迭代计数器、第一次迭代标志和最后一次迭代标志。表 4-1 说明了{% for %}语句变量。
{% for %}同if。-{% for %}语句还支持包含if语句来过滤字典、列表、元组或字符串变量的迭代。通过这种方式,您可以将迭代限制为通过或未通过某个标准的元素。带有if子句的{% for %}语句语法是{% for <reference> in <variable> if <test_for_reference>%}(例如{% for drink in drinks if drink not in ['Cappuccino'] %})
{% for %}带recursive关键字。-{% for %}语句还支持嵌套字典、列表、元组或字符串变量的递归。您可以使用递归在每个嵌套结构上重用相同的布局,而不是创建多个嵌套的{% for %}语句。清单 4-16 展示了 Jinja 中一个递归循环的例子。
# Dictionary definition
coffees={
'espresso':
{'nothing else':'Espresso',
'water': 'Americano',
'steamed milk': {'more steamed milk than milk foam': 'Latte',
'chocolate syrup': {'Whipped cream': 'Mocha'}
},
'more milk foam than steamed milk': 'Capuccino'
}
}
# Template definition with for and recursive
{% for ingredient,result in coffees.iteritems() recursive %}
<li>{{ ingredient }}
{% if result is mapping %}
<ul>{{ loop(result.iteritems()) }}</ul>
{% else %}
YOU GET: {{ result }}
{% endif %}</li>
{% endfor %}
# Output
espresso
water YOU GET: Americano
steamed milk
more steamed milk than milk foam YOU GET: Latte
chocolate syrup
Whipped cream YOU GET: Mocha
more milk foam than steamed milk YOU GET: Capuccino
nothing else YOU GET: Espresso
Listing 4-16.Jinja {% for %} statement with recursive keyword
{% set slash_joiner = joiner("/ ") %}
User: {% if username %} {{ slash_joiner() }}
{{username}}
{% endif %}
{% if alias %} {{ slash_joiner() }}
{{alias}}
{% endif %}
{% if nickname %} {{ slash_joiner() }}
{{nickname}}
{% endif %}
# Output
# If all variables are defined
User: username / alias / nickname
# If only nickname is defined
User: nickname
# If only username and alias is defined
User: username / alias
# Etc, the joiner function avoids any unnecessary preceding slash because it doesn't print anything the first time its called
Listing 4-18.Jinja joiner function
Python 和过滤器操作
{% set %}。-{% set %}语句允许您在 Jinja 模板的上下文中定义变量。当您需要为 Django view 方法没有公开的值创建变量时,或者当一个变量与一个重量级操作相关联时,这很有用。下面是这个声明{% set drinkwithtax=drink.cost*1.07 %}的一个示例声明。在{% set %}语句中定义的变量的范围是从它的声明开始直到模板结束。{% set %}语句也可以定义内容块。例如,语句{% set advertisement %}<div class'banner'><img src=.....></div>{% endset %}创建变量advertisement,其内容包含在{% set %}和{% endset %}之间,以后可以在模板的其他部分重用(例如{{advertisement}})。内置的{% macro %}语句——在模板结构一节中描述——为内容块提供了更高级的重用功能。
{% do %}(该语句需要启用内置的 jinja2.ext.do 扩展;有关更多详细信息,请参见启用 Jinja 扩展一节)。-{% do %}语句是一个表达式求值器,其工作方式类似于{{ }}变量语法,只是它不产生输出。例如,要增加一个变量的值或添加一个新元素而不产生任何输出,可以使用{% do %}语句(例如,{ % do itemlist.append('Forgot to add this other item') %})。
{% with %}。-{% with %}语句类似于{% set %}语句;唯一的区别是{% with %}语句用{% endwith %}语句限制了变量的范围(例如,{% with myvar=1 %}...{% endwith %}中声明的任何元素...可以访问myvar变量)。在{% with %}和{% endwith %}语句中声明{% set %}语句来限制变量的范围也是有效的(例如{% with %}{% set myvar=1 %}...{% endwith %})。
Note
{% with %}需要启用内置的 jinja2.ext.with_ extension。请参阅本章倒数第二节,了解如何启用 Jinja 扩展。
{% include %}。-{% include %}语句用于将一个 Jinja 模板嵌入到另一个 Jinja 模板中。注意,默认情况下,{% include %}语句可以访问当前的模板实例值(即它的上下文)。如果你想禁止访问模板的上下文,你可以使用{% import %}语句或者将关键字without context传递到{% include %}语句的末尾(例如{% from 'footer.html' without context %})。此外,{% include %}语句还接受ignore missing关键字,它告诉 Jinja 如果要包含的模板不存在,就忽略该语句。请参阅上一节关于创建可重用 Jinja 模板的内容,以获得该语句的详细示例。
reject。-reject过滤器移除通过特定测试的元素--参见本章中标有(测试)的小节中的项目符号,了解可接受的值。例如,对于一个包含[1,2,3,4,5]的列表变量,带有这个过滤器{% for var in variable|reject("odd") %}{{var}}{% endfor %}的循环语句——其中odd是 Jinja 测试——拒绝奇数元素,因此它的输出是2和4。
select。-select过滤器选择通过特定测试的元素-参见本章中标有(测试)的章节中的项目符号,了解可接受的值。例如,对于一个包含[1,2,3,4,5]的列表变量,带有这个过滤器{% for var in variable|select("odd") %}{{var}}{% endfor %}的循环语句——其中odd是 Jinja 测试——选择奇数的元素,因此它的输出是1、3和5。
slice。-slice过滤器返回列表的一部分。例如,对于包含["Capuccino"]的变量,过滤语句{% for var in variable|slice(4) %}{{var}}{% endfor %}输出['C', 'a', 'p'],['u', 'c'],['c', 'i'], ['n', 'o']。可以使用fill_with作为第二个参数——默认为 None——所以所有段都包含相同数量的元素,并填充给定值。比如{% for var in variable|slice(4,'FILLER') %}{{var}}{% endfor %}输出:['C', 'a', 'p'],['u', 'c','FILLER'],['c', 'i','FILLER'], ['n', 'o','FILLER']。
batch。-batch过滤器返回一批列表。例如,包含["Capuccino"]的变量过滤语句{% for var in variable|batch(4) %}{{var}}{% endfor %}输出['C', 'a', 'p', 'u'],['c', 'c', 'i', 'n'],['o']。可以使用fill_with作为第二个参数——默认为 None——所以所有段都包含相同数量的元素,并填充给定值。比如{% for var in variable|slice(4,'FILLER') %}{{var}}{% endfor %}输出:['C', 'a', 'p', 'u'],['c', 'c', 'i', 'n'],['o','FILLER','FILLER','FILLER']。
rejectattr。—rejectattr过滤器删除不包含属性的对象或某个属性未通过测试的对象——有关可接受的值,请参阅本章中标有(测试)一节中的项目符号。例如,{% for ch in coffeehouses|rejectattr("closedon") %}为没有closedon属性的coffeehouse对象生成一个循环,或者{% for u in users|rejectattr("email", "none") %}为没有email None 的user对象生成一个循环——注意第二个参数none代表测试。
selectattr。-selectattr过滤器选择包含某一属性的对象或某一属性通过测试的对象——有关可接受的值,请参阅本章中标有(测试)一节中的项目符号。例如,{% for u in users|selectattr("superuser") %}为具有superuser属性的user对象生成一个循环,或者{% for u in users|selectattr("email", "none") %}为具有email None 属性的user对象生成一个循环——注意第二个参数none代表测试。
safe。-safe过滤器将一个字符串标记为不需要进一步的 HTML 转义。当此过滤器与没有自动转义的环境一起使用时,它不起作用。
striptags。-striptags过滤器从一个值中删除所有 HTML 标签。例如,如果一个变量包含<b>Coffee</b>house, the <i>best</i> <span>drinks</span>,过滤语句{{variable|striptags}}输出Coffeehouse, the best drinks。
truncate。-truncate过滤器将字符串截断成给定的字符数——默认为 255 个字符——并附加一个省略号序列。例如,如果变量包含Coffeehouse started as a small store,过滤语句{{variable|truncate(20)}}输出Coffeehouse ...,一直到第 20 个字符,然后丢弃最后一个完整的单词,最后添加省略号。您可以添加true作为第二个参数,这样字符串就会被精确地切割(例如,{{variable|truncate(20,true)}}输出Coffeehouse start...包括省略号)。可以提供不同于传递第二个参数的省略号的符号(例如,{{variable|truncate(20,true,"!!!")}}将输出!!!而不是椭圆形)。最后,truncate过滤器接受第四个参数leeway来指定字符的字符串容差——默认为 5——以避免截断字符串(例如,这可以避免截断少于 5 个字符的单词)。
# Variable definition
Coffeehouse started as a small store
# Template definition with wordwrap filter for every 12 characters
{{variable|wordwrap(12)}}
# Output
Coffeehouse
started as a
small store
Listing 4-21.Jinja wordwrap filter
All extensions with the exception of jdj_tags.extensions.DjangoCompat are part of Jinja itself, so they require no additional installation. To install jdj_tags.extensions.DjangoCompat use pip install jinja2-django-tags.
接下来在清单 4-28 中你可以看到_now和parse方法。_now方法执行实际的当前日期或时间计算,并在settings.py中检查 Django 项目的时区配置——这个过程就像 Django 的{% now %}标记。parse方法表示执行定制{% now %}语句/标签的入口点,在这里它使用 Jinja 扩展 API 来分析输入,并根据{% now %}声明(例如{% now "F jS o" %}、{% now "F jS o" as today %})执行_now方法并返回结果。
# Import socket to read host name
import socket
# If the host name starts with 'live', DJANGO_HOST = "production"
if socket.gethostname().startswith('live'):
DJANGO_HOST = "production"
# Else if host name starts with 'test', set DJANGO_HOST = "test"
elif socket.gethostname().startswith('test'):
DJANGO_HOST = "testing"
else:
# If host doesn't match, assume it's a development server, set DJANGO_HOST = "development"
DJANGO_HOST = "development"
# Define general behavior variables for DJANGO_HOST and all others
if DJANGO_HOST == "production":
DEBUG = False
STATIC_URL = 'http://static.coffeehouse.com/'
else:
DEBUG = True
STATIC_URL = '/static/'
# Define DATABASES variable for DJANGO_HOST and all others
if DJANGO_HOST == "production":
# Use mysql for live host
DATABASES = {
'default': {
'NAME': 'housecoffee',
'ENGINE': 'django.db.backends.mysql',
'USER': 'coffee',
'PASSWORD': 'secretpass'
}
}
else:
# Use sqlite for non live host
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'coffee.sqlite3'),
}
}
# Define EMAIL_BACKEND variable for DJANGO_HOST
if DJANGO_HOST == "production":
# Output to SMTP server on DJANGO_HOST production
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
elif DJANGO_HOST == "testing":
# Nullify output on DJANGO_HOST test
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
else:
# Output to console for all others
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Define CACHES variable for DJANGO_HOST production and all other hosts
if DJANGO_HOST == "production":
# Set cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'TIMEOUT':'1800',
}
}
CACHE_MIDDLEWARE_SECONDS = 1800
else:
# No cache for all other hosts
pass
Listing 5-6.Django settings.py with control variable with host name to load different sets of variables
$ export DJANGO_SETTINGS_MODULE=coffeehouse.load_testing
$ python manage.py runserver
Validating models...
0 errors found
Django version 1.11, using settings 'coffeehouse.load_testing'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Listing 5-9.Override DJANGO_SETTINGS_MODULE to load application variables from a file called testing.py and not the default settings.py
当您将 Django 项目的DEBUG变量切换到True或者切换到不同的 web 服务器(例如 Apache、Nginx)时,您会惊讶地发现项目中不再出现任何静态资源。不要惊慌,这是故意的。当DEBUG=True使用 Django 的内置 web 服务器或者如果你切换到第三方 web 服务器时,设置 Django 来服务静态资源并不太困难。
Tip
您可以访问静态资源,使 Django 的内置 web 服务器在实际设置为 DEBUG=True 时提供静态资源,就像 DEBUG=False 一样。使用- insecure 标志运行 web 服务器:python manage . py run server–insecure。
Caution
虽然前面的解决方法是可用的,但我建议您不要使用它,以防标志名本身不安全不足以阻止您使用它。
Django 的内置 web 服务器(即python manage.py runserver)确实是一个快速启动和运行的便利工具,作为这种便利的一部分,它还在DEBUG=False时提供静态资源。
然而,允许同一个 web 服务器进程同时处理动态内容(Django web 页面)和静态资源(图像、CSS、JavaScript)确实是一种浪费。推荐的方法是完全使用一个单独的 web 服务器来服务静态资源,这就是为什么 Django 在切换DEBUG=True时打破了内置 web 服务器的便利性。
[user@coffeehouse ∼]$ python manage.py collectstatic
You have requested to collect static files at the destination
location as specified in your settings:
/www/STORE/coffeestatic
This will overwrite existing files!
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
yes
Copying '/www/STORE/website-static-default/sitemap.xml'
Copying '/www/STORE/website-static-default/robots.txt'
Copying '/www/STORE/website-static-default/favicon.ico'
....
....
....
Copying '/www/STORE/coffeehouse/about/static/css/custom.css'
732 static files copied to '/www/STORE/coffeestatic'.
Listing 5-14.Django collectstatic command to copy all static resources
# Python logging package
import logging
# Standard instance of a logger with __name__
stdlogger = logging.getLogger(__name__)
# Custom instance logging with explicit name
dbalogger = logging.getLogger('dba')
Listing 5-16.Define loggers in a Python module
| Django 电子邮件后端 | 配置 | 描述/注释 |
| --- | --- | --- |
| 用于开发(调试=真) |
| 控制台电子邮件 | EMAIL _ back end = ' django . core . mail . back ends . console . EMAIL back end ' | 将所有电子邮件输出发送到运行 Django 的控制台。 |
| 文件电子邮件 | ' EMAIL _ back end = ' django . core . mail . backends . FILE based . EMAIL back end ' EMAIL _ FILE _ PATH = '/tmp/django-EMAIL-dev ' | 将所有电子邮件输出发送到电子邮件文件路径中指定的平面文件。 |
| 内存电子邮件 | EMAIL _ back end = ' django . core . mail . backends . locmem . EMAIL back end ' | 将所有电子邮件输出发送到 django.core.mail.outbox 中的内存属性。 |
| 取消电子邮件 | EMAIL _ back end = ' django . core . mail . backends . dummy . EMAIL back end ' | 不处理所有电子邮件输出。 |
| Python 电子邮件服务器模拟器 | EMAIL _ back end = ' django . core . mail . backends . SMTP . EMAIL back end ' EMAIL _ HOST = 127 . 0 . 0 . 1 EMAIL _ PORT = 2525 还需要 Python 命令行电子邮件服务器:Python-m smtpd-n-c debuggings server localhost:2525 | 将所有电子邮件输出发送到通过命令行设置的 Python 电子邮件服务器。这类似于控制台电子邮件选项,因为 Python 电子邮件服务器将内容输出到控制台。 |
| 用于生产(调试=假) |
| SMTP 电子邮件服务器(标准) | EMAIL _ back end = ' django . core . mail . backends . SMTP . EMAIL back ends '1EMAIL _ HOST = 127 . 0 . 0 . 11EMAIL _ PORT = 252EMAIL _ HOST _ USER =2EMAIL _ HOST _ PASSWORD = | 将所有电子邮件输出发送到 SMTP 电子邮件服务器。 |
| SMTP 电子邮件服务器( * Secure-TLS) | EMAIL _ back end = ' django . core . mail . backends . SMTP . EMAIL back ends '1EMAIL _ HOST = 127 . 0 . 0 . 11EMAIL _ PORT = 5872EMAIL _ HOST _ USER =2EMAIL _ HOST _ PASSWORD =EMAIL _ USE _ TLS = True | 将所有电子邮件输出发送到安全的 SMTP (TLS)电子邮件服务器。 |
| SMTP 电子邮件服务器( * 安全-SSL) | EMAIL _ back end = ' django . core . mail . backends . SMTP . EMAIL back ends '1EMAIL _ HOST = 127 . 0 . 0 . 11EMAIL _ PORT = 4652EMAIL _ HOST _ USER =2EMAIL _ HOST _ PASSWORD =EMAIL _ USE _ SSL = True | 将所有电子邮件输出发送到安全的 SMTP (SSL)电子邮件服务器。 |
1 If the SMTP email server is running on a network or a different port than the default, adjust EMAIL_HOST and EMAIL_PORT accordingly. 2 In today’s email, spam-infested Internet, nearly all SMTP email servers require authentication to send email. If your SMTP server doesn’t require authentication you can omit EMAIL_HOST_USER and EMAIL_HOST_PASSWORD. * The terms SSL and TLS are often used interchangeably or in conjunction with each other (TLS/SSL). There are differences, though, in terms of their underlying protocol. From a Django setup prescriptive, you only need to ensure what type of secure email server you connect to, as they operate differently and on different ports.
Method arguments without a default value (e.g. subject,message) must always be provided. Method arguments with a default value (e.g. fail_silently=False, connection=None) are optional. Note
from django.core.mail.message import EmailMessage
# Build message
email = EmailMessage(subject='Coffeehouse specials', body='We would like to let you know about this week\'s specials....', from_email='stores@coffeehouse.com',
to=['ilovecoffee@hotmail.com', 'officemgr@startups.com'], bcc=['marketing@coffeehouse.com'], cc=['ceo@coffeehouse.com']
headers = {'Reply-To': 'support@coffeehouse.com'})
# Send message with built-in send() method
email.send()
Listing 5-25.Send basic email with EmailMessage class
from django.core import mail
connection = mail.get_connection()
# Manually open the connection
connection.open()
# Build message
email = EmailMessage(subject='Coffeehouse specials', body='We would like to let you know about this week\'s specials....', from_email='stores@coffeehouse.com',
to=['ilovecoffee@hotmail.com', 'officemgr@startups.com'], bcc=['marketing@coffeehouse.com'], cc=['ceo@coffeehouse.com']
headers = {'Reply-To': 'support@coffeehouse.com'})
# Build message
email2 = EmailMessage(subject='Coffeehouse coupons', body='New coupons for our best customers....', from_email='stores@coffeehouse.com',
to=['officemgr@startups.com','food@momandpopshop.com'], bcc=['marketing@coffeehouse.com'], cc=['ceo@coffeehouse.com']
headers = {'Reply-To': 'support@coffeehouse.com'})
# Send the two emails in a single call
connection.send_messages([email, email2])
# The connection was already open so send_messages() doesn't close it.
# We need to manually close the connection.
connection.close()
Listing 5-26.Send multiple emails in a single connection with EmailMessage class
另一个常见的电子邮件场景是发送 HTML 电子邮件。Django 为此提供了EmailMultiAlternatives类,它是EmailMessage类的子类。作为一个子类,这意味着你可以利用与EmailMessage相同的功能(例如,抄送、密件抄送),但你不需要做很多工作,因为子类EmailMultiAlternatives是专门为处理多种类型的消息而设计的。清单 5-27 展示了如何使用EmailMultiAlternatives类。
from django.core.mail import EmailMultiAlternatives
subject, from_email, to = 'Important support message', 'support@coffeehouse.com', 'ceo@coffeehouse.com'
text_content = 'This is an important message.'
html_content = '
This is an important message.
'
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=from_email, to=[to])
msg.attach_alternative(html_content, "text/html")
msg.send()
Listing 5-27.Send HTML (w/text) emails with EmailMultiAlternatives, a subclass of the EmailMessage class
清单 5-27 首先定义了所有的电子邮件字段,包括电子邮件的文本和 HTML 版本。请注意,拥有文本和 HTML 版本的电子邮件内容是常见的做法,因为不能保证最终用户会允许或能够阅读 HTML 电子邮件,所以提供文本版本作为备份。接下来,定义一个EmailMultiAlternatives类的实例;注意这些参数与那些EmailMessage类的参数是内联的。
接下来,在清单 5-27 中,您可以看到对attach_alternative方法的调用,它特定于EmailMultiAlternatives类。这个方法的第一个参数是 HTML 内容,第二个参数是对应于text/html的内容类型。最后,清单 5-27 调用send()方法——是EmailMessage类的一部分,但也是 to EmailMultiAlternatives的一部分,因为它是一个子类——来发送实际的电子邮件。
在可以保证所有终端用户都能够查看 HTML 电子邮件的受控环境(例如,公司电子邮件)中,只发送电子邮件的 HTML 版本并完全绕过文本版本可能是实用的。在这些情况下,您实际上可以直接使用EmailMesssage类,只需稍加修改。清单 5-28 展示了如何用EmailMessage类发送 HTML 电子邮件。
subject, from_email, to = 'Important support message', 'support@coffeehouse.com', 'ceo@coffeehouse.com'
html_content = '
This is an important message.
'
msg = EmailMessage(subject=subject, body=html_content, from_email=from_email, to=[to])
msg.content_subtype = "html" # Main content is now text/html
msg.send()
Listing 5-28.Send HTML emails
with EmailMessage class
清单 5-28 看起来像一个标准的EmailMessage过程定义;然而,第四行——msg.content_subtype——是清单 5-28 与众不同的地方。如果发送的 HTML 内容没有行设置msg.content_subtype,最终用户将收到 HTML 内容的一字不差的版本(即,没有呈现 HTML 标签)。这是因为默认情况下,EmailMessage类将内容类型指定为文本。为了切换一个EmailMessage实例的默认内容类型,在第四行调用将content_subtype设置为html。通过这一更改,电子邮件内容类型被设置为 HTML,最终用户能够查看呈现为 HTML 的内容。
Beware of Just Sending Html Email Versions to the Public
虽然发送 HTML 电子邮件版本比发送文本和 HTML 电子邮件版本更快,但如果您不能确定最终用户在哪里阅读他们的电子邮件,这可能会有问题。出于安全原因,某些用户会禁用查看 HTML 电子邮件的功能,某些电子邮件产品也不能或不太擅长呈现 HTML 电子邮件。因此,如果你只是发送一个 HTML 版本的电子邮件,可能会有一部分最终用户无法看到电子邮件的内容。
由于这个原因,如果你发送电子邮件给你无法控制其环境的最终用户(即电子邮件阅读器),最好发送文本和 HTML 电子邮件版本——如清单 5-27 所示——而不是发送清单 5-28 所示的 HTML 电子邮件版本。
发送电子邮件时的另一个常见做法是附加文件。清单 5-29 展示了如何将 PDF 附加到电子邮件中。
from django.core.mail.message import EmailMessage
# Build message
email = EmailMessage(subject='Coffeehouse sales report', body='Attached is sales report....', from_email='stores@coffeehouse.com',
to=['ceo@coffeehouse.com', 'marketing@coffeehouse.com']
headers = {'Reply-To': 'sales@coffeehouse.com'})
# Open PDF file
attachment = open('SalesReport.pdf', 'rb')
# Attach PDF file
email.attach('SalesReport.pdf',attachment.read(),'application/pdf')
# Send message with built-in send() method
email.send()
Listing 5-29.Send email with PDF attachment
with EmailMessage class
正如您在清单 5-29 中看到的,在创建了一个EmailMessage实例之后,您只需使用 Python 的标准open()方法打开 PDF 文件。接下来,使用来自EmailMessage的attach()方法,该方法有三个参数:文件名、文件内容和文件内容类型或 MIME 类型。最后,调用send()方法来发送邮件。
[user@coffeehouse ∼]$ python manage.py debugsqlshell
Python 2.7.3
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from coffeehouse.items.models import *
>>> Drink.objects.filter(item__price__lt=2).filter(caffeine__lt=100).count()
SELECT COUNT(*) AS "__count"
FROM "items_drink"
INNER JOIN "items_item" ON ("items_drink"."item_id" = "items_item"."id")
WHERE ("items_item"."price" < 2.0
AND "items_drink"."caffeine" < 100) [0.54ms]
Listing 5-31.Django debugsqlshell
sample expressions
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
class Command(BaseCommand):
help = 'Send test emails'
def handle(self, *args, **options):
for admin_name,email in settings.ADMINS:
try:
self.stdout.write(self.style.WARNING("About to send email to %s" % (email)))
# Logic to send email here
# Any other Python logic can also go here
self.stdout.write(self.style.SUCCESS('Successfully sent email to "%s"' % email))
raise Exception
except Exception:
raise CommandError('Failed to send test email')
Listing 5-33.Django management command class with no arguments
# forms.py in app named 'contact'
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea)
Listing 6-1.Django form class definition
# views.py in app named 'contact'
from django.shortcuts import render
from .forms import ContactForm
def contact(request):
form = ContactForm()
return render(request,'about/contact.html',{'form':form})
Listing 6-2.Django view method that uses a Django form
<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" name="name" type="text" /></td></tr>
<tr><th><label for="id_email">Your email:</label></th><td><input id="id_email" required name="email" type="email" /></td></tr>
<tr><th><label for="id_comment">Comment:</label></th><td><textarea cols="40" id="id_comment" required name="comment" rows="10">
</textarea></td></tr>
Listing 6-3.Django form instance rendered in template as HTML
在清单 6-3 中,您可以看到 Django 表单是如何被翻译成 HTML 标签的!注意 Django 表单如何为每个表单字段生成适当的 HTML <input>标签(例如,forms.EmailField(label='Your email')创建指定的<label>和一个 HTML 5 type="email"来执行电子邮件的客户端验证)。此外,请注意name字段缺少 HTML 5 required属性,这是因为清单 6-1 中的表单字段使用了required=False语句。
如果仔细观察清单 6-3 ,Django 表单实例的 HTML 输出只是内部 HTML 表格标签(即<tr>、<th>、<td>)。输出缺少一个 HTML <table>包装器标签和支持 HTML 表单标签(即,<form>标签和action属性,用于指示将表单发送到哪里,以及一个submit按钮)。这意味着您需要将缺少的 HTML 表单标签添加到模板中来创建一个工作的 web 表单——这个过程将在清单 6-4 中简要描述。
此外,如果您不想像清单 6-3 那样输出被 HTML 表格元素包围的整个表单域,有许多其他的语法变体可以输出精细的表单域并去掉 HTML 标签。在这种情况下,模板中的{{form.as_table}}引用是用来简化事情的,但是本章的下一节“在模板中设置 Django 表单的布局”将详细阐述在模板中输出 Django 表单的不同语法变化。
清单 6-4 中的下一个是包装在<table>标签中的{{form.as_table}}片段,它表示 Django 表单实例并输出清单 6-3 中所示的 HTML 输出。最后,还有<input type="submit">标记,它生成表单的提交按钮——当用户点击时提交表单——和结束的</form>标记。
Django 查看流程表单的方法(后期处理)
一旦在 Django 中有了一个功能性的 web 表单,就有必要创建一个视图方法来处理它。在上一节中,我提到了同一个 URL(通过扩展,view 方法)如何处理空白 HTML 表单的生成,以及带有数据的 HTML 表单的处理。清单 6-5 展示了清单 6-2 中视图方法的一个修改版本,它就是这样做的。
from django.shortcuts import render
from django.http import HttpResponseRedirect
from .forms import ContactForm
def contact(request):
if request.method == 'POST':
# POST, generate form with data from the request
form = ContactForm(request.POST)
# check if it's valid:
if form.is_valid():
# process data, insert into DB, generate email,etc
# redirect to a new URL:
return HttpResponseRedirect('/about/contact/thankyou')
else:
# GET, generate blank form
form = ContactForm()
return render(request,'about/contact.html',{'form':form})
Listing 6-5.Django view method that sends and processes Django form
CSRF 或跨站请求伪造是网络犯罪分子使用的一种技术,目的是迫使用户在 web 应用上执行不想要的操作。当用户与 web 表单交互时,他们会执行各种状态更改任务,从下订单(例如,产品、汇款)到更改数据(例如,姓名、电子邮件、地址)。大多数用户在与 web 表单交互时会有一种增强的安全感,因为他们看到了一个 HTTPS/SSL 安全符号,或者他们在与 web 表单交互之前使用了用户名/密码,所有这些都会让人觉得网络罪犯无法窃听、猜测或干扰他们的行为。
CSRF 攻击在很大程度上依赖于社交工程和 web 应用松散的应用安全性,因此攻击媒介是开放的,与其他安全措施无关(例如,HTTPS/SSL、强密码)。图 6-2 展示了一个 web 应用的 CSRF 漏洞场景。
图 6-2。
Web application with no CSRF protection
在用户“X”与网络应用“A”交互之后(例如,下订单、更新他的电子邮件),他简单地导航离开并转到其他站点。像大多数 web 应用一样,web 应用“A”将有效的用户会话保持几个小时或几天,以防用户回来决定做其他事情而不必再次登录。同时,网络罪犯也使用了站点“A ”,并且确切地知道其所有网络表单在哪里以及如何工作(例如,URL、诸如电子邮件、信用卡之类的输入参数)。
接下来,网络犯罪分子创建链接或页面,模仿在 web 应用“a”上提交 web 表单。例如,这可能是一个更改用户电子邮件地址以控制帐户或从用户帐户转移资金以窃取资金的表单。然后,网络罪犯通过电子邮件、社交媒体或其他带有诱人或可怕标题的网站,在互联网上植入这些链接或页面:“从网站‘A’获得 100 美元优惠券”、“紧急:由于安全风险,请更改您在网站‘A’的密码”。实际上,这些链接或页面并不像它们宣传的那样,而是在一次点击中模仿来自站点“A”的 web 表单提交(例如,更改用户的电子邮件或转移资金)。
现在让我们把注意力转向几小时或几天前访问过站点 A 的不知情用户 X。他瞥了一眼这些最后的广告,心想:“哇,我不能错过这个机会。”考虑到点击会造成什么危害,他点击了虚假广告,然后用户被发送到一个合法网站的“A”页面,或者点击“看起来”什么也没做。用户“X”对此毫不在意,继续执行其他任务。如果站点“A”没有带 CSRF 保护的 web 窗体,那么用户“X”只是无意中——在一次单击中——在站点“A”上执行了一个他没有意识到的操作。
如果希望在某些 web 表单上启用或禁用 CSRF,可以有选择地在处理 web 表单 POST 请求的视图方法上使用@csrf_exempt()和@csrf_protect装饰器。
要在所有 web 表单上启用 CSRF,并在某些 web 表单上禁用 CSRF 行为,请在MIDDLEWARE中保留django.middleware.csrf.CsrfViewMiddleware类,并使用@csrf_exempt()装饰器将不需要的视图方法装饰为 CSRF 验证,如清单 6-6 所示。
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def contact(request):
# Any POST-processing inside view method
# ignores if there is or isn't a CSRF token
Listing 6-6.Django view method decorated with @csrf_exempt() to bypass CSRF enforcement
要在所有 web 表单上禁用 CSRF,并在某些 web 表单上启用 CSRF 行为,请从MIDDLEWARE中移除django.middleware.csrf.CsrfViewMiddleware类,并用@csrf_protect()装饰器装饰您想要 CSRF 验证的视图方法,如清单 6-7 所示。
from django.views.decorators.csrf import csrf_protect
@csrf_protect
def contact(request):
# Any POST processing inside view method
# checks for the presence of a CSRF token
# even when CsrfViewMiddleware is removed
Listing 6-7.Django view method decorated with @csrf_protect() to enforce CSRF when CSRF is disabled at the project level
from django import forms
from django.shortcuts import render
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea)
def contact(request):
if request.method == 'POST':
# POST, generate form with data from the request
form = ContactForm(request.POST)
# Reference is now a bound instance with user data sent in POST
# process data, insert into DB, generate email, redirect to a new URL,etc
else:
# GET, generate blank form
form = ContactForm()
# Reference is now an unbound (empty) form
# Reference form instance (bound/unbound) is sent to template for rendering
return render(request,'about/contact.html',{'form':form})
Listing 6-8.Django form class with backing processing view method
def contact(request):
....
....
else:
# GET, generate blank form
form = ContactForm(initial={'email':'johndoe@coffeehouse.com','name':'John Doe'})
# Form is now initialized for first presentation to display these values
# Reference form instance (bound/unbound) is sent to template for rendering
return render(request,'about/contact.html',{'form':form})
Listing 6-9.Django form instance with initial argument declared in view method
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False,initial='Please provide your name')
email = forms.EmailField(label='Your email', initial='We need your email')
comment = forms.CharField(widget=forms.Textarea)
def contact(request):
....
....
else:
# GET, generate blank form
form = ContactForm()
# Form is now initialized for first presentation and is filled with initial values in form definition
# Reference form instance (bound/unbound) is sent to template for rendering
return render(request,'about/contact.html',{'form':form})
Listing 6-10.Django form fields with initial argument
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea)
def __init__(self, *args, **kwargs):
# Get 'initial' argument if any
initial_arguments = kwargs.get('initial', None)
updated_initial = {}
if initial_arguments:
# We have initial arguments, fetch 'user' placeholder variable if any
user = initial_arguments.get('user',None)
# Now update the form's initial values if user
if user:
updated_initial['name'] = getattr(user, 'first_name', None)
updated_initial['email'] = getattr(user, 'email', None)
# You can also initialize form fields with hardcoded values
# or perform complex DB logic here to then perform initialization
updated_initial['comment'] = 'Please provide a comment'
# Finally update the kwargs initial reference
kwargs.update(initial=updated_initial)
super(ContactForm, self).__init__(*args, **kwargs)
def contact(request):
....
....
else:
# GET, generate blank form
form = ContactForm(initial={'user':request.user,'otherstuff':'otherstuff'})
# Form is now initialized via the form's __init__ method
# Reference form instance (bound/unbound) is sent to template for rendering
return render(request,'about/contact.html',{'form':form})
Listing 6-11.Django form initialized with __init__ method
这也意味着语法 contact form(initial = { ' email ':' John Doe @ coffee house . com ',' name':'John Doe'})不等同于 contact form({ ' email ':' John Doe @ coffee house . com ',' name':'John Doe'})。第一种变体使用初始参数创建一个未绑定表单实例,而第二种变体通过直接传递值而不使用任何参数来创建一个绑定表单实例。
最后,use_required_attribute选项允许您设置 HTML 5 required属性的总体用途。默认情况下use_required_attribute=True,这意味着所有必需的表单域都用 HTML 5 required属性输出,确保浏览器总是提供这些表单域。通过用use_required_attribute=False初始化表单,可以禁用这个 HTML 5 客户端验证required属性。请注意,设置use_required_attribute=False不会影响 Django 服务器端对表单字段的验证(例如,如果表单字段是必需的,Django 服务器端验证仍然会捕获未提供的字段,而不管use_required_attribute选项如何)。
访问表单值:请求。发布和清理 _ 数据
一旦用户填写了 Django 表单,该表单将被发送回服务器进行处理,并对用户数据执行一个操作(例如,创建订单、发送电子邮件、将数据保存到数据库)——这一步在清单 6-8 的 POST 部分中有所描述。
按照设计,除非首先调用is_valid()方法,否则不可能访问表单实例的cleaned_data字典。如果你试图在调用is_valid()之前访问cleaned_data,你会得到错误AttributeError: 'form_reference' object has no attribute 'cleaned_data'。
from django.http import HttpResponseRedirect
def contact(request):
if request.method == 'POST':
# POST, generate form with data from the request
form = ContactForm(request.POST)
# Reference is now a bound instance with user data sent in POST
# Call is_valid() to validate data and create cleaned_data and errors dict
if form.is_valid():
# Form data is valid, you can now access validated values in the cleaned_data dict
# e.g. form.cleaned_data['email']
# process data, insert into DB, generate email
# Redirect to a new URL
return HttpResponseRedirect('/about/contact/thankyou')
else:
pass # Not needed
# is_valid() method created errors dict, so form reference now contains errors
# this form reference drops to the last return statement where errors
# can then be presented accessing form.errors in a template
else:
# GET, generate blank form
form = ContactForm()
# Reference is now an unbound (empty) form
# Reference form instance (bound/unbound) is sent to template for rendering
return render(request,'about/contact.html',{'form':form})
Listing 6-13.Django form is_valid() method
for form processing
from django import forms
import re
def validate_comment_word_count(value):
count = len(value.split())
if count < 30:
raise forms.ValidationError(('Please provide at least a 30 word message, %(count)s words is not descriptive enough'), params={'count': count},)
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea,validators=[validate_comment_word_count])
Listing 6-14.Django form field validators option with custom validator method for form processing
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea)
def clean_name(self):
# Get the field value from cleaned_data dict
value = self.cleaned_data['name']
# Check if the value is all upper case
if value.isupper():
# Value is all upper case, raise an error
raise forms.ValidationError("Please don't use all upper case for your name, use lower case",code='uppercase')
# Always return value
return value
def clean_email(self):
# Get the field value from cleaned_data dict
value = self.cleaned_data['email']
# Check if the value end in @hotmail.com
if value.endswith('@hotmail.com'):
# Value ends in @hotmail.com, raise an error
raise forms.ValidationError("Please don't use a hotmail email, we simply don't like it",code='hotmail')
# Always return value
return value
Listing 6-15.Django form field validation with clean_<field>() methods
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea)
def clean(self):
# Call clean() method to ensure base class validation
super(ContactForm, self).clean()
# Get the field values from cleaned_data dict
name = self.cleaned_data.get('name','')
email = self.cleaned_data.get('email','')
# Check if the name is part of the email
if name.lower() not in email:
# Name is not in email, raise an error
raise forms.ValidationError("Please provide an email that contains your name, or viceversa")
Listing 6-16.Django form field validation with clean() method
def clean(self):
# Call clean() method to ensure base class validation
super(ContactForm, self).clean()
# Get the field values from cleaned_data dict
name = self.cleaned_data.get('name','')
# Check if the name is part of the email
if name.lower() not in email:
# Name is not in email, raise an error
message = "Please provide an email that contains your name, or viceversa"
self.add_error('name', message)
self.add_error('email', forms.ValidationError(message))
self.add_error(None, message)
Listing 6-17.Django form field error assignment with add_error()
in clean() method
from django import forms
# Placed inside def clean_email(self):
raise forms.ValidationError("Please don't use a hotmail email, we simply don't like it",code='hotmail')
# Placed inside def clean(self):
raise forms.ValidationError(
forms.ValidationError("Please provide an email that matches your name, or viceversa",code='custom'),
forms.ValidationError("Please provide your professional email, %(value)s doesn't look professional ",code='required',params={'value':self.cleaned_data.get('email') })
Listing 6-18.Django form ValidationError
instance creation
由于 Internet 不受控制的特性——它有许多类型的设备、浏览器和用户体验级别——它对 web 表单必须处理的数据类型提出了各种各样的要求,以及如何净化数据以符合 web 表单的原始目的的各种各样的要求。
当您为 Django 应用创建 web 表单时,您依赖于由表单字段组成的 Django 表单类。每个 Django 表单字段都很重要,因为它规定了最终构成 web 表单整体行为的一小部分功能。
Django 表单域定义了两种类型的功能,表单域的 HTML 标记和它的服务器端验证工具。例如,Django 表单字段转换成实际的 HTML 表单标记(例如,<input>、<select>或<textarea>标签)、HTML 标记属性(例如,长度、字段是否可以留空,或者字段是否必须禁用),以及对表单字段的数据轻松执行服务器端验证的必要挂钩。
Django 表单字段出于需要定义了这两种类型的 web 表单功能。尽管随着 HTML5 等技术的发展,浏览器已经取得了巨大的进步,通过 HTML 标记本身(无需 JavaScript)提供了开箱即用的表单字段验证,但浏览器仍然完全控制着最终用户,只要有足够的知识,最终用户就可以绕过表单,将他想要的任何数据输入表单。因此,一旦用户提交了表单域数据,标准的做法是进一步检查它是否符合表单的规则,由于使用了 Django 表单域,这个过程变得非常容易。
正如你在表 6-2 中看到的,Django 表单域为几乎所有现存的 HTML 表单输入的生成提供了现成的支持,并为各种数据类型提供了必要的服务器端验证。例如,您可以使用CharField()表单字段类型来捕获标准文本,或者使用更专业的EmailField()表单字段类型来确保捕获的值是有效的电子邮件。正如您可以使用ChoiceField()生成一个带有预定义值的表单列表,或者使用DateField()强制表单值是一个有效日期。
小部件和表单域之间的关系
在表 6-2 中,你可以看到除了实际的 Django 表单域语法(例如forms.CharField()、forms.ImageField())之外,每个表单域都与一个默认的小部件相关联。Django 小部件在很大程度上没有被注意到,并且经常与表单字段本身的功能混合在一起(例如,如果您想要一个 HTML 文本输入<input type="text"..>,您可以使用forms.CharField())。但是,当您需要更改表单域生成的 HTML 或者表单域数据最初的处理方式时,您需要使用小部件。
更令人困惑的是,您在表单字段上指定的许多选项最终都被用作小部件的一部分。例如,表单字段forms.CharField(max_length=25)告诉 Django 在处理时将一个值限制为最多 25 个字符,但是这个相同的max_length选项被传递给forms.widgets.TextInput()小部件以生成 HTML <input type="text" maxlength="25"...>,从而通过 HTML maxlength="25"属性在浏览器上执行相同的规则。所以在这种情况下,您实际上可以通过一个表单域选项来更改 HTML 输出,甚至不需要了解小部件!
那么,您真的需要使用小部件来改变表单字段产生的 HTML 吗?答案是视情况而定。许多表单域选项被自动传递给幕后的小部件,实际上改变了生成的 HTML,但是不要搞错,它是一个负责生成 HTML 而不是表单域的小部件。如果表单域的选项不能实现期望的 HTML 输出,那么就有必要改变表单域的小部件来实现自定义的 HTML 输出。
如果您不想让用户为字段引入开放式值,您可以通过choices参数将字段值限制为一组预先确定的值。如果您想使用choices属性,您必须使用一个表单域数据类型,该数据类型被设计用来生成一个 HTML <select>列表,如forms.ChoiceField()、forms.MultipleChoiceField或forms.FielPathField()。choices参数不能用于像forms.CharField()这样为开放式输入设计的数据类型。
每个 Django 字段数据类型都有内置的错误消息。例如,当一个字段的数据类型是required并且用户没有添加任何值时,Django 会将错误消息This field is required分配给该字段,作为表单的errors字典的一部分。类似地,如果一个字段数据类型使用了max_length参数,并且用户提供的值超过了这个阈值,Django 就会创建错误消息Ensure this value has at most X characters (it has X)。
error_messages参数需要一个字典,其中每个键是消息错误代码,其值是自定义错误消息。例如,为了给required代码提供一个定制的消息,你可以使用语法forms.CharField(error_messages={"required":"Please, pretty please provide a comment"})。类似地,如果您希望一个表单字段违反它的max_length值,您可以通过max_length代码(例如error_messages={"max_length":"This value exceeds its max length value"})指定一个定制的错误消息。
错误代码通常直接映射到它们违反的规则(例如,如果一个forms.IntegerField违反了它的max_value,Django 使用max_value代码分配一个默认的错误消息,您可以使用这个代码覆盖它)。然而,有二十多个内置错误消息代码(例如'missing'、'contradiction'),其中一些并不太明显。例如,forms.ImageField可以生成错误消息'Upload a valid image. The file you uploaded was either not an image or a corrupted image.',它使用'invalid_image'错误代码,这意味着要覆盖这个默认的错误消息,您需要事先知道错误代码,以将其声明为error_messages的一部分。
当你在一个模板中输出一个表单域时,除了基本的 HTML 表单域标记(例如<input type="text">)之外,它几乎总是伴随着一个人类友好的描述符来指示一个域是做什么用的(例如Email: <input type="text">)。这个对人友好的描述符被称为标签,在 Django 中默认情况下,它被赋予与字段名相同的值。
要定制一个表单域的标签,你可以使用label参数。例如,要为名为 email 的字段提供更具描述性的标签,可以使用语法email = EmailField(label="Please provide your email")。默认情况下,表单上的所有标签都带有一个:符号,它的作用相当于一个后缀。您可以使用单个表单字段或表单实例本身的label_suffix参数进一步定制字段标签的输出。
在某些情况下,向表单域添加更明确的说明会很有帮助;对于这种情况,您可以使用help_text参数。根据您使用的模板布局,help_text值被添加到 HTML 表单字段标记的右侧。
例如,语法comment = CharField(help_text="Please be as specific as possible to receive a quick response")生成紧挨着comment输入字段的给定的html_text值(例如Please be as specific as possible to receive a quick response <input type="text">)。下一节“在模板中设置 Django 表单的布局”将更详细地介绍help_text和其他表单布局属性的使用。
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea)
Listing 6-19.Django form class definition
输出表单字段:form.as_table、form.as_p、form.as_ul 和按字段粒度
Django 表单提供了三个助手方法来简化所有表单字段的输出。语法form.as_table o 输出一个表单的字段来容纳一个 HTML <table>,如清单 6-20 所示。语法form.as_p输出带有 HTML <p>标签的表单字段,如清单 6-21 所示。Where as 语法form.as_ul输出一个表单的字段来容纳一个 HTML <ul>列表标签,如清单 6-22 所示。
Caution
如果您使用 form.as_table、form.as_p、form.as_ul,您必须声明开始/结束 HTML 标签、换行
通过用auto_id=False初始化表单,可以使form.as_table、form.as_p & form.as_ul输出不那么冗长——省略标签和 id 属性。此外,您还可以通过使用label_suffix变量初始化表单,来更改分隔标签名称的符号(默认为:)和另一个符号。也可以使用field_order选项改变输出场顺序。
清单 6-23 展示了一个标准的{% for %}循环,它确保你不会错过任何字段,并提供了比之前的form.as_table、form.as_p & form.as_ul方法更大的灵活性。
{% for field in form %}
<div class="row">
<div class="col-md-2">
{{ field.label_tag }}
{% if field.help_text %}
<sup>{{ field.help_text }}</sup>
{% endif %}
{{ field.errors }}
</div><div class="col-md-10 pull-left">
{{ field }}
</div>
</div>
{% endfor %}
Listing 6-23.Django form {% for %} loop over all fields
在清单 6-23 中,在form引用上创建了一个循环,以确保没有字段被遗漏。如果您想避免在某些表单布局中显示某个字段,那么我建议您使用{{field.as_hidden}} vs. {{field}},因为这样可以确保该字段仍然是表单的一部分,以便进行验证,并且只是对用户隐藏——关于这种情况的更多细节将在下一节高级表单处理和部分表单中提供。
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea)
field_order = ['email','comment','name']
Listing 6-24.Django form field_order option to enforce field order
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email')
comment = forms.CharField(widget=forms.Textarea)
error_css_class = 'error'
required_css_class = 'bold'
Listing 6-25.Django form error_css_class and required_css_class
fields to apply CSS formatting
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(required=False)
email = forms.EmailField(label='Your email', widget=forms.TextInput(attrs={'class' : 'myemailclass'}))
comment = forms.CharField(widget=forms.Textarea)
Listing 6-26.Django form with inline widget definition to add custom CSS class
清单 6-26 中介绍的方法是一种强大的技术,因为正如您可以声明 CSS class属性一样,您也可以声明任何其他表单域 HTML 属性。例如,如果你想声明定制的 HTML 属性——比如那些被像 jQuery 或 Bootstrap 这样的框架使用的属性——你可以很容易地使用同样的技术(例如,widget=forms.TextInput(attrs={'role' : 'dialog'})将输出<input role="dialog" type="text"...>)。
但是,现在需要注意的是,您已经知道在 Django 表单字段旁边输出任何 HTML 属性是多么容易。请注意,几乎所有 Django 表单字段数据类型都带有内置选项,可以转换成 HTML 属性。例如,forms.CharField(max_length=25)语句输出到<input type="text" maxlength="25"...>,这意味着表单字段max_length选项自动生成 HTML maxlength="25"属性。所以在开始使用清单 6-26 中的方法添加 HTML 属性时要小心,因为它们可能已经被内置的数据类型选项所支持。有关这些内置数据类型选项的更多细节,请参见上一节“Django 表单字段类型:小部件、选项和验证”。
输出表单域错误:表单。<field_name>。错误,表单.错误,表单. _ field _ 错误</field_name>
正如表单域可以用不同的方式输出一样,表单域错误也可以用不同的方式输出。在清单 6-23 第一部分的末尾,您可以看到我们如何使用{{field.errors}}语法来输出与特定字段相关的错误。然而,当以这种方式输出字段的errors值时,需要记住的一件重要事情是,输出是作为 HTML 格式的列表生成的:
<ul class="errorlist">
<li>Name is required.</li>
</ul>
请注意,如果您使用form.errors或form.non_field_errors来输出错误,默认情况下,错误引用——清单 6-28 中的{{error_messages}}和{{error}}——被包装为 HTML 格式的列表(例如<ul class="errorlist"><li>...</li></ul>),但是您可以向错误列表添加一个额外的 for 循环——如清单 6-27 所示——来创建一个定制的 HTML 错误布局。
接下来是两个类字段。template_name字段定义了自定义小部件的支持模板,该模板指向'about/placeholder.html'–注意,如果您省略了template_name字段,则使用父类模板(即对于forms.widgets.Input小部件,模板是django/forms/widgets/input.html)。input_type字段是forms.widgets.Input子类所必需的,用于分配 HTML 输入type属性,值可以包括:text、number、email、url、password或任何其他有效的 HTML 输入type值。
# about/placeholder.html
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />
# django/forms/widgets/attrs.html
{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value }}"{% endif %}{% endif %}{% endfor %}
Listing 6-31.Django custom form widget inherits behavior from forms.widgets.Input
正如您所看到的,除了存储在attrs键下的 HTML 输入属性之外,还有其他的widget字段键(例如name、is_hidden)可以在模板中使用,以呈现最终的输出。这也意味着您可以在get_context()方法中向context['widget']字典添加自定义数据键,将它们集成为最终模板布局的一部分(例如'react':{<react_data>}、'jquery':{<jquery_data>}).
实现部分表单的第二种方法是从表单类创建一个子类,并删除不需要的字段。这是一种更简单的方法,但是它需要创建一个 form 子类,这对于某些场景来说可能是多余的。清单 6-33 展示了如何子类化 Django 表单类并移除其父类的一些字段。
from coffeehouse.about.forms import ContactForm
class ContactCommentOnlyForm(ContactForm):
def __init__(self, *args, **kwargs):
super(ContactCommentOnlyForm, self).__init__(*args, **kwargs)
del self.fields['name']
del self.fields['email']
Listing 6-33.Django form subclass with removed parent fields
Django 提供了几个表单域来通过表单捕获文件——如表 6-2 中描述的forms.ImageField()和forms.ImageField()。然而,尽管这些表单域生成必要的 HTML 和验证逻辑来强制文件通过表单提交,但是在表单中处理文件还是有一些微妙之处。
第一个问题是用于传输文件的表单的 HTML <form>标签必须显式地将编码类型设置为enctype="multipart/form-data",此外还要使用method=POST。第二个问题是文件的内容被放在 Django request对象的特殊FILES字典键下。清单 6-36 展示了一个带有文件字段的表单,它对应的视图方法,以及它的模板布局。
# forms.py
from django import forms
class SharingForm(forms.Form):
# NOTE: forms.PhotoField requires Python PIL & other operating system libraries,
# so generic FileField is used instead
video = forms.FileField()
photo = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))
# views.py
def index(request):
if request.method == 'POST':
# POST, generate form with data from the request
form = SharingForm(request.POST,request.FILES)
# check if it's valid:
if form.is_valid():
# Process file data in request.FILES
# Process data, insert into DB, generate email,etc
# redirect to a new URL:
return HttpResponseRedirect('/about/contact/thankyou')
else:
# GET, generate blank form
form = SharingForm()
return render(request,'social/index.html',{'form':form})
# social/index.html
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<ul>
{{form.as_ul}}
</ul>
<input type="submit" value="Submit photo" class="btn btn-primary">
</form>
Listing 6-36.Django form with file fields, corresponding view method, and template layout
请注意 HTML <form>标签是如何声明所需属性的,另外请注意request.FILES引用是如何用于创建绑定表单的——以及标准的request.POST。清单 6-36 中剩余的表单结构与常规的非文件表单相同(例如,创建未绑定表单,调用is_valid())。
除了在settings.py中定义FILE_UPLOAD_HANDLERS之外,您还可以在settings.py中声明另外两个参数来影响文件上传处理程序的行为。FILE_UPLOAD_MAX_MEMORY_SIZE变量设置文件保存在内存中与作为临时文件处理的阈值大小,默认为2621440字节(或 2.5 MB)。并且FILE_UPLOAD_TEMP_DIR变量设置必须保存临时上传文件的目录,默认为None,意味着使用操作系统的临时文件目录(例如,在 Linux OS 上/tmp/)。
# views.py
from django.conf import settings
def save_uploaded_file_to_media_root(f):
with open('%s%s' % (settings.MEDIA_ROOT,f.name), 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)
def index(request):
if request.method == 'POST':
# POST, generate form with data from the request
form = SharingForm(request.POST,request.FILES)
# check if it's valid:
if form.is_valid():
for field in request.FILES.keys():
for formfile in request.FILES.getlist(field):
save_uploaded_file_to_media_root(formfile)
return HttpResponseRedirect('/about/contact/thankyou')
else:
# GET, generate blank form
form = SharingForm()
return render(request,'social/index.html',{'form':form})
Listing 6-37.Django form file processing with save procedure to MEDIA_ROOT
在 form is_valid()方法之后,您知道request.FILES字典中的每个键都是文件表单字段,因此创建了一个循环来获取每个文件字段(即video、photo)。接下来,使用request.FILES字典的getlist()方法,获得每个文件字段的文件实例列表(即InMemoryUploadedFile,TemporaryUploadedFile),并遍历每个元素以获得文件实例。最后,在每个文件实例上执行save_uploaded_file_to_media_root()方法。
from django.forms import BaseFormSet
class BaseDrinkFormSet(BaseFormSet):
def clean(self):
# Check errors dictionary first, if there are any error, no point in validating further
if any(self.errors):
return
name_size_tuples = []
for form in self.forms:
name_size = (form.cleaned_data['name'],form.cleaned_data['size'])
if name_size in name_size_tuples:
raise forms.ValidationError("Ups! You have multiple %s %s items in your order, keep one and increase the amount" % (dict(SIZES)[name_size[1]],dict(DRINKS)[int(name_size[0])]))
name_size_tuples.append(name_size)
Listing 6-41.Django custom formset with custom validation
from __future__ import unicode_literals
from django.utils.encoding import python_2_unicode_compatible
from django.db import models
@python_2_unicode_compatible
class Store(models.Model):
#id = models.AutoField(primary_key=True)# Added by default, not required explicitly
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
#objects = models.Manager()# Added by default, to required explicitly
def __str__(self):
return "%s (%s,%s)" % (self.name, self.city, self.state)
Listing 7-1.Django model class definition in models.py
如果你想了解更多关于所有 Django 模型默认添加的 id 字段,表 7-1 描述了 AutoField 数据类型,这是 id 字段的基础;本章后面的“Django 模型数据类型”一节描述了 id 字段使用的 primary_key 属性的用途;本章后面的“模型方法”一节中描述的 save()方法描述了 id 字段的实际应用。
[user@coffeehouse ∼]$ python manage.py makemigrations stores
Migrations for 'stores':
0001_initial.py:
- Create model Store
Listing 7-3.Django makemigrations command
to create migration file for changes made to models.py
def default_city():
return "San Diego"
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30,default=default_city)
state = models.CharField(max_length=2,default='CA')
Listing 7-6.Django model default option use
from datetime import date
from django.utils import timezone
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
date = models.DateField(default=date.today)
datetime = models.DateTimeField(default=timezone.now)
date_lastupdated = models.DateField(auto_now=True)
date_added = models.DateField(auto_now_add=True)
timestamp_lastupdated = models.DateTimeField(auto_now=True)
timestamp_added = models.DateTimeField(auto_now_add=True)
Listing 7-7.Django model default options for dates and times, as well as auto_now and auto_now_add use
ITEM_SIZES = (
('S','Small'),
('M','Medium'),
('L','Large'),
('P','Portion'),
)
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100,help_text="Ensure you provide some description of the ingredients")
size = models.CharField(choices=ITEM_SIZES,max_length=1)
calories = models.IntegerField(help_text="Calorie count should reflect <b>size</b> of the item")
Listing 7-9.Django model help_text option
ITEM_SIZES = (
('S','Small'),
('M','Medium'),
('L','Large'),
('P','Portion'),
)
# Import built-in validator
from django.core.validators import MinLengthValidator
# Create custom validator
from django.core.exceptions import ValidationError
def calorie_watcher(value):
if value > 5000:
raise ValidationError(
('Whoa! calories are %(value)s ? We try to serve healthy food, try something less than 5000!'),
params={'value': value},
)
if value < 0:
raise ValidationError(
('Strange calories are %(value)s ? This can\'t be, value must be greater than 0'),
params={'value': value},
)
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30,validators=[MinLengthValidator(5)])
description = models.CharField(max_length=100)
size = models.CharField(choices=ITEM_SIZES,max_length=1)
calories = models.IntegerField(validators=[calorie_watcher])
Listing 7-10.Django model field validators option with built-in and custom validator
# Import Django model class
from coffeehouse.stores.models import Store
# Create a model Store instance
store_corporate = Store(name='Corporate',address='624 Broadway',city='San Diego',state='CA',email='corporate@coffeehouse.com')
# Invoke the save() method to create/save the record
# No record id reference, so a create operation is made and the reference is updated with id
store_corporate.save()
# Change field on instance
store_corporate.city='625 Broadway'
# Invoke the save() method to update/save the record
# Record has id reference from prior save() call, so operation is update
store_corporate.save()
Listing 7-11.Django model use of the save() method
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
def save(self, *args, **kwargs):
# Do custom logic here (e.g. validation, logging, call third party service)
# Run default save() method
super(Store,self).save(*args, **kwargs)
Listing 7-12.Django model with custom save() method
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
# Create a model Store instance, that violates the max_length rule
store_corporate = Store(name='This is a very long name for the Corporate store that exceeds the 30 character limit',address='624 Broadway',city='San Diego',state='AZ',email='corporate@coffeehouse.com')
# No error yet
# You could call save() and let the database reject the instance...
# But you can also validate at the Django/Python level with the clean_fields() method
store_corporate.clean_fields()
Traceback (most recent call last):
raise ValidationError(errors)
ValidationError: {'name': [u'Ensure this value has at most 30 characters (it has 84).']}
Listing 7-13.Django model use of validation clean_fields() method
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
def clean(self):
# Don't allow 'San Diego' city entries that have state different than 'CA'
if self.city == 'San Diego' and self.state != 'CA':
raise ValidationError('Wait San Diego is CA!, are you sure there is another San Diego in %s ?' % self.state)
# Create a model Store instance, that violates city/state rule
store_corporate = Store(name='Corporate',address='624 Broadway',city='San Diego',state='AZ',email='corporate@coffeehouse.com')
# To enforce more complex rules call the clean() method implemented on a model
store_corporate.clean()
Traceback (most recent call last):
raise ValidationError('Wait San Diego is in CA!, are you sure there is another San Diego in %s ?' % (self.state))
ValidationError: [u'Wait San Diego is in CA!, are you sure there is another San Diego in AZ ?']
Listing 7-14.Django model use of validation clean() method
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
# Create a model Store instance
store_corporate = Store(name='Downtown',address='624 Broadway',city='San Diego',state='AZ',email='corporate@coffeehouse.com')
# Save instance
store_corporate.save()
# Create another instance to violate uniqueness of address field
store_uptown = Store(name='Uptown',address='624 Broadway', city='San Diego',state='CA')
# You could call save() and let the database reject the instance...
# But you can also validate at the Django/Python level with the validate_unique() method
store_uptown.validate_unique()
Traceback (most recent call last):
raise ValidationError(errors)
ValidationError: {'address': [u'Store with this Address already exists.']}
Listing 7-15.Django model use of validation clean_unique() method
with unique* fields
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
email = models.EmailField()
class Meta:
unique_together = ("name", "email")
# Create instance to show use of validate_unique() via Meta option
store_downtown_horton = Store(name='Downtown',address'Horton Plaza',city='San Diego',state='CA',email='downtown@coffeehouse.com')
# Save intance to DB
store_downtown_horton.save()
# Create additional instance that violated unique_together rule in Meta class
store_downtown_fv = Store(name='Downtown',address'Fashion Valley',city='San Diego',state='CA',email='downtown@coffeehouse.com')
# You could call save() and let the database reject the instance but lets use validate_unique
store_downtown_fv.validate_unique()
Traceback (most recent call last):
ValidationError: {'__all__': [u'Store with this Name and Email already exists.']}
Listing 7-16.Django model use of validation clean_unique() method
with Meta unique_together
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
def latitude_longitude(self):
# Call remote service to get latitude & longitude
latitude, longitude = geocoding_method(self.address, self.city, self.state)
return latitude, longitude
Listing 7-17.Django model with custom method
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
mgr = models.Manager()
Listing 7-18.Django default model manager renamed
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
class Meta:
ordering = ['-state']
Listing 7-19.Django model with Meta class and ordering option
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
class Meta:
indexes = [
models.Index(fields=['city','state']),
models.Index(fields=['city'],name='city_idx')
]
Listing 7-20.Django model with meta class and index option
Django 应用是所有 Django 模型不可或缺的一部分,因为它定义了模型的默认数据库表前缀、模型迁移的位置以及模型的默认引用标签。元类app_label属性允许您为 Django 模型分配一个显式的应用名称。Django meta app_label优先于默认的应用模型命名机制。本章中关于将 Django 模型放在models.py之外的最后一节包含了关于 meta app_label选项的更多细节。
继承元选项:抽象和代理
meta abstract选项允许 Django 模型作为没有后台数据库表的基类,但是作为其他 Django 模型类的基础。清单 7-21 展示了一组使用abstract选项的 Django 模型。
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class Meta:
abstract = True
class Drink(Item):
mililiters = models.IntegerField()
Listing 7-21.Django model abstract option
meta proxy选项也是为 Django 模型继承场景设计的。但是不像abstract选项中父类被声明为抽象的,子类继承它们的行为,proxy选项被设计成让子类访问父类,而不用让子类成为成熟的模型类。标有 meta proxy选项的类使它们能够对父类及其数据库表执行操作,而无需拥有自己的数据库表。
from django.db import models
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
Listing 7-22.One to many Django model relationship
from django.db import models
class Amenity(models.Model):
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
email = models.EmailField()
amenities = models.ManyToManyField(Amenity,blank=True)
Listing 7-23.Many to many Django model relationship
from django.db import models
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
calories = models.IntegerField()
price = models.FloatField()
class Drink(models.Model):
item = models.OneToOneField(Item,on_delete=models.CASCADE,primary_key=True)
caffeine = models.IntegerField()
Listing 7-24.One to one Django model relationship
from django.db import models
class Category(models.Model):
menu = models.ForeignKey('self')
class Person(models.Model):
relatives = models.ManyToManyField('self')
Listing 7-25.One to many Django model relationship with self-referencing model
from django.db import models
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
price = models.FloatField(blank=True,null=True)
breakfast = Menu.objects.get(name='Breakfast')
# Direct access
all_items_with_breakfast_menu = Item.objects.filter(menu=breakfast)
# Reverse access through instance
same_all_items_with_breakfast_menu = breakfast.item_set.all()
Listing 7-26.One to many Django model relationship with reverse relationship references
# Based on models from listing 7-26
# Direct access, Item records with price higher than 1
Items.objects.filter(price__gt=1)
# Reverse access query, Menu records with Item price higher than 1
Menu.objects.filter(item__price__gt=1)
Listing 7-27.One to many Django model relationship with reverse relationship queries
from django.db import transaction
# When ATOMIC_REQUESTS=True you can individually disable atomic requests
@transaction.non_atomic_requests
def index(request):
# Data operations with transactions commit/rollback individually
# Failure of one operation does not influence other
data_operation_1()
data_operation_2()
data_operation_3()
# When ATOMIC_REQUESTS=False you can individually enable atomic requests
@transaction.atomic
def detail(request):
# Start transaction.
# Failure of any operation, rollbacks other operations
data_operation_1()
data_operation_2()
data_operation_3()
# Commit transaction if all operation successful
Listing 7-28.Selectively activate and deactivate atomic requests with @non_atomic_requests and @atomic
from django.db import transaction
def login(request):
# With AUTO_COMMIT=True and ATOMIC_REQUEST=False
# Data operation runs in its own transaction due to AUTO_COMMIT=True
data_operation_standalone()
# Open new transaction with context manager
with transaction.atomic():
# Start transaction.
# Failure of any operation, rollbacks other operations
data_operation_1()
data_operation_2()
data_operation_3()
# Commit transaction if all operation successful
# Data operation runs in its own transaction due to AUTO_COMMIT=True
data_operation_standalone2()
Listing 7-29.Transactions with context managers
[user@coffeehouse ∼]$ python manage.py makemigrations --empty stores
Migrations for 'stores':
0002_auto_20180124_0507.py:
Listing 7-31.Create empty Django migration file to load initial data for Django model
from django.dispatch import receiver
@receiver(<signal_to_listen_for_from_django_core_signals>,sender=<model_class_to_listen_to>)
def method_with_logic_to_run_when_signal_is_emitted(sender, **kwargs):
# Logic when signal is emitted
# Access sender & kwargs to get info on model that emitted signal
Listing 7-37.Basic syntax to listen for Django signals
from django.dispatch import receiver
from django.db.models.signals import pre_save
from django.dispatch import receiver
import logging
stdlogger = logging.getLogger(__name__)
@receiver(pre_save, sender='items.Item')
def run_before_saving(sender, **kwargs):
stdlogger.info("Start pre_save Item in signals.py under items app")
stdlogger.info("sender %s" % (sender))
stdlogger.info("kwargs %s" % str(kwargs))
Listing 7-38.Listen for Django pre_save signal on Item model in signals.py
from django.apps import AppConfig
class ItemsConfig(AppConfig):
name = 'coffeehouse.items'
def ready(self):
import coffeehouse.items.signals
Listing 7-39.Django apps.py with custom ready() method
to load signals.py
Option 1) Declare apps.py class as part of INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
'coffeehouse.items.apps.ItemsConfig',
...
]
Option 2) Declare default_app_config inside the __init__ file of the app
#/coffeehouse/items/__init__.py
default_app_config = 'coffeehouse.items.apps.ItemsConfig'
Listing 7-40.Django configuration options
to load apps.py
from django.db import models
from coffeehouse.stores.signals import store_closed
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
...
def closing(self,employee):
store_closed.send(sender=self.__class__, employee=employee)
Listing 7-41.Django model emitting custom signal
这种技术要求你通过元类app_label选项为模型提供一个明确的应用名称,这样它们就被分配给一个应用。当 Django 检测到一个模型时,它声明 meta app_label选项,这将优先为一个模型指定应用名称。因此,即使在名为common的随机文件夹或名为items的应用中声明了一个模型,如果模型的元app_label值被设置为stores,那么该模型就会被分配给stores应用,而不管其位置如何。
**Hints are additional information provided to each router method to further determine which database to use each routing case, in addition to the other input parameters.
class DatabaseForDevOps(object):
def db_for_read(self, model, **hints):
if model._meta.app_label in ['auth','admin','sessions','contenttypes']:
return 'devops'
# Returning None is no opinion, defer to other routers or default database
return None
def db_for_write(self, model, **hints):
if model._meta.app_label in ['auth','admin','sessions','contenttypes']:
return 'devops'
# Returning None is no opinion, defer to other routers or default database
return None
def allow_relation(self, obj1, obj2, **hints):
# Allow relations between two models that are both Django core app models
if obj1._meta.app_label in ['auth','admin','sessions','contenttypes'] and obj2._meta.app_label in ['auth','admin','sessions','contenttypes']:
return True
# If neither object is in a Django core app model (defer to other routers or default database)
elif obj1._meta.app_label not in ['auth','admin','sessions','contenttypes'] or obj2._meta.app_label not in ['auth','admin','sessions','contenttypes']:
return None
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
if db == 'devops':
# Migrate Django core app models if current database is devops
if app_label in ['auth','admin','sessions','contenttypes']:
return True
else:
# Non Django core app models should not be migrated if database is devops
return False
# Other database should not migrate Django core app models
elif app_label in ['auth','admin','sessions','contenttypes']:
return False
# Otherwise no opinion, defer to other routers or default database
return None
Listing 7-45.Django database router to store core app models in devops database and all other models in default database
# Import Django model class
from coffeehouse.stores.models import Store
# Create a model Store instance
store_corporate = Store(name='Corporate',address='624 Broadway',state='CA',email='corporate@coffeehouse.com')
# Assign attribute value to instance with Python dotted notation
store_corporate.city = 'San Diego'
# Invoke the save() method to create the record
store_corporate.save() # If successful, record reference has id
store_corporate.id
Listing 8-1.Create a single record with model save() method
# Import Django model class
from coffeehouse.stores.models import Store
# Create a model Store instance which is saved automatically
store_corporate = Store.objects.create(name='Corporate',address='624 Broadway',city='San Diego',state='CA',email='corporate@coffeehouse') # If successful, record reference has id
store_corporate.id
Listing 8-2.Create a single record with create() method
# Import Django model class
from coffeehouse.stores.models import Store
# Get the store with the name "Downtown" or equivalent SQL: 'SELECT....WHERE name = "Downtown"
downtown_store = Store.objects.get(name="Downtown")
# Define uptown_email for the query
uptown_email = "uptown@coffeehouse.com"
# Get the store with the email value uptown_email or equivalent SQL: 'SELECT....WHERE email = "uptown@coffeehouse.com"'
uptown_email_store = Store.objects.get(email=uptown_email)
# Once the get() method runs, you can access an object's attributes
# either in logging statements, functions or templates
downtown_store.address
downtown_store.email
# Note you can access the object without attributes.
# If the Django model has a __str__/ method definition, the output is based on this method
# If the Django model has no __str__ method definition, the output is just <object>
print(uptown_email_store)
Listing 8-3.Read model record with get() method
# Import Django model class
from coffeehouse.items.models import Menu
# Get or create a menu instance with name="Breakfast"
menu_target, created = Menu.objects.get_or_create(name="Breakfast")
Listing 8-4.Read or create model record with get_or_create() method
from django.core.exceptions import ObjectDoesNotExist
from coffeehouse.items.models import Menu
try:
menu_target = Menu.objects.get(name="Dinner")
# If get() throws an error you need to handle it.
# You can use either the generic ObjectDoesNotExist or
# <model>.DoesNotExist which inherits from
# django.core.exceptions.ObjectDoesNotExist, so you can target multiple
# DoesNotExist exceptions
except Menu.DoesNotExist: # or the generic "except ObjectDoesNotExist:"
menu_target = Menu(name="Dinner")
menu_target.save()
Listing 8-5.Replicate get_or_create() method with explicit try/except block and save method
# Import Django model class
from coffeehouse.stores.models import Store
# Get the store with the name "Downtown" or equivalent SQL: 'SELECT....WHERE name = "Downtown"
downtown_store = Store.objects.get(name="Downtown")
# Update the name value
downtown_store.name = "Downtown (Madison)"
# Call save() with the update_fields arg and a list of record fields to update selectively
downtown_store.save(update_fields=['name'])
# Or you can call save() without any argument and all record fields are updated
downtown_store.save()
Listing 8-6.Update model record with the save() method
from coffeehouse.stores.models import Store
Store.objects.filter(id=1).update(name="Downtown (Madison)")
from coffeehouse.items.models import Item
from django.db.models import F
Item.objects.filter(id=3).update(stock=F('stock') +100)
Listing 8-7.Update model record with the update() method
如果不小心的话,update()方法可以跨多个记录更新一个字段。update()方法前面是 objects.filter()方法,它可以返回多条记录的查询结果。注意,在清单 8-7 中,查询使用 id 字段来定义查询,确保只有一条记录与查询匹配,因为 id 是表的主键。如果 objects.filter()中的查询定义使用不太严格的查找(例如,字符串),您可能会无意中更新比预期更多的记录。
# Import Django model class
from coffeehouse.stores.models import Store
values_to_update = {'email':'downtown@coffeehouse.com'}
# Update for record with name='Downtown' and city='San Diego' is found, otherwise create record
obj_store, created = Store.objects.update_or_create(
name='Downtown',city='San Diego', defaults=values_to_update)
Listing 8-8.Update or create model record with the update_or_create() method
from coffeehouse.stores.models import Store
store_corporate = Store.objects.get(id=1)
store_corporate.name = 'Not sure about this name'
# Update from db again
store_corporate.refresh_from_db() # Model record name now reflects value in database again
store_corporate.name
# Multiple edits
store_corporate.name = 'New store name'
store_corporate.email = 'newemail@coffeehouse.com' store_corporate.address = 'To be confirmed'
# Update from db again, but only address field
# so store name and email remain with local values
store_corporate.refresh_from_db(fields=['address'])
Listing 8-9.Update model record from database with the refresh_from_db() method
# Import Django model class
from coffeehouse.stores.models import Store
# Get the store with the name "Downtown" or equivalent SQL: 'SELECT....WHERE name = "Downtown"
downtown_store = Store.objects.get(name="Downtown")
# Call delete() to delete the record in the database
downtown_store.delete()
Listing 8-10.Delete model record with the delete() method
如果不小心的话,delete()方法可以删除多条记录。delete()方法前面是 objects.filter()方法,该方法可以返回包含多条记录的查询结果。注意,在清单 8-11 中,查询使用 id 字段来定义查询,确保只有一条记录与查询匹配,因为 id 是表的主键。如果 objects.filter()中的查询定义使用不太严格的查找(例如,字符串),您可能会无意中删除比预期更多的记录。
# Import Django model class
from coffeehouse.stores.models import Store
# Create model Store instances
store_corporate = Store(name='Corporate',address='624 Broadway',city ='San Diego',state='CA',email='corporate@coffeehouse.com')
store_downtown = Store(name='Downtown',address='Horton Plaza',city ='San Diego',state='CA',email='downtown@coffeehouse.com')
store_uptown = Store(name='Uptown',address='240 University Ave',city ='San Diego',state='CA',email='uptown@coffeehouse.com')
store_midtown = Store(name='Midtown',address='784 W Washington St',city ='San Diego',state='CA',email='midtown@coffeehouse.com')
# Create store list
store_list = [store_corporate,store_downtown,store_uptown,store_midtown]
# Call bulk_create to create records in a single call
Store.objects.bulk_create(store_list)
Listing 8-12.Create multiple records of a Django model with the bulk_create() method
# Same store_list as Listing 8-12
# Loop over each store and invoke save() on each entry # save() method called on each list member to create record
for store in store_list:
store.save()
Listing 8-13.Create multiple records with the save() method
# Import Django model and transaction class
from coffeehouse.stores.models import Store
from django.db import transaction
# Create store list, with same references from Listing 8-12
first_store_list = [store_corporate,store_downtown]
second_store_list = [store_uptown,store_midtown]
# Trigger atomic transaction so loop is executed in a single transaction
with transaction.atomic():
# Loop over each store and invoke save() on each entry
for store in first_store_list:
# save() method called on each member to create record
store.save()
# Method decorated with @transaction.atomic to ensure logic is executed in single transaction
@transaction.atomic
def bulk_store_creator(store_list):
# Loop over each store and invoke save() on each entry
for store in store_list:
# save() method called on each member to create record
store.save()
# Call bulk_store_creator with Store list
bulk_store_creator(second_store_list)
Listing 8-14.Create multiple records with save() method in a single transaction
# Import Django model class
from coffeehouse.stores.models import Store
# Query with all() method or equivalent SQL: 'SELECT * FROM ...'
all_stores = Store.objects.all()
# Query with include() method or equivalent SQL: 'SELECT....WHERE city = "San Diego"'
san_diego_stores = Store.objects.filter(city='San Diego')
# Query with exclude() method or equivalent SQL: 'SELECT....WHERE NOT (city = "San Diego")'
non_san_diego_stores = Store.objects.exclude(city='San Diego')
# Query with include() and exclude() methods or equivalent SQL: 'SELECT....WHERE STATE='CA' AND NOT (city = "San Diego")'
ca_stores_without_san_diego = Store.objects.filter(state='CA').exclude(city='San Diego')
Listing 8-15.Read multiple records with with all(), filter(), and exclude() methods
from coffeehouse.stores.models import Store
import logging
stdlogger = logging.getLogger(__name__)
# Get the Store records with city San Diego
san_diego_stores = Store.objects.filter(city='San Diego')
stdlogger.debug("Query %s" % str(san_diego_stores.query))
# You can also use print(san_diego_stores.query)
除了单个字段-city = " San Diego "或 state="CA "--all()、filter()和 exclude()方法还可以接受多个字段来生成 and 查询(例如,filter(city="San Diego ",state = " CA ")以获取城市和州匹配的记录)。请参阅“按 SQL 关键字分类的查询”一章的后面部分。
# Import Django model class
from coffeehouse.stores.models import Store
# Query with in_bulk() all
Store.objects.in_bulk()
# Outputs: {1: <Store: Corporate (San Diego,CA)>, 2: <Store: Downtown (San Diego,CA)>, 3: <Store: Uptown (San Diego,CA)>, 4: <Store: Midtown (San Diego,CA)>}
# Compare in_bulk query to all() that produces QuerySet
Store.objects.all()
# Outputs: <QuerySet [<Store: Corporate (San Diego,CA)>, <Store: Downtown (San Diego,CA)>, <Store: Uptown (San Diego,CA)>, <Store: Midtown (San Diego,CA)>]>
# Query to get single Store by id
Store.objects.in_bulk([1])
# Outputs: {1: <Store: Corporate (San Diego,CA)>}
# Query to get multiple Stores by id
Store.objects.in_bulk([2,3])
# Outputs: {2: <Store: Downtown (San Diego,CA)>, 3: <Store: Uptown (San Diego,CA)>}
Listing 8-16.Read multiple records with with in_bulk() method
# Import Django model class
from coffeehouse.stores.models import Store
# Query with all() method
stores = Store.objects.all()
# Chain filter() method on query
stores = stores.filter(state='CA')
# Chain exclude() method on query
stores = stores.exclude(city='San Diego')
Listing 8-17.Chained model methods to illustrate concept of QuerySet lazy evaluation
Django QuerySet evaluation triggers that invoke an actual database call
| 评估触发器 | 描述 | 例子 |
| --- | --- | --- |
| 循环 | 在 QuerySet 上创建循环会触发数据库调用。 | 对于 Store.objects.all()中的 store: |
| 使用“step”参数切片 | 用第三个参数(也称为“step”或“stride”参数)分割 QuerySet 会触发数据库调用。注意:用 1 或 2 个参数分割 Queryset 只会创建另一个 QuerySet。 | #每第 5 条记录的列表,用于前 100 条记录 Store.objects.all()[:100:5] #这不会触发数据库命中,(2 个参数)#记录 50 到 100 Store.objects.all()[49:99] #从第 6 条 Store.objects.all()[5:] #这不会触发数据库命中(1 个参数)Store.objects.all()[0] #第一条记录 |
| 酸洗* | 酸洗一个查询集会强制在酸洗之前将所有结果加载到内存中。 | import pickle stores = store . objects . all()pickle _ stores = pickle . dumps(stores) |
| repr()方法 | 对 QuerySet 调用 repr()会触发数据库调用。注意:这是为了在 Python 交互式解释器中方便起见,所以您可以立即看到查询结果。 | repr(Store.objects.all()) |
| len()方法 | 对 QuerySet 调用 len()会触发数据库调用。注意:如果您只需要记录的数量,使用 Django model count()方法会更有效。 | total _ stores = len(store . objects . all())#注意:count()方法可以更有效地获得总计数 efficient _ total _ stores = store . objects . count() |
| list()方法 | 对 QuerySet 调用 list()会触发数据库调用。 | store _ list = list(store . objects . all()) |
| 布尔测试(bool()、or 和 or if 语句) | 对 QuerySet 进行布尔测试会触发数据库调用。注意:如果您只想检查记录是否存在,使用 Django model exists()方法会更有效。 | #如果 store . objects . filter(city='San Diego '),则检查是否有 city = ' San Diego '的商店:#在' San Diego' pass 中有一个商店#注意:exists()方法对于布尔值检查更有效 San _ Diego _ stores = store . objects . exists(city = ' San Diego ') |
Pickling is Python’s standard mechanism for object serialization, a process that converts a Python object into a character stream. The character stream contains all the information necessary to reconstruct the object at a later time. Pickling in the context of Django queries is typically used for heavyweight queries in an attempt to save resources (e.g., make a heavyweight query, pickle it, and on subsequent occasions consult the pickled query). You can consider pickling Django queries a rudimentary form of caching.
# Import Django model class
from coffeehouse.stores.models import Store
# CACHE USING SEQUENCE
# Query awaiting evaluation
lazy_stores = Store.objects.all()
# Iteration triggers evaluation and hits database
store_emails = [store.email for store in lazy_stores]
# Uses QuerySet cache from lazy_stores, since lazy_stores is evaluated in previous line
store_names = [store.name for store in lazy_stores]
# NON-CACHE SEQUENCE
# Iteration triggers evaluation and hits database
heavy_store_emails = [store.email for store in Store.objects.all()]
# Iteration triggers evaluation and hits database again, because it uses another QuerySet ref
heavy_store_names = [store.name for store in Store.objects.all()]
# CACHE USING SEQUENCE
# Query wrapped as list() for immediate evaluation
stores = list(Store.objects.all())
# Uses QuerySet cache from stores
first_store = stores[0]
# Uses QuerySet cache from stores
second_store = stores[1]
# Uses QuerySet cache from stores, set() is just used to eliminate duplicates
store_states = set([store.state for store in stores])
# Uses QuerySet cache from stores, set() is just used to eliminate duplicates
store_cities = set([store.city for store in stores])
# NON-CACHE SEQUENCE
# Query awaiting evaluation
all_stores = Store.objects.all()
# list() triggers evaluation and hits database
store_one = list(all_stores[0:1])
# list() triggers evaluation and hits database again, because partially evaluating a QuerySet does not populate the cache
store_one_again = list(all_stores[0:1])
# CACHE USING SEQUENCE
# Query awaiting evaluation
coffee_stores = Store.objects.all()
# Iteration triggers evaluation and hits database
[store for store in coffee_stores]
# Uses QuerySet cache from coffee_stores, because it's evaluated fully in previous line
store_1 = coffee_stores[0]
# Uses QuerySet cache from coffee_stores, because it's already evaluated in full
store_1_again = coffee_stores[0]
Listing 8-18.QuerySet caching behavior
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Item names on the breakfast menu
breakfast_items = Item.objects.filter(menu__name='Breakfast').only('name')
# All Store records with no email
all_stores = Store.objects.defer('email').all()
# Confirm loaded fields on overall query
breakfast_items.query.get_loaded_field_names()
# Outputs: {<class 'coffeehouse.items.models.Item'>: {'id', 'name'}}
all_stores.query.get_loaded_field_names()
# Outputs: {<class 'coffeehouse.stores.models.Store'>: {'id', 'address', 'state', 'city', 'name'}}
# Confirm deferred fields on individual model records breakfast_items[0].get_deferred_fields()
# Outputs: {'calories', 'stock', 'price', 'menu_id', 'size', 'description'}
all_stores[1].get_deferred_fields()
# Outputs: {'email'}
# Access deferred fields, note each call on a deferred field implies a database hit
breakfast_items[0].price
breakfast_items[0].size
all_stores[1].email
Listing 8-19.Read performance with defer() and only() to selectively read record fields
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Item names on the breakfast menu
breakfast_items = Item.objects.filter(menu__name='Breakfast').values('name')
print(breakfast_items)
# Outputs: <QuerySet [{'name': 'Whole-Grain Oatmeal'}, {'name': 'Bacon, Egg & Cheese Biscuit'}]>
# All Store records with no email
all_stores = Store.objects.values_list('email','name','city').all()
print(all_stores)
# Outputs: <QuerySet [('corporate@coffeehouse.com', 'Corporate', 'San Diego'), ('downtown@coffeehouse.com', 'Downtown', 'San Diego'), ('uptown@coffeehouse.com', 'Uptown', 'San Diego'), ('midtown@coffeehouse.com', 'Midtown', 'San Diego')]>
all_stores_flat = Store.objects.values_list('email',flat=True).all()
print(all_stores_flat)
# Outputs: <QuerySet ['corporate@coffeehouse.com', 'downtown@coffeehouse.com', 'midtown@coffeehouse.com', 'uptown@coffeehouse.com']>
# It isn't possible to access undeclared model fields with values() and values_list()
breakfast_items[0].price #ERROR
# Outputs AttributeError: 'dict' object has no attribute 'price'
Listing 8-20.Read performance with values() and values_list() to selectively read record fields
from coffeehouse.stores.models import Store
# All Store with iterator()
stores_on_iterator = Store.objects.all().iterator()
print(stores_on_iterator)
# Outputs: <generator object __iter__ at 0x7f2864db8fc0>
# Advance through iterator with __next__()
stores_on_iterator.__next__()
# Outputs: <Store: Corporate (San Diego,CA)>
stores_on_iterator.__next__()
# Outputs: <Store: Downtown (San Diego,CA)>
# Check if Store object with id=5 exists
Store.objects.filter(id=5).exists()
# Outputs: False
# Create empty QuerySet on Store model
Store.objects.none()
# Outputs: <QuerySet []>
Listing 8-21.Read performance with iterator(), exists(), and none()
from coffeehouse.stores.models import Store
Store.objects.all().update(email="contact@coffeehouse.com")
from coffeehouse.items.models import Item
from django.db.models import F
Item.objects.all().update(stock=F('stock') +100)
Listing 8-22.Update multiple records with the update() method
# Import Django model class
from coffeehouse.stores.models import Store
from django.db import transaction
# Trigger atomic transaction so loop is executed in a single transaction
with transaction.atomic():
store_list = Store.objects.select_for_update().filter(state='CA')
# Loop over each store to update and invoke save() on each entry
for store in store_list:
# Add complex update logic here for each store
# save() method called on each member to update
store.save()
# Method decorated with @transaction.atomic to ensure logic is executed in single transaction
@transaction.atomic
def bulk_store_updae(store_list):
store_list = Store.objects.select_for_update().exclude(state='CA')
# Loop over each store and invoke save() on each entry
for store in store_list:
# Add complex update logic here for each store
# save() method called on each member to update
store.save()
# Call bulk_store_update to update store records
bulk_store_update(store_list_to_update)
Listing 8-23.Update multiple records with a Django model with the select_for_update() method
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
# Get the Menu of a given Item
Item.objects.get(name='Whole-Grain Oatmeal').menu.id
# Get the Menu id of a given Item
Item.objects.get(name='Whole-Grain Oatmeal').menu.name
# Get Item elements that belong to the Menu with name 'Drinks'
Item.objects.filter(menu__name='Drinks')
Listing 8-25.One to many ForeignKey direct query read operations
from coffeehouse.items.models import Menu, Item
breakfast_menu = Menu.objects.get(name='Breakfast')
# Fetch all Item records for the Menu
breakfast_menu.item_set.all()
# Get the total Item count for the Menu
breakfast_menu.item_set.count()
# Fetch Item records that match a filter for the Menu
breakfast_menu.item_set.filter(name__startswith='Whole')
Listing 8-26.One to many ForeignKey reverse query read operations with _set syntax
from coffeehouse.items.models import Menu, Item
breakfast_menu = Menu.objects.get(name='Breakfast')
# Create an Item directly on the Menu
# NOTE: Django also supports the get_or_create() and update_or_create() operations
breakfast_menu.item_set.create(name='Bacon, Egg & Cheese Biscuit',description='A fresh buttermilk biscuit...',calories=450)
# Create an Item separately and then add it to the Menu
new_menu_item = Item(name='Grilled Cheese',description='Flat bread or whole wheat ...',calories=500)
# Add item to menu using add()
# NOTE: bulk=False is necessary for new_menu_item to be saved by the Item model manager first
# it isn't possible to call new_menu_item.save() directly because it lacks a menu instance
breakfast_menu.item_set.add(new_menu_item,bulk=False)
# Create copy of breakfast items for later
breakfast_items = [bi for bi in breakfast_menu.item_set.all()]
# Clear menu references from Item elements (i.e. reset the Item elements menu field to null)
# NOTE: This requires the ForeignKey definition to have null=True
# (e.g. models.ForeignKey(Menu, null=True)) so the key is allowed to be turned null
# otherwise the error 'RelatedManager' object has no attribute 'clear' is thrown
breakfast_menu.item_set.clear()
# Verify Item count is now 0
breakfast_menu.item_set.count()
0
# Reassign Item set from copy of breakfast items
breakfast_menu.item_set.set(breakfast_items)
# Verify Item count is now back to original count
breakfast_menu.item_set.count()
3
# Clear menu reference from single Item element (i.e. reset an Item element menu field to null)
# NOTE: This requires the ForeignKey definition to have null=True
# (e.g. models.ForeignKey(Menu, null=True)) so the key is allowed to be turned null
# otherwise the error 'RelatedManager' object has no attribute 'remove' is thrown
item_grilled_cheese = Item.objects.get(name='Grilled Cheese')
breakfast_menu.item_set.remove(item_grilled_cheese)
# Delete the Menu element along with its associated Item elements
# NOTE: This requires the ForeignKey definition to have blank=True and on_delete=models.CASCADE (e.g. models.ForeignKey(Menu, blank=True, on_delete=models.CASCADE))
breakfast_menu.delete()
Listing 8-27.One to many ForeignKey reverse query create, update, delete operations with _set syntax
class Amenity(models.Model):
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
email = models.EmailField()
amenities = models.ManyToManyField(Amenity,blank=True)
# Get the Amenity elements of a given Store
Store.objects.get(name='Downtown').amenities.all()
# Fetch store named Midtown
midtown_store = Store.objects.get(name='Midtown')
# Create and add Amenity element to Store
midtown_store.amenities.create(name='Laptop Lock',description='Ask our baristas...')
# Get all Store elements that have amenity id=3
Store.objects.filter(amenities__id=3)
Listing 8-28.Many to many ManyToManyField direct query read operations
from coffeehouse.stores.models import Store, Amenity
wifi_amenity = Amenity.objects.get(name='WiFi')
# Fetch all Store records with Wifi Amenity
wifi_amenity.store_set.all()
# Get the total Store count for the Wifi Amenity
wifi_amenity.store_set.count()
# Fetch Store records that match a filter with the Wifi Amenity
wifi_amenity.store_set.filter(city__startswith='San Diego')
# Create a Store directly with the Wifi Amenity
# NOTE: Django also supports the get_or_create() and update_or_create() operations
wifi_amenity.store_set.create(name='Uptown',address='1240 University Ave...')
# Create a Store separately and then add the Wifi Amenity to it
new_store = Store(name='Midtown',address='844 W Washington St...')
new_store.save()
wifi_amenity.store_set.add(new_store)
# Create copy of breakfast items for later
wifi_stores = [ws for ws in wifi_amenity.store_set.all()]
# Clear all the Wifi amenity records in the junction table for all Store elements
wifi_amenity.store_set.clear()
# Verify Wifi count is now 0
wifi_amenity.store_set.count()
0
# Reassign Wifi set from copy of Store elements
wifi_amenity.store_set.set(wifi_stores)
# Verify Item count is now back to original count
wifi_amenity.store_set.count()
6
# Reassign Store set from copy of wifi stores
wifi_amenity.store_set.set(wifi_stores)
# Clear the Wifi amenity record from the junction table for a certain Store element
store_to_remove_amenity = Store.objects.get(name__startswith='844 W Washington St')
wifi_amenity.store_set.remove(store_to_remove_amenity)
# Delete the Wifi amenity element along with its associated junction table records for Store elements
wifi_amenity.delete()
Listing 8-29.Many to many ManyToManyField reverse query create, read, update, and delete operations with _set syntax
from coffeehouse.items.models import Item
# See Listing 8-25 for Item model definition
class Drink(models.Model):
item = models.OneToOneField(Item,on_delete=models.CASCADE,primary_key=True)
caffeine = models.IntegerField()
# Get Item instance named Mocha
mocha_item = Item.objects.get(name='Mocha')
# Access the Drink element and its fields through its base Item element
mocha_item.drink.caffeine
# Get Drink objects through Item with caffeine field less than 200
Item.objects.filter(drink__caffeine__lt=200)
# Delete the Item element and its associated Drink record
# NOTE: This deletes the associated Drink record due to the on_delete=models.CASCADE in the OneToOneField definition
mocha_item.delete()
# Query a Drink through an Item property
Drink.objects.get(item__name='Latte')
Listing 8-30.One to one OneToOneField query
operations
from coffeehouse.items.models import Item
# See Listing 8-25 for Item and Menu model definitions
# Inefficient access to related model
for item in Item.objects.all():
item.menu # Each call to menu creates an additional database hit
# Efficient access to related model with selected_related()
for item in Item.objects.select_related('menu').all():
item.menu # All menu data references have been fetched on initial query
# Raw SQL query with select_related
print(Item.objects.select_related('menu').all().query)
SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name", "items_item"."description", "items_item"."size", "items_item"."calories", "items_item"."price", "items_item"."stock", "items_menu"."id", "items_menu"."name" FROM "items_item" LEFT OUTER JOIN "items_menu" ON ("items_item"."menu_id" = "items_menu"."id")
# Raw SQL query without select_related
print(Item.objects.all().query)
SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name", "items_item"."description", "items_item"."size", "items_item"."calories", "items_item"."price", "items_item"."stock" FROM "items_item"
Listing 8-31.Django model select_related syntax and generated SQL
from coffeehouse.items.models import Item
from coffeehouse.stores.models import Store
# See Listing 8-25 for Item model definitions
# See Listing 8-28 for Store model definitions
# Efficient access to related model with prefetch_related()
for item in Item.objects.prefetch_related('menu').all():
item.menu # All menu data references have been fetched on initial query
# Efficient access to many to many related model with prefetch_related()
# NOTE Store.objects.select_related('amenities').all() is invalid due to many to many model
for store in Store.objects.prefetch_related('amenities').all():
store.amenities.all()
# Raw SQL query with prefetch_related
print(Item.objects.prefetch_related('menu').all().query)
SELECT "items_item"."id", "items_item"."menu_id", "items_item"."name", "items_item"."description", "items_item"."size", "items_item"."calories", "items_item"."price", "items_item"."stock" FROM "items_item"
# Raw SQL query with prefetch_related
print(Store.objects.prefetch_related('amenities').all().query)
SELECT "stores_store"."id", "stores_store"."name", "stores_store"."address", "stores_store"."city", "stores_store"."state", "stores_store"."email" FROM "stores_store"
Listing 8-32.Django model prefetch_related syntax and generated SQL
在前面的章节中,您学习了如何使用 Django 模型方法查询单个、多个和相关的记录。然而,匹配过程大部分是在精确值的基础上完成的。例如,查询将id=1转换为 SQL WHERE ID=1的Store记录,或者查询将state="CA"转换为 SQL WHERE STATE="CA"的所有Store记录。
SQL WHERE 关键字是关系数据库查询中最常用的关键字之一,因为它用于通过字段值来限定查询中的记录数量。到目前为止,您主要使用 SQL WHERE 关键字来创建对精确值的查询(例如,WHERE ID=1);然而,SQL WHERE 关键字还有许多其他变体。
在 Django 模型中,SQL WHERE 关键字的变体通过字段查找得到支持,字段查找是使用__(两个下划线)(也称为“跟随符号”)附加到字段过滤器的关键字。
The PK Lookup Shortcut
Django 查询依靠模型字段名称来分类查询。例如,Django 查询中的 SQL WHERE ID=1 语句写成…(id=1),Django 查询中的 SQL WHERE NAME="CA "语句写成…(state="CA ")。
此外,Django 模型还可以使用 pk 快捷方式——其中 PK =“primary key”——对模型的主键执行查询。默认情况下,Django 模型的 id 字段是主键,因此 id 字段和 pk 快捷方式查询被认为是等效的(例如 store . objects . get(id = 1)store . objects . get(PK = 1))。
当模型定义了自定义主键模型字段时,具有 pk 查找的查询与具有 id 字段的查询只有不同的含义。
=/等于和!=/不相等查询:exact,iexact
等式或=查询是 Django 模型中使用的默认 WHERE 行为。等式搜索有两种语法变体;一个是简写版本,另一个使用exact字段查找,清单 8-33 展示了这两种方法。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Get the Store object with id=1
Store.objects.get(id__exact=1)
# Get the Store object with id=1 (Short-handed version)
Store.objects.get(id=1)
# Get the Drink objects with name="Mocha"
Item.objects.filter(name__exact="Mocha")
# Get the Drink objects with name="Mocha" (Short-handed version)
Item.objects.filter(name="Mocha")
Listing 8-33.Django equality = or EQUAL query
正如您在清单 8-33 中看到的,您可以使用exact字段查找来明确限定查询,或者使用简写语法<field>=<value>。因为 exact WHERE 查询是最常见的,Django 意味着默认的exact搜索。
Tip
您可以使用 iexact 字段查找进行不区分大小写的相等查询(例如,匹配“if”、“If”、“iF”或“IF”)。详情参见 LIKE 和 ILIKE 查询部分。
不平等还是!=搜索还有两种语法变体,如清单 8-34 所示。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
from django.db.models import Q
# Get the Store records that don't have state 'CA'
Store.objects.exclude(state='CA')
# Get the Store records that don't have state 'CA', using Q
Store.objects.filter(∼Q(state="CA"))
# Get the Item records and exclude items that have more than 100 calories
Item.objects.exclude(calories__gt=100)
# Get the Item records and exclude those with 100 or more calories, using Q
Item.objects.filter(∼Q(calories__gt=100))
Listing 8-34.Django inequality != or NOT EQUAL query with exclude() and Q objects
要使用 AND 语句创建 SQL WHERE 查询,您可以向一个查询添加多个语句或使用Q对象,如清单 8-35 所示。
from coffeehouse.stores.models import Store
from django.db.models import Q
# Get the Store records that have state 'CA' AND city 'San Diego'
Store.objects.filter(state='CA', city='San Diego')
# Get the Store records that have state 'CA' AND city not 'San Diego'
Store.objects.filter(Q(state='CA') & ∼Q(city='San Diego'))
Listing 8-35.Django AND query
清单 8-35 中的第一个例子将多个字段值添加到filter()方法中,以产生一个WHERE <field_1> AND <field_2>语句。清单 8-35 中的第二个例子也使用了filter()方法,但是使用了两个Q对象通过&操作符与AND语句(即WHERE <field_1> AND NOT <field2>)产生一个否定。
Tip
例如,如果您正在寻找一个比清单 8-35 中的查询更广泛的 AND 查询,获取状态为‘CA’的商店对象和状态为‘AZ’的商店对象,查看 or 查询或 in 查询。
如果您希望合并两个查询,例如 query1 和 query 2,请参阅本章后面的合并查询部分。
OR 查询:Q()对象
要用 OR 语句创建 SQL WHERE 查询,您可以使用Q对象,如清单 8-36 所示。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
from django.db.models import Q
# Get the Store records that have state 'CA' OR state='AZ'
Store.objects.filter(Q(state='CA') | Q(state='AZ'))
# Get the Item records with name "Mocha" or "Latte"
Item.objects.filter(Q(name="Mocha") | Q(name='Latte'))
Listing 8-36.Django OR query
清单 8-36 中的两个例子都在Q对象之间使用|(管道)操作符来产生一个WHERE <field1> OR <field2>语句,类似于&操作符如何用于 AND 条件。
IS 和 IS NOT 查询:isnull
SQL IS 和 IS NOT 语句通常在涉及空值的查询中与 WHERE 一起使用。根据数据库品牌的不同,SQL IS 和 IS NOT 也可以用于布尔查询。要创建带有 IS 或 NOT 语句的 SQL WHERE 查询,您可以使用带有等价测试的 Python None数据类型或isnull字段查找,如清单 8-37 所示。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Drink
from django.db.models import Q
# Get the Store records that have email NULL
Store.objects.filter(email=None)
# Get the Store records that have email NULL
Store.objects.filter(email__isnull=True)
# Get the Store records that have email NOT NULL
Store.objects.filter(email__isnull=False)
Listing 8-37.Django IS and IS NOT queries
SQL IN 语句与 WHERE 子句一起使用,生成与值列表匹配的查询。要用 IN 语句创建 SQL WHERE 查询,可以使用in字段查找,如清单 8-38 所示。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Drink
# Get the Store records that have state 'CA' OR state='AZ'
Store.objects.filter(state__in=['CA','AZ'])
# Get the Item records with id 1,2 or 3
Item.objects.filter(id__in=[1,2,3])
Listing 8-38.Django IN queries
LIKE 和 ILIKE 查询:contains、icontains、startswith、istartswith、endswith、iendswith
SQL LIKE 和 ILIKE 查询与 WHERE 子句一起使用来匹配字符串模式,前者区分大小写,后者不区分大小写。Django 提供了三种字段查找来生成类似 SQL 的查询,这取决于您希望匹配的字符串模式。清单 8-39 展示了如何使用 Django 字段查找生成三个不同的类似 SQL 的查询。
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item, Drink
# Get the Store records that contain a 'C' anywhere in state (LIKE '%C%')
Store.objects.filter(state__contains='C')
# Get the Store records that start with 'San' in city (LIKE 'San%')
Store.objects.filter(city__startswith='San')
# Get the Item records that end with 'e' in name (LIKE '%e')
Drink.objects.filter(item__name__endswith='e')
Listing 8-39.Django LIKE queries
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Get the Store recoeds that contain 'a' in state anywhere case insensitive (ILIKE '%a%')
Store.objects.filter(state__icontains='a')
# Get the Store records that start with 'san' in city case insensitive (ILIKE 'san%')
Store.objects.filter(city__istartswith='san')
# Get the Item records that end with 'a' in name case insensitive (ILIKE '%A')
Item.objects.filter(name__iendswith='A')
# Get the Store records that have state 'ca' case insensitive (ILIKE 'ca')
Store.objects.filter(state__iexact='ca')
Listing 8-40.Django ILIKE queries
值得一提的是清单 8-40 中的最后一个例子是=/EQUAL and 的不区分大小写版本!=/不等于查询。然而,因为iexact在引擎盖下使用了 ILIKE,所以在本节中再次提到它。
REGEXP 查询:regex,iregex
有时 SQL LIKE & ILIKE 语句支持的模式过于基本,在这种情况下,您可以使用 SQL REGEXP 语句将复杂模式定义为正则表达式。正则表达式更强大,因为它们可以定义碎片化的模式,例如:以 sa 开头,后面跟任意字母,后面跟一个数字的模式;或者条件模式,例如以 Los 开始或以 Angeles 结束模式。Django 通过regex字段查找支持 SQL REGEXP 关键字,还通过iregex字段查找支持不区分大小写的正则表达式查询。
注定义regex或iregex字段查找模式的推荐实践是使用 Python 原始字符串。Python 原始字符串文字是一个以r开头的字符串,它方便地表达将被转义序列处理修改的字符串(例如,原始字符串r'\n'与标准字符串'\\n'相同)。这种行为对于严重依赖转义字符的正则表达式尤其有用。附录 A 更详细地描述了 Python 原始字符串的使用。
>/大于和小于查询:gt,gte,lt,lte
与数字字段相关联的 SQL WHERE 语句通常使用数学运算符>、> =、> =、< and <= through the 【 , 【 , 【 , and 【 field lookups, respectively. Listing 8-41 说明了这些字段查找在 Django 中的用法。
from coffeehouse.items.models import Item
# Get Item records with stock > 5
Item.objects.filter(stock__gt=5)
# Get Item records with stock > or equal 10
Item.objects.filter(stock__gte=10)
# Get Item records with stock < 100
Item.objects.filter(stock__lt=100)
# Get Item records with stock < or equal 50
Item.objects.filter(stock__lte=50)
Listing 8-41.Django GREATER THAN and LESSER THAN queries
日期和时间查询:范围,日期,年,月,日,周,星期 _ 日,时间,小时,分钟,秒
虽然 SQL WHERE 对日期和时间字段的查询可以用等号、大于号和小于号来完成,但由于它们的特殊性质,编写 SQL 日期和时间查询可能会很耗时。例如,要创建一个 SQL 查询来获取时间戳为 2018 年的所有记录,您需要创建一个类似于'WHERE date BETWEEN '2018-01-01' AND '2018-12-31'的查询。如您所见,如果您添加了处理时区、月份和闰年等内容的需求,这些查询在语法上可能会变得复杂和容易出错。
为了简化带有日期和时间值的 SQL WHERE 查询的创建,Django 提供了各种字段查找,如清单 8-42 所示。
from coffeehouse.online.models import Order
from django.utils.timezone import utc
import datetime
# Define custom dates
start_date = datetime.datetime(2017, 5, 10).replace(tzinfo=utc)
end_date = datetime.datetime(2018, 5, 21).replace(tzinfo=utc)
# Get Order recrods from custom dates, starting May 10 2017 to May 21 2018
Order.objects.filter(created__range=(start_date, end_date))
# Get Order records with exact start date
orders_2018 = Order.objects.filter(created__date=start_date)
# Get Order records with year 2018
Order.objects.filter(created__year=2018)
# Get Order records with month January, values can be 1 through 12 (1=January, 12=December).
Order.objects.filter(created__month=1)
# Get Order records with day 1, where values can be 1 through 31.
Order.objects.filter(created__day=1)
# Get Order records from January 1 2018
Order.objects.filter(created__year=2018,create__month=1,created__day=1)
# Get Order records that fall on week number 24 of the yr, where values can be 1 to 53.
Order.objects.filter(created__week=24)
# Get Order recrods that fall on Monday, where values can be 1 to 7 (1=Sunday, 7=Saturday).
Order.objects.filter(created__week_day=2)
# Get Order records made at 2:30pm using a time object
Order.objects.filter(created__time=datetime.time(14, 30))
# Get Order records made at 10am, where values can be 0 to 23 (0=12am, 23=11pm).
Order.objects.filter(date__hour=10)
# Get Order records made at the top of the hour, where values are 0 to 59.
Order.objects.filter(date__minute=0)
# Get Order records made the 30 second mark of every minute, where values are 0 to 59.
Order.objects.filter(date__second=30)
Listing 8-42.Django date and time queries with field lookups
from coffeehouse.stores.models import Store
# Get all Store records number
Store.objects.all().count()
4
# Get all distinct Store record number
Store.objects.distinct().count()
4
# Get distinct state Store record values
Store.objects.values('state').distinct().count()
1
# ONLY for PostgreSQL, distinct() can accept model fields to create DISTINCT ON query
Store.objects.distinct('state')
Listing 8-43.Django DISTINCT queries with distinct()
from coffeehouse.online.models import Order
# Get distinct years (as datetime.date) for Order objects
Order.objects.dates('created','year')
# Outputs: <QuerySet [datetime.date(2017, 1, 1),datetime.date(2018, 1, 1)]>
# Get distinct months (as datetime.date) for Order objects
Order.objects.dates('created','month')
# Outputs: <QuerySet [datetime.date(2017, 3, 1),datetime.date(2017, 6, 1),datetime.date(2018, 2, 1)]>
# Get distinct days (as datetime.datetime) for Order objects
Order.objects.datetimes('created','day')
# Outputs: <QuerySet [datetime.datetime(2017, 6, 17, 0, 0, tzinfo=<UTC>)...]>
# Get distinct minutes (as datetime.datetime) for Order objects
Order.objects.datetimes('created','minute')
# Outputs: <QuerySet [datetime.datetime(2017, 6, 17, 3, 13, tzinfo=<UTC>)...]>
Listing 8-44.Django DISTINCT date and time queries with dates and datetimes() methods
SQL 查询通常使用 ORDER 关键字告诉数据库引擎根据某个或某些字段对查询结果进行排序。这种技术很有帮助,因为它避免了在数据库之外(即在 Python 中)对记录进行排序的额外开销。Django 模型通过order_by()方法支持 SQL ORDER 语句。order_by()方法接受模型字段作为输入来定义查询顺序,这个过程如清单 8-45 所示。
from coffeehouse.stores.models import Store
# Get Store records and order by city (ORDER BY city)
Store.objects.all().order_by('city')
# Get Store recrods, order by name descending, email ascending (ORDER BY name DESC, email ASC)
Store.objects.filter(city='San Diego').order_by('-name','email')
Listing 8-45.Django ORDER queries
from coffeehouse.stores.models import Store
from coffeehouse.items.models import Item
# Get the first five (LIMIT=5) Store records that have state 'CA'
Store.objects.filter(state='CA')[:5]
# Get the second five (OFFSET=5,LIMIT=5) Item records (after the first 5)
Item.objects.all()[5:10]
# Get the first (LIMIT=1) Item object
Item.objects.all()[0]
Listing 8-46.Django LIMIT and OFFSET queries with Python slice syntax
from coffeehouse.items.models import Item, Drink
from itertools import chain
menu_sandwich_items = Item.objects.filter(menu__name='Sandwiches')
menu_salads_items = Item.objects.filter(menu__name='Salads')
drinks = Drink.objects.all()
# A pipe applied to two QuerySets generates a larger QuerySet
lunch_items = menu_sandwich_items | menu_salads_items
# | can't be used to merge QuerySet's with different models # ERROR menu_sandwich_items | drinks
# itertools.chain generates a Python list and can merge different QuerySet model types
lunch_items_with_drinks = list(chain(menu_sandwich_items, drinks))
Listing 8-47.Combine two Django queries with | (pipe) and itertools.chain
from coffeehouse.stores.models import Store
from django.db.models import Count
# Get the number of stores (COUNT(*))
stores_count = Store.objects.all().count()
print(stores_count)
4
# Get the number of stores that have city 'San Diego' (COUNT(*))
stores_san_diego_count = Store.objects.filter(city='San Diego').count()
# Get the number of emails, NULL values are not counted (COUNT(email))
emails_count = Store.objects.aggregate(Count('email'))
print(emails_count)
{'email__count': 4}
# Get the number of emails, NULL values are not counted (COUNT(email) AS "coffeehouse_store_emails_count")
emails_count_custom = Store.objects.aggregate(coffeehouse_store_emails_count=Count('email'))
print(emails_count_custom)
{'coffeehouse_store_emails_count': 4}
# Get number of distinct Amenities in all Stores, NULL values not counted (COUNT(DISTINCT name))
different_amenities_count = Store.objects.aggregate(Count('amenities',distinct=True))
print(different_amenities_count)
{'amenities__count': 5}
# Get number of Amenities per Store with annotate
stores_with_amenities_count = Store.objects.annotate(Count('amenities'))
# Get amenities count in individual Store
stores_With_amenities_count[0].amenities__count
# Get number of Amenities per Store with annotate and custom name
stores_amenities_count_custom = Store.objects.annotate(amenities_per_store=Count('amenities'))
stores_amenities_count_custom[0].amenities_per_store
Listing 8-51.Django COUNT queries with aggregate(), annotate(), and Count()
from coffeehouse.items.models import Item
from django.db.models import Avg, Max, Min
from django.db.models import Sum
from django.db.models import Variance, StdDev
# Get the average, maximum and minimum number of stock for all Item records
avg_max_min_stock = Item.objects.aggregate(Avg('stock'), Max('stock'), Min('stock'))
print(avg_max_min_stock)
{'stock__avg': 29.0, 'stock__max': 36, 'stock__min': 27}
# Get the total stock for all Items
item_all_stock = Item.objects.aggregate(all_stock=Sum('stock'))
print(item_all_stock)
{'all_stock': 261}
# Get the variance and standard deviation for all Item records
# NOTE: Variance & StdDev return the population variance & standard deviation, respectively.
# But it's also possible to return sample variance & standard deviation,
# using the sample=True argument
item_statistics = Item.objects.aggregate(Variance('stock'), std_dev_stock= StdDev('stock'))
{'std_dev_stock': 5.3748, 'stock__variance': 28.8888}
Listing 8-52.Django MAX, MIN,SUM, AVG, VARIANCE and STDDEV queries with Max(), Min(), Sum(), Avg(), Variance() and StdDev() classes
Django F 表达式是 Django 模型中最常见的 SQL 表达式。在本章开始时,您已经了解了 F 表达式的用途,了解了如何在一个步骤中更新一条记录,并让数据库引擎执行逻辑,而不需要从数据库中提取记录。
通过 F 表达式,可以在查询中引用模型字段,并让数据库对模型字段值执行操作,而无需从数据库中提取数据。反过来,这不仅提供了更简洁的查询语法——单个更新查询,而不是两个(一个读取,一个更新)——它还避免了“竞争条件”。 4
清单 8-53 展示了在更新查询中使用 F 表达式的各种方式。
from coffeehouse.items.models import Item
from django.db.models import F
# Get single item
egg_biscuit = Item.objects.get(id=2)
# Check stock
egg_biscuit.stock
2
# Add 10 to stock value with F() expression
egg_biscuit.stock = F('stock') + 10
# Trigger save() to apply F() expression
egg_biscuit.save()
# Check stock again
egg_biscuit.stock
<CombinedExpression: F(stock) + Value(10)>
# Ups, need to re-read/refresh from DB
egg_biscuit.refresh_from_db()
# Check stock again
egg_biscuit.stock
12
# Decrease stock value by 1 for Item records on the Breakfast menu
breakfast_items = Item.objects.filter(menu__name='Breakfast')
breakfast_items.update(stock=F('stock') – 1)
# Increase all Item records stock by 20
Item.objects.all().update(stock=F('stock') + 20)
Listing 8-53.Django F() expression
update queries
from django.db.models import F, Func, Value
from django.db.models.functions import Upper, Concat
from coffeehouse.stores.models import Store
# SQL Upper function call via Func expression and F expression
stores_w_upper_names = Store.objects.annotate(name_upper=Func(F('name'), function='Upper'))
stores_w_upper_names[0].name_upper
'CORPORATE'
stores_w_upper_names[0].name
'Corporate'
# Equivalent SQL Upper function call directly with Django SQL Upper function
stores_w_upper_names_function = Store.objects.annotate(name_upper=Upper('name'))
stores_w_upper_names_function[0].name_upper
'CORPORATE'
# SQL Concat function called directly with Django SQL Concat function
stores_w_full_address = Store.objects.annotate(full_address=
Concat('address',Value(' - '),'city',Value(' , '),'state'))
stores_w_full_address[0].full_address
'624 Broadway - San Diego, CA'
stores_w_full_address[0].city
'San Diego'
Listing 8-55.Django Func() expressions
for SQL functions and Django SQL functions
from django.db.models import OuterRef, Subquery
class Order(models.Model):
created = models.DateTimeField(auto_now_add=True)
class OrderItem(models.Model):
item = models.IntegerField()
amount = models.IntegerField()
order = models.ForeignKey(Order)
# Get Items in order number 1
order_items = OrderItem.objects.filter(order__id=1)
# Get item
order_items[0].item
1
# Get item name ?
# OrderItem item field is IntegerField, lacks Item relationship
# Create sub-query to get Item records with id
item_subquery = Item.objects.filter(id=(OuterRef('id')))
# Annotate previous query with sub-query
order_items_w_name = order_items.annotate(item_name=Subquery(item_subquery.values('name')[:1]))
# Output SQL to verify
print(order_items_w_name.query)
SELECT `online_orderitem`.`id`, `online_orderitem`.`item`,
`online_orderitem`.`amount`, `online_orderitem`.`order_id`,
(SELECT U0.`name` FROM `items_item` U0 WHERE U0.`id` = (online_orderitem.`id`) LIMIT 1)
AS `item_name` FROM `online_orderitem` WHERE `online_orderitem`.`order_id` = 1
# Access item and item_name
order_items_w_name[0].item
1
order_items_w_name[0].item_name
'Whole-Grain Oatmeal'
Listing 8-56.Django Subquery expression with SQL subquery to get related model data
涉及子查询的第二种情况是,一个 SQL 查询必须生成一个 WHERE 语句,该语句的值依赖于另一个 SQL 查询的结果。这个场景在清单 8-57 中进行了说明。
# See Listing 8-56 for referenced model definitions
from coffeehouse.online.models import Order
from coffeehouse.items.models import Item
from django.db.models import OuterRef, Subquery
# Get Item records in lastest Order to replenish stock
most_recent_items_on_order = Order.objects.latest('created').orderitem_set.all()
# Get a list of Item records based on recent order using a sub-query
items_to_replenish = Item.objects.filter(id__in=Subquery(
most_recent_items_on_order.values('item')))
print(items_to_replenish.query)
SELECT `items_item`.`id`, `items_item`.`menu_id`, `items_item`.`name`, `items_item`.`description`, `items_item`.`size`, `items_item`.`calories`,
`items_item`.`price`, `items_item`.`stock` FROM `items_item` WHERE `items_item`.`id`
IN (SELECT U0.`item` FROM `online_orderitem` U0 WHERE U0.`order_id` = 1)
Listing 8-57.Django Subquery expression with SQL subquery in WHERE statement
清单 8-57 的第一步是从最新的Order记录中获取所有的OrderItem记录,目的是检测要补充哪个Item库存。然而,因为OrderItem记录使用一个普通的整数 id 来引用Item记录,所以有必要创建一个子查询来获取基于OrderItem整数引用的所有Item记录。
接下来在清单 8-57 中,查询 id 包含在子查询中的项目记录。在这种情况下,Subquery表达式用于指向most_recent_items_on_order查询,该查询仅从最近的Order记录中获取item值(即整数值)。
from coffeehouse.items.models import Drink, Item
# Get all drink
all_drinks = Drink.objects.raw("SELECT * FROM items_drink")
# Confirm type
type(all_drinks)
# Outputs: <class 'django.db.models.query.RawQuerySet'>
# Get first drink with index 0
first_drink = all_drinks[0]
# Get Drink name (via item OneToOne relationship)
first_drink.item.name
# Use parameters to limit a raw SQL query
caffeine_limit = 100
# Create raw() query with params argument to pass dynamic arguments
drinks_low_caffeine = Drink.objects.raw("SELECT * FROM items_drink where caffeine < %s",params=[caffeine_limit]);
Listing 8-58.Django model manager raw() method
清单 8-58 中的第一个代码片段使用Drink模型管理器的raw()方法发出SELECT * FROM items_drink查询。值得一提的是,这个原始查询产生与原生 Django 查询Drink.objects.all()相同的结果,但是与产生QuerySet数据结构的原生查询不同,请注意raw()方法查询是如何产生RawQuerySet数据结构的。
# Map results from legacy table into Item model
all_legacy_items = Item.objects.raw("SELECT product_name AS name, product_description AS description from coffeehouse_products")
# Access legacy results as if they are standard Item model records
all_legacy_items[0].name
# Use explicit mapping argument instead of 'as' statements in SQL query
legacy_mapping = {'product_name':'name','product_description':'description'}
# Create raw() query with translations argument to map table results
all_legacy_items_with_mapping = Item.objects.raw("SELECT * from coffeehouse_products", translations=legacy_mapping)
# Deferred model field loading, get item one with limited fields
item_one = Item.objects.raw("SELECT id,name from items_item where id=1")
# Acess model fields not referenced in the raw query, just like QuerySet defer()
item_one[0].calories
item_one[0].price
# Raw SQL query with aggregate function added as extra model field
items_with_inventory = Item.objects.raw("SELECT *, sum(price*stock) as assets from items_item");
# Access extra field directly as part of the model
items_with_inventory[0].assets
Listing 8-59.Django model manager raw() method with mapping, deferred fields, and aggregate queries
清单 8-59 中的第一个例子用多个 SQL AS 语句声明了一个raw()方法,在这种情况下,每个 AS 子句对应一个项目模型字段。以这种方式,当 Django 检查原始查询的结果时,它知道如何将结果映射到Item实例,而不考虑底层数据库表列名。
在这种情况下,您需要使用第二个 Django 选项来执行原始 SQL 查询,包括直接连接到数据库并显式提取查询结果。尽管第二种 Django 方法在技术上是与数据库交互最灵活的,但它也需要使用来自 Python 的 DB API 的低级调用。
使用这种技术执行原始 SQL 查询时,Django 中唯一可以利用的是 Django 项目中定义的数据库连接(即settings.py中的DATABASES变量)。一旦建立了数据库连接,您将需要依赖 Python DB API 方法,如cursor()、fetchone()和fetchall(),以及执行结果的手动提取,以便能够成功运行原始 SQL 查询。
清单 8-60 展示了在 Django 环境中使用 Python DB API 的 SQL 查询。
from django.db import connection
# Delete record
target_id = 1
with connection.cursor() as cursor:
cursor.execute("DELETE from items_item where id = %s", [target_id])
# Select one record
salad_item = None
with connection.cursor() as cursor:
cursor.execute("SELECT * from items_item where name='Red Fruit Salad'")
salad_item = cursor.fetchone()
# DB API fetchone produces a tuple, where elements are accessible by index
salad_item[0] # id
salad_item[1] # name
salad_item[2] # description
# Select multiple records
all_drinks = None
with connection.cursor() as cursor:
cursor.execute("SELECT * from items_drink")
all_drinks = cursor.fetchall()
# DB API fetchall produces a list of tuples
all_drinks[0][0] # first drink id
Listing 8-60.Django raw SQL queries with connection() and low-level DB API methods
清单 8-60 中的第一条语句导入了django.db.connection,它代表了在settings.py的DATABASES变量中定义的default数据库连接。一旦有了对 Django 数据库的connection引用,就可以开始使用 Python DB API,这通常是从使用cursor()方法开始的。 8
from django.db import models
# Create custom model manager
class ItemMenuManager(models.Manager):
def salad_items(self):
return self.filter(menu__name='Salads')
def sandwich_items(self):
return self.filter(menu__name='Sandwiches')
# Option 1) Override default model manager
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
...
objects = ItemMenuManager()
# Queries on default custom model manager
Item.objects.all()
Item.objects.salad_items()
Item.objects.sandwich_items()
# Option 2) Create new model manager field and leave default model manager as is
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
...
objects = models.Manager()
menumgr = ItemMenuManager()
# Queries on default and custom model managers
Item.objects.all()
Item.menumgr.salad_items()
Item.menumgr.sandwich_items()
# ERROR Item.objects.salad_items() # 'Manager' object has no attribute 'salad_items'
# ERROR Item.objects.sandwich_items() # 'Manager' object has no attribute 'sandwich_items'
Item.menumgr.all()
Listing 8-61.Django custom model manager with custom manager methods
from django.db import models
class Item(models.Model):
...
objects = models.Manager() # Default manager for direct queries
reverseitems = CustomReverseManagerForItems() # Custom Manager for reverse queries
# Get Menu record named Breakfast
breakfast_menu = Menu.objects.get(name='Breakfast')
# Fetch all Item records in the Menu, using Item custom model manager for reverse queries
breakfast_menu.item_set(manager='reverseitems').all()
# Call on_sale_items() custom manager method in CustomReverseManagerForItems
breakfast_menu.item_set(manager='reverseitems').on_sale_items()
Listing 8-65.Django custom model manager for reverse query operations
from django import forms
class Contact(models.Model):
name = models.CharField(max_length=50,blank=True)
email = models.EmailField()
comment = models.CharField(max_length=1000)
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
fields = '__all__'
Listing 9-1.Django model class and model form
# views.py method to process model form
def contact(request):
if request.method == 'POST':
# POST, generate bound form with data from the request
form = ContactForm(request.POST)
# check if it's valid:
if form.is_valid():
# Insert into DB
form.save()
# redirect to a new URL:
return HttpResponseRedirect('/about/contact/thankyou')
else:
# GET, generate unbound (blank) form
form = ContactForm()
return render(request,'about/contact.html',{'form':form})
# See chapter 6 for form layout template syntax in about/contact.html
Listing 9-2.Django model form processing
在清单 9-2 中,你可以看到视图方法序列遵循与标准 Django 表单相同的模式。当用户在 view 方法上发出 GET 请求时,会创建一个未绑定表单实例,并发送给用户,并通过about/contact.html模板呈现。接下来,当用户通过 POST 请求提交表单时,使用request.POST参数创建一个绑定表单,然后使用is_valid()方法对其进行验证。如果表单值无效,则有错误的绑定表单被返回给用户,以便他可以纠正错误,如果表单值有效,在清单 9-2 的特定情况下,用户被重定向到/about/contact/thankyou页面。
from django import forms
def faq_suggestions(value):
# Validate value and raise forms.ValidationError for invalid values
pass
class Contact(models.Model):
name = models.CharField(max_length=50,blank=True)
email = models.EmailField()
comment = models.CharField()
class ContactForm(forms.ModelForm):
age = forms.IntegerField()
comment = forms.CharField(widget=forms.Textarea,validators=[faq_suggestions])
class Meta:
model = Contact
fields = '__all__'
Listing 9-3.Django model form with new and custom field
from django import forms
class Contact(models.Model):
name = models.CharField(max_length=50,blank=True)
email = models.EmailField()
comment = models.CharField()
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
fields = '__all__'
widgets = {
'name': models.CharField(max_length=25),
'comment': form.Textarea(attrs={'cols': 100, 'rows': 40})
}
labels = {
'name': 'Full name',
'comment': 'Issue'
}
help_texts = {
'comment': 'Provide a detailed account of the issue to receive a quick answer'
}
error_messages = {
'name': {
'max_length': "Name can only be 25 characters in length"
}
}
field_classes = {
'email': EmailCoffeehouseFormField
},
localized_fields = '__all__'
Listing 9-4.Django model form with meta options to override default form field behavior
ModelChoiceField和ModelMultipleChoiceField表单字段的好处是它们基于 Django 模型查询生成表单字段。因此,ModelChoiceField和ModelMultipleChoiceField表单域生成一个包含模型记录的友好的 HTML <select>/<option>输入域,而不是用模型数据手工填充表单域。
from django import forms
from coffeehouse.stores.models import Amenity
class Menu(models.Model):
name = models.CharField(max_length=30)
def __str__(self):
return "%s" % (self.name)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class ItemForm(forms.ModelForm):
menu = forms.ModelChoiceField(queryset=Menu.objects.filter(id=1))
class Meta:
model = Item
fields = '__all__'
class StoreForm(forms.Form):
name = forms.CharField()
address = forms.CharField()
amenities = forms.ModelMultipleChoiceField(queryset=None)
def __init__(self, *args, **kwargs):
super(StoreForm, self).__init__(*args, **kwargs)
self.fields['amenities'].queryset = Amenity.objects.filter(name__contains='W')
Listing 9-5.Django model form and standard form with custom query for ModelChoiceField and ModelMultipleChoiceField form fields
默认情况下,不定义initial值的ModelChoiceField()表单字段生成时会将空的 HTML <option>---------</option>选项作为默认字段值。可以通过empty_label选项自定义该空选项的值(例如empty_label='Please select a value',输出<option>Please select a value</option>)。也可以用empty_label=None禁用这个空选项。
默认情况下,ModelChoiceField()和ModelMultipleChoiceField()表单字段都从模型记录的主键值(即id)和模型__str__方法表示)生成它们的 HTML <select>/<option>输入字段值。例如,给定清单 9-5 中的Menu模型定义,该模型的 HTML <select>/<option>输入字段将如下所示:
可以使用to_field_name选项定制ModelChoiceField()和ModelMultipleChoiceField()表单字段中使用的<option> value。例如,在最后一个代码片段的上下文中设置to_field_name='name',将 HTML <select>/<option>输入字段更改为格式<option value="Breakfast">Breakfast</option>。
from django import forms
from django.forms import ModelChoiceField
class MenuModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return "Menu #%s) %s" % (obj.id,obj.name)
class ItemForm(forms.ModelForm):
menu = MenuModelChoiceField(queryset=Menu.objects.all())
class Meta:
model = Item
fields = '__all__'
# HTML menu form field output
<select name="menu" id="id_menu" required>
<option value="" selected>---------</option>
<option value="1">Menu #1) Breakfast</option>
<option value="2">Menu #2) Salads</option>
<option value="3">Menu #3) Sandwiches</option>
<option value="4">Menu #4) Drinks</option>
</select>
Listing 9-6.Django custom form field to customize <option> text for ModelChoiceField and ModelMultipleChoiceField form fields
from coffeehouse.items.models import Item
preloaded_item = Item.objects.get(id=1)
# Model form from Listing 9-6, initialize with instance
form = ItemForm(instance=preloaded_item)
# Unbound form set up with instance values
form.as_p()
<p>
<label for="id_menu">Menu:</label>
<select name="menu" required id="id_menu">
<option value="">---------</option>
<option value="1" selected>Menu #1) Breakfast</option>
<option value="2">Menu #2) Salads</option>
<option value="3">Menu #3) Sandwiches</option>
<option value="4">Menu #4) Drinks</option>
</select>
</p>
<p>
<label for="id_name">Name:</label>
<input type="text" name="name"
value="Whole-Grain Oatmeal" required maxlength="30" id="id_name" />
</p>
# Remaining fields committed for brevity
# Model form from Listing 9-6, initialize with instance and override with initial
form2 = ItemForm(initial={'menu':3},instance=preloaded_item)
# Unbound form set up with instance values
form2.as_p()
# Unbound form set up with instance values, but overridden with initial
form2.as_p()
<p>
<label for="id_menu">Menu:</label>
<select name="menu" required id="id_menu">
<option value="">---------</option>
<option value="1">Menu #1) Breakfast</option>
<option value="2">Menu #2) Salads</option>
<option value="3" selected>Menu #3) Sandwiches</option>
<option value="4">Menu #4) Drinks</option>
</select>
</p>
# Remaining fields committed for brevity
Listing 9-7.Django model form initialization with initial and instance
from django import forms
from django.conf import settings
class Contact(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, default=None)
name = models.CharField(max_length=50,blank=True)
email = models.EmailField()
comment = models.CharField()
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
exclude = ['user']
# Option 1) Form model processing with missing value assigned with instance
if form.is_valid():
# Check if user is available
if request.user.is_authenticated():
# Add missing user to model form
form.instance.user = request.user
# Insert into DB
form.save()
# Option 2) Form model processing with missing value assigned after model form sequence
if form.is_valid():
# Save instance but don't commit until model instance is complete
# form.save() returns a materialized model instance that has yet to be saved
pending_contact = form.save(commit=False)
# Check if user is available
if request.user.is_authenticated():
# Add missing user to model form
pending_contact.user = request.user
# Insert into DB
pending_contact.save()
Listing 9-8.Django model form with reduced form that requires model update before saving
# views.py
from django.views.generic.edit import CreateView
from .models import Item, ItemForm
from django.core.urlresolvers import reverse_lazy
class ItemCreation(CreateView):
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
# models.py
from django import forms
from django.db import models
class Menu(models.Model):
name = models.CharField(max_length=30)
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class ItemForm(forms.ModelForm):
class Meta:
model = Item
fields = '__all__'
widgets = {
'description': forms.Textarea(),
}
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^new/$', items_views.ItemCreation.as_view(), name='new'),
]
# templates/items/item_form.html
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Create</button>
</form>
Listing 9-9.Django class-based view with CreateView to create model records
那么,为什么这些课程如此重要呢?因为它们为所有基于类的视图提供了默认行为。清单 9-9 中的ItemCreation基于类的视图中声明的稀疏字段对于CreateView基于类的视图来说只有三个字段。可以声明十几个以上的字段和方法——属于这个过去的类列表——来提供行为,比如使用另一个模板而不是<app_name>/<model_name>_form.html;指定响应的内容类型(例如,text/csv);模型表单有效或无效时运行的自定义方法;以及声明自定义方法来手动执行 GET 和 POST 工作流逻辑。
# views.py
from django.views.generic.edit import CreateView
from django.http import HttpResponseRedirect
from django.contrib import messages
from .models import Item, ItemForm, Menu
class ItemCreation(CreateView):
initial = {'size':'L'}
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
def form_valid(self,form):
super(ItemCreation,self).form_valid(form)
# Add action to valid form phase
messages.success(self.request, 'Item created successfully!')
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self,form):
# Add action to invalid form phase
return self.render_to_response(self.get_context_data(form=form))
Listing 9-13.Django class-based view with CreateView with form_valid() and form_invalid()
# views.py
from django.views.generic.edit import CreateView
from django.shortcuts import render
from django.contrib import messages
class ItemCreation(CreateView):
initial = {'size':'L'}
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
template_name = "items/item_form.html"
def get(self,request, *args, **kwargs):
form = super(ItemCreation, self).get_form()
# Set initial values and custom widget
initial_base = self.get_initial()
initial_base['menu'] = Menu.objects.get(id=1)
form.initial = initial_base
form.fields['name'].widget = forms.widgets.Textarea()
# return response using standard render() method
return render(request,self.template_name,
{'form':form,
'special_context_variable':'My special context variable!!!'})
def post(self,request,*args, **kwargs):
form = self.get_form()
# Verify form is valid
if form.is_valid():
# Call parent form_valid to create model record object
super(ItemCreation,self).form_valid(form)
# Add custom success message
messages.success(request, 'Item created successfully!')
# Redirect to success page
return HttpResponseRedirect(self.get_success_url())
# Form is invalid
# Set object to None, since class-based view expects model record object
self.object = None
# Return class-based view form_invalid to generate form with errors
return self.form_invalid(form)
Listing 9-14.Django class-based view with CreateView with get() and post()
您可以在清单 9-14 中看到,get()和post()方法都可以访问一个request输入——一个HttpRequest实例——就像标准视图方法一样。这个request引用允许基于类的视图访问请求数据,例如 HTTP 元数据(例如,用户的 IP 地址),这是在第二章中详细描述的主题。
# views.py
from django.views.generic.list import ListView
from .models import Item
class ItemList(ListView):
model = Item
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^$',items_views.ItemList.as_view(),name="index"),
]
# templates/items/item_list.html
{% regroup object_list by menu as item_menu_list %}
{% for menu_section in item_menu_list %}
<li>{{ menu_section.grouper }}
<ul>
{% for item in menu_section.list %}
<li>{{item.name|title}}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
Listing 9-15.Django class-based view with ListView to read list of records
# views.py
from django.views.generic. import DetailView
from .models import Item
class ItemDetail(DetailView):
model = Item
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^(?P<pk>\d+)/$',items_views.ItemDetail.as_view(),name="detail"),
]
# templates/items/item_detail.html
<h4> {{item.name|title}}</h4>
<p>{{item.description}}</p>
<p>${{item.price}}</p>
<p>For {{item.get_size_display}} size: Only {{item.calories}} calories
{% if item.drink %}
and {{item.drink.caffeine}} mg of caffeine.</p>
{% endif %}
</p>
Listing 9-16.Django class-based view with DetailView to read model record
# views.py
from django.views.generic.list import ListView
from .models import Item
class ItemList(ListView):
model = Item
queryset = Item.objects.filter(menu__id=1)
ordering = ['name']
Listing 9-17.Django class-based view with ListView to reduce record list with queryset
# views.py
from django.views.generic.list import ListView
from .models import Item
class ItemList(ListView):
model = Item
paginate_by = 5
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^$',items_views.ItemList.as_view(),name="index"),
url(r'^page/(?P<page>\d+)/$',items_views.ItemList.as_view(),name="page"),
]
# templates/items/item_list.html
{% regroup object_list by menu as item_menu_list %}
{% for menu_section in item_menu_list %}
<li>{{ menu_section.grouper }}
<ul>
{% for item in menu_section.list %}
<li>{{item.name|title}}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
{% if is_paginated %}
{{page_obj}}
{% endif %}
Listing 9-18.Django class-based view with ListView to read list of records with pagination
# views.py
# views.py
from django.views.generic import DetailView
from .models import Item
class ItemDetail(DetailView):
model = Item
slug_field = 'name__iexact'
# urls.py
from django.conf.urls import url
from coffeehouse.items import views as items_views
urlpatterns = [
url(r'^(?P<slug>\w+)/$',items_views.ItemDetail.as_view(),name="detail"),
]
# Template identical to listing to template in Listing 9-16
Listing 9-19.Django class-based view with DetailView and slug_field option
如果在一个基于DeleteView类的视图上发出一个 HTTP GET 请求,那么一个带有这个伪表单的页面将返回给用户,向他提出问题Do you really want to delete "{{ object }}"?,其中object是要删除的模型记录。如果用户点击这个伪表单提交按钮,它会向同一个基于DeleteView类的视图发出一个 HTTP POST 请求——注意<form method="post">标记——调用实际的删除过程。以这种方式,这种具有伪形式的模板允许用户在点击 url 删除链接(例如/items/delete/1/)之后确认他是否真的想要删除记录,而不是在点击 url 删除链接时立即删除记录。
# views.py
from django.views.generic.edit import CreateView
from django.contrib.messages.views import SuccessMessageMixin
from .models import Item, ItemForm
class ItemCreation(SuccessMessageMixin,CreateView):
model = Item
form_class = ItemForm
success_url = reverse_lazy('items:index')
success_message = "Item %(name)s created successfully"
Listing 9-22.Django class-based view with CreateView and mixin class
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
class Meta:
default_permissions = ('add',)
permissions = (('give_refund','Can refund customers'),('can_hire','Can hire employees'))
Listing 10-4.Customize a Django model’s permissions with default_permissions and permissions
# Internal check to see if user is anonymous or not
def homepage(request):
if request.user.is_anonymous():
# Logic for AnonymousUser
else:
# Logic for User
# Method check to see if user is logged in or not (i.e. a User)
from django.contrib.auth.decorators import login_required
@login_required
def profile(request):
# Logic for profile
Listing 10-5.Permission check in view methods with internal checks and @login_required
# Method check to see if User belongs to group called 'Barista'
from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import Group
@user_passes_test(lambda u: Group.objects.get(name='Baristas') in u.groups.all())
def dashboard(request):
# Logic for dashboard
# Explicit method check, if User is authenticated and has permissions to change Store model
# Explicit method with test
def user_of_stores(user):
if user.is_authenticated() and user.has_perm("stores.change_store"):
return True
else:
return False
# Method check using method
@user_passes_test(user_of_stores)
def store_manager(request):
# Logic for store_manager
# Method check to see if User has permissions to add Store model
from django.contrib.auth.decorators import permission_required
@permission_required('stores.add_store')
def store_creator(request):
# Logic for store_creator
Listing 10-6.Permission check in view methods with @user_passes_test and @permission_required
清单 10-6 中的第一个例子使用了@user_passes_test装饰器并定义了一个内嵌测试。代码片段lambda u: Group.objects.get(name='Baristas') in u.groups.all()获取名为Baristas的Group模型记录,并检查请求用户是否属于这个组。如果请求用户不属于Baristas组,则测试失败,访问被拒绝;否则,允许用户运行视图方法。
{% if user.is_authenticated %}
{# Content for authenticated users #}
{% endif %}
{% if perms.stores.add_store %}
{# Content for users that can add stores #}
{% endif %}
{% for group in user.groups.all %}
{% if group.name == 'Baristas' %}
{# Content for users with 'Baristas' group #}
{% endif %}
{% endfor %}
Listing 10-9.Permission checks in templates
# urls.py main
from django.conf.urls import url
from django.contrib.auth import views
from coffeehouse.registration import views as registration_views
urlpatterns = [
url(r'^accounts/', include('django.contrib.auth.urls')),
url(r'^accounts/signup/$',registration_views.UserSignUp.as_view(),name="signup"),
]
Listing 10-12.Configure url for user sing up workflow
from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponseRedirect
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
class UserSignupForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ("username", "email", "password1", "password2")
class UserSignUp(SuccessMessageMixin,CreateView):
model = User
form_class = UserSignupForm
success_url = reverse_lazy('items:index')
success_message = "User created successfully"
template_name = "registration/signup.html"
def form_valid(self, form):
super(UserSignUp,self).form_valid(form)
# The form is valid, automatically sign-in the user
user = authenticate(self.request, username=form.cleaned_data['username'],
password=form.cleaned_data['password1'])
if user == None:
# User not validated for some reason, return standard form_valid() response
return self.render_to_response(self.get_context_data(form=form))
else:
# Log the user in
login(self.request, user)
# Redirect to success url
return HttpResponseRedirect(self.get_success_url())
Listing 10-13.Signup workflow fulfilled by custom CreateView class-based view
from django.contrib.auth.models import User
from django.db import models
class UserExtra(models.Model):
user = models.ForeignKey(User)
age = models.IntegerField(blank=True,null=True)
telephone = models.CharField(max_length=15,blank=True,null=True)
Listing 10-14.Model with extra user fields related to default Django user model
# models.py (app registration)
from django.contrib.auth.models import AbstractUser
from django.db import models
class CoffeehouseUser(AbstractUser):
age = models.IntegerField(blank=True,null=True)
telephone = models.CharField(max_length=15,blank=True,null=True)
# admin.py (app registration)
from django.contrib import admin
from .models import CoffeehouseUser
class CoffeehouseUserAdmin(admin.ModelAdmin):
pass
admin.site.register(CoffeehouseUser, CoffeehouseUserAdmin)
# settings.py
AUTH_USER_MODEL = 'registration.CoffeehouseUser'
Listing 10-15.Custom User model to override default Django User model
正如您在本章中所看到的,django.contrib.auth包提供了大量管理用户和组、权限以及认证工作流的功能。但是您可能也意识到了,django.contrib.auth包也可能会受到很多遗留行为的影响,这些行为在当今时代不适用于 web 应用。例如,django.contrib.auth不支持社交认证之类的东西——这几乎是当今互联网的一项要求——此外,django.contrib.auth还被设计为使用开箱即用的用户名,而不是电子邮件——这也是一种非常过时的做法。
# Ensure the 'django.contrib.sites' is declared in INSTALLED_APPS
# And also add the allauth, allauth.account and allauth.socialaccount to INSTALLED_APPS
INSTALLED_APPS = [
# Django sites app required
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
]
# Ensure SITE_ID is set sites app
SITE_ID = 1
# Add the 'allauth' backend to AUTHENTICATION_BACKEND and keep default ModelBackend
AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend']
# EMAIL_BACKEND so allauth can proceed to send confirmation emails
# ONLY for development/testing use console
EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend'
# Custom allauth settings
# Use email as the primary identifier
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
# Make email verification mandatory to avoid junk email accounts
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
# Eliminate need to provide username, as it's a very old practice
ACCOUNT_USERNAME_REQUIRED = False
Listing 10-17.Base Django allauth settings.py configuration
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: [coffeehouse.com] Please Confirm Your E-mail Address
From: webmaster@localhost
To: cashier@coffeehouse.com
Date: Wed, 12 Aug 2018 00:57:50 -0000
Message-ID: <20180812005750.15177.32621@laptop>
Hello from coffeehouse.com!
You're receiving this e-mail because user daniel at example.com has given yours as an e-mail address to connect their account.
To confirm this is correct, go to http://localhost:8000/accounts/confirm-email/1aflixsbb6sn14hptkntiyrgvk1r0pppqsurx6fxrs6wmlb96u8y3gotxzep0qie/
Thank you from coffeehouse.com!
coffeehouse.com
Listing 10-18.Confirm email for new user in django-allauth
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Subject: [coffeehouse.com] Password Reset E-mail
From: webmaster@localhost
To: nancy@coffeehouse.com
Date: Wed, 19 Aug 2018 03:05:09 -0000
Message-ID: <20180819030509.15780.98825@laptop>
Hello from coffeehouse.com!
You're receiving this e-mail because you or someone else has requested a password for your user account at coffeehouse.com.
It can be safely ignored if you did not request a password reset. Click the link below to reset your password.
http://localhost:8000/accounts/password/reset/key/5-44f-0f6fbf1251fd33ee4b40/
Thank you for using coffeehouse.com!
coffeehouse.com
-------------------------------------------------------------------------------
Listing 10-19.Reset email for new password django-allauth
要在 Django allauth 设置 Google social authentication,你需要一个 Google 帐户在他们的网站上创建一个应用。头转向 https://console.developers.google.com/ 。如果你以前创建了谷歌项目,你将登陆一个默认项目。虽然您可以在任何 Google 项目上创建社交认证工作流,但我建议您为此创建一个新项目。
from django.contrib import admin
from coffeehouse.stores.models import Store
class StoreAdmin(admin.ModelAdmin):
list_display = ['name','address','city','state']
admin.site.register(Store, StoreAdmin)
Listing 11-2.Django admin list_display option
在某些情况下,你可能想在 Django admin 中将 HTML 作为记录列表的一部分(例如,添加粗体标签或彩色标签)。要在这些情况下包含 HTML,您必须使用format_html方法,因为 Django admin 默认情况下会对所有 HTML 输出进行转义——因为它使用 Django 模板。清单 11-4 展示了format_html方法的使用。
# models.py
from django.db import models
from django.utils.html import format_html
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30,unique=True)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
def full_address(self):
return format_html('%s - <b>%s,%s</b>' % (self.address,self.city,self.state))
# admin.py
from django.contrib import admin
from coffeehouse.stores.models import Store
class StoreAdmin(admin.ModelAdmin):
list_display = ['name','full_address']
Listing 11-4.Django admin list_display option with callable and format_html
Django admin override display for empty values with empty_value_display
# Option 1 - Globally set empty values to ???
# settings.py
from django.contrib import admin
admin.site.empty_value_display = '???'
# Option 2 - Set all fields in a class to 'Unknown Item field'
# admin.py to show "Unknown Item field" instead of '-' for NULL values in all Item fields
# NOTE: Item model in items app
class ItemAdmin(admin.ModelAdmin):
list_display = ['menu','name','price']
empty_value_display = 'Unknown Item field'
admin.site.register(Item, ItemAdmin)
# Option 3 - Set individual field in a class to 'No known price'
class ItemAdmin(admin.ModelAdmin):
list_display = ['menu','name','price_view']
def price_view(self, obj):
return obj.price
price_view.empty_value_display = 'No known price'
Listing 11-5.Django admin empty_value_display option global, class, or field-level configuration
Django admin search box due to search_fields option
from django.contrib import admin
from coffeehouse.stores.models import Store
class StoreAdmin(admin.ModelAdmin):
search_fields = ['city','state']
admin.site.register(Store, StoreAdmin)
Listing 11-10.Django admin search_fields option
Django admin list_display option with ManyToManyField field
# models.py
from django.db import models
class Amenity(models.Model):
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class Store(models.Model):
name = models.CharField(max_length=30)
address = models.CharField(max_length=30)
city = models.CharField(max_length=30)
state = models.CharField(max_length=2)
email = models.EmailField()
amenities = models.ManyToManyField(Amenity,blank=True)
# admin.py
from django.contrib import admin
from coffeehouse.stores.models import Store
class StoreAdmin(admin.ModelAdmin):
list_display = ['name','address','city','state','list_of_amenities']
def list_of_amenities(self, obj):
return ("%s" % ','.join([amenity.name for amenity in obj.amenities.all()]))
list_of_amenities.short_description = 'Store amenities'
admin.site.register(Store, StoreAdmin)
Listing 11-12.Django admin list_display option with ManyToManyField field
Django admin readonly_fields option for Django admin forms
from django.utils.safestring import mark_safe
class StoreAdmin(admin.ModelAdmin):
fields = ['name','address',('city','state'),'email','custom_amenities_display']
readonly_fields = ['name','custom_amenities_display']
def custom_amenities_display(self, obj):
return mark_safe("Amenities can only be modified by special request, please contact the store manager at %s to create a request" % (obj.email,obj.email))
custom_amenities_display.short_description = "Amenities"
admin.site.register(Store, StoreAdmin)
Listing 11-17.Django admin readonly_fields option with callable for Django admin forms
from django.contrib import admin
from coffeehouse.items.models import Menu
class MenuAdmin(admin.ModelAdmin):
formfield_overrides = {
models.CharField: {'widget': forms.Textarea}
}
admin.site.register(Menu, MenuAdmin)
Listing 11-19.Django admin formfield_overrides option
for Django admin forms
清单 11-19 中的formfield_overrides选项告诉 Django 管理员为所有使用 CharField的模型字段使用forms.Textarea小部件——它生成一个标准的 HTML
图 11-32。
Django admin custom CharField field display in Django admin form using formfield_overrides
图 11-31。
Django admin default CharField field display in Django admin form
# admin.py (ForeignKey)
from django.contrib import admin
from coffeehouse.items.models import Item, Menu
class ItemInline(admin.TabularInline):
model = Item
class MenuAdmin(admin.ModelAdmin):
list_display = ['name']
inlines = [
ItemInline,
]
admin.site.register(Menu, MenuAdmin)
# admin.py (ManyToManyField)
from django.contrib import admin
from coffeehouse.stores.models import Store, Amenity
class StoreInline(admin.StackedInline):
model = Store.amenities.through
class AmenityAdmin(admin.ModelAdmin):
inlines = [
StoreInline,
]
admin.site.register(Amenity, AmenityAdmin)
Listing 11-21.Django admin inlines option
for ForeignKey and ManyToManyField field
from django.contrib import admin
from coffeehouse.items.models Item
class ItemAdmin(admin.ModelAdmin):
list_per_page = 5
class Media:
css = {
"screen": ("css/items/items.css",)
}
js = ("js/items/items.js",)
admin.site.register(Item, ItemAdmin)
Listing 11-23.Django admin class with Media class to define custom static resources
# models.py
from django.db import models
class Menu(models.Model):
name = models.CharField(max_length=30)
creator = models.CharField(max_length=100,default='Coffeehouse Chef')
class Item(models.Model):
menu = models.ForeignKey(Menu, on_delete=models.CASCADE)
name = models.CharField(max_length=30)
description = models.CharField(max_length=100)
class Meta:
permissions = (
('read_item','Can read item'),
)
# admin.py
from django.contrib import admin
from coffeehouse.items.models import Item
class ItemAdmin(admin.ModelAdmin):
list_per_page = 5
list_display = ['menu','name','menu_creator']
def get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser and request.user.has_perm('items.read_item'):
return [f.name for f in self.model._meta.fields]
return super(ItemAdmin, self).get_readonly_fields(
request, obj=obj
)
admin.site.register(Item, ItemAdmin)
Listing 11-25.Django model with custom read permission and Django admin class enforcing read permission
# admin.py (stores app)
from django.contrib import admin
from coffeehouse.stores.models import Store,Amenity
class StoreAdmin(admin.ModelAdmin):
search_fields = ['city','state']
# Default model registration on main Django admin
admin.site.register(Store, StoreAdmin)
# Model registration on custom Django admin named provideradmin
from coffeehouse.admin import provideradmin
provideradmin.register(Store, StoreAdmin)
# admin.py (items app)
from django.contrib import admin
from coffeehouse.items.models import Menu
class MenuAdmin(admin.ModelAdmin):
list_display = ['name','creator']
# Default model registration on main Django admin
admin.site.register(Menu, MenuAdmin)
# Model registration on custom Django admin named provideradmin
from coffeehouse.admin import employeeadmin
employeeadmin.register(Menu, MenuAdmin)
Listing 11-27.Django admin class registration on multiple Django admin sites
# urls.py (In stores app)
from coffeehouse.stores import views as stores_views
urlpatterns = [
url(r'^rest/$',stores_views.rest_store,name="rest_index"),
url(r'^(?P<store_id>\d+)/rest/$',stores_views.rest_store,name="rest_detail"),
]
# views.py (In stores app)
from django.http import HttpResponse
from coffeehouse.stores.models import Store
from django.core import serializers
def rest_store(request,store_id=None):
store_list = Store.objects.all()
if store_id:
store_list = store_list.filter(id=store_id)
if 'type' in request.GET and request.GET['type'] == 'xml':
serialized_stores = serializers.serialize('xml',store_list)
return HttpResponse(serialized_stores, content_type='application/xml')
else:
serialized_stores = serializers.serialize('json',store_list)
return HttpResponse(serialized_stores, content_type='application/json')
Listing 12-2.Standard view method as REST service with parameters and different output formats
# coffeehouse.stores.serializers.py file
from rest_framework import serializers
class StoreSerializer(serializers.Serializer):
name = serializers.CharField(max_length=200)
email = serializers.EmailField()
Listing 12-3.Serializer class based on Django REST framework
from rest_framework import serializers
from coffeehouse.stores.models import Store
class StoreSerializer(serializers.ModelSerializer):
class Meta:
model = Store
fields = '__all__'
Listing 12-5.Serializer class using Django model based on Django REST framework
from coffeehouse.stores.models import Store
from coffeehouse.stores.serializers import StoreSerializer
from rest_framework import generics
class StoreList(generics.ListCreateAPIView):
queryset = Store.objects.all()
serializer_class = StoreSerializer
Listing 12-8.Django mixed-in generic class views in Django REST framework
from coffeehouse.stores.models import Store
from coffeehouse.stores.serializers import StoreSerializer
from rest_framework import viewsets
class StoreViewSet(viewsets.ModelViewSet):
queryset = Store.objects.all()
serializer_class = StoreSerializer
Listing 12-9.Django viewset class in Django REST framework
如图 12-4 所示,REST 框架呈现了一个默认的 Api 根页面。您可以进一步导航到 Api 根页面下的其他 URL(即/stores/rest/)来执行与视图集相关联的其他 CRUD 操作(例如,对/stores/rest/stores/的 HTTP GET 请求以获取所有Store记录的列表,对/stores/rest/stores/1/的 HTTP GET 请求以获取带有id=1,的Store记录,或者对/stores/rest/stores/2/的 HTTP DELETE 请求以删除带有id=3的Store记录)。
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
Listing 12-11.Django REST framework set to restrict all services to authenticated users
from coffeehouse.stores.models import Store
from coffeehouse.stores.serializers import StoreSerializer
from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets
class StoreViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
queryset = Store.objects.all()
serializer_class = StoreSerializer
Listing 12-13.Django viewset class in Django REST framework and permission_classes field