Django组件之Form表单

一、Django中的Form表单介绍

我们之前在HTML页面中利用form表单向后端提交数据时,都会写一些获取用户输入的标签并且用form标签把它们包起来。

与此同时我们在好多场景下都需要对用户的输入做校验,比如校验用户是否输入,输入的长度和格式等正不正确。如果用户输入的内容有错误就需要在页面上相应的位置显示对应的错误信息.。

Django form组件就实现了上面所述的功能:
    生成页面可用的HTML标签
    对用户提交的数据进行校验
    保留上次输入内容

 

二、普通方式的form表单注册

def register(request):
    error_msg = ""
    if request.method == "POST":
        username = request.POST.get("username")
        pwd = request.POST.get("pwd")
        # 对注册信息做校验
        if len(username) < 6:
            # 用户长度小于6位
            error_msg = "用户名长度不能小于6位"
        else:
            # 将用户名和密码存到数据库
            UserInfo.objects.create(username='username', password='pwd')
            return redirect("/login/")
    return render(request, "register.html", {"error_msg": error_msg})
1.views.py
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta http-equiv="content-type" charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>注册页面</title>
</head>
<body>
<form action="/register/" method="post">
    {% csrf_token %}
    <p>
        <label for="t1">用户名</label>
        <input type="text" name="username" id="t1">
    </p>
    <p>
        <label for="p1">密码</label>
        <input type="password" name="pwd" id="p1">
    </p>
    <p>
        <input type="submit" value="注册">
        <p style="color: red">{{ error_msg }}</p>
    </p>
</form>
</body>
</html>
2.regirest.html

 

三、使用form组件实现注册功能

数据库
class UserInfo(models.Model):
    username = models.CharField(max_length=12)
    password = models.CharField(max_length=20)


1、views.py
1.先定义好一个RegForm类
from django import forms

# 按照Django form组件的要求自己写一个类
class RegForm(forms.Form):
    name = forms.CharField(max_length=12, label="用户名")
    pwd = forms.CharField(min_length=6, max_length=18, label="密码")


2.再写一个视图函数
# 使用form组件实现注册方式
def register(request):
    form_obj = RegForm()
    if request.method == "POST":
        # 实例化form对象的时候,把post提交过来的数据直接传进去
        form_obj = RegForm(request.POST)
        # 调用form_obj校验数据的方法is_valid
        if form_obj.is_valid():
            username = form_obj.cleaned_data.get('name')  # cleaned_data会自动把form提交的数据提取出来形成一个字典
            pwd = form_obj.cleaned_data.get('pwd')
            UserInfo.objects.create(username=username, password=pwd)
            return redirect("/login/")
        else:
            # 如果数据有问题
            return render(request, "register.html", {'form_obj': form_obj})
    return render(request, "register.html", {"form_obj": form_obj})
View Code

 

2、regirest.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta http-equiv="content-type" charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>注册页面</title>
<body>
    <form action="/register/" method="post" novalidate autocomplete="off">
        {% csrf_token %}
        <div>
            <label for="{{ form_obj.name.id_for_label }}">{{ form_obj.name.label }}</label>
            {{ form_obj.name }} <span>{{ form_obj.name.errors.0 }}</span>
        </div>
        <div>
            <label for="{{ form_obj.pwd.id_for_label }}">{{ form_obj.pwd.label }}</label>
            {{ form_obj.pwd }} <span>{{ form_obj.pwd.errors.0 }}</span>
        </div>
        <div>
            <input type="submit" class="btn btn-success" value="注册">
        </div>
    </form>
</body>
</html>
View Code

3、分析
form的功能:
前端页面是使用Django模板语言和form类的对象生成的                     -->生成HTML标签功能
当用户名和密码输入为空或输错form组件会默认为我们设置错误信息          -->用户提交校验功能
当用户输错之后仍保留着上次输入的内容在input框                        -->保留上次输入内容

简析后端:
form_obj = RegForm(request.POST)    把POST提交的数据存到RegForm进行实例化
form_obj.is_valid()                 自动帮我们去校验form_obj接收到的数据
form_obj.cleaned_data               如果校验通过,会把信息存到cleaned_data,在后端cleaned_data其实是一个字典
                                    取值:form_obj.cleaned_data['name']或者form_obj.cleaned_data.get('name')
form_obj.errors                     如果校验不通过,会把错误信息存到errors,在后端errors其实是一个字典
                                    取值:form_obj.errors['name']或者form_obj.errors.get('name')

简析_HTML:
name字段
{{ form_obj.name }}                 自动生成一个input框,属性就是我们在views.py里面设置的form类的属性
{{ form_obj.name.id_for_label }}    关联自动生成的input框
{{ form_obj.name.label }}           form对象的name的label属性的值
{{ form_obj.name.errors.0 }}        错误信息



pwd字段
{{ form_obj.pwd }}
{{ form_obj.pwd.id_for_label }}
{{ form_obj.pwd.label }}   
{{ form_obj.pwd.errors.0 }} 




也可以使用循环
{% for field in form_obj %}
    {{ field }}
    {{ field.id_for_label }}
    {{ field.label }}
    {{ field.errors.0 }}
{% endfor %}

 

