# HG changeset patch # User Adrien Di Mascio # Date 1319732340 -7200 # Node ID 552d85fcb587d390d120625ad38c8d7433ed1ba4 # Parent 805d4e121b656c8c409e32901ee91c1cde0bc2c6 [uicfg] add uihelper module with high-level helpers (closes #1809414) diff -r 805d4e121b65 -r 552d85fcb587 doc/book/en/devweb/rtags.rst --- a/doc/book/en/devweb/rtags.rst Thu Oct 27 10:38:16 2011 +0200 +++ b/doc/book/en/devweb/rtags.rst Thu Oct 27 18:19:00 2011 +0200 @@ -19,3 +19,9 @@ .. automodule:: cubicweb.web.uicfg + +The uihelper module +~~~~~~~~~~~~~~~~~~~ + +.. automodule:: cubicweb.web.uihelper + diff -r 805d4e121b65 -r 552d85fcb587 web/test/unittest_uicfg.py --- a/web/test/unittest_uicfg.py Thu Oct 27 10:38:16 2011 +0200 +++ b/web/test/unittest_uicfg.py Thu Oct 27 18:19:00 2011 +0200 @@ -15,9 +15,10 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . +import copy from logilab.common.testlib import tag from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.web import uicfg +from cubicweb.web import uicfg, uihelper, formwidgets as fwdgs abaa = uicfg.actionbox_appearsin_addmenu @@ -33,7 +34,9 @@ the more accurate apply""" def setUp(self): - + super(DefinitionOrderTC, self).setUp() + for rtag in (uicfg.autoform_section, uicfg.autoform_field_kwargs): + rtag._old_tagdefs = copy.deepcopy(rtag._tagdefs) new_def = ( (('*', 'login', '*'), {'formtype':'main', 'section':'hidden'}), @@ -48,15 +51,13 @@ (('CWUser', 'login', 'String'), {'formtype':'inlined', 'section':'attributes'}), ) - self._old_def = [] - for key, kwargs in new_def: - nkey = key[0], key[1], key[2], 'subject' - self._old_def.append((nkey, uicfg.autoform_section._tagdefs.get(nkey))) uicfg.autoform_section.tag_subject_of(key, **kwargs) - super(DefinitionOrderTC, self).setUp() - + def tearDown(self): + super(DefinitionOrderTC, self).tearDown() + for rtag in (uicfg.autoform_section, uicfg.autoform_field_kwargs): + rtag._tagdefs = rtag._old_tagdefs @tag('uicfg') def test_definition_order_hidden(self): @@ -64,18 +65,47 @@ expected = set(['main_inlined', 'muledit_attributes', 'inlined_attributes']) self.assertSetEqual(result, expected) - def tearDown(self): - super(DefinitionOrderTC, self).tearDown() - for key, tags in self._old_def: - if tags is None: - uicfg.autoform_section.del_rtag(*key) - else: - for tag in tags: - formtype, section = tag.split('_') - uicfg.autoform_section.tag_subject_of(key[:3], formtype=formtype, section=section) + @tag('uihelper', 'order', 'func') + def test_uihelper_set_fields_order(self): + afk_get = uicfg.autoform_field_kwargs.get + self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {}) + uihelper.set_fields_order('CWUser', ('login', 'firstname', 'surname')) + self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {'order': 1}) + + @tag('uihelper', 'kwargs', 'func') + def test_uihelper_set_field_kwargs(self): + afk_get = uicfg.autoform_field_kwargs.get + self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {}) + wdg = fwdgs.TextInput({'size': 30}) + uihelper.set_field_kwargs('CWUser', 'firstname', widget=wdg) + self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {'widget': wdg}) - uicfg.autoform_section.clear() - uicfg.autoform_section.init(self.repo.vreg.schema) + @tag('uihelper', 'hidden', 'func') + def test_uihelper_hide_fields(self): + # original conf : in_group is edited in 'attributes' section everywhere + section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject') + self.assertItemsEqual(section_conf, ['main_attributes', 'muledit_attributes']) + # hide field in main form + uihelper.hide_fields('CWUser', ('login', 'in_group')) + section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject') + self.assertItemsEqual(section_conf, ['main_hidden', 'muledit_attributes']) + # hide field in muledit form + uihelper.hide_fields('CWUser', ('login', 'in_group'), formtype='muledit') + section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject') + self.assertItemsEqual(section_conf, ['main_hidden', 'muledit_hidden']) + + @tag('uihelper', 'hidden', 'formconfig') + def test_uihelper_formconfig(self): + afk_get = uicfg.autoform_field_kwargs.get + class CWUserFormConfig(uihelper.FormConfig): + etype = 'CWUser' + hidden = ('in_group',) + fields_order = ('login', 'firstname') + section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject') + self.assertItemsEqual(section_conf, ['main_hidden', 'muledit_attributes']) + self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {'order': 1}) + + if __name__ == '__main__': from logilab.common.testlib import unittest_main diff -r 805d4e121b65 -r 552d85fcb587 web/uihelper.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/uihelper.py Thu Oct 27 18:19:00 2011 +0200 @@ -0,0 +1,335 @@ +# copyright 2011 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 . +"""This module provide highlevel helpers to avoid uicfg boilerplate +for most common tasks such as fields ordering, widget customization, etc. + + +Here are a few helpers to customize *action box* rendering: + +.. autofunction:: cubicweb.web.uihelper.append_to_addmenu +.. autofunction:: cubicweb.web.uihelper.remove_from_addmenu + + +and a few other ones for *form configuration*: + +.. autofunction:: cubicweb.web.uihelper.set_fields_order +.. autofunction:: cubicweb.web.uihelper.hide_field +.. autofunction:: cubicweb.web.uihelper.hide_fields +.. autofunction:: cubicweb.web.uihelper.set_field_kwargs +.. autofunction:: cubicweb.web.uihelper.set_field +.. autofunction:: cubicweb.web.uihelper.edit_inline +.. autofunction:: cubicweb.web.uihelper.edit_as_attr +.. autofunction:: cubicweb.web.uihelper.set_muledit_editable + +The module also provides a :class:`FormConfig` base class that lets you gather +uicfg declaration in the scope of a single class, which can sometimes +be clearer to read than a bunch of sequential function calls. + +.. autoclass:: cubicweb.web.uihelper.FormConfig + +""" +__docformat__ = "restructuredtext en" + +from cubicweb.web import uicfg +from functools import partial + +def _tag_rel(rtag, etype, attr, desttype='*', *args, **kwargs): + if isinstance(attr, basestring): + attr, role = attr, 'subject' + else: + attr, role = attr + if role == 'subject': + rtag.tag_subject_of((etype, attr, desttype), *args, **kwargs) + else: + rtag.tag_object_of((desttype, attr, etype), *args, **kwargs) + + +## generic uicfg helpers ###################################################### +def append_to_addmenu(etype, attr, createdtype='*'): + """adds `attr` in the actions box *addrelated* submenu of `etype`. + + :param etype: the entity type as a string + :param attr: the name of the attribute or relation to hide + + `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) + + """ + _tag_rel(uicfg.actionbox_appearsin_addmenu, etype, attr, createdtype, True) + +def remove_from_addmenu(etype, attr, createdtype='*'): + """removes `attr` from the actions box *addrelated* submenu of `etype`. + + :param etype: the entity type as a string + :param attr: the name of the attribute or relation to hide + + `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) + """ + _tag_rel(uicfg.actionbox_appearsin_addmenu, etype, attr, createdtype, False) + + +## form uicfg helpers ########################################################## +def set_fields_order(etype, attrs): + """specify the field order in `etype` main edition form. + + :param etype: the entity type as a string + :param attrs: the ordered list of attribute names (or relations) + + `attrs` can be strings or 2-tuples (relname, role_of_etype_in_the_relation) + + Unspecified fields will be displayed after specified ones, their + order being consistent with the schema definition. + + Examples: + +.. sourcecode:: python + + from cubicweb.web import uihelper + uihelper.set_fields_order('CWUser', ('firstname', 'surname', 'login')) + uihelper.set_fields_order('CWUser', ('firstname', ('in_group', 'subject'), 'surname', 'login')) + + """ + afk = uicfg.autoform_field_kwargs + for index, attr in enumerate(attrs): + _tag_rel(afk, etype, attr, '*', {'order': index}) + + +def hide_field(etype, attr, desttype='*', formtype='main'): + """hide `attr` in `etype` forms. + + :param etype: the entity type as a string + :param attr: the name of the attribute or relation to hide + :param formtype: which form will be affected ('main', 'inlined', etc.), *main* by default. + + `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) + + Examples: + +.. sourcecode:: python + + from cubicweb.web import uihelper + uihelper.hide_field('CWUser', 'login') + uihelper.hide_field('*', 'name') + uihelper.hide_field('CWUser', 'use_email', formtype='inlined') + + """ + _tag_rel(uicfg.autoform_section, etype, attr, desttype, + formtype=formtype, section='hidden') + + +def hide_fields(etype, attrs, formtype='main'): + """simple for-loop wrapper around :func:`hide_field`. + + :param etype: the entity type as a string + :param attrs: the ordered list of attribute names (or relations) + :param formtype: which form will be affected ('main', 'inlined', etc.), *main* by default. + + `attrs` can be strings or 2-tuples (relname, role_of_etype_in_the_relation) + + Examples: + +.. sourcecode:: python + + from cubicweb.web import uihelper + uihelper.hide_fields('CWUser', ('login', ('use_email', 'subject')), formtype='inlined') + """ + for field in fields: + hide_field(etype, field, formtype=formtype) + + +def set_field_kwargs(etype, attr, **kwargs): + """tag `attr` field of `etype` with additional named paremeters. + + :param etype: the entity type as a string + :param attr: the name of the attribute or relation + + `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) + + Examples: + +.. sourcecode:: python + + from cubicweb.web import uihelper, formwidgets as fwdgs + + uihelper.set_field_kwargs('Person', 'works_for', widget=fwdgs.AutoCompletionWidget()) + uihelper.set_field_kwargs('CWUser', 'login', label=_('login or email address'), + widget=fwdgs.TextInput(attrs={'size': 30})) + """ + _tag_rel(uicfg.autoform_field_kwargs, etype, attr, '*', kwargs) + + +def set_field(etype, attr, field): + """sets the `attr` field of `etype`. + + :param etype: the entity type as a string + :param attr: the name of the attribute or relation + + `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) + + """ + _tag_rel(uicfg.autoform_field, etype, attr, '*', field) + + +def edit_inline(etype, attr, desttype='*', formtype=('main', 'inlined')): + """edit `attr` with and inlined form. + + :param etype: the entity type as a string + :param attr: the name of the attribute or relation + :param desttype: the destination type(s) concerned, default is everything + :param formtype: which form will be affected ('main', 'inlined', etc.), *main* and *inlined* by default. + + `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) + + Examples: + +.. sourcecode:: python + + from cubicweb.web import uihelper + + uihelper.edit_inline('*', 'use_email') + """ + _tag_rel(uicfg.autoform_section, etype, attr, desttype, + formtype=formtype, section='inlined') + + +def edit_as_attr(etype, attr, desttype='*', formtype=('main', 'muledit')): + """make `attr` appear in the *attributes* section of `etype` form. + + :param etype: the entity type as a string + :param attr: the name of the attribute or relation + :param desttype: the destination type(s) concerned, default is everything + :param formtype: which form will be affected ('main', 'inlined', etc.), *main* and *muledit* by default. + + `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) + + Examples: + +.. sourcecode:: python + + from cubicweb.web import uihelper + + uihelper.edit_as_attr('CWUser', 'in_group') + """ + _tag_rel(uicfg.autoform_section, etype, attr, desttype, + formtype=formtype, section='attributes') + + +def set_muledit_editable(etype, attrs): + """make `attrs` appear in muledit form of `etype`. + + :param etype: the entity type as a string + :param attrs: the ordered list of attribute names (or relations) + + `attrs` can be strings or 2-tuples (relname, role_of_etype_in_the_relation) + + Examples: + +.. sourcecode:: python + + from cubicweb.web import uihelper + + uihelper.set_muledit_editable('CWUser', ('firstname', 'surname', 'in_group')) + """ + for attr in attrs: + edit_as_attr(etype, attr, formtype='muledit') + + +class meta_formconfig(type): + """metaclass of FormConfig classes, only for easier declaration purpose""" + def __init__(cls, name, bases, classdict): + if cls.etype is None: + return + for attr_role in cls.hidden: + hide_field(cls.etype, attr_role, formtype=cls.formtype) + for attr_role in cls.rels_as_attrs: + edit_as_attr(cls.etype, attr_role, formtype=cls.formtype) + for attr_role in cls.inlined: + edit_inline(cls.etype, attr_role, formtype=cls.formtype) + for rtype, widget in cls.widgets.items(): + set_field_kwargs(cls.etype, rtype, widget=widget) + for rtype, field in cls.fields.items(): + set_field(cls.etype, rtype, field) + set_fields_order(cls.etype, cls.fields_order) + super(meta_formconfig, cls).__init__(name, bases, classdict) + + +class FormConfig: + """helper base class to define uicfg rules on a given entity type. + + In all descriptions below, attributes list can either be a list of + attribute names of a list of 2-tuples (relation name, role of + the edited entity in the relation). + + **Attributes** + + :attr:`etype` + which entity type the form config is for. This attribute is **mandatory** + + :attr:`formtype` + the formtype the class tries toc customize (i.e. *main*, *inlined*, or *muledit*), + default is *main*. + + :attr:`hidden` + the list of attributes or relations to hide. + + :attr:`rels_as_attrs` + the list of attributes to edit in the *attributes* section. + + :attr:`inlined` + the list of attributes to edit in the *inlined* section. + + :attr:`fields_order` + the list of attributes to edit, in the desired order. Unspecified + fields will be displayed after specified ones, their order + being consistent with the schema definition. + + :attr:`widgets` + a dictionnary mapping attribute names to widget instances. + + :attr:`fields` + a dictionnary mapping attribute names to field instances. + + Examples: + +.. sourcecode:: python + + from cubicweb.web import uihelper, formwidgets as fwdgs + + class LinkFormConfig(uihelper.FormConfig): + etype = 'Link' + hidden = ('title', 'description', 'embed') + widgets = dict( + url=fwdgs.TextInput(attrs={'size':40}), + ) + + class UserFormConfig(uihelper.FormConfig): + etype = 'CWUser' + hidden = ('login',) + rels_as_attrs = ('in_group',) + fields_order = ('firstname', 'surname', 'in_group', 'use_email') + inlined = ('use_email',) + + """ + __metaclass__ = meta_formconfig + formtype = 'main' + etype = None # must be defined in concrete subclasses + hidden = () + rels_as_attrs = () + inlined = () + fields_order = () + widgets = {} + fields = {}