# HG changeset patch # User sylvain.thenault@logilab.fr # Date 1238090341 -3600 # Node ID 402e8a8b1d6afc11645d3d8771f9b7b7874a5520 # Parent 5476815927657e91d05018602060b69a5111c0eb more form works diff -r 547681592765 -r 402e8a8b1d6a web/form.py --- a/web/form.py Thu Mar 26 18:58:14 2009 +0100 +++ b/web/form.py Thu Mar 26 18:59:01 2009 +0100 @@ -8,36 +8,24 @@ from warnings import warn -from simplejson import dumps - from logilab.common.compat import any from logilab.common.decorators import iclassmethod -from logilab.mtconverter import html_escape -from cubicweb import typed_eid -from cubicweb.appobject import AppObject -from cubicweb.selectors import yes, non_final_entity +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 NAV_FORM_PARAMETERS, redirect_params +from cubicweb.web.controller import NAV_FORM_PARAMETERS from cubicweb.web.formfields import (Field, StringField, RelationField, HiddenInitialValueField) -from cubicweb.web.formwidgets import HiddenInput +from cubicweb.web.formrenderers import FormRenderer +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) - -def toggable_relation_link(eid, nodeid, label='x'): - js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid))) - return u'[%s]' % (js, nodeid, label) - - +# XXX should disappear class FormMixIn(object): """abstract form mix-in XXX: you should inherit from this FIRST (obscure pb with super call)""" @@ -49,8 +37,8 @@ add_to_breadcrumbs = False skip_relations = set() - def __init__(self, req, rset): - super(FormMixIn, self).__init__(req, rset) + def __init__(self, req, rset, **kwargs): + super(FormMixIn, self).__init__(req, rset, **kwargs) 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') @@ -110,7 +98,42 @@ if inlined_entity.get_widget(irschema, x).need_multipart: return True 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) + + # XXX deprecated with new form system def error_message(self): """return formatted error message @@ -137,113 +160,6 @@ errormsg = '' % errormsg return u'
%s
' % errormsg return u'' - - def restore_pending_inserts(self, entity, cell=False): - """used to restore edition page as it was before clicking on - 'search for ' - - """ - 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 - 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'' % self.req._('view all')) - - def relations_table(self, entity): - """yiels 3-tuples (rtype, target, related_list) - where 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 - """ - 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 - - - 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 - """ - rparams = redirect_params(self.req.form) - if rparams: - return self.build_url('view', **rparams) - return self.reset_url(entity) - - def reset_url(self, entity): - raise NotImplementedError('implement me in concrete classes') - - BUTTON_STR = u'' - ACTION_SUBMIT_STR = u'' - - 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) - - 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'' % ( - label, tabindex or 4) ############################################################################### @@ -264,38 +180,43 @@ return super(metafieldsform, mcs).__new__(mcs, name, bases, classdict) -class FieldsForm(FormMixIn, AppObject): +class FieldsForm(FormMixIn, AppRsetObject): __metaclass__ = metafieldsform __registry__ = 'forms' __select__ = yes() + + is_subform = False + + # attributes overrideable through __init__ internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS needs_js = ('cubicweb.edition.js',) needs_css = ('cubicweb.form.css',) - - def __init__(self, req, rset=None, domid=None, title=None, action='edit', - onsubmit="return freezeFormButtons('%(domid)s');", - cssclass=None, cssstyle=None, cwtarget=None, buttons=None, - redirect_path=None, set_error_url=True, copy_nav_params=False): - self.req = req - self.rset = rset - self.config = req.vreg.config - self.domid = domid or 'form' - self.title = title - self.action = action - self.onsubmit = onsubmit - self.cssclass = cssclass - self.cssstyle = cssstyle - self.cwtarget = cwtarget - self.redirect_path = redirect_path + domid = 'form' + title = None + action = None + onsubmit = "return freezeFormButtons('%(domid)s');" + cssclass = None + cssstyle = None + cwtarget = None + buttons = None + redirect_path = None + set_error_url = True + copy_nav_params = False + + def __init__(self, req, rset=None, row=None, col=None, **kwargs): + super(FieldsForm, self).__init__(req, rset, row=row, col=col) + self.buttons = kwargs.pop('buttons', []) + for key, val in kwargs.items(): + assert hasattr(self.__class__, key) and not key[0] == '_', key + setattr(self, key, val) self.fields = list(self.__class__._fields_) - if set_error_url: + if self.set_error_url: self.form_add_hidden('__errorurl', req.url()) - if copy_nav_params: + if self.copy_nav_params: for param in NAV_FORM_PARAMETERS: - value = req.form.get(param) + value = kwargs.get(param, req.form.get(param)) if value: self.form_add_hidden(param, initial=value) - self.buttons = buttons or [] self.context = None @iclassmethod @@ -309,14 +230,24 @@ return field raise Exception('field %s not found' % name) + @iclassmethod + def remove_field(cls_or_self, field): + if isinstance(cls_or_self, type): + fields = cls_or_self._fields_ + else: + fields = cls_or_self.fields + fields.remove(field) + @property def form_needs_multipart(self): return any(field.needs_multipart for field in self.fields) def form_add_hidden(self, name, value=None, **kwargs): - self.fields.append(StringField(name=name, widget=HiddenInput, - initial=value, **kwargs)) - + field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value, + **kwargs) + self.fields.append(field) + return field + def add_media(self): """adds media (CSS & JS) required by this widget""" if self.needs_js: @@ -357,6 +288,14 @@ else: value = field.initial return value + + def form_field_error(self, field): + """return validation error for widget's field, if any""" + errex = self.req.data.get('formerrors') + if errex and field.name in errex.errors: + self.req.data['displayederrors'].add(field.name) + return u'%s' % errex.errors[field.name] + return u'' def form_field_format(self, field): return self.req.property_value('ui.default-text-format') @@ -378,19 +317,28 @@ class EntityFieldsForm(FieldsForm): - __select__ = non_final_entity() + __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity())) internal_fields = FieldsForm.internal_fields + ('__type', 'eid') + domid = 'entityForm' def __init__(self, *args, **kwargs): - kwargs.setdefault('domid', 'entityForm') - self.entity = kwargs.pop('entity', None) + 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) 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) def form_render(self, **values): - self.form_add_entity_hiddens(self.entity.e_schema) + self.form_add_entity_hiddens(self.edited_entity.e_schema) return super(EntityFieldsForm, self).form_render(**values) def form_add_entity_hiddens(self, eschema): @@ -433,21 +381,24 @@ fieldname = field.name if fieldname.startswith('edits-') or fieldname.startswith('edito-'): # edit[s|o]- fieds must have the actual value stored on the entity - if self.entity.has_eid(): - value = self._form_field_entity_value(field.visible_field, - default_initial=False) + if hasattr(field, 'visible_field'): + if self.edited_entity.has_eid(): + value = self._form_field_entity_value(field.visible_field, + default_initial=False) + else: + value = INTERNAL_FIELD_VALUE else: - value = INTERNAL_FIELD_VALUE + value = field.initial elif fieldname == '__type': - value = self.entity.id + value = self.edited_entity.id elif fieldname == 'eid': - value = self.entity.eid + value = self.edited_entity.eid elif fieldname in values: value = values[fieldname] elif fieldname in self.req.form: value = self.req.form[fieldname] else: - if self.entity.has_eid() and field.eidparam: + if self.edited_entity.has_eid() and field.eidparam: # use value found on the entity or field's initial value if it's # not an attribute of the entity (XXX may conflicts and get # undesired value) @@ -455,11 +406,11 @@ load_bytes=load_bytes) else: defaultattr = 'default_%s' % fieldname - if hasattr(self.entity, defaultattr): + if hasattr(self.edited_entity, defaultattr): # XXX bw compat, default_ on the entity warn('found %s on %s, should be set on a specific form' - % (defaultattr, self.entity.id), DeprecationWarning) - value = getattr(self.entity, defaultattr) + % (defaultattr, self.edited_entity.id), DeprecationWarning) + value = getattr(self.edited_entity, defaultattr) elif hasattr(self, defaultattr): # search for default_ on the form instance value = getattr(self, defaultattr) @@ -471,38 +422,47 @@ return value def form_field_format(self, field): - entity = self.entity + 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.entity.attribute_metadata(field.name, 'format') + return self.edited_entity.attribute_metadata(field.name, 'format') return self.req.property_value('ui.default-text-format') def form_field_encoding(self, field): - entity = self.entity + 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.entity.attribute_metadata(field.name, 'encoding') + return self.edited_entity.attribute_metadata(field.name, 'encoding') return super(EntityFieldsForm, self).form_field_encoding(field) + + def form_field_error(self, field): + """return validation error for widget's field, if any""" + errex = self.req.data.get('formerrors') + if errex and errex.eid == self.edited_entity.eid and field.name in errex.errors: + self.req.data['displayederrors'].add(field.name) + return u'%s' % errex.errors[field.name] + return u'' def _form_field_entity_value(self, field, default_initial=True, load_bytes=False): - attr = field.name + attr = field.name + entity = self.edited_entity if field.role == 'object': attr = 'reverse_' + attr - else: - attrtype = self.entity.e_schema.destination(attr) + elif entity.e_schema.subject_relation(attr).is_final(): + attrtype = entity.e_schema.destination(attr) if attrtype == 'Password': - return self.entity.has_eid() and INTERNAL_FIELD_VALUE or '' + return entity.has_eid() and INTERNAL_FIELD_VALUE or '' if attrtype == 'Bytes': - if self.entity.has_eid(): + if entity.has_eid(): if load_bytes: - return getattr(self.entity, attr) + return getattr(entity, attr) # XXX value should reflect if some file is already attached return True return False if default_initial: - value = getattr(self.entity, attr, field.initial) + value = getattr(entity, attr, field.initial) else: - value = getattr(self.entity, attr) + value = getattr(entity, attr) if isinstance(field, RelationField): # in this case, value is the list of related entities value = [ent.eid for ent in value] @@ -510,24 +470,24 @@ def form_field_name(self, field): if field.eidparam: - return eid_param(field.name, self.entity.eid) + return eid_param(field.name, self.edited_entity.eid) return field.name def form_field_id(self, field): if field.eidparam: - return eid_param(field.id, self.entity.eid) + return eid_param(field.id, self.edited_entity.eid) return field.id def form_field_vocabulary(self, field, limit=None): role, rtype = field.role, field.name try: - vocabfunc = getattr(self.entity, '%s_%s_vocabulary' % (role, rtype)) + vocabfunc = getattr(self.edited_entity, '%s_%s_vocabulary' % (role, rtype)) except AttributeError: vocabfunc = getattr(self, '%s_relation_vocabulary' % role) else: # XXX bw compat, default_ on the entity warn('found %s_%s_vocabulary on %s, should be set on a specific form' - % (role, rtype, self.entity.id), DeprecationWarning) + % (role, rtype, 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 @@ -544,7 +504,7 @@ """defaut vocabulary method for the given relation, looking for relation's object entities (i.e. self is the subject) """ - entity = self.entity + entity = self.edited_entity if isinstance(rtype, basestring): rtype = entity.schema.rschema(rtype) done = None @@ -566,7 +526,7 @@ """defaut vocabulary method for the given relation, looking for relation's subject entities (i.e. self is the object) """ - entity = self.entity + entity = self.edited_entity if isinstance(rtype, basestring): rtype = entity.schema.rschema(rtype) done = None @@ -587,7 +547,7 @@ limit=None, done=None): if done is None: done = set() - rset = self.entity.unrelated(rtype, targettype, role, limit) + rset = self.edited_entity.unrelated(rtype, targettype, role, limit) res = [] for entity in rset.entities(): if entity.eid in done: @@ -597,118 +557,13 @@ return res -class MultipleFieldsForm(FieldsForm): +class CompositeForm(FieldsForm): + """form composed for sub-forms""" + def __init__(self, *args, **kwargs): - super(MultipleFieldsForm, self).__init__(*args, **kwargs) + super(CompositeForm, self).__init__(*args, **kwargs) self.forms = [] def form_add_subform(self, subform): + subform.is_subform = True self.forms.append(subform) - - -# form renderers ############ - -class FormRenderer(object): - button_bar_class = u'formButtonBar' - - def __init__(self, display_fields=None, display_label=True, - display_help=True, button_bar_class=None): - self.display_fields = display_fields # None -> all fields - self.display_label = display_label - self.display_help = display_help - if button_bar_class is not None: - self.button_bar_class = button_bar_class - - # renderer interface ###################################################### - - def render(self, form, values): - form.add_media() - data = [] - w = data.append - w(self.open_form(form)) - w(u'
%s
' % form.req._('validating...')) - w(u'
') - w(tags.input(type='hidden', name='__form_id', value=form.domid)) - if form.redirect_path: - w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path)) - self.render_fields(w, form, values) - self.render_buttons(w, form) - w(u'
') - w(u'') - return '\n'.join(data) - - def render_label(self, form, field): - label = form.req._(field.label) - attrs = {'for': form.context[field]['id']} - if field.required: - attrs['class'] = 'required' - return tags.label(label, **attrs) - - def render_help(self, form, field): - help = [ u'
' ] - descr = field.help - if descr: - help.append('%s' % form.req._(descr)) - example = field.example_format(form.req) - if example: - help.append('(%s: %s)' - % (form.req._('sample format'), example)) - return u' '.join(help) - - # specific methods (mostly to ease overriding) ############################# - - def open_form(self, form): - if form.form_needs_multipart: - enctype = 'multipart/form-data' - else: - enctype = 'application/x-www-form-urlencoded' - tag = ('
' - - def display_field(self, form, field): - return (self.display_fields is None - or field.name in self.display_fields - or field.name in form.internal_fields) - - def render_fields(self, w, form, values): - form.form_build_context(values) - fields = form.fields[:] - for field in form.fields: - if not self.display_field(form, field): - fields.remove(field) - - if not field.is_visible(): - w(field.render(form, self)) - fields.remove(field) - if fields: - self._render_fields(fields, w, form) - for childform in getattr(form, 'forms', []): - self.render_fields(w, childform, values) - - def _render_fields(self, fields, w, form,): - w(u'') - for field in fields: - w(u'') - if self.display_label: - w(u'' % self.render_label(form, field)) - w(u'') - w(u'
%s') - w(field.render(form, self)) - if self.display_help: - w(self.render_help(form, field)) - w(u'
') - - def render_buttons(self, w, form): - w(u'\n\n' % self.button_bar_class) - for button in form.form_buttons(): - w(u'\n' % button) - w(u'
%s
') diff -r 547681592765 -r 402e8a8b1d6a web/formfields.py --- a/web/formfields.py Thu Mar 26 18:58:14 2009 +0100 +++ b/web/formfields.py Thu Mar 26 18:59:01 2009 +0100 @@ -307,7 +307,7 @@ return RelationField(widget=Select(multiple=card in '*+'), **kwargs) def vocabulary(self, form): - entity = form.entity + entity = form.edited_entity req = entity.req # first see if its specified by __linkto form parameters linkedto = entity.linked_to(self.name, self.role) @@ -334,8 +334,8 @@ field = None for cstr in constraints: if isinstance(cstr, StaticVocabularyConstraint): - return StringField(widget=Select(vocabulary=cstr.vocabulary), - **kwargs) + kwargs.setdefault('widget', Select(vocabulary=cstr.vocabulary)) + return StringField(**kwargs) if isinstance(cstr, SizeConstraint) and cstr.max is not None: if cstr.max > 257: rows_cols_from_constraint(cstr, kwargs) @@ -375,7 +375,8 @@ if fieldclass is StringField: if targetschema == 'Password': # special case for Password field: specific PasswordInput widget - return StringField(widget=PasswordInput(), **kwargs) + kwargs.setdefault('widget', PasswordInput()) + return StringField(**kwargs) if eschema.has_metadata(rschema, 'format'): # use RichTextField instead of StringField if the attribute has # a "format" metadata. But getting information from constraints diff -r 547681592765 -r 402e8a8b1d6a web/formrenderers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/formrenderers.py Thu Mar 26 18:59:01 2009 +0100 @@ -0,0 +1,399 @@ +"""form renderers, responsible to layout a form to html + +:organization: Logilab +:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" +__docformat__ = "restructuredtext en" + +from logilab.common import dictattr +from logilab.mtconverter import html_escape + +from simplejson import dumps + +from cubicweb.common import tags +from cubicweb.web import eid_param +from cubicweb.web import formwidgets as fwdgs +from cubicweb.web.widgets import checkbox + +class FormRenderer(object): + """basic renderer displaying fields in a two columns table label | value + """ + button_bar_class = u'formButtonBar' + + def __init__(self, display_fields=None, display_label=True, + display_help=True, button_bar_class=None): + self.display_fields = display_fields # None -> all fields + self.display_label = display_label + self.display_help = display_help + if button_bar_class is not None: + self.button_bar_class = button_bar_class + + # renderer interface ###################################################### + + def render(self, form, values): + form.add_media() + data = [] + w = data.append + w(self.open_form(form, values)) + w(u'
%s
' % form.req._('validating...')) + w(u'
') + w(tags.input(type=u'hidden', name=u'__form_id', + value=values.get('formvid', form.id))) + if form.redirect_path: + w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path)) + self.render_fields(w, form, values) + self.render_buttons(w, form) + w(u'
') + w(u'
') + errormsg = self.error_message(form) + if errormsg: + data.insert(0, errormsg) + return '\n'.join(data) + + def render_label(self, form, field): + label = form.req._(field.label) + attrs = {'for': form.context[field]['id']} + if field.required: + attrs['class'] = 'required' + return tags.label(label, **attrs) + + def render_help(self, form, field): + help = [ u'
' ] + descr = field.help + if descr: + help.append('%s' % form.req._(descr)) + example = field.example_format(form.req) + if example: + help.append('(%s: %s)' + % (form.req._('sample format'), example)) + return u' '.join(help) + + # specific methods (mostly to ease overriding) ############################# + + def error_message(self, form): + """return formatted error message + + This method should be called once inlined field errors has been consumed + """ + req = form.req + errex = req.data.get('formerrors') + # get extra errors + if errex is not None: + errormsg = req._('please correct the following errors:') + displayed = req.data['displayederrors'] + errors = sorted((field, err) for field, err in errex.errors.items() + if not field in displayed) + if errors: + if len(errors) > 1: + templstr = '
  • %s
  • \n' + else: + templstr = ' %s\n' + for field, err in errors: + if field is None: + errormsg += templstr % err + else: + errormsg += templstr % '%s: %s' % (req._(field), err) + if len(errors) > 1: + errormsg = '
      %s
    ' % errormsg + return u'
    %s
    ' % errormsg + return u'' + + def open_form(self, form, values): + if form.form_needs_multipart: + enctype = 'multipart/form-data' + else: + enctype = 'application/x-www-form-urlencoded' + if form.action is None: + action = form.req.build_url('edit') + else: + action = form.action + tag = ('
    ' + + def display_field(self, form, field): + return (self.display_fields is None + or field.name in self.display_fields + or field.name in form.internal_fields) + + def render_fields(self, w, form, values): + form.form_build_context(values) + fields = self._render_hidden_fields(w, form) + if fields: + self._render_fields(fields, w, form, values) + self.render_child_forms(w, form, values) + + def render_child_forms(self, w, form, values): + # render + for childform in getattr(form, 'forms', []): + self.render_fields(w, childform, values) + + def _render_hidden_fields(self, w, form): + fields = form.fields[:] + for field in form.fields: + if not self.display_field(form, field): + fields.remove(field) + elif not field.is_visible(): + w(field.render(form, self)) + fields.remove(field) + return fields + + def _render_fields(self, fields, w, form, values): + w(u'') + for field in fields: + w(u'') + if self.display_label: + w(u'' % self.render_label(form, field)) + error = form.form_field_error(field) + if error: + w(u'') + w(u'
    %s') + w(error) + else: + w(u'') + w(field.render(form, self)) + if self.display_help: + w(self.render_help(form, field)) + w(u'
    ') + + def render_buttons(self, w, form): + w(u'\n\n' % self.button_bar_class) + for button in form.form_buttons(): + w(u'\n' % button) + w(u'
    %s
    ') + + + +class EntityCompositeFormRenderer(FormRenderer): + """specific renderer for multiple entities edition form (muledit)""" + def render_fields(self, w, form, values): + if not form.is_subform: + w(u'') + super(EntityCompositeFormRenderer, self).render_fields(w, form, values) + if not form.is_subform: + w(u'
    ') + + def _render_fields(self, fields, w, form, values): + if form.is_subform: + entity = form.edited_entity + values = form.req.data.get('formvalues', ()) + qeid = eid_param('eid', entity.eid) + cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % html_escape(dumps(entity.eid)) + w(u'' % (entity.row % 2 and u'even' or u'odd')) + # XXX turn this into a widget used on the eid field + w(u'%s' % checkbox('eid', entity.eid, checked=qeid in values)) + for field in fields: + error = form.form_field_error(field) + if error: + w(u'') + w(error) + else: + w(u'') + if isinstance(field.widget, (fwdgs.Select, fwdgs.CheckBox, fwdgs.Radio)): + field.widget.attrs['onchange'] = cbsetstate + elif isinstance(field.widget, fwdgs.Input): + field.widget.attrs['onkeypress'] = cbsetstate + w(u'
    %s
    ' % field.render(form, self)) + w(u'/') + else: + # main form, display table headers + w(u'') + w(u'%s' + % tags.input(type='checkbox', title=form.req._('toggle check boxes'), + onclick="setCheckboxesState('eid', this.checked)")) + for field in self.forms[0].fields: + if self.display_field(form, field) and field.is_visible(): + w(u'%s' % form.req._(field.label)) + w(u'') + + + +class EntityFormRenderer(FormRenderer): + """specific renderer for entity edition form (edition)""" + + def open_form(self, form, values): + attrs_fs_label = ('
    %s
    ' + % form.req._('main informations')) + attrs_fs_label += '
    ' + return super(EntityFormRenderer, self).open_form(form, values) + attrs_fs_label + + def render_fields(self, w, form, values): + super(EntityFormRenderer, self).render_fields(w, form, values) + self.inline_entities_form(w, form) + if form.edited_entity.has_eid(): + self.relations_form(w, form) + w(u'
    ') # close extra div introducted by open_form + + def _render_fields(self, fields, w, form, values): + if not form.edited_entity.has_eid() or form.edited_entity.has_perm('update'): + super(EntityFormRenderer, self)._render_fields(fields, w, form, values) + + def render_buttons(self, w, form): + buttons = form.form_buttons() + if len(buttons) == 3: + w(""" + + + +
    + %s + + %s + %s +
    """ % tuple(buttons)) + else: + super(EntityFormRenderer, self).render_buttons(w, form) + + def relations_form(self, w, form): + srels_by_cat = form.srelations_by_category(('generic', 'metadata'), 'add') + if not srels_by_cat: + return u'' + req = form.req + _ = req._ + label = u'%s :' % _('This %s' % form.edited_entity.e_schema).capitalize() + eid = form.edited_entity.eid + w(u'
    ') + w(u'%s' % label) + w(u'') + for rschema, target, related in form.relations_table(): + # already linked entities + if related: + w(u'' % rschema.display_name(req, target)) + w(u'') + w(u'') + pendings = list(form.restore_pending_inserts()) + if not pendings: + w(u'') + else: + for row in pendings: + # soon to be linked to entities + w(u'' % row[1]) + w(u'' % row[3]) + w(u'') + w(u'') + w(u'' % eid) + w(u'') + w(u'' % eid) + w(u'') + w(u'
    %s') + w(u'
      ') + for viewparams in related: + w(u'' + % (viewparams[1], viewparams[0], viewparams[2], viewparams[3])) + if not form.force_display and form.maxrelitems < len(related): + link = (u'' % form.req._('view all')) + w(u'' % link) + w(u'
    ') + w(u'
      
    %s') + w(u'[x]' % + (_('cancel this insert'), row[2])) + w(u'%s' + % (row[1], row[4], html_escape(row[5]))) + w(u'
    ') + w(u'%s' % _('add relation')) + w(u'') + w(u'
    ') + w(u'
    ') + + def inline_entities_form(self, w, form): + """create a form to edit entity's inlined relations""" + entity = form.edited_entity + __ = form.req.__ + for rschema, targettypes, role in form.relations_by_category('inlineview', 'add'): + # show inline forms only if there's one possible target type + # for rschema + if len(targettypes) != 1: + self.warning('entity related by the %s relation should have ' + 'inlined form but there is multiple target types, ' + 'dunno what to do', rschema) + continue + targettype = targettypes[0].type + if form.should_inline_relation_form(rschema, targettype, role): + w(u'
    ' % rschema) + existant = entity.has_eid() and entity.related(rschema) + if existant: + # display inline-edition view for all existing related entities + w(self.view('inline-edition', existant, rtype=rschema, role=role, + ptype=entity.e_schema, peid=entity.eid, + **kwargs)) + if role == 'subject': + card = rschema.rproperty(entity.e_schema, targettype, 'cardinality')[0] + else: + card = rschema.rproperty(targettype, entity.e_schema, 'cardinality')[1] + # there is no related entity and we need at least one: we need to + # display one explicit inline-creation view + if form.should_display_inline_creation_form(rschema, existant, card): + w(self.view('inline-creation', None, etype=targettype, + peid=entity.eid, ptype=entity.e_schema, + rtype=rschema, role=role, **kwargs)) + # we can create more than one related entity, we thus display a link + # to add new related entities + if form.should_display_add_new_relation_link(rschema, existant, card): + divid = "addNew%s%s%s:%s" % (targettype, rschema, role, entity.eid) + w(u'
    ' + % divid) + js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s')" % ( + entity.eid, entity.e_schema, targettype, rschema, role) + if card in '1?': + js = "toggleVisibility('%s'); %s" % (divid, js) + w(u'+ %s.' + % (rschema, entity.eid, js, __('add a %s' % targettype))) + w(u'
    ') + w(u'
     
    ') + w(u'
    ') + + +class EntityInlinedFormRenderer(EntityFormRenderer): + """specific renderer for entity inlined edition form + (inline-[creation|edition]) + """ + def render(self, form, values): + form.add_media() + data = [] + w = data.append + try: + w(u'
    ' % values) + except KeyError: + w(u'
    ' % values) + else: + w(u'
    %s
    ' % ( + values['divid'], form.req._('click on the box to cancel the deletion'))) + w(u'
    ') + values['removemsg'] = form.req.__('remove this %s' % form.edited_entity.e_schema) + w(u'
    %(title)s ' + '#1 ' + '[%(removemsg)s]
    ' + % values) + self.render_fields(w, form, values) + w(u'
    ') + return '\n'.join(data) + + def render_fields(self, w, form, values): + form.form_build_context(values) + w(u'
    ' % values) + fields = self._render_hidden_fields(w, form) + w(u'
    ') + w(u'
    ') + if fields: + self._render_fields(fields, w, form, values) + self.render_child_forms(w, form, values) + self.inline_entities_form(w, form) + w(u'
    ') + diff -r 547681592765 -r 402e8a8b1d6a web/formwidgets.py --- a/web/formwidgets.py Thu Mar 26 18:58:14 2009 +0100 +++ b/web/formwidgets.py Thu Mar 26 18:59:01 2009 +0100 @@ -206,8 +206,6 @@ if not value: value = date.today() year, month = value.year, value.month - onclick = "toggleCalendar('%s', '%s', %s, %s);" % ( - helperid, inputid, year, month) return (u""" """ % (helperid, inputid, year, month, @@ -226,5 +224,5 @@ def render(self, form, field): self.add_media(form) - name, values, attrs = self._render_attrs(form, field) + attrs = self._render_attrs(form, field)[-1] return tags.div(**attrs) diff -r 547681592765 -r 402e8a8b1d6a web/test/unittest_form.py --- a/web/test/unittest_form.py Thu Mar 26 18:58:14 2009 +0100 +++ b/web/test/unittest_form.py Thu Mar 26 18:59:01 2009 +0100 @@ -25,31 +25,56 @@ creation_date = DateTimeField(widget=DateTimePicker) form = CustomChangeStateForm(self.req, redirect_path='perdu.com', entity=self.entity) - self.assertTextEquals(form.form_render(state=123, trcomment=u''), - ''' ''') + form.form_render(state=123, trcomment=u'') def test_change_state_form(self): form = ChangeStateForm(self.req, redirect_path='perdu.com', entity=self.entity) - self.assertTextEquals(form.form_render(state=123, trcomment=u''), - ''' ''') + form.form_render(state=123, trcomment=u'') def test_delete_conf_form_multi(self): rset = self.execute('EGroup X') - self.assertTextEquals(self.view('deleteconf', rset, template=None).source, - '') + self.view('deleteconf', rset, template=None).source def test_massmailing_form(self): self.execute('INSERT EmailAddress X: X address L + "@cubicweb.org", ' 'U use_email X WHERE U is EUser, U login L') rset = self.execute('EUser X') - self.assertTextEquals(self.view('massmailing', rset, template=None).source, - '') + self.view('massmailing', rset, template=None) + + def test_automatic_edition_form(self): + rset = self.execute('EUser X') + self.view('edition', rset, row=0, template=None).source + + def test_automatic_edition_form(self): + rset = self.execute('EUser X') + self.view('copy', rset, row=0, template=None).source + + def test_automatic_creation_form(self): + self.view('creation', None, etype='EUser', template=None).source + + def test_automatic_muledit_form(self): + rset = self.execute('EUser X') + self.view('muledit', rset, template=None).source + + def test_automatic_reledit_form(self): + rset = self.execute('EUser X') + self.view('reledit', rset, row=0, rtype='login', template=None).source + + def test_automatic_inline_edit_form(self): + geid = self.execute('EGroup X LIMIT 1')[0][0] + rset = self.execute('EUser X LIMIT 1') + self.view('inline-edition', rset, row=0, rtype='in_group', peid=geid, template=None).source + + def test_automatic_inline_creation_form(self): + geid = self.execute('EGroup X LIMIT 1')[0][0] + self.view('inline-creation', None, etype='EUser', rtype='in_group', peid=geid, template=None).source + # fields tests ############################################################ def _render_entity_field(self, name, form): - form.form_add_entity_hiddens(form.entity.e_schema) + form.form_add_entity_hiddens(form.edited_entity.e_schema) form.form_build_context({}) return form.field_by_name(name).render(form, self.renderer) diff -r 547681592765 -r 402e8a8b1d6a web/views/editforms.py --- a/web/views/editforms.py Thu Mar 26 18:58:14 2009 +0100 +++ b/web/views/editforms.py Thu Mar 26 18:59:01 2009 +0100 @@ -7,20 +7,43 @@ """ __docformat__ = "restructuredtext en" +from copy import copy + from simplejson import dumps -from cubicweb.selectors import match_kwargs, one_line_rset, non_final_entity +from logilab.mtconverter import html_escape + +from cubicweb import typed_eid +from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity, + specified_etype_implements, yes) +from cubicweb.rtags import RelationTags from cubicweb.utils import make_uid from cubicweb.view import EntityView from cubicweb.common import tags -from cubicweb.web import stdmsgs -from cubicweb.web.form import MultipleFieldsForm, EntityFieldsForm, FormMixIn, FormRenderer +from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs, formwidgets +from cubicweb.web.form import CompositeForm, EntityFieldsForm, FormMixIn from cubicweb.web.formfields import guess_field +from cubicweb.web.formrenderers import (FormRenderer, EntityFormRenderer, + EntityCompositeFormRenderer, + EntityInlinedFormRenderer) +_ = unicode -_ = unicode +def relation_id(eid, rtype, role, reid): + """return an identifier for a relation between two entities""" + if role == 'subject': + return u'%s:%s:%s' % (eid, rtype, reid) + return u'%s:%s:%s' % (reid, rtype, eid) + +def toggable_relation_link(eid, nodeid, label='x'): + """return javascript snippet to delete/undelete a relation between two + entities + """ + js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid))) + return u'[%s]' % (js, nodeid, label) class DeleteConfForm(EntityView): + """form used to confirm deletion of some entities""" id = 'deleteconf' title = _('delete') domid = 'deleteconf' @@ -37,19 +60,18 @@ % _('this action is not reversible!')) # XXX above message should have style of a warning w(u'

    %s

    \n' % _('Do you want to delete the following element(s) ?')) - form = MultipleFieldsForm(req, domid='deleteconf', action=self.build_url('edit'), + form = CompositeForm(req, domid='deleteconf', action=self.build_url('edit'), onsubmit=self.onsubmit, copy_nav_params=True) + # XXX tabindex form.buttons.append(form.button_delete(label=stdmsgs.YES)) form.buttons.append(form.button_cancel(label=stdmsgs.NO)) done = set() w(u'
      \n') - for i in xrange(self.rset.rowcount): - if self.rset[i][0] in done: + for entity in self.rset.entities(): + if entity.eid in done: continue - done.add(self.rset[i][0]) - entity = self.rset.get_entity(i, 0) - subform = EntityFieldsForm(req, set_error_url=False, - entity=entity) + done.add(entity.eid) + subform = EntityFieldsForm(req, entity=entity, set_error_url=False) form.form_add_subform(subform) # don't use outofcontext view or any other that may contain inline edition form w(u'
    • %s
    • ' % tags.a(entity.view('textoutofcontext'), @@ -59,12 +81,21 @@ class ClickAndEditForm(FormMixIn, EntityView): + """form used to permit ajax edition of an attribute of an entity in a view + + (double-click on the field to see an appropriate edition widget) + """ id = 'reledit' __select__ = non_final_entity() & match_kwargs('rtype') - + # FIXME editableField class could be toggleable from userprefs - + + onsubmit = ("return inlineValidateForm('%(divid)s-form', '%(rtype)s', " + "'%(eid)s', '%(divid)s', %(reload)s);") + ondblclick = "showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')" + def cell_call(self, row, col, rtype=None, role='subject', reload=False): + """display field to edit entity's `rtype` relation on double-click""" entity = self.entity(row, col) if getattr(entity, rtype) is None: value = self.req._('not specified') @@ -78,84 +109,520 @@ edit_key = make_uid('%s-%s' % (rtype, eid)) divid = 'd%s' % edit_key reload = dumps(reload) - buttons = [tags.input(klass="validateButton", type="submit", name="__action_apply", - value=self.req._(stdmsgs.BUTTON_OK), tabindex=self.req.next_tabindex()), + # XXX tab index + buttons = [tags.input(klass="validateButton", type="submit", + name="__action_apply", + value=self.req._(stdmsgs.BUTTON_OK), + tabindex=self.req.next_tabindex()), tags.input(klass="validateButton", type="button", value=self.req._(stdmsgs.BUTTON_CANCEL), onclick="cancelInlineEdit(%s,\'%s\',\'%s\')" % (eid, rtype, divid), tabindex=self.req.next_tabindex())] - form = self.vreg.select_object('forms', 'edition', self.req, self.rset, row=row, col=col, - entity=entity, domid='%s-form' % divid, action='#', - cssstyle='display: none', buttons=buttons, - onsubmit="return inlineValidateForm('%(divid)s-form', '%(rtype)s', '%(eid)s', '%(divid)s', %(reload)s);" % locals()) + form = self.vreg.select_object('forms', 'edition', self.req, self.rset, + row=row, col=col, buttons=buttons, + domid='%s-form' % divid, action='#', + cssstyle='display: none', + onsubmit=self.onsubmit % locals()) renderer = FormRenderer(display_label=False, display_help=False, display_fields=(rtype,), button_bar_class='buttonbar') self.w(tags.div(value, klass='editableField', id=divid, - ondblclick="showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')" % locals())) - self.w(form.render(renderer=renderer)) + ondblclick=self.ondblclick % locals())) + self.w(form.form_render(renderer=renderer)) class AutomaticEntityForm(EntityFieldsForm): + """base automatic form to edit any entity + + Designed to be flly generated from schema but highly configurable through: + * rtags (rcategories, rwidgets, inlined, rpermissions) + * various standard form parameters + + You can also easily customise it by adding/removing fields in + AutomaticEntityForm instances. + """ id = 'edition' + needs_js = EntityFieldsForm.needs_js + ('cubicweb.ajax.js',) + cwtarget = 'eformframe' + cssclass = 'entityForm' + copy_nav_params = True + attrcategories = ('primary', 'secondary') + + # relations'category (eg primary/secondary/generic/metadata/generated) + rcategories = RelationTags() + # use primary and not generated for eid since it has to be an hidden + rcategories.set_rtag('primary', 'subject', 'eid') + rcategories.set_rtag('metadata', 'subject', 'creation_date') + rcategories.set_rtag('metadata', 'subject', 'modification_date') + rcategories.set_rtag('generated', 'subject', 'has_text') + + # relations'widget (eg one of available class name in cubicweb.web.formwidgets) + rwidgets = RelationTags() + # inlined view flag for non final relations + inlined = RelationTags() + # set of tags of the form _on_new on relations. is a + # schema action (add/update/delete/read), and when such a tag is found + # permissions checking is by-passed and supposed to be ok + rpermissions_overrides = RelationTags(use_set=True) + + @classmethod + def registered(cls, registry): + """build class using descriptor at registration time""" + super(AutomaticEntityForm, cls).registered(registry) + cls.init_rtags_category() + return cls + + @classmethod + def init_rtags_category(cls): + """set default category tags for relations where it's not yet defined in + the category relation tags + """ + for eschema in cls.schema.entities(): + for rschema, tschemas, role in eschema.relation_definitions(True): + for tschema in tschemas: + if role == 'subject': + X, Y = eschema, tschema + card = rschema.rproperty(X, Y, 'cardinality')[0] + composed = rschema.rproperty(X, Y, 'composite') == 'object' + else: + X, Y = tschema, eschema + card = rschema.rproperty(X, Y, 'cardinality')[1] + composed = rschema.rproperty(X, Y, 'composite') == 'subject' + if not cls.rcategories.rtag(role, rschema, X, Y): + if card in '1+': + if not rschema.is_final() and composed: + category = 'generated' + else: + category = 'primary' + elif rschema.is_final(): + category = 'secondary' + else: + category = 'generic' + cls.rcategories.set_rtag(category, role, rschema, X, Y) + def __init__(self, *args, **kwargs): super(AutomaticEntityForm, self).__init__(*args, **kwargs) - self.entity.complete() - for rschema, target in self.editable_attributes(self.entity): - field = guess_field(self.entity.__class__, self.entity.e_schema, - rschema, target) + if self.edited_entity.has_eid(): + self.edited_entity.complete() + for rschema, role in self.editable_attributes(): + wdgname = self.rwidgets.etype_rtag(self.edited_entity.id, role, rschema) + if wdgname: + field = guess_field(self.edited_entity.__class__, rschema, role, + eidparam=True, widget=getattr(formwidgets, wdgname)) + else: + field = guess_field(self.edited_entity.__class__, rschema, role, + eidparam=True) self.fields.append(field) - + + def action(self): + """return the form's action attribute""" + try: + return self._action + except AttributeError: + return self.build_url('validateform') + + def set_action(self, value): + self._action = value + + action = property(action, set_action) + def form_buttons(self): + """return the form's buttons (as string)""" return [self.button_ok(tabindex=self.req.next_tabindex()), self.button_apply(tabindex=self.req.next_tabindex()), self.button_cancel(tabindex=self.req.next_tabindex())] - def editable_attributes(self, entity): - # XXX both (add, delete) required for non final relations - return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add') + def editable_attributes(self): + """return a list of (relation schema, role) to edit for the entity + """ + return [(rschema, x) for rschema, _, x in self.relations_by_category(self.attrcategories, 'add') if rschema != 'eid'] -class _EditionForm(EntityView): - """primary entity edition form + def relations_by_category(self, categories=None, permission=None): + """return a list of (relation schema, target schemas, role) matching + categories and permission + """ + if categories is not None: + if not isinstance(categories, (list, tuple, set, frozenset)): + categories = (categories,) + if not isinstance(categories, (set, frozenset)): + categories = frozenset(categories) + eschema = self.edited_entity.e_schema + rtags = self.rcategories + permsoverrides = self.rpermissions_overrides + if self.edited_entity.has_eid(): + eid = self.edited_entity.eid + else: + eid = None + for rschema, targetschemas, role in eschema.relation_definitions(True): + if rschema in ('identity', 'has_text'): + continue + # check category first, potentially lower cost than checking + # permission which may imply rql queries + if categories is not None: + targetschemas = [tschema for tschema in targetschemas + if rtags.etype_rtag(eschema, role, rschema, tschema) in categories] + if not targetschemas: + continue + if permission is not None: + # tag allowing to hijack the permission machinery when + # permission is not verifiable until the entity is actually + # created... + if eid is None and '%s_on_new' % permission in permsoverrides.etype_rtags(eschema, role, rschema): + yield (rschema, targetschemas, role) + continue + if rschema.is_final(): + if not rschema.has_perm(self.req, permission, eid): + continue + elif role == 'subject': + if not ((eid is None and rschema.has_local_role(permission)) or + rschema.has_perm(self.req, permission, fromeid=eid)): + continue + # on relation with cardinality 1 or ?, we need delete perm as well + # if the relation is already set + if (permission == 'add' + and rschema.cardinality(eschema, targetschemas[0], role) in '1?' + and eid and self.edited_entity.related(rschema.type, role) + and not rschema.has_perm(self.req, 'delete', fromeid=eid, + toeid=self.edited_entity.related(rschema.type, role)[0][0])): + continue + elif role == 'object': + if not ((eid is None and rschema.has_local_role(permission)) or + rschema.has_perm(self.req, permission, toeid=eid)): + continue + # on relation with cardinality 1 or ?, we need delete perm as well + # if the relation is already set + if (permission == 'add' + and rschema.cardinality(targetschemas[0], eschema, role) in '1?' + and eid and self.edited_entity.related(rschema.type, role) + and not rschema.has_perm(self.req, 'delete', toeid=eid, + fromeid=self.edited_entity.related(rschema.type, role)[0][0])): + continue + yield (rschema, targetschemas, role) + + def srelations_by_category(self, categories=None, permission=None): + """filter out result of relations_by_category(categories, permission) by + removing final relations - When generating a new attribute_input, the editor will look for a method - named 'default_ATTRNAME' on the entity instance, where ATTRNAME is the - name of the attribute being edited. You may use this feature to compute - dynamic default values such as the 'tomorrow' date or the user's login - being connected - """ + return a list of (relation's label, relation'schema, role) + """ + result = [] + for rschema, ttypes, role in self.relations_by_category(categories, + permission): + if rschema.is_final(): + continue + result.append( (rschema.display_name(self.req, role), rschema, role) ) + return sorted(result) + + def relations_table(self): + """yiels 3-tuples (rtype, target, related_list) + where 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 + """ + entity = self.edited_entity + pending_deletes = self.req.get_pending_deletes(entity.eid) + # XXX add metadata category quick fix to get Folder relations + for label, rschema, role in self.srelations_by_category(('generic', 'metadata'), 'add'): + relatedrset = entity.related(rschema, role, limit=self.limit) + if rschema.has_perm(self.req, 'delete'): + toggable_rel_link_func = toggable_relation_link + else: + toggable_rel_link_func = lambda x, y, z: u'' + related = [] + for row in xrange(relatedrset.rowcount): + nodeid = relation_id(entity.eid, rschema, role, + relatedrset[row][0]) + if nodeid in pending_deletes: + status = u'pendingDelete' + label = '+' + else: + status = u'' + label = 'x' + dellink = toggable_rel_link_func(entity.eid, nodeid, label) + eview = self.view('oneline', relatedrset, row=row) + related.append((nodeid, dellink, status, eview)) + yield (rschema, role, related) + + def restore_pending_inserts(self, cell=False): + """used to restore edition page as it was before clicking on + 'search for ' + """ + eid = self.edited_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) == eid: # subject + label = display_name(self.req, rtype, 'subject') + reid = eidto + 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 + + # should_* method extracted to allow overriding + + def should_inline_relation_form(self, rschema, targettype, role): + """return true if the given relation with entity has role and a + targettype target should be inlined + """ + return self.inlined.etype_rtag(self.edited_entity.id, role, rschema, targettype) + + def should_display_inline_creation_form(self, rschema, existant, card): + """return true if a creation form should be inlined + + by default true if there is no related entity and we need at least one + """ + return not existant and card in '1+' + + def should_display_add_new_relation_link(self, rschema, existant, card): + """return true if we should add a link to add a new creation form + (through ajax call) + + by default true if there is no related entity or if the relation has + multiple cardinality + """ + return not existant or card in '+*' + + +class EditionFormView(EntityView): + """display primary entity edition form""" id = 'edition' - __select__ = one_line_rset() & non_final_entity() + # add yes() so it takes precedence over deprecated views in baseforms, + # though not baseforms based customized view + __select__ = one_line_rset() & non_final_entity() & yes() title = _('edition') controller = 'edit' - skip_relations = FormMixIn.skip_relations.copy() - + def cell_call(self, row, col, **kwargs): - self.req.add_js( ('cubicweb.ajax.js',) ) - self.initialize_varmaker() entity = self.complete_entity(row, col) + self.render_form(entity) + + def render_form(self, entity): + """fetch and render the form""" + self.form_title(entity) + form = self.vreg.select_object('forms', 'edition', self.req, self.rset, + row=self.row, col=self.col, entity=entity, + domid=self.id, submitmsg=self.submited_message()) + self.init_form(form, entity) + self.w(form.form_render(renderer=EntityFormRenderer(), formvid=u'edition')) - def initialize_varmaker(self): - varmaker = self.req.get_page_data('rql_varmaker') - if varmaker is None: - varmaker = self.req.varmaker - self.req.set_page_data('rql_varmaker', varmaker) - self.varmaker = varmaker + def init_form(self, form, entity): + """customize your form before rendering here""" + form.form_add_hidden(u'__maineid', entity.eid) + + def form_title(self, entity): + """the form view title""" + ptitle = self.req._(self.title) + self.w(u'
      %s %s
      ' % ( + entity.dc_type(), ptitle and '(%s)' % ptitle)) + + def submited_message(self): + """return the message that will be displayed on successful edition""" + return self.req._('entity edited') + - def edit_form(self, entity, kwargs): - form = EntityFieldsForm(self.req, entity=entity) - for rschema, target in self.editable_attributes(entity): - field = guess_field(entity.__class__, entity.e_schema, rschema, target) - form.fields.append(field) - form.buttons.append(form.button_ok()) - form.buttons.append(form.button_apply()) - form.buttons.append(form.button_cancel()) - self.w(form.form_render()) +class CreationFormView(EditionFormView): + """display primary entity creation form""" + id = 'creation' + __select__ = specified_etype_implements('Any') & yes() + + title = _('creation') + + def call(self, **kwargs): + """creation view for an entity""" + etype = kwargs.pop('etype', self.req.form.get('etype')) + try: + entity = self.vreg.etype_class(etype)(self.req, None, None) + except: + self.w(self.req._('no such entity type %s') % etype) + else: + self.initialize_varmaker() + entity.eid = self.varmaker.next() + self.render_form(entity) + + def form_title(self, entity): + """the form view title""" + if '__linkto' in self.req.form: + if isinstance(self.req.form['__linkto'], list): + # XXX which one should be considered (case: add a ticket to a + # version in jpl) + rtype, linkto_eid, role = self.req.form['__linkto'][0].split(':') + else: + rtype, linkto_eid, role = self.req.form['__linkto'].split(':') + linkto_rset = self.req.eid_rset(linkto_eid) + linkto_type = linkto_rset.description[0][0] + if role == 'subject': + title = self.req.__('creating %s (%s %s %s %%(linkto)s)' % ( + entity.e_schema, entity.e_schema, rtype, linkto_type)) + else: + title = self.req.__('creating %s (%s %%(linkto)s %s %s)' % ( + entity.e_schema, linkto_type, rtype, entity.e_schema)) + msg = title % {'linkto' : self.view('incontext', linkto_rset)} + self.w(u'
      %s
      ' % msg) + else: + super(CreationFormView, self).form_title(entity) + + def url(self): + """return the url associated with this view""" + return self.create_url(self.req.form.get('etype')) + + def submited_message(self): + """return the message that will be displayed on successful edition""" + return self.req._('entity created') + + +class CopyFormView(EditionFormView): + """display primary entity creation form initialized with values from another + entity + """ + id = 'copy' + def render_form(self, entity): + """fetch and render the form""" + # make a copy of entity to avoid altering the entity in the + # request's cache. + self.newentity = copy(entity) + self.copying = self.newentity.eid + self.newentity.eid = None + self.w(u'\n' + % self.req._('Please note that this is only a shallow copy')) + super(CopyFormView, self).render_form(entity) + del self.newentity + + def init_form(self, form, entity): + """customize your form before rendering here""" + super(CopyFormView, self).init_form(form, entity) + if entity.eid == self.newentity.eid: + form.form_add_hidden('__cloned_eid', self.copying, eidparam=True) + + def submited_message(self): + """return the message that will be displayed on successful edition""" + return self.req._('entity copied') + + +class TableEditForm(CompositeForm): + id = 'muledit' + onsubmit = "return validateForm('entityForm', null);" + + def __init__(self, *args, **kwargs): + super(TableEditForm, self).__init__(*args, **kwargs) + for row in xrange(len(self.rset)): + form = self.vreg.select_object('forms', 'edition', self.req, self.rset, + row=row, domid=self.id, + attrcategories=('primary',), + set_error_url=False) + # XXX rely on the MultipleEntityFormRenderer to put the eid input + form.remove_field(form.field_by_name('eid')) + self.form_add_subform(form) - def editable_attributes(self, entity): - # XXX both (add, delete) - return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add') - if rschema != 'eid'] + def form_buttons(self): + """return the form's buttons (as string)""" + okt = self.req._('validate modifications on selected items').capitalize() + resett = self.req._('revert changes').capitalize() + return [self.button_ok(title=okt), self.button_reset(title=resett)] + + +class TableEditFormView(EntityView): + id = 'muledit' + __select__ = EntityView.__select__ & yes() + title = _('multiple edit') + + def call(self, **kwargs): + """a view to edit multiple entities of the same type the first column + should be the eid + """ + #self.form_title(entity) + form = self.vreg.select_object('forms', self.id, self.req, self.rset, + domid=self.id) + self.w(form.form_render(renderer=EntityCompositeFormRenderer())) + + +class InlineEntityEditionFormView(EntityView): + id = 'inline-edition' + __select__ = non_final_entity() & match_kwargs('peid', 'rtype') + removejs = "removeInlinedEntity('%s', '%s', '%s')" + + def call(self, **kwargs): + """redefine default call() method to avoid automatic + insertions of
      between each row of + the resultset + """ + rset = self.rset + for i in xrange(len(rset)): + self.wview(self.id, rset, row=i, **kwargs) + + def cell_call(self, row, col, peid, rtype, role='subject', **kwargs): + """ + :param peid: the parent entity's eid hosting the inline form + :param rtype: the relation bridging `etype` and `peid` + :param role: the role played by the `peid` in the relation + """ + entity = self.entity(row, col) + divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (peid, rtype, + entity.eid) + self.render_form(entity, peid, rtype, role, divonclick=divonclick) + + def render_form(self, entity, peid, rtype, role, **kwargs): + """fetch and render the form""" + rschema = self.schema.rschema(rtype) + divid = '%s-%s-%s' % (peid, rtype, entity.eid) + title = rschema.display_name(self.req, role) + form = self.vreg.select_object('forms', 'edition', self.req, + entity=entity) + removejs = self.removejs % (peid, rtype,entity.eid) + if self.keep_entity(entity, peid, rtype): + if entity.has_eid(): + rval = entity.eid + else: + rval = INTERNAL_FIELD_VALUE + form.form_add_hidden('edit%s-%s:%s' % (role[0], rtype, peid), rval) + form.form_add_hidden(name='%s:%s' % (rtype, peid), value=entity.eid, + id='rel-%s-%s-%s' % (peid, rtype, entity.eid)) + self.w(form.form_render(renderer=EntityInlinedFormRenderer(), divid=divid, + title=title, removejs=removejs,**kwargs)) + + def keep_entity(self, entity, peid, rtype): + if not entity.has_eid(): + return True + # are we regenerating form because of a validation error ? + erroneous_post = self.req.data.get('formvalues') + if erroneous_post: + cdvalues = self.req.list_form_param('%s:%s' % (rtype, peid), + erroneous_post) + if unicode(entity.eid) not in cdvalues: + return False + return True + + +class InlineEntityCreationFormView(InlineEntityEditionFormView): + id = 'inline-creation' + __select__ = (match_kwargs('peid', 'rtype') + & specified_etype_implements('Any')) + + def call(self, etype, peid, rtype, role='subject', **kwargs): + """ + :param etype: the entity type being created in the inline form + :param peid: the parent entity's eid hosting the inline form + :param rtype: the relation bridging `etype` and `peid` + :param role: the role played by the `peid` in the relation + """ + try: + entity = self.vreg.etype_class(etype)(self.req, None, None) + except: + self.w(self.req._('no such entity type %s') % etype) + return + self.initialize_varmaker() + entity.eid = self.varmaker.next() + self.render_form(entity, peid, rtype, role)