四、Form常用字段与插件

创建Form类时,主要涉及到 【字段】 和 【插件】,字段用于对用户请求数据的验证,插件用于自动生成HTML;


1、required
设置这个字段必须要填,默认也是True必填的

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        required=False  # 设置成不是必须要填
    )
    pwd = forms.CharField(min_length=6, label="密码")


2、initial
设置input框里面的初始值。

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三"  # 设置默认值
    )
    pwd = forms.CharField(min_length=6, label="密码")


3、error_messages
1.局部重写错误信息,哪个字段需要重写错误信息就在哪个字段设置

class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三",
        error_messages={
            "required": "不能为空",
            "invalid": "格式错误",
            "min_length": "用户名最短8位"
        }
    )
    pwd = forms.CharField(min_length=6, label="密码")

2.设置全局错误信息(把默认的错误信息由英文改成中文)
在settings.py里面设置
# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'zh-hans'  # 把语言改成中文(汉字)

# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Shanghai'  # 时区改成亚洲上海


4、widget
设置input框的type类型,默认类型是text,密码框的type应该设置为password
还可以设置input框的属性,比如class的样式

class LoginForm(forms.Form):
    ...
    pwd = forms.CharField(
        min_length=6,
        label="密码",
        # 把密码框设置为password类型,并指定class为c1和c2的样式类
        # 当出现出错时,其他类型的input框默认是保留你写的内容,只有密码框不会保存
        # 想要密码框错误时也保留内容,就设置render_value=True
        widget=forms.widgets.PasswordInput(attrs={'class': 'c1 c2'}, render_value=True)  
    )


5、选择字段:ChoiceField和MultipleChoiceField
注意:在选择字段中
     单选字段使用:forms.fields.ChoiceField 也可以写成:forms.ChoiceField
     多选字段使用:forms.fields.MultipleChoiceField 也可以写成:forms.MultipleChoiceField


1.单选框radio
class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三",
        error_messages={
            "required": "不能为空",
            "invalid": "格式错误",
            "min_length": "用户名最短8位"
        }
    )
    pwd = forms.CharField(min_length=6, label="密码")
    gender = forms.fields.ChoiceField(
        choices=((1, ""), (2, ""), (3, "保密")),
        label="性别",
        initial=3,
        widget=forms.widgets.RadioSelect()
    )


2.单选下拉框select
class LoginForm(forms.Form):
    ...
    hobby = forms.fields.ChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ),
        label="爱好",
        initial=3,
        widget=forms.widgets.Select()
    )


3.单选checkbox
class LoginForm(forms.Form):
    ...
    keep = forms.fields.ChoiceField(
        choices=((True, ""), (False, ""))
        label="是否记住密码",
        initial="checked",
        widget=forms.widgets.CheckboxInput()
    )


4.多选select
class LoginForm(forms.Form):
    ...
    hobby = forms.fields.MultipleChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ),
        label="爱好",
        initial=[1, 3],
        widget=forms.widgets.SelectMultiple()
    )


5.多选checkbox
class LoginForm(forms.Form):
    ...
    hobby = forms.fields.MultipleChoiceField(
        choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
        label="爱好",
        initial=[1, 3],
        widget=forms.widgets.CheckboxSelectMultiple()
    )

 

五、choice字段注意事项

在使用选择标签时,需要注意choices的选项可以配置从数据库中获取,但是由于Form是懒加载模式的,获取的值无法实时更新,
就是说,你创建Form的时候从数据库加载到的值是什么,它就只显示那些值,即使数据库更新了,它也不会自动更新,如果需要实现实时更新,
需要重写构造方法从而实现choice实时更新。

1、方式一
重写Form类的__init__,使其每次初始化都去查一次数据库
class RegForm(forms.Form):
    hobby = forms.fields.MultipleChoiceField(
        choices=Hobby.objects.all().values_list('id', 'name'),
        label='爱好',
        initial=[1, 3],
        widget=forms.widgets.CheckboxSelectMultiple()
    )
    def __init__(self):
        super(RegForm, self).__init__()
        self.fields['hobby'].choices=Hobby.objects.all().values_list('id', 'name')



2、方式二
使用From里面的models模块,这个模块是依赖于APP里面的models的(注意:两个models不是同一个),得到的是一个个对象,
所有还要要操作APP里面的models,使其显示出具体的值。
1.APP下的models.py
from django.db import models


class Hobby(models.Model):
    name = models.CharField(max_length=12)

    def __str__(self):
        return self.name


2.views.py
from django import forms


class RegForm(forms.Form):
    hobby2 = forms.models.ModelMultipleChoiceField(
        queryset=Hobby.objects.all(),
        label='第二爱好',
        initial=[1, 2],
        widget=forms.widgets.CheckboxSelectMultiple()
    )

 

六、Django Form所有内置字段

Field
    required=True,               是否允许为空
    widget=None,                 HTML插件
    label=None,                  用于生成Label标签或显示内容
    initial=None,                初始值
    help_text='',                帮助信息(在标签旁边显示)
    error_messages=None,         错误信息 {'required': '不能为空', 'invalid': '格式错误'}
    validators=[],               自定义验证规则
    localize=False,              是否支持本地化
    disabled=False,              是否可以编辑
    label_suffix=None            Label内容后缀
 
 
