Django-深度分析Django基于类的视图(3)(翻译)


在前两个帖子中,我们探讨了Django中基于类的视图的基本概念,并且开始了解并使用其中两个基本的通用视图:ListViewDetailView。这两个视图都是从数据库读取一些数据然后将数据显示在模版里。


这第三个帖子希望给读者介绍Django中基于类的表单。本帖不打算给表单库做个完全介绍;我只是想给你演示基于类的通用视图是如何实现CRUDCreateReadUpdateDelete)操作中的CUD那部分,至于Read部分,已经由标准化通用视图实现了。


一个非常简单的例子

为了开始使用CBF(基于类的表单),让我们从StickyNote类开始,它表示一个有日期属性的简单文本日志。


class StickyNote(models.Model):
    timestamp = models.DateTimeField()
    text = models.TextField(blank=True, null=True)


其中首先做的事情一般是建一个表单,让用户在数据库中创建一条记录,本例中,也就是创建一条日志。函数版的表单处理功能如下:

def note_add(request):
    form = StickyNoteForm(request.POST or None)
    if form.is_valid():
        new_note = form.save()
        new_note.save()
    return render_to_response('note_add.html')

urlpatterns = patterns('',
    url(r'^note_add/$', 'note_add'),
)


很复杂,很难掌握,是不是?请注意,我省略了一些import;并且StickyNote类是用model表单创建的。因为你已经理解了函数表单视图如何工作了,让我们来将它同基于类的视图进行比较:

class NoteAdd(CreateView):

    model = StickyNote

并不奇怪,大多数代码不见了,这要感谢继承。如前两个帖子所示,在类的体系构造中,类的机制给我们提供了一系列代码并在幕后勤劳地工作着。我们现在的任务就是解开这些代码并指出CBF究竟是如何工作的,并且我将就如何更改这些代码实现我们的需求进行探讨。

为了理解方便,请记住,基于类的表单只是基于类的表单视图的简写。也就是说,CBF也是视图,它们的工作也是处理进入的HTTP请求然后返回一个HTTP响应。表单视图有点不太一样,主要因为POST的特性与GET不太一样。好了,让我们先看看这两个概念。


HTTP请求:GETPOST

请注意,这是一个很宽泛的话题,我们这里只是希望对跟DJango CBF有关的概念给一个非常快速的回顾。

取决于其所带的方法,HTTP请求具有不同的形式。这些方法叫做HTTP动词(谓词?verb),其中最常用的是GETPOSTGET方法告诉服务器客户需要获取某个资源(也就是连接相关URL的资源),并且它不会对资源本身造成影响(比如改变资源)。POST方法则用来给服务器发送一些数据,而指定的URL,也就是资源,将处理这些数据。

如你所见,POST的定义非常的宽泛:服务器接收进来的数据,可以对这些数据执行任何动作,比如创建一条新的纪录,编辑或者删除一条或者多条记录,等等。

请牢记,表单跟POST请求不是一回事儿。实际上,它们只是偶然地联系在一起的:一个表单是用户在浏览HTML网页时用来搜集数据的一种方式,而POST只是说明了数据是如何传输到服务器的。你可以不需要有个表单来进行一次POST请求。你只是需要发送一些数据而已。HTML表单只是发送POST请求的其中一种有用的方式,但不是唯一的方式。


表单视图

为什么表单视图与标准视图不一样呢?我们可以看看一个典型的网站数据提交过程就可以有个大概的了解:

  1. 用户浏览一个网页(GET
  2. 服务器用一个包含了表单的网页给GET请求以应答
  3. 用户填写表单并提交(POST
  4. 服务器接收并处理数据


如你所见,此过程有一个与服务器双交互的过程:第一个请求GET了页面,第二个请求POST这些数据。因此,你需要建立一个视图用来应答GET请求,另一个视图用来应答POST请求。

由于大部分时间,我们用于处理POST数据的URL和用来GET页面的URL是同一个URL,我们需要创建一个视图同时接受这两种方法。这也是为什么你在函数表单视图中看到同样也使用这一模式。下面是官方文档关于这一主题的代码段:

def contact(request):
    if request.method == 'POST': # If the form has been submitted...
        form = ContactForm(request.POST) # A form bound to the POST data
        if form.is_valid(): # All validation rules pass
            # Process the data in form.cleaned_data
            # ...
        return HttpResponseRedirect('/thanks/') # Redirect after POST
    else:
        form = ContactForm() # An unbound form

    return render(request, 'contact.html', {
        'form': form,
    })


如你所见,第一个条件路径处理数据提交(POST),而其他部分则处理GET请求。

现在是时候深入了解基于类的表单并理解他们是如何处理这种双交互的情形。


让我们从CreateView开始,我们在前面的小例子中用过它,它在views/generic/edit.py的202行中定义的。这是一个几乎空的类,继承自SingleObjectTemplateResponseMixinBaseCreateView。第一个类用选中的模版渲染响应,第二个类,我们先放在一边。实际上,对于第二个类,我们可以在views/generic/edit.py的187行中找到一些代码片段,而且其实现了两个方法,也就是get()和post()。


处理GETPOST请求

我们讨论View类中dispatch()的时候已经遇到过get()方法。这里重新快速地回顾一下其目标:此方法在HTTP请求带着GET动词进入时被调用,并且用来处理请求。不出意料,当进来的是POST请求时,post()方法被调用。这两个方法已经在父类BaseCreateView定义,也是继承自ProcessFormViewviews/generic/edit.py 145行)。看看最后这个类的源代码还是有帮助的。

class ProcessFormView(View):
    """
    A mixin that renders a form on GET and processes it on POST.
    """
    def get(self, request, *args, **kwargs):
        """
        Handles GET requests and instantiates a blank version of the form.
        """
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        return self.render_to_response(self.get_context_data(form=form))

    def post(self, request, *args, **kwargs):
        """
        Handles POST requests, instantiating a form instance with the passed
        POST variables and then checked for validity.
        """
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        if form.is_valid():
            return self.form_valid(form)
        else:
            return self.form_invalid(form)


你可以看到,这两个方法非常直接。他们都是通过get_form_class()来获取表单的类,并且将其用get_form()实例化(待会儿再详述)。接下来,get()方法调用render_to_response()方法去渲染一个模版,同时将由get_context_data()生成的上下文传递给它。请注意,上下文接受的表单是由get_form()方法创建的。

Post()方法并不直接渲染模版,因为它需要在做这最后一步前要先处理进来的数据。相反,它要先用它的is_valid()方法来验证表单的有效性,并根据验证结果,来决定是否调用另外两个方法form_valid()和form_invalid()。请参考官方文档有关表单验证的内容。

请注意,这些类的行为模式遵循ListViewDetailView一样的模式。这一点,我们已经在前两个帖子中论述过了。

ProcessFormVIew继承自View,我们已经在前两篇文章中对View做了深入的分析。在分析中,你会发现as_view()和dispatch()这两个方法是CBV系统的基础。

表单工作流 - 第一部分

ProcessFormView开始的继承路径跨越了所有处理输入请求的类,并且将GETPOST方法区分开来。第二条继承路径由BaseCreateView开始,然后来到ModelFormMixin,后者在views/generic/edit.py的75行中定义。这一条路径包含的类实现了表单的管理方法。前两个用来处理表单的方法是get_form_class()和get_form(),我们在讨论get()和post()方法时已经遇到过了

get_form_class()试图从self.form_class属性获得表单model。如果它没有被定义的话,get_form_class()就从self.model或者从查询集里提取model。然后,它通过一个在forms/model.py中定义的工厂(factory)来返回一个合适的modelform

get_form()方法定义在FormMixin中(views/generic/edit.py的10行),并且由get_form_kwargs()(其实现在views/generic/edit.py的100行)返回的关键词对表单类进行实例化。我们很快就会发现,最后这个方法是非常重要的,因为它在处理POST请求的双交互过程中发挥着巨大的作用。

我们在其父类树中发现,get_form_kwargs()方法的第一个实现在ModelFormMixin中(views/generic/edit.py的100行),但是其立即就调用定义在FormMixin中同一个方法(views/generic/edit.py的10行)。其代码如下:

def get_form_kwargs(self):
    kwargs = {'initial': self.get_initial()}
    if self.request.method in ('POST', 'PUT'):
        kwargs.update({
            'data': self.request.POST,
            'files': self.request.FILES,
        })
    return kwargs


表单关键词字典的第一个值来自self.initial字典的拷贝,如官方文档所书,此字典由get_initial()方法返回。接下来,如果处理的请求方法是POSTPUT,则这些关键词由请求本身的内容更新,也就是说,那些被提交上来的数据和上传的文件。如代码forms/forms.py的77中所示,这一过程用来初始化表单对象本身。我会过一会儿讲述这一机制。

在这一方法返回其字典后,执行流程来到了ModelFormMixin。方法的代码如下:

def get_form_kwargs(self):
    kwargs = super(ModelFormMixin, self).get_form_kwargs()
    kwargs.update({'instance': self.object})
    return kwargs


其所做的只是在关键词字典中将self.object添加在instance之下。我们在讨论DetailView的时候已经遇到过self.object了。在那里,self.object包含的是查询集的结果,也即是由视图显示的对象。

那么现在self.object是什么呢?由于在CreateViewBaseCreateView的父类中,self.object被定义为None,所以我们先暂时把它放一边。当我们讨论更新和删除表单的时候我们再来讨论它。

我们在get()方法中发现的最后事情就是,在get_form_class()和get_form()之后,是get_context_data()。就像在ListViewDetailView里发生的一样,这一方法创建了用来渲染模版的字典(上下文)。你可以在views/generic/edit.py 130行中找到get_context_data()的实现。你会看到,self.object已经被设置成None了,而上下文仅仅包含了实例化的表单,放在form关键词下面(见代码views/generic/edit.py的155行

让我们从新回顾一下目前为止的流程:

  1. URL分发器分发了一个GET请求,其中包含了表单页面。
  2. ProcessFormViewget()方法通过get_form_class()找到了表单类
  3. 通过包含在self.initial字典中的值,get_form()实例化了这个表单类
  4. 在这一刻,像往常一样,get_context_data()返回一个上下文,并被用来渲染一个模版。上下文里包含着这一表单。


表单工作流 - 第二部分

现在,用户获取了请求的页面,但是它只是个空的表单。填完表单后,他或她点击提交按钮,一个新的HTTP请求来到了服务器,只不过这次带的是POST方法以及一组从表单输入框中获得的数据。我们的表单现在要处理第二个交互步骤,也就是在函数视图中通常由 if request.method == ‘POST’: 代码开始那段来处理。

我们已经知道,进入的请求是由ProcessFormView中的post()处理的,其工作原理跟第一部分代码很像,也是调用get_form_class()和get_form()。后者现在处理POST请求,于是FormMixin(views/generic/edit.py的10行geto_form_kwargs()的代码将提交上来的数据添加在关键词字典中的data键下面,把上传上来的文件添加在files键下面。为什么Django要做这一步骤呢?如你在forms/forms.py的77行代码中所见,一个Django表单可以通过一个可选的data键来进行实例化,而这一data键就是存储在表单对象里,以便用于后续的验证阶段。

因此,现在表单被绑定了(也就是说,它包含了一些数据和文件,见forms/forms.py80)。post()方法现在测试is_valid()的结果,根据结果来决定是调用form_valid()还是form_invalid()。请注意,尽管is_valid()是表单本身的方法,但是这后两个方法都属于BaseCreateView,由相同的父类实现了get_form_kwargs(),它们就是ModelFormMixinFormMixin

前一个类(MoedelFormMixin)在代码views/generic/edit.py123实现之,并把form.save()的结果放入self.object中。请记住self.object是通过get_context_data()方法并将其附加在上下文中的object键下面,如前面段落所示。针对modelformform.save()定义在BaseModelFormforms/models.py中的357),并且,基本上就是将与modelform相连接的Django model的实例保存起来。这也就实现了基于CreateView表单视图的实际创建过程。随着将对象保存到数据库,顺理成章地,将它存入self.object,同时将其传递给模版。

现在,form_valid()执行到了代码views/generic/edit.py61FormMixin类的实现。在这里,这一方法返回一个HttpResponseRedirect对象,也就是你将浏览器指向给定的URL的过程。在此场景中,self.get_success_url()给出一个URL,并试图返回self.success_url,如果它有定义的话,否则,返回由新建对象的get_absolute_url()返回的结果。

在另一方面,在代码views/generic/edit.py67form_invalide()负责处理表单包含错误的情况,它就简单地调用render_to_response(),把上下文传给它,上下文里form键下是已经编译过的表单。


更新和删除操作

这一着实代码丰富的旅程揭示了CreateView的内部机制,它可以用来在数据库中创建一个新的对象。而UpdateViewDeleteView类则遵循类似的路径,只是在具体实现某些行为上有些微小的不同。

UpdateView希望显示已经填好数据的表单,因此它在处理请求之前实例化self.objectviews/generic/edit.py 210)。这样的结果就是,对象出现在关键词字典的instance键下(views/generic/edit.py105),并且由modelform使用来初始化数据(forms/models.py244)。BaseModelFormform.save()方法足够聪明到可以明白对象是被创建的还是刚刚更新的(forms/models.py365),因此,UpdateViewpost()方法和CreateView的一样。

DeleteViewCreateViewUpdateView有点不同。如官方文档所书,如果由GET方法调用,它会显示一个与用于处理POST页面相同确认页面。因此,对于GET请求,DeleteView只是使用由其父类BaseDetailView定义的get()方法(views/generic/detail.py103),来渲染模版同时将对象放入上下文。当通过POST请求调用时,视图就会使用DeleteMixin中定义的post()方法(views/generic/edit.py233),而post()只是反过来调用在同一个类中定义的delete()方法(views/generic/edit.py239)。而delete()执行数据库上的删除操作并重定向到成功URL


结论


如你所见,当前Django基于类的表单视图的实现相当复杂。这可以让用户通过定义几个类就可以实现如CUD操作这样的复杂行为,就如我在文章开头的小例子中所做的一样。然而,大多数时间,这样一种简化却让程序员在理解如何实现期望的对类行为的改变时感到困难。因此,我希望通过对Django代码的浏览能够让读者明白在你的HTTP请求的生命周期里,需要调用哪些方法,这样你就能够更好地知道需要重载哪些方法。

当执行一些超出标准CUD操作的特殊动作时,你最好从FormView开始继承(views/generic/edit.py181)。你要做的第一件事就是判断你是否以及如何定制get()和post()方法。请牢记,你要做的,要不就是将这些方法自己全部实现,要不就是做一些改动并调用父类的实现。如果这些对你的应用来说还不足够,则考虑重载其中某个更专用的方法,比如get_form_kwargs()或form_valid()。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值