====================
Automatic form setup
====================

This package provides tools to construct z3c.form forms out of hints stored
in tagged values on schema interfaces. A special form base class is used to
set up the 'fields' and 'groups' properties on form instances.

The tagged values are stored under keys represented by the following 
constants:

    >>> from plone.autoform.interfaces import OMITTED_KEY
    >>> from plone.autoform.interfaces import WIDGETS_KEY
    >>> from plone.autoform.interfaces import MODES_KEY
    >>> from plone.autoform.interfaces import ORDER_KEY
    >>> from plone.autoform.interfaces import READ_PERMISSIONS_KEY
    >>> from plone.autoform.interfaces import WRITE_PERMISSIONS_KEY

In addition, field groups are constructed from plone.supermodel fieldsets,
which are also stored in tagged values, under the following constant:

    >>> from plone.supermodel.interfaces import FIELDSETS_KEY

There are several ways to set the form data:

    - Manually, by using setTaggedValue() on an interface.

    - By loading the schema from a plone.supermodel XML file. This package
      provides a schema handler for the 'form' prefix that can be used to
      incorporate form hints. See supermodel.txt for details.
      
    - By using the grok directives in the plone.directives.form package.
    
For the purposes of this test, we'll set the form data manually.

Test setup
----------

First, let's load this package's ZCML so that we can run the tests:

    >>> configuration = """\
    ... <configure xmlns="http://namespaces.zope.org/zope">
    ...
    ...     <include package="Products.Five" file="configure.zcml" />
    ...     <include package="plone.autoform" />
    ...     
    ... </configure>
    ... """
    >>> from StringIO import StringIO
    >>> from zope.configuration import xmlconfig
    >>> xmlconfig.xmlconfig(StringIO(configuration))

We also need a few sample interfaces:

    >>> from zope.interface import Interface
    >>> from zope import schema

    >>> class ITestSchema(Interface):
    ...     one = schema.TextLine(title=u"One")
    ...     two = schema.TextLine(title=u"Two")
    ...     three = schema.TextLine(title=u"Three")
    ...     four = schema.TextLine(title=u"Four")
    ...     five = schema.TextLine(title=u"Five")
    ...     six = schema.TextLine(title=u"Six")

    >>> class ISupplementarySchema(Interface):
    ...     one = schema.TextLine(title=u"One")
    ...     two = schema.TextLine(title=u"Two")

    >>> class IOtherSchema(Interface):
    ...     three = schema.TextLine(title=u"Three")
    ...     four = schema.TextLine(title=u"Four")
    ...     five = schema.TextLine(title=u"Five")
    ...     six = schema.TextLine(title=u"Six")

And a test context and request, marked with the IFormLayer interface to make
z3c.form happy:

    >>> from zope.publisher.browser import TestRequest
    >>> from z3c.form.interfaces import IFormLayer
    >>> context = object()
    >>> request = TestRequest(environ={'AUTHENTICATED_USER': 'user1'}, skin=IFormLayer)

Note that we need to pretend that we have authenticated a user. Without this,
the permission checks will be turned off. This is to support setting up a form
pre-traversal in the ++widget++ namespace in plone.z3cform.

And finally, a form:

    >>> from plone.autoform.form import AutoExtensibleForm
    >>> from z3c.form import form, button
    >>> class TestForm(AutoExtensibleForm, form.Form):
    ...     schema = ITestSchema
    ...     additionalSchemata = (ISupplementarySchema, IOtherSchema,)
    ...     
    ...     ignoreContext = True

This form is in input mode:

    >>> TestForm.mode
    'input'

Adding form data
----------------

Form data can be held under the following keys:

    OMITTED_KEY -- A dict of fieldName => boolean. If the second value
        evaluates to true, the given field name is omitted.
    
    MODES_KEY -- A dict of fieldName => mode string, specifying z3c.form
        widget modes, including 'hidden', 'input' and 'display'.
    
    WIDGETS_KEY -- A dict of fieldName => widget. The widget can be
        the dotted name of a z3c.form field widget factory, or an actual
        instance of one.
        
    ORDER_KEY -- A list of (fieldName, direction, relative_to) triples.
        direction can be one of 'before' or 'after'. relative_to can be '*'
        (any/all fields), or the name of a field to move the given field 
        before or after in the form.
    
    READ_PERMISSIONS_KEY -- A dict of fieldName => permission id. When a 
        form is in 'display' mode, the field will be omitted unless the user
        has the given permission in the form's context. The permission id
        should be a Zope 3 style IPermission utility name, not a Zope 2
        permission string.

    WRITE_PERMISSIONS_KEY -- A dict of fieldName => permission id. When a 
        form is in 'input' mode, the field will be omitted unless the user
        has the given permission in the form's context. The permission id
        should be a Zope 3 style IPermission utility name, not a Zope 2
        permission string.

