django随笔 : 申请表单
要实现下图所示的申请表单。其中:
学校名称、联系电话必填;
学校地址、联系人、邮箱选填;
关注的室内环境必须选择,可以多选。
申请表单的数据需要保存到数据库中。
申请试用表单.png按照Django实现的基本流程,实现这个功能需要完成以下工作:
- 定义保存申请表内容的模型;
- 创建表单和视图来处理申请表单验证及逻辑问题;
- 创建模板、设置URL模式来实现表单。
实现代码
模型
class Questions(models.Model):
def __unicode__(self):
return smart_unicode(self.describe)
class Meta:
verbose_name = u'关注的环境问题'
verbose_name_plural = u'关注的环境问题'
describe = models.CharField(verbose_name=u'问题描述', max_length=64)
class Applications(models.Model):
def __unicode__(self):
return smart_unicode('school :{0},connect:{1}'.format(self.school_name,
self.connect_tel))
class Meta:
verbose_name = u'申请试用'
verbose_name_plural = u'申请试用列表'
PROBLEMS_CHOICES = (
(1, u'室内空气质量(PM2.5、PM10、CO2等)'), (2, u'室内热舒适(温度、湿度等)'),
(3, u'室内装修污染(甲醛、苯等)'))
user = models.ForeignKey(UserProfile, related_name='application')
school_name = models.CharField(verbose_name=u'学校名称', max_length=32)
school_address = models.CharField(verbose_name=u'学校地址', max_length=64,
blank=True, null=True)
problems = models.ManyToManyField(Questions, verbose_name=u'关注的室内环境')
# problems = models.SmallIntegerField(verbose_name='关注的室内环境',
# choices=PROBLEMS_CHOICES)
room_num = models.CharField(verbose_name=u'教室数量', max_length=8, blank=True,
null=True)
connect_name = models.CharField(verbose_name=u'联系人', max_length=16,
blank=True, null=True)
connect_tel = models.CharField(verbose_name=u'联系电话', max_length=16)
e_mail = models.EmailField(verbose_name=u'邮箱', null=True, blank=True)
created = models.DateField(verbose_name=u'创建时间', auto_now_add=True,
db_index=True)
def get_absolute_url(self):
return reverse('application-detail', kwargs={'pk': self.pk})
表单
from wechatmp.models import Applications
import re
# 正则匹配电话号码
tel_re = re.compile(r'^\(?0\d{2,3}[\) -]?\d{7,8}$')
# 验证手机号码
# 移动号段:134\135\136\137\138\139\147\148\150\151\152\157\158\159
# 172\178\182\183\184\187\188\198
# 联通号段:130\131\132\145\146\155\156\166
# 171\175\176\185\186
# 电信号段:133\149\153
# 173\174\177\180\181\189\199
# 虚拟运营商: 170
# ^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\\d{8}$
phone_re = re.compile(
'^1[38]\d{9}$|^14[5-9]\d{8}$|^15[0-3,5-9]\d{8}$|^166\d{8}$^17[0-8]\d{8}$|^19[89]\d{8}$')
class ApplicationForm(forms.ModelForm):
class Meta:
model = Applications
widgets = {'problems': forms.CheckboxSelectMultiple()}
exclude = ['user']
def clean_connect_tel(self):
connect_tel = self.cleaned_data['connect_tel']
tel_match = tel_re.match(connect_tel) or phone_re.match(connect_tel)
if not tel_match:
raise forms.ValidationError(u'您输入的联系方式有误,请修改')
return connect_tel
视图
from django.views.generic import CreateView
from wechat_api import WechatMixin
from wechatmp.forms import ApplicationForm
class ApplicationTest(WechatMixin, CreateView):
form_class = ApplicationForm
def get_success_url(self):
return reverse('app_name:view_name')
def get_context_data(self, **kwargs):
context = super(ApplicationView, self).get_context_data()
context['user'] = self.deal_openid(self.request)
return context
def form_valid(self, form):
form.instance.user = self.deal_openid(self.request)
return super(ApplicationTest, self).form_valid(form)
模板
{% extends "base_wechat.html" %}
{% load static %}
{% block context %}
<div class="m-group view12">
<div class="main">
<img src="{% static 'wechatmp/images/banner.jpg' %}" alt="">
{% with user.application.count as commit_num %}
{% if commit_num > 0 %}
<p>您已经提交了{{ commit_num }}条申请。</p>
{% endif %}
{% if commit_num > 100 %}
<p>请直接联系我们,联系电话:13673263290。</p>
{% else %}
<form method="post" action=".">
<ul>
{% csrf_token %}
{{ form.as_ul }}
{# <input type="hidden" name="user" value="{{ user.id }}"/>#}
<li><input class="sub" type="submit" value="提交" name=""></li>
</ul>
</form>
{% endif %}
{% endwith %}
</div>
</div>
{% endblock %}
URL
url(r'^application/$', ApplicationView.as_view(),
name='applicaiton')
详细分析
模型
在应用的 models.py 文件中添加以下模型:
class Applications(models.Model):
def __unicode__(self):
return smart_unicode('school :{0},connect:{1}'.format(self.school_name,
self.connect_tel))
class Meta:
verbose_name = u'申请试用'
verbose_name_plural = u'申请试用列表'
user = models.ForeignKey(UserProfile, related_name='application')
school_name = models.CharField(verbose_name=u'学校名称', max_length=32)
school_address = models.CharField(verbose_name=u'学校地址', max_length=64,
blank=True, null=True)
problems = models.ManyToManyField(Questions, verbose_name=u'关注的室内环境')
room_num = models.CharField(verbose_name=u'教室数量', max_length=8, blank=True,
null=True)
connect_name = models.CharField(verbose_name=u'联系人', max_length=16,
blank=True, null=True)
connect_tel = models.CharField(verbose_name=u'联系电话', max_length=16)
e_mail = models.EmailField(verbose_name=u'邮箱', null=True, blank=True)
created = models.DateField(verbose_name=u'创建时间', auto_now_add=True,
db_index=True)
这个模型用于保存用户的申请信息。这个模型的字段包括:
-
user : 申请试用的 UserProfile 对象,这里的UserProfile是一个保存微信用户信息的模型。这里用的是外键( ForeignKey ),它指定了一个一对多关系。一个用户可以多次申请试用,但是每个申请试用只有一个用户。
-
school_name :学校名称,该字段为 CharField ,必须设定 max_length参数。
-
school_address :学校地址,CharField 字段,blank=True 表示表单中的该字段可以为空,null=True 表示数据库表中该字段内容可以为空。
-
problems:关注的室内环境问题,由于要求可以多选,因此,这里用了ManyToManyField 实现多对多关系。对应的多对多关系模型为Questions:
class Questions(models.Model): def __unicode__(self): return smart_unicode(self.describe) class Meta: verbose_name = u'关注的环境问题' verbose_name_plural = u'关注的环境问题' describe = models.CharField(verbose_name=u'问题描述', max_length=64)
如果要求单选,则可以不使用多对多模型,直接使用字段的choice解决:
PROBLEMS_CHOICES = ( (1, u'室内空气质量(PM2.5、PM10、CO2等)'), (2, u'室内热舒适(温度、湿度等)'), (3, u'室内装修污染(甲醛、苯等)')) problems = models.SmallIntegerField(verbose_name='关注的室内环境', choices=PROBLEMS_CHOICES)
这里的 PROBLEMS_CHOICES 为二元组,元组内部元组第一个元素为存储到数据库的数值,第二个元素为显示在页面上的内容。
-
room_num:房间数量,为了显示格式统一,这里使用 models.CharField 字段,也可以使用models.IntegerField。
-
connect_name:联系人。
-
connect_tel:联系电话。
-
e_mail:联系邮箱,采用 models.EmailField 字段,将验证输入的格式是否为 e_mail 格式。
-
created:创建时间,采用 models.DataField ,这里设置 auto_now_add 为 True ,将在添加记录时自定添加创建时间,db_index 为 True ,这样 Django 将在数据库中为这个字段创建一个索引。
模型创建完成后,运行以下命令生成迁移文件:
python manage.py makemigrations
然后运行以下命令同步到数据库:
python manage.py migrate
项目使用的是 PostGreSQL 数据库,在数据库中可以看到新增加了三个数据库表:
- wechatmp_applications
- wechatmp_questions
- wechatmp_applications_problems
每个数据库表中的 wechatmp 为 Django 应用的名称,applications 和 questions 分别为模型的名称,wechatmp_applications_problems为 applications 模型中 problems字段定义的多对对关系建立的模型,该模型有id、applications_id、questions_id 三个字段。
注意:
由于wechatmp_applications_problems实现了多对多关系,因此applications数据库表中没有problems字段。
HTML表单
申请表单的通过HTML中的<form>...<form>格式的元素实现,它接收访问者输入的文本、选项、操作对象等操作,然后将信息发送到服务器。本文中实现申请表单的HTML表单为:
<form method="post" action=".">
<ul>
<li><label for="id_school_name">学校名称:</label> <input type="text" name="school_name" required id="id_school_name" maxlength="32" /></li>
<li><label for="id_school_address">学校地址:</label> <input type="text" name="school_address" id="id_school_address" maxlength="64" /></li>
<li><label>关注的室内环境:</label>
<ul id="id_problems">
<li><label for="id_problems_0"><input type="checkbox" name="problems" value="1" id="id_problems_0" />室内空气质量(PM2.5、PM10、CO2等)</label></li>
<li><label for="id_problems_1"><input type="checkbox" name="problems" value="2" id="id_problems_1" />室内热舒适(温度、湿度等)</label></li>
<li><label for="id_problems_2"><input type="checkbox" name="problems" value="3" id="id_problems_2" />室内装修污染(甲醛、苯等)</label></li>
</ul>
</li>
<li><label for="id_room_num">教室数量:</label> <input type="text" name="room_num" id="id_room_num" maxlength="8" /></li>
<li><label for="id_connect_name">联系人:</label> <input type="text" name="connect_name" id="id_connect_name" maxlength="16" /></li>
<li><label for="id_connect_tel">联系电话:</label> <input type="text" name="connect_tel" required id="id_connect_tel" maxlength="16" /></li>
<li><label for="id_e_mail">邮箱:</label> <input type="email" name="e_mail" id="id_e_mail" maxlength="254" /></li>
<li><input class="sub" type="submit" value="提交" name=""></li>
</ul>
</form>
从上述HTML中可以看到表单的用户输入字段通过<li><label></label><input></li>实现,多选字段通过<li><label></label><ul></ul></li>实现,除了各字段,form还需要设定属性:
<form method="post" action=".">
- method:返回数据使用的HTTP方法,可以选择GET或POST,本例需要将数据写入数据库,因此使用POST。
- action:用户输入数据后应该跳转到的URL,这里使用'.'表示跳转到本页。
此外还需要submit类型的<input>元素用于提交数据:
<li><input class="sub" type="submit" value="提交" name=""></li>
本节中的HTML表单可以显示我们需要的申请表单,我们可以直接将其保存到HTML文件中,此时,我们还有下面的问题需要解决:
如何验证用户输入的数据?
如果用户输入的数据通过验证,如何保存用户输入的数据?
如果用户输入的数据无法通过验证,如何进行错误提示?
Django通过Form和FormView提供表单功能,可以帮助我们自动解决上述问题的大部分内容。
django表单(Form)
这里的Form 是指 Django 的 Form 类,它主要用于实现以下两项功能:
-
创建HTML表单,即自动生成我们上面写出的HTML表单,本文中的表单手写问题还不大,但是如果表单的字段过多,则非常费时费力;
-
验证用户输入的数据,
如果用户输入的数据通过验证,保存用户输入的数据;
如果用户输入的数据无法通过验证,输出验证错误信息;
创建HTML表单
可以分别通过继承forms.Form和forms.ModelForm实现创建HTML表单。
建议通过继承forms.ModelForm实现,这样便于django 视图通过 ModelForm.save() 方法将数据保存到数据库中。
通过forms.Form实现
我们已经看到了上一节中HTML表单的样子,在Django中这样实现:
from django import forms
from wechatmp.models import Questions
class ApplicationForm(forms.Form):
school_name = forms.CharField(label=u'学校名称', max_length=32)
school_address = forms.CharField(label=u'学校地址', max_length=64)
problems = forms.ModelMultipleChoiceField(label=u'关注的室内环境',queryset=Questions.objects.all())
room_num = forms.CharField(label=u'教室数量', max_length=8)
connect_name = forms.CharField(label=u'联系人', max_length=16)
connect_tel = forms.CharField(label=u'联系电话', max_length=16)
e_mail = models.EmailField(label=u'邮箱')
我们定义了7个字段的 Form 类,这与定义模型的方式非常相似,区别在于:
- 字段使用 label 定义了显示在HTML<label>中的内容;
- problems字段使用的是forms.MultipleChoiceField 实现多选。
定义表单后,我们可以将原来的HTML表单简化为:
<form method="post" action="." >
{% csrf_token %}
{{ form.as_ul }}
<li><input class="sub" type="submit" value="提交" name=""></li>
</form>
其中:
{{ form.as_ul }}
中,form为我们定义的 ApplicationForm 表单实例,as_ul 将渲染为<li>标签封装的内容。
{% csrf_token %}为django CSRF防护的校验字段,form采用post方式发送数据时,HTML <form>需要添加该元素。
这样,我们可以简化实现上面HTML表单中的工作。
使用forms.ModelForm实现
我们可以发现模型和表单几乎定义了相同的字段,为了简化这种情况,Django 提供ModelForm 类来实现根据模型创建表单,这样我们的表单可以简化为:
from django import forms
class ApplicationForm(forms.ModelForm):
class Meta:
model = Applications
widgets = {'problems': forms.CheckboxSelectMultiple()}
exclude = ['user']
简化后的Django 表单 与 原表单相比,具有以下不同:
- ApplicationFrom继承的类由 forms.Form 变为了 forms.ModelForm;
- 原来ApplicationForm中的字段全都去掉了;
- 新的ApplicationForm中增加了 Meta 类,它定义了三项内容:
- model:表示表单对应的模型;
- widgets:可用于重定义字段使用的显示控件,这一项可以根据需要进行设定;
- exclude:可以表单中排除模型中的字段列表,即在本例中,模型中的user字段不显示在表单中。这里也可以使用fields定义要显示的字段列表。exclude和field只需要定义其中一个即可。
forms.Form与forms.ModelForm的区别
本节中的两个表单分别集成了 forms.Form 和 forms.ModelForm,它们的关系是:
-
forms.ModelForm可以看做forms.Form的子类,forms.ModelForm继承的BaseModelForm, forms.Form继承的BaseForm,BaseModelForm继承BaseForm,因此,form.ModelForm具备forms.Form的基本功能;
-
forms.ModelForm 额外增加了与模型相关的方法,最主要的是增加了 save() 、save_m2m()方法来保存模型数据,其中save_m2m()用于save(commit=False)情况下手动保存多对多关系。
验证用户输入的数据
forms.Form验证
Django 的 forms.Form(forms.ModelForm) 提供两步的用户输入数据验证,它们都可以通过定义方法覆盖原方法来实现自定义。
clean_<foo>
该方法用于验证表单中的一个字段,<foo>为字段名称,我们可以在自己的表单中定义该方法来进行进一步的字段验证,比如,我们要进行进一步的电话号码验证,可以这样实现:
from wechatmp.models import Applications
import re
# 正则匹配电话号码
tel_re = re.compile(r'^\(?0\d{2,3}[\) -]?\d{7,8}$')
# 验证手机号码
# 移动号段:134\135\136\137\138\139\147\148\150\151\152\157\158\159
# 172\178\182\183\184\187\188\198
# 联通号段:130\131\132\145\146\155\156\166
# 171\175\176\185\186
# 电信号段:133\149\153
# 173\174\177\180\181\189\199
# 虚拟运营商: 170
# ^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\\d{8}$
phone_re = re.compile(
'^1[38]\d{9}$|^14[5-9]\d{8}$|^15[0-3,5-9]\d{8}$|^166\d{8}$^17[0-8]\d{8}$|^19[89]\d{8}$')
class ApplicationForm(forms.ModelForm):
class Meta:
model = Applications
widgets = {'problems': forms.CheckboxSelectMultiple()}
exclude = ['user']
def clean_connect_tel(self):
connect_tel = self.cleaned_data['connect_tel']
tel_match = tel_re.match(connect_tel) or phone_re.match(connect_tel)
if not tel_match:
raise forms.ValidationError(u'您输入的联系方式有误,请修改')
return connect_tel
clean
用于进行表单级别的验证,如果要组合表单中的不同字段进行验证,可以重写该方法,该方法的源代码为:
def clean(self):
"""
Hook for doing any extra form-wide cleaning after Field.clean() has been
called on every field. Any ValidationError raised by this method will
not be associated with a particular field; it will have a special-case
association with the field named '__all__'.
"""
return self.cleaned_data
比如,某个表单需要有since、until两个字段表示用户输入的起始日期,那么我们可以覆盖clean方法限制用户输入的since和until:
def clean(self):
since = self.cleaned_data['since']
until = self.cleaned_data['until']
if since > date.today():
raise forms.ValidationError('start date later than today',code='invalid_date_range')
if since > until:
raise forms.ValidationError('ends before starts',code='invalid_date_range')
return self.cleaned_data
我们从上面两个级别的验证可以看到,我们使用raise forms.ValidationError
表示验证失败。Django 将为我们处理这些错误,对于 clean_<foo> 引发的验证错误可以通过访问 form.<foo>.errors 获得,对于clean的引发的验证错误可以通过访问 form.non_field_errors 获得。这样我们可以修改HTML来显示验证错误:
<form method="post" action="." >
{% csrf_token %}
{{ field.non_field_errors }}
{% for field in form %}
<li>
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</li>
{% endfor %}
<li><input class="sub" type="submit" value="提交" name=""></li>
</form>
forms.ModelForm验证
forms.ModelForm除了进行上述的forms.Form验证,还将对设置unique、unique_together或unique_for_date|month|year的字段进行唯一性验证validate_unique()。
什么时候进行模型验证
上述两种表单,一般都在调用表单的is_valid()方法时进行验证。
Django 视图
用于进行表单处理的视图需要实现以下三种工作:
-
初始GET(空或者预填充表单)
-
具有无效数据的POST(通常重新显示带有错误的表单)
-
具有有效数据的POST(处理数据并通常重定向)
这里可以通过继承 django 的类视图 FormView 或者 CreateView 实现模型表单的处理。其中:
FormView 用于处理表单视图。
CreateView 用于处理模型表单视图。
通过重写部分代码,两种方法都可以实现功能,可以根据业务逻辑选择要使用的方法。
当然,我们也可以自己写视图来处理表单,但是 类视图已经实现了部分工作,我们只需要根据自己的需求更改部分内容即可,这样速度更快,而且不容易出错。
继承 FormView
FormView已经帮我们实现了大部分工作,它的详细分析见 django 类函数分析-Form视图。这里要更改的只有以下四点:
-
设置类的 template_name 属性,指定表单使用的模板:
template_name = 'wechatmp/application.html'
-
设置类的 form_class 属性,指定要使用的表单类:
form_class = ApplicationForm
通过这两步的设置 FormView 可以帮助我们实现初始GET 和具有无效数据的POST 处理,下面的设置帮助我们处理具有有效数据的POST :
-
重写get_success_url()方法,确定具有有效数据的POST完成后要跳转的页面:
def get_success_url(self): return reverse('app_name:view_name')
-
重写 form_valid() 方法将POST的有效数据保存到数据库:
def form_valid(self, form): form.instance.user = self.deal_openid(self.request) form.save() return super(ApplicationView, self).form_valid(form)
这里,如果使用django权限系统,也可以使用以下代码得到表单的用户信息:
form.instance.user = self.request.user
上面4项构成了视图的整套代码:
class ApplicationView(WechatMixin, FormView):
template_name = 'wechatmp/application.html'
form_class = ApplicationForm
def get_success_url(self):
return reverse('app_name:view_name')
def form_valid(self, form):
form.instance.user = self.deal_openid(self.request)
form.save()
return super(ApplicationView, self).form_valid(form)
这就是继承 FormView 实现表单处理的全部工作。
继承 CreateView
CreateView 与 FormView 相比,除了FormView 具备的功能,它还实现了以下三点:
-
设置了默认模板的名称,默认名称为 ModelForm 使用的模型名称小写加上‘_form' 后缀,本例中为'wechatmp/application_form.html',我们只需在templates文件夹的wechatmp文件夹下添加该模板即可,无需再定义 template_name 属性。
注意:
我们可以通过设置 template_name_suffix 来更换'_form'后缀;
我们可以设置 template_name,如果设置了 template_name ,将使用设置的 template_name。
-
可以提供默认的跳转页面。
用户可以通过继承 FormView 中使用的get_success_url() 设置成功跳转页面。
如果用户没有提供,CreateView 将使用 ModelForm 使用的模型的 get_absolute_url(),跳转到表单创建的模型对象的详情页面。实现这项功能需要提供一下两项内容:
- ModelForm 使用的模型需要定义 get_absolute_url()方法;
- 需要为get_absolute_url()方法定义的url 实现相应的 视图、模板和url。
-
可以使用验证成功的数据创建模型实例,并保存到数据库。
这样,使用 CreateView 最简单的方法只需要设置 form_class 属性:
form_class = ApplicationForm
如果 ModelForm 没有需要定义的特殊情况,我们甚至可以省略 django表单一节定义的 ApplicationForm。直接在视图中定义以下内容即可:
model = Applications
exclude = ['user']
由于表单中没有设置 user 字段的值,因此,我们还需要对 form_valid() 进行部分更改:
def form_valid(self, form):
form.instance.user = self.deal_openid(self.request)
return super(ApplicationTest, self).form_valid(form)
也就是说,继承CreateView的视图,只需下列代码即可实现功能:
class ApplicationTest(WechatMixin, CreateView):
form_class = ApplicationForm
def form_valid(self, form):
form.instance.user = self.deal_openid(self.request)
return super(ApplicationTest, self).form_valid(form)
视图对应的模板
{% extends "base_wechat.html" %}
{% load static %}
{% block context %}
<div class="m-group view12">
<div class="main">
<img src="{% static 'wechatmp/images/banner.jpg' %}" alt="">
{% with user.application.count as commit_num %}
{% if commit_num > 0 %}
<p>您已经提交了{{ commit_num }}条申请。</p>
{% endif %}
{% if commit_num > 10 %}
<p>请直接联系我们,联系电话:****。</p>
{% else %}
<form method="post" action=".">
<ul>
{% csrf_token %}
{{ form.as_ul }}
{# <input type="hidden" name="user" value="{{ user.id }}"/>#}
<li><input class="sub" type="submit" value="提交" name=""></li>
</ul>
</form>
{% endif %}
{% endwith %}
</div>
</div>
{% endblock %}
这里的user 通过在视图中重写 get_context_data() 方法实现的,如果使用django的权限系统,可以直接将user改为request.user。