Django之forms组件

在常规表单开发中,我们常用的做法是手动编写表单至html模板中,请求数据到视图中处理,如果有多个表单就要多次这样操作,视图中也要多次处理验证这些数据,有一个字段要修改就要去所有与表单关联的地方处理,Django为我们提供了Froms组件帮助我们解决这种问题,统一管理统一处理。

类似模型,Django表单也由各种字段组成。表单可以自定义(forms.Form),也可以由模型Models创建(forms.ModelForm)。值得注意的是模型里用的是verbose_name来描述一个字段, 而表单用的是label。这两种方式其实只在forms中有区别,视图和模板中使用方式一致,我们这里就先将自定义表单,不同的地方会讲下模型表单。

Django的常用做法是在app文件夹下创建一个forms.py,专门存放app中所定义的各种表单,这样方便集中管理表单。如果要使用上述表单,我们可以在视图views.py里把它们像模型一样import进来直接使用。我们这里统一在我们的wechat应用下创建forms.py文件,来存放我们的自定义表单

表单字段类型

讲到表单之前我们先讲下表单字段的类型:

西面的StudentForm类只有一个字段,字段类型是CharField,对应的HTML元素是
<input type=”text ” ...> ,这里的HTML元素叫作字段的Widget。除此之外, DjangoForm
还提供了几十种字段类型, 每种类型分别对应不同的HTML 元素,下面对这些类型进行简
单介绍。如果需要更详细的表单字段介绍,可以参考Django 官网: https://docs.djangoproject.com/en/2.0/ref/forms/fields

  1. BooleanField
    Widget: Checkboxlnput( < input type=checkbox ” ...>)
    空值: False
    标惟值: TrueFalse
    验证:如果设置了required =True , 则验证字段值是否为True
    验证点: required
  2. CharField
    Widget : Textlnput( < input type=" text ” ...> )
    空值: empty_value
    标准值: 字符串。
    验证: 如果设置了max_length, min_length ,则验证字段长度是再符合要求,否则不验证。
    验证点: required , max_length , min_length
  3. ChoiceField
    Widget: Select( <select><option ...> ... </select>)
    空值:””。
    标准值: 字符串。
    验证: 验证字段值是否存在。
    验证点: required, invalid_choice
  4. DateField
    Widget: Datelnput( < input type="text” ...>)
    空值: None
    标准值: Python datetime.date巳对象。
    验证:验证字段值是否是正确的时间格式字符串、datetime .date 对象、datetime.datetime
    对象。
    验证点: required, invalid
  5. DateTimeField
    Widget: Datelnput( < input type=" text" ... > )
    空值: None
    标准值: Python datetime. datetime对象。
    验证: 验证字段值是否是正确的时间格式字符串、datetime.date 对象、datetime.datetime
    对象。
    验证点: required , invalid
  6. DecimalField
    Widget:当Field.localize=False时对应Numberlnput(<input type=”number”...>),否则对
    Textlnput(<inputtype="text” ...>)
    空值:None
    标准值:Python decimal对象。
    验证:验证字段值是否是数值类型。
    验证点:required,invalid,max_value,min_value,max_digits,max_decimal_places,max_whole_digits
  7. FileField
    Widget:ClearableFilelnput(<input type="file”...>)
    空值:None
    标准值:包含文件内容与文件名的UploadedFile对象。
    验证:空文件或者没有选择文件。
    验证点:required,invalid,missing,empty,max_length
  8. FilePathField
    Widget:Select(<select><option ...> ...</select>)
    空值:None
    标准值:字符串。
    验证:选中的选项是否存在于下拉列表中。
    验证点:required,invalid_choice
  9. lmageField
    Widget:ClearableFilelnput(<input type="file” ...>)
    空值:None
    标准值:包含文件内容与文件名的UploadedFile对象。
    验证:空文件或者没有选择文件。
    验证点:required,invalid,missiingempty,invalid_image
  10. lntegerField
    Widget:当Field.localize=False时对应Numberlnput(<inputtype="number” ...>),否则对
    Textlnput(<inputtype=”text” ...>)
    空值:None
    标准值: Python integer 对象。
    验证:验证宇段值是否是一个整数。
    验证点: required, invalid, max_value, min_value
  11. MultipleChoiceField
    Widget: SelectMultiple(<select multiple="multiple”> ... </select>)
    空值: [](空列表)。
    标准值:一组字符串。
    验证:所有选中值存在于下拉列表中。
    验证点: required, invalid_ choice, invalid_list

表单通用属性

  1. required
    默认情况下,所有的表单字段都是必填字段,这样如果提交表单时没有为字段赋值,则
    会抛出ValidationError 异常。
    对于非必填宇段可以设置required=False 避免验证错误,例如:
1
forms.CharField(required=False)

label
为表单字段指定一个label 元素用于显示字段信息,如上面your_name字段将会额外显
示一个label:

1
<label for="your name">Your name : </label>

initial
为宇段设置初始值。

help_ text
为字段添加帮助性文字。

error_messages
重写字段的默认错误提示信息, error_messages 是一个字典类型。
例如设置当CharFieldrequired验证失败时显示‘请输入你的名字’:

  1. name= forms.CharField(error_messages={'required':'请输入你的名字可'})
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148

    2. `localize`
    设置表单宇段是否启用本地化。

    3. `disabled`
    当设置`disabled=True` 时,使用`HTML disabled` 属性禁用字段。

    ### 表单字段内置属性

    除了上面的通用属性,每个字段都有自己的内置属性

    ```python
    Field
    required=True, 是否允许为空
    widget=None, HTML插件
    label=None, 用于生成Label标签或显示内容
    initial=None, 初始值
    help_text='', 帮助信息(在标签旁边显示)
    error_messages=None, 错误信息 {'required': '不能为空', 'invalid': '格式错误'}
    show_hidden_initial=False, 是否在当前插件后面再加一个隐藏的且具有默认值的插件(可用于检验两次输入是否一直)
    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类型
    ...

自定义表单

编写自定义表单

1
2
3
4
5
6
7
8
9
from django import forms
from .models import Student

CLASSES_CHOICES = ('软件二班', '图像一班', '师范一班')
class StudentForm(forms.Form):
name = forms.CharField(min_length=4, required=True, label='姓名')
age = forms.IntegerField()
score = forms.DecimalField()
classes = forms.ChoiceField(choices=CLASSES_CHOICES)

表单实例化

下面方法可以实例化一个空表单,但里面没有任何数据

1
form = StudentForm()

用户提交的数据可以通过以下方法与表单结合,生成与数据结合过的表单(Bound forms)。Django只能对Bound forms进行验证。

1
form = StudentForm(data=request.POST, files=request.FILES)

我们暂时的视图代码:

1
2
3
4
5
6
7
8
9
10
from django.shortcuts import render, HttpResponse
from .forms import StudentForm

def index(request):
# 实例化自定义表单
form = StudentForm()
if request.method == 'POST':
form = StudentForm(request.POST)
# form传给前端做渲染
return render(request, 'index.html', {'form':form})

模板使用表单

模板文件中我们可以通过, 中渲染表单。如果使用则默认等于使用了

1
2
3
4
5
6
7
8
9
{% extends 'layout.html' %}
{% block main %}
<div class="uk-container">
<form action="{% url 'wechat:index' %}" method="post">
{% csrf_token %}
{{ form }}
</form>
</div>
{% endblock %}

渲染后的页面为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.2.2/dist/css/uikit.min.css"/>


</head>
<body>

<div class="uk-container">
<form action="/wechat/index/" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="j4mMZkPNIjVGEwr4wc8u1iywfuIMFOu300iU1gmOjP8FJdJmMSiNHhxxPzPP6kKZ">
<tr><th><label for="id_name">姓名:</label></th><td><input type="text" name="name" minlength="4" required id="id_name"></td></tr>
<tr><th><label for="id_age">Age:</label></th><td><input type="number" name="age" required id="id_age"></td></tr>
<tr><th><label for="id_score">Score:</label></th><td><input type="number" name="score" step="any" required id="id_score"></td></tr>
<tr><th><label for="id_classes">Classes:</label></th><td><select name="classes" id="id_classes">
<option value="软件二班">软件二班</option>

<option value="图像一班">图像一班</option>

<option value="师范一班">师范一班</option>

</select></td></tr>

</form>
</div>


<!-- UIkit JS -->
<script src="https://cdn.jsdelivr.net/npm/uikit@3.2.2/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.2.2/dist/js/uikit-icons.min.js"></script>
</body>
</html>
##### 使用`{{ form.as_p }}`渲染

表单部分生成的结果为:

1
2
3
4
5
6
7
8
9
10
11
 <p><label for="id_name">姓名:</label> <input type="text" name="name" minlength="4" required id="id_name"></p>
<p><label for="id_age">Age:</label> <input type="number" name="age" required id="id_age"></p>
<p><label for="id_score">Score:</label> <input type="number" name="score" step="any" required id="id_score"></p>
<p><label for="id_classes">Classes:</label> <select name="classes" id="id_classes">
<option value="软件二班">软件二班</option>

<option value="图像一班">图像一班</option>

<option value="师范一班">师范一班</option>

</select></p>
##### 使用`{{form.as_ul}}`渲染

表单部分生成的结果为:

1
2
3
4
5
6
7
8
9
10
11
<li><label for="id_name">姓名:</label> <input type="text" name="name" minlength="4" required id="id_name"></li>
<li><label for="id_age">Age:</label> <input type="number" name="age" required id="id_age"></li>
<li><label for="id_score">Score:</label> <input type="number" name="score" step="any" required id="id_score"></li>
<li><label for="id_classes">Classes:</label> <select name="classes" id="id_classes">
<option value="软件二班">软件二班</option>

<option value="图像一班">图像一班</option>

<option value="师范一班">师范一班</option>

</select></li>

如果你想详细控制每个field的格式,你可以采取以下方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{% extends 'layout.html' %}

{% block main %}
<div class="uk-container">
<form action="{% url 'wechat:index' %}" method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">{{ field.label }}</label>
<div class="uk-form-controls">
{{ field }}
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<div class="uk-margin">
<button class="uk-button uk-button-primary " type="submit">提交</button>
</div>
</form>
</div>
{% endblock %}

发起post请求后渲染出的表单部分为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">姓名</label>
<div class="uk-form-controls">
<input type="text" name="name" minlength="4" required id="id_name">
</div>

<ul class="errorlist"><li>这个字段是必填项。</li></ul>
</div>

<div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">Age</label>
<div class="uk-form-controls">
<input type="number" name="age" value="23" required id="id_age">
</div>


</div>

<div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">Score</label>
<div class="uk-form-controls">
<input type="number" name="score" step="any" required id="id_score">
</div>

<ul class="errorlist"><li>这个字段是必填项。</li></ul>
</div>

<div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">Classes</label>
<div class="uk-form-controls">
<select name="classes" id="id_classes">
<option value="软件二班" selected>软件二班</option>

<option value="图像一班">图像一班</option>

<option value="师范一班">师范一班</option>

</select>
</div>

示例

我们通常将自定义表单写入到forms.py中,当然你也可以不用新建forms.py而直接在html模板里写表单,但我并不建议这么做。用forms.py的好处显而易见:

  • 所有的表单在一个文件里,非常便于后期维护,比如增添或修订字段。
  • forms.py可通过clean方法自定义表单验证,非常便捷(见后文)。

继续刚才我们的视图,我们对用户提交的数据进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.shortcuts import render, HttpResponse
from .forms import StudentForm

def index(request):
# 实例化自定义表单
form = StudentForm()
if request.method == 'POST':
form = StudentForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
age = form.cleaned_data['age']
score = form.cleaned_data['score']
classes = form.cleaned_data['classes']
stu_obj = Student.objects.create(name=name, age=age, score=score, classes=classes)
return HttpResponse('学生《%s》创建成功' % name)
# form传给前端做渲染
return render(request, 'index.html', {'form':form})

我们来理下StudentForm整个流程:

  • 当用户通过POST方法提交表单,我们将提交的数据与StudentForm结合,然后验证表单StudentForm的数据是否有效。
  • 如果表单数据有效,我们创建student对象。用户通过一张表单提交数据。
  • 如果添加学员成功,我们通过HttpResponse返回页面学生添加成功提示
  • 如果用户没有提交表单或不是通过POST方法提交表单,我们转到添加页面,生成一张空的StudentForm

表单验证

每个forms类可以通过clean方法自定义表单验证。如果你只想对某些字段进行验证,你可以通过clean_字段名方式自定义表单验证。如果用户提交的数据未通过验证,会返回ValidationError,并呈现给用户。如果用户提交的数据有效form.is_valid(),则会将数据存储在cleaned_data里。

在上述添加学员的案例里,我们在StudentForm``通过``clean方法添加了姓名验证,年龄验证和分数验证。代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from django import forms
from .models import Student

CLASSES_CHOICES = (
('软件二班', '软件二班'),
('图像一班', '图像一班'),
('师范一班', '师范一班')
)


class StudentForm(forms.Form):
name = forms.CharField(label='姓名')
age = forms.IntegerField()
score = forms.DecimalField()
classes = forms.ChoiceField(choices=CLASSES_CHOICES)
def clean_name(self):
name = self.cleaned_data.get('name')

# 判断姓名长度及是否存在
if len(name)<2:
raise forms.ValidationError('你的名字也太短了,蒙我的吧!')
elif len(name)>10:
raise forms.ValidationError('这么长的名字?滚犊子!')
else:
stu_obj = Student.objects.filter(name__exact=name)
if len(stu_obj)>0:
raise forms.ValidationError('这学生报过名了,你走开!')
return name

def clean_age(self):
age = self.cleaned_data.get('age')

if age<1 or age>120:
raise forms.ValidationError('你这年龄绝了,拒绝!')
return age
def clean_score(self):
score = self.cleaned_data.get('score')

if score<0 or score>120:
raise forms.ValidationError('啥情况啊,分页能飞啊!')
return score

自定义widget

Django forms的每个字段你都可以选择你喜欢的输入widget,比如多选,复选框。你还可以定义每个widgetcss属性。如果你不指定,Django会使用默认的widget,有时比较丑。

比如下面这段代码定义了表单姓名字段的输入控件为Textarea,还指定了其样式css

1
2
3
4
5
6
class StudentForm(forms.Form):
name = forms.CharField(label='姓名', widget=forms.Textarea(
attrs={
'class':'uk-textarea'
}
))

模板渲染出来的结果为:

1
<textarea name="name" cols="40" rows="10" class="uk-textarea" required id="id_name">

设置widget可以是你的表单大大美化,方便用户选择输入。比如下面案例里对年份使用了SelectDateWidget,颜色则使用了复选框CheckboxSelectMultiple。单选可以用RadioSelectSelect。常见文本输入可以用TextInputTextArea

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
BIRTH_YEAR_CHOICES = ('1980', '1981', '1982')
COLORS_CHOICES = (
('blue', 'Blue'),
('green', 'Green'),
('black', 'Black'),
)
GENDER_CHOICES = (
('male', '男'),
('female', '女')
)
class SimpleForm(forms.Form):
birth_year = forms.DateField(widget=forms.SelectDateWidget(years=BIRTH_YEAR_CHOICES))
favorite_colors = forms.MultipleChoiceField(
required=False,
widget=forms.CheckboxSelectMultiple(
attrs={
'class':'uk-checkbox'
}
),
choices=COLORS_CHOICES,
)
gender = forms.CharField(widget=forms.RadioSelect(
choices=GENDER_CHOICES,
attrs={
'class': 'uk-radio'
}
))

渲染结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
     <div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">Birth year</label>
<div class="uk-form-controls">
<select name="birth_year_year" required id="id_birth_year_year"><option value="1980">1980</option><option value="1981">1981</option><option value="1982">1982</option></select><select name="birth_year_month" required id="id_birth_year_month"><option value="1">一月</option><option value="2">二月</option><option value="3">三月</option><option value="4">四月</option><option value="5">五月</option><option value="6">六月</option><option value="7">七月</option><option value="8">八月</option><option value="9">九月</option><option value="10">十月</option><option value="11">十一月</option><option value="12">十二月</option></select><select name="birth_year_day" required id="id_birth_year_day"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8">8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option><option value="13">13</option><option value="14">14</option><option value="15">15</option><option value="16">16</option><option value="17">17</option><option value="18">18</option><option value="19">19</option><option value="20">20</option><option value="21">21</option><option value="22">22</option><option value="23">23</option><option value="24">24</option><option value="25">25</option><option value="26">26</option><option value="27">27</option><option value="28">28</option><option value="29">29</option><option value="30">30</option><option value="31">31</option></select>
</div>


</div>

<div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">Favorite colors</label>
<div class="uk-form-controls">
<ul id="id_favorite_colors" class="uk-checkbox">
<li><label for="id_favorite_colors_0"><input type="checkbox" name="favorite_colors" value="blue" class="uk-checkbox" id="id_favorite_colors_0">
Blue</label>

</li>
<li><label for="id_favorite_colors_1"><input type="checkbox" name="favorite_colors" value="green" class="uk-checkbox" id="id_favorite_colors_1">
Green</label>

</li>
<li><label for="id_favorite_colors_2"><input type="checkbox" name="favorite_colors" value="black" class="uk-checkbox" id="id_favorite_colors_2">
Black</label>

</li>
</ul>
</div>


</div>

<div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">Gender</label>
<div class="uk-form-controls">
<ul id="id_gender" class="uk-radio">
<li><label for="id_gender_0"><input type="radio" name="gender" value="male" class="uk-radio" required id="id_gender_0">
</label>

</li>
<li><label for="id_gender_1"><input type="radio" name="gender" value="female" class="uk-radio" required id="id_gender_1">
</label>

</li>
</ul>
</div>

自定义属性和错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from django import forms

class LoginForm(forms.Form):
username = forms.CharField(
required=True,
max_length=20,
min_length=6,
error_messages={
'required': '用户名不能为空',
'max_length': '用户名长度不得超过20个字符',
'min_length': '用户名长度不得少于6个字符',
}
)
password = forms.CharField(
required=True,
max_length=20,
min_length=6,
error_messages={
'required': '密码不能为空',
'max_length': '密码长度不得超过20个字符',
'min_length': '密码长度不得少于6个字符',
}
)

表单数据初始化**

有时我们需要对表单设置一些初始数据,我们可以通过initial方法,如下所示。

1
2
3
4
5
def index(request):
# 实例化自定义表单
form = StudentForm(initial={
'name':'默认叫张三吧'
})

渲染结果:

1
2
<textarea name="name" cols="40" rows="10" class="uk-textarea" required id="id_name">
默认叫张三吧</textarea>

模型表单

自定义表单时我们简单提了一下模型表单,跟自定义表单最大的区别就是定义表单的部分,视图及模板没有任何改变。

定义模型表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django import forms
from .models import Student
class StudentForm(forms.ModelForm):
class Meta:
# 绑定的模型
model = Student
# 要显示的字段
fields = ('name', 'age', 'score', 'classes')
widgets = {
'name':forms.TextInput(attrs={
'class':'你好'
})
}
error_messages = {
'name':{
'max_length':'太长了'
}
}

视图和模板我们不做任何改变,正常渲染结果

img

Meta选项

我们 在class Meta 中使用了一些元数据项,比如说 exclude、labels 以及 fields,当然还有些其他的选项,在 Django 官方网站 ModelForm 的定义如下所示

1
2
3
4
def fields_for_model(model, fields=None, exclude=None, widgets=None,
formfield_callback=None, localized_fields=None,
labels=None, help_texts=None, error_messages=None,
field_classes=None, *, apply_limit_choices_to=True):
1) fields

其为列表或元组类型,与 exclude 相反,它指定当前的表单应该包含哪些字段,如果要所有的 Model 字段都包含在表单中,可以设定 fields=’all‘。ModelForm 的定义中必须要包含 fields 或 exclude 选项,否则将会抛出异常,同时给出错误提示:

Creating a ModelForm without either the’fields’attribute or the’exclude’attribute is prohibited。

2) labels

其为字典类型,用于定义表单字段的名称(输入框左边显示的名称)。表单字段的名称首先会使用 Model 字段定义设置的 verbose_name,如果没有设置,则直接使用字段名。因此当没有定义 verbo se_name 时,就可以使用 labels 选项来指定字段名。例如:

1
labels={'title''标题', 'price':'价格'}
3) help_texts

其为字典类型,用于给表单字段添加帮助信息。目前页面中表单字段的帮助信息(输入框下方显示的内容)来自 Model字段的 help_texts 定义,如果没有定义则什么都不显示。help_texts 的定义方式与 labels 选项类似,例如:

1
help_texts={"title":"书籍的名称", "price':"书籍价格"}
4) widgets

其为字典类型,用于定义表单字段选用的控件。默认情况下,ModelForm 会根据Model字段的类型映射表单 Field 类,因此会应用 Field 类中默认定义的 widgets。这个选项用于自定义控件类型,例如:

1
2
3
4
5
widgets = {
'type': forms.SelectMultiple(attrs={
'class': 'form-control'
})
}
5) field_classes

字典类型,用于指定表单字段使用的 Field 类型。默认情况下,对于 title 字段,ModelForm 会将它映射为 fields.CharField 类型。可以根据需要改变这种默认行为,例如,将 title 设置为如下类型:

1
field_calss={"title":forms.URLField}
6) error_messages

字典类型,用来指定表单字段校验规则,即验证失败时的报错信息。

1
2
3
4
5
error_messages = {
'type': {
'required': '移民类型至少选一个'
}
}

表单数据初始化

除了跟自定义表单中视图示例初始化使用initial外,我们还可以初始化一个学员对象,比如我们编辑的时候就经常会显示原来数据。

1
2
3
4
5
6
7
8
from django.shortcuts import render, HttpResponse
from .forms import StudentForm

def index(request):
stu_obj = Student.objects.first()
# 实例化自定义表单
form = StudentForm(instance=stu_obj)
return render(request, 'index.html', {'form':form})

模板渲染结果:

img

FormSet

Formset(表单集)是多个表单的集合。FormsetWeb开发中应用很普遍,它可以让用户在同一个页面上提交多张表单,一键添加多个数据,比如一个页面上添加多个学员。

Formset的分类

Django针对不同的formset提供了3种方法: formset_factory, modelformset_factoryinlineformset_factory。我们接下来分别看下如何使用它们。

formset_factory

对于继承forms.Form的自定义表单,我们可以使用formset_factory。我们可以通过设置extramax_num属性来确定我们想要展示的表单数量。注意: max_num优先级高于extra。比如下例中,我们想要显示3个空表单(extra=3),但最后只会显示2个空表单,因为max_num=2

forms.py自定义表单文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django import forms
from .models import Student

class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = ('name',)
widgets = {
'name':forms.TextInput(attrs={
'class':'你好'
})
}
error_messages = {
'name':{
'max_length':'太长了'
}
}
# extra: 额外的空表单数量
# max_num: 包含表单数量(不含空表单)
StudentFormSet = forms.formset_factory(
form=StudentForm,
extra=3,
max_num=2
)

view视图文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from django.shortcuts import render, HttpResponse

from .models import Student
from .forms import StudentFormSet

def index(request):
formset = StudentFormSet()
if request.method == 'POST':
formset = StudentFormSet(request.POST)
# 判断表单验证是否通过
if formset.is_valid():
# 循环每个form数据
for form_data in formset.cleaned_data:
# 依次打散添加至数据库
Student.objects.create(**form_data)
return HttpResponse('都添加成功了')
return render(request, 'index.html', {'formset':formset})
# formset传给前端做渲染
return render(request, 'index.html', {'formset':formset})

template模板文件:

方式一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% extends 'layout.html' %}

{% block main %}
<div class="uk-container">
<form action="{% url 'wechat:index' %}" method="post" novalidate>
{{ formset.management_form }}
{% csrf_token %}
{{ formset }}
<div class="uk-margin">
<button class="uk-button uk-button-primary " type="submit">提交</button>
</div>
</form>
</div>
{% endblock %}

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{% extends 'layout.html' %}

{% block main %}
<div class="uk-container">
<form action="{% url 'wechat:index' %}" method="post" novalidate>
{{ formset.management_form }}
{% csrf_token %}
{% for form in formset %}
{{ form }}
<hr>
{% endfor %}
<div class="uk-margin">
<button class="uk-button uk-button-primary " type="submit">提交</button>
</div>
</form>
</div>
{% endblock %}

方式三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{% extends 'layout.html' %}

{% block main %}
<div class="uk-container">
<form action="{% url 'wechat:index' %}" method="post" novalidate>
{{ formset.management_form }}
{% csrf_token %}
{% for form in formset %}

{% for field in form %}
<div class="uk-margin">
<label class="uk-form-label" for="form-horizontal-text">{{ field.label }}</label>
<div class="uk-form-controls">
{{ field }}
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<hr>
{% endfor %}
<div class="uk-margin">
<button class="uk-button uk-button-primary " type="submit">提交</button>
</div>
</form>
</div>
{% endblock %}

提交后的渲染结果:

img

modelformset_factory

Formset也可以直接由模型model创建,这时你需要使用modelformset_factory。你可以指定需要显示的字段和表单数量。

1
2
3
4
5
6
7
8
9
from django import forms
from .models import Student

StudentFormSet = forms.modelformset_factory(
model=Student,
fields=('name', 'age'),
extra=3,
max_num=2
)

当然上面方法并不推荐,因为对单个表单添加验证方法非常不方便。我更喜欢的方式先创建自定义的ModelForm,添加单个表单验证,然后再利用modelformset_factory创建formset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django import forms
from .models import Student

class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = ('name',)
widgets = {
'name':forms.TextInput(attrs={
'class':'你好'
})
}
error_messages = {
'name':{
'max_length':'太长了'
}
}
# extra: 额外的空表单数量
# max_num: 包含表单数量(不含空表单)
StudentFormSet = forms.modelformset_factory(
model=Student,
form = StudentForm,
extra=3,
max_num=2
)
inlineformset_factory

试想我们有如下province模型,provincecity是单对多的关系。一般的formset只允许我们一次性提交多个province或多个city。但如果我们希望同一个页面上添加一个省份(province)和多个城市(city),这时我们就需要用使用inlineformset了。

模型models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Province(models.Model):
id = models.AutoField(primary_key = True)
name = models.CharField(max_length=10, verbose_name='省份')
desc = models.CharField(max_length=255, verbose_name='介绍')
def __str__(self):
return self.name
class Meta:
verbose_name = '省份'
verbose_name_plural = verbose_name

class City(models.Model):
id = models.AutoField(primary_key = True)
name = models.CharField(max_length=20, verbose_name='城市')
desc = models.CharField(max_length=255, verbose_name='介绍')
province = models.ForeignKey(to=Province, verbose_name='省份', on_delete=models.CASCADE)
def __str__(self):
return self.name
class Meta:
verbose_name = '城市'
verbose_name_plural = verbose_name
自定义表单form.py

利用inlineformset_factory创建formset的方法如下所示。该方法的第一个参数和第二个参数都是模型,其中第一个参数必需是ForeignKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from django import forms
from .models import Province
from .models import City

class ProvinceForm(forms.ModelForm):
class Meta:
model = Province
fields = ('name', 'desc')
widgets = {
'name':forms.TextInput(attrs={
'class':'你好'
})
}
error_messages = {
'name':{
'max_length':'太长了'
}
}

CityFormSet = forms.inlineformset_factory(
parent_model=Province,
model=City,
fields=('name', ),
extra=3,
max_num=2,
can_delete=False
)
视图views.py

views.py中使用formset创建和更新province的代码如下。在对IngredientFormSet进行实例化的时候,必需指定province的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from django.shortcuts import render, HttpResponse

from .models import Province
from .models import City

from .forms import ProvinceForm
from .forms import CityFormSet

def index(request):
form = ProvinceForm()
city_formset = CityFormSet()
if request.method == 'POST':
form = ProvinceForm(request.POST)
# 判断省份验证是否通过
if form.is_valid():
# 保存提交的省份数据,返回添加的省份对象
province = form.save()
city_formset = CityFormSet(request.POST, instance = province)
# 判断城市验证是否通过
if city_formset.is_valid():
# 保存城市
city_formset.save()
return render(request, 'index.html', {'form':form, 'city_formset':city_formset})
# form传给前端做渲染
return render(request, 'index.html', {'form':form, 'city_formset':city_formset})

def update(request):
# 取出pk等于3的省份对象
province_obj = Province.objects.get(pk=3)
# 实例化自定义表单和formset,指定示例
form = ProvinceForm(instance=province_obj)
city_formset = CityFormSet(instance=province_obj)
if request.method == 'POST':
form = ProvinceForm(request.POST, instance=province_obj)
# 判断省份验证是否通过
if form.is_valid():
# 保存提交的省份数据,返回添加的省份对象
province = form.save()
city_formset = CityFormSet(request.POST, instance = province)
# 判断城市验证是否通过
if city_formset.is_valid():
# 保存城市
city_formset.save()
return render(request, 'index.html', {'form':form, 'city_formset':city_formset})
# form传给前端做渲染
return render(request, 'index.html', {'form':form, 'city_formset':city_formset})
模板templates

action添加和更新请修改url路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% extends 'layout.html' %}

{% block main %}
<div class="uk-container">
<form action="{% url 'wechat:index' %}" method="post" novalidate>
{% csrf_token %}
{{ form.as_p }}
<hr>
<legend>添加城市</legend>
{{ city_formset.management_form }}
{{ city_formset.non_form_errors }}
{% for city_form in city_formset %}
{{ city_form }}
<hr>
{% endfor %}
<div class="uk-margin">
<button class="uk-button uk-button-primary " type="submit">提交</button>
</div>
</form>
</div>
{% endblock %}

渲染效果:

img