--- a/web/form.py Tue May 05 17:18:49 2009 +0200
+++ b/web/form.py Thu May 14 12:48:11 2009 +0200
@@ -1,65 +1,35 @@
"""abstract form classes for CubicWeb web client
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
-from simplejson import dumps
+from warnings import warn
-from logilab.mtconverter import html_escape
+from logilab.common.compat import any
+from logilab.common.decorators import iclassmethod
-from cubicweb import typed_eid
-from cubicweb.common.selectors import match_form_params
-from cubicweb.common.registerers import accepts_registerer
-from cubicweb.common.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView
-from cubicweb.web import stdmsgs
+from cubicweb.appobject import AppRsetObject
+from cubicweb.selectors import yes, non_final_entity, match_kwargs, one_line_rset
+from cubicweb.view import NOINDEX, NOFOLLOW
+from cubicweb.common import tags
+from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param, stdmsgs
from cubicweb.web.httpcache import NoHTTPCacheManager
-from cubicweb.web.controller import redirect_params
-
+from cubicweb.web.controller import NAV_FORM_PARAMETERS
+from cubicweb.web.formfields import (Field, StringField, RelationField,
+ HiddenInitialValueField)
+from cubicweb.web import formrenderers
+from cubicweb.web import formwidgets as fwdgs
-def relation_id(eid, rtype, target, reid):
- if target == 'subject':
- return u'%s:%s:%s' % (eid, rtype, reid)
- return u'%s:%s:%s' % (reid, rtype, eid)
-
-
-class FormMixIn(object):
- """abstract form mix-in"""
+class FormViewMixIn(object):
+ """abstract form view mix-in"""
category = 'form'
controller = 'edit'
- domid = 'entityForm'
-
http_cache_manager = NoHTTPCacheManager
add_to_breadcrumbs = False
- skip_relations = set()
-
- def __init__(self, req, rset):
- super(FormMixIn, self).__init__(req, rset)
- self.maxrelitems = self.req.property_value('navigation.related-limit')
- self.maxcomboitems = self.req.property_value('navigation.combobox-limit')
- self.force_display = not not req.form.get('__force_display')
- # get validation session data which may have been previously set.
- # deleting validation errors here breaks form reloading (errors are
- # no more available), they have to be deleted by application's publish
- # method on successful commit
- formurl = req.url()
- forminfo = req.get_session_data(formurl)
- if forminfo:
- req.data['formvalues'] = forminfo['values']
- req.data['formerrors'] = errex = forminfo['errors']
- req.data['displayederrors'] = set()
- # if some validation error occured on entity creation, we have to
- # get the original variable name from its attributed eid
- foreid = errex.entity
- for var, eid in forminfo['eidmap'].items():
- if foreid == eid:
- errex.eid = var
- break
- else:
- errex.eid = foreid
-
+
def html_headers(self):
"""return a list of html headers (eg something to be inserted between
<head> and </head> of the returned page
@@ -67,18 +37,109 @@
by default forms are neither indexed nor followed
"""
return [NOINDEX, NOFOLLOW]
-
+
def linkable(self):
"""override since forms are usually linked by an action,
so we don't want them to be listed by appli.possible_views
"""
return False
- @property
- def limit(self):
- if self.force_display:
- return None
- return self.maxrelitems + 1
+
+# XXX should disappear
+class FormMixIn(object):
+ """abstract form mix-in
+ XXX: you should inherit from this FIRST (obscure pb with super call)
+ """
+
+ def session_key(self):
+ """return the key that may be used to store / retreive data about a
+ previous post which failed because of a validation error
+ """
+ return '%s#%s' % (self.req.url(), self.domid)
+
+ def __init__(self, req, rset, **kwargs):
+ super(FormMixIn, self).__init__(req, rset, **kwargs)
+ self.restore_previous_post(self.session_key())
+
+ def restore_previous_post(self, sessionkey):
+ # get validation session data which may have been previously set.
+ # deleting validation errors here breaks form reloading (errors are
+ # no more available), they have to be deleted by application's publish
+ # method on successful commit
+ forminfo = self.req.get_session_data(sessionkey, pop=True)
+ if forminfo:
+ # XXX remove req.data assigment once cw.web.widget is killed
+ self.req.data['formvalues'] = self.form_previous_values = forminfo['values']
+ self.req.data['formerrors'] = self.form_valerror = forminfo['errors']
+ self.req.data['displayederrors'] = self.form_displayed_errors = set()
+ # if some validation error occured on entity creation, we have to
+ # get the original variable name from its attributed eid
+ foreid = self.form_valerror.entity
+ for var, eid in forminfo['eidmap'].items():
+ if foreid == eid:
+ self.form_valerror.eid = var
+ break
+ else:
+ self.form_valerror.eid = foreid
+ else:
+ self.form_previous_values = {}
+ self.form_valerror = None
+
+ # XXX deprecated with new form system. Should disappear
+
+ domid = 'entityForm'
+ category = 'form'
+ controller = 'edit'
+ http_cache_manager = NoHTTPCacheManager
+ add_to_breadcrumbs = False
+
+ def html_headers(self):
+ """return a list of html headers (eg something to be inserted between
+ <head> and </head> of the returned page
+
+ by default forms are neither indexed nor followed
+ """
+ return [NOINDEX, NOFOLLOW]
+
+ def linkable(self):
+ """override since forms are usually linked by an action,
+ so we don't want them to be listed by appli.possible_views
+ """
+ return False
+
+
+ def button(self, label, klass='validateButton', tabindex=None, **kwargs):
+ if tabindex is None:
+ tabindex = self.req.next_tabindex()
+ return tags.input(value=label, klass=klass, **kwargs)
+
+ def action_button(self, label, onclick=None, __action=None, **kwargs):
+ if onclick is None:
+ onclick = "postForm('__action_%s', \'%s\', \'%s\')" % (
+ __action, label, self.domid)
+ return self.button(label, onclick=onclick, **kwargs)
+
+ def button_ok(self, label=None, type='submit', name='defaultsubmit',
+ **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_OK).capitalize()
+ return self.button(label, name=name, type=type, **kwargs)
+
+ def button_apply(self, label=None, type='button', **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize()
+ return self.action_button(label, __action='apply', type=type, **kwargs)
+
+ def button_delete(self, label=None, type='button', **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize()
+ return self.action_button(label, __action='delete', type=type, **kwargs)
+
+ def button_cancel(self, label=None, type='button', **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
+ return self.action_button(label, __action='cancel', type=type, **kwargs)
+
+ def button_reset(self, label=None, type='reset', name='__action_cancel',
+ **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
+ return self.button(label, type=type, **kwargs)
def need_multipart(self, entity, categories=('primary', 'secondary')):
"""return a boolean indicating if form's enctype should be multipart
@@ -102,16 +163,16 @@
This method should be called once inlined field errors has been consumed
"""
- errex = self.req.data.get('formerrors')
+ errex = self.req.data.get('formerrors') or self.form_valerror
# get extra errors
if errex is not None:
errormsg = self.req._('please correct the following errors:')
- displayed = self.req.data['displayederrors']
+ displayed = self.req.data.get('displayederrors') or self.form_displayed_errors
errors = sorted((field, err) for field, err in errex.errors.items()
if not field in displayed)
if errors:
if len(errors) > 1:
- templstr = '<li>%s</li>\n'
+ templstr = '<li>%s</li>\n'
else:
templstr = ' %s\n'
for field, err in errors:
@@ -123,131 +184,490 @@
errormsg = '<ul>%s</ul>' % errormsg
return u'<div class="errorMessage">%s</div>' % errormsg
return u''
-
- def restore_pending_inserts(self, entity, cell=False):
- """used to restore edition page as it was before clicking on
- 'search for <some entity type>'
-
- """
- eid = entity.eid
- cell = cell and "div_insert_" or "tr"
- pending_inserts = set(self.req.get_pending_inserts(eid))
- for pendingid in pending_inserts:
- eidfrom, rtype, eidto = pendingid.split(':')
- if typed_eid(eidfrom) == entity.eid: # subject
- label = display_name(self.req, rtype, 'subject')
- reid = eidto
+
+
+###############################################################################
+
+class metafieldsform(type):
+ """metaclass for FieldsForm to retrieve fields defined as class attributes
+ and put them into a single ordered list: '_fields_'.
+ """
+ def __new__(mcs, name, bases, classdict):
+ allfields = []
+ for base in bases:
+ if hasattr(base, '_fields_'):
+ allfields += base._fields_
+ clsfields = (item for item in classdict.items()
+ if isinstance(item[1], Field))
+ for fieldname, field in sorted(clsfields, key=lambda x: x[1].creation_rank):
+ if not field.name:
+ field.set_name(fieldname)
+ allfields.append(field)
+ classdict['_fields_'] = allfields
+ return super(metafieldsform, mcs).__new__(mcs, name, bases, classdict)
+
+
+class FieldNotFound(Exception):
+ """raised by field_by_name when a field with the given name has not been
+ found
+ """
+
+class FieldsForm(FormMixIn, AppRsetObject):
+ __metaclass__ = metafieldsform
+ __registry__ = 'forms'
+ __select__ = yes()
+
+ renderer_cls = formrenderers.FormRenderer
+ is_subform = False
+
+ # attributes overrideable through __init__
+ internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
+ needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',)
+ needs_css = ('cubicweb.form.css',)
+ domid = 'form'
+ title = None
+ action = None
+ onsubmit = "return freezeFormButtons('%(domid)s');"
+ cssclass = None
+ cssstyle = None
+ cwtarget = None
+ redirect_path = None
+ set_error_url = True
+ copy_nav_params = False
+ form_buttons = None # form buttons (button widgets instances)
+
+ def __init__(self, req, rset=None, row=None, col=None, submitmsg=None,
+ **kwargs):
+ super(FieldsForm, self).__init__(req, rset, row=row, col=col)
+ self.fields = list(self.__class__._fields_)
+ for key, val in kwargs.items():
+ if key in NAV_FORM_PARAMETERS:
+ self.form_add_hidden(key, val)
else:
- label = display_name(self.req, rtype, 'object')
- reid = eidfrom
- jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \
- % (pendingid, cell, eid)
- rset = self.req.eid_rset(reid)
- eview = self.view('text', rset, row=0)
- # XXX find a clean way to handle baskets
- if rset.description[0][0] == 'Basket':
- eview = '%s (%s)' % (eview, display_name(self.req, 'Basket'))
- yield rtype, pendingid, jscall, label, reid, eview
-
-
- def force_display_link(self):
- return (u'<span class="invisible">'
- u'[<a href="javascript: window.location.href+=\'&__force_display=1\'">%s</a>]'
- u'</span>' % self.req._('view all'))
-
- def relations_table(self, entity):
- """yiels 3-tuples (rtype, target, related_list)
- where <related_list> itself a list of :
- - node_id (will be the entity element's DOM id)
- - appropriate javascript's togglePendingDelete() function call
- - status 'pendingdelete' or ''
- - oneline view of related entity
+ assert hasattr(self.__class__, key) and not key[0] == '_', key
+ setattr(self, key, val)
+ if self.set_error_url:
+ self.form_add_hidden('__errorurl', self.session_key())
+ if self.copy_nav_params:
+ for param in NAV_FORM_PARAMETERS:
+ if not param in kwargs:
+ value = req.form.get(param)
+ if value:
+ self.form_add_hidden(param, value)
+ if submitmsg is not None:
+ self.form_add_hidden('__message', submitmsg)
+ self.context = None
+ if 'domid' in kwargs:# session key changed
+ self.restore_previous_post(self.session_key())
+
+ @iclassmethod
+ def field_by_name(cls_or_self, name, role='subject'):
+ """return field with the given name and role"""
+ if isinstance(cls_or_self, type):
+ fields = cls_or_self._fields_
+ else:
+ fields = cls_or_self.fields
+ for field in fields:
+ if field.name == name and field.role == role:
+ return field
+ raise FieldNotFound(name)
+
+ @iclassmethod
+ def remove_field(cls_or_self, field):
+ """remove a field from form class or instance"""
+ if isinstance(cls_or_self, type):
+ fields = cls_or_self._fields_
+ else:
+ fields = cls_or_self.fields
+ fields.remove(field)
+
+ @iclassmethod
+ def append_field(cls_or_self, field):
+ """append a field to form class or instance"""
+ if isinstance(cls_or_self, type):
+ fields = cls_or_self._fields_
+ else:
+ fields = cls_or_self.fields
+ fields.append(field)
+
+ @property
+ def form_needs_multipart(self):
+ """true if the form needs enctype=multipart/form-data"""
+ return any(field.needs_multipart for field in self.fields)
+
+ def form_add_hidden(self, name, value=None, **kwargs):
+ """add an hidden field to the form"""
+ field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value,
+ **kwargs)
+ if 'id' in kwargs:
+ # by default, hidden input don't set id attribute. If one is
+ # explicitly specified, ensure it will be set
+ field.widget.setdomid = True
+ self.append_field(field)
+ return field
+
+ def add_media(self):
+ """adds media (CSS & JS) required by this widget"""
+ if self.needs_js:
+ self.req.add_js(self.needs_js)
+ if self.needs_css:
+ self.req.add_css(self.needs_css)
+
+ def form_render(self, **values):
+ """render this form, using the renderer given in args or the default
+ FormRenderer()
+ """
+ renderer = values.pop('renderer', self.renderer_cls())
+ return renderer.render(self, values)
+
+ def form_build_context(self, rendervalues=None):
+ """build form context values (the .context attribute which is a
+ dictionary with field instance as key associated to a dictionary
+ containing field 'name' (qualified), 'id', 'value' (for display, always
+ a string).
+
+ rendervalues is an optional dictionary containing extra kwargs given to
+ form_render()
"""
- eid = entity.eid
- pending_deletes = self.req.get_pending_deletes(eid)
- # XXX (adim) : quick fix to get Folder relations
- for label, rschema, target in entity.srelations_by_category(('generic', 'metadata'), 'add'):
- if rschema in self.skip_relations:
- continue
- relatedrset = entity.related(rschema, target, limit=self.limit)
- toggable_rel_link = self.toggable_relation_link_func(rschema)
- related = []
- for row in xrange(relatedrset.rowcount):
- nodeid = relation_id(eid, rschema, target, relatedrset[row][0])
- if nodeid in pending_deletes:
- status = u'pendingDelete'
- label = '+'
- else:
- status = u''
- label = 'x'
- dellink = toggable_rel_link(eid, nodeid, label)
- eview = self.view('oneline', relatedrset, row=row)
- related.append((nodeid, dellink, status, eview))
- yield (rschema, target, related)
-
- def toggable_relation_link_func(self, rschema):
- if not rschema.has_perm(self.req, 'delete'):
- return lambda x, y, z: u''
- return toggable_relation_link
+ self.context = context = {}
+ # ensure rendervalues is a dict
+ if rendervalues is None:
+ rendervalues = {}
+ for field in self.fields:
+ for field in field.actual_fields(self):
+ field.form_init(self)
+ value = self.form_field_display_value(field, rendervalues)
+ context[field] = {'value': value,
+ 'name': self.form_field_name(field),
+ 'id': self.form_field_id(field),
+ }
+
+ def form_field_display_value(self, field, rendervalues, load_bytes=False):
+ """return field's *string* value to use for display
+
+ looks in
+ 1. previously submitted form values if any (eg on validation error)
+ 2. req.form
+ 3. extra kw args given to render_form
+ 4. field's typed value
+
+ values found in 1. and 2. are expected te be already some 'display'
+ value while those found in 3. and 4. are expected to be correctly typed.
+ """
+ value = self._req_display_value(field)
+ if value is None:
+ if field.name in rendervalues:
+ value = rendervalues[field.name]
+ else:
+ value = self.form_field_value(field, load_bytes)
+ if callable(value):
+ value = value(self)
+ if value != INTERNAL_FIELD_VALUE:
+ value = field.format_value(self.req, value)
+ return value
+
+ def _req_display_value(self, field):
+ qname = self.form_field_name(field)
+ if qname in self.form_previous_values:
+ return self.form_previous_values[qname]
+ if qname in self.req.form:
+ return self.req.form[qname]
+ return None
+
+ def form_field_value(self, field, load_bytes=False):
+ """return field's *typed* value"""
+ value = field.initial
+ if callable(value):
+ value = value(self)
+ return value
+
+ def form_field_error(self, field):
+ """return validation error for widget's field, if any"""
+ if self._field_has_error(field):
+ self.form_displayed_errors.add(field.name)
+ return u'<span class="error">%s</span>' % self.form_valerror.errors[field.name]
+ return u''
+
+ def form_field_format(self, field):
+ """return MIME type used for the given (text or bytes) field"""
+ return self.req.property_value('ui.default-text-format')
+
+ def form_field_encoding(self, field):
+ """return encoding used for the given (text) field"""
+ return self.req.encoding
+
+ def form_field_name(self, field):
+ """return qualified name for the given field"""
+ return field.name
+
+ def form_field_id(self, field):
+ """return dom id for the given field"""
+ return field.id
+
+ def form_field_vocabulary(self, field, limit=None):
+ """return vocabulary for the given field. Should be overriden in
+ specific forms using fields which requires some vocabulary
+ """
+ raise NotImplementedError
+
+ def _field_has_error(self, field):
+ """return true if the field has some error in given validation exception
+ """
+ return self.form_valerror and field.name in self.form_valerror.errors
- def redirect_url(self, entity=None):
- """return a url to use as next direction if there are some information
- specified in current form params, else return the result the reset_url
- method which should be defined in concrete classes
+class EntityFieldsForm(FieldsForm):
+ __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity()))
+
+ internal_fields = FieldsForm.internal_fields + ('__type', 'eid', '__maineid')
+ domid = 'entityForm'
+
+ def __init__(self, *args, **kwargs):
+ self.edited_entity = kwargs.pop('entity', None)
+ msg = kwargs.pop('submitmsg', None)
+ super(EntityFieldsForm, self).__init__(*args, **kwargs)
+ if self.edited_entity is None:
+ self.edited_entity = self.complete_entity(self.row or 0, self.col or 0)
+ self.form_add_hidden('__type', eidparam=True)
+ self.form_add_hidden('eid')
+ if msg is not None:
+ # If we need to directly attach the new object to another one
+ for linkto in self.req.list_form_param('__linkto'):
+ self.form_add_hidden('__linkto', linkto)
+ msg = '%s %s' % (msg, self.req._('and linked'))
+ self.form_add_hidden('__message', msg)
+ # in case of direct instanciation
+ self.schema = self.edited_entity.schema
+ self.vreg = self.edited_entity.vreg
+
+ def _field_has_error(self, field):
+ """return true if the field has some error in given validation exception
+ """
+ return super(EntityFieldsForm, self)._field_has_error(field) \
+ and self.form_valerror.eid == self.edited_entity.eid
+
+ def _relation_vocabulary(self, rtype, targettype, role,
+ limit=None, done=None):
+ """return unrelated entities for a given relation and target entity type
+ for use in vocabulary
"""
- rparams = redirect_params(self.req.form)
- if rparams:
- return self.build_url('view', **rparams)
- return self.reset_url(entity)
+ if done is None:
+ done = set()
+ rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
+ res = []
+ for entity in rset.entities():
+ if entity.eid in done:
+ continue
+ done.add(entity.eid)
+ res.append((entity.view('combobox'), entity.eid))
+ return res
+
+ def _req_display_value(self, field):
+ value = super(EntityFieldsForm, self)._req_display_value(field)
+ if value is None:
+ value = self.edited_entity.linked_to(field.name, field.role) or None
+ return value
- def reset_url(self, entity):
- raise NotImplementedError('implement me in concrete classes')
+ def _form_field_default_value(self, field, load_bytes):
+ defaultattr = 'default_%s' % field.name
+ if hasattr(self.edited_entity, defaultattr):
+ # XXX bw compat, default_<field name> on the entity
+ warn('found %s on %s, should be set on a specific form'
+ % (defaultattr, self.edited_entity.id), DeprecationWarning)
+ value = getattr(self.edited_entity, defaultattr)
+ if callable(value):
+ value = value()
+ else:
+ value = super(EntityFieldsForm, self).form_field_value(field,
+ load_bytes)
+ return value
+
+ def form_build_context(self, values=None):
+ """overriden to add edit[s|o] hidden fields and to ensure schema fields
+ have eidparam set to True
- BUTTON_STR = u'<input class="validateButton" type="submit" name="%s" value="%s" tabindex="%s"/>'
- ACTION_SUBMIT_STR = u'<input class="validateButton" type="button" onclick="postForm(\'%s\', \'%s\', \'%s\')" value="%s" tabindex="%s"/>'
+ edit[s|o] hidden fields are used to indicate the value for the
+ associated field before the (potential) modification made when
+ submitting the form.
+ """
+ eschema = self.edited_entity.e_schema
+ for field in self.fields[:]:
+ for field in field.actual_fields(self):
+ fieldname = field.name
+ if fieldname != 'eid' and (
+ (eschema.has_subject_relation(fieldname) or
+ eschema.has_object_relation(fieldname))):
+ field.eidparam = True
+ self.fields.append(HiddenInitialValueField(field))
+ return super(EntityFieldsForm, self).form_build_context(values)
+
+ def form_field_value(self, field, load_bytes=False):
+ """return field's *typed* value
- def button_ok(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_OK).capitalize()
- return self.BUTTON_STR % ('defaultsubmit', label, tabindex or 2)
-
- def button_apply(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize()
- return self.ACTION_SUBMIT_STR % ('__action_apply', label, self.domid, label, tabindex or 3)
+ overriden to deal with
+ * special eid / __type / edits- / edito- fields
+ * lookup for values on edited entities
+ """
+ attr = field.name
+ entity = self.edited_entity
+ if attr == 'eid':
+ return entity.eid
+ if not field.eidparam:
+ return super(EntityFieldsForm, self).form_field_value(field, load_bytes)
+ if attr.startswith('edits-') or attr.startswith('edito-'):
+ # edit[s|o]- fieds must have the actual value stored on the entity
+ assert hasattr(field, 'visible_field')
+ vfield = field.visible_field
+ assert vfield.eidparam
+ if entity.has_eid():
+ return self.form_field_value(vfield)
+ return INTERNAL_FIELD_VALUE
+ if attr == '__type':
+ return entity.id
+ if self.schema.rschema(attr).is_final():
+ attrtype = entity.e_schema.destination(attr)
+ if attrtype == 'Password':
+ return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
+ if attrtype == 'Bytes':
+ if entity.has_eid():
+ if load_bytes:
+ return getattr(entity, attr)
+ # XXX value should reflect if some file is already attached
+ return True
+ return False
+ if entity.has_eid() or attr in entity:
+ value = getattr(entity, attr)
+ else:
+ value = self._form_field_default_value(field, load_bytes)
+ return value
+ # non final relation field
+ if entity.has_eid() or entity.relation_cached(attr, field.role):
+ value = [r[0] for r in entity.related(attr, field.role)]
+ else:
+ value = self._form_field_default_value(field, load_bytes)
+ return value
+
+ def form_field_format(self, field):
+ """return MIME type used for the given (text or bytes) field"""
+ entity = self.edited_entity
+ if field.eidparam and entity.e_schema.has_metadata(field.name, 'format') and (
+ entity.has_eid() or '%s_format' % field.name in entity):
+ return self.edited_entity.attr_metadata(field.name, 'format')
+ return self.req.property_value('ui.default-text-format')
+
+ def form_field_encoding(self, field):
+ """return encoding used for the given (text) field"""
+ entity = self.edited_entity
+ if field.eidparam and entity.e_schema.has_metadata(field.name, 'encoding') and (
+ entity.has_eid() or '%s_encoding' % field.name in entity):
+ return self.edited_entity.attr_metadata(field.name, 'encoding')
+ return super(EntityFieldsForm, self).form_field_encoding(field)
+
+ def form_field_name(self, field):
+ """return qualified name for the given field"""
+ if field.eidparam:
+ return eid_param(field.name, self.edited_entity.eid)
+ return field.name
+
+ def form_field_id(self, field):
+ """return dom id for the given field"""
+ if field.eidparam:
+ return eid_param(field.id, self.edited_entity.eid)
+ return field.id
- def button_delete(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize()
- return self.ACTION_SUBMIT_STR % ('__action_delete', label, self.domid, label, tabindex or 3)
-
- def button_cancel(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
- return self.ACTION_SUBMIT_STR % ('__action_cancel', label, self.domid, label, tabindex or 4)
-
- def button_reset(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
- return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % (
- label, tabindex or 4)
-
-def toggable_relation_link(eid, nodeid, label='x'):
- js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid)))
- return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label)
+ def form_field_vocabulary(self, field, limit=None):
+ """return vocabulary for the given field"""
+ role, rtype = field.role, field.name
+ method = '%s_%s_vocabulary' % (role, rtype)
+ try:
+ vocabfunc = getattr(self, method)
+ except AttributeError:
+ try:
+ # XXX bw compat, <role>_<rtype>_vocabulary on the entity
+ vocabfunc = getattr(self.edited_entity, method)
+ except AttributeError:
+ vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
+ else:
+ warn('found %s on %s, should be set on a specific form'
+ % (method, self.edited_entity.id), DeprecationWarning)
+ # NOTE: it is the responsibility of `vocabfunc` to sort the result
+ # (direclty through RQL or via a python sort). This is also
+ # important because `vocabfunc` might return a list with
+ # couples (label, None) which act as separators. In these
+ # cases, it doesn't make sense to sort results afterwards.
+ return vocabfunc(rtype, limit)
+
+ def subject_relation_vocabulary(self, rtype, limit=None):
+ """defaut vocabulary method for the given relation, looking for
+ relation's object entities (i.e. self is the subject)
+ """
+ entity = self.edited_entity
+ if isinstance(rtype, basestring):
+ rtype = entity.schema.rschema(rtype)
+ done = None
+ assert not rtype.is_final(), rtype
+ if entity.has_eid():
+ done = set(e.eid for e in getattr(entity, str(rtype)))
+ result = []
+ rsetsize = None
+ for objtype in rtype.objects(entity.e_schema):
+ if limit is not None:
+ rsetsize = limit - len(result)
+ result += self._relation_vocabulary(rtype, objtype, 'subject',
+ rsetsize, done)
+ if limit is not None and len(result) >= limit:
+ break
+ return result
+
+ def object_relation_vocabulary(self, rtype, limit=None):
+ """defaut vocabulary method for the given relation, looking for
+ relation's subject entities (i.e. self is the object)
+ """
+ entity = self.edited_entity
+ if isinstance(rtype, basestring):
+ rtype = entity.schema.rschema(rtype)
+ done = None
+ if entity.has_eid():
+ done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype))
+ result = []
+ rsetsize = None
+ for subjtype in rtype.subjects(entity.e_schema):
+ if limit is not None:
+ rsetsize = limit - len(result)
+ result += self._relation_vocabulary(rtype, subjtype, 'object',
+ rsetsize, done)
+ if limit is not None and len(result) >= limit:
+ break
+ return result
+
+ def subject_in_state_vocabulary(self, rtype, limit=None):
+ """vocabulary method for the in_state relation, looking for relation's
+ object entities (i.e. self is the subject) according to initial_state,
+ state_of and next_state relation
+ """
+ entity = self.edited_entity
+ if not entity.has_eid() or not entity.in_state:
+ # get the initial state
+ rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S'
+ rset = self.req.execute(rql, {'etype': str(entity.e_schema)})
+ if rset:
+ return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])]
+ return []
+ results = []
+ for tr in entity.in_state[0].transitions(entity):
+ state = tr.destination_state[0]
+ results.append((state.view('combobox'), state.eid))
+ return sorted(results)
-class Form(FormMixIn, View):
- """base class for forms. Apply by default according to request form
- parameters specified using the `form_params` class attribute which
- should list necessary parameters in the form to be accepted.
- """
- __registerer__ = accepts_registerer
- __select__ = classmethod(match_form_params)
+class CompositeForm(FieldsForm):
+ """form composed for sub-forms"""
- form_params = ()
+ def __init__(self, *args, **kwargs):
+ super(CompositeForm, self).__init__(*args, **kwargs)
+ self.forms = []
-class EntityForm(FormMixIn, EntityView):
- """base class for forms applying on an entity (i.e. uniform result set)
- """
-
-class AnyRsetForm(FormMixIn, AnyRsetView):
- """base class for forms applying on any empty result sets
- """
-
+ def form_add_subform(self, subform):
+ """mark given form as a subform and append it"""
+ subform.is_subform = True
+ self.forms.append(subform)