返回介绍

第三章 表单与视图

发布于 2025-02-26 23:49:05 字数 44819 浏览 0 评论 0 收藏 0

在本章,我们学习以下内容:

  • 传递 HttpRequest 到表单
  • 利用表单的 save 方法
  • 上传图片
  • 使用 django-crispy-forms 生成表单布局
  • 过滤对象列表
  • 管理分页列表
  • 编写类视图
  • 生成 PDF 文档

引言

当数据库结构定在模型中时,我们需要有一些视图供用户输入数据,或者对用户显示数据。本章,我们会关注管理表单的视图,列表视图,以及生成可替换的输出而不仅仅是 HTML。举个最简单的例子来说,我们将把模板和 URL 规则的创建的决定权下放给你。

传递 HttpRequest 到表单

Django 的每一个视图的第一个参数通常是命名为 requestHttpRequest 对象。它包含了请求的元数据,例如,当前语言编码,当前用户,当前 cookie,或者是当前的 session。默认,表单被用在视图中用来接受 GET 或者 POST 参数,文件,初始化数据,以及其他的参数,但却不是 HttpRequest 对象暗沟。某些情况下,特别是当你想要使用请求数据过滤出表单字段的选择,又或者是你想要处理像在表单中保存当前用户或者当前 IP 这样事情时,额外地传递地 HttpRequest 到表单会非常有用的。

在本方法中,我会向你展示一个在表单中某人可以选择一个用户并对此用户发送消息的例子。我们会传递 HttpRequest 对象到表单以暴露接受选项的当前用户:我们不想要任何人都可以给他们发送消息。

预热

让我们来创建一个叫做 email_messages 的应用,并把它放到设置中的 INSTALLED_APPS 去。该 app 仅含有表单和视图而没有模型。

具体做法

  1. 添加一个新文件, forms.py ,其中消息表单包含两个字段:接收人选项和消息文本。同时,该表单拥有一个初始化方法,它会接受 request 对象并对接收人的选项字段修改:
# -*- 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)
  1. 然后,利用 message_to_user 视图创建 views.py 以处理表单。如你所见, request 对象作为第一个参数被传递到了表单:
#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 方法并尝试发送邮件到所选择的接收人,发生错误时静默:
#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,
    )
  1. 最后,假如发送的数据有效,则在视图中调用 save 方法:
#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 模型:作者,名言内容,以及图片,一如下面所示:

#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 文件,并于其中编写一个简单的模型表单:

#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 方法:

#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” 十分的重要,否则文件上传不会起作用的:

{% 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:

#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:

(myproject_env)$ pip install django-crispy-forms

3.确保 csirpy-form 添加到了 INSTALLED_APPS ,然后在该项目中设置 bootstrap3 作为模板包来使用:

#settings.py
INSTALLED_APPS = (
  # ...
  "crispy_forms",
)
# ...
CRISPY_TEMPLATE_PACK = "bootstrap3"

4.让我们来创建一个应用 bulletin_board 来阐明 django-crispy-forms 的用法,并把应用添加到设置中的 INSTALLED_APPS 。我们会拥有一个含有这些字段的 Bulletin 模型:类型,名称,描述,联系人,电话,电邮,以及图片:

#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:

#bulletin_board/forms.py
# -*- coding: UTF-8 -*-
from django import forms
from django.utils.translation import ugettext_lazy as _, ugettext
from crispy_forms.helper import FormHelper
from crispy_forms import layout, bootstrap
from models import Bulletin

class BulletinForm(forms.ModelForm):
  class Meta:
    model = Bulletin
    fields = ['bulletin_type', 'title', 'description',
      'contact_person', 'phone', 'email', 'image']

  def __init__(self, *args, **kwargs):
    super(BulletinForm, self).__init__(*args, **kwargs)

    self.helper = FormHelper()
    self.helper.form_action = ""
    self.helper.form_method = "POST"

    self.fields['bulletin_type'].widget = forms.RadioSelect()
    # delete empty choice for the type
    del self.fields['bulletin_type'].choices[0]

    self.helper.layout = layout.Layout(
      layout.Fieldset(
        _("Main data"),
        layout.Field("bulletin_type"),
        layout.Field("title", css_class="input-block-level"),
        layout.Field("description",
          css_class="input-blocklevel", rows="3"),
      ),
      layout.Fieldset(
        _("Image"),
        layout.Field("image", css_class="input-block-level"),
        layout.HTML(u"""{% load i18n %}
          <p class="help-block">{% trans "Available formats are JPG, GIF, and PNG. Minimal “size is 800 × 800 px." %}</p>
        """),
        title=_("Image upload"),
        css_id="image_fieldset",
      ),
      layout.Fieldset(
        _("Contact"),
        layout.Field("contact_person",
           css_class="input-blocklevel"),
        layout.Div(
          bootstrap.PrependedText("phone", """<span
class="glyphicon glyphicon-earphone"></span>""",
            css_class="inputblock-level"),
          bootstrap.PrependedText("email", "@",
            css_class="input-block-level",
            placeholder="contact@example.com"),
          css_id="contact_info",
        ),
      ),
      bootstrap.FormActions(
        layout.Submit('submit', _('Save')),
      )
    )

要渲染模板中的表单,如下,我们只需载入标签冷酷 crispy_forms_tags ,然后使用模板标签 {% crispy %}

#templates/bulletin_board/change_form.html}
{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block content %}
  {% crispy form %}
{% endblock %}

工作原理

拥有新闻简报表的页面的样子大概如此:

图片:略

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

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,然后在这个新应用中定义前面提及的模型:

#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

#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 ,再然后通过所选择的种类进行过滤。要注意字典这一方面,这里改字典用来列出目录以及当前选定的选项:

#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:

最后,我们为列表视图创建模板。我们会使用

{#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 来稍微修改上下文字典。

#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:

{#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:

#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:

#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:

(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:

#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:

#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:

#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

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

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, 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

本章完

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文