Skip to content

Latest commit

 

History

History
221 lines (159 loc) · 14 KB

File metadata and controls

221 lines (159 loc) · 14 KB

WTForms

新手上路 ?? {: #getting-started }

  • Crash Course - WTForms Documentation #ril
    • With WTForms, your form field HTML can be generated for you, but we let you customize it in your TEMPLATES. This allows you to maintain SEPARATION OF CODE AND PRESENTATION, and keep those messy parameters out of your python code. 透過 Field() 傳入的 keyword params??

    • Form 是 core container,表示一群 field,可以透過 dictionary-style 或 attribute style 存取個別的 field。

    • Field 則擔負了所有吃力的工作 (heavy lifting),從 "Each field represents a data type and the field handles COERCING FORM INPUT TO THAT DATATYPE." 看來,內部會做資料的強制轉型 (coerce);也就是 field = input + data (type),在 input -> data 之間,有 validation (可以有多個 validator)、data conversion??

    • 每個 field 背後預設都會有個 widget,負責畫出代表這個 field 的 HTML representation。

    • 首先,像 ORM 一樣用 class variables 宣告 fields:

      from wtforms import Form, BooleanField, StringField, validators
      
      class RegistrationForm(Form):
          username     = StringField('Username', [validators.Length(min=4, max=25)])
          email        = StringField('Email Address', [validators.Length(min=6, max=35)])
          accept_rules = BooleanField('I accept the site rules', [validators.InputRequired()])
      
    • 就像一般的 Python class,也可以繼承 -- 共用一些 field。

      class ProfileForm(Form):
          birthday  = DateTimeField('Your Birthday', format='%m/%d/%y')
          signature = TextAreaField('Forum Signature')
      
      class AdminProfileForm(ProfileForm):
          username = StringField('Username', [validators.Length(max=40)])
          level    = IntegerField('User Level', [validators.NumberRange(min=0, max=10)])
      
    • 使用時就是產生一個 instance,例如:

      def register(request):
          form = RegistrationForm(request.POST) # 如果進來的是 GET request 會怎樣??
          if request.method == 'POST' and form.validate():
              user = User()
              user.username = form.username.data
              user.email = form.email.data
              user.save()
              redirect('register') # 成功就轉向
          return render_response('register.html', form=form) # 失敗就重畫
      
    • 如果要編修現有的資料? 透過 Form(formdata, obj) 第 2 個參數提供初始資料,再用 Form.populate_obj() 將 updated data 抄寫到另一個 object,這對簡單的 CRUD、admin forms 很實用。

      def edit_profile(request):
          user = request.current_user
          form = ProfileForm(request.POST, user) # 會從 obj (user) 取 post data 裡拿不到的資料
          if request.method == 'POST' and form.validate():
              form.populate_obj(user) # 將 post data (轉換過的) 寫回與 field 同名的 names
              user.save()
              redirect('edit_profile')
          return render_response('edit_profile.html', form=form)
      

Form ??

Field ??

Validation (Field-level) ??

Validation (Form-level) ??

Data Access, Processing ??

  • How Forms get data - Crash Course - WTForms Documentation
    • 除了透過 Form(formdata, obj) 提供 data 外,也可以透過 keyword arguments 來提供,但要避開現有的參數名稱就是,例如 formdataobjprefix 等。
    • formdata takes precendence over obj, which itself takes precedence over keyword arguments. 然後才是 field 宣告時提供的 default value。
    • If a form was submitted (request.POST is not empty), process the form input. ... If there was no form input, then try the following in order: obj argument -> keyword arguments -> default value (field)
  • wtforms/core.py at master · wtforms/wtforms DateTimeField 內部 process_formdata() 會做型態轉換,然後存進 self.data 裡,而 _value() 則會將 self.data 格式化成字串;中間出現的 self.raw_data 又有什麼不同??
  • Custom Fields - WTForms Documentation #ril
    • 這裡示範 "comma-separated list of tags",用 _value()list (of tags) 轉為 comma-separated tags (string),反之 process_formdata() 則將 comma-separated tags (string) 轉回 list (of tags);又 process_formdata() 是發生在 validation 之後嗎??

Widget, Rendering ??

  • Rendering Fields - Crash Course - WTForms Documentation

    • 要把一個 field 畫出來,其實就是轉型成 string,例如 str(form.my_field)unicode(form.my_field)
    • 但它真正的威力來自於 __call__(),可以額外提供 keyword arguments,會被輸出成 HTML attributes,例如 form.my_field(style="width: 200px;", class_="bar")
    • 在 Jinja template 裡用起來像這樣 <div>{{ form.username.label }}: {{ form.username(class="css_class") }}</div>,或是 Django template 要用 {% form_field form.username class="css_class" %};其中的 form_field templatetag (Django 專屬的 extension) 用於要額外傳遞 keyword arguments 時。
  • Displaying Errors - Crash Course - WTForms Documentation #ril

    • 分 field 印 error message:

      <form method="POST" action="/login">
          <div>{{ form.username.label }}: {{ form.username(class="css_class") }}</div>
          {% if form.username.errors %}
              <ul class="errors">{% for error in form.username.errors %}<li>{{ error }}</li>{% endfor %}</ul>
          {% endif %}
      
          <div>{{ form.password.label }}: {{ form.password() }}</div>
          {% if form.password.errors %}
              <ul class="errors">{% for error in form.password.errors %}<li>{{ error }}</li>{% endfor %}</ul>
          {% endif %}
      </form>
      

    或是集中在一起提示:

    • 這時候 label 的識別度很重要,否則 "This field is required" 看不出來是在講誰。

    • 另外 |dictsort 是必要的,否則會遇到 ValueError: too many values to unpack,不過因為它是按 field name 排序 (form.errors 的結構 {field_name: [errors]})),所以排出來的結果可能跟 UI 的順序不同,或許個別顯示是比較好的。

      {% if form.errors %}
          <ul class="errors">
              {% for field_name, field_errors in form.errors|dictsort if field_errors %}
                  {% for error in field_errors %}
                      <li>{{ form[field_name].label }}: {{ error }}</li>
                  {% endfor %}
              {% endfor %}
          </ul>
      {% endif %}
      
    • 不過 error handling 很煩雜,最後建議用 Jinja 的 macro 來簡化。

  • Rendering Errors - WTForms Documentation

  • Widgets - WTForms Documentation #ril

