Document Actions
2.3. Framework Internals
Introduction
In the Bungeni zope3 system a lot of the boiler plate code is generated at runtime from the python representation of the data model, which can be interacted with outside of Zope.
The data model is defined in bungeni.main/trunk/bungeni/models/schema.py The domain model is defined in bungeni.main/trunk/bungeni/models/domain.py The domain model is mapped to the database schema by the orm model in bungeni.main/trunk/bungeni/models/orm.py
This provides a functional API which can be interacted from a python interpreter. An example of this is provided in the doc test.
bungeni.main/trunk/bungeni/core/readme.txt
The doctests can be run via
./bin/test -s bungeni.core
Auto generation using catalyst
The user interface and the portal builds on top of this core data model. Runtime auto-generation facilities (alchemist.catalyst) are used to create much of the system. To achieve this auto generation the core data model is annotated with additional metadata (e.g. what sort of widgets to use in forms what kind of fields in a listing, which permissions to use on a field, i18n labels , form help). These metadata components called 'model descriptors' are similar to archetype schemas or django models. The model descriptors are defined in bungeni.main/trunk/bungeni/ui/descriptor.py
Additonal behaviors like versioning, relation ui, workflow security, audit are also driven by the same model descriptors.
e.g. QuestionDescriptor :
class QuestionDescriptor(ParliamentaryItemDescriptor):
display_name = _(u"Question")
container_name = _(u"Questions")
custom_validators = ()
fields = deepcopy(ParliamentaryItemDescriptor.fields)
fields.extend([
dict(name="question_id", omit=True),
dict(name="question_number",
property=schema.Int(title=_(u"Question Number"), required=False),
listing=True, add=False, edit=True),
dict(name="short_name", omit=True),
dict(name="supplement_parent_id",
label=_(u"Initial/supplementary question"),
view_widget=SupplementaryQuestionDisplay,
add=False, edit=False, view=False),
dict(name="ministry_id",
property = schema.Choice(title=_(u"Ministry"),
source=vocabulary.MinistrySource("ministry_id"),
required=True),
listing_column=ministry_column("ministry_id" , _(u'Ministry')),
listing=True,
),
dict(name="approval_date",
property=schema.Date(title=_(u"Date approved"), required=False),
edit_widget=DateWidget,
add_widget=DateWidget,
listing=False,
add=False),
dict(name="ministry_submit_date",
property=schema.Date(title=_(u"Submitted to ministry"), required=False),
edit_widget=DateWidget,
add_widget=DateWidget,
listing=False,
add=False),
..............
..............
])
public_wfstates = get_states("question", tagged=["public"])
class QuestionVersionDescriptor(VersionDescriptor):
display_name = _(u"Question version")
container_name = _(u"Versions")
fields = deepcopy(VersionDescriptor.fields)
There are many examples of descriptors in the descriptor model -- it is essentially a sub-class with a list of dictionaries. Based on the schema for the domain object in question there is an appropriate field descriptor. The order of the fields in the descriptor defines the order of appearance on the form. If no field is specified explicitly for listing model, all fields are used.
To know what can be set on a descriptor -- refer to ore.alchemist.model.Field which provides a full set of things that can be set on a descriptor.
Here is an extract of the Field class from ore.alchemist.model :
class Field( object ):
interface.implements( IModelDescriptorField )
name = "" # field name
label = "" # title for field
description = "" # description for field
fieldset = "default"
modes = "edit|view|add" # see _valid modes for allows values, also can be done as bool keyword args
omit = False
required = False # required flag can only be used if the field is not required by the database.
property = None
view_permission = "zope.Public"
edit_permission = "zope.ManageContent"
view_widget = None # zope.app.form.interaces.IDisplayWidget
edit_widget = None # zope.app.form.interfaces.IInputWidget
listing_column = None # zc.table.interfaces.IColumn object
add_widget = None # zope.app.form.interfaces.IInputWidget object
search_widget = None # zope.app.form.interfaces.IInputWidget object
differ = None # z3c.schemadiff.interfaces.IFieldDiff
# for relations, we want to enable grouping them together based on
# model, this attribute specifies a group. the relation name will be
# used on a vocabulary. perhaps an example is cleaner, so say we
# have a movie object with separate relations to directors and actors
# if we specify both as group 'People', we inform the view machinery
# to create a single relation viewlet, that displays and edits
# via a single provider with a vocabulary for the relation.
group = None
_valid_modes = ('edit', 'view', 'read', 'add', 'listing', 'search')
The file catalyst.zcml specifies the declaration which explicitly applies the generation components to the domain model using the descriptor definition. An optional debugging attribute is provided here -- setting echo="True" on the db:catalyst declaration will log to standard output all catalyst.zcml processing.
e.g. catalyst.zcml entry for Question :
<db:catalyst
descriptor=".descriptor.QuestionDescriptor"
interface_module="bungeni.models.interfaces"
ui_module="bungeni.ui.content"
echo="False"
/>
Putting it all together
At this point you would have the forms, container, interface views of the content type from the generation but isn't yet wired into the site layout/hierarchy.
To do that look at bungeni.main/trunk/bungeni/core/app.py the app object is basically the root of the site, this includes an appsetup adapter which setups the application structure when the app server is started. you can adjust this to add a container for a new content type.
Parliamentary content types get auditing, indexing, and versioning behavior and ui applied, via marker interfaces on the domain class, which triggers event subscribers, views, and adapters to integrate those features with the content classes.
Permission Setting
Permission checkers are constructed from class zcml directives, which allow for permissions definition for read / edit access for a set of fields or interfaces. This is done by the catalyst process. The setup in the descriptor on a field level basis, also allows setting view_permission and edit_permission.
the view / edit attributes for the ore.alchemist.model.Field class :
class Field( object ):
interface.implements( IModelDescriptorField )
......
......
view_permission = "zope.Public"
edit_permission = "zope.ManageContent"
......
Field level permissions and view level filtering of fields is done via alchemist.catalyst.domain (which sets up the checkers according to the model descriptor settings -- see the ApplySecurity API ) and alchemist.ui.core for filtering fields based on permissions.
DEVELOPER NOTE - presently field level view and edit permissions are not being used -- it isnt clear why. It is not clear if the implementation actually works -- to also note that the default permission (zope.Public and zope.ManageContent) set on the fields appears to be significant as removing it appears to break the forms. There is one context where it is being used -- a permission called bungeni.edit.historical is set on certains field in the bungeni model descriptor but never referred to either in the code -- and never appears in the zope_role_permission_map. This permission implementation of bungeni.edit.historical needs to be removed -- and impact tested.
In general we try to minimize the number of distinct permissions we have whenever possible, as we need to roundtrip to the db for each distinct permission checked ( a permission decision is cached on the proxy for the lifetime of the request).



