{% raw %} # 第三章-表单和视图 *************** 在本章,我们学习以下内容: * 传递HttpRequest到表单 * 利用表单的save方法 * 上传图片 * 使用django-crispy-forms生成表单布局 * 过滤对象列表 * 管理分页列表 * 编写类视图 * 生成PDF文档 ## 引言 当数据库结构定在模型中时,我们需要有一些视图供用户输入数据,或者对用户显示数据。本章,我们会关注管理表单的视图,列表视图,以及生成可替换的输出而不仅仅是HTML。举个最简单的例子来说,我们将把模板和URL规则的创建的决定权下放给你。 ## 传递HttpRequest到表单 Django的每一个视图的第一个参数通常是命名为`request`的`HttpRequest`对象。它包含了请求的元数据,例如,当前语言编码,当前用户,当前cookie,或者是当前的session。默认,表单被用在视图中用来接受GET或者POST参数,文件,初始化数据,以及其他的参数,但却不是`HttpRequest`对象暗沟。某些情况下,特别是当你想要使用请求数据过滤出表单字段的选择,又或者是你想要处理像在表单中保存当前用户或者当前IP这样事情时,额外地传递地`HttpRequest`到表单会非常有用的。 在本方法中,我会向你展示一个在表单中某人可以选择一个用户并对此用户发送消息的例子。我们会传递`HttpRequest`对象到表单以暴露接受选项的当前用户:我们不想要任何人都可以给他们发送消息。 ## 预热 让我们来创建一个叫做`email_messages`的应用,并把它放到设置中的`INSTALLED_APPS`去。该app仅含有表单和视图而没有模型。 ## 具体做法 1. 添加一个新文件,`forms.py`,其中消息表单包含两个字段:接收人选项和消息文本。同时,该表单拥有一个初始化方法,它会接受`request`对象并对接收人的选项字段修改: ```python # -*- coding: UTF-8 -*- from django import forms from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User class MessageForm(forms.Form): recipient = forms.ModelChoiceField( label=_("Recipient"), queryset=User.objects.all(), required=True, ) message = forms.CharField( label=_("Message"), widget=forms.Textarea, required=True, ) def __init__(self, request, *args, **kwargs): super(MessageForm, self).__init__(*args, **kwargs) self.request = request self.fields['recipient'].queryset = self.fields['recipient'].queryset.exclude(pk=request.user.pk) ``` 2. 然后,利用`message_to_user`视图创建`views.py`以处理表单。如你所见,`request`对象作为第一个参数被传递到了表单: ```python #email_messages/views.py # -*- coding: UTF-8 -*- from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect from forms import MessageForm @login_required def message_to_user(request): if request.method == "POST": form = MessageForm(request, data=request.POST) if form.is_valid(): # do something with the form return redirect("message_to_user_done") else: form = MessageForm(request) return render(request, "email_messages/message_to_user.html", {'form:form'}) ``` ## 工作原理 在初始化方法中,我们拥有表现表单自身实例的`self`变量,然后是新近添加的`request`变量,在然后是位置参数(`*args`)和命名参数(`**kwargs`)。我们调用`super`构造方法传递所有的位置参数以及命名参数,这样表单就可以正确地初始化。接着,我们把`request`变量赋值到一个新的表单的`request`变量,以便之后在表单的其他方法中可以访问。再然后,我们修改接收人选项字段的`queryset`属性以便从request暴露当前用户。 在视图中,我们将`HttpRequest`作为两种场合下的第一个参数来传递:当表单第一次载入时,以及表单发布之后。 ## 参阅 表单的save方法的使用 ## 表单的save方法使用 为了让视图变得简洁和简单,最好的做法是移动表单数据的处理到表单本身,不论是否可行。常见的做法是利用一个可以保存数据的save方法来执行搜索,或者来完成其他的复杂的行为。我们利用save方法扩展之前方法中所定义的表单,save方法会发送电子邮件到所选择的接收人。 ## 预备开始 我们从定义在`传递HttpRequest到表单`这个例子开始。 ## 具体做法 执行以下两步: 1. 在应用的表单中导入函数以便发送邮件。然后添加save方法并尝试发送邮件到所选择的接收人,发生错误时静默: ```python #email_messages/forms.py # -*- coding: UTF-8 -*- from django import forms from django.utils.translation import ugettext, ugettext_lazy as _ from django.core.mail import send_mail from django.contrib.auth.models import User class MessageForm(forms.Form): recipient = forms.ModelChoiceField( label=_("Recipient"), queryset=User.objects.all(), required=True, ) message = forms.CharField( label=_("Message"), widget=forms.Textarea, required=True, ) def __init__(self, request, *args, **kwargs): super(MessageForm, self).__init__(*args, **kwargs) self.request = request self.fields['recipient'].queryset = \ self.fields['recipient'].queryset.\ exclude(pk=request.user.pk) def save(self): cleaned_data = self.cleaned_data send_mail( subject=ugettext("A message from %s") % \ self.request.user, message=cleaned_data['message'], from_email=self.request.user.email, recipient_list=[cleaned_data['recipient'].email], fail_silently=True, ) ``` 2. 最后,假如发送的数据有效,则在视图中调用save方法: ```python #email_messages/views.py # -*- coding: UTF-8 -*- from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect from forms import MessageForm @login_required def message_to_user(request): if request.method == "POST": form = MessageForm(request, data=request.POST) if form.is_valid(): form.save() return redirect("message_to_user_done") else: form = MessageForm(request) return render(request, "email_messages/message_to_user.html",{'form': form}) ``` ## 工作原理 首先让我们看下表单。save方法使用来自表单的清洁数据来读取接收人的电邮地址,以及电邮信息。电邮的发送人就是当前的request中的用户。假如电邮由于不正确的邮件服务器配置或者其他原因导致未能发送,那么错误也是静默的,即,不会在表单中抛出错误。 现在,我们来看下视图。当用户发送的表单有效时,表单的save方法会被调用,接着用户被重定向到成功页面。 ## 参阅 传递HttpRequest到表单 ## 上传图片 于此做法中,我们会看到处理图片上传的最简单的办法。你会见到一个应用中访客可以上传励志名言图片的例子。 ## 预热 首先,让我们来创建一个应用`quotes`,并把它放到设置中的`INSTALLED_APPS`。然后,我们添加一个拥有三个字段的`InspirationalQuote`模型:作者,名言内容,以及图片,一如下面所示: ```python #quotes/models.py #-*- coding:utf-8 -*- import os from django.db import models from django.utils.timezone import now as timezone_now from django.utils.translation impot ugetext_lazy as _ def upload_to(instance, filename): now = timezone_now() filename_base, filename_ext = os.path.splitext(filename) return 'quotes{}{}'.format(now.strftime("%Y/%m/%Y%m%d%H%M%S"), filename_ext.lower(),) class InspirationalQuote(models.Model): author = models.CharField(_("Author"), max_length=200) quote = models.TextField(_("Quote")) picture = mdoels.ImageField(_("Picture"), upload_to=upload_to, blank=True, null=True, ) class Meta: verbose_name = _("Inspirational Quote") verbose_name_plural = _("Inspiration Quotes") def __unicode__(self): return self.quote ``` 此外,我们创建了一个函数`upload_to`,该函数设置类似`quotes/2014/04/20140424140000`这样的图片上传路径。你也看到了,我们使用日期时间戳作为文件名以确保文件的唯一性。我们传递该函数到`picture`图片字段。 ## 具体做法 创建`forms.py`文件,并于其中编写一个简单的模型表单: ```python #quotes/forms.py #-*- coding:utf-8 -*- from django import forms from models import InspirationQuote class InspirationQuoteForm(forms.ModelForm): class Meta: model = InspirationQuote ``` 在`views.py`文件中写入一个视图以处理表单。不要忘了传递类字典对象`FILES`到表单。如下,当表单有效时便会触发`save`方法: ```python #quotes/views.py # -*- coding: UTF-8 -*- from django.shortcuts import redirect from django.shortcuts import render from forms import InspirationQuoteForm def add_quote(request): if request.method == 'POST': form = InspirationQuoteForm( data=request.POST, files=request.FIELS, ) if form.is_valid(): quote = form.save() return redirect("add_quote_done") else: form = InspirationQuoteForm() return render(request, "quotes/change_quote.html", {'form': form}) ``` 最后,在`templates/quotes/change_quote.html`中给视图创建一个模板。为HTML表单设置`enctype`属性为`“multipart/form-data”`十分的重要,否则文件上传不会起作用的: ```python {% extends "base.html" %} {% load i18n %} {% block content %} <form method="post" action="" enctype="multipart/form-data"> {% csrf_token %} {{ form.as_p }} <button type="submit">{% trans "Save" %} </button> </form> {% endblock %} ``` ## 工作原理 Django的模型表单通过模型生成的表单。它们提供了所有的模型字段,因此你不要重复定义这些字段。前面的例子中,我们给`InspirationQuote`模型生成了一个模型表单。当我们保存表单时,表单知道如何包每个字段都保存到数据中去,也知道如何上传图片并在媒体目录中保存这些图片。 ## 还有更多 作为奖励,我会想你演示一个如何从已经上传的图片中生成一个缩略图。利用这个技术,你也可以生成多个其他图片特定的版本,不如,列表版本,移动设备版本,桌面电脑版本。 我们添加三个方法到模型`InspirationQuote`(quotes/modles.py)。它们是save,create_thumbnail,和get_thumbnail_picture_url。当模型被保存时,我们就触发了缩略图的创建。如下,当我们需要在一个模板中显示缩略图时,我们可以通过使用`{{ quote.get_thumbnail_picture_url }}`来获取图片的URL: ```python #quotes/models.py class InspirationQuote(models.Model): # ... def save(self, *args, **kwargs): super(InspirationQuote, self).save(*args, **kwargs) # 生成缩略图 self.create_thumbnail() def create_thumbnail(self): from django.core.files.storage import default_storage as \ storage if not self.picture: return "" file_path = self.picture.name filename_base, filename_ext = os.path.splitext(file_path) thumbnail_file_path = "%s_thumbnail.jpg" % filename_base if storage.exists(thumbnail_file_path): # if thumbnail version exists, return its url path # 如果缩略图存在,则返回缩略图的url路径 return "exists" try: # resize the original image and # return URL path of the thumbnail version # 改变原始图片的大小并返回缩略图的URL路径 f = storage.open(file_path, 'r') image = Image.open(f) width, height = image.size thumbnail_size = 50, 50 if width > height: delta = width - height left = int(delta/2) upper = 0 right = height + left lower = height else: delta = height - width left = 0 upper = int(delta/2) right = width lower = width + upper image = image.crop((left, upper, right, lower)) image = image.resize(thumbnail_size, Image.ANTIALIAS) f_mob = storage.open(thumbnail_file_path, "w") image.save(f_mob, "JPEG") f_mob.close() return "success" except: return "error" def get_thumbnail_picture_url(self): from PIL import Image from django.core.files.storage import default_storage as \ storage if not self.picture: return "" file_path = self.picture.name filename_base, filename_ext = os.path.splitext(file_path) thumbnail_file_path = "%s_thumbnail.jpg" % filename_base if storage.exists(thumbnail_file_path): # if thumbnail version exists, return its URL path # 如果缩略图存在,则返回图片的URL路径 return storage.url(thumbnail_file_path) # return original as a fallback # 返回一个图片的原始url路径 return self.picture.url ``` 之前的方法中,我们使用文件存储API而不是直接地与应对文件系统,因为我们之后可以使用亚马逊的云平台,或者其他的存储服务和方法来与默认的存储切换也可以正常工作。 缩略图的创建具体是如何工作的?如果原始图片保存为`quotes/2014/04/20140424140000.png`,我们 ## 参见 使用django-crispy-forms创建表单布局 ## 使用django-crispy-forms创建表单布局 Django的应用,`django-crispy-forms`允许你使用下面的CSS框架构建,定制,重复使用表单:`Uni-Form`, `Bootstrap`, 或者`Foundation`。django-crispy-form的用法类似于Django自带管理中的字段集合,而且它更高级,更富于定制化。按照Python代码定义表单布局,而且你不需要太过关心HTML中的每个字段。假如你需要添加指定的HTML属性或者指定的外观,你仍旧可以轻松地做到。 这个方法中,我们会向你演示一个如何使用拥有Bootstrap 3——它是开发响应式,移动设备优先的web项目的最流行的前端框架——的django-crispy-forms。 ## 预热 我们一步接一步地执行这些步骤: 1.从http://getbootstrap.com/下载前端框架Bootstrap并将CSS和JavaScript集成到模板。更多内容详见第四章-模板和Javascript中的`管理base.html模板`。 2.使用下面的命令在虚拟环境中安装django-crispy-forms: ```python (myproject_env)$ pip install django-crispy-forms ``` 3.确保csirpy-form添加到了`INSTALLED_APPS`,然后在该项目中设置`bootstrap3`作为模板包来使用: ```python #settings.py INSTALLED_APPS = ( # ... "crispy_forms", ) # ... CRISPY_TEMPLATE_PACK = "bootstrap3" ``` 4.让我们来创建一个应用`bulletin_board`来阐明django-crispy-forms的用法,并把应用添加到设置中的`INSTALLED_APPS`。我们会拥有一个含有这些字段的`Bulletin`模型:类型,名称,描述,联系人,电话,电邮,以及图片: ```python #bulletin_board/models.py # -*- coding: UTF-8 -*- from django.db import models from django.utils.translation import ugettext_lazy as _ TYPE_CHOICES = ( ('searching', _("Searching")), ('offering', _("Offering")), ) class Bulletin(models.Model): bulletin_type = models.CharField(_("Type"), max_length=20, choices=TYPE_CHOICES) title = models.CharField(_("Title"), max_length=255) description = models.TextField(_("Description"),max_length=300) contact_person = models.CharField(_("Contact person"), max_length=255) phone = models.CharField(_("Phone"), max_length=200, blank=True) email = models.EmailField(_("Email"), blank=True) image = models.ImageField(_("Image"), max_length=255, upload_to="bulletin_board/", blank=True) class Meta: verbose_name = _("Bulletin") verbose_name_plural = _("Bulletins") ordering = ("title",) def __unicode__(self): return self.title ``` ## 具体做法 Let's add a model form for the bulletin in the newly created app. We will attach a form helper to the form itself in the initialization method. The form helper will have the layout property, which will define the layout for the form, as follows: ```python #bulletin_board/forms.py # -*- coding: UTF-8 -*- from django import forms from django.utils.translation import ugettext_lazy as _, ugettext from crispy_forms.helper import FormHelper from crispy_forms import layout, bootstrap from models import Bulletin class BulletinForm(forms.ModelForm): class Meta: model = Bulletin fields = ['bulletin_type', 'title', 'description', 'contact_person', 'phone', 'email', 'image'] def __init__(self, *args, **kwargs): super(BulletinForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_action = "" self.helper.form_method = "POST" self.fields['bulletin_type'].widget = forms.RadioSelect() # delete empty choice for the type del self.fields['bulletin_type'].choices[0] self.helper.layout = layout.Layout( layout.Fieldset( _("Main data"), layout.Field("bulletin_type"), layout.Field("title", css_class="input-block-level"), layout.Field("description", css_class="input-blocklevel", rows="3"), ), layout.Fieldset( _("Image"), layout.Field("image", css_class="input-block-level"), layout.HTML(u"""{% load i18n %} <p class="help-block">{% trans "Available formats are JPG, GIF, and PNG. Minimal “size is 800 × 800 px." %}</p> """), title=_("Image upload"), css_id="image_fieldset", ), layout.Fieldset( _("Contact"), layout.Field("contact_person", css_class="input-blocklevel"), layout.Div( bootstrap.PrependedText("phone", """<span class="glyphicon glyphicon-earphone"></span>""", css_class="inputblock-level"), bootstrap.PrependedText("email", "@", css_class="input-block-level", placeholder="contact@example.com"), css_id="contact_info", ), ), bootstrap.FormActions( layout.Submit('submit', _('Save')), ) ) ``` 要渲染模板中的表单,如下,我们只需载入标签冷酷`crispy_forms_tags`,然后使用模板标签`{% crispy %}`: ```python #templates/bulletin_board/change_form.html} {% extends "base.html" %} {% load crispy_forms_tags %} {% block content %} {% crispy form %} {% endblock %} ``` ## 工作原理 拥有新闻简报表的页面的样子大概如此: 图片:略 As you see, the fields are grouped by fieldsets. The first argument of the Fieldset object defines the legend, the other positional arguments define fields. You can also pass named arguments to define HTML attributes for the fieldset; for example, for the second fieldset, we are passing title and css_id to set the HTML attributes title and id. 如你所见,字段是由字段集合组成的。 Fields can also have additional attributes passed by named arguments, for example, for the description field, we are passing css_class and rows to set the HTML attributes class and rows. 字段也可以通过传递命名参数拥有额外的属性,例如, Besides the normal fields, you can pass HTML snippets as this is done with the help block for the image field. You can also have prepended-text fields in the layout, for example, we added a phone icon to the phone field, and an @ sign for the email field. As you see from the example with contact fields, we can easily wrap fields into HTML <div> elements using Div objects. This is useful when specific JavaScript needs to be applied to some form fields. 除了常规字段,你可以传递HTML片段 The action attribute for the HTML form is defined by the `form_action` property of the form helper. The method attribute of the HTML form is defined by the form_method property of the form helper. Finally, there is a Submit object to render the submit button, which takes the name of the button as the first positional argument, and the value of the button as the second argument. ## 还有更多 For the basic usage, the given example is more than necessary. However, if you need specific markup for forms in your project, you can still overwrite and modify templates of the `django-crispy-forms` app, as there is no markup hardcoded in Python files, but rather all the generated markup is rendered through the templates. Just copy the templates from the django-crispy-forms app to your project's template directory and change them as you need. 为了说明基本用法,给出例子是很有必要的一件事。不过,加入你需要在项目中为表单指定装饰,你仍然可以重写并修改`django-crispy-forms`这个应用的模板,在Python文件中不仅不存在由装饰的硬编码。 ## 参阅 * *过滤对象列表* * *管理分页对象* ## 过滤对象列表 在web开发中,除了视图和表单,拥有对象列表视图和详细视图是很典型的情况。列表视图可以简单的排列对象的顺序,例如,按找首字母排序或者创建日期来调用,不过对于非常庞大的数据来说就不是那么的友好了。 ## 预备工作 For the filtering example, we will use the Movie model with relations to genres, directors, and actors to filter by. It will also be possible to filter by ratings, which is PositiveIntegerField with choices. Let's create the movies app, put it into INSTALLED_APPS in the settings (movies/models.py), and define the mentioned models in the new app: 为了说明过滤例子,我们会使用关联了种类、导演与演员的Moive模型进行过滤。而且通过评级过滤也是可以的,这是一个含有PositiveIntegerField的多选列表。我们来创建应用movies,并将它放到设置文件中的INSTALLED_APPS,然后在这个新应用中定义前面提及的模型: ```python #movies/models.py # -*- coding: UTF-8 -*- from django.db import models from django.utils.translation import ugettext_lazy as _ RATING_CHOICES = ( (1, u"✩"), (2, u"✩✩"), (3, u"✩✩✩"), (4, u"✩✩✩✩"), (5, u"✩✩✩✩✩"), ) class Genre(models.Model): title = models.CharField(_("Title"), max_length=100) def __unicode__(self): return self.title class Director(models.Model): first_name = models.CharField(_("First name"), max_length=40) last_name = models.CharField(_("Last name"), max_length=40) def __unicode__(self): return self.first_name + " " + self.last_name class Actor(models.Model): first_name = models.CharField(_("First name"), max_length=40) last_name = models.CharField(_("Last name"), max_length=40) def __unicode__(self): return self.first_name + " " + self.last_name class Movie(models.Model): title = models.CharField(_("Title"), max_length=255) genres = models.ManyToManyField(Genre, blank=True) directors = models.ManyToManyField(Director, blank=True) actors = models.ManyToManyField(Actor, blank=True) rating = models.PositiveIntegerField(choices=RATING_CHOICES) def __unicode__(self): return self.title ``` ## 具体做法 首先,我们创建能够尽可能过滤所有目录的`MovieFilterForm`: ```python #movies/forms.py # -*- coding: UTF-8 -*- from django import forms from django.utils.translation import ugettext_lazy as _ from models import Genre from models import Director from models import Actor from models import RATING_CHOICES class MovieFilterForm(forms.Form): genre = forms.ModelChoiceField( label=_("Genre"), required=False, queryset=Genre.objects.all(), ) director = forms.ModelChoiceField( label=_("Director"), required=False, queryset=Director.objects.all(), ) actor = forms.ModelChoiceField( label=_("Actor"), required=False, queryset=Actor.objects.all(), ) rating = forms.ChoiceField( label=_("Rating"), required=False, choices=RATING_CHOICES, ) ``` Then, we create a `movie_list` view that will use MovieFilterForm to validate the request query parameters and do the filtering by chosen categories. Note the facets dictionary, which is used here to list the categories and also the currently selected choices: 然后,我们创建一个使用MovieFilterForm验证请求查询参数的视图`movie_list`,再然后通过所选择的种类进行过滤。要注意字典这一方面,这里改字典用来列出目录以及当前选定的选项: ```python #movies/views.py # -*- coding: UTF-8 -*- from django.shortcuts import render from models import Genre from models import Director from models import Actor from models import Movie, RATING_CHOICES from forms import MovieFilterForm def movie_list(request): qs = Movie.objects.order_by('title') form = MovieFilterForm(data=request.REQUEST) facets = { 'selected': {}, 'categories': { 'genres': Genre.objects.all(), 'directors': Director.objects.all(), 'actors': Actor.objects.all(), 'ratings': RATING_CHOICES, }, } if form.is_valid(): genre = form.cleaned_data['genre'] if genre: facets['selected']['genre'] = genre qs = qs.filter(genres=genre).distinct() director = form.cleaned_data['director'] if director: facets['selected']['director'] = director qs = qs.filter(directors=director).distinct() actor = form.cleaned_data['actor'] if actor: facets['selected']['actor'] = actor qs = qs.filter(actors=actor).distinct() rating = form.cleaned_data['rating'] if rating: facets['selected']['rating'] = (int(rating), dict(RATING_CHOICES)[int(rating)]) qs = qs.filter(rating=rating).distinct() context = { 'form': form, 'facets': facets, 'object_list': qs, } return render(request, "movies/movie_list.html", context) ``` Lastly, we create the template for the list view. We will use the facets dictionary here to list the categories and to know which category is currently selected. To generate URLs for the filters, we will use the `{% append_to_query %}` template tag, which will be described later in the Creating a template tag to modify request query parameters recipe in Chapter 5, Custom Template Filters and Tags. Copy the following code in the templates/movies/movie_list.html directory: 最后,我们为列表视图创建模板。我们会使用 ```python {#termplates/movies/movie_list.html} {% extends "base_two_columns.html" %} {% load i18n utility_tags %} {% block sidebar %} <div class="filters"> <h6>{% trans "Filter by Genre" %}</h6> <div class="list-group"> <a class="list-group-item{% if not facets.selected.genre %} active{% endif %}" href="{% append_to_query genre="" page="" %}">{% trans "All" %}</a> {% for cat in facets.categories.genres %} <a class="list-group-item{% if facets.selected.genre == cat %} active{% endif %}" href="{% append_to_query genre=cat.pk page="" %}">{{ cat }}</a> {% endfor %} </div> <h6>{% trans "Filter by Director" %}</h6> <div class="list-group"> <a class="list-group-item{% if not facets.selected.director %} active{% endif %}" href="{% append_to_query director="" page="" %}">{% trans "All" %}</a> {% for cat in facets.categories.directors %} <a class="list-group-item{% if facets.selected.director == cat %} active{% endif %}" href="{% append_to_query director=cat.pk page="" %}">{{ cat }}</a> {% endfor %} </div> <h6>{% trans "Filter by Actor" %}</h6> <div class="list-group"> <a class="list-group-item{% if not facets.selected.actor %} active{% endif %}" href="{% append_to_query actor="" page="" %}">{% trans "All" %}</a> {% for cat in facets.categories.actors %} <a class="list-group-item{% if facets.selected.actor == cat %} active{% endif %}" href="{% append_to_query actor=cat.pk page="" %}">{{ cat }}</a> {% endfor %} </div> <h6>{% trans "Filter by Rating" %}</h6> <div class="list-group"> <a class="list-group-item{% if not facets.selected.rating %} active{% endif %}" href="{% append_to_query rating="" page="" %}">{% trans "All"%}</a> {% for r_val, r_display in facets.categories.ratings %} <a class="list-group-item{% if facets.selected.rating.0 == r_val %} active{% endif %}" href="{% append_to_query rating=r_val page="" %}">{{ r_display }}</a> {% endfor %} </div> </div> {% endblock %} {% block content %} <div class="movie_list"> {% for movie in object_list %} <div class="movie"> <h3>{{ movie.title }}</h3> </div> {% endfor %} </div> {% endblock %} ``` ## 工作原理 If we use the Bootstrap 3 frontend framework, the end result will look like this in the browser with some filters applied: 图片:略 So, we are using the facets dictionary that is passed to the template context, to know what filters we have and which filters are selected. To look deeper, the facets dictionary consists of two sections: the categories dictionary and the selected dictionary. The categories dictionary contains the QuerySets or choices of all filterable categories. The selected dictionary contains the currently selected values for each category. In the view, we check if the query parameters are valid in the form and then we drill down the QuerySet of objects by the selected categories. Additionally, we set the selected values to the facets dictionary, which will be passed to the template. In the template, for each categorization from the facets dictionary, we list all categories and mark the currently selected category as active. It is as simple as that. ## 参阅 *The Managing paginated lists recipe* *The Composing class-based views recipe* *The Creating a template tag to modify request query parameters recipe in Chapter 5, Custom Template Filters and Tags* ## 管理分页列表 If you have dynamically changing lists of objects or when the amount of them can be greater than 30-50, you surely need pagination for the list. With pagination instead of the full QuerySet, you provide a fraction of the dataset limited to a specific amount per page and you also show the links to get to the other pages of the list. Django has classes to manage paginated data, and in this recipe, I will show you how to do that for the example from the previous recipe. ## 预热 Let's start with the movies app and the forms as well as the views from the Filtering object lists recipe. ## 具体做法 At first, import the necessary pagination classes from Django. We will add pagination management to the `movie_list` view just after filtering. Also, we will slightly modify the context dictionary by passing a page instead of the movie QuerySet as `object_list`: 首先,从Django导入必需的分页类。我们会在过滤之后将分页管理添加到`movie_list`。而且,我们也要传递一个页面而不是movie的查询集合`object_list`来稍微修改上下文字典。 ```python #movies/views.py # -*- coding: UTF-8 -*- from django.shortcuts import render from django.core.paginator import Paginator, EmptyPage,\ PageNotAnInteger from models import Movie from forms import MovieFilterForm def movie_list(request): qs = Movie.objects.order_by('title') # ... filtering goes here... paginator = Paginator(qs, 15) page_number = request.GET.get('page') try: page = paginator.page(page_number) except PageNotAnInteger: # If page is not an integer, show first page. page = paginator.page(1) except EmptyPage: # If page is out of range, show last existing page. page = paginator.page(paginator.num_pages) context = { 'form': form, 'object_list': page, } return render(request, "movies/movie_list.html", context) ``` In the template, we will add pagination controls after the list of movies as follows: ```python {#templates/movies/movie_list.html#} {% extends "base.html" %} {% load i18n utility_tags %} {% block sidebar %} {# ... filters go here... #} {% endblock %} {% block content %} <div class="movie_list"> {% for movie in object_list %} <div class="movie alert alert-info"> <p>{{ movie.title }}</p> </div> {% endfor %} </div> {% if object_list.has_other_pages %} <ul class="pagination"> {% if object_list.has_previous %} <li><a href="{% append_to_query page=object_list.previous_page_number %}">&laquo;</a></li> {% else %} <li class="disabled"><span>&laquo;</span></li> {% endif %} {% for page_number in object_list.paginator.page_range %} {% if page_number == object_list.number %} <li class="active"> <span>{{ page_number }} <span class="sr-only">(current)</span></span> </li> {% else %} <li> <a href="{% append_to_query page=page_number %}">{{ page_number }}</a> </li> {% endif %} {% endfor %} {% if object_list.has_next %} <li><a href="{% append_to_query page=object_list.next_page_number %}">&raquo;</a></li> {% else %} <li class="disabled"><span>&raquo;</span></li> {% endif %} </ul> {% endif %} {% endblock %} ``` ## 工作原理 When you look at the results in the browser, you will see pagination controls like these, added after the list of movies: 图片:略 How do we achieve that? When the QuerySet is filtered out, we create a paginator object passing the QuerySet and the maximal amount of items we want to show per page (which is 15 here). Then, we read the current page number from the query parameter, page. The next step is retrieving the current page object from the paginator. If the page number was not an integer, we get the first page. If the number exceeds the amount of possible pages, the last page is retrieved. The page object has methods and attributes necessary for the pagination widget shown in the preceding screenshot. Also, the page object acts like a QuerySet, so that we can iterate through it and get the items from the fraction of the page. The snippet marked in the template creates a pagination widget with the markup for the Bootstrap 3 frontend framework. We show the pagination controls only if there are more pages than the current one. We have the links to the previous and next pages, and the list of all page numbers in the widget. The current page number is marked as active. To generate URLs for the links, we are using the template tag {% append_to_query %}, which will be described later in the Creating a template tag to modify request query parameters recipe in Chapter 5, Custom Template Filters and Tags. ## 参阅 The Filtering object lists recipe The Composing class-based views recipe The Creating a template tag to modify request query parameters recipe in Chapter 5, Custom Template Filters and Tags ## 编写类视图 Django views are callables that take requests and return responses. In addition to function-based views, Django provides an alternative way to define views as classes. This approach is useful when you want to create reusable modular views or when you want to combine views out of generic mixins. In this recipe, we will convert the previously shown function-based view, movie_list, into a class-based view, MovieListView. ## 预热 Create the models, the form, and the template like in the previous recipes, Filtering object lists and Managing paginated lists. ## 具体做法 We will need to create a URL rule in the URL configuration and add a class-based view. To include a class-based view in the URL rules, the as_view()method is used like this: ```python #movies/urls.py # -*- coding: UTF-8 -*- from django.conf.urls import patterns, url from views import MovieListView urlpatterns = patterns('', url(r'^$', MovieListView.as_view(), name="movie_list"), ) ``` Our class-based view, MovieListView, will overwrite the get and post methods of the View class, which are used to distinguish between requests by GET and POST. We will also add the get_queryset_and_facets and get_page methods to make the class more modular: ```python #movies/views.py # -*- coding: UTF-8 -*- from django.shortcuts import render from django.core.paginator import Paginator, EmptyPage,\ PageNotAnInteger from django.views.generic import View from models import Genre from models import Director from models import Actor from models import Movie, RATING_CHOICES from forms import MovieFilterForm class MovieListView(View): form_class = MovieFilterForm template_name = 'movies/movie_list.html' paginate_by = 15 def get(self, request, *args, **kwargs): form = self.form_class(data=request.REQUEST) qs, facets = self.get_queryset_and_facets(form) page = self.get_page(request, qs) context = { 'form': form, 'facets': facets, 'object_list': page, } return render(request, self.template_name, context) def post(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def get_queryset_and_facets(self, form): qs = Movie.objects.order_by('title') facets = { 'selected': {}, 'categories': { 'genres': Genre.objects.all(), 'directors': Director.objects.all(), 'actors': Actor.objects.all(), 'ratings': RATING_CHOICES, }, } if form.is_valid(): genre = form.cleaned_data['genre'] if genre: facets['selected']['genre'] = genre qs = qs.filter(genres=genre).distinct() director = form.cleaned_data['director'] if director: facets['selected']['director'] = director qs = qs.filter(directors=director).distinct() actor = form.cleaned_data['actor'] if actor: facets['selected']['actor'] = actor qs = qs.filter(actors=actor).distinct() rating = form.cleaned_data['rating'] if rating: facets['selected']['rating'] = (int(rating), dict(RATING_CHOICES)[int(rating)]) qs = qs.filter(rating=rating).distinct() return qs, facets def get_page(self, request, qs): paginator = Paginator(qs, self.paginate_by) page_number = request.GET.get('page') try: page = paginator.page(page_number) except PageNotAnInteger: # If page is not an integer, show first page. page = paginator.page(1) except EmptyPage: # If page is out of range, show last existing page. page = paginator.page(paginator.num_pages) return page ``` ## 工作原理 No matter whether the request was called by the GET or POST methods, we want the view to act the same; so, the post method is just calling the get method in this view, passing all positional and named arguments. These are the things happening in the get method: ``` At first, we create the form object passing the REQUEST dictionary-like object to it. The REQUEST object contains all the query variables passed using the GET or POST methods. Then, the form is passed to the get_queryset_and_facets method, which respectively returns a tuple of two elements: the QuerySet and the facets dictionary. Then, the current request object and the QuerySet is passed to the get_page method, which returns the current page object. Lastly, we create a context dictionary and render the response. ``` ## 还有更多 As you see, the get, post, and get_page methods are quite generic, so that we could create a generic class, FilterableListView, with those methods in the utils app. Then in any app, which needs a filterable list, we could create a view that extends FilterableListView and defines only the form_class and template_name attributes and the get_queryset_and_facets method. This is how class-based views work. ## 参阅 The Filtering object lists recipe The Managing paginated lists recipe ## 生成PDF文档 Django views allow you to create much more than just HTML pages. You can generate files of any type. For example, you can create PDF documents for invoices, tickets, booking confirmations, or some other purposes. In this recipe, we will show you how to generate resumes (curriculum vitae) in PDF format out of the data from the database. We will be using the Pisa xhtml2pdf library, which is very practical as it allows you to use HTML templates to make PDF documents. ## 预热 First of all, we need to install the Python libraries reportlab and xhtml2pdf in your virtual environment: ```python (myproject_env)$ pip install reportlab==2.4 (myproject_env)$ pip install xhtml2pdf ``` Then, let us create a cv app with a simple CV model with the Experience model attached to it through a foreign key. The CV model will have these fields: first name, last name, and e-mail. The Experience model will have these fields: the start date at a job, the end date at a job, company, position at that company, and skills gained: ```python #cv/models.py # -*- coding: UTF-8 -*- from django.db import models from django.utils.translation import ugettext_lazy as _ class CV(models.Model): first_name = models.CharField(_("First name"), max_length=40) last_name = models.CharField(_("Last name"), max_length=40) email = models.EmailField(_("Email")) def __unicode__(self): return self.first_name + " " + self.last_name class Experience(models.Model): cv = models.ForeignKey(CV) from_date = models.DateField(_("From")) till_date = models.DateField(_("Till"), null=True, blank=True) company = models.CharField(_("Company"), max_length=100) position = models.CharField(_("Position"), max_length=100) skills = models.TextField(_("Skills gained"), blank=True) def __unicode__(self): till = _("present") if self.till_date: till = self.till_date.strftime('%m/%Y') return _("%(from)s-%(till)s %(position)s at %(company)s") % { 'from': self.from_date.strftime('%m/%Y'), 'till': till,'position': self.position, 'company': self.company, } class Meta: ordering = ("-from_date",) ``` ## 工作原理 In the URL rules, let us create a rule for the view to download a PDF document of a resume by the ID of the CV model, as follows: ```python #cv/urls.py # -*- coding: UTF-8 -*- from django.conf.urls import patterns, url urlpatterns = patterns('cv.views', url(r'^(?P<cv_id>\d+)/pdf/$', 'download_cv_pdf', name='download_cv_pdf'), ) ``` Now let us create the download_cv_pdf view. This view renders an HTML template and then passes it to the PDF creator pisaDocument: ```python #cv/views.py # -*- coding: UTF-8 -*- try: from cStringIO import StringIO except ImportError: from StringIO import StringIO from xhtml2pdf import pisa from django.conf import settings from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.http import HttpResponse from cv.models import CV def download_cv_pdf(request, cv_id): cv = get_object_or_404(CV, pk=cv_id) response = HttpResponse(mimetype='application/pdf') response['Content-Disposition'] = 'attachment; ' \ 'filename=%s_%s.pdf' % (cv.first_name, cv.last_name) html = render_to_string("cv/cv_pdf.html", { 'cv': cv, 'MEDIA_ROOT': settings.MEDIA_ROOT, 'STATIC_ROOT': settings.STATIC_ROOT, }) pdf = pisa.pisaDocument( StringIO(html.encode("UTF-8")), response, encoding='UTF-8', ) return response ``` At last, we create the template by which the document will be rendered, as follows: ``` {#templates/cv/cv_pdf.html#} <!DOCTYPE HTML> <html> <head> <meta charset="utf-8" /> <title>My Title</title> <style type="text/css"> @page { size: "A4"; margin: 2.5cm 1.5cm 2.5cm 1.5cm; @frame footer { -pdf-frame-content: footerContent; bottom: 0cm; margin-left: 0cm; margin-right: 0cm; height: 1cm; } } #footerContent { color: #666; font-size: 10pt; text-align: center; } /* ... Other CSS Rules go here ... */ </style> </head> <body> <div> <p class="h1">Curriculum Vitae</p> <table> <tr> <td><p><b>{{ cv.first_name }} {{ cv.last_name }}</b><br /> Contact: {{ cv.email }}</p> </td> <td align="right"> <img src="{{ STATIC_ROOT }}/site/img/smiley.jpg" width="100" height="100" /> </td> </tr> </table> <p class="h2">Experience</p> <table> {% for experience in cv.experience_set.all %} <tr> <td><p>{{ experience.from_date|date:"F Y" }} - {% if experience.till_date %} {{ experience.till_date|date:"F Y" }} {% else %} present {% endif %}<br /> {{ experience.position }} at {{ experience.company }}</p> </td> <td><p><b>Skills gained</b><br> {{ experience.skills|linebreaksbr }} <br> <br> </p> </td> </tr> {% endfor %} </table> </div> <pdf:nextpage> <div> This is an empty page to make a paper plane. </div> <div id="footerContent"> Document generated at {% now "Y-m-d" %} | Page <pdf:pagenumber> of <pdf:pagecount> </div> </body> </html> ``` ## 工作原理 Depending on the data entered into the database, the rendered PDF document might look like this: 图片:略 How does the view work? At first, we load a curriculum vitae by its ID if it exists, or raise the Page-not-found error if it does not. Then, we create the response object with mimetype of the PDF document. We set the Content-Disposition header to attachment with the specified filename. This will force browsers to open a dialog box prompting to save the PDF document and will suggest the specified name for the file. Then, we render the HTML template as a string passing curriculum vitae object, and the MEDIA_ROOT and STATIC_ROOT paths. >#####提示 Note that the src attribute of the <img> tag used for PDF creation needs to point to the file in the filesystem or the full URL of the image online. Then, we create a pisaDocument file with the UTF-8 encoded HTML as source, and response object as the destination. The response object is a file-like object, and pisaDocument writes the content of the document to it. The response object is returned by the view as expected. Let us have a look at the HTML template used to create this document. The template has some unusual HTML tags and CSS rules. If we want to have some elements on each page of the document, we can create CSS frames for that. In the preceding example, the <div> tag with the footerContent ID is marked as a frame, which will be repeated at the bottom of each page. In a similar way, we can have a header or a background image for each page. Here are the specific HTML tags used in this document: ``` The <pdf:nextpage> tag sets a manual page break The <pdf:pagenumber> tag returns the number of the current page The <pdf:pagecount> tag returns the total number of pages ``` The current version 3.0.33 of the Pisa xhtml2pdf library does not fully support all HTML tags and CSS rules; for example, <h1>, <h2>, and other headings are broken and there is no concept of floating, so instead you have to use paragraphs with CSS classes for different font styles and tables for multi-column layouts. However, this library is still mighty enough for customized layouts, which basically can be created just with the knowledge of HTML and CSS. ## 参阅 The Managing paginated lists recipe ********** 本章完 {% endraw %}