Testing ??

  • Exploring in the console - Crash Course - WTForms Documentation 再次強調 form 只是個 simple container object,可以在 Python console 裡直接玩 -- 不需要 web framework;呼叫 Form.validate(),若 invalid 會傳回 False,從 Form.errors (dict) 可以得到錯誤的細節。

    >>> from wtforms import Form, StringField, validators
    >>> class UsernameForm(Form):
    ...     username = StringField('Username', [validators.Length(min=5)], default=u'test')
    ...
    >>> form = UsernameForm()
    >>> form['username']
    <wtforms.fields.StringField object at 0x827eccc>
    >>> form.username.data
    u'test'
    >>> form.validate()
    False
    >>> form.errors
    {'username': [u'Field must be at least 5 characters long.']}
    
    
    >>> form2 = UsernameForm(username=u'Robert') # 由 **kwargs 提供資料
    >>> form2.data
    {'username': u'Robert'}
    >>> form2.validate()
    True
    
  • class wtforms.form.Form - WTForms Documentation Form(data=<dict>) 似乎更適合拿來測試??

File Upload ??

CSRF ??

Form object 不適合傳進 domain model?

  • Form 比較像是 presentation mode。
  • 假設前端 UI 輸入日期的設計是用分開的下拉清單分別選最年、月、日,那麼 form 顯然就不適合傳進 domain model,應該要解析成 date 型態後再傳進去。

安裝設置 {: #setup }

參考資料 {: #reference }

社群:

相關:

手冊: