Two Scoops of Django 1.8 学习笔记七

12. 表单通用模式

Django的表单强大、灵活、扩展性强、健全。因此,Django admin 和 CBVs广泛的使用了表单。
事实上,所有重大的Django API 框架用ModelForms 或者类似的实现来作为它们校验的一部分。

结合表单、模型、视图让我们用较少的努力做了较多的事。学习曲线是值得的:
一旦你学会了使用这些组件流畅的进行工作,你会发现,Django提供了以惊人的速度创造数量惊人的有用且稳定功能的能力

这一章明确讲解了Django最好的部分之一:forms, models, CBVs之间的协同工作。
它覆盖了五个通用表单模式。每个Django开发者都应该掌握

12.1 模式一:简易的ModelForm使用默认校验

如果你还记得,用CBVs就能实现表单的增加和修改,而且只需要几行代码:

# flavors/views.py
from django.views.generic import CreateView, UpdateView

from braces.views import LoginRequiredMixin

from .models import Flavor

class FlavorCreateView(LoginRequiredMixin, CreateView):
    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')

class FlavorUpdateView(LoginRequiredMixin, UpdateView):
    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')

总结一下我们在这儿是如何使用默认校验的:

  • 将Flavor作为模型分配到FlavorCreateView和FlavorUpdateView
  • 这两个视图根据Falvor模型自动生成了一个ModelForm
  • 这些ModelForm依赖Falvor模型中默认的字段校验规则

Django提供了很多不错的数据校验,但在实践中还远远不够。
我们认识到了这一点,因此将它作为第一步,下一模式中将会演示如何创建一个自定义的字段校验器

12.2 模式二:ModelForm中自定义表单字段校验

如果我们想要确定dessert应用中的title字段是否是以Tasty开头该怎么办。可以用简单的自定义字段校验器解决。

想象一下这个例子:我们的项目中有两个不同的dessert相关的模型:一个描述冰淇淋口味的Flavor模型和一个描述不同种类奶昔的Milkshake模型。假设它们都有title字段。

为了校验所有可编辑模型的title字段,我们先创建一个validators.py模块

# core/validators.py
from django.core.exceptions import ValidationError

def validate_tasty(value):
    """Raise a ValidationError if the value doesn't start with the
        word 'Tasty'.
    """
    if not value.startswith(u"Tasty"):
        msg = u"Must start with Tasty"
        raise ValidationError(msg)

这是一个函数校验器,如果不能通过测试则抛出错误。我们的validate_tasty()函数校验器看起来就是一个简单的字符串检查,而在实践中,表单字段校验器相当复杂。

为了在两个不同的dessert模型中都能使用到validate_tasty(),我们会把这个校验器放在一个叫做TastyTitleAbstractModel的抽象模型中,我们就能在整个项目中使用了。

假设我们的Flavor模型和Milkshake模型在各自独立的应用中,把校验器放在任一个应用中都不合理。因此,我们把TastyTitleAbstractModel放在core/models.py核心模块中。

# core/models.py
from django.db import models

from .validators import validate_tasty

class TastyTitleAbstractModel(models.Model):

    title = models.CharField(max_length=255, validators=[validate_tasty])

    class Meta:
        abstract = True

如我们所愿,代码最后两行让TastyTitleAbstractModel成为了抽象模型。

我们修改一下 flavor/models.py 的原始代码,把TastyTitleAbstractModel作为父类:

# flavors/models.py
from django.core.urlresolvers import reverse
from django.db import models

from core.models import TastyTitleAbstractModel

class Flavor(TastyTitleAbstractModel):
    slug = models.SlugField()
    scoops_remaining = models.IntegerField(default=0)

    def get_absolute_url(self):
        return reverse("flavors:detail", kwargs={"slug": self.slug})

它可以用到Falvor模型中,也可以用到任何一个与tasty相关的模型中,
比如一个描述华夫饼的WaffleCone模型或者一个描述蛋糕的Cake模型。
如果他们的title字段不是以Tasty开头的,都会抛出一个校验错误。

现在,我们来探讨一下可能存在你脑海中的两个疑问:

  • 如果我们只想在表单中使用validate_tasty()怎么办?
  • 除了title字段,我们还想在别的字段中使用怎么办?

那就需要利用这个校验器来创建一个自定义的FalvorForm:

# flavors/forms.py
from django import forms

from core.validators import validate_tasty
from .models import Flavor

class FlavorForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(FlavorForm, self).__init__(*args, **kwargs)
        self.fields["title"].validators.append(validate_tasty)
        self.fields["slug"].validators.append(validate_tasty)

    class Meta:
        model = Flavor

这个模式最赞的地方是我们没有改变校验器的原始代码,仅仅只是将它导入,并且在新的位置使用了它
给这个视图附上自定义表单是我们的下一步,GCBVs中的edit视图会基于视图的模型属性自动生成ModelForm,我们将通过定制的FlavorForm来覆盖默认的。

# flavors/views.py
from django.contrib import messages
from django.views.generic import CreateView, UpdateView, DetailView

from braces.views import LoginRequiredMixin

from .models import Flavor
from .forms import FlavorForm

class FlavorActionMixin(object):

    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')

    @property
    def success_msg(self):
        return NotImplemented

    def form_valid(self, form):
        messages.info(self.request, self.success_msg)
        return super(FlavorActionMixin, self).form_valid(form)

class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin,
                            CreateView):
    success_msg = "created"
    # Explicitly attach the FlavorForm class
    form_class = FlavorForm

class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin,
                            UpdateView):
    success_msg = "updated"
    # Explicitly attach the FlavorForm class
    form_class = FlavorForm

class FlavorDetailView(DetailView):
    model = Flavor

现在FlavorCreateView和FlavorUpdateView这两个视图使用新的FlavorForm来校验传入数据。

12.3 模式三:覆写校验的clean阶段

我们来讨论一下有趣的校验使用情况:

  • 多字段校验
  • 校验涉及已验证的数据库中的现有数据

这两种场景都需要根据自定义的校验逻辑覆写clean()和clean_field_name()方法

在默认的校验器和自定义的字段校验器运行之后,Django提供了第二阶段的校验,那就是clean()和clean_field_name()方法。你可能想知道为什么Django提供了更多的钩子进行校验,下是我们最喜欢的原因:

  1. 由于clean()不是针对任何一个特定的字段,clean()方法是用来验证两个或更多的字段互相冲突的场所,
  2. clean 阶段是一个针对持久类数据校验的更好的场所,由于数据已经有了一些验证,你就不会将数据库的循环浪费在不必要的查询中。

我们通过另一个例子来探讨一下,也许你想实现一个冰淇淋的订购表单,用户可以指定想要的口味,配料。然后来我们的店里取走。由于我们需要给顾客展示库存不够的情况,我们会用到clean_slug()方法:

# flavors/forms.py
from django import forms
from flavors.models import Flavor

class IceCreamOrderForm(forms.Form):
    """Normally done with forms.ModelForm. But we use forms.Form here
        to demonstrate that these sorts of techniques work on every
        type of form.
    """

    slug = forms.ChoiceField("Flavor")
    toppings = forms.CharField()

    def __init__(self, *args, **kwargs):
        super(IceCreamOrderForm, self).__init__(*args,
                    **kwargs)
        # We dynamically set the choices here rather than
        # in the flavor field definition. Setting them in
        # the field definition means status updates won't
        # be reflected in the form without server restarts.
        self.fields["slug"].choices = [
            (x.slug, x.title) for x in Flavor.objects.all()
        ]
        # NOTE: We could filter by whether or not a flavor
        #       has any scoops, but this is an example of
        #       how to use clean_slug, not filter().

    def clean_slug(self):
        slug = self.cleaned_data["slug"]
        if Flavor.objects.get(slug=slug).scoops_remaining <= 0:
            msg = u"Sorry, we are out of that flavor."
            raise forms.ValidationError(msg)
        return slug

对于由HTML驱动的视图,如果库存不够了就会抛出Sorry, we are out of that flavor的错误。简直方便的不要不要的。

现在想象一下,如果我们接到了客户投诉说巧克力太多。是的,这是愚蠢的也不可能发生的,我们只是用这个虚构的例子来为我们的知识点举例。

# attach this code to the previous example
def clean(self):
    cleaned_data = super(IceCreamOrderForm, self).clean()
    slug = cleaned_data.get("slug", "")
    toppings = cleaned_data.get("toppings", "")

    # Silly "too much chocolate" validation example
    if u"chocolate" in slug.lower() and \
           u"chocolate" in toppings.lower():
        msg = u"Your order has too much chocolate."
        raise forms.ValidationError(msg)
    return cleaned_data

12.4 模式四:破解表单字段(2 CBVs, 2 Forms, 1 Model)

