解决django中ModelForm多表单组合的问题

django是python语言快速实现web服务的大杀器,其开发效率可以非常的高!但因为秉承了语言的灵活性,django框架又太灵活,以至于想实现任何功能都有种“条条大路通罗马”的感觉。这么多种选择放在一起,如何分出高下?我想此时的场景下就两个标准:

1、相同的功能用最少的代码实现(代码少BUG也会少);

2、相对最易于理解,从而易于维护和扩展。书归正传,web服务允许用户输入,基本上要靠表单。而django对表单的支持力度非常大,我们用不着在浏览器端的html文件里写大量<form>代码,再到web端去匹配form里的id/name/value、验证规则,再与持久层数据库比较并做操作。我们需要完成的工作非常少,可以没有相似的重复代码。有些复杂的场景,会要求一个表单的内容存放到多张表里,本文将通过4个部分,阐述它的实现方法。

1、django基础表单的功能

定义一个表单非常简单,继承类django.forms.Form即可,例如:

class ProjectForm(forms.Form):
  name = forms.CharField(label='项目名称', max_length=20)

这个表单类可以生成HTML形式的form,可以从request.POST中解析form到ProjectForm类实例。怎么做到的呢?

看下django.forms.Form定义:

class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)):
  "A collection of Fields, plus their associated data."
  # This is a separate class from BaseForm in order to abstract the way
  # self.fields is specified. This class (Form) is the one that does the
  # fancy metaclass stuff purely for the semantic sugar -- it allows one
  # to define a form using declarative syntax.
  # BaseForm itself has no way of designating self.fields.

注释说得很清楚,Form这个类就是为了实现declarative syntax的,也就是说,继承了Form后,我们直观的表达ProjectForm里要有一个Field名叫name,不关心其语法实现,而通过Form多继承中的DeclarativeFieldsMetaclass语法糖,将会把name弄到类实例的self.fields里。

我们重点关注表单的BaseForm类,它实现了基本的逻辑。截选了一小段对接下来的陈述有意义的代码,做一个简单的注释。

class BaseForm(object):
  def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
         initial=None, error_class=ErrorList, label_suffix=None,
         empty_permitted=False, field_order=None, use_required_attribute=None):
    #data参数用于接收request.POST字典,如果是GET方法就不传
	self.data = data or {}
	#files用于接收request.FILES,也就是处理上传文件
	self.files = files or {}
	#本篇文章的重点在于多个表单集成到一个form中,此时为防止有同名的field,需要加prefix前缀
    if prefix is not None:
      self.prefix = prefix
	#GET显示表单时,如果要显示初始值,请用initial参数
    self.initial = initial or {}

  #模板中显示{{form}}时,默认是以<table></table>显示的
  def __str__(self):
    return self.as_table()

  #如果模板中不想写重复代码,只以固定的格式来显示每一个field,那么就用{% for field, val in form %}来遍历处理吧
  def __iter__(self):
    for name in self.fields:
      yield self[name]

  #如果传入了prefix参数,html中每个field的name和id里都会加上prefix前缀
  def add_prefix(self, field_name):
    return '%s-%s' % (self.prefix, field_name) if self.prefix else field_name

  #模板中以html格式显示form就靠这个方法
  def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
    "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
    top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
    output, hidden_fields = [], []

  #除了默认的table方式显示外,还可以<ul><li>或者<p>方式显示
  def as_table(self):
    "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
  def as_ul(self):
    "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
  def as_p(self):
    "Returns this form rendered as HTML <p>s."

所以,基本表单的功能看BaseForm已经足够了。

2、从模型创建表单

django对于MVC中的C与M间的映射是非常体贴的,集中体现中Model模型中(比如模型的权限与用户认证)。那么,一个模型代表着RDS中的一张表,模型的实例代表着关系数据库中的一行,而form如何与一行相对应呢?

定义一个模型引申出的表单非常简单,例如:

class ProjectForm(ModelForm):
  class Meta:
    model = Project
    fields = ['approvals','manager','name','fund_rource','content','range',]

在model中告诉django模型是谁,在fields中告诉django需要在表单中创建哪些字段。django会有一个django.db.models.Field到django.forms.Field的转换规则,此时会生成Form。我们看看ModelForm是什么样的:

class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
  pass

类似Form类,ModelFormMetaclass就是语法糖,我们重点看BaseModelForm类:

class BaseModelForm(BaseForm):
  def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
         initial=None, error_class=ErrorList, label_suffix=None,
         empty_permitted=False, instance=None, use_required_attribute=None):
    opts = self._meta
	#相比较BaseForm,多了instance参数,它等价于Model模型的一个实例
    if instance is None:
      #不传instance参数,则会新构造model对象
      self.instance = opts.model()
      object_data = {}
    else:
      self.instance = instance
      object_data = model_to_dict(instance, opts.fields, opts.exclude)
	#此时传递了initial也一样可以生效,同时还会设置到Model中
    if initial is not None:
      object_data.update(initial)

  def save(self, commit=True):
	#默认commit是True,此时就会保存Model实例到数据库
    if commit:
      self.instance.save()
	#同时保存many-to-many字段对应的关系表
      self._save_m2m()
    else:
	#注意,本篇文章主要用到commit=False这个参数,它会返回Model实例,允许我们在修改instance后,在instance上再调用save方法
      self.save_m2m = self._save_m2m
    return self.instance

所以,对于ModelForm我们可以传入instance参数初始化表单,可以调用save()方法直接将从html里得到的表单数据持久化到数据库中。而我们只需要几十行代码就可以完成这么多工作。

3、通用视图

django.views.generic.ListView和django.views.generic.edit下的CreateView, UpdateView, DeleteView都是通用视图。即,我们又可以通过它们,把很多重复的工作交给django完成,又可以少写很多代码完成同样的功能了。这里仅以CreateView为例说明,因为它相对最复杂,接下来的多ModelForm的提交也是在CreateView上进行的。

通用视图使用时,只需要承继后,再设置model或者form_class即可。比如CreateView就会由django自动的把页面上POST出的form数据解析到model生成的表单(或者form_calss指定的ModelForm类型表单),同时调用表单的save方法将数据添加到模型对应的数据库表中。当然GET请求时会生成空form到页面上。可以看到,除去定义model或者form类外,几行代码就可以搞定这么多事。我们看看CreateView的继承关系:

简单介绍下CreateView通用视图中每个父类的作用。

View是所有视图类的父类,根据方法名分发请求到具体的get或者post等方法,提供as_view方法。

TemplateResponseMixin提供render_to_response方法将响应通过context上下文在模板上渲染。

ContextMixin在context上下文中加入'view'元素,值为self实例。

ProcessFormView在GET请求上渲染表单,在POST请求上解析form到表单实例。注意,它会在post请求中判断表单是否可用,is_valid为真时,会调用form_valid方法,因此,重写form_valid方法是第4部分处理多model到一个form的关键。

FormMixin允许处理表单,可指定form_class为某个表单。

SingleObjectMixin生成context上下文,同时根据model模型名称生成object并添加到上下文中的'object'元素。

ModelFormMixin提供在请求中处理modelform的方式。

SingleObjectTemplateResponseMixin帮助TemplateResponseMixin提供模板。

所以,在用CreateView、一个模型、一个模板实现添加一行记录的功能时是多么简单,因为这些父类会自动生成object,渲染到模板,解析form表单,save到数据库中。所以,从模型创建出的表单ModelForm,配合上通用视图后,威力巨大!!

4、多个ModelForm在一个form里提交

终于可以回到本文的主题了。CreateView默认是处理一个Model模型、一个ModelForm表单的,然而,很多时候为了解耦,会把一张表拆成多张表,通过id关联在一起。在django的模型中就体现为ForeignKey、ManyToManyField或者OneToOneField。而在业务逻辑上,需要体现为一张表单,对应着数据库里的多张表。

