[uicfg] add uihelper module with high-level helpers (closes #1809414)
authorAdrien Di Mascio <Adrien.DiMascio@logilab.fr>
Thu, 27 Oct 2011 18:19:00 +0200
changeset 8030 552d85fcb587
parent 8029 805d4e121b65
child 8032 bcb87336c7d2
[uicfg] add uihelper module with high-level helpers (closes #1809414)
doc/book/en/devweb/rtags.rst
web/test/unittest_uicfg.py
web/uihelper.py
--- 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
+
--- 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 <http://www.gnu.org/licenses/>.
+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
--- /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 <http://www.gnu.org/licenses/>.
+"""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 = {}