CharField(Field)
    max_length=None,             最大长度
    min_length=None,             最小长度
    strip=True                   是否移除用户输入空白
 
IntegerField(Field)
    max_value=None,              最大值
    min_value=None,              最小值
 
FloatField(IntegerField)
    ...
 
DecimalField(IntegerField)
    max_value=None,              最大值
    min_value=None,              最小值
    max_digits=None,             总长度
    decimal_places=None,         小数位长度
 
BaseTemporalField(Field)
    input_formats=None          时间格式化   
 
DateField(BaseTemporalField)    格式:2015-09-01
TimeField(BaseTemporalField)    格式:11:12
DateTimeField(BaseTemporalField)格式:2015-09-01 11:12
 
DurationField(Field)            时间间隔:%d %H:%M:%S.%f
    ...
 
RegexField(CharField)
    regex,                      自定制正则表达式
    max_length=None,            最大长度
    min_length=None,            最小长度
    error_message=None,         忽略,错误信息使用 error_messages={'invalid': '...'}
 
EmailField(CharField)      
    ...
 
FileField(Field)
    allow_empty_file=False     是否允许空文件
 
ImageField(FileField)      
    ...
    注:需要PIL模块,pip3 install Pillow
    以上两个字典使用时,需要注意两点:
        - form表单中 enctype="multipart/form-data"
        - view函数中 obj = MyForm(request.POST, request.FILES)
 
URLField(Field)
    ...
 
 
BooleanField(Field)  
    ...
 
NullBooleanField(BooleanField)
    ...
 
ChoiceField(Field)
    ...
    choices=(),                选项,如:choices = ((0,'上海'),(1,'北京'),)
    required=True,             是否必填
    widget=None,               插件,默认select插件
    label=None,                Label内容
    initial=None,              初始值
    help_text='',              帮助提示
 
 
ModelChoiceField(ChoiceField)
    ...                        django.forms.models.ModelChoiceField
    queryset,                  # 查询数据库中的数据
    empty_label="---------",   # 默认空显示内容
    to_field_name=None,        # HTML中value的值对应的字段
    limit_choices_to=None      # ModelForm中对queryset二次筛选
     
ModelMultipleChoiceField(ModelChoiceField)
    ...                        django.forms.models.ModelMultipleChoiceField
 
 
     
TypedChoiceField(ChoiceField)
    coerce = lambda val: val   对选中的值进行一次转换
    empty_value= ''            空值的默认值
 
MultipleChoiceField(ChoiceField)
    ...
 
TypedMultipleChoiceField(MultipleChoiceField)
    coerce = lambda val: val   对选中的每一个值进行一次转换
    empty_value= ''            空值的默认值
 
ComboField(Field)
    fields=()                  使用多个验证,如下:即验证最大长度20,又验证邮箱格式
                               fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),])
 
MultiValueField(Field)
    PS: 抽象类,子类中可以实现聚合多个字典去匹配一个值,要配合MultiWidget使用
 
SplitDateTimeField(MultiValueField)
    input_date_formats=None,   格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y']
    input_time_formats=None    格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
 
FilePathField(ChoiceField)     文件选项,目录下文件显示在页面中
    path,                      文件夹路径
    match=None,                正则匹配
    recursive=False,           递归下面的文件夹
    allow_files=True,          允许文件
    allow_folders=False,       允许文件夹
    required=True,
    widget=None,
    label=None,
    initial=None,
    help_text=''
 
GenericIPAddressField
    protocol='both',           both,ipv4,ipv6支持的IP格式
    unpack_ipv4=False          解析ipv4地址,如果是::ffff:192.0.2.1时候,可解析为192.0.2.1, PS:protocol必须为both才能启用
 
SlugField(CharField)           数字,字母,下划线,减号(连字符)
    ...
 
UUIDField(CharField)           uuid类型
View Code

 

七、Form校验规则

1、使用RegexValidator验证器(正则表达式)
from django.core.validators import RegexValidator  # 导入使用正则表达的模块

class RegForm(forms.Form):

    phone = forms.CharField(
        label='手机号',
        widget=forms.widgets.TextInput(attrs={'class': 'form-control'}),
        validators=[RegexValidator(r'^1[3-9]\d{9}$', '手机号码不正确')]
    )

validators=一个可迭代对象
可迭代对象里面调用RegexValidator方法
RegexValidator方法有两个参数,一个是用于校验的正则表达式,另一个是匹配失败时的错误提示



2、自定义校验规则
from django.core.exceptions import ValidationError

# 专门屏蔽'JPM'的校验规则
def check_jpm(value):
    # value就是用户输入的值
    if 'JPM' in value:
        raise ValidationError('包含敏感词汇')

class RegForm(forms.Form):
    name = forms.CharField(
        min_length=2,
        max_length=12,
        label="用户名",
        widget=forms.widgets.TextInput(),
        validators=[check_jpm]  # 直接调用自定义的函数
    )


