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"}),
        }

核心用法

  1. 首先从django.forms导入ModelForm;
  2. 编写一个自己的类,继承ModelForm;
  3. 在新类里,设置元类Meta;
  4. 在Meta中,设置model属性为你要关联的ORM模型,这里是Article;
  5. 在Meta中,设置fields属性为你要在表单中使用的字段列表;
  6. 列表里的值,应该是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个字段namebirth_datetitle,上面的例子会让birth_datename出现在表单中。

批量添加样式:和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_messageshelp_textslabels属性,比如:

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="textclass="form-controlid="titleplaceholder="titlevalue="{{ book_obj.title }}">

                </div>
                <div class="form-group">
                    <label for="publishDate">出版日期</label>
                    <input type="textclass="form-controlid="publishDateplaceholder="publishDatevalue="{{ 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.fieldsMeta.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="POSTnovalidate>
        {% csrf_token %}
        {# {{ student_list.as_p }}#}
        {% for student in student_list %}    # 拿到student对象,直接在前端打印这个student,是个inputstudent.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()方法也无法直接提交保存。 但是耦合的话使用场景通常局限用于小程序,写大程序就最好不用。

posted @ 2020-08-05 21:52  虫萧  阅读(273)  评论(0编辑  收藏  举报