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
')