3、使用RegexField字段
class RegForm(forms.Form):
    phone = forms.RegexField(
        label='手机号码',
        regex=r'^1[3-9]\d{9}$',
        widget=forms.widgets.TextInput(attrs={'class': 'form-control'})
    )

 

八、钩子(Hook)函数

1、介绍
局部钩子函数:
    在Form类里面实现以clean_开头,字段结尾的函数,例如clean_name,
    在实例化对象调用is_valid()方法进行校验的时候,会自动执行clean_方法。 

    局部钩子函数:在实例化对象的时候,数据已经存在cleaned_data里面了
    但是局部钩子函数是循环每一个具体的字段(例如:name, pwd等),所以在局部钩子函数只能取到它本身的值
    如果抛出错误,则把错误信息添加进errors字典里面
    如果没有抛出错误,则把这个字段对应的cleaned_data的值修改成return返回的值(一般直接返回原本的值,不做修改)


全局钩子函数:
    在Form类里面实现以clean函数,
    在实例化对象调用is_valid()方法进行校验的时候,会自动执行clean方法。

    全局钩子:可以取到cleaned_data所有的数据
    如果没有抛出错误,把返回值直接赋值给cleaned_data这个变量
    如果抛出错误,需要手动调用add_error()方法,明确指定给哪个字段增加错误信息
    注意:局部钩子函数没有抛出错误是修改cleaned_data字典里面的某个值
    而全局钩子没有抛出错误是直接修改cleaned_data这个变量,可以把cleaned_data改成列表、数字等
    一般没有抛出错误就直接把原来的cleaned_data字典重新赋值给cleaned_data变量

2、示例
from django.core.validators import RegexValidator
from django.core.exceptions import ValidationError


class RegForm(forms.Form):
    name = forms.CharField(
        min_length=2,
        max_length=12,
        label="用户名",
        # 利用error_messages自定义错误信息
        error_messages={'min_length': '用户名至少2位', 'max_length': '用户名最多12位'},
        widget=forms.widgets.TextInput(attrs={'class': 'form-control'}),
    )
    pwd = forms.CharField(
        min_length=6,
        max_length=18,
        label="密码",
        # 把密码框设置为password类型,并指定class为form-control样式类,render_value=True设置 密码错误时,把密码回写到输入框里(即密码错误时也保存密码)
        widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}, render_value=True)
    )
    re_pwd = forms.CharField(
        min_length=6,
        max_length=18,
        label="确认密码",
        required=False,  # 设置成不是必须要填的字段
        widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
    )
    phone = forms.CharField(
        label='手机号',
        widget=forms.widgets.TextInput(attrs={'class': 'form-control'}),
        validators=[RegexValidator(r'^1[3-9]\d{9}$', '手机号码不正确')]
    )

    # 局部钩子函数:在实例化对象的时候,数据已经存在cleaned_data里面了
    # 但是局部钩子函数是循环每一个具体的字段(例如:name, pwd等),所以在局部钩子函数只能取到它本身的值
    # 如果抛出错误,则把错误信息添加进errors字典里面
    # 如果没有抛出错误,则把这个字段对应的cleaned_data的值修改成return返回的值(一般直接返回原本的值,不做修改)
    def clean_name(self):
        # 局部钩子只能cleaned_data.get('自己的值')
        # cleaned_data.get('phone') 取不到
        value = self.cleaned_data.get('name')
        if 'JPM' in value:
            raise ValidationError('太sq了,不能填')
        else:
            return value

    # 全局钩子:可以取到cleaned_data所有的数据
    # 如果没有抛出错误,把返回值直接赋值给cleaned_data这个变量
    # 如果抛出错误,需要手动调用add_error()方法,明确指定给哪个字段增加错误信息
    # 注意:局部钩子函数没有抛出错误是修改cleaned_data字典里面的某个值
    # 而全局钩子没有抛出错误是直接修改cleaned_data这个变量,可以把cleaned_data改成列表、数字等
    # 一般没有抛出错误就直接把原来的cleaned_data字典重新赋值给cleaned_data变量
    def clean(self):
        pwd = self.cleaned_data.get('pwd')
        re_pwd = self.cleaned_data.get('re_pwd')
        if pwd == re_pwd:
            return self.cleaned_data
        else:
            self.add_error('re_pwd', '两次密码不一致')
            raise ValidationError('两次密码不一致')
View Code

 

九、在Form组件应用Bootstrap样式

