《Django By Example》djangopython

django随笔 : 申请表单

2017-12-27  本文已影响89人  学以致用123

要实现下图所示的申请表单。其中:

​ 学校名称、联系电话必填;

​ 学校地址、联系人、邮箱选填;

​ 关注的室内环境必须选择,可以多选。

申请表单的数据需要保存到数据库中。

申请试用表单.png

按照Django实现的基本流程,实现这个功能需要完成以下工作:

  1. 定义保存申请表内容的模型;
  2. 创建表单和视图来处理申请表单验证及逻辑问题;
  3. 创建模板、设置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)

这个模型用于保存用户的申请信息。这个模型的字段包括:

模型创建完成后,运行以下命令生成迁移文件:

python manage.py makemigrations

然后运行以下命令同步到数据库:

python manage.py migrate

项目使用的是 PostGreSQL 数据库,在数据库中可以看到新增加了三个数据库表:

  1. wechatmp_applications
  2. wechatmp_questions
  3. 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=".">

此外还需要submit类型的<input>元素用于提交数据:

 <li><input class="sub" type="submit" value="提交"  name=""></li>

本节中的HTML表单可以显示我们需要的申请表单,我们可以直接将其保存到HTML文件中,此时,我们还有下面的问题需要解决:

​ 如何验证用户输入的数据?

​ 如果用户输入的数据通过验证,如何保存用户输入的数据?

​ 如果用户输入的数据无法通过验证,如何进行错误提示?

Django通过Form和FormView提供表单功能,可以帮助我们自动解决上述问题的大部分内容。

django表单(Form)

这里的Form 是指 Django 的 Form 类,它主要用于实现以下两项功能:

  1. 创建HTML表单,即自动生成我们上面写出的HTML表单,本文中的表单手写问题还不大,但是如果表单的字段过多,则非常费时费力;

  2. 验证用户输入的数据,

    如果用户输入的数据通过验证,保存用户输入的数据;

    如果用户输入的数据无法通过验证,输出验证错误信息;

创建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 类,这与定义模型的方式非常相似,区别在于:

  1. 字段使用 label 定义了显示在HTML<label>中的内容;
  2. 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 表单 与 原表单相比,具有以下不同:

  1. ApplicationFrom继承的类由 forms.Form 变为了 forms.ModelForm;
  2. 原来ApplicationForm中的字段全都去掉了;
  3. 新的ApplicationForm中增加了 Meta 类,它定义了三项内容:
    • model:表示表单对应的模型;
    • widgets:可用于重定义字段使用的显示控件,这一项可以根据需要进行设定;
    • exclude:可以表单中排除模型中的字段列表,即在本例中,模型中的user字段不显示在表单中。这里也可以使用fields定义要显示的字段列表。exclude和field只需要定义其中一个即可。
forms.Form与forms.ModelForm的区别

本节中的两个表单分别集成了 forms.Form 和 forms.ModelForm,它们的关系是:

  1. forms.ModelForm可以看做forms.Form的子类,forms.ModelForm继承的BaseModelForm, forms.Form继承的BaseForm,BaseModelForm继承BaseForm,因此,form.ModelForm具备forms.Form的基本功能;

  2. 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 视图

用于进行表单处理的视图需要实现以下三种工作:

  1. 初始GET(空或者预填充表单)

  2. 具有无效数据的POST(通常重新显示带有错误的表单)

  3. 具有有效数据的POST(处理数据并通常重定向)

这里可以通过继承 django 的类视图 FormView 或者 CreateView 实现模型表单的处理。其中:

FormView 用于处理表单视图。

CreateView 用于处理模型表单视图。

通过重写部分代码,两种方法都可以实现功能,可以根据业务逻辑选择要使用的方法。

当然,我们也可以自己写视图来处理表单,但是 类视图已经实现了部分工作,我们只需要根据自己的需求更改部分内容即可,这样速度更快,而且不容易出错。

继承 FormView

FormView已经帮我们实现了大部分工作,它的详细分析见 django 类函数分析-Form视图。这里要更改的只有以下四点:

  1. 设置类的 template_name 属性,指定表单使用的模板:

    template_name = 'wechatmp/application.html'
    
  2. 设置类的 form_class 属性,指定要使用的表单类:

    form_class = ApplicationForm
    

通过这两步的设置 FormView 可以帮助我们实现初始GET 和具有无效数据的POST 处理,下面的设置帮助我们处理具有有效数据的POST :

  1. 重写get_success_url()方法,确定具有有效数据的POST完成后要跳转的页面:

    def get_success_url(self):
        return reverse('app_name:view_name')
    
  2. 重写 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 具备的功能,它还实现了以下三点:

  1. 设置了默认模板的名称,默认名称为 ModelForm 使用的模型名称小写加上‘_form' 后缀,本例中为'wechatmp/application_form.html',我们只需在templates文件夹的wechatmp文件夹下添加该模板即可,无需再定义 template_name 属性。

    注意:

    我们可以通过设置 template_name_suffix 来更换'_form'后缀;

    我们可以设置 template_name,如果设置了 template_name ,将使用设置的 template_name。

  2. 可以提供默认的跳转页面。

    用户可以通过继承 FormView 中使用的get_success_url() 设置成功跳转页面。

    如果用户没有提供,CreateView 将使用 ModelForm 使用的模型的 get_absolute_url(),跳转到表单创建的模型对象的详情页面。实现这项功能需要提供一下两项内容:

    • ModelForm 使用的模型需要定义 get_absolute_url()方法;
    • 需要为get_absolute_url()方法定义的url 实现相应的 视图、模板和url。
  3. 可以使用验证成功的数据创建模型实例,并保存到数据库。

这样,使用 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。

上一篇下一篇

猜你喜欢

热点阅读