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 = errorThen 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()