1、可通过重写form类的init方法来实现。
class LoginForm(forms.Form):
    username = forms.CharField(
        min_length=8,
        label="用户名",
        initial="张三",
        error_messages={
            "required": "不能为空",
            "invalid": "格式错误",
            "min_length": "用户名最短8位"
        }

    # 重写方式一:
    def __init__(self, *args, **kwargs):
        super(LoginForm, self).__init__(*args, **kwargs)
        for field in iter(self.fields):
            self.fields[field].widget.attrs.update({
                'class': 'form-control'
            })

    # 重写方式二:
    def __init__(self, *args, **kwargs):
        super(LoginForm, self).__init__(*args, **kwargs)
        # self.fields是一个大字典,key是字段名,value是字段对象
        for field in self.fields.values():
            field.widget.attrs.update({'class': 'form-control'})

 

十、Form组件使用ajax提交示例

1、register.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>注册</title>
    {% load static %}
    <link rel="stylesheet" href="{% static 'bootstrap-3.3.7/css/bootstrap.css' %}">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-4 col-md-offset-4" style="margin-top: 70px">
            <form novalidate id="f1">
                {% csrf_token %}
                {% for field in form_obj %}
                    <div class="form-group">
                        <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                        {{ field }}
                        <span class="help-block"></span>
                    </div>
                {% endfor %}
                <div>
                    <button type="button" id="b1" class="btn btn-success">注册</button>
                </div>
            </form>
        </div>
    </div>
</div>
<script src="{% static 'jquery.js' %}"></script>
<script>
    $('#b1').click(function () {
        // 自定义一个对象用来存储表单的提交信息
        var dataObj = {};
        $('input').each(function () {
            dataObj[$(this).attr('name')] = $(this).val();
        });
        console.log(dataObj);
        // 取到input标签的值
        $.ajax({
            url: '/register/',
            type: 'post',
            data: dataObj,
            success: function (res) {
                console.log(res);
                // 把所有的错误提示信息展示到页面的指定位置
                if (res.code === 0){
                    // 没错
                    location.href = res.url;
                }else {
                    $.each(res.error_msg, function (k, v) {
                        // 根据k找到对应的input框,把v中第一个字符串显示出来
                        $('#id_'+k).next('span').text(v[0]).parent().addClass('has-error');
                    })
                }
            }
        })
    });
    // input标签获取焦点之后清除之前的错误提示
    $('form input').focus(function () {
        $(this).next('span').text('').parent().removeClass('has-error');
    })
</script>
</body>
</html>
View Code

 

2、models.py
from django.db import models

# Create your models here.

GENDER_CHOICES = ((0, ''), (1, ''), (2, '保密'))
DEFAULT_GENDER = 2


class UserInfo(models.Model):
    username = models.CharField(max_length=12, unique=True)
    password = models.CharField(max_length=20)
    phone = models.CharField(max_length=11, unique=True)
    email = models.EmailField()
    gender = models.PositiveIntegerField(choices=GENDER_CHOICES, default=DEFAULT_GENDER)


    def __str__(self):
        return self.username
View Code


3、Form组件单独写一个py文件 forms.py
from django import forms
from django.core.validators import RegexValidator
from django.core.exceptions import ValidationError
from app01.models import GENDER_CHOICES, DEFAULT_GENDER, UserInfo


class RegForm(forms.Form):
    username = forms.CharField(
        max_length=12,
        min_length=2,
        label='用户名',
        widget=forms.widgets.TextInput()
    )
    password = forms.CharField(
        max_length=20,
        min_length=6,
        label='密码',
        widget=forms.widgets.PasswordInput()
    )
    re_password = forms.CharField(
        max_length=20,
        min_length=6,
        label='确认密码',
        widget=forms.widgets.PasswordInput()
    )
    phone = forms.RegexField(
        regex=r'^1[3-9]\d{9}$',
        label='手机号码',
        max_length=11,
        min_length=11,
        widget=forms.widgets.TextInput(),

    )
    email = forms.CharField(
        label='邮箱',
        widget=forms.widgets.EmailInput(),
        validators=[RegexValidator(r'^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$', '邮箱格式不正确'), ]
    )
    gender = forms.ChoiceField(
        label='性别',
        widget=forms.widgets.RadioSelect(),
        choices=GENDER_CHOICES,
        initial=DEFAULT_GENDER,
    )

    # 写一个局部钩子方法,校验用户名是否已被注册
    def clean_name(self):
        name_value = self.cleaned_data.get('name')
        is_exist = UserInfo.objects.filter(name=name_value)
        if is_exist:
            raise ValidationError('用户名已存在')
        else:
            return name_value

    # 写一个局部钩子方法,校验手机号是否已被注册
    def clean_phone(self):
        phone_value = self.cleaned_data.get('phone')
        is_exist = UserInfo.objects.filter(phone=phone_value)
        if is_exist:
            raise ValidationError('手机号已存在')
        else:
            return phone_value

    def clean(self):
        # 判断密码和确认密码是否相等
        # self.cleaned_data  --> 所有经过校验的数据
        pwd = self.cleaned_data.get('password')
        re_pwd = self.cleaned_data.get('re_password')
        if pwd == re_pwd:
            return self.cleaned_data
        else:
            self.add_error('re_password', '两次密码不一致')
            raise ValidationError('两次密码不一致')

    def __init__(self, *args, **kwargs):
        super(RegForm, self).__init__(*args, **kwargs)
        for field in iter(self.fields):
            if field == 'gender':
                continue
            self.fields[field].widget.attrs.update({
                'class': 'form-control'
            })
View Code


4、views.py
from django.shortcuts import render, HttpResponse, redirect
from app01.forms import RegForm
from app01.models import UserInfo
from django.http import JsonResponse
# Create your views here.


# 登录
def login(request):
    return HttpResponse('login')


# 注册
def register(request):
    if request.method == 'POST':
        res = {'code': 0}
        # 利用post提交的数据实例化form类
        form_obj = RegForm(request.POST)
        # 校验数据的有效性
        if form_obj.is_valid():  # form_obj2.cleaned_data
            form_obj.cleaned_data.pop('re_password')
            UserInfo.objects.create(**form_obj.cleaned_data)
            res['url'] = '/login/'
        else:
            # 数据有问题
            res['code'] = 1
            res['error_msg'] = form_obj.errors
        return JsonResponse(res)

    form_obj = RegForm()
    return render(request, 'register.html', {'form_obj': form_obj})


# 首页
def index(request):
    return HttpResponse('index')
View Code

 

 

十一、ModelForm

1、介绍
我们在设计Form表单的时候,一般都会把字段名设置成和数据库(ORM)的字段名一模一样,因为这样设计的话,你后面设计的逻辑中,如果需要创建用户,
那么可以从表单的数据中提取出来,用关键字参数**kwargs直接创建,如果Form表单的字段和ORM的不一样,那么创建用户的时候就需要一个个指定,
比如username=name,password=pwd等。

那么问题来了,Form表单的字段和ORM的字段一样,那么写Fomr表单的时候不就等于再写一次ORM的字段吗,有简单的方法吗?

显然是有的,Django给我们考虑到了。

用法是:
建Form表单类的时候继承ModelForm,在Meta类里面指定继承ORM的哪些字段,也可以新增你要的字段。

2、示例
from django import forms
from crm.models import UserProfile  # 导入UserProfile这张ORM表


class RegForm(forms.ModelForm):  # 继承ModelForm
    # 新增的字段(UserProfile中并没有这个字段的)
    re_password = forms.CharField(
        label='确认密码',
        widget=forms.widgets.PasswordInput()
    )

    class Meta:
        model = UserProfile  # 表明使用UserProfile这张表作为模型
        # fields = '__all__'  # 展示UserProfile的所有字段
        fields = ['email', 'password', 're_password', 'name', 'mobile']  # 按照顺序展示列表中的字段
        # exclude = ['']  # 把不需要的字段排查,其他字段展示出来
        labels = {
            'email': '邮箱'
        }

        error_messages = {
            'email': {
                'max_length': '邮箱长度过长',
                'unique': '邮箱不能为空'
            }
        }

        widgets = {
            'password': forms.widgets.PasswordInput()
        }

    # 重写init方法批量应用bootstrap样式
    def __init__(self):
        super().__init__()
        # self.fields是一个大字典,key是字段名,value是字段对象
        for field in self.fields.values():
            field.widget.attrs.update({'class': 'form-control'})


3、class Meta下的配置
1. model = 'ORM类名'

2. 字段
    1. fields = '__all__'  # 展示ORM表的所有字段
    2. fields = ['要展示的字段1', '字段2']  # 指定的字段显示出来,其他字段不显示
    3. exclude = ['需要排除的字段1', ...]  # 指定的字段不显示,其他字段都显示出来

3. labels = {
      '字段名1': 'label名1',
      '字段名2': 'label名2',
   }

4. error_messages = {
      '字段名': {
        'min_length': '最小长度不能小于xx位',
        'max_length': '最大长度不能超过xx位',
        'required': '这个字段是必填项'
      }
   }

5. widgets = {
        'password': forms.widgets.PasswordInput()
   }

6.help_texts = None  # 帮助提示信息

7. 校验
    1. 可以在model的字段中配置validators
    2. 自己在Form类中重写字段,定义validators配置
    3. 局部钩子方法和全局钩子方法 同Form的用法

注意:modelform中的字段的label默认就是对应的model表的verbose_name,若表中的字段没有定义verbose_name,那么页面显示的label就是字段名,当然也可以直接在modelform中定义label


4、ModelForm中含有外键字段
创建ModelForm的时候,如果model中有含有外键字段,
那么这个外键字段在ModelForm默认是单选的select字段,label默认是model的verbose_name的值,如果没有则显示字段名,
或者可以在modelform的Meta里面通过labels设置,choices默认是这个外键关联的所有数据,HTML上的select标签的value默认这个外键关联的表对应的主键,
显示的文本默认是外键关联的表对应的对象。
注意:如果是多对多字段,则生成多选的select字段

例如:
1.models.py
class ConsultRecord(models.Model):
    customer = models.ForeignKey('Customer', verbose_name="所咨询客户")



2.modelsform
class ConsultRecordForm(forms.ModelForm):

    class Meta:
        model = ConsultRecord
        fields = '__all__'

上面的modelsform字段customer实际上等于下面这个普通的form
class ConsultRecordForm(forms.Form):

    customer = forms.fields.ChoiceField(
        choices=Customer.objects.all().values_list('id', 'name'),
        label="所咨询客户",  # verbose_name
        widget=forms.widgets.Select()
    )


5、save()方法
每个ModelForm还具有一个save()方法。 这个方法根据表单绑定的数据创建并保存数据库对象。 
ModelForm的子类可以接受现有的模型实例作为关键字参数instance的值,如果提供instance参数,则save()将更新该实例。
如果没有提供,save()将创建模型的一个新实例。
注意:instance接收的是一个实例化对象,普通的Form并没有instance参数,且当modelform中含有多对多字段时,调用save方法后,
会自动帮你更新自己的表和多对多的第三张表,否则,你得自己手动更新自己的表,然后再通过多对多字段找到第三张表再次更新。

1.没有提供参数instance,save()创建新的实例
def reg(request):
    if request.method == 'POST':
        form_obj = RegForm(request.POST)
        # form_obj是一个ModelForm对象,它和数据库的Model类对应
        form_obj.save()  # 相当于UserProfile.objects.create(**form_obj.cleaned_data)
        return redirect('/login/')


2.提供参数instance,save()跟新这个实例
1.models.py
from django.db import models

# Create your models here.


class Publisher(models.Model):
    name = models.CharField(max_length=12)
    addr = models.CharField(max_length=255)


2.forms.py
from django import forms
from myapp.models import Publisher


class PublisherForm(forms.ModelForm):

    class Meta:
        model = Publisher
        fields = '__all__'


3.views.py
from django.shortcuts import render, redirect
from myapp.models import Publisher
from myapp.forms import PublisherForm
# Create your views here.


def publisher_list(request):
    data = Publisher.objects.all()
    return render(request, 'publisher_list.html', {'publisher_list': data})


def edit_publisher(request):
    edit_id = request.GET.get('id')
    publisher_obj = Publisher.objects.filter(id=edit_id).first()
    # instance=某个对象:把这个对象的原始数据填到PublisherForm这个表单里面
    form_obj = PublisherForm(instance=publisher_obj)
    if request.method == 'POST':
        # 把request.POST的数据更新到instance=这个对象里面
        form_obj = PublisherForm(request.POST, instance=publisher_obj)  # DRF框架的时候也会用到
        if form_obj.is_valid():
            # 去数据库编辑
            # 方法一:
            # new_name = form_obj.cleaned_data.get('name')
            # new_addr = form_obj.cleaned_data.get('addr')
            # publisher_obj.name = new_name
            # publisher_obj.addr = new_addr
            # publisher_obj.save()

            # 方法二:
            form_obj.save()
            return redirect('/publisher_list/')
    return render(request, 'edit_publisher.html', {'form_obj': form_obj})

 

十二、formset_factory的使用

1.介绍
    from django.forms import formset_factory
    对于继承forms.Form的Form类,我们可以使用formset_factory。
    注意:modelform类其实也继承了forms.Form的父类,因此modelform也可使用formset_factory

2. 必填参数
    1. form      --> 继承了forms.form的form类

3. 额外参数
    extra:想要生成的空表单的数量
    max_num:最多可以展示的表单数量
    initial: 初始化formset的默认值(实例化formset时候的参数)
    注意: max_num优先级高于extra,比如,我们想要显示5个空表单(extra=5),但max_num=2,最后只会显示2个空表单

4. 示例
        FormSet = formset_factory(CourseForm)  # 实例化一个formset_factory
        formset_obj = FormSet(initial=[{}, {}...])  # formset_obj就可以跟form表单一样使用了

5. 注意事项
    1. 生成的FormSet在html页面中使用的时候,每一个form对象必须带上自己的form.id标识这是哪个对象的form,页面是会默认hidden这个参数(添加的时候不需要,编辑的时候需要)
    2. 如果要提交这些form对象,则需要在html中的form表单为formset设置一个参数:{{ formset_obj.management_form }},页面是会默认hidden这个参数
       是告诉后端我这个formset有多少个小form
    3. FormSet默认会给你多生成一个form对象,可以通过参数 extra 设置额外加多少个,0代表不额外加form对象
    4. FormSet其实就是批量生成Form对象的操作,注意是Form对象
    6. formset_factory和form一样,没有instance参数,所以也没有queryset参数
    7. 其他操作和单个form操作是一样的,只是这个formset等于批量操作而已,一样有is_valid等(没有save,因为form是没有save的,modelform才有)
        1. form表单
        class CourseForm(forms.Form):
            title = forms.CharField(max_length=32, label="课程名称")
            score = forms.IntegerField(max_value=100, label="分数")
         
        2. views.py 
        from django.forms import formset_factory


        def course(request):
            FormSet = formset_factory(CourseForm, extra=1)
            formset_obj = FormSet(initial=[{"title": "语文"}, {"title": "数学", "score": 59}])
            if request.method == 'POST':
                formset_obj = FormSet(request.POST)
                if formset_obj.is_valid():
                    # 手动创建Course
                    objs = (Course(**item) for item in formset_obj.cleaned_data)
                    Course.objects.bulk_create(objs)
                    return redirect("/index/")
                return render(request, "xx.html", {"formset_obj": formset_obj})
            return render(request, 'xx.html', {'formset_obj': formset_obj})
        
        
        3.html
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Title</title>
            {% load my_filter%}
        </head>
        <body>
         <form action="" method="post" novalidate>
                    {% csrf_token %}
                    {{ formset_obj.management_form }}  <!--提交formset时必须要添加的参数-->
                    <div class="col-md-12">
                        <table class="table table-striped table-bordered">
                            <thead>
                                <tr>
                                    <th>#</th>
                                    <th>课程名称</th>
                                    <th>分数</th>
                                </tr>
                            </thead>
                            <tbody>
                                {% for form_obj in formset_obj %}
                                    <tr>
                                    <!--每一个form对象必须带上自己的form.id标识这是哪个对象的form -->
                                        {{ form_obj.id }}
                                        <td>{{ forloop.counter }}</td>
                                        <td>{{ form_obj.title }}</td>
                                        <td>{{ form_obj.score }}</td>
                                    </tr>
                                {% endfor %}
                            </tbody>
                        </table>
                        <button class="btn btn-success pull-right" type="submit">提交</button>
                    </div>
                </form>
        </body>
        </html>

 

十三、modelformset_factory的使用

1. 介绍
    from django.forms import modelformset_factory
    对于继承forms.ModelForm的Form类,我们可以使用modelformset_factory

2. 必填参数
    1. model      --> 数据库中的某个表
    2. modelform  --> 继承了forms.ModelForm的类

3. 其他参数
    extra:想要生成的空表单的数量
    max_num:最多可以展示的表单数量
    queryset:初始化modelformset(实例化modelformset时候的参数)

4. 示例
    # 生成一个formset类
    FormSet = modelformset_factory(Book, BookForm, extra=0)  # extra=0默认不给我生成额外的form对象
    # 拿到所有书的实例(多个Book实例组成的query_set)
    query_set = Book.objects.all()
    # 从query_set中取出每一个Book实例生成form表单,存到formset里面
    formset_obj = FormSet(queryset=query_set)  # formset_obj是一个可迭代对象,里面是一个个小form

5. 注意事项
    1. 生成的FormSet在html页面中使用的时候,每一个modelform对象必须带上自己的modelform.id标识这是哪个对象的modelform,页面是会默认hidden这个参数(添加的时候不需要,编辑的时候需要)
    2. 如果要提交这些modelform对象,则需要在html中的modelform表单为formset设置一个参数:{{ formset_obj.management_form }},页面是会默认hidden这个参数
       是告诉后端我这个modelformset有多少个小modelform
    3. FormSet默认会给你多生成一个modelform对象,可以通过参数 extra 设置额外加多少个,0代表不额外加modelform对象
    4. FormSet其实就是批量生成ModelForm对象的操作,注意是ModelForm对象
    5. modelformset_factory实例设置默认值用queryset而不是instance,queryset相当于为每个modelform设置instance
    6. 在html页面仍然可以使用具体的某个modelform对象的instance参数拿到传进来的对象,但是如果使用的不是自己的modelform对象的字段,提交数据时并不生效
    7. 其他操作和单个modelform操作是一样的,只是这个modelformset等于批量操作而已,一样有is_valid,save等
    8. 示例
        1. modelform
        class BookForm(forms.ModelForm):
    
            class Meta:
                model = Book
                fields = ["title", "price", "publisher"]

        
        2. views.py
        from django.forms import modelformset_factory


        def book(request):
            # 生成一个formset类
            FormSet = modelformset_factory(Book, BookForm, extra=0)  # extra=0默认不给我生成额外的form对象
            # 拿到所有书的实例(多个Book实例组成的query_set)
            query_set = Book.objects.all()
            # 从query_set中取出每一个Book实例生成form表单,存到formset里面
            formset_obj = FormSet(queryset=query_set)  # formset_obj是一个可迭代对象,里面是一个个小form
            # 上面的代码相当于:
            # form_obj1 = BookForm(instance=query_set[0])
            # form_obj2 = BookForm(instance=query_set[1])  ...
            # 然后把这些form_obj存到formset_obj里面
            if request.method == 'POST':
                formset_obj = FormSet(request.POST, queryset=query_set)
                if formset_obj.is_valid():
                    # formset_obj.save()
                    return redirect('/index/')
                return render(request, 'tem1.html', {'formset_obj': formset_obj})
            return render(request, 'tem1.html', {'formset_obj': formset_obj})


        3. html代码
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Title</title>
            {% load my_filter%}
        </head>
        <body>
         <form action="" method="post" novalidate>
                    {% csrf_token %}
                    {{ formset_obj.management_form }}  <!--提交formset时必须要添加的参数-->
                    <div class="col-md-12">
                        <table class="table table-striped table-bordered">
                            <thead>
                                <tr>
                                    <th>#</th>
                                    <th>书名</th>
                                    <th>价格</th>
                                    <th>出版社</th>
                                </tr>
                            </thead>
                            <tbody>
                                {% for form_obj in formset_obj %}
                                    <tr>
                                    <!--每一个form对象必须带上自己的form.id标识这是哪个对象的form -->
                                        {{ form_obj.id }}
                                        <td>{{ forloop.counter }}</td>
                                        <td>{{ form_obj.title }}</td>
                                        <td>{{ form_obj.price }}</td>
                                        <td>{{ form_obj.publisher }}</td>
                                    </tr>
                                {% endfor %}
                            </tbody>
                        </table>
                        <button class="btn btn-success pull-right" type="submit">提交</button>
                    </div>
                </form>
        </body>
        </html>

 

posted @ 2018-11-15 23:06  我用python写Bug  阅读(1099)  评论(0编辑  收藏  举报