# HG changeset patch # User Sylvain Thénault # Date 1263978372 -3600 # Node ID 35cd057339b2b284a8fa90ef192f8f7ba5d9f864 # Parent e2ed81c20e7485e3a14f7ceac92dee55b1cf53c1 turn all the stuff used to handle 'generic relations' in forms into proper field / widget. -> regroup code from web.request, web.controller, web.views.autoform, web.views.formrenderers, web.views.editcontroller (!) into GenericRelationsField, GenericRelationsWidget in the editviews module (together with the UnrelatedDiv view). So: * almost everything in one place * no more specific behaviour in the form renderer * almost no custom behaviour in autoform (simply add the field when it think it should) Also, the form renderer now display field's value with colspan=2 when field.label is None. diff -r e2ed81c20e74 -r 35cd057339b2 web/controller.py --- a/web/controller.py Wed Jan 20 08:43:41 2010 +0100 +++ b/web/controller.py Wed Jan 20 10:06:12 2010 +0100 @@ -35,19 +35,6 @@ params[navparam] = form[redirectparam] return params -def parse_relations_descr(rdescr): - """parse a string describing some relations, in the form - subjeids:rtype:objeids - where subjeids and objeids are eids separeted by a underscore - - return an iterator on (subject eid, relation type, object eid) found - """ - for rstr in rdescr: - subjs, rtype, objs = rstr.split(':') - for subj in subjs.split('_'): - for obj in objs.split('_'): - yield typed_eid(subj), rtype, typed_eid(obj) - def append_url_params(url, params): """append raw parameters to the url. Given parameters, if any, are expected to be already url-quoted. @@ -137,22 +124,6 @@ else: self._cw.set_message(self._cw._('entity deleted')) - def delete_relations(self, rdefs): - """delete relations from the repository""" - # FIXME convert to using the syntax subject:relation:eids - execute = self._cw.execute - for subj, rtype, obj in rdefs: - rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype - execute(rql, {'x': subj, 'y': obj}, ('x', 'y')) - self._cw.set_message(self._cw._('relations deleted')) - - def insert_relations(self, rdefs): - """insert relations into the repository""" - execute = self._cw.execute - for subj, rtype, obj in rdefs: - rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype - execute(rql, {'x': subj, 'y': obj}, ('x', 'y')) - def reset(self): """reset form parameters and redirect to a view determinated by given diff -r e2ed81c20e74 -r 35cd057339b2 web/request.py --- a/web/request.py Wed Jan 20 08:43:41 2010 +0100 +++ b/web/request.py Wed Jan 20 10:06:12 2010 +0100 @@ -386,37 +386,7 @@ raise RequestError(self._('missing parameters for entity %s') % eid) return params - def get_pending_operations(self, entity, relname, role): - operations = {'insert' : [], 'delete' : []} - for optype in ('insert', 'delete'): - data = self.get_session_data('pending_%s' % optype) or () - for eidfrom, rel, eidto in data: - if relname == rel: - if role == 'subject' and entity.eid == eidfrom: - operations[optype].append(eidto) - if role == 'object' and entity.eid == eidto: - operations[optype].append(eidfrom) - return operations - - def get_pending_inserts(self, eid=None): - """shortcut to access req's pending_insert entry - - This is where are stored relations being added while editing - an entity. This used to be stored in a temporary cookie. - """ - pending = self.get_session_data('pending_insert') or () - return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending - if eid is None or eid in (subj, obj)] - - def get_pending_deletes(self, eid=None): - """shortcut to access req's pending_delete entry - - This is where are stored relations being removed while editing - an entity. This used to be stored in a temporary cookie. - """ - pending = self.get_session_data('pending_delete') or () - return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending - if eid is None or eid in (subj, obj)] + # XXX this should go to the GenericRelationsField. missing edition cancel protocol. def remove_pending_operations(self): """shortcut to clear req's pending_{delete,insert} entries diff -r e2ed81c20e74 -r 35cd057339b2 web/views/autoform.py --- a/web/views/autoform.py Wed Jan 20 08:43:41 2010 +0100 +++ b/web/views/autoform.py Wed Jan 20 10:06:12 2010 +0100 @@ -9,12 +9,12 @@ __docformat__ = "restructuredtext en" _ = unicode -from logilab.common.decorators import cached +from logilab.common.decorators import cached, iclassmethod from cubicweb import typed_eid from cubicweb.web import stdmsgs, uicfg from cubicweb.web import form, formwidgets as fwdgs -from cubicweb.web.views import forms, editforms +from cubicweb.web.views import forms, editforms, editviews _afs = uicfg.autoform_section @@ -44,6 +44,31 @@ # which relations should be edited display_fields = None + def _generic_relations_field(self): + try: + srels_by_cat = self.srelations_by_category('generic', 'add', strict=True) + warn('[3.6] %s: srelations_by_category is deprecated, use uicfg or ' + 'override editable_relations instead' % classid(form), + DeprecationWarning) + except AttributeError: + srels_by_cat = self.editable_relations() + if not srels_by_cat: + raise form.FieldNotFound('_cw_generic_field') + return editviews.GenericRelationsField(self.editable_relations()) + + @iclassmethod + def field_by_name(cls_or_self, name, role=None, eschema=None): + """return field with the given name and role. If field is not explicitly + defined for the form but `eclass` is specified, guess_field will be + called. + """ + try: + return super(AutomaticEntityForm, cls_or_self).field_by_name(name, role, eschema) + except form.FieldNotFound: + if name == '_cw_generic_field' and not isinstance(cls_or_self, type): + return cls_or_self._generic_relations_field() + raise + # base automatic entity form methods ####################################### def __init__(self, *args, **kwargs): @@ -64,6 +89,12 @@ except form.FieldNotFound: # meta attribute such as _format continue + if self.formtype == 'main' and entity.has_eid(): + try: + self.fields.append(self.field_by_name('_cw_generic_field')) + except form.FieldNotFound: + # no editable relation + pass self.maxrelitems = self._cw.property_value('navigation.related-limit') self.force_display = bool(self._cw.form.get('__force_display')) fnum = len(self.fields) @@ -163,63 +194,6 @@ # generic relations modifier ############################################### - 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._cw.get_pending_deletes(entity.eid) - for label, rschema, role in self.editable_relations(): - relatedrset = entity.related(rschema, role, limit=self.related_limit) - if rschema.has_perm(self._cw, 'delete'): - toggleable_rel_link_func = editforms.toggleable_relation_link - else: - toggleable_rel_link_func = lambda x, y, z: u'' - related = [] - for row in xrange(relatedrset.rowcount): - nodeid = editforms.relation_id(entity.eid, rschema, role, - relatedrset[row][0]) - if nodeid in pending_deletes: - status = u'pendingDelete' - label = '+' - else: - status = u'' - label = 'x' - dellink = toggleable_rel_link_func(entity.eid, nodeid, label) - eview = self._cw.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._cw.get_pending_inserts(eid)) - for pendingid in pending_inserts: - eidfrom, rtype, eidto = pendingid.split(':') - if typed_eid(eidfrom) == eid: # subject - label = display_name(self._cw, rtype, 'subject', - self.edited_entity.__regid__) - reid = eidto - else: - label = display_name(self._cw, rtype, 'object', - self.edited_entity.__regid__) - reid = eidfrom - jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \ - % (pendingid, cell, eid) - rset = self._cw.eid_rset(reid) - eview = self._cw.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._cw, 'Basket')) - yield rtype, pendingid, jscall, label, reid, eview - # inlined forms support #################################################### @cached diff -r e2ed81c20e74 -r 35cd057339b2 web/views/editcontroller.py --- a/web/views/editcontroller.py Wed Jan 20 08:43:41 2010 +0100 +++ b/web/views/editcontroller.py Wed Jan 20 10:06:12 2010 +0100 @@ -13,7 +13,7 @@ from cubicweb import Binary, ValidationError, typed_eid from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError -from cubicweb.web.controller import parse_relations_descr +from cubicweb.web.views.editviews import delete_relations, insert_relations from cubicweb.web.views.basecontrollers import ViewController @@ -73,8 +73,6 @@ # no specific action, generic edition self._to_create = req.data['eidmap'] = {} self._pending_fields = req.data['pendingfields'] = set() - todelete = self._cw.get_pending_deletes() - toinsert = self._cw.get_pending_inserts() try: methodname = req.form.pop('__method', None) for eid in req.edited_eids(): @@ -88,7 +86,7 @@ except (RequestError, NothingToEdit), ex: if '__linkto' in req.form and 'eid' in req.form: self.execute_linkto() - elif not ('__delete' in req.form or '__insert' in req.form or todelete or toinsert): + elif not ('__delete' in req.form or '__insert' in req.form): raise ValidationError(None, {None: unicode(ex)}) # handle relations in newly created entities if self._pending_fields: @@ -99,13 +97,15 @@ self._cw.execute(*querydef) # XXX this processes *all* pending operations of *all* entities if req.form.has_key('__delete'): - todelete += req.list_form_param('__delete', req.form, pop=True) - if todelete: - self.delete_relations(parse_relations_descr(todelete)) + todelete = req.list_form_param('__delete', req.form, pop=True) + if todelete: + delete_relations(self._cw, todelete) if req.form.has_key('__insert'): + warn('[3.6] stop using __insert, support will be removed', + DeprecationWarning) toinsert = req.list_form_param('__insert', req.form, pop=True) - if toinsert: - self.insert_relations(parse_relations_descr(toinsert)) + if toinsert: + insert_relations(self._cw, toinsert) self._cw.remove_pending_operations() if self.errors: errors = dict((f.name, unicode(ex)) for f, ex in self.errors) @@ -171,13 +171,11 @@ if is_main_entity: self.notify_edited(entity) if formparams.has_key('__delete'): + # XXX deprecate? todelete = self._cw.list_form_param('__delete', formparams, pop=True) - self.delete_relations(parse_relations_descr(todelete)) + delete_relations(self._cw, todelete) if formparams.has_key('__cloned_eid'): entity.copy_relations(typed_eid(formparams['__cloned_eid'])) - if formparams.has_key('__insert'): - toinsert = self._cw.list_form_param('__insert', formparams, pop=True) - self.insert_relations(parse_relations_descr(toinsert)) if is_main_entity: # only execute linkto for the main entity self.execute_linkto(entity.eid) return eid diff -r e2ed81c20e74 -r 35cd057339b2 web/views/editforms.py --- a/web/views/editforms.py Wed Jan 20 08:43:41 2010 +0100 +++ b/web/views/editforms.py Wed Jan 20 10:06:12 2010 +0100 @@ -30,21 +30,6 @@ _pvdc = uicfg.primaryview_display_ctrl -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 toggleable_relation_link(eid, nodeid, label='x'): - """return javascript snippet to delete/undelete a relation between two - entities - """ - js = u"javascript: togglePendingDelete('%s', %s);" % ( - nodeid, xml_escape(dumps(eid))) - return u'[%s]' % ( - js, nodeid, label) - class DeleteConfForm(forms.CompositeForm): __regid__ = 'deleteconf' diff -r e2ed81c20e74 -r 35cd057339b2 web/views/editviews.py --- a/web/views/editviews.py Wed Jan 20 08:43:41 2010 +0100 +++ b/web/views/editviews.py Wed Jan 20 10:06:12 2010 +0100 @@ -13,14 +13,29 @@ from logilab.common.decorators import cached from logilab.mtconverter import xml_escape -from cubicweb import typed_eid +from cubicweb import typed_eid, uilib +from cubicweb.schema import display_name from cubicweb.view import EntityView from cubicweb.selectors import (one_line_rset, non_final_entity, match_search_state, match_form_params) -from cubicweb.uilib import cut -from cubicweb.web.views import linksearch_select_url -from cubicweb.web.views.editforms import relation_id -from cubicweb.web.views.baseviews import FinalView +from cubicweb.web import formwidgets as fw, formfields as ff +from cubicweb.web.views import baseviews, linksearch_select_url + + +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 toggleable_relation_link(eid, nodeid, label='x'): + """return javascript snippet to delete/undelete a relation between two + entities + """ + js = u"javascript: togglePendingDelete('%s', %s);" % ( + nodeid, xml_escape(dumps(eid))) + return u'[%s]' % ( + js, nodeid, label) class SearchForAssociationView(EntityView): @@ -73,6 +88,195 @@ entity.view('outofcontext', w=self.w) +def get_pending_inserts(req, eid=None): + """shortcut to access req's pending_insert entry + + This is where are stored relations being added while editing + an entity. This used to be stored in a temporary cookie. + """ + pending = req.get_session_data('pending_insert') or () + return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending + if eid is None or eid in (subj, obj)] + +def get_pending_deletes(req, eid=None): + """shortcut to access req's pending_delete entry + + This is where are stored relations being removed while editing + an entity. This used to be stored in a temporary cookie. + """ + pending = req.get_session_data('pending_delete') or () + return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending + if eid is None or eid in (subj, obj)] + +def parse_relations_descr(rdescr): + """parse a string describing some relations, in the form + subjeids:rtype:objeids + where subjeids and objeids are eids separeted by a underscore + + return an iterator on (subject eid, relation type, object eid) found + """ + for rstr in rdescr: + subjs, rtype, objs = rstr.split(':') + for subj in subjs.split('_'): + for obj in objs.split('_'): + yield typed_eid(subj), rtype, typed_eid(obj) + +def delete_relations(req, rdefs): + """delete relations from the repository""" + # FIXME convert to using the syntax subject:relation:eids + execute = req.execute + for subj, rtype, obj in parse_relations_descr(rdefs): + rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype + execute(rql, {'x': subj, 'y': obj}, ('x', 'y')) + req.set_message(req._('relations deleted')) + +def insert_relations(req, rdefs): + """insert relations into the repository""" + execute = req.execute + for subj, rtype, obj in parse_relations_descr(rdefs): + rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype + execute(rql, {'x': subj, 'y': obj}, ('x', 'y')) + + + +class GenericRelationsWidget(fw.FieldWidget): + + def render(self, form, field, renderer): + stream = [] + w = stream.append + req = form._cw + _ = 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, role, related in field.relations_table(form): + # already linked entities + if related: + w(u'' % rschema.display_name(req, role)) + w(u'') + w(u'') + pendings = list(field.restore_pending_inserts(form)) + 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'' % self._cw._('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], xml_escape(row[5]))) + w(u'
') + w(u'') + w(u'
') + w(u'
') + return '\n'.join(stream) + + +class GenericRelationsField(ff.Field): + widget = GenericRelationsWidget + + def __init__(self, relations, name='_cw_generic_field', **kwargs): + assert relations + kwargs['eidparam'] = True + super(GenericRelationsField, self).__init__(name, **kwargs) + self.relations = relations + self.label = None + + def process_posted(self, form): + todelete = get_pending_deletes(form._cw) + if todelete: + delete_relations(form._cw, todelete) + toinsert = get_pending_inserts(form._cw) + if toinsert: + insert_relations(form._cw, toinsert) + return () + + def relations_table(self, form): + """yiels 3-tuples (rtype, role, 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 = form.edited_entity + pending_deletes = get_pending_deletes(form._cw, entity.eid) + for label, rschema, role in self.relations: + related = [] + if entity.has_eid(): + rset = entity.related(rschema, role, limit=form.related_limit) + if rschema.has_perm(form._cw, 'delete'): + toggleable_rel_link_func = toggleable_relation_link + else: + toggleable_rel_link_func = lambda x, y, z: u'' + for row in xrange(rset.rowcount): + nodeid = relation_id(entity.eid, rschema, role, + rset[row][0]) + if nodeid in pending_deletes: + status, label = u'pendingDelete', '+' + else: + status, label = u'', 'x' + dellink = toggleable_rel_link_func(entity.eid, nodeid, label) + eview = form._cw.view('oneline', rset, row=row) + related.append((nodeid, dellink, status, eview)) + yield (rschema, role, related) + + def restore_pending_inserts(self, form): + """used to restore edition page as it was before clicking on + 'search for ' + """ + entity = form.edited_entity + pending_inserts = set(get_pending_inserts(form._cw, form.edited_entity.eid)) + for pendingid in pending_inserts: + eidfrom, rtype, eidto = pendingid.split(':') + if typed_eid(eidfrom) == entity.eid: # subject + label = display_name(form._cw, rtype, 'subject', + entity.__regid__) + reid = eidto + else: + label = display_name(form._cw, rtype, 'object', + entity.__regid__) + reid = eidfrom + jscall = "javascript: cancelPendingInsert('%s', 'tr', null, %s);" \ + % (pendingid, entity.eid) + rset = form._cw.eid_rset(reid) + eview = form._cw.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(form._cw, 'Basket')) + yield rtype, pendingid, jscall, label, reid, eview + + class UnrelatedDivs(EntityView): __regid__ = 'unrelateddivs' __select__ = match_form_params('relation') @@ -97,7 +301,7 @@ else: targettypes = rschema.subjects(entity.e_schema) etypes = '/'.join(sorted(etype.display_name(self._cw) for etype in targettypes)) - etypes = cut(etypes, self._cw.property_value('navigation.short-line-size')) + etypes = uilib.cut(etypes, self._cw.property_value('navigation.short-line-size')) options.append('' % (self._cw._('select a'), etypes)) options += self._get_select_options(entity, rschema, target) options += self._get_search_options(entity, rschema, target, targettypes) @@ -117,15 +321,16 @@ def _get_select_options(self, entity, rschema, target): """add options to search among all entities of each possible type""" options = [] - pending_inserts = self._cw.get_pending_inserts(entity.eid) + pending_inserts = get_pending_inserts(self._cw, entity.eid) rtype = rschema.type form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity) field = form.field_by_name(rschema, target, entity.e_schema) limit = self._cw.property_value('navigation.combobox-limit') for eview, reid in field.choices(form, limit): # XXX expect 'limit' arg on choices if reid is None: - options.append('' - % xml_escape(eview)) + if eview: # skip blank value + options.append('' + % xml_escape(eview)) else: optionid = relation_id(entity.eid, rtype, target, reid) if optionid not in pending_inserts: @@ -193,7 +398,7 @@ self.wview('textoutofcontext', self.cw_rset, row=row, col=col) -class EditableFinalView(FinalView): +class EditableFinalView(baseviews.FinalView): """same as FinalView but enables inplace-edition when possible""" __regid__ = 'editable-final' diff -r e2ed81c20e74 -r 35cd057339b2 web/views/formrenderers.py --- a/web/views/formrenderers.py Wed Jan 20 08:43:41 2010 +0100 +++ b/web/views/formrenderers.py Wed Jan 20 10:06:12 2010 +0100 @@ -205,14 +205,15 @@ w(u'' % self.table_class) for field in fields: w(u'' % (field.name, field.role)) - if self.display_label: + if self.display_label and field.label is not None: w(u'' % self.render_label(form, field)) + w('') - self.render_error(w, error) - else: - w(u'
%s') + w(u' class="error"') + w(u'>') w(field.render(form, self)) if self.display_help: w(self.render_help(form, field)) @@ -392,72 +393,6 @@
""" % tuple(button.render(form) for button in form.form_buttons)) else: super(EntityFormRenderer, self).render_buttons(w, form) - - def relations_form(self, w, form): - try: - srels_by_cat = form.srelations_by_category('generic', 'add', strict=True) - warn('[3.6] %s: srelations_by_category is deprecated, override ' - 'editable_relations instead' % classid(form), DeprecationWarning) - except AttributeError: - srels_by_cat = form.editable_relations() - if not srels_by_cat: - return u'' - req = self._cw - _ = 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'' % self._cw._('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], xml_escape(row[5]))) - w(u'
') - w(u'') - w(u'
') - w(u'
') - # NOTE: should_* and display_* method extracted and moved to the form to # ease overriding