============== Field Managers ============== One of the features in ``zope.formlib`` that works really well is the syntax used to define the contents of the form. The formlib uses form fields, to describe how the form should be put together. Since we liked this way of working, this package offers this feature as well in a very similar way. A field manager organizes all fields to be displayed within a form. Each field is associated with additional meta-data. The simplest way to create a field manager is to specify the schema from which to extract all fields. Thus, the first step is to create a schema: >>> import zope.interface >>> import zope.schema >>> class IPerson(zope.interface.Interface): ... id = zope.schema.Int( ... title=u'Id', ... readonly=True) ... ... name = zope.schema.TextLine( ... title=u'Name') ... ... country = zope.schema.Choice( ... title=u'Country', ... values=(u'Germany', u'Switzerland', u'USA'), ... required=False) We can now create the field manager: >>> from z3c.form import field >>> manager = field.Fields(IPerson) Like all managers in this package, it provides the enumerable mapping API: >>> manager['id'] >>> manager['unknown'] Traceback (most recent call last): ... KeyError: 'unknown' >>> manager.get('id') >>> manager.get('unknown', 'default') 'default' >>> 'id' in manager True >>> 'unknown' in manager False >>> manager.keys() ['id', 'name', 'country'] >>> [key for key in manager] ['id', 'name', 'country'] >>> manager.values() [, , ] >>> manager.items() [('id', ), ('name', ), ('country', )] >>> len(manager) 3 You can also select the fields that you would like to have: >>> manager = manager.select('name', 'country') >>> manager.keys() ['name', 'country'] Changing the order is simply a matter of changing the selection order: >>> manager = manager.select('country', 'name') >>> manager.keys() ['country', 'name'] Selecting a field becomes a little bit more tricky when field names overlap. For example, let's say that a person can be adapted to a pet: >>> class IPet(zope.interface.Interface): ... id = zope.schema.TextLine( ... title=u'Id') ... ... name = zope.schema.TextLine( ... title=u'Name') The pet field(s) can only be added to the fields manager with a prefix: >>> manager += field.Fields(IPet, prefix='pet') >>> manager.keys() ['country', 'name', 'pet.id', 'pet.name'] When selecting fields, this prefix has to be used: >>> manager = manager.select('name', 'pet.name') >>> manager.keys() ['name', 'pet.name'] However, sometimes it is tedious to specify the prefix together with the field; for example here: >>> manager = field.Fields(IPerson).select('name') >>> manager += field.Fields(IPet, prefix='pet').select('pet.name', 'pet.id') >>> manager.keys() ['name', 'pet.name', 'pet.id'] It is easier to specify the prefix as an afterthought: >>> manager = field.Fields(IPerson).select('name') >>> manager += field.Fields(IPet, prefix='pet').select( ... 'name', 'id', prefix='pet') >>> manager.keys() ['name', 'pet.name', 'pet.id'] Alternatively, you can specify the interface: >>> manager = field.Fields(IPerson).select('name') >>> manager += field.Fields(IPet, prefix='pet').select( ... 'name', 'id', interface=IPet) >>> manager.keys() ['name', 'pet.name', 'pet.id'] Sometimes it is easier to simply omit a set of fields instead of selecting all the ones you want: >>> manager = field.Fields(IPerson) >>> manager = manager.omit('id') >>> manager.keys() ['name', 'country'] Again, you can solve name conflicts using the full prefixed name, ... >>> manager = field.Fields(IPerson).omit('country') >>> manager += field.Fields(IPet, prefix='pet') >>> manager.omit('pet.id').keys() ['id', 'name', 'pet.name'] using the prefix keyword argument, ... >>> manager = field.Fields(IPerson).omit('country') >>> manager += field.Fields(IPet, prefix='pet') >>> manager.omit('id', prefix='pet').keys() ['id', 'name', 'pet.name'] or, using the interface: >>> manager = field.Fields(IPerson).omit('country') >>> manager += field.Fields(IPet, prefix='pet') >>> manager.omit('id', interface=IPet).keys() ['id', 'name', 'pet.name'] You can also add two field managers together: >>> manager = field.Fields(IPerson).select('name', 'country') >>> manager2 = field.Fields(IPerson).select('id') >>> (manager + manager2).keys() ['name', 'country', 'id'] Adding anything else to a field manager is not well defined: >>> manager + 1 Traceback (most recent call last): ... TypeError: unsupported operand type(s) for +: 'Fields' and 'int' You also cannot make any additions that would cause a name conflict: >>> manager + manager Traceback (most recent call last): ... ValueError: ('Duplicate name', 'name') When creating a new form derived from another, you often want to keep existing fields and add new ones. In order to not change the super-form class, you need to copy the field manager: >>> manager.keys() ['name', 'country'] >>> manager.copy().keys() ['name', 'country'] More on the Constructor ----------------------- The constructor does not only accept schemas to be passed in; one can also just pass in schema fields: >>> field.Fields(IPerson['name']).keys() ['name'] However, the schema field has to have a name: >>> email = zope.schema.TextLine(title=u'E-Mail') >>> field.Fields(email) Traceback (most recent call last): ... ValueError: Field has no name Adding a name helps: >>> email.__name__ = 'email' >>> field.Fields(email).keys() ['email'] Or, you can just pass in other field managers, which is the feature that the add mechanism uses: >>> field.Fields(manager).keys() ['name', 'country'] Last, but not least, the constructor also accepts form fields, which are used by ``select()`` and ``omit()``: >>> field.Fields(manager['name'], manager2['id']).keys() ['name', 'id'] If the constructor does not recognize any of the types above, it raises a ``TypeError`` exception: >>> field.Fields(object()) Traceback (most recent call last): ... TypeError: ('Unrecognized argument type', ) Additionally, you can specify several keyword arguments in the field manager constructor that are used to set up the fields: * ``omitReadOnly`` When set to ``True`` all read-only fields are omitted. >>> field.Fields(IPerson, omitReadOnly=True).keys() ['name', 'country'] * ``keepReadOnly`` Sometimes you want to keep a particular read-only field around, even though in general you want to omit them. In this case you can specify the fields to keep: >>> field.Fields( ... IPerson, omitReadOnly=True, keepReadOnly=('id',)).keys() ['id', 'name', 'country'] * ``prefix`` Sets the prefix of the fields. This argument is passed on to each field. >>> manager = field.Fields(IPerson, prefix='myform.') >>> manager['myform.name'] * ``interface`` Usually the interface is inferred from the field itself. The interface is used to determine whether an adapter must be looked up for a given context. But sometimes fields are generated in isolation to an interface or the interface of the field is not the one you want. In this case you can specify the interface: >>> class IMyPerson(IPerson): ... pass >>> manager = field.Fields(email, interface=IMyPerson) >>> manager['email'].interface * ``mode`` The mode in which the widget will be rendered. By default there are two available, "input" and "display". When mode is not specified, "input" is chosen. >>> from z3c.form import interfaces >>> manager = field.Fields(IPerson, mode=interfaces.DISPLAY_MODE) >>> manager['country'].mode 'display' * ``ignoreContext`` While the ``ignoreContext`` flag is usually set on the form, it is sometimes desirable to set the flag for a particular field. >>> manager = field.Fields(IPerson) >>> manager['country'].ignoreContext >>> manager = field.Fields(IPerson, ignoreContext=True) >>> manager['country'].ignoreContext True >>> manager = field.Fields(IPerson, ignoreContext=False) >>> manager['country'].ignoreContext False Fields Widget Manager --------------------- When a form (or any other widget-using view) is updated, one of the tasks is to create the widgets. Traditionally, generating the widgets involved looking at the form fields (or similar) of a form and generating the widgets using the information of those specifications. This solution is good for the common (about 85%) use cases, since it makes writing new forms very simple and allows a lot of control at a class-definition level. It has, however, its limitations. It does not, for example, allow for customization without rewriting a form. This can range from omitting fields on a particular form to generically adding a new widget to the form, such as an "object name" button on add forms. This package solves this issue by providing a widget manager, which is responsible providing the widgets for a particular view. The default widget manager for forms is able to look at a form's field definitions and create widgets for them. Thus, let's create a schema first: >>> import zope.interface >>> import zope.schema >>> class LastNameTooShort(zope.schema.interfaces.ValidationError): ... """The last name is too short.""" >>> def lastNameConstraint(value): ... if value and value == value.lower(): ... raise zope.interface.Invalid(u"Name must have at least one capital letter") ... return True >>> class IPerson(zope.interface.Interface): ... id = zope.schema.TextLine( ... title=u'ID', ... description=u"The person's ID.", ... readonly=True, ... required=True) ... ... lastName = zope.schema.TextLine( ... title=u'Last Name', ... description=u"The person's last name.", ... default=u'', ... required=True, ... constraint=lastNameConstraint) ... ... firstName = zope.schema.TextLine( ... title=u'First Name', ... description=u"The person's first name.", ... default=u'-- unknown --', ... required=False) ... ... @zope.interface.invariant ... def twiceAsLong(person): ... if len(person.lastName) >= 2 * len(person.firstName): ... raise LastNameTooShort() Next we need a form that specifies the fields to be added: >>> from z3c.form import field >>> class PersonForm(object): ... prefix = 'form.' ... fields = field.Fields(IPerson) >>> personForm = PersonForm() For more details on how to define fields within a form, see ``form.txt``. We can now create the fields widget manager. Its discriminators are the form for which the widgets are created, the request, and the context that is being manipulated. In the simplest case the context is ``None`` and ignored, as it is true for an add form. >>> from z3c.form.testing import TestRequest >>> request = TestRequest() >>> context = object() >>> manager = field.FieldWidgets(personForm, request, context) >>> manager.ignoreContext = True Widget Mapping ~~~~~~~~~~~~~~ The main responsibility of the manager is to provide the ``IEnumerableMapping`` interface and an ``update()`` method. Initially the mapping, going from widget id to widget value, is empty: >>> from zope.interface.common.mapping import IEnumerableMapping >>> IEnumerableMapping.providedBy(manager) True >>> manager.keys() [] Only by "updating" the manager, will the widgets become available; before we can use the update method, however, we have to register the ``IFieldWidget`` adapter for the ``ITextLine`` field: >>> from z3c.form import interfaces, widget >>> @zope.component.adapter(zope.schema.TextLine, TestRequest) ... @zope.interface.implementer(interfaces.IFieldWidget) ... def TextFieldWidget(field, request): ... return widget.FieldWidget(field, widget.Widget(request)) >>> zope.component.provideAdapter(TextFieldWidget) >>> from z3c.form import converter >>> zope.component.provideAdapter(converter.FieldDataConverter) >>> zope.component.provideAdapter(converter.FieldWidgetDataConverter) >>> manager.update() Other than usual mappings in Python, the widget manager's widgets are always in a particular order: >>> manager.keys() ['id', 'lastName', 'firstName'] As you can see, if we call update twice, we still get the same amount and order of keys: >>> manager.update() >>> manager.keys() ['id', 'lastName', 'firstName'] Let's make sure that all enumerable mapping functions work correctly: >>> manager['lastName'] >>> manager['unknown'] Traceback (most recent call last): ... KeyError: 'unknown' >>> manager.get('lastName') >>> manager.get('unknown', 'default') 'default' >>> 'lastName' in manager True >>> 'unknown' in manager False >>> [key for key in manager] ['id', 'lastName', 'firstName'] >>> manager.values() [, , ] >>> manager.items() [('id', ), ('lastName', ), ('firstName', )] >>> len(manager) 3 It is also possible to delete widgets from the manager: >>> del manager['firstName'] >>> len(manager) 2 >>> manager.values() [, ] >>> manager.keys() ['id', 'lastName'] >>> manager.items() [('id', ), ('lastName', )] Note that deleting a non-existent widget causes a ``KeyError`` to be raised: >>> del manager['firstName'] Traceback (most recent call last): ... KeyError: 'firstName' Properties of widgets within a manager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When a widget is added to the widget manager, it is located: >>> lname = manager['lastName'] >>> lname.__name__ 'lastName' >>> lname.__parent__ All widgets created by this widget manager are context aware: >>> interfaces.IContextAware.providedBy(lname) True >>> lname.context is context True Determination of the widget mode ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, all widgets will also assume the mode of the manager: >>> manager['lastName'].mode 'input' >>> manager.mode = interfaces.DISPLAY_MODE >>> manager.update() >>> manager['lastName'].mode 'display' The exception is when some fields specifically desire a different mode. In the first case, all "readonly" fields will be shown in display mode: >>> manager.mode = interfaces.INPUT_MODE >>> manager.update() >>> manager['id'].mode 'display' An exception is made when the flag, "ignoreReadonly" is set: >>> manager.ignoreReadonly = True >>> manager.update() >>> manager['id'].mode 'input' In the second case, the last name will inherit the mode from the widget manager, while the first name will want to use a display widget: >>> personForm.fields = field.Fields(IPerson).select('lastName') >>> personForm.fields += field.Fields( ... IPerson, mode=interfaces.DISPLAY_MODE).select('firstName') >>> manager.mode = interfaces.INPUT_MODE >>> manager.update() >>> manager['lastName'].mode 'input' >>> manager['firstName'].mode 'display' In a third case, the widget will be shown in display mode, if the attribute of the context is not writable. Clearly this can never occur in add forms, since there the context is ignored, but is an important use case in edit forms. Thus, we need an implementation of the ``IPerson`` interface including some security declarations: >>> from zope.security import checker >>> class Person(object): ... zope.interface.implements(IPerson) ... ... def __init__(self, firstName, lastName): ... self.id = firstName[0].lower() + lastName.lower() ... self.firstName = firstName ... self.lastName = lastName >>> PersonChecker = checker.Checker( ... get_permissions = {'id': checker.CheckerPublic, ... 'firstName': checker.CheckerPublic, ... 'lastName': checker.CheckerPublic}, ... set_permissions = {'firstName': 'test.Edit', ... 'lastName': checker.CheckerPublic} ... ) >>> srichter = checker.ProxyFactory( ... Person(u'Stephan', u'Richter'), PersonChecker) In this case the last name is always editable, but for the first name the user will need the edit ("test.Edit") permission. We also need to register the data manager and setup a new security policy: >>> from z3c.form import datamanager >>> zope.component.provideAdapter(datamanager.AttributeField) >>> from zope.security import management >>> from z3c.form import testing >>> management.endInteraction() >>> newPolicy = testing.SimpleSecurityPolicy() >>> oldpolicy = management.setSecurityPolicy(newPolicy) >>> management.newInteraction() Now we can create the widget manager: >>> personForm = PersonForm() >>> request = TestRequest() >>> manager = field.FieldWidgets(personForm, request, srichter) After updating the widget manager, the fields are available as widgets, the first name being in display and the last name is input mode: >>> manager.update() >>> manager['id'].mode 'display' >>> manager['firstName'].mode 'display' >>> manager['lastName'].mode 'input' However, explicitly overriding the mode in the field declaration overrides this selection for you: >>> personForm.fields['firstName'].mode = interfaces.INPUT_MODE >>> manager.update() >>> manager['id'].mode 'display' >>> manager['firstName'].mode 'input' >>> manager['lastName'].mode 'input' Required fields --------------- There is a flag for required fields. This flag get set if at least one field is required. This let us render a required info legend in forms if required fields get used. >>> manager.hasRequiredFields True Data extraction and validation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Besides managing widgets, the widget manager also controls the process of extracting and validating extracted data. Let's start with the validation first, which only validates the data as a whole, assuming each individual value being already validated. Before we can use the method, we have to register a "manager validator": >>> from z3c.form import validator >>> zope.component.provideAdapter(validator.InvariantsValidator) >>> personForm.fields = field.Fields(IPerson) >>> manager.update() >>> manager.validate( ... {'firstName': u'Stephan', 'lastName': u'Richter'}) () The result of this method is a tuple of errors that occurred during the validation. An empty tuple means the validation succeeded. Let's now make the validation fail: >>> errors = manager.validate( ... {'firstName': u'Stephan', 'lastName': u'Richter-Richter'}) >>> [error.doc() for error in errors] ['The last name is too short.'] A special case occurs when the schema fields are not associated with an interface: >>> name = zope.schema.TextLine(__name__='name') >>> class PersonNameForm(object): ... prefix = 'form.' ... fields = field.Fields(name) >>> personNameForm = PersonNameForm() >>> manager = field.FieldWidgets(personNameForm, request, context) In this case, the widget manager's ``validate()`` method should simply ignore the field and not try to look up any invariants: >>> manager.validate({'name': u'Stephan'}) () Let's now have a look at the widget manager's ``extract()``, which returns a data dictionary and the collection of errors. Before we can validate, we have to register a validator for the widget: >>> zope.component.provideAdapter(validator.SimpleFieldValidator) When all goes well, the data dictionary is complete and the error collection empty: >>> request = TestRequest(form={ ... 'form.widgets.id': u'srichter', ... 'form.widgets.firstName': u'Stephan', ... 'form.widgets.lastName': u'Richter'}) >>> manager = field.FieldWidgets(personForm, request, context) >>> manager.ignoreContext = True >>> manager.update() >>> data, errors = manager.extract() >>> data['firstName'] u'Stephan' >>> data['lastName'] u'Richter' >>> errors () Since all errors are immediately converted to error view snippets, we have to provide the adapter from a validation error to an error view snippet first: >>> from z3c.form import error >>> zope.component.provideAdapter(error.ErrorViewSnippet) >>> zope.component.provideAdapter(error.InvalidErrorViewSnippet) Let's now cause a widget-level error by not submitting the required last name: >>> request = TestRequest(form={ ... 'form.widgets.firstName': u'Stephan', 'form.widgets.id': u'srichter'}) >>> manager = field.FieldWidgets(personForm, request, context) >>> manager.ignoreContext = True >>> manager.update() >>> manager.extract() ({'firstName': u'Stephan'}, (,)) Or, we could violate a constraint. This constraint raises Invalid, which is a convenient way to raise errors where we mainly care about providing a custom error message. >>> request = TestRequest(form={ ... 'form.widgets.firstName': u'Stephan', ... 'form.widgets.lastName': u'richter', ... 'form.widgets.id': u'srichter'}) >>> manager = field.FieldWidgets(personForm, request, context) >>> manager.ignoreContext = True >>> manager.update() >>> extracted = manager.extract() >>> extracted ({'firstName': u'Stephan'}, (,)) >>> extracted[1][0].createMessage() u'Name must have at least one capital letter' Finally, let's ensure that invariant failures are also caught: >>> request = TestRequest(form={ ... 'form.widgets.id': u'srichter', ... 'form.widgets.firstName': u'Stephan', ... 'form.widgets.lastName': u'Richter-Richter'}) >>> manager = field.FieldWidgets(personForm, request, context) >>> manager.ignoreContext = True >>> manager.update() >>> data, errors = manager.extract() >>> errors[0].error.doc() 'The last name is too short.' Note that the errors coming from invariants are all error view snippets as well, just as it is the case for field-specific validation errors. And that's really all there is! By default, the ``extract()`` method not only returns the errors that it catches, but also sets them on individual widgets and on the manager: >>> manager.errors (,) This behavior can be turned off. To demonstrate, let's make a new request that causes a widget-level error: >>> request = TestRequest(form={ ... 'form.widgets.firstName': u'Stephan', 'form.widgets.id': u'srichter'}) >>> manager = field.FieldWidgets(personForm, request, context) >>> manager.ignoreContext = True >>> manager.update() We have to set the setErrors property to False before calling extract, we still get the same result from the method call, ... >>> manager.setErrors = False >>> manager.extract() ({'firstName': u'Stephan'}, (,)) but there are no side effects on the manager and the widgets: >>> manager.errors () >>> manager['lastName'].error is None True Customization of Ignoring the Context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Note that you can also manually control ignoring the context per field. >>> class CustomPersonForm(object): ... prefix = 'form.' ... fields = field.Fields(IPerson).select('id') ... fields += field.Fields(IPerson, ignoreContext=True).select( ... 'firstName', 'lastName') >>> customPersonForm = CustomPersonForm() Let's now create a manager and update it: >>> customManager = field.FieldWidgets(customPersonForm, request, context) >>> customManager.update() >>> customManager['id'].ignoreContext False >>> customManager['firstName'].ignoreContext True >>> customManager['lastName'].ignoreContext True Fields -- Custom Widget Factories --------------------------------- It is possible to declare custom widgets for fields within the field's declaration. Let's have a look at the default form first. Initially, the standard registered widgets are used: >>> manager = field.FieldWidgets(personForm, request, srichter) >>> manager.update() >>> manager['firstName'] Now we would like to have our own custom input widget: >>> class CustomInputWidget(widget.Widget): ... pass >>> def CustomInputWidgetFactory(field, request): ... return widget.FieldWidget(field, CustomInputWidget(request)) It can be simply assigned as follows: >>> personForm.fields['firstName'].widgetFactory = CustomInputWidgetFactory >>> personForm.fields['lastName'].widgetFactory = CustomInputWidgetFactory Now this widget should be used instead of the registered default one: >>> manager = field.FieldWidgets(personForm, request, srichter) >>> manager.update() >>> manager['firstName'] In the background the widget factory assignment really just registered the default factory in the ``WidgetFactories`` object, which manages the custom widgets for all modes. Now all modes show this input widget: >>> manager = field.FieldWidgets(personForm, request, srichter) >>> manager.mode = interfaces.DISPLAY_MODE >>> manager.update() >>> manager['firstName'] However, we can also register a specific widget for the display mode: >>> class CustomDisplayWidget(widget.Widget): ... pass >>> def CustomDisplayWidgetFactory(field, request): ... return widget.FieldWidget(field, CustomDisplayWidget(request)) >>> personForm.fields['firstName']\ ... .widgetFactory[interfaces.DISPLAY_MODE] = CustomDisplayWidgetFactory >>> personForm.fields['lastName']\ ... .widgetFactory[interfaces.DISPLAY_MODE] = CustomDisplayWidgetFactory Now the display mode should produce the custom display widget, ... >>> manager = field.FieldWidgets(personForm, request, srichter) >>> manager.mode = interfaces.DISPLAY_MODE >>> manager.update() >>> manager['firstName'] >>> manager['lastName'] ... while the input mode still shows the default custom input widget on the ``lastName`` field but not on the ``firstName`` field since we don't have the ``test.Edit`` permission: >>> manager = field.FieldWidgets(personForm, request, srichter) >>> manager.mode = interfaces.INPUT_MODE >>> manager.update() >>> manager['firstName'] >>> manager['lastName'] The widgets factories component, >>> factories = personForm.fields['firstName'].widgetFactory >>> factories {'display': } is pretty much a standard dictionary that also manages a default value: >>> factories.default When getting a value for a key, if the key is not found, the default is returned: >>> factories.keys() ['display'] >>> factories[interfaces.DISPLAY_MODE] >>> factories[interfaces.INPUT_MODE] >>> factories.get(interfaces.DISPLAY_MODE) >>> factories.get(interfaces.INPUT_MODE) If no default is specified, >>> factories.default = None then the dictionary behaves as usual: >>> factories[interfaces.DISPLAY_MODE] >>> factories[interfaces.INPUT_MODE] Traceback (most recent call last): ... KeyError: 'input' >>> factories.get(interfaces.DISPLAY_MODE) >>> factories.get(interfaces.INPUT_MODE)