例如,我们希望录入合同,其中合同Model中还有地址Model和项目Model,而项目Model中又有地址Model,等等。

当然,我们有很多种实现的方案,但是,前面三部分说了那么多,不是浪费口水的。我们已经有了通用视图+ModelForm这样的利器,难道还需要手动去写Form表单?我们已经习惯了在Model里定义好类型和有点注释作用还能当label的verbose_name,还需要在forms.Form里再来一遍?还需要在视图中写这么通用的逻辑代码吗?当然不用。

inlineformset_factory是一种方案,但它限制太多,而且有些晦涩,我个人感觉是不太好用的。

那么,从第1部分我介绍的Form里的prefix,以及第3部分里类图中的ProcessFormView允许重定义form_valid,以及第2部分中ModelForm的save方法的行为控制,解决方案已经一目了然了。

拿上面提到的例子来说,我们创建合同时,指明了项目,包括项目地址和合同签订地址,这涉及到三张表和四条记录(地址表有两条)。

我们三张表的模型如下:

class PrimeContract(models.Model):
  address = models.ForeignKey(Address, related_name="prime_contract_address", verbose_name="address")
  project = models.ForeignKey(Project, related_name="prime_contract", verbose_name="project")
class Project(models.Model):
  address = models.ForeignKey(Address, related_name="project_address", verbose_name="project address")
class Address(models.Model):
  pass

接着,定义ModelForm表单,这非常简单:

class AddressForm(ModelForm):
  class Meta:
    model = Address
 fields = ...
class ProjectForm(ModelForm):
  class Meta:
    model = Project
 fields = ...
class PrimeContractForm(ModelForm):
  class Meta:
    model = PrimeContract
 fields = ...

再写视图,这里要重写2个方法:

class PrimeContractAdd(CreateView):
  success_url = ...
  template_name = ...
  form_class = PrimeContractForm
  def get_context_data(self, **kwargs):
    context = super(PrimeContractAdd, self).get_context_data(**kwargs)
    #SingleObjectMixin父类只会处理PrimeContractForm表单,另外三条数据库记录对应的表单我们要自己处理了,此时prefix派上用场了,因为Field重名是百分百的事
    if self.request.method == 'POST':
      contractAddressForm = AddressForm(self.request.POST, prefix='contractAddressForm')
      projectAddressForm = AddressForm(self.request.POST, prefix='projectAddressForm')
      projectForm = ProjectForm(self.request.POST, prefix='projectForm')
    else:
      contractAddressForm = AddressForm(prefix='contractAddressForm')
      projectAddressForm = AddressForm(prefix='projectAddressForm')
      projectForm = ProjectForm(prefix='projectForm')
    #注意要把自己处理的表单放到context上下文中,供模板文件使用
    context['contractAddressForm'] = contractAddressForm
    context['projectAddressForm'] = projectAddressForm
    context['projectForm'] = projectForm
    return context

	#重写form_valid,父类ProcessFormView会在PrimeContractForm表单is_valid方法返回True时调用该方法
  def form_valid(self, form):
	  #首先我们要获取到PrimeContractForm表单对应的模型,此时是不能save的,因为外键project和address对应的数据库记录还没有创建,所以commit传为False
    contract = form.save(commit=False)
		#获取上面get_context_data方法中在POST里得到的表单
    context = self.get_context_data()
		#按照四条数据库记录的顺序依次的创建(调用save方法)、主键赋到下一条记录的外键中、下一次记录创建(save)
    projectAddress = context['projectAddressForm'].save()
		#从项目表单中获取到模型,先把地址的id赋到外键上再保存
    project = context['projectForm'].save(commit=False)
    project.address = projectAddress
    project.save()
    contractAddress = context['contractAddressForm'].save()
		#将合同模型中的address和project都设置好后再保存
    contract.address = contractAddress
    contract.project = project
    contract.save()

    return super(PrimeContractAdd, self).form_valid(form)

