深入理解Django的ModelForm操作
原文連結 :https://www.cnblogs.com/shenjianping/p/11562148.html
一、ModelForm的使用
顧名思義,ModelForm就是將Model與Form進行繫結,Form有自動生成表單的作用,但是每一個forms欄位需要自己手動填寫,而Model就是資料庫表包含了所有的資料欄位。所以ModelForm有著以下功能:
Form所有的功能
將Model欄位自動轉換成forms欄位
1.1、例項演示
1、建立ModelForm
from app01 import models
from django.forms import ModelForm
from django.forms.widgets import Textarea
class BookModelForm(ModelForm):
class Meta:
model = models.Book #對應的Model類
fields = '__all__' #對應的Model類中欄位
exclude = None #排除的欄位
labels = {
"title":"書籍名", #用於html頁面中顯示的名字
"price":"價格"
}
help_texts = {
"title":"我是書籍的幫助資訊" #自定義幫助資訊
}
error_messages = {
"title":{"required":"書籍名不能為空"} #自定義錯誤資訊
}
widgets = {
"title":Textarea(attrs={"class":"form-control"}) #自定義屬性
}
2、新增資料
from django.shortcuts import render,redirect,HttpResponse
def BookAdd(request):
book_list = models.Book.objects.all()
#獲取新增資料的表單
if request.method == "GET":
form = BookModelForm()
return render(request,'booklist.html',locals())
#POST請求新增資料
form = BookModelForm(data=request.POST)
if form.is_valid():
#儲存資料
form.save()
return HttpResponse('...')
3、修改資料
def BookEdit(request,id):
book = models.Book.objects.filter(id=id).first()
#獲取修改資料的表單
if request.method == "GET":
form = BookModelForm(instance=book)
return render(request, 'booklist.html', locals())
#POST請求新增修改過後的資料
form = BookModelForm(data=request.POST,instance=book)
#對資料驗證並且儲存
if form.is_valid():
form.save()
return HttpResponse('...')
4、路由配置
urlpatterns = [
re_path('books/$', tests.BookAdd),
re_path('books/(?P<id>\d+)/$', tests.BookEdit),
]
5、前端html渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="POST" novalidate>
{% csrf_token %}
{% for book in form %}
<div>
{# 拿到資料欄位的labels,沒有就預設顯示欄位名 #}
<label >{{ book.label }}</label>
<div>{{ book }}{{ book.help_text }}</div>
</div>
{% endfor %}
<div class="col-md-2 col-md-offset-10">
<input type="submit" value="提交" class="btn-primary">
</div>
</form>
</body>
</html>
1.2、例項解析
1、model欄位轉成forms欄位
在建立ModelForm類時會將model欄位轉成forms欄位,這裡著重說明三種情況:
model欄位是ForeignKey
如果model中是外來鍵,那麼在forms欄位中對應的就是ModelChoiceField,如果使用的是Form,那麼外來鍵就應該這樣定義:
publish=forms.ModelChoiceField(queryset=Publish.objects.all())
當然,在ModelForm中已經幫你自動實現了,將會產生這樣的標籤:
<select id="id_publish" name="publish">
<option value="obj1.pk">Object1</option>
<option value="obj2.pk">Object2</option>
...
</select>
model欄位是ManyToMany
如果model中是ManyToMany,那麼在forms欄位中對應的就是ModelMultipleChoiceField,如果使用的是Form,那麼ManyToMany就應該這樣定義:
authors=forms.ModelMultipleChoiceField(queryset=Author.objects.all())
當然,在ModelForm中已經幫你自動實現了,將會產生這樣的標籤:
<select name="authors" id="id_authors" multiple="multiple" required="">
<option value="obj1.pk">obj1</option>
<option value="obj2.pk">obj2</option>
...
</select>
model欄位中有choice引數
在model中可能會遇到這樣的情況:
status_choices=(
(1,'已籤合同'),
(2,'未籤合同')
)
status=models.IntegerField(choices=status_choices,verbose_name='狀態',default=2)
這樣的情況在forms中對應的欄位是ChoiceField欄位,如果使用Form自定義欄位,可以這樣寫:
status=forms.ChoiceField(choices=((1,"已籤合同"),(2,"未籤合同")))
當然,在ModelForm中也已經幫你自動實現了。
總結
在檢視ChoiceField、ModelChoiceField、ModelMultipleChoiceField原始碼可知它們三者關係:
ChoiceField(Field)
ModelChoiceField(ChoiceField)
ModelMultipleChoiceField(ModelChoiceField)
它們分別依次繼承,所以最後一個有它們所有的屬性和方法。
2、儲存資料時使用save方法
新增資料
這比Form更為簡單和直接,在forms中要麼透過cleaned_data將資料依次取出分別儲存,要麼以字典的形式一次存入:
obj = BookForm(request.POST)
if obj.is_valid():
models.Book.objects.create(**obj.cleaned_data)
但是在ModelForm中,可以這樣使用:
obj = BookModelForm(request.POST)
if obj.is_valid():
obj.save()
修改資料
在修改資料時Form和ModelForm也是略有不同的在Form中:
obj = BookForm(request.POST)
if obj.is_valid():
models.Book.objects.filter(id=nid).update(**obj.cleaned_data)
而在ModelForm中需要傳入例項:
obj = BookModelForm(data=request.POST,instance=book)
if form.is_valid():
form.save()
二、原始碼一覽
假設這裡以修改檢視流程來了解一下原始碼:
2.1、ModelForm例項化
form = BookModelForm(instance=book)
ModelForm例項化並且傳入instance引數,它會先使用元類生成自己,如果有__new__先執行__new__方法並且返回生成的物件,然後執行__init__方法初始化引數。在元類當中可以看到最後返回了當前BookModelForm,並且將收集了所有的已經宣告的欄位賦值給該類:
class ModelFormMetaclass(DeclarativeFieldsMetaclass):
def __new__(mcs, name, bases, attrs):
...
...
new_class.base_fields = fields
...
...
return new_class
然後再執行BaseModelForm中的__init__方法進行初始化:
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, instance=None, use_required_attribute=None):......
object_data = {}...
self.instance = instance......
#獲取self.fields = copy.deepcopy(self.base_fields)
super().__init__(
data, files, auto_id, prefix, object_data, error_class,
label_suffix, empty_permitted, use_required_attribute=use_required_attribute,
)
初始化過程中將instance接收進來,並且將self.base_fields進行深複製給self.fields。
2.2、is_valid
這個對ModelForm進行校驗就是和Form的一樣:
def is_valid(self):
"""Return True if the form has no errors, or False otherwise."""
return self.is_bound and not self.errors
在self.errors方法中執行的self.full_clean方法:
def full_clean(self):
"""
Clean all of self.data and populate self._errors and self.cleaned_data.
"""
self._errors = ErrorDict()
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
# If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed():
return
self._clean_fields()#對每一個欄位進行執行clean_fieldname方法
self._clean_form() #返回cleaned_data
self._post_clean() #預留鉤子
這也就註定了Form中存在的功能ModelForm都有,無論是欄位的驗證還是其它的功能。
2.3、save
save方法應該說比Form操作更方便快捷,可以簡單的看看內部原始碼:
def save(self, commit=True):
"""
Save this form's self.instance object if commit=True. Otherwise, add
a save_m2m() method to the form which can be called after the instance
is saved manually at a later time. Return the model instance.
"""
if self.errors:
raise ValueError(
"The %s could not be %s because the data didn't validate." % (
self.instance._meta.object_name,
'created' if self.instance._state.adding else 'changed',
)
)
if commit:
# If committing, save the instance and the m2m data immediately.
self.instance.save()
self._save_m2m()
else:
# If not committing, add a method to the form to allow deferred
# saving of m2m data.
self.save_m2m = self._save_m2m
return self.instance
save方法有一個預設引數commit=True,表示儲存例項以及ManyToMany資料,self.instance.save(),呼叫的是model例項的save方法(位於django.db.models.Model):
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
"""
Save the current instance. Override this in a subclass if you want to
control the saving process.
The 'force_insert' and 'force_update' parameters can be used to insist
that the "save" must be an SQL insert or update (or equivalent for
non-SQL backends), respectively. Normally, they should not be set.
"""
# Ensure that a model instance without a PK hasn't been assigned to
# a ForeignKey or OneToOneField on this model. If the field is
# nullable, allowing the save() would result in silent data loss.
for field in self._meta.concrete_fields:
# If the related field isn't cached, then an instance hasn't
# been assigned and there's no need to worry about this check.
if field.is_relation and field.is_cached(self):
obj = getattr(self, field.name, None)
# A pk may have been assigned manually to a model instance not
# saved to the database (or auto-generated in a case like
# UUIDField), but we allow the save to proceed and rely on the
# database to raise an IntegrityError if applicable. If
# constraints aren't supported by the database, there's the
# unavoidable risk of data corruption.
if obj and obj.pk is None:
# Remove the object from a related instance cache.
if not field.remote_field.multiple:
field.remote_field.delete_cached_value(obj)
raise ValueError(
"save() prohibited to prevent data loss due to "
"unsaved related object '%s'." % field.name
)
using = using or router.db_for_write(self.__class__, instance=self)
if force_insert and (force_update or update_fields):
raise ValueError("Cannot force both insert and updating in model saving.")
deferred_fields = self.get_deferred_fields()
if update_fields is not None:
# If update_fields is empty, skip the save. We do also check for
# no-op saves later on for inheritance cases. This bailout is
# still needed for skipping signal sending.
if len(update_fields) == 0:
return
update_fields = frozenset(update_fields)
field_names = set()
for field in self._meta.fields:
if not field.primary_key:
field_names.add(field.name)
if field.name != field.attname:
field_names.add(field.attname)
non_model_fields = update_fields.difference(field_names)
if non_model_fields:
raise ValueError("The following fields do not exist in this "
"model or are m2m fields: %s"
% ', '.join(non_model_fields))
# If saving to the same database, and this model is deferred, then
# automatically do a "update_fields" save on the loaded fields.
elif not force_insert and deferred_fields and using == self._state.db:
field_names = set()
for field in self._meta.concrete_fields:
if not field.primary_key and not hasattr(field, 'through'):
field_names.add(field.attname)
loaded_fields = field_names.difference(deferred_fields)
if loaded_fields:
update_fields = frozenset(loaded_fields)
self.save_base(using=using, force_insert=force_insert,
force_update=force_update, update_fields=update_fields)
如果commit=False,就不會儲存例項,當呼叫save方法後不會儲存ManyToMany欄位,需要自行去呼叫save_m2m方法,例如:
參考文章:https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#modelform
# Create a form instance with POST data.
>>> f = BookForm(request.POST)
# Create, but don't save the new book instance.
>>> new_book = f.save(commit=False)
# Modify the book in some way.
>>> new_book.some_field = 'some_value'
# Save the new instance.
>>> new_book.save()
# Now, save the many-to-many data for the form.
>>> f.save_m2m()
三、擴充套件
3.1、自定義BaseModelForm
class BaseRequestModelForm(object):
def __init__(self, request, *args, **kwargs):
self.request = request
super(BaseRequestModelForm, self).__init__(*args, **kwargs)
這樣,ModelForm中可以傳入request引數,當然還可以新增其它引數,然後再繼承自己的ModelForm,這樣自定義的ModelForm不僅僅有自己的功能,還可以傳參定製其它功能,在使用時繼承下面這個ModelForm即可:
class BaseModelForm(BaseRequestModelForm,forms.ModelForm):
def __init__(self,request,*args,**kwargs):
super().__init__(request,*args,**kwargs)
#####給modelform欄位加樣式
for name,field in self.fields.items():
attrs_dict={'class':'form-control'}
if 'DateTimeField' in field.__repr__():
attrs_dict = {'class': 'form-control', 'date_time': 'datetimepicker', 'size': '16'}
field.widget.attrs.update(attrs_dict)
3.1、動態生成ModelForm
每一個model都可以對應一個ModelForm類可用於自動生成表單等功能,但是如果能夠動態生成ModelForm豈不是更加省事,其實就是動態的生成一個類,並且設定類的一些屬性,首先先看一個普通的ModelForm類,依照此類動態生成:
class BookModelForm(ModelForm):
class Meta:
model = models.Book #對應的Model類
fields = '__all__' #對應的Model類中欄位
def __new__(cls,*args,**kwargs):
"""
:param cls:
:param args:
:param kwargs:
:return:
"""
#base_fields = [{'field_name':field_obj},] forms欄位物件
for field_name in cls.base_fields:
field_obj = cls.base_fields[field_name]
field_obj.widget.attrs.update({'class':'form-control'})
if field_name is not 'Checker':
field_obj.widget.attrs.update({'readonly': "readonly"}) # 將其他欄位 在 html 表單 中 顯示為只讀
return ModelForm.__new__(cls)
然後就可以進行動態生成這樣的ModelForm類了。
from django.forms import ModelForm
def CreateDynamicModelForm(model,fields=None,form_create=False,*args,**kwargs):
#預設為修改表單
attrs = {} #建立類使用的屬性字典
#如果沒有傳入fields預設就是全部
if not fields:
fields = "__all__"
#傳入request引數
if kwargs.get('request'):
attrs["request"] = kwargs.get('request')
class Meta:
pass
setattr(Meta,'model',model)
setattr(Meta,'fields',fields)
attrs["Meta"] = Meta
#如果給每一個欄位加入樣式,重寫__new__方法
def __new__(cls,*args,**kwargs):
"""
:param cls:
:param args:
:param kwargs:
:return:
"""
#base_fields = [{'field_name':field_obj},] forms欄位物件
for field_name in cls.base_fields:
field_obj = cls.base_fields[field_name]
field_obj.widget.attrs.update({'class':'form-control'})
return ModelForm.__new__(cls)
attrs["__new__"] = __new__
##建立類
name = 'DynamicModelForm' #建立類的名稱
bases = (ModelForm,) #建立類的基類
dynamic_model_form = type(name,bases,attrs)
return dynamic_model_form
以後就可以這樣使用了,也不用那麼麻煩的手動寫ModelForm類了。
from django.shortcuts import render,redirect,HttpResponse
from app01 import models
== #新增資料==
def BookAdd(request):
book_list = models.Book.objects.all()
BookModelForm = CreateDynamicModelForm(models.Book, form_create=True, request=request)
#獲取新增資料的表單
if request.method == "GET":
form = BookModelForm()
return render(request,'booklist.html',locals())
#POST請求新增資料
form = BookModelForm(data=request.POST)
if form.is_valid():
#儲存資料
form.save()
return HttpResponse('...')
== #修改資料==
def BookEdit(request,id):
#動態生成ModelForm類
BookModelForm = CreateDynamicModelForm(models.Book,request=request)
book = models.Book.objects.filter(id=id).first()
#獲取修改資料的表單
if request.method == "GET":
form = BookModelForm(instance=book)
return render(request, 'booklist.html', locals())
#POST請求新增修改過後的資料
form = BookModelForm(data=request.POST,instance=book)
#對資料驗證並且儲存
if form.is_valid():
form.save()
return HttpResponse('...')