Note that 'order' directives are processed after all schemata in the form are
set up. Ordering will start by going through the additionalSchemata in order.
The form's base schema is processed last.

This means that the last ordering directive to be run is the last item in the
list in the form's base schema. Hence, this can be used to override any
ordering information from additional schemata.

The fieldName should never contain a prefix or a dot. However, the
relative_to name under ORDER_KEY should contain a prefixed name. The default
form schema will not have a prefix, but additional schemata will have a prefix
constructed from their __identifier__ (full dotted name). To explicitly
reference a field in the current schema (or a base schema), use a leading
dot, e.g. ".title" would refer to the "title" field in the current schema,
whereas "title" would refer to the "title" field in the form's base schema.

Fieldset data is kept under the key defined in the constant FIELDSETS_KEY.
This contains a list of plone.supermodel.model.Fieldset instances.

At this point, there is no form data. When the form is updated, the 'fields'
and 'groups' properties will be set.

    >>> test_form = TestForm(context, request)
    >>> test_form.update()
    >>> test_form.fields.keys()
    ['one', 'two', 'three', 'four', 'five', 'six',
     'ISupplementarySchema.one', 'ISupplementarySchema.two',
     'IOtherSchema.three', 'IOtherSchema.four', 'IOtherSchema.five', 'IOtherSchema.six']
    >>> test_form.groups
    ()

Note how we have all the fields from all the schemata, and that the fields
from the additional schemata have been prefixed with the schema dotted name.

Let us now set up some form data.

Omitted fields are listed like this:

    >>> ITestSchema.setTaggedValue(OMITTED_KEY, {'four': True, 'five': True})

Field modes can be set like this:

    >>> ITestSchema.setTaggedValue(MODES_KEY, {'one': 'hidden', 'two': 'display'})

Widgets can be specified either by a dotted name string or an actual instance:

    >>> from z3c.form.browser.password import PasswordFieldWidget
    >>> ITestSchema.setTaggedValue(WIDGETS_KEY, {'two': PasswordFieldWidget})
    >>> IOtherSchema.setTaggedValue(WIDGETS_KEY, {'five': 'z3c.form.browser.password.PasswordFieldWidget'})

Fields can be moved like this:

    >>> IOtherSchema.setTaggedValue(ORDER_KEY, [('four', 'before', 'ISupplementarySchema.one'),
    ...                                         ('five', 'after', '.six',)])

    >>> ISupplementarySchema.setTaggedValue(ORDER_KEY, [('one', 'before', '*'),
    ...                                                 ('two', 'before', 'one')])

    >>> ITestSchema.setTaggedValue(ORDER_KEY,          [('one', 'after', 'two')])

Note how the second value of each tuple refers to the full name with a prefix,
so the field 'two' from ISupplementarySchema is moved before the field 'one'
from the default (un-prefixed) ITestSchema. However, we move IOtherSchema's
field 'five' after the field 'six' in the same schema by using a shortcut:
'.six' is equivalent to 'IOtherSchema.six' in this case.

Field permissions can be set like this:

    >>> ITestSchema.setTaggedValue(WRITE_PERMISSIONS_KEY, { 'five': u'dummy.PermissionOne',
    ...                                                      'six': u'cmf.ManagePortal'})

Note that if a permission is not found, the field will be allowed.

Finally, fieldsets are configured like this:

    >>> from plone.supermodel.model import Fieldset
    >>> ITestSchema.setTaggedValue(FIELDSETS_KEY, 
    ...                                 [Fieldset('fieldset1', fields=['three'],
    ...                                           label=u"Fieldset one",
    ...                                           description=u"Description of fieldset one")])
    >>> IOtherSchema.setTaggedValue(FIELDSETS_KEY, [Fieldset('fieldset1', fields=['three'])])

Note how the label/description need only be specified once.

The results of all of this can be seen below:

    >>> test_form = TestForm(context, request)
    >>> test_form.update()
    >>> test_form.fields.keys()
    ['IOtherSchema.four',
     'ISupplementarySchema.one',
     'ISupplementarySchema.two',
     'two',
     'one',
     'IOtherSchema.six',
     'IOtherSchema.five']