这一节,我们会深入研究一个模型中对应两个视图/表单的情况。我们将会破解表单来创作一个自定义行为的表单。

一个商店列表的例子,我们希望这些商店的信息能尽快的存到系统中,但是之后希望还能添加例如电话号码和街道地址的数据。下面是我们的冰淇淋商店IceCreamStore模型:

# stores/models.py
from django.core.urlresolvers import reverse
from django.db import models

class IceCreamStore(models.Model):
    title = models.CharField(max_length=100)
    block_address = models.TextField()
    phone = models.CharField(max_length=20, blank=True)
    description = models.TextField(blank=True)

    def get_absolute_url(self):
        return reverse("store_detail", kwargs={"pk": self.pk})

默认生成的ModelForm会强制用户输入title和block_address字段,但是允许用户不填phone和description字段

我们通过下面的表单来实现商店信息的更新:

# stores/forms.py
# Call phone and description from the self.fields dict-like object
from django import forms

from .models import IceCreamStore

class IceCreamStoreUpdateForm(forms.ModelForm):

    class Meta:
        model = IceCreamStore

    def __init__(self, *args, **kwargs):
        # Call the original __init__ method before assigning
        # field overloads
        super(IceCreamStoreUpdateForm, self).__init__(*args,
                            **kwargs)
        self.fields["phone"].required = True
        self.fields["description"].required = True

一个重要的知识点需要记住!!!Django的表单就是python的类。它们可以作为对象来实现,它们能继承自其他类,也可以当作父类。
因此,我们可以这样写我们的两个表单:

# stores/forms.py
from django import forms

from .models import IceCreamStore

class IceCreamStoreCreateForm(forms.ModelForm):

    class Meta:
        model = IceCreamStore
        fields = ("title", "block_address", )

class IceCreamStoreUpdateForm(IceCreamStoreCreateForm):

    def __init__(self, *args, **kwargs):
        super(IceCreamStoreUpdateForm,
                self).__init__(*args, **kwargs)
        self.fields["phone"].required = True
        self.fields["description"].required = True

    class Meta(IceCreamStoreCreateForm.Meta):
        # show all the fields!
        fields = ("title", "block_address", "phone",
                "description", )

警告!!!使用Meta.fields!!不要使用Meta.exclude!!!

最后,我们来写对应的CBVs视图:

# stores/views
from django.views.generic import CreateView, UpdateView

from .forms import IceCreamStoreCreateForm
from .forms import IceCreamStoreUpdateForm
from .models import IceCreamStore

class IceCreamCreateView(CreateView):
    model = IceCreamStore
    form_class = IceCreamStoreCreateForm

class IceCreamUpdateView(UpdateView):
    model = IceCreamStore
    form_class = IceCreamStoreUpdateForm

12.5 模式五:可复用的搜索mixin视图

在这个例子中,我们将会了解如何在两个对应不同模型的视图中复用一个搜索表单。
假设这两个模型都有一个叫做title的字段,这个例子演示了如何在Flavor模型和IceCreamStore模型中使用单一的CBV视图提供简单的搜索功能。

我们先给视图创建一个简单的mixin搜索:

# core/views.py
class TitleSearchMixin(object):

    def get_queryset(self):
        # Fetch the queryset from the parent's get_queryset
        queryset = super(TitleSearchMixin, self).get_queryset()

        # Get the q GET parameter
        q = self.request.GET.get("q")
        if q:
            # return a filtered queryset
            return queryset.filter(title__icontains=q)
        # No q is specified so we return queryset
        return queryset

接下来就是让它工作在Flavor和IceCreamUpdateView视图中:

# add to flavors/views.py
from django.views.generic import ListView

from core.views import TitleSearchMixin
from .models import Flavor

class FlavorListView(TitleSearchMixin, ListView):
    model = Flavor


# add to stores/views.py
from django.views.generic import ListView

from core.views import TitleSearchMixin
from .models import Store

class IceCreamStoreListView(TitleSearchMixin, ListView):
    model = Store

然后给每个ListView定义HTML:

<form action="" method="GET">
    <input type="text" name="q" />
    <button type="submit">search</button>
</form>


<form action="" method="GET">
    <input type="text" name="q" />
    <button type="submit">search</button>
</form>

Mixins是复用代码很好的方式,但是在一个类中使用太多的mixins会制造出非常难维护代码。
一如既往的保证代码简洁就行