# HG changeset patch # User Sylvain Thénault # Date 1253872187 -7200 # Node ID 6e927b729ae1f8aa9e2819b13c23d2f4af89ed80 # Parent 9c07e6c48e35517cb79ebfb529ef248542ad0475 [uicfg, autoform] more consistent/powerful autoform_section rtags by using formtype/section; deprecates autoform_is_inlined; refactor automatci form and renderer thanks to this diff -r 9c07e6c48e35 -r 6e927b729ae1 devtools/devctl.py --- a/devtools/devctl.py Fri Sep 25 11:30:59 2009 +0200 +++ b/devtools/devctl.py Fri Sep 25 11:49:47 2009 +0200 @@ -124,19 +124,19 @@ if libconfig is not None: from cubicweb.cwvreg import CubicWebVRegistry, clear_rtag_objects libschema = libconfig.load_schema(remove_unused_rtypes=False) - rinlined = deepcopy(uicfg.autoform_is_inlined) + afs = deepcopy(uicfg.autoform_section) appearsin_addmenu = deepcopy(uicfg.actionbox_appearsin_addmenu) clear_rtag_objects() cleanup_sys_modules(libconfig) libvreg = CubicWebVRegistry(libconfig) libvreg.set_schema(libschema) # trigger objects registration - librinlined = uicfg.autoform_is_inlined + libafs = uicfg.autoform_section libappearsin_addmenu = uicfg.actionbox_appearsin_addmenu # prefill vregdone set list(_iter_vreg_objids(libvreg, vregdone)) else: libschema = {} - rinlined = uicfg.autoform_is_inlined + afs = uicfg.autoform_section appearsin_addmenu = uicfg.actionbox_appearsin_addmenu done = set() for eschema in sorted(schema.entities()): @@ -154,9 +154,11 @@ continue for rschema, targetschemas, role in eschema.relation_definitions(True): for tschema in targetschemas: - if rinlined.etype_get(eschema, rschema, role, tschema) and \ + fsections = afs.etype_get(eschema, rschema, role, tschema) + if 'inlined_attributes' in fsections and \ (libconfig is None or not - librinlined.etype_get(eschema, rschema, role, tschema)): + 'inlined_attributes' in libafs.etype_get( + eschema, rschema, role, tschema)): add_msg(w, 'add a %s' % tschema, 'inlined:%s.%s.%s' % (etype, rschema, role)) add_msg(w, 'remove this %s' % tschema, diff -r 9c07e6c48e35 -r 6e927b729ae1 web/uicfg.py --- a/web/uicfg.py Fri Sep 25 11:30:59 2009 +0200 +++ b/web/uicfg.py Fri Sep 25 11:49:47 2009 +0200 @@ -57,8 +57,44 @@ simple boolean relation tags used to control the "add entity" submenu. Relations whose rtag is True will appears, other won't. + Automatic form configuration ```````````````````````````` +:autoform_section: + where to display a relation in entity form, according to form type. + `tag_attribute`, `tag_subject_of` and `tag_object_of` methods for this + relation tags expect two arguments additionaly to the relation key: a + `formtype` and a `section`. + + formtype may be one of: + * 'main', the main entity form + * 'inlined', the form for an entity inlined into another's one + * 'muledit', the multiple entity (table) form + + section may be one of: + * 'hidden', don't display + * 'attributes', display in the attributes section + * 'relations', display in the relations section, using the generic relation + selector combobox (available in main form only, and not for attribute + relation) + * 'metadata', display in a special metadata form (NOT YET IMPLEMENTED, + subject to changes) + +:autoform_field: + specify a custom field instance to use for a relation + +:autoform_field_kwargs: + + specify a dictionnary of arguments to give to the field constructor for a + relation. You usually want to use either `autoform_field` or + `autoform_field_kwargs`, not both. The later won't have any effect if the + former is specified for a relation. + +:autoform_permissions_overrides: + + provide a way to by-pass security checking for dark-corner case where it can't + be verified properly. XXX documents. + :organization: Logilab :copyright: 2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. @@ -67,9 +103,12 @@ """ __docformat__ = "restructuredtext en" +from warnings import warn + from cubicweb import neg_role -from cubicweb.rtags import (RelationTags, RelationTagsBool, - RelationTagsSet, RelationTagsDict, register_rtag) +from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet, + RelationTagsDict, register_rtag, _ensure_str_key) +from cubicweb.schema import META_RTYPES from cubicweb.web import formwidgets @@ -144,6 +183,7 @@ primaryview_display_ctrl = DisplayCtrlRelationTags('primaryview_display_ctrl', init_primaryview_display_ctrl) + # index view configuration #################################################### # entity type section in the index/manage page. May be one of # * 'application' @@ -176,38 +216,182 @@ # autoform.AutomaticEntityForm configuration ################################## -# relations'section (eg primary/secondary/generic/metadata/generated) +def _formsections_as_dict(formsections): + result = {} + for formsection in formsections: + formtype, section = formsection.split('_', 1) + result[formtype] = section + return result + +def _card_and_comp(sschema, rschema, oschema, role): + if role == 'subject': + card = rschema.rproperty(sschema, oschema, 'cardinality')[0] + composed = rschema.rproperty(sschema, oschema, 'composite') == 'object' + else: + card = rschema.rproperty(sschema, oschema, 'cardinality')[1] + composed = rschema.rproperty(sschema, oschema, 'composite') == 'subject' + return card, composed + +class AutoformSectionRelationTags(RelationTagsSet): + """autoform relations'section""" -def init_autoform_section(rtag, sschema, rschema, oschema, role): - if rtag.get(sschema, rschema, oschema, role) is None: - if autoform_is_inlined.get(sschema, rschema, oschema, role) or \ - autoform_is_inlined.get(sschema, rschema, oschema, neg_role(role)): - section = 'generated' - elif sschema.is_metadata(rschema): - section = 'metadata' - else: - if role == 'subject': - card = rschema.rproperty(sschema, oschema, 'cardinality')[0] - composed = rschema.rproperty(sschema, oschema, 'composite') == 'object' + bw_tag_map = { + 'primary': {'main': 'attributes', 'muledit': 'attributes'}, + 'secondary': {'main': 'attributes', 'muledit': 'hidden'}, + 'metadata': {'main': 'metadata'}, + 'generic': {'main': 'relations'}, + 'generated': {'main': 'hidden'}, + } + + _allowed_form_types = ('main', 'inlined', 'muledit') + _allowed_values = {'main': ('attributes', 'relations', 'metadata', 'hidden'), + 'inlined': ('attributes', 'hidden'), + 'muledit': ('attributes', 'hidden'), + } + + @staticmethod + def _initfunc(self, sschema, rschema, oschema, role): + formsections = self.init_get(sschema, rschema, oschema, role) + if formsections is None: + formsections = self.tag_container_cls() + sectdict = _formsections_as_dict(formsections) + if rschema in META_RTYPES: + sectdict.setdefault('main', 'hidden') + sectdict.setdefault('muledit', 'hidden') + sectdict.setdefault('inlined', 'hidden') + # ensure we have a tag for each form type + if not 'main' in sectdict: + if not rschema.is_final() and ( + sectdict.get('inlined') == 'attributes' or + 'inlined_attributes' in self.init_get(sschema, rschema, oschema, + neg_role(role))): + sectdict['main'] = 'hidden' + elif sschema.is_metadata(rschema): + sectdict['main'] = 'metadata' else: - card = rschema.rproperty(sschema, oschema, 'cardinality')[1] - composed = rschema.rproperty(sschema, oschema, 'composite') == 'subject' - if card in '1+': - if not rschema.is_final() and composed: - # XXX why? probably because we want it unlined, though this - # is not the case by default - section = 'generated' + card, composed = _card_and_comp(sschema, rschema, oschema, role) + if card in '1+': + if not rschema.is_final() and composed: + # XXX why? probably because we want it unlined, though + # this is not the case by default + sectdict['main'] = 'hidden' + else: + sectdict['main'] = 'attributes' + if not 'muledit' in sectdict: + sectdict['muledit'] = 'attributes' + elif rschema.is_final(): + sectdict['main'] = 'attributes' else: - section = 'primary' - elif rschema.is_final(): - section = 'secondary' - else: - section = 'generic' - rtag.tag_relation((sschema, rschema, oschema, role), section) + sectdict['main'] = 'relations' + if not 'muledit' in sectdict: + sectdict['muledit'] = 'hidden' + if sectdict['main'] == 'attributes': + card, composed = _card_and_comp(sschema, rschema, oschema, role) + if card in '1+' and not composed: + sectdict['muledit'] = 'attributes' + if not 'inlined' in sectdict: + sectdict['inlined'] = 'hidden' + # recompute formsections and set it to avoid recomputing + for formtype, section in sectdict.iteritems(): + formsections.add('%s_%s' % (formtype, section)) + key = _ensure_str_key( (sschema, rschema, oschema, role) ) + self._tagdefs[key] = formsections + + def tag_relation(self, key, formtype, section=None): + if section is None: + tag = formtype + for formtype, section in self.bw_tag_map[tag].iteritems(): + warn('[3.6] add tag to autoform section by specifying form ' + 'type and tag. Replace %s by formtype=%s, section=%s' + % (tag, formtype, section), DeprecationWarning, stacklevel=2) + self.tag_relation(key, formtype, section) + assert formtype in self._allowed_form_types, \ + 'formtype should be in (%s), not %s' % ( + ','.join(self._allowed_form_types), formtype) + assert section in self._allowed_values[formtype], \ + 'section for %s should be in (%s), not %s' % ( + formtype, ','.join(self._allowed_values[formtype]), section) + rtags = self._tagdefs.setdefault(_ensure_str_key(key), + self.tag_container_cls()) + # remove previous section for this form type if any + if rtags: + for tag in rtags.copy(): + if tag.startswith(formtype): + rtags.remove(tag) + rtags.add('%s_%s' % (formtype, section)) + return rtags + + def init_get(self, *key): + return super(AutoformSectionRelationTags, self).get(*key) + + def get(self, *key): + # overriden to avoid recomputing done in parent classes + return self._tagdefs[key] + + def relations_by_section(self, entity, formtype, section, + permission=None, strict=False): + """return a list of (relation schema, target schemas, role) for the + given entity matching categories and permission. -autoform_section = RelationTags('autoform_section', init_autoform_section, - set(('primary', 'secondary', 'generic', - 'metadata', 'generated'))) + `strict`: + bool telling if having local role is enough (strict = False) or not + """ + tag = '%s_%s' % (formtype, section) + eschema = entity.e_schema + permsoverrides = autoform_permissions_overrides + if entity.has_eid(): + eid = entity.eid + else: + eid = None + strict = False + for rschema, targetschemas, role in eschema.relation_definitions(True): + # check category first, potentially lower cost than checking + # permission which may imply rql queries + if tag is not None: + targetschemas = [tschema for tschema in targetschemas + if tag in self.etype_get(eschema, rschema, + role, tschema)] + 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_get(eschema, rschema, role): + yield (rschema, targetschemas, role) + continue + if rschema.is_final(): + if not rschema.has_perm(entity._cw, permission, eid): + continue + elif role == 'subject': + if not ((not strict and rschema.has_local_role(permission)) or + rschema.has_perm(entity._cw, 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 entity.related(rschema.type, role) + and not rschema.has_perm(entity._cw, 'delete', fromeid=eid, + toeid=entity.related(rschema.type, role)[0][0])): + continue + elif role == 'object': + if not ((not strict and rschema.has_local_role(permission)) or + rschema.has_perm(entity._cw, 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 entity.related(rschema.type, role) + and not rschema.has_perm(entity._cw, 'delete', toeid=eid, + fromeid=entity.related(rschema.type, role)[0][0])): + continue + yield (rschema, targetschemas, role) + + + +autoform_section = AutoformSectionRelationTags('autoform_section') # relations'field class autoform_field = RelationTags('autoform_field') @@ -215,11 +399,6 @@ # relations'field explicit kwargs (given to field's __init__) autoform_field_kwargs = RelationTagsDict() -# inlined view flag for non final relations: when True for an entry, the -# entity(ies) at the other end of the relation will be editable from the -# form of the edited entity -autoform_is_inlined = RelationTagsBool('autoform_is_inlined') - # 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 @@ -238,3 +417,20 @@ actionbox_appearsin_addmenu = RelationTagsBool('actionbox_appearsin_addmenu', init_actionbox_appearsin_addmenu) + + +# deprecated ################################################################### + +class AutoformIsInlined(RelationTags): + """XXX for < 3.6 bw compat""" + def tag_relation(self, key, tag): + warn('autoform_is_inlined rtag is deprecated, use autoform_section ' + 'with inlined formtype and "attributes" or "hidden" section', + DeprecationWarning, stacklevel=2) + section = tag and 'attributes' or 'hidden' + autoform_section.tag_relation(key, 'inlined', section) + +# inlined view flag for non final relations: when True for an entry, the +# entity(ies) at the other end of the relation will be editable from the +# form of the edited entity +autoform_is_inlined = AutoformIsInlined('autoform_is_inlined') diff -r 9c07e6c48e35 -r 6e927b729ae1 web/views/actions.py --- a/web/views/actions.py Fri Sep 25 11:30:59 2009 +0200 +++ b/web/views/actions.py Fri Sep 25 11:49:47 2009 +0200 @@ -18,7 +18,6 @@ from cubicweb.web import uicfg, controller from cubicweb.web.action import Action from cubicweb.web.views import linksearch_select_url, vid_from_rset -from cubicweb.web.views.autoform import AutomaticEntityForm class has_editable_relation(EntitySelector): @@ -31,11 +30,17 @@ def score_entity(self, entity): # if user has no update right but it can modify some relation, # display action anyway - for dummy in AutomaticEntityForm.esrelations_by_category( - entity, 'generic', 'add', strict=True): + form = self._cw.vreg['forms'].select('edition', self._cw, + entity=entity) + for dummy in form.editable_relations(): return 1 - for rschema, targetschemas, role in AutomaticEntityForm.erelations_by_category( - entity, ('primary', 'secondary'), 'add', strict=True): + try: + editableattrs = form.editable_attributes(strict=True) + except TypeError: + warn('[3.6] %s: editable_attributes now take strict=False as ' + 'optional argument', DeprecationWarning) + editableattrs = form.editable_attributes() + for rschema, targetschemas, role in editableattrs: if not rschema.is_final(): return 1 return 0 diff -r 9c07e6c48e35 -r 6e927b729ae1 web/views/autoform.py --- a/web/views/autoform.py Fri Sep 25 11:30:59 2009 +0200 +++ b/web/views/autoform.py Fri Sep 25 11:49:47 2009 +0200 @@ -16,18 +16,19 @@ from cubicweb.web.formfields import guess_field from cubicweb.web.views import forms, editforms +_afs = uicfg.autoform_section class AutomaticEntityForm(forms.EntityFieldsForm): """base automatic form to edit any entity. Designed to be fully generated from schema but highly configurable through: - * rtags (rcategories, rfields, rwidgets, inlined, rpermissions) + + * uicfg (autoform_* relation tags) * various standard form parameters - - XXX s/rtags/uicfg/ ? + * overriding You can also easily customise it by adding/removing fields in - AutomaticEntityForm instances. + AutomaticEntityForm instances or by inheriting from it. """ __regid__ = 'edition' @@ -37,100 +38,18 @@ form_buttons = [fwdgs.SubmitButton(), fwdgs.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'), fwdgs.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')] - attrcategories = ('primary', 'secondary') + # for attributes selection when searching in uicfg.autoform_section + formtype = 'main' + # set this to a list of [(relation, role)] if you want to explictily tell + # which relations should be edited + display_fields = None # class attributes below are actually stored in the uicfg module since we # don't want them to be reloaded - rcategories = uicfg.autoform_section rfields = uicfg.autoform_field rfields_kwargs = uicfg.autoform_field_kwargs - rinlined = uicfg.autoform_is_inlined - rpermissions_overrides = uicfg.autoform_permissions_overrides # class methods mapping schema relations to fields in the form ############ - @classmethod - def erelations_by_category(cls, entity, categories=None, permission=None, - rtags=None, strict=False): - """return a list of (relation schema, target schemas, role) matching - categories and permission - - `strict`: - bool telling if having local role is enough (strict = False) or not - """ - 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 = entity.e_schema - if rtags is None: - rtags = cls.rcategories - permsoverrides = cls.rpermissions_overrides - if entity.has_eid(): - eid = entity.eid - else: - eid = None - strict = False - for rschema, targetschemas, role in eschema.relation_definitions(True): - # 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_get(eschema, rschema, role, 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_get(eschema, rschema, role): - yield (rschema, targetschemas, role) - continue - if rschema.is_final(): - if not rschema.has_perm(entity._cw, permission, eid): - continue - elif role == 'subject': - if not ((not strict and rschema.has_local_role(permission)) or - rschema.has_perm(entity._cw, 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 entity.related(rschema.type, role) - and not rschema.has_perm(entity._cw, 'delete', fromeid=eid, - toeid=entity.related(rschema.type, role)[0][0])): - continue - elif role == 'object': - if not ((not strict and rschema.has_local_role(permission)) or - rschema.has_perm(entity._cw, 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 entity.related(rschema.type, role) - and not rschema.has_perm(entity._cw, 'delete', toeid=eid, - fromeid=entity.related(rschema.type, role)[0][0])): - continue - yield (rschema, targetschemas, role) - - @classmethod - def esrelations_by_category(cls, entity, categories=None, permission=None, - strict=False): - """filter out result of relations_by_category(categories, permission) by - removing final relations - - return a sorted list of (relation's label, relation'schema, role) - """ - result = [] - for rschema, ttypes, role in cls.erelations_by_category( - entity, categories, permission, strict=strict): - if rschema.is_final(): - continue - result.append((rschema.display_name(entity._cw, role), rschema, role)) - return sorted(result) - @iclassmethod def field_by_name(cls_or_self, name, role='subject', eschema=None): """return field with the given name and role. If field is not explicitly @@ -167,14 +86,14 @@ entity = self.edited_entity if entity.has_eid(): entity.complete() - for rschema, role in self.editable_attributes(): + for rtype, role in self.editable_attributes(): try: - self.field_by_name(rschema.type, role) + self.field_by_name(str(rtype), role) continue # explicitly specified except form.FieldNotFound: # has to be guessed try: - field = self.field_by_name(rschema.type, role, + field = self.field_by_name(str(rtype), role, eschema=entity.e_schema) self.fields.append(field) except form.FieldNotFound: @@ -234,34 +153,38 @@ # methods mapping edited entity relations to fields in the form ############ - def relations_by_category(self, categories=None, permission=None): + def _relations_by_section(self, section, permission='add', strict=False): """return a list of (relation schema, target schemas, role) matching given category(ies) and permission """ - return self.erelations_by_category(self.edited_entity, categories, - permission) + return _afs.relations_by_section( + self.edited_entity, self.formtype, section, permission, strict) + + def editable_attributes(self, strict=False): + """return a list of (relation schema, role) to edit for the entity""" + if self.display_fields is not None: + return self.display_fields + # XXX we should simply put eid in the generated section, no? + return [(rtype, role) for rtype, _, role in self._relations_by_section( + 'attributes', strict=strict) if rtype != 'eid'] + + def editable_relations(self): + """return a sorted list of (relation's label, relation'schema, role) for + relations in the 'relations' section + """ + result = [] + for rschema, _, role in self._relations_by_section('relations', + strict=True): + result.append( (rschema.display_name(entity._cw, role, + entity.__regid__), + rschema, role) ) + return sorted(result) def inlined_relations(self): """return a list of (relation schema, target schemas, role) matching given category(ies) and permission """ - return self.erelations_by_category(self.edited_entity, True, 'add', - self.rinlined) - - def srelations_by_category(self, categories=None, permission=None, - strict=False): - """filter out result of relations_by_category(categories, permission) by - removing final relations - - return a sorted list of (relation's label, relation'schema, role) - """ - return self.esrelations_by_category(self.edited_entity, categories, - permission, strict=strict) - - def editable_attributes(self): - """return a list of (relation schema, role) to edit for the entity""" - return [(rschema, role) for rschema, _, role in self.relations_by_category( - self.attrcategories, 'add') if rschema != 'eid'] + return self._relations_by_section('inlined') # generic relations modifier ############################################### @@ -275,8 +198,7 @@ """ entity = self.edited_entity pending_deletes = self._cw.get_pending_deletes(entity.eid) - for label, rschema, role in self.srelations_by_category('generic', 'add', - strict=True): + 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 diff -r 9c07e6c48e35 -r 6e927b729ae1 web/views/editforms.py --- a/web/views/editforms.py Fri Sep 25 11:30:59 2009 +0200 +++ b/web/views/editforms.py Fri Sep 25 11:49:47 2009 +0200 @@ -105,10 +105,6 @@ __select__ = non_final_entity() & match_kwargs('rtype') # FIXME editableField class could be toggleable from userprefs - # add metadata to allow edition of metadata attributes (not considered by - # edition form by default) - attrcategories = ('primary', 'secondary', 'metadata') - _onclick = u"showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')" _defaultlandingzone = (u'