您好,登錄后才能下訂單哦!
django是python語言快速實現web服務的大殺器,其開發效率可以非常的高!但因為秉承了語言的靈活性,django框架又太靈活,以至于想實現任何功能都有種“條條大路通羅馬”的感覺。這么多種選擇放在一起,如何分出高下?我想此時的場景下就兩個標準:
1、相同的功能用最少的代碼實現(代碼少BUG也會少);
2、相對最易于理解,從而易于維護和擴展。書歸正傳,web服務允許用戶輸入,基本上要靠表單。而django對表單的支持力度非常大,我們用不著在瀏覽器端的html文件里寫大量<form>代碼,再到web端去匹配form里的id/name/value、驗證規則,再與持久層數據庫比較并做操作。我們需要完成的工作非常少,可以沒有相似的重復代碼。有些復雜的場景,會要求一個表單的內容存放到多張表里,本文將通過4個部分,闡述它的實現方法。
1、django基礎表單的功能
定義一個表單非常簡單,繼承類django.forms.Form即可,例如:
class ProjectForm(forms.Form): name = forms.CharField(label='項目名稱', max_length=20)
這個表單類可以生成HTML形式的form,可以從request.POST中解析form到ProjectForm類實例。怎么做到的呢?
看下django.forms.Form定義:
class Form(six.with_metaclass(DeclarativeFieldsMetaclass, BaseForm)): "A collection of Fields, plus their associated data." # This is a separate class from BaseForm in order to abstract the way # self.fields is specified. This class (Form) is the one that does the # fancy metaclass stuff purely for the semantic sugar -- it allows one # to define a form using declarative syntax. # BaseForm itself has no way of designating self.fields.
注釋說得很清楚,Form這個類就是為了實現declarative syntax的,也就是說,繼承了Form后,我們直觀的表達ProjectForm里要有一個Field名叫name,不關心其語法實現,而通過Form多繼承中的DeclarativeFieldsMetaclass語法糖,將會把name弄到類實例的self.fields里。
我們重點關注表單的BaseForm類,它實現了基本的邏輯。截選了一小段對接下來的陳述有意義的代碼,做一個簡單的注釋。
class BaseForm(object): def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, label_suffix=None, empty_permitted=False, field_order=None, use_required_attribute=None): #data參數用于接收request.POST字典,如果是GET方法就不傳 self.data = data or {} #files用于接收request.FILES,也就是處理上傳文件 self.files = files or {} #本篇文章的重點在于多個表單集成到一個form中,此時為防止有同名的field,需要加prefix前綴 if prefix is not None: self.prefix = prefix #GET顯示表單時,如果要顯示初始值,請用initial參數 self.initial = initial or {} #模板中顯示{{form}}時,默認是以<table></table>顯示的 def __str__(self): return self.as_table() #如果模板中不想寫重復代碼,只以固定的格式來顯示每一個field,那么就用{% for field, val in form %}來遍歷處理吧 def __iter__(self): for name in self.fields: yield self[name] #如果傳入了prefix參數,html中每個field的name和id里都會加上prefix前綴 def add_prefix(self, field_name): return '%s-%s' % (self.prefix, field_name) if self.prefix else field_name #模板中以html格式顯示form就靠這個方法 def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row): "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()." top_errors = self.non_field_errors() # Errors that should be displayed above all fields. output, hidden_fields = [], [] #除了默認的table方式顯示外,還可以<ul><li>或者<p>方式顯示 def as_table(self): "Returns this form rendered as HTML <tr>s -- excluding the <table></table>." def as_ul(self): "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>." def as_p(self): "Returns this form rendered as HTML <p>s."
所以,基本表單的功能看BaseForm已經足夠了。
2、從模型創建表單
django對于MVC中的C與M間的映射是非常體貼的,集中體現中Model模型中(比如模型的權限與用戶認證)。那么,一個模型代表著RDS中的一張表,模型的實例代表著關系數據庫中的一行,而form如何與一行相對應呢?
定義一個模型引申出的表單非常簡單,例如:
class ProjectForm(ModelForm): class Meta: model = Project fields = ['approvals','manager','name','fund_rource','content','range',]
在model中告訴django模型是誰,在fields中告訴django需要在表單中創建哪些字段。django會有一個django.db.models.Field到django.forms.Field的轉換規則,此時會生成Form。我們看看ModelForm是什么樣的:
class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)): pass
類似Form類,ModelFormMetaclass就是語法糖,我們重點看BaseModelForm類:
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): opts = self._meta #相比較BaseForm,多了instance參數,它等價于Model模型的一個實例 if instance is None: #不傳instance參數,則會新構造model對象 self.instance = opts.model() object_data = {} else: self.instance = instance object_data = model_to_dict(instance, opts.fields, opts.exclude) #此時傳遞了initial也一樣可以生效,同時還會設置到Model中 if initial is not None: object_data.update(initial) def save(self, commit=True): #默認commit是True,此時就會保存Model實例到數據庫 if commit: self.instance.save() #同時保存many-to-many字段對應的關系表 self._save_m2m() else: #注意,本篇文章主要用到commit=False這個參數,它會返回Model實例,允許我們在修改instance后,在instance上再調用save方法 self.save_m2m = self._save_m2m return self.instance
所以,對于ModelForm我們可以傳入instance參數初始化表單,可以調用save()方法直接將從html里得到的表單數據持久化到數據庫中。而我們只需要幾十行代碼就可以完成這么多工作。
3、通用視圖
django.views.generic.ListView和django.views.generic.edit下的CreateView, UpdateView, DeleteView都是通用視圖。即,我們又可以通過它們,把很多重復的工作交給django完成,又可以少寫很多代碼完成同樣的功能了。這里僅以CreateView為例說明,因為它相對最復雜,接下來的多ModelForm的提交也是在CreateView上進行的。
通用視圖使用時,只需要承繼后,再設置model或者form_class即可。比如CreateView就會由django自動的把頁面上POST出的form數據解析到model生成的表單(或者form_calss指定的ModelForm類型表單),同時調用表單的save方法將數據添加到模型對應的數據庫表中。當然GET請求時會生成空form到頁面上。可以看到,除去定義model或者form類外,幾行代碼就可以搞定這么多事。我們看看CreateView的繼承關系:
簡單介紹下CreateView通用視圖中每個父類的作用。
View是所有視圖類的父類,根據方法名分發請求到具體的get或者post等方法,提供as_view方法。
TemplateResponseMixin提供render_to_response方法將響應通過context上下文在模板上渲染。
ContextMixin在context上下文中加入'view'元素,值為self實例。
ProcessFormView在GET請求上渲染表單,在POST請求上解析form到表單實例。注意,它會在post請求中判斷表單是否可用,is_valid為真時,會調用form_valid方法,因此,重寫form_valid方法是第4部分處理多model到一個form的關鍵。
FormMixin允許處理表單,可指定form_class為某個表單。
SingleObjectMixin生成context上下文,同時根據model模型名稱生成object并添加到上下文中的'object'元素。
ModelFormMixin提供在請求中處理modelform的方式。
SingleObjectTemplateResponseMixin幫助TemplateResponseMixin提供模板。
所以,在用CreateView、一個模型、一個模板實現添加一行記錄的功能時是多么簡單,因為這些父類會自動生成object,渲染到模板,解析form表單,save到數據庫中。所以,從模型創建出的表單ModelForm,配合上通用視圖后,威力巨大!!
4、多個ModelForm在一個form里提交
終于可以回到本文的主題了。CreateView默認是處理一個Model模型、一個ModelForm表單的,然而,很多時候為了解耦,會把一張表拆成多張表,通過id關聯在一起。在django的模型中就體現為ForeignKey、ManyToManyField或者OneToOneField。而在業務邏輯上,需要體現為一張表單,對應著數據庫里的多張表。
例如,我們希望錄入合同,其中合同Model中還有地址Model和項目Model,而項目Model中又有地址Model,等等。
當然,我們有很多種實現的方案,但是,前面三部分說了那么多,不是浪費口水的。我們已經有了通用視圖+ModelForm這樣的利器,難道還需要手動去寫Form表單?我們已經習慣了在Model里定義好類型和有點注釋作用還能當label的verbose_name,還需要在forms.Form里再來一遍?還需要在視圖中寫這么通用的邏輯代碼嗎?當然不用。
inlineformset_factory是一種方案,但它限制太多,而且有些晦澀,我個人感覺是不太好用的。
那么,從第1部分我介紹的Form里的prefix,以及第3部分里類圖中的ProcessFormView允許重定義form_valid,以及第2部分中ModelForm的save方法的行為控制,解決方案已經一目了然了。
拿上面提到的例子來說,我們創建合同時,指明了項目,包括項目地址和合同簽訂地址,這涉及到三張表和四條記錄(地址表有兩條)。
我們三張表的模型如下:
class PrimeContract(models.Model): address = models.ForeignKey(Address, related_name="prime_contract_address", verbose_name="address") project = models.ForeignKey(Project, related_name="prime_contract", verbose_name="project") class Project(models.Model): address = models.ForeignKey(Address, related_name="project_address", verbose_name="project address") class Address(models.Model): pass
接著,定義ModelForm表單,這非常簡單:
class AddressForm(ModelForm): class Meta: model = Address fields = ... class ProjectForm(ModelForm): class Meta: model = Project fields = ... class PrimeContractForm(ModelForm): class Meta: model = PrimeContract fields = ...
再寫視圖,這里要重寫2個方法:
class PrimeContractAdd(CreateView): success_url = ... template_name = ... form_class = PrimeContractForm def get_context_data(self, **kwargs): context = super(PrimeContractAdd, self).get_context_data(**kwargs) #SingleObjectMixin父類只會處理PrimeContractForm表單,另外三條數據庫記錄對應的表單我們要自己處理了,此時prefix派上用場了,因為Field重名是百分百的事 if self.request.method == 'POST': contractAddressForm = AddressForm(self.request.POST, prefix='contractAddressForm') projectAddressForm = AddressForm(self.request.POST, prefix='projectAddressForm') projectForm = ProjectForm(self.request.POST, prefix='projectForm') else: contractAddressForm = AddressForm(prefix='contractAddressForm') projectAddressForm = AddressForm(prefix='projectAddressForm') projectForm = ProjectForm(prefix='projectForm') #注意要把自己處理的表單放到context上下文中,供模板文件使用 context['contractAddressForm'] = contractAddressForm context['projectAddressForm'] = projectAddressForm context['projectForm'] = projectForm return context #重寫form_valid,父類ProcessFormView會在PrimeContractForm表單is_valid方法返回True時調用該方法 def form_valid(self, form): #首先我們要獲取到PrimeContractForm表單對應的模型,此時是不能save的,因為外鍵project和address對應的數據庫記錄還沒有創建,所以commit傳為False contract = form.save(commit=False) #獲取上面get_context_data方法中在POST里得到的表單 context = self.get_context_data() #按照四條數據庫記錄的順序依次的創建(調用save方法)、主鍵賦到下一條記錄的外鍵中、下一次記錄創建(save) projectAddress = context['projectAddressForm'].save() #從項目表單中獲取到模型,先把地址的id賦到外鍵上再保存 project = context['projectForm'].save(commit=False) project.address = projectAddress project.save() contractAddress = context['contractAddressForm'].save() #將合同模型中的address和project都設置好后再保存 contract.address = contractAddress contract.project = project contract.save() return super(PrimeContractAdd, self).form_valid(form)
最后寫模板:
#這三個表單我們手動處理過的 {{ contractAddressForm }} {{ projectAddressForm }} {{ projectForm }} #這是FormMixin父類幫我們生成的 {{ form }}
至此,我們可以只用幾十行代碼就完成復雜的功能,代碼邏輯也清晰可控。
從這篇文章里也可以看得出,django實在是快速開發網站的必備神器!當然,快速不代表不能夠支撐大并發的應用,instagram這個很火的服務就是用django寫的。由于python和django過于靈活,都將要求django的開發者們唯有更資深才能寫出生產環境下的服務。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。