diff -r 000000000000 -r b97547f5f1fa web/form.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/form.py Wed Nov 05 15:52:50 2008 +0100 @@ -0,0 +1,253 @@ +"""abstract form classes for CubicWeb web client + +:organization: Logilab +:copyright: 2001-2008 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 logilab.mtconverter import html_escape + +from cubicweb import typed_eid +from cubicweb.common.selectors import req_form_params_selector +from cubicweb.common.registerers import accepts_registerer +from cubicweb.common.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView +from cubicweb.web import stdmsgs +from cubicweb.web.httpcache import NoHTTPCacheManager +from cubicweb.web.controller import redirect_params + + +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""" + 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 + and 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 + + @property + def limit(self): + if self.force_display: + return None + return self.maxrelitems + 1 + + def need_multipart(self, entity, categories=('primary', 'secondary')): + """return a boolean indicating if form's enctype should be multipart + """ + for rschema, _, x in entity.relations_by_category(categories): + if entity.get_widget(rschema, x).need_multipart: + return True + # let's find if any of our inlined entities needs multipart + for rschema, targettypes, x in entity.relations_by_category('inlineview'): + assert len(targettypes) == 1, \ + "I'm not able to deal with several targets and inlineview" + ttype = targettypes[0] + inlined_entity = self.vreg.etype_class(ttype)(self.req, None, None) + for irschema, _, x in inlined_entity.relations_by_category(categories): + if inlined_entity.get_widget(irschema, x).need_multipart: + return True + return False + + def error_message(self): + """return formatted error message + + This method should be called once inlined field errors has been consumed + """ + errex = self.req.data.get('formerrors') + # get extra errors + if errex is not None: + errormsg = self.req._('please correct the following errors:') + displayed = self.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' % (self.req._(field), err) + if len(errors) > 1: + 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) + +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) + + +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(req_form_params_selector) + + form_params = () + +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 + """ +