Usage#
This chapter covers common usage patterns for YAFOWIL forms.
Widget Trees#
Forms are organized as widget trees. The form is the root, containing compounds (fieldsets) and leaves (input fields):
form = factory('form', name='myform', props={'action': '/submit'})
form['personal'] = factory('fieldset', props={'legend': 'Personal Info'})
form['personal']['name'] = factory('field:label:text', props={
'label': 'Name',
})
form['personal']['email'] = factory('field:label:email', props={
'label': 'Email',
})
form['submit'] = factory('submit', props={'action': True})
Data Extraction#
After processing, extracted data is available on controller.data:
request = {
'myform.personal.name': 'Alice',
'myform.personal.email': 'alice@example.com',
'action.myform.submit': 'submit',
}
controller = Controller(form, request)
name = controller.data.fetch('myform.personal.name').extracted
email = controller.data.fetch('myform.personal.email').extracted
Validation#
Required Fields#
Use the required property:
form['name'] = factory('field:label:error:text', props={
'label': 'Name',
'required': True, # or a custom message string
})
Datatype Conversion#
The datatype property converts extracted strings:
form['age'] = factory('field:label:text', props={
'label': 'Age',
'datatype': int,
})
form['id'] = factory('field:label:text', props={
'label': 'ID',
'datatype': uuid.UUID,
})
Supported by: hidden, proxy, text, lines, select, number.
Use emptyvalue for non-required fields with datatype:
form['count'] = factory('field:label:text', props={
'label': 'Count',
'datatype': int,
'emptyvalue': 0,
})
Custom Validation#
Inject custom extractors using the * prefix:
from yafowil.base import ExtractionError
def validate_range(widget, data):
value = data.extracted
if value is not None and not (0 <= value <= 100):
raise ExtractionError('Value must be between 0 and 100')
return value
form['score'] = factory('field:label:error:*rangecheck:text', props={
'label': 'Score',
'datatype': int,
}, custom={
'rangecheck': {'extractors': [validate_range]},
})
Invariants#
Cross-field validation uses extractors on parent compounds:
def xor_validator(widget, data):
a = data['field_a'].extracted
b = data['field_b'].extracted
if bool(a) == bool(b):
error = ExtractionError('Fill exactly one field')
data['field_a'].errors.append(error)
data['field_b'].errors.append(error)
return data.extracted
form = factory('*xor:form', name='myform', props={'action': '/'}, custom={
'xor': {'extractors': [xor_validator]},
})
form['field_a'] = factory('field:label:error:text', props={'label': 'A'})
form['field_b'] = factory('field:label:error:text', props={'label': 'B'})
Persistence#
YAFOWIL provides delegation for single-model forms.
Manual Persistence#
Handle extraction manually in the submit handler:
def save(widget, data):
model.name = data.fetch('myform.name').extracted
model.email = data.fetch('myform.email').extracted
session.commit()
Automatic Persistence#
Use data.write() for automatic delegation:
from yafowil.persistence import attribute_writer
form = factory('form', name='myform', props={
'action': '/submit',
'persist_writer': attribute_writer,
})
form['name'] = factory('field:label:text', props={
'label': 'Name',
'persist': True, # Default for most input blueprints
})
def save(widget, data):
data.write(model) # Writes all persist=True fields to model
session.commit()
Available writers:
attribute_writer- Sets attributes on modelwrite_mapping_writer- Sets dict keys on modelnode_attribute_writer- Sets node.attrs keys on model
Customizing Persistence#
Override target attribute name:
form['user_name'] = factory('text', props={
'persist': True,
'persist_target': 'name', # Writes to model.name, not model.user_name
})
Override writer per field:
form['tags'] = factory('text', props={
'persist': True,
'persist_writer': my_custom_writer,
})
Form Actions#
Submit buttons trigger handlers when clicked:
def save_handler(widget, data):
# Process the form
pass
def next_url(request):
return '/success'
form['submit'] = factory('submit', props={
'handler': save_handler,
'next': next_url,
'action': True,
})
Multiple actions:
form['save'] = factory('submit', props={
'handler': save_handler,
'action': 'save',
})
form['delete'] = factory('submit', props={
'handler': delete_handler,
'action': 'delete',
})