The field ISupplementarySchema['one'] was moved to the top of the form, but
then IOtherSchema['four'] was moved before this one again. ITestSchema['one']
was moved after ITestSchema['two']. ISupplementarySchema['two'] was then
moved before ITestSchema['one'], coming between ITestSchema['one'] and
ITestSchema['two'].

ITestSchema['one'] was hidden and ITestSchema['two'] was put into display
mode:

    >>> test_form.widgets['one'].mode
    'hidden'
    >>> test_form.widgets['two'].mode
    'display'

ITestSchema['two'] and IOtherSchema['five'] were both given a password
widget - one by instance, the other by dotted name:

    >>> test_form.widgets['two']
    <PasswordWidget 'form.widgets.two'>

    >>> test_form.widgets['IOtherSchema.five']
    <PasswordWidget 'form.widgets.IOtherSchema.five'>

There is one group corresponding to the fieldset where we put two fields. It
has taken the label and description from the first definition.

    >>> len(test_form.groups)
    1
    >>> test_form.groups[0].label
    u'Fieldset one'
    >>> test_form.groups[0].description
    u'Description of fieldset one'
    >>> test_form.groups[0].fields.keys()
    ['three', 'IOtherSchema.three']

Pre-traversal
-------------

plone.z3cform installs a ++widget++ namespace to allow traversal to widgets.
Unfortunately, traversal happens before authentication. Thus, all security
checks (read/write permissions) will fail.

To work around this, we ignore security checks if no authenticated user is
set in the request. Previously, we added one to the test request. If we
run the same tests without an authenticated user, the field 'six' should
return.

    
    >>> request = TestRequest(skin=IFormLayer)

    >>> test_form = TestForm(context, request)
    >>> test_form.update()
    >>> test_form.fields.keys()
    ['IOtherSchema.four',
     'ISupplementarySchema.one',
     'ISupplementarySchema.two',
     'two',
     'one',
     'six',
     'IOtherSchema.six',
     'IOtherSchema.five']

Automatic field sets
--------------------

It is possible to create fieldsets automatically, on the principle of one
fieldset per schema. In this case, the fieldset name is the schema name,
the schema docstring becomes the schema description, and all fields in that
schema that are not explicitly assigned to another fieldset, will be in the
the per-schema fieldset.

    >>> class Basics(Interface):
    ...     """Basic metadata"""
    ...     title = schema.TextLine(title=u"Title")
    ...     description = schema.TextLine(title=u"Description")
    ...     creation_date = schema.Date(title=u"Creation date")
    ...     hidden_secret = schema.TextLine(title=u"Hidden secret!")

Let's change some field settings to ensure that they are still processed,
and move the creation_date field to another fieldset, which we will define
in full.

    >>> Basics.setTaggedValue(MODES_KEY, {'hidden_secret': 'hidden'})
    >>> Basics.setTaggedValue(FIELDSETS_KEY, [Fieldset('Dates', label="Cool dates", fields=['creation_date'])])
    
    >>> class Dates(Interface):
    ...     """Date information"""
    ...     start_date = schema.Date(title=u"Start date")
    ...     end_date = schema.Date(title=u"End date")
    
    >>> class Ownership(Interface):
    ...     """Ownership information"""
    ...     owner = schema.Date(title=u"The owner")

We can make a form of these schemata. For the sake of this demo, we'll also
set ignorePrefix to true, so that the form fields don't get a prefix. Note
that this may cause clashes if fields in different schemata share a name.

    >>> class CombiForm(AutoExtensibleForm, form.Form):
    ...     schema = Basics
    ...     additionalSchemata = (Dates, Ownership,)
    ...     
    ...     ignoreContext = True
    ...     ignorePrefix = True
    ...     autoGroups = True

    >>> combi_form = CombiForm(context, request)
    >>> combi_form.update()

The default fields are those from the base schema, minus the one moved to
another fieldset.

    >>> combi_form.fields.keys()
    ['title', 'description', 'hidden_secret']
    
    >>> combi_form.widgets['hidden_secret'].mode
    'hidden'
    
Each additional schema then has its own fields. Note that setting the 'dates'
fieldset in the base schema had the effect of giving a more specific
label to the automatically created group for the Dates schema.
    
    >>> [(g.__name__, g.label, g.description, g.fields.keys(),) for g in combi_form.groups]
    [('Dates', 'Cool dates', None, ['creation_date', 'start_date', 'end_date']),
     ('Ownership', 'Ownership', 'Ownership information', ['owner'])]