最后写模板:

#这三个表单我们手动处理过的
{{ contractAddressForm }}
{{ projectAddressForm }}
{{ projectForm }}
#这是FormMixin父类帮我们生成的
{{ form }}

至此,我们可以只用几十行代码就完成复杂的功能,代码逻辑也清晰可控。

从这篇文章里也可以看得出,django实在是快速开发网站的必备神器!当然,快速不代表不能够支撑大并发的应用,instagram这个很火的服务就是用django写的。由于python和django过于灵活,都将要求django的开发者们唯有更资深才能写出生产环境下的服务。

(0)

相关推荐

  • Python的Django框架中forms表单类的使用方法详解

    Form表单的功能 自动生成HTML表单元素 检查表单数据的合法性 如果验证错误,重新显示表单(数据不会重置) 数据类型转换(字符类型的数据转换成相应的Python类型) Form相关的对象包括 Widget:用来渲染成HTML元素的工具,如:forms.Textarea对应HTML中的<textarea>标签 Field:Form对象中的一个字段,如:EmailField表示email字段,如果这个字段不是有效的email格式,就会产生错误. Form:一系列Field对象的集合,负责验证和

  • 在Django同1个页面中的多表单处理详解

    快速上手Django实现项目 近期公司在做1个海淘的项目,APP为pylot.由于时间比较赶,加上隔壁那哥们不在,只能自己挑大梁了.结果,当项目做出来之后,被领导狠狠的批了一顿,说怎么用django写,你能解决Django的内存问题吗,你能解决并发的问题吗?Django那么重. 然后我只好回答说,正是因为它重,所以人家拿来写大型项目.虽然这里不是为了上面这2个问题的,而是来说下如何快速开发原型的问题. 对于Django这样基于模型的Web框架,实话说真的解决了很多繁琐的工作.由于它1个模型对应1

  • 基于Django的ModelForm组件(详解)

    创建类 from django.forms import ModelForm from django.forms import widgets as wd from app01 import models class 类名(ModelForm): class Meta: model = models.表名 #models中的表名 fields="__all__", # 字段 exclude=None, # 排除字段 widgets=None, # 自定义插件 error_message

  • 改进Django中的表单的简单方法

    首先,search()视图对于空字符串的处理相当薄弱--仅显示一条"Please submit a search term."的提示信息. 若用户要重新填写表单必须自行点击"后退"按钮, 这种做法既糟糕又不专业.如果在现实的案例中,我们这样子编写,那么Django的优势将荡然无存. 在检测到空字符串时更好的解决方法是重新显示表单,并在表单上面给出错误提示以便用户立刻重新填写. 最简单的实现方法既是添加else分句重新显示表单,代码如下: from django.ht

  • 利用Django提供的ModelForm增删改数据的方法

    上一篇我们写了Django基于类如何增删改数据的方法,方法虽然简单,但新手可能对其原理不是很清楚,那么我们这次就用Django提供的ModelForm方法来实现增删改数据,这是一种基于现有模型的增删改方法. 一个简单的例子加以说明,前提是你已经具备Django创建项目应用的基本知识: 01.首先创建一个简单的模型,模型只有三个文本字段,标题 title , 内容 text , 添加日期 date_added ,如下: # models.py from django.db import model

  • 解决django中ModelForm多表单组合的问题

    django是python语言快速实现web服务的大杀器,其开发效率可以非常的高!但因为秉承了语言的灵活性,django框架又太灵活,以至于想实现任何功能都有种"条条大路通罗马"的感觉.这么多种选择放在一起,如何分出高下?我想此时的场景下就两个标准: 1.相同的功能用最少的代码实现(代码少BUG也会少): 2.相对最易于理解,从而易于维护和扩展.书归正传,web服务允许用户输入,基本上要靠表单.而django对表单的支持力度非常大,我们用不着在浏览器端的html文件里写大量<fo

  • 解决layui中的form表单与button的点击事件冲突问题

    layui的form表单位置和button标签的位置重合,会使得button的click事件得不到响应,如图: 蓝色底为form的位置,"重新搜索"为<button>标签的位置,此时,button标签的click事件无法响应. 必须将form的宽度减小,到不和button标签重合 如下图: 此时按钮正常响应click 以上这篇解决layui中的form表单与button的点击事件冲突问题就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

  • 解决django中form表单设置action后无法回到原页面的问题

    django中form表单设置action后,点提交按钮是跳转到action页面的,比如设置action为login,网址为192.168.1.128,跳转后便会来到192.168.1.128/login,F5刷新也会是重新提交表单对话框,无法回到原页面. 因此就要在django服务器进行重定向,具体就是 from django.shortcuts import redirect #最后返回原页面 return redirect(url) 补充知识:Django + Ajax发送POST表单,并

  • 解决Django中多条件查询的问题

    tags: django中对条件查询 一些cms项目都会使用到多条件查询,我们后端如何处理请求的条件呢? 满足一个条件 满足两个条件 满足多个条件 -------. 这样处理起来会非常的恼火. 其实有多方法比如(传参数,传字典,传Q对象,传F对象-)陷入深深的思考中-怎么用做简单的方法把这个需求解决了. 个人觉得.把我们的查询的所有条件来构建一个字典来查询起来比较高效.具体如何操作见下面的代码: 视图函数. def order_list(request): if request.method =

  • 解决Django中checkbox复选框的传值问题

    Django 中,html 页面通过 form 标签来传递表单数据. 对于复选框信息,即 checkbox 类型,点击 submit 后,数据将提交至 view 中的函数. 我们通过request.POST.get() 函数来获取来自 html 页面的值,但是该函数只能 get 到选中的最后一个值. 因此想要传递选中的多个值,需要用 request.POST.getlist() 函数 该函数返回一个列表,可通过迭代来获取列表中每一项的值. 补充知识:解决checkbox复选框选中传值,不选中不传

  • Django 构建模板form表单的两种方法

    通常情况下,我们想构建一张表单时会在模板文件login.html中写入 <form action="/your-name/" method="post"> <label for="your_name">Your name: </label> <input id="your_name" type="text" name="your_name" va

  • Django中ModelForm组件的简单配置与使用教程

    目录 1.前言 2.简单配置 3.创建模板文件,使用ModelForm组件 补充:django中的modelform快速上手 后言 1.前言 hello大家好,今天学习了modelform组件,所以自己总结一下内容,一来,是怕自己忘记了,二来,可以巩固下今天的知识,三来,或许我写的东西可以对一些初学Django的小伙伴带来帮助呢,哈哈,废话不多说,开始进入主题! 2.简单配置 创建好Django项目之后,首先到自己创建的app的models.py的文件下创建模型类,我就以一个添加学生的项目来介绍

  • 在Python的Flask框架中构建Web表单的教程

    尽管Flask的request对象提供的支持足以处理web表单,但依然有许多任务会变得单调且重复.表单的HTML代码生成和验证提交的表单数据就是两个很好的例子. Flask-WTF扩展使得处理web表单能获得更愉快的体验.该扩展是一个封装了与框架无关的WTForms包的Flask集成. Flask-WTF和它的依赖集可以通过pip来安装: (venv) $ pip install flask-wtf 1.跨站请求伪造(CSRF)保护 默认情况下,Flask-WTF保护各种形式对跨站请求伪造(CS

  • 在Python的Flask中使用WTForms表单框架的基础教程

    下载和安装 安装 WTForms 最简单的方式是使用 easy_install 和 pip: easy_install WTForms # or pip install WTForms 你可以从 PyPI 手动 下载 WTForms 然后运行 python setup.py install . 如果你是那种喜欢这一切风险的人, 就运行来自 Git 的最新版本, 你能够获取最新变更集的 打包版本, 或者前往 项目主页 克隆代码仓库. 主要概念 Forms 类是 WTForms 的核心容器. 表单(

随机推荐