My favorites | Sign in
Project Home Downloads Wiki Issues Source
Search
for
HowTo_CreateManualFormsInBungeni  
Create Form manually and override the built in automatic form generation
zope, sqlalchemy, forms
Updated Feb 2, 2012 by mario.ruggier@gmail.com

Introduction

For some use cases the autogenerated forms are not sufficient.

Details

depends on what you mean by computed fields.. read only fields on a form, sure. computed fields in the sense that the value is dynamic basic on whole object state yes, though the latter would require.

Both of these require actually creating a form, not using the autogenerated facilities. This is pretty easy.

first there are two tasks here. using the subclassing alchemist content forms, means you will get autogenerated ui implemenentation, which sets up form_fields on the view when published, which is not what you want. typically you would just use the corresponding base formlib class or formlib viewlet classes.

the default edit views, are composed of various viewlets, an edit attribute viewlet is used for an object's attributes, and typically relations viewlets for other parts. if you want a completely custom view, you can just replace the entire view, but probably more typical is just replacing the edit attributes viewlet.

Customize the edit attribute viewlet for a particular class, i.e. say i want a readonly field on an edit form. You can prep formlib by telling it the adapter, so instead of going to query the component architecture it just uses a dictionary mapping interface to adapter.

from alchemist.ui.viewlet import EditFormViewlet
from zope.formlib import form
from zope import schema, interface
from bungeni.core.interfaces import IMyContent

class ContentEditForm( EditFormViewlet ):
   form_fields = form.Fields( interfaces.IMyContent )
   form_fields['name'].for_display= True
   template = NamedTemplate('alchemist.subform')   

    def update(self):
        """
        Called by formlib after __init__ for every page update. This is
        the method you can use to update form fields from your class
        and adapt the custom fields to our object
        """               
        self.adapters = { IMyContent : self.context }        

The name of the view should be the same as the autogenerated view, and you have to register it for the class, which will override the default registration of the autogenerated form.

To get the name of the autogenerated form you may use echo="True" on the db:catalyst declaration and it will log to the stdout all the things that its doing.

  <db:catalyst
     class=".domain.GroupSittingAttendance"
     descriptor=".descriptor.AttendanceDescriptor"
     interface_module=".interfaces"
     ui_module="bungeni.ui.content"
     echo="True"
    />  

will produce the output:

catalyst domain -> GroupSittingAttendance: generated interface bungeni.core.interfaces.IGroupSittingAttendance
catalyst container -> GroupSittingAttendance: generated container bungeni.core.domain.GroupSittingAttendanceContainer
catalyst container -> GroupSittingAttendance: generated container interface bungeni.core.domain.IGroupSittingAttendanceContainer
catalyst ui -> GroupSittingAttendance: generated add view bungeni.ui.content.GroupSittingAttendanceAddForm
catalyst ui -> GroupSittingAttendance: registered GroupSittingAttendanceAddForm for IGroupSittingAttendanceContainer, layer Default, permission zope.Public
catalyst ui -> GroupSittingAttendance: generated edit view bungeni.ui.content.GroupSittingAttendanceEditForm
catalyst ui -> GroupSittingAttendance: registered GroupSittingAttendanceEditForm for IGroupSittingAttendance, layer Default, permission zope.Public
catalyst ui -> GroupSittingAttendance: generated view view bungeni.ui.content.GroupSittingAttendanceDisplayForm
catalyst ui -> GroupSittingAttendance: registered GroupSittingAttendanceDisplayForm for IGroupSittingAttendance, layer Default, permission zope.Public

The add form is also a bit of special case. Its not a set of viewlets, just a dynamic form view. i need to do a refactor on it, as currently tied into the dynamic form proccesing, and you do want the add behavior it specifies. as temporary work around you can define an update method, and prevent the default form from dynamically resetting its form_fields. Set the adapters in the finishConstruction method.

class MyContentAddForm( content.ContentAddForm ):
    form_fields = form.Fields( IMyCustomFields )

    def update( self ):
         self.status = self.request.get('portal_status_message','')
         form.AddForm.update( self )

    def finishConstruction( self, ob ):
         self.adapters = { IMyCustomFields : ob }

Any manually created forms need also to be manually registered in zcml. Note that the add form is registered for the container where you add the contents, whereas the edit form is tied to the interface of the content itself.

