# HG changeset patch # User Aurelien Campeas # Date 1284555604 -7200 # Node ID 62e25fac41cdaa05df16b88a02fedeb4a44bbf43 # Parent e7e9d73d0c07eafb4bd600751a81e7f81cf231ef [views/reledit] refactor composite handling diff -r e7e9d73d0c07 -r 62e25fac41cd doc/book/en/devweb/views/reledit.rst --- a/doc/book/en/devweb/views/reledit.rst Wed Sep 15 11:13:17 2010 +0200 +++ b/doc/book/en/devweb/views/reledit.rst Wed Sep 15 15:00:04 2010 +0200 @@ -70,12 +70,23 @@ controlled using the reledit_ctrl rtag, defined in :mod:`cubicweb.web.uicfg`. -This rtag provides three control variables: +This rtag provides four control variables: -* ``default_value`` -* ``reload``, to specificy if edition of the relation entails a full page - reload, which defaults to False -* ``noedit``, to explicitly inhibit edition +* ``default_value``: alternative default value + The default value is what is shown when there is no value. +* ``reload``: boolean, eid (to reload to) or function taking subject + and returning bool/eid This is useful when editing a relation (or + attribute) that impacts the url or another parts of the current + displayed page. Defaults to false. +* ``rvid``: alternative view id (as str) for relation or composite + edition Default is 'incontext' or 'csv' depending on the + cardinality. They can also be statically changed by subclassing + ClickAndEditFormView and redefining _one_rvid (resp. _many_rvid). +* ``edit_target``: 'rtype' (to edit the relation) or 'related' (to + edit the related entity) This controls whether to edit the relation + or the target entity of the relation. Currently only one-to-one + relations support target entity edition. By default, the 'related' + option is taken whenever the relation is composite and one-to-one. Let's see how to use these controls. @@ -86,15 +97,13 @@ reledit_ctrl.tag_attribute(('Company', 'name'), {'reload': lambda x:x.eid, 'default_value': xml_escape(u'')}) - reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'noedit': True}) + reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'edit_target': 'related'}) The `default_value` needs to be an xml escaped unicode string. -The `noedit` attribute is convenient to programmatically disable some -relation edition on views that apply it systematically (the prime -example being the primary view). Here we use it to forbid changing the -`boss` relation from a `Person` side (as it could have unwanted -effects). +The `edit_target` tag on the `boss` relation being set to `related` will +ensure edition of the `Person` entity instead (using a standard +automatic form) of the association of Company and Person. Finally, the `reload` key accepts either a boolean, an eid or an unicode string representing an url. If an eid is provided, it will be diff -r e7e9d73d0c07 -r 62e25fac41cd rtags.py --- a/rtags.py Wed Sep 15 11:13:17 2010 +0200 +++ b/rtags.py Wed Sep 15 15:00:04 2010 +0200 @@ -210,4 +210,27 @@ _allowed_values = frozenset((True, False)) +class NoTargetRelationTagsDict(RelationTagsDict): + + @property + def name(self): + return self.__class__.name + + def tag_subject_of(self, key, tag): + subj, rtype, obj = key + if obj != '*': + self.warning('using explict target type in %s.tag_subject_of() ' + 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)', + self.name, subj, rtype, subj, rtype, obj) + super(NoTargetRelationTagsDict, self).tag_subject_of((subj, rtype, '*'), tag) + + def tag_object_of(self, key, tag): + subj, rtype, obj = key + if subj != '*': + self.warning('using explict subject type in %s.tag_object_of() ' + 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)', + self.name, rtype, obj, subj, rtype, obj) + super(NoTargetRelationTagsDict, self).tag_object_of(('*', rtype, obj), tag) + + set_log_methods(RelationTags, logging.getLogger('cubicweb.rtags')) diff -r e7e9d73d0c07 -r 62e25fac41cd web/data/cubicweb.css --- a/web/data/cubicweb.css Wed Sep 15 11:13:17 2010 +0200 +++ b/web/data/cubicweb.css Wed Sep 15 15:00:04 2010 +0200 @@ -867,7 +867,7 @@ } /********************************/ -/* rest releted classes */ +/* rest related classes */ /********************************/ img.align-right { @@ -878,6 +878,18 @@ margin-right: 1.5em; } +/******************************/ +/* reledit */ +/******************************/ + +.releditField { + display: inline; +} + +.releditForm { + display:none; +} + /********************************/ /* overwite other css here */ /********************************/ @@ -905,4 +917,3 @@ margin-left: 0.5em; vertical-align: bottom; } - diff -r e7e9d73d0c07 -r 62e25fac41cd web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Wed Sep 15 11:13:17 2010 +0200 +++ b/web/data/cubicweb.old.css Wed Sep 15 15:00:04 2010 +0200 @@ -860,3 +860,16 @@ .otherView { float: right; } + + +/******************************/ +/* reledit */ +/******************************/ + +.releditField { + display: inline; +} + +.releditForm { + display:none; +} diff -r e7e9d73d0c07 -r 62e25fac41cd web/test/data/schema.py --- a/web/test/data/schema.py Wed Sep 15 11:13:17 2010 +0200 +++ b/web/test/data/schema.py Wed Sep 15 15:00:04 2010 +0200 @@ -75,3 +75,19 @@ cp = String(maxsize=12) ville= String(maxsize=32) +# enough relations to cover most reledit use cases +class Project(EntityType): + title = String(maxsize=32, required=True, fulltextindexed=True) + long_desc = SubjectRelation('Blog', composite='subject', cardinality='?*') + manager = SubjectRelation('Personne', cardinality='?*') + +class composite_card11_2ttypes(RelationDefinition): + subject = 'Project' + object = ('File', 'Blog') + composite = 'subject' + cardinality = '?*' + +class Ticket(EntityType): + title = String(maxsize=32, required=True, fulltextindexed=True) + concerns = SubjectRelation('Project', composite='object') + diff -r e7e9d73d0c07 -r 62e25fac41cd web/test/unittest_reledit.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/unittest_reledit.py Wed Sep 15 15:00:04 2010 +0200 @@ -0,0 +1,223 @@ +# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +""" +mainly regression-preventing tests for reledit/doreledit views +""" + +from cubicweb.devtools.testlib import CubicWebTC +from cubicweb.web.uicfg import reledit_ctrl + +class ReleditMixinTC(object): + + def setup_database(self): + self.req = self.request() + self.proj = self.req.create_entity('Project', title=u'cubicweb-world-domination') + self.tick = self.req.create_entity('Ticket', title=u'write the code') + self.toto = self.req.create_entity('Personne', nom=u'Toto') + +class ClickAndEditFormTC(ReleditMixinTC, CubicWebTC): + + def test_default_config(self): + reledit = {'title': """
cubicweb-world-domination
""", + 'long_desc': """
<long_desc not specified>
""", + 'manager': """
<manager not specified>
""", + 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""", + 'concerns': """<concerns_object not specified>"""} + + for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True): + if rschema not in reledit: + continue + rtype = rschema.type + self.assertTextEquals(reledit[rtype], self.proj.view('reledit', rtype=rtype, role=role), rtype) + + def test_default_forms(self): + doreledit = {'title': """
cubicweb-world-domination
+
+ + + + + + + + + + + + + + + +
+ + + +
+ +
+ + + + + + + +
+
+
""", + + 'long_desc': """
<long_desc not specified>
+
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+ +
+ +
+ +
+ + + + + + + +
+
+
""", + + 'manager': """
<manager not specified>
+
+ + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ + + + + + + +
+
+
""", + 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""", + 'concerns': """<concerns_object not specified>""" + } + for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True): + if rschema not in doreledit: + continue + rtype = rschema.type + self.assertTextEquals(doreledit[rtype], + self.proj.view('doreledit', rtype=rtype, role=role, + formid='edition' if rtype == 'long_desc' else 'base'), + rtype) + +class ClickAndEditFormUICFGTC(ReleditMixinTC, CubicWebTC): + + def setup_database(self): + super(ClickAndEditFormUICFGTC, self).setup_database() + self.tick.set_relations(concerns=self.proj) + self.proj.set_relations(manager=self.toto) + + def test_with_uicfg(self): + old_rctl = reledit_ctrl._tagdefs.copy() + reledit_ctrl.tag_attribute(('Project', 'title'), + {'default_value': '', 'reload': True}) + reledit_ctrl.tag_subject_of(('Project', 'long_desc', '*'), + {'reload': True, 'edit_target': 'rtype', + 'default_value': u'<long_desc is required>'}) + reledit_ctrl.tag_subject_of(('Project', 'manager', '*'), + {'edit_target': 'related'}) + reledit_ctrl.tag_subject_of(('Project', 'composite_card11_2ttypes', '*'), + {'edit_target': 'related'}) + reledit_ctrl.tag_object_of(('Ticket', 'concerns', 'Project'), + {'edit_target': 'rtype'}) + reledit = { + 'title': """<div id="title-subject-917-reledit" onmouseout="jQuery('#title-subject-917').addClass('hidden')" onmouseover="jQuery('#title-subject-917').removeClass('hidden')" class="releditField"><div id="title-subject-917-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-917" class="editableField hidden"><div id="title-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'title', 'subject', 'title-subject-917', true, '', '<title is required>');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""", + 'long_desc': """<div id="long_desc-subject-917-reledit" onmouseout="jQuery('#long_desc-subject-917').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-917').removeClass('hidden')" class="releditField"><div id="long_desc-subject-917-value" class="editableFieldValue"><long_desc is required></div><div id="long_desc-subject-917" class="editableField hidden"><div id="long_desc-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'long_desc', 'subject', 'long_desc-subject-917', true, 'incontext', '<long_desc is required>');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""", + 'manager': """<div id="manager-subject-917-reledit" onmouseout="jQuery('#manager-subject-917').addClass('hidden')" onmouseover="jQuery('#manager-subject-917').removeClass('hidden')" class="releditField"><div id="manager-subject-917-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/personne/919" title="">Toto</a></div><div id="manager-subject-917" class="editableField hidden"><div id="manager-subject-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('edition', 917, 'manager', 'subject', 'manager-subject-917', false, 'incontext', '&lt;manager not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div><div id="manager-subject-917-delete" class="editableField" onclick="cw.reledit.loadInlineEditionForm('deleteconf', 917, 'manager', 'subject', 'manager-subject-917', false, 'incontext', '&lt;manager not specified&gt;');" title="click to delete this value"><img title="click to delete this value" src="data/cancel.png" alt="click to delete this value"/></div></div></div>""", + 'composite_card11_2ttypes': """<composite_card11_2ttypes not specified>""", + 'concerns': """<div id="concerns-object-917-reledit" onmouseout="jQuery('#concerns-object-917').addClass('hidden')" onmouseover="jQuery('#concerns-object-917').removeClass('hidden')" class="releditField"><div id="concerns-object-917-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/ticket/918" title="">write the code</a></div><div id="concerns-object-917" class="editableField hidden"><div id="concerns-object-917-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm('base', 917, 'concerns', 'object', 'concerns-object-917', false, 'csv', '&lt;concerns_object not specified&gt;');" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""" + } + for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True): + if rschema not in reledit: + continue + rtype = rschema.type + self.assertTextEquals(reledit[rtype], + self.proj.view('reledit', rtype=rtype, role=role), + rtype) + reledit_ctrl.clear() + reledit_ctrl._tagdefs.update(old_rctl) diff -r e7e9d73d0c07 -r 62e25fac41cd web/uicfg.py --- a/web/uicfg.py Wed Sep 15 11:13:17 2010 +0200 +++ b/web/uicfg.py Wed Sep 15 15:00:04 2010 +0200 @@ -53,7 +53,8 @@ from cubicweb import neg_role from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet, - RelationTagsDict, register_rtag, _ensure_str_key) + RelationTagsDict, NoTargetRelationTagsDict, + register_rtag, _ensure_str_key) from cubicweb.schema import META_RTYPES @@ -83,27 +84,11 @@ 'sideboxes', 'hidden'))) -class DisplayCtrlRelationTags(RelationTagsDict): +class DisplayCtrlRelationTags(NoTargetRelationTagsDict): def __init__(self, *args, **kwargs): super(DisplayCtrlRelationTags, self).__init__(*args, **kwargs) self.counter = 0 - def tag_subject_of(self, key, tag): - subj, rtype, obj = key - if obj != '*': - self.warning('using explict target type in display_ctrl.tag_subject_of() ' - 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)', - subj, rtype, subj, rtype, obj) - super(DisplayCtrlRelationTags, self).tag_subject_of((subj, rtype, '*'), tag) - - def tag_object_of(self, key, tag): - subj, rtype, obj = key - if subj != '*': - self.warning('using explict subject type in display_ctrl.tag_object_of() ' - 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)', - rtype, obj, subj, rtype, obj) - super(DisplayCtrlRelationTags, self).tag_object_of(('*', rtype, obj), tag) - def init_primaryview_display_ctrl(rtag, sschema, rschema, oschema, role): if role == 'subject': oschema = '*' @@ -381,7 +366,7 @@ autoform_field = RelationTags('autoform_field') # relations'field explicit kwargs (given to field's __init__) -autoform_field_kwargs = RelationTagsDict() +autoform_field_kwargs = RelationTagsDict('autoform_field_kwargs') # set of tags of the form <action>_on_new on relations. <action> is a @@ -389,31 +374,49 @@ # permissions checking is by-passed and supposed to be ok autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides') -class _ReleditTags(RelationTagsDict): - _keys = frozenset('reload default_value noedit'.split()) - - def tag_subject_of(self, key, *args, **kwargs): - subj, rtype, obj = key - if obj != '*': - self.warning('using explict target type in display_ctrl.tag_subject_of() ' - 'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)', - subj, rtype, subj, rtype, obj) - super(_ReleditTags, self).tag_subject_of(key, *args, **kwargs) - - def tag_object_of(self, key, *args, **kwargs): - subj, rtype, obj = key - if subj != '*': - self.warning('using explict subject type in display_ctrl.tag_object_of() ' - 'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)', - rtype, obj, subj, rtype, obj) - super(_ReleditTags, self).tag_object_of(key, *args, **kwargs) +class ReleditTags(NoTargetRelationTagsDict): + """ + default_value: alternative default value + The default value is what is shown when there is no value. + reload: boolean, eid (to reload to) or function taking subject and returning bool/eid + This is useful when editing a relation (or attribute) that impacts the url + or another parts of the current displayed page. Defaults to False. + rvid: alternative view id (as str) for relation or composite edition + Default is 'incontext' or 'csv' depending on the cardinality. They can also be + statically changed by subclassing ClickAndEditFormView and redefining _one_rvid + (resp. _many_rvid). + edit_target: 'rtype' (to edit the relation) or 'related' (to edit the related entity) + This controls whether to edit the relation or the target entity of the relation. + Currently only one-to-one relations support target entity edition. By default, + the 'related' option is taken whenever the relation is composite and one-to-one. + """ + _keys = frozenset('default_value reload rvid edit_target'.split()) def tag_relation(self, key, tag): for tagkey in tag.iterkeys(): assert tagkey in self._keys, 'tag %r not in accepted tags: %r' % (tag, self._keys) - return super(_ReleditTags, self).tag_relation(key, tag) + return super(ReleditTags, self).tag_relation(key, tag) -reledit_ctrl = _ReleditTags('reledit') +def init_reledit_ctrl(rtag, sschema, rschema, oschema, role): + if rschema.final: + return + composite = rschema.rdef(sschema, oschema).composite == role + if role == 'subject': + oschema = '*' + else: + sschema = '*' + values = rtag.get(sschema, rschema, oschema, role) + edittarget = values.get('edit_target') + if edittarget not in (None, 'rtype', 'related'): + rtag.warning('reledit: wrong value for edit_target on relation %s: %s', + rschema, edittarget) + edittarget = None + if not edittarget: + edittarget = 'related' if composite else 'rtype' + rtag.tag_relation((sschema, rschema, oschema, role), + {'edit_target': edittarget}) + +reledit_ctrl = ReleditTags('reledit', init_reledit_ctrl) # boxes.EditBox configuration ################################################# diff -r e7e9d73d0c07 -r 62e25fac41cd web/views/forms.py --- a/web/views/forms.py Wed Sep 15 11:13:17 2010 +0200 +++ b/web/views/forms.py Wed Sep 15 15:00:04 2010 +0200 @@ -188,7 +188,7 @@ if self.formvalues is not None: return # already built self.formvalues = formvalues or {} - # use a copy in case fields are modified while context is build (eg + # use a copy in case fields are modified while context is built (eg # __linkto handling for instance) for field in self.fields[:]: for field in field.actual_fields(self): diff -r e7e9d73d0c07 -r 62e25fac41cd web/views/primary.py --- a/web/views/primary.py Wed Sep 15 11:13:17 2010 +0200 +++ b/web/views/primary.py Wed Sep 15 15:00:04 2010 +0200 @@ -336,6 +336,28 @@ if url: self.w(u'<a href="%s">%s</a>' % (url, url)) +class AttributeView(EntityView): + """use this view on an entity as an alternative to more sophisticated + views such as reledit. + + Ex. usage: + + uicfg.primaryview_display_ctrl.tag_attribute(('Foo', 'bar'), {'vid': 'attribute'}) + """ + __regid__ = 'attribute' + __select__ = EntityView.__select__ & match_kwargs('rtype') + + def cell_call(self, row, col, rtype, **kwargs): + entity = self.cw_rset.get_entity(row, col) + if self._cw.vreg.schema.rschema(rtype).final: + self.w(entity.printable_value(rtype)) + else: + dispctrl = uicfg.primaryview_display_ctrl.etype_get( + entity.e_schema, rtype, kwargs['role'], '*') + rset = entity.related(rtype, role) + if rset: + self.wview('autolimited', rset, initargs={'dispctrl': dispctrl}) + ## default primary ui configuration ########################################### diff -r e7e9d73d0c07 -r 62e25fac41cd web/views/reledit.py --- a/web/views/reledit.py Wed Sep 15 11:13:17 2010 +0200 +++ b/web/views/reledit.py Wed Sep 15 15:00:04 2010 +0200 @@ -21,6 +21,7 @@ import copy from logilab.mtconverter import xml_escape +from logilab.common.deprecation import deprecated from cubicweb import neg_role from cubicweb.schema import display_name @@ -39,8 +40,8 @@ return u'' def append_field(self, *args): pass - def field_by_name(self, rtype, role, eschema=None): - return None + +rctrl = uicfg.reledit_ctrl class ClickAndEditFormView(EntityView): __regid__ = 'doreledit' @@ -60,8 +61,11 @@ _editzonemsg = _('click to edit this field') # default relation vids according to cardinality + # can be changed per rtype using reledit_ctrl rtag _one_rvid = 'incontext' _many_rvid = 'csv' + # renderer + _form_renderer_id = 'base' def cell_call(self, row, col, rtype=None, role='subject', reload=False, # controls reloading the whole page after change @@ -69,89 +73,98 @@ # function taking the subject entity & returning a boolean or an eid rvid=None, # vid to be applied to other side of rtype (non final relations only) default_value=None, - formid=None + formid='base' ): """display field to edit entity's `rtype` relation on click""" assert rtype assert role in ('subject', 'object'), '%s is not an acceptable role value' % role - if self.__regid__ == 'doreledit': - assert formid - self._cw.add_js('cubicweb.reledit.js') - if formid: - self._cw.add_js('cubicweb.edition.js') self._cw.add_css('cubicweb.form.css') + self._cw.add_js('cubicweb.reledit.js', 'cubicweb.edition.js') entity = self.cw_rset.get_entity(row, col) rschema = self._cw.vreg.schema[rtype] + self._rules = rctrl.etype_get(entity.e_schema.type, rschema.type, role, '*') reload = self._compute_reload(entity, rschema, role, reload) default_value = self._compute_default_value(entity, rschema, role, default_value) divid = self._build_divid(rtype, role, entity.eid) if rschema.final: - self._handle_attributes(entity, rschema, role, divid, reload, default_value) + self._handle_attribute(entity, rschema, role, divid, reload, default_value) else: - self._handle_relations(entity, rschema, role, divid, reload, default_value, formid) + if self._is_composite(): + self._handle_composite(entity, rschema, role, divid, reload, default_value, formid) + else: + self._handle_relation(entity, rschema, role, divid, reload, default_value, formid) - def _handle_attributes(self, entity, rschema, role, divid, reload, default_value): + def _handle_attribute(self, entity, rschema, role, divid, reload, default_value): rtype = rschema.type value = entity.printable_value(rtype) - form, renderer = self._build_form(entity, rtype, role, divid, 'base', - default_value, reload) - if not self._should_edit_attribute(entity, rschema, form): + if not self._should_edit_attribute(entity, rschema): self.w(value) return + + display_label, related_entity = self._prepare_form(entity, rtype, role) + form, renderer = self._build_form(entity, rtype, role, divid, 'base', default_value, + reload, display_label, related_entity) value = value or default_value - field = form.field_by_name(rtype, role, entity.e_schema) - form.append_field(field) self.view_form(divid, value, form, renderer) - def _handle_relations(self, entity, rschema, role, divid, reload, default_value, formid): - rtype = rschema.type - rvid = self._compute_best_vid(entity.e_schema, rschema, role) - related_rset = entity.related(rtype, role) + def _compute_formid_value(self, entity, rschema, role, default_value, rvid, formid): + related_rset = entity.related(rschema.type, role) if related_rset: value = self._cw.view(rvid, related_rset) else: value = default_value - ttypes = self._compute_ttypes(rschema, role) + if not self._should_edit_relation(entity, rschema, role): + return None, value + return formid, value + + def _handle_relation(self, entity, rschema, role, divid, reload, default_value, formid): + rvid = self._compute_best_vid(entity.e_schema, rschema, role) + formid, value = self._compute_formid_value(entity, rschema, role, default_value, rvid, formid) + if formid is None: + return self.w(value) - if not self._should_edit_relation(entity, rschema, role): - self.w(value) - return - # this is for attribute-like composites (1 target type, 1 related entity at most) + rtype = rschema.type + display_label, related_entity = self._prepare_form(entity, rtype, role) + form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, reload, + display_label, related_entity, dict(vid=rvid)) + self.view_form(divid, value, form, renderer) + + def _handle_composite(self, entity, rschema, role, divid, reload, default_value, formid): + # this is for attribute-like composites (1 target type, 1 related entity at most, for now) + ttypes = self._compute_ttypes(rschema, role) + related_rset = entity.related(rschema.type, role) add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes) edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes) delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role) - # compute formid - if len(ttypes) > 1: # redundant safety belt - formid = 'base' - else: - afs = uicfg.autoform_section.etype_get(entity.e_schema, rschema, role, ttypes[0]) - # is there an afs spec that says we should edit - # the rschema as an attribute ? - if afs and 'main_attributes' in afs: - formid = 'base' + + rvid = self._compute_best_vid(entity.e_schema, rschema, role) + formid, value = self._compute_formid_value(entity, rschema, role, default_value, rvid, formid) + if formid is None or not (edit_related or add_related): + # till we learn to handle cases where not (edit_related or add_related) + self.w(value) + return - form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, - reload, dict(vid=rvid), - edit_related, add_related and ttypes[0]) - if formid == 'base': - field = form.field_by_name(rtype, role, entity.e_schema) - form.append_field(field) - self.view_form(divid, value, form, renderer, edit_related, - delete_related, add_related) - + rtype = rschema.type + ttype = ttypes[0] + _fdata = self._prepare_composite_form(entity, rtype, role, edit_related, add_related and ttype) + display_label, related_entity = _fdata + form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, reload, + display_label, related_entity, dict(vid=rvid)) + self.view_form(divid, value, form, renderer, + edit_related, add_related, delete_related) def _compute_best_vid(self, eschema, rschema, role): + rvid = self._one_rvid if eschema.rdef(rschema, role).role_cardinality(role) in '+*': - return self._many_rvid - return self._one_rvid + rvid = self._many_rvid + return self._rules.get('rvid', rvid) def _compute_ttypes(self, rschema, role): dual_role = neg_role(role) return getattr(rschema, '%ss' % dual_role)() def _compute_reload(self, entity, rschema, role, reload): - rule = uicfg.reledit_ctrl.etype_get(entity.e_schema.type, rschema.type, role, '*') - ctrl_reload = rule.get('reload', reload) + ctrl_reload = self._rules.get('reload', reload) if callable(ctrl_reload): ctrl_reload = ctrl_reload(entity) if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False @@ -160,8 +173,7 @@ def _compute_default_value(self, entity, rschema, role, default_value): etype = entity.e_schema.type - rule = uicfg.reledit_ctrl.etype_get(etype, rschema.type, role, '*') - ctrl_default = rule.get('default_value', default_value) + ctrl_default = self._rules.get('default_value', default_value) if ctrl_default: return ctrl_default if default_value is None: @@ -169,47 +181,42 @@ display_name(self._cw, rschema.type, role)) return default_value - def _is_composite(self, eschema, rschema, role): - return eschema.rdef(rschema, role).composite == role + def _is_composite(self): + return self._rules.get('edit_target') == 'related' def _may_add_related(self, related_rset, entity, rschema, role, ttypes): """ ok for attribute-like composite entities """ - if self._is_composite(entity.e_schema, rschema, role): - if len(ttypes) > 1: # wrong cardinality: do not handle - return False - rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role) - card = rdef.role_cardinality(role) - if related_rset and card in '?1': - return False - if role == 'subject': - kwargs = {'fromeid': entity.eid} - else: - kwargs = {'toeid': entity.eid} - if rdef.has_perm(self._cw, 'add', **kwargs): - return True - return False + if len(ttypes) > 1: # many etypes: learn how to do it + return False + rdef = rschema.role_rdef(entity.e_schema, ttypes[0], role) + card = rdef.role_cardinality(role) + if related_rset or card not in '?1': + return False + if role == 'subject': + kwargs = {'fromeid': entity.eid} + else: + kwargs = {'toeid': entity.eid} + return rdef.has_perm(self._cw, 'add', **kwargs) def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes): """ controls the edition of the related entity """ - if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1': - return False - if len(related_rset.rows) != 1: + if len(ttypes) > 1 or len(related_rset.rows) != 1: return False - if len(ttypes) > 1: - return False - if not self._is_composite(entity.e_schema, rschema, role): + if entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1': return False return related_rset.get_entity(0, 0).cw_has_perm('update') def _may_delete_related(self, related_rset, entity, rschema, role): - # we assume may_edit_related - kwargs = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid} - if not rschema.has_perm(self._cw, 'delete', **kwargs): + # we assume may_edit_related, only 1 related entity + if not related_rset: return False - for related_entity in related_rset.entities(): - if not related_entity.cw_has_perm('delete'): - return False - return True + rentity = related_rset.get_entity(0, 0) + if role == 'subject': + kwargs = {'fromeid': entity.eid, 'toeid': rentity.eid} + else: + kwargs = {'fromeid': rentity.eid, 'toeid': entity.eid} + # NOTE: should be sufficient given a well built schema/security + return rschema.has_perm(self._cw, 'delete', **kwargs) def _build_edit_zone(self): return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))} @@ -234,32 +241,43 @@ event_args.update(extradata) return event_args - def _build_form(self, entity, rtype, role, divid, formid, default_value, reload, - extradata=None, edit_related=False, add_related=False, **formargs): - event_args = self._build_args(entity, rtype, role, formid, default_value, - reload, extradata) - cancelclick = self._cancelclick % divid + def _prepare_form(self, entity, _rtype, role): + display_label = False + related_entity = entity + return display_label, related_entity + + def _prepare_composite_form(self, entity, rtype, role, edit_related, add_related): if edit_related and not add_related: - display_fields = None display_label = True related_entity = entity.related(rtype, role).get_entity(0, 0) - self._cw.form['eid'] = related_entity.eid elif add_related: - display_fields = None display_label = True _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw) _new_entity.eid = self._cw.varmaker.next() related_entity = _new_entity + # XXX see forms.py ~ 276 and entities.linked_to method + # is there another way ? self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role)) - else: # base case: edition/attribute relation - display_fields = [(rtype, role)] - display_label = False - related_entity = entity + return display_label, related_entity + + def _build_renderer(self, related_entity, display_label): + return self._cw.vreg['formrenderers'].select( + self._form_renderer_id, self._cw, entity=related_entity, + display_label=display_label, + table_class='attributeForm' if display_label else '', + display_help=False, button_bar_class='buttonbar', + display_progress_div=False) + + def _build_form(self, entity, rtype, role, divid, formid, default_value, reload, + display_label, related_entity, extradata=None, **formargs): + event_args = self._build_args(entity, rtype, role, formid, default_value, + reload, extradata) + cancelclick = self._cancelclick % divid form = self._cw.vreg['forms'].select( - formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, domid='%s-form' % divid, - display_fields=display_fields, formtype='inlined', - action=self._cw.build_url('validateform?__onsuccess=window.parent.cw.reledit.onSuccess'), - cwtarget='eformframe', cssstyle='display: none', + formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, + domid='%s-form' % divid, formtype='inlined', + action=self._cw.build_url('validateform', __onsuccess='window.parent.cw.reledit.onSuccess'), + cwtarget='eformframe', cssclass='releditForm', **formargs) # pass reledit arguments for pname, pvalue in event_args.iteritems(): @@ -279,53 +297,38 @@ form.form_buttons = [SubmitButton(), Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)] form.event_args = event_args - renderer = self._cw.vreg['formrenderers'].select( - 'base', self._cw, entity=related_entity, display_label=display_label, - display_help=False, table_class='', - button_bar_class='buttonbar', display_progress_div=False) - return form, renderer + if formid == 'base': + field = form.field_by_name(rtype, role, entity.e_schema) + form.append_field(field) + return form, self._build_renderer(related_entity, display_label) - def _should_edit_attribute(self, entity, rschema, form): - # examine rtags - noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rschema.type, 'subject').get('noedit', False) - if noedit: - return False + def _should_edit_attribute(self, entity, rschema): rdef = entity.e_schema.rdef(rschema) - afs = uicfg.autoform_section.etype_get(entity.__regid__, rschema, 'subject', rdef.object) - if 'main_hidden' in afs: - return False # check permissions if not entity.cw_has_perm('update'): return False rdef = entity.e_schema.rdef(rschema) - if not rdef.has_perm(self._cw, 'update', eid=entity.eid): - return False - # XXX ? - try: - form.field_by_name(str(rschema), 'subject', entity.e_schema) - except FieldNotFound: - return False - return True + return rdef.has_perm(self._cw, 'update', eid=entity.eid) + + should_edit_attributes = deprecated('[3.9] should_edit_attributes is deprecated,' + ' use _should_edit_attribute instead', + _should_edit_attribute) def _should_edit_relation(self, entity, rschema, role): # examine rtags rtype = rschema.type - noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rtype, role).get('noedit', False) - if noedit: - return False rdef = entity.e_schema.rdef(rschema, role) - afs = uicfg.autoform_section.etype_get( - entity.__regid__, rschema, role, rdef.object) - if 'main_hidden' in afs: - return False perm_args = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid} return rschema.has_perm(self._cw, 'add', **perm_args) - def view_form(self, divid, value, form=None, renderer=None, - edit_related=False, delete_related=False, add_related=False): + should_edit_relations = deprecated('[3.9] should_edit_relations is deprecated,' + ' use _should_edit_relation instead', + _should_edit_relation) + + def _open_form_wrapper(self, divid, value, form, renderer): w = self.w - w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s">' % - {'id': divid, + w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s" class="%(css)s">' % + {'id': divid, 'css': 'releditField', 'out': "jQuery('#%s').addClass('hidden')" % divid, 'over': "jQuery('#%s').removeClass('hidden')" % divid}) w(u'<div id="%s-value" class="editableFieldValue">' % divid) @@ -333,15 +336,24 @@ w(u'</div>') w(form.render(renderer=renderer)) w(u'<div id="%s" class="editableField hidden">' % divid) + + def _close_form_wrapper(self): + self.w(u'</div>') + self.w(u'</div>') + + def view_form(self, divid, value, form=None, renderer=None, + edit_related=False, add_related=False, delete_related=False): + self._open_form_wrapper(divid, value, form, renderer) + w = self.w args = form.event_args.copy() - if not add_related: # excludes edition - args['formid'] = 'edition' + if not add_related: # currently, excludes edition + args['formid'] = 'edition' if edit_related else 'base' w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' % (divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg))) w(self._build_edit_zone()) w(u'</div>') else: - args['formid'] = 'edition' + args['formid'] = 'edition' if add_related else 'base' w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' % (divid, xml_escape(self._onclick % args), self._cw._(self._addmsg))) w(self._build_add_zone()) @@ -352,14 +364,14 @@ (divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg))) w(self._build_delete_zone()) w(u'</div>') - w(u'</div>') - w(u'</div>') + self._close_form_wrapper() + class AutoClickAndEditFormView(ClickAndEditFormView): __regid__ = 'reledit' def _build_form(self, entity, rtype, role, divid, formid, default_value, reload, - extradata=None, edit_related=False, add_related=False, **formargs): + display_label, related_entity, extradata=None, **formargs): event_args = self._build_args(entity, rtype, role, 'base', default_value, reload, extradata) form = _DummyForm()