ModelForm
模型表单ModelForm
通常在Django项目中,我们编写的大部分都是与Django 的模型紧密映射的表单。 基于这个原因,Django 提供一个辅助类来让我们可以从Django 的模型创建Form,这就是ModelForm。
modelForm定义:form与model的结合,会根据model中的字段转换成对应的form字段,并且并生成标签等操作。
例:
models.py内容:
class Book(models.Model):
title = models.CharField( max_length=32)
publishDate=models.DateField()
price=models.DecimalField(max_digits=5,decimal_places=2)
publish=models.ForeignKey(to="Publish",to_field="nid")
authors=models.ManyToManyField(to='Author',)
def __str__(self):
return self.title
form.py内modelform类的写法
class BookForm(forms.ModelForm):
title = forms.CharField(min_length=1) # 自定义验证标准
class Meta:
model = models.Book
fields = "__all__"
labels = {
"title": "书名",
"price": "价格"
}
widgets = {
"password": forms.widgets.PasswordInput(attrs={"class": "c1"}),
"publishDate": forms.widgets.DateInput(attrs={"type": "date"}),
}
核心用法
- 首先从django.forms导入ModelForm;
- 编写一个自己的类,继承ModelForm;
- 在新类里,设置元类Meta;
- 在Meta中,设置model属性为你要关联的ORM模型,这里是Article;
- 在Meta中,设置fields属性为你要在表单中使用的字段列表;
- 列表里的值,应该是ORM模型model中的字段名。
上面的例子中,因为model和form比较简单,字段数量少。但如果是大型项目,每个模型的字段数量几十上百,这么做的收益将非常巨大,而且还可以一步保存数据save()
。
class Meta下常用参数:
model = models.Book # 对应的Model中的类
fields = "__all__" # 字段,如果是__all__,就是表示列出所有的字段
exclude = None # 排除的字段
labels = None # 提示信息
help_texts = None # 帮助提示信息
widgets = None # 自定义插件
error_messages = None # 自定义错误信息
error_messages = { 'title':{'required':'不能为空',...} }#每个字段的所有的错误都可以写
ModelForm的fields属性,可以赋值一个列表,将要使用的字段添加进列表中。这样做的好处是,安全可靠。但有时候,字段太多,不愿意一个一个输入,也可以用__all__
。
将fields属性的值设为__all__
,表示将映射的模型中的全部字段都添加到表单类中来。
from django.forms import ModelForm
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = '__all__'
exclude属性:
表示将model中,除了exclude属性中列出的字段之外的所有字段,添加到表单类中作为表单字段。
class PartialAuthorForm(ModelForm):
class Meta:
model = Author
exclude = ['title']
如果Author模型有3个字段name
、birth_date
和title
,上面的例子会让birth_date
和name
出现在表单中。
批量添加样式:和form的一样
class BookForm(forms.ModelForm):
# password = forms.CharField(min_length=10) 可以重写字段,会覆盖modelform中的这个字段和下面modelform中关于这个字段的设置就会被覆盖,比如果设置插件啊,error_messages啊等等,
# r_password = forms.CharField() # 想多验证一些字段可以单独拿出来写,按照form的写法,写在Meta的上面或者下面都可以
class Meta:
model = models.Book
# fields = ['title','price']
fields = "__all__" # ['title,'price'] 指定字段生成form
# exclude=['title',] # 排除字段
labels = {
"title": "书名",
"price": "价格"
}
error_messages = {
'title':{'required':'不能为空',} # 每个字段的错误都可以写
}
# 如果models中的字段和咱们需要验证的字段对不齐的时,比如注册时,咱们需要验证密码和确认密码两个字段数据,但是后端数据库就保存一个数据就行,那么验证是两个,数据保存是一个,就可以再接着写form字段
r_password = forms.CharField()
# 同样的,如果想做一些特殊的验证定制,那么和form一样,也是全局和局部钩子,写法也是form的写法,直接在类里面写:
#局部钩子:
def clean_title(self):
pass
#全局钩子
def clean(self):
pass
def __init__(self,*args,**kwargs): # 批量操作
super().__init__(*args,**kwargs)
for field in self.fields: # field.error_messages = {'required':'不能为空'} # 批量添加错误信息,这是都一样的错误,不一样的还是要单独写。
self.fields[field].widget.attrs.update({'class':'form-control'})
字段类型
生成的Form类中将具有和指定的模型字段对应的表单字段,顺序为fields属性列表中指定的顺序。
每个模型字段有一个对应的默认表单字段。下面是完整的映射列表:
模型字段 | 表单字段 |
---|---|
AutoField | 在Form类中无法使用 |
BigAutoField | 在Form类中无法使用 |
BigIntegerField | IntegerField,最小-9223372036854775808,最大9223372036854775807. |
BooleanField | BooleanField |
CharField | CharField,同样的最大长度限制。如果model设置了null=True,Form将使用empty_value |
CommaSeparatedIntegerField | CharField |
DateField | DateField |
DateTimeField | DateTimeField |
DecimalField | DecimalField |
EmailField | EmailField |
FileField | FileField |
FilePathField | FilePathField |
FloatField | FloatField |
ForeignKey | ModelChoiceField |
ImageField | ImageField |
IntegerField | IntegerField |
IPAddressField | IPAddressField |
GenericIPAddressField | GenericIPAddressField |
ManyToManyField | ModelMultipleChoiceField |
NullBooleanField | NullBooleanField |
PositiveIntegerField | IntegerField |
PositiveSmallIntegerField | IntegerField |
SlugField | SlugField |
SmallIntegerField | IntegerField |
TextField | CharField,并带有widget=forms.Textarea参数 |
TimeField | TimeField |
URLField | URLField |
可以看出,Django在设计model字段和表单字段时存在大量的相似和重复之处。
ManyToManyField和 ForeignKey字段类型属于特殊情况:
- ForeignKey被映射成为表单类的django.forms.ModelChoiceField,它的选项是一个模型的QuerySet,也就是可以选择的对象的列表,但是只能选择一个。
- ManyToManyField被映射成为表单类的django.forms.ModelMultipleChoiceField,它的选项也是一个模型的QuerySet,也就是可以选择的对象的列表,但是可以同时选择多个,多对多嘛。
同时,在表单属性设置上,还有下面的映射关系:
- 如果模型字段设置blank=True,那么表单字段的required设置为False。 否则,required=True。
- 表单字段的label属性根据模型字段的verbose_name属性设置,并将第一个字母大写。
- 如果模型的某个字段设置了editable=False属性,那么它表单类中将不会出现该字段。
- 表单字段的
help_text
设置为模型字段的help_text
。 - 如果模型字段设置了choices参数,那么表单字段的widget属性将设置成Select框,其选项来自模型字段的choices。选单中通常会包含一个空选项,并且作为默认选择。如果该字段是必选的,它会强制用户选择一个选项。 如果模型字段具有default参数,则不会添加空选项到选单中。
自定义ModelForm字段
在表格中,展示了从模型到模型表单在字段上的映射关系。但是,有时候可能这种默认映射关系不是我们想要的,或者想进行一些更加灵活的定制,那就可以自定义ModelForm字段。
使用Meta类内部的widgets属性:
widgets属性接收一个数据字典。其中每个元素的键必须是模型中的字段名之一,键值就是我们要自定义的内容了,具体格式和写法,参考下面的例子。
例如,更改Author模型中的name字段的类型,可以如下重写字段的Widget:
from django.forms import ModelForm, Textarea
from myapp.models import Author
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
widgets = {
'name': Textarea(attrs={'cols': 80, 'rows': 20}),
}
上面还展示了添加样式参数的格式。
如果希望进一步自定义字段,还可以指定Meta类内部的error_messages
、help_texts
和labels
属性,比如:
from django.utils.translation import ugettext_lazy as _
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
labels = {
'name': _('Writer'),
}
help_texts = {
'name': _('Some useful help text.'),
}
error_messages = {
'name': {
'max_length': _("This writer's name is too long."),
},
}
还可以指定field_classes
属性将字段类型设置你自己写的表单字段类型。
例如,如果想为slug字段使用MySlugFormField,可以像下面这样:
from django.forms import ModelForm
from myapp.models import Article
class ArticleForm(ModelForm):
class Meta:
model = Article
fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
field_classes = {
'slug': MySlugFormField,
}
最后,如果你、想完全控制一个字段包括它的类型,验证器,是否必填等等。可以显式地声明或指定这些性质,就像在普通表单中一样。比如,如果想要指定某个字段的验证器,可以显式定义字段并设置它的validators参数:
from django.forms import ModelForm, CharField
from myapp.models import Article
class ArticleForm(ModelForm):
slug = CharField(validators=[validate_slug])
class Meta:
model = Article
fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
ModelForm的验证
验证ModelForm主要分两步:
- 验证表单
- 验证模型实例
与普通的表单验证类似,模型表单的验证也是调用is_valid()
方法或访问errors属性。模型的验证(Model.full_clean())紧跟在表单的clean()方法调用之后。通常情况下,如果我们不重写具体字段并设置validators属性的话,ModelForm是按照模型中字段的validators来校验的。如果需要,可以重写模型表单的clean()来提供额外的验证,方法和普通的表单一样。
save()方法
每个ModelForm还具有一个save()方法。 这个方法根据表单绑定的数据创建并保存数据库对象。 ModelForm的子类可以接受现有的模型实例作为关键字参数instance;如果提供此功能,则save()将更新该实例。 如果没有提供,save() 将创建模型的一个新实例:
>>> from myapp.models import Book
>>> from myapp.forms import BookForm
# 根据POST数据创建一个新的form对象
>>> form_obj = BookForm(request.POST)
# 创建书籍对象
>>> new_ book = form_obj.save()
# 基于一个书籍对象创建form对象
>>> edit_obj = Book.objects.get(id=1)
# 使用POST提交的数据更新书籍对象
>>> form_obj = BookForm(request.POST, instance=edit_obj)
>>> form_obj.save()
通过form组件来保存书籍表数据的写法:
def index(request):
if request.method == 'GET':
form_obj = BookForm()
return render(request,'index.html',{'form_obj':form_obj})
else:
form_obj = BookForm(request.POST)
if form_obj.is_valid():
# authors_obj = form_obj.cleaned_data.pop('authors')
# new_book_obj = models.Book.objects.create(**form_obj.cleaned_data)
# new_book_obj.authors.add(*authors_obj)
# 改进:
form_obj.save() # 因为在Meta中指定了是哪张表,所以它会自动识别,不管是外键还是多对多等,都会自行处理保存,它完成的就是上面三句话做的事情,并且如果验证的数据比后端数据表中的字段多,那么它会自动剔除多余的不需要保存的字段,比如那个重复确认密码就不要保存。
return redirect('show')
else:
print(form_obj.errors)
return render(request,'index.html',{'form_obj':form_obj})
简单图书管理系统页面:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="{% static 'bootstrap-3.3.0-dist/dist/css/bootstrap.min.css' %}">
</head>
<body>
<h1>编辑页面</h1>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="">
<div class="form-group">
<label for="title">书名</label>
<input type="text" class="form-control" id="title" placeholder="title" value="{{ book_obj.title }}">
</div>
<div class="form-group">
<label for="publishDate">出版日期</label>
<input type="text" class="form-control" id="publishDate" placeholder="publishDate" value="{{ book_obj.publishDate|date:'Y-m-d' }}">
</div>
<div class="form-group">
<label for="price">价格</label>
<input type="number" class="form-control" id="price" placeholder="price" value="{{ book_obj.price }}">
</div>
<div class="form-group">
<label for="publish">书名</label>
<select name="publish" id="publish" class="form-control">
{% for publish in all_publish %}
{% if publish == book_obj.publish %}
<option value="{{ publish.id }}" selected>{{ publish.name }}</option>
{% else %}
<option value="{{ publish.id }}">{{ publish.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="authors">书名</label>
<select name="authors" id="authors" multiple class="form-control">
{% for author in all_authors %}
{% if author in book_obj.authors.all %}
<option value="{{ author.id }}" selected>{{ author.name }}</option>
{% else %}
<option value="{{ author.id }}" >{{ author.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</form>
</div>
</div>
</div>
</body>
<script src="{% static 'bootstrap-3.3.0-dist/dist/jQuery/jquery-3.1.1.js' %}"></script>
<script src="{% static 'bootstrap-3.3.0-dist/dist/js/bootstrap.min.js' %}"></script>
</html>
views.py:
def edit_book(request,n):
book_obj = models.Book.objects.filter(pk=n).first()
if request.method == 'GET':
all_authors = models.Author.objects.all() #
all_publish = models.Publish.objects.all()
return render(request,'edit_book.html',{'book_obj':book_obj,'all_authors':all_authors,'all_publish':all_publish})
改成使用modelform之后:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="{% static 'bootstrap-3.3.0-dist/dist/css/bootstrap.min.css' %}">
</head>
<body>
<h1>编辑页面</h1>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="{% url 'edit_book' n %}" novalidate method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
<span class="text-danger">{{ field.errors.0 }}</span>
</div>
{% endfor %}
<div class="form-group">
<input type="submit" class="btn btn-primary pull-right">
</div>
</form>
</div>
</div>
</div>
</body>
<script src="{% static 'bootstrap-3.3.0-dist/dist/jQuery/jquery-3.1.1.js' %}"></script>
<script src="{% static 'bootstrap-3.3.0-dist/dist/js/bootstrap.min.js' %}"></script>
</html>
views.py这样写:
def edit_book(request,n):
book_obj = models.Book.objects.filter(pk=n).first()
if request.method == 'GET':
# all_authors = models.Author.objects.all() #
# all_publish = models.Publish.objects.all()
form = BookForm(instance=book_obj)
return render(request,'edit_book.html',{'form':form,'n':n}) # 传递的这个n参数是给form表单提交数据的是的action的url用的,因为它需要一个参数来识别是更新的哪条记录
else:
form = BookForm(request.POST,instance=book_obj) # 必须指定instance,不然调用save方法就变成了添加操作
if form.is_valid():
form.save()
return redirect('show')
else:
return render(request,'edit_book.html',{'form':form,'n':n})
启用字段本地化
默认情况下,ModelForm中的字段不会本地化它们的数据。可以使用Meta类的localized_fields
属性来启用字段的本地化功能。
>>> from django.forms import ModelForm
>>> from myapp.models import Author
>>> class AuthorForm(ModelForm):
... class Meta:
... model = Author
... localized_fields = ('birth_date',)
如果localized_fields
设置为__all__
这个特殊的值,所有的字段都将本地化。
表单的继承
ModelForms是可以被继承的。子模型表单可以添加额外的方法和属性,比如下面的例子:
>>> class EnhancedArticleForm(ArticleForm):
... def clean_pub_date(self):
... ...
以上创建了一个ArticleForm的子类EnhancedArticleForm,并增加了一个clean_pub_date
方法。
还可以修改Meta.fields
或Meta.exclude
列表,只要继承父类的Meta类,如下所示:
>>> class RestrictedArticleForm(EnhancedArticleForm):
... class Meta(ArticleForm.Meta):
... exclude = ('body',)
提供初始值
可以在实例化一个表单时通过指定initial参数来提供表单中数据的初始值。
>>> article = Article.objects.get(pk=1)
>>> article.headline
'My headline'
>>> form = ArticleForm(initial={'headline': 'Initial headline'}, instance=article)
>>> form['headline'].value()
'Initial headline'
完整示例
数据库中有一张学生表,字段有姓名,年龄,爱好,邮箱,电话,住址,注册时间等等一大堆信息,现在写一个创建学生的页面,重点是合法性验证,需要在前端判断用户输入是否合法,比如姓名必须在多少字符以内,电话号码必须是多少位的数字,邮箱必须是邮箱的格式等等。
创建modelform
# 首先导入ModelForm
from django.forms import ModelForm
# 在视图函数中,定义一个类,比如叫StudentList,这个类要继承ModelForm,在这个类中再写一个原类Meta。
# 在这个原类中,有以下属性(部分):
class StudentList(ModelForm):
class Meta:
model = Student # 对应的Model中的类
fields = "__all__" # 表示列出所有的字段
exclude = None # 排除的字段
# error_messages用法:
error_messages = {
'name':{'required':"用户名不能为空",},
'age':{'required':"年龄不能为空",},
}
# widgets用法,比如把输入用户名的input框变为Textarea
# 导入模块
from django.forms import widgets as wid # 因为重名,所以起个别名
widgets = {
"name":wid.Textarea(attrs={"class":"c1"}) # 自定义属性
}
# labels,自定义在前端显示的名字
labels= {
"name":"用户名"
}
然后在url对应的视图函数中实例化这个类,把这个对象传给前端
def student(request):
if request.method == 'GET':
student_list = StudentList()
return render(request,'student.html',{'student_list':student_list})
前端可以用as_p显示全部{{ student_list.as_p }}
字段,如果感觉用as_p的页面太丑,也可以通过for循环student_list
,拿到一个个input框,再自定义样式。
<body>
<div class="container">
<h1>student</h1>
<form method="POST" novalidate>
{% csrf_token %}
{# {{ student_list.as_p }}#}
{% for student in student_list %} # 拿到student对象,直接在前端打印这个student,是个input框student.label
<div class="form-group col-md-6">
{# 拿到数据字段的verbose_name,如果没有设置这个属性,拿到的默认就是字段名 #}
<label class="col-md-3 control-label">{{ student.label }}</label>
<div class="col-md-9" style="position: relative;">{{ student }}</div>
<div style="color:red" foot-siez:14px>{{student.errors.0}}</div>
# 拿到错误信息。
</div>
{% endfor %}
<div class="col-md-2 col-md-offset-10">
<input type="submit" value="提交" class="btn-primary">
</div>
</form>
</div>
</body>
现在还缺一个input框的form-contral样式,可以考虑在后台的widget里面添加,如:
from django.forms import widgets as wid # 因为重名,所以起个别名
widgets = {
"name":wid.TextInput(attrs={'class':'form-control'}),
"age":wid.NumberInput(attrs={'class':'form-control'}),
"email":wid.EmailInput(attrs={'class':'form-control'})
}
也可以在js中,找到所有的input框,加上这个样式。
添加纪录
保存数据的时候,不用一个个取数据了,只需要save一下。
def student(request):
if request.method == 'GET':
student_list = StudentList()
return render(request,'student.html',{'student_list':student_list})
else:
student_list = StudentList(request.POST)
if student_list.is_valid():
student_list.save() # 保存数据
return redirect(request,'student_list.html',{'student_list':student_list})
编辑数据
如果不用ModelForm,编辑的时候得显示之前的数据,需要一个个取值,用ModelForm,只需要加instance=obj
(obj是要修改的数据库的一条数据的对象)就可以得到同样的效果。
保存的时候要注意,一定要注意有这个对象(instance=obj),否则不知道更新哪一个数据。
示例:
from django.shortcuts import render,HttpResponse,redirect
from django.forms import ModelForm
# Create your views here.
from text_app import models
def test(request):
# model_form = models.Student
model_form = models.Student.objects.all()
return render(request,'test.html',{'model_form':model_form})
class StudentList(ModelForm):
...
def student(request):
...
def student_edit(request,pk):
obj = models.Student.objects.filter(pk=pk).first()
if not obj:
return redirect('test')
if request.method == "GET":
student_list = StudentList(instance=obj)
return render(request,'student_edit.html',{'student_list':student_list})
else:
student_list = StudentList(request.POST,instance=obj) # 加instance=obj
if student_list.is_valid():
student_list.save()
return render(request,'student_edit.html',{'student_list':student_list})
总结: 从上边可以看到ModelForm用起来是非常方便的,比如增加修改之类的操作。但是也带来额外不好的地方,model和form之间耦合了。如果不耦合的话,mf.save()方法也无法直接提交保存。 但是耦合的话使用场景通常局限用于小程序,写大程序就最好不用。