<configure xmlns="http://namespaces.zope.org/zope"
           xmlns:browser="http://namespaces.zope.org/browser" >
  <!-- Add Form -->
   <browser:view
      name="add"
      for="bungeni.core.interfaces.IMyContentContainer"
      class=".forms.MyContentAddForm"              
      permission="zope.View"
      />

    
  <!-- Edit Form -->
    <browser:viewlet
      name="alchemist.attributes.edit"
      manager="alchemist.ui.interfaces.IContentEditManager"
      class=".forms.ContentEditForm"
      for="bungeni.core.interfaces.IMyContent"
      permission="zope.View"
      allowed_interface="zope.formlib.interfaces.ISubPageForm"      
      />
      

</configure>

first, this really doesn't have anything to do with computed fields. second when accessing context its important to understand that their are two different categories of forms with two different contexts, ie. add form and edit forms. unlike cmf, zope3 isn't required to create a content stub just to have a form, but that distinction also means we have two forms for a content that would need to be potentially modified to handle these constraints.

second, any formlib form can have a custom validator independent of any schema invariants, as part of the action handler. see zope.formlib readme for examples.

def CheckSittingDatesInsideParentDates( context, data ):
    """
    start date must be >= parents start date
    end date must be <= parents end date (if parents end date is set)
    """
    errors =[]
    if context.__parent__.start_date > data['start_date'].date():
        errors.append( interface.Invalid(_("Start must be after Session Start Date")) )
    if context.__parent__.end_date is not None:
        if data['end_date'].date() > context.__parent__.end_date:
            errors.append(  interface.Invalid(_("End must be before Session End Date")) )
    return errors

class GroupSittingAdd( ContentAddForm ):
    """
    override the AddForm for GroupSittingAttendance
    """
    form_fields = form.Fields( IGroupSitting )
    form_fields["start_date"].custom_widget = SelectDateTimeWidget
    form_fields["end_date"].custom_widget = SelectDateTimeWidget
                      
    def update(self):
        """
        Called by formlib after __init__ for every page update. This is
        the method you can use to update form fields from your class
        """        
        self.status = self.request.get('portal_status_message','')        
        form.AddForm.update( self )
 
    def finishConstruction( self, ob ):
        """
        adapt the custom fields to the object
        """
        self.adapters = { IGroupSitting : ob }
        
    def validate(self, action, data):    
        """
        validation that require context must be called here,
        invariants may be defined in the descriptor
        """                                       
        return (form.getWidgetsData(self.widgets, self.prefix, data) +
                 form.checkInvariants(self.form_fields, data) +
                 CheckSittingDatesInsideParentDates( self.context, data))      

Display Invariant or custom validation errors in the context of the field(s) that caused it

The exceptions raised by invariants/custom validations can be displayed at the fields involved. If you test for start/end date the invariant error or validation error should not be displayed at the top of the form but at the start/end date fields (like a widget validation error)

####
# Display invariant errors /  custom validation errors in the context of the field
# that raised the error.

def set_widget_errors(widgets, errors):
    for widget in widgets:
        name = widget.context.getName()
        for error in errors:
            if isinstance(error, interface.Invalid) and name in error.args[1:]:
                if widget._error is None:
                    widget._error = error

Then make the form update method do:

    def update(self):
        """
        Called by formlib after __init__ for every page update. This is
        the method you can use to update form fields from your class
        """        
        self.status = self.request.get('portal_status_message','')        
        form.AddForm.update( self )
        set_widget_errors(self.widgets, self.errors)    

Then when you define your invariant, pass the field names it refers, such as:

     @invariant
     def checkSomething(data):
         raise Invalid("Some error", 'field1', 'field2') 

Changing the sort order and attributes of the fields

A simple way to define the order in which the fields are displayed is to define their order by explicitly selecting them for display in the form. You may also change attribute of the fields here.

    form_fields = form.Fields( IMemberOfParliamentAdd ).select( "user_id",    
                    "elected_nominated", "start_date", "end_date", "leave_reason")
    form_fields["start_date"].custom_widget = SelectDateWidget
    form_fields["start_date"].field.description = _(u"Begin of the parliamentary mandate")
    form_fields["start_date"].field.title = _(u"Mandate start")
    form_fields["start_date"].field.default = datetime.date.today()

Sign in to add a comment
Powered by Google Project Hosting