edit box refactoring to gain more control by using actions 3.5
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 14 Sep 2009 19:01:24 +0200
branch3.5
changeset 3219 be8cfc00ae04
parent 3218 2a4bbe3fa4f3
child 3220 11b6016e3970
edit box refactoring to gain more control by using actions
web/action.py
web/views/actions.py
web/views/boxes.py
web/views/workflow.py
--- a/web/action.py	Mon Sep 14 18:59:17 2009 +0200
+++ b/web/action.py	Mon Sep 14 19:01:24 2009 +0200
@@ -34,6 +34,12 @@
     }
     site_wide = True # don't want user to configuration actions eproperties
     category = 'moreactions'
+    # actions in category 'moreactions' can specify a sub-menu in which they should be filed
+    submenu = None
+
+    def fill_menu(self, box, menu):
+        """add action(s) to the given submenu of the given box"""
+        menu.append(box.box_action(self))
 
     def url(self):
         """return the url associated with this action"""
@@ -75,7 +81,7 @@
                   & partial_may_add_relation())
     registered = accepts_compat(Action.registered)
 
-    category = 'addrelated'
+    submenu = 'addrelated'
 
     def url(self):
         current_entity = self.rset.get_entity(self.row or 0, self.col or 0)
--- a/web/views/actions.py	Mon Sep 14 18:59:17 2009 +0200
+++ b/web/views/actions.py	Mon Sep 14 19:01:24 2009 +0200
@@ -226,6 +226,71 @@
         return self.build_url('add/%s' % self.rsettype)
 
 
+class AddRelatedActions(Action):
+    """fill 'addrelated' sub-menu of the actions box"""
+    id = 'addrelated'
+    __select__ = Action.__select__ & one_line_rset() & non_final_entity()
+
+    submenu = _('addrelated')
+    order = 20
+
+    def fill_menu(self, box, menu):
+        # when there is only one item in the sub-menu, replace the sub-menu by
+        # item's title prefixed by 'add'
+        menu.label_prefix = self.req._('add')
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
+        user = self.req.user
+        actions = []
+        _ = self.req._
+        eschema = entity.e_schema
+        for rschema, teschema, x in self.add_related_schemas(entity):
+            if x == 'subject':
+                label = 'add %s %s %s %s' % (eschema, rschema, teschema, x)
+                url = self.linkto_url(entity, rschema, teschema, 'object')
+            else:
+                label = 'add %s %s %s %s' % (teschema, rschema, eschema, x)
+                url = self.linkto_url(entity, rschema, teschema, 'subject')
+            menu.append(box.mk_action(_(label), url))
+
+    def add_related_schemas(self, entity):
+        """this is actually used ui method to generate 'addrelated' actions from
+        the schema.
+
+        If you don't want any auto-generated actions, you should overrides this
+        method to return an empty list. If you only want some, you can configure
+        them by using uicfg.actionbox_appearsin_addmenu
+        """
+        appearsin_addmenu = uicfg.actionbox_appearsin_addmenu
+        req = self.req
+        eschema = entity.e_schema
+        for role, rschemas in (('subject', eschema.subject_relations()),
+                               ('object', eschema.object_relations())):
+            for rschema in rschemas:
+                if rschema.is_final():
+                    continue
+                # check the relation can be added as well
+                # XXX consider autoform_permissions_overrides?
+                if role == 'subject'and not rschema.has_perm(req, 'add',
+                                                             fromeid=entity.eid):
+                    continue
+                if role == 'object'and not rschema.has_perm(req, 'add',
+                                                            toeid=entity.eid):
+                    continue
+                # check the target types can be added as well
+                for teschema in rschema.targets(eschema, role):
+                    if not appearsin_addmenu.etype_get(eschema, rschema,
+                                                       role, teschema):
+                        continue
+                    if teschema.has_local_role('add') or teschema.has_perm(req, 'add'):
+                        yield rschema, teschema, role
+
+    def linkto_url(self, entity, rtype, etype, target):
+        return self.build_url(vid='creation', etype=etype,
+                              __linkto='%s:%s:%s' % (rtype, entity.eid, target),
+                              __redirectpath=entity.rest_path(), # should not be url quoted!
+                              __redirectvid=self.req.form.get('vid', ''))
+
+
 # logged user actions #########################################################
 
 class UserPreferencesAction(Action):
--- a/web/views/boxes.py	Mon Sep 14 18:59:17 2009 +0200
+++ b/web/views/boxes.py	Mon Sep 14 19:01:24 2009 +0200
@@ -16,6 +16,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from warnings import warn
+
 from logilab.mtconverter import xml_escape
 
 from cubicweb.selectors import match_user_groups, non_final_entity
@@ -26,7 +28,7 @@
 from cubicweb.web.box import BoxTemplate
 
 
-class EditBox(BoxTemplate):
+class EditBox(BoxTemplate): # XXX rename to ActionsBox
     """
     box with all actions impacting the entity displayed: edit, copy, delete
     change state, add related entities
@@ -36,9 +38,6 @@
 
     title = _('actions')
     order = 2
-    # class attributes below are actually stored in the uicfg module since we
-    # don't want them to be reloaded
-    appearsin_addmenu = uicfg.actionbox_appearsin_addmenu
 
     def call(self, view=None, **kwargs):
         _ = self.req._
@@ -50,111 +49,59 @@
                 etypelabel = display_name(self.req, iter(etypes).next(), plural)
                 title = u'%s - %s' % (title, etypelabel.lower())
         box = BoxWidget(title, self.id, _class="greyBoxFrame")
+        self._menus_in_order = []
+        self._menus_by_id = {}
         # build list of actions
         actions = self.vreg['actions'].possible_actions(self.req, self.rset,
                                                         view=view)
-        add_menu = BoxMenu(_('add')) # 'addrelated' category
-        other_menu = BoxMenu(_('more actions')) # 'moreactions' category
-        searchstate = self.req.search_state[0]
-        for category, menu in (('mainactions', box),
-                               ('addrelated', add_menu),
-                               ('moreactions', other_menu)):
+        other_menu = self._get_menu('moreactions', _('more actions'))
+        for category, defaultmenu in (('mainactions', box),
+                                      ('moreactions', other_menu),
+                                      ('addrelated', None)):
             for action in actions.get(category, ()):
-                menu.append(self.box_action(action))
-        if self.rset and self.rset.rowcount == 1 and \
-               not self.schema[self.rset.description[0][0]].is_final() and \
-               searchstate == 'normal':
-            entity = self.rset.get_entity(0, 0)
-            for action in self.schema_actions(entity):
-                add_menu.append(action)
-            self.workflow_actions(entity, box)
+                if category == 'addrelated':
+                    warn('"addrelated" category is deprecated, use "moreaction"'
+                         ' category w/ "addrelated" submenu',
+                         DeprecationWarning)
+                    defaultmenu = self._get_menu('addrelated', _('add'), _('add'))
+                if action.submenu:
+                    menu = self._get_menu(action.submenu)
+                else:
+                    menu = defaultmenu
+                action.fill_menu(self, menu)
         if box.is_empty() and not other_menu.is_empty():
             box.items = other_menu.items
             other_menu.items = []
-        self.add_submenu(box, add_menu, _('add'))
-        self.add_submenu(box, other_menu)
+        else: # ensure 'more actions' menu appears last
+            self._menus_in_order.remove(other_menu)
+            self._menus_in_order.append(other_menu)
+        for submenu in self._menus_in_order:
+            self.add_submenu(box, submenu)
         if not box.is_empty():
             box.render(self.w)
 
+    def _get_menu(self, id, title=None, label_prefix=None):
+        try:
+            return self._menus_by_id[id]
+        except KeyError:
+            if title is None:
+                title = self.req._(id)
+            self._menus_by_id[id] = menu = BoxMenu(title)
+            menu.label_prefix = label_prefix
+            self._menus_in_order.append(menu)
+            return menu
+
     def add_submenu(self, box, submenu, label_prefix=None):
-        if len(submenu.items) == 1:
+        appendanyway = getattr(submenu, 'append_anyway', False)
+        if len(submenu.items) == 1 and not appendanyway:
             boxlink = submenu.items[0]
-            if label_prefix:
-                boxlink.label = u'%s %s' % (label_prefix, boxlink.label)
+            if submenu.label_prefix:
+                boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
             box.append(boxlink)
         elif submenu.items:
             box.append(submenu)
-
-    def schema_actions(self, entity):
-        user = self.req.user
-        actions = []
-        _ = self.req._
-        eschema = entity.e_schema
-        for rschema, teschema, x in self.add_related_schemas(entity):
-            if x == 'subject':
-                label = 'add %s %s %s %s' % (eschema, rschema, teschema, x)
-                url = self.linkto_url(entity, rschema, teschema, 'object')
-            else:
-                label = 'add %s %s %s %s' % (teschema, rschema, eschema, x)
-                url = self.linkto_url(entity, rschema, teschema, 'subject')
-            actions.append(self.mk_action(_(label), url))
-        return actions
-
-    def add_related_schemas(self, entity):
-        """this is actually used ui method to generate 'addrelated' actions from
-        the schema.
-
-        If you don't want any auto-generated actions, you should overrides this
-        method to return an empty list. If you only want some, you can configure
-        them by using uicfg.actionbox_appearsin_addmenu
-        """
-        req = self.req
-        eschema = entity.e_schema
-        for role, rschemas in (('subject', eschema.subject_relations()),
-                               ('object', eschema.object_relations())):
-            for rschema in rschemas:
-                if rschema.is_final():
-                    continue
-                # check the relation can be added as well
-                # XXX consider autoform_permissions_overrides?
-                if role == 'subject'and not rschema.has_perm(req, 'add',
-                                                             fromeid=entity.eid):
-                    continue
-                if role == 'object'and not rschema.has_perm(req, 'add',
-                                                            toeid=entity.eid):
-                    continue
-                # check the target types can be added as well
-                for teschema in rschema.targets(eschema, role):
-                    if not self.appearsin_addmenu.etype_get(eschema, rschema,
-                                                            role, teschema):
-                        continue
-                    if teschema.has_local_role('add') or teschema.has_perm(req, 'add'):
-                        yield rschema, teschema, role
-
-
-    def workflow_actions(self, entity, box):
-        if entity.e_schema.has_subject_relation('in_state') and entity.in_state:
-            _ = self.req._
-            menu_title = u'%s: %s' % (_('state'), entity.printable_state)
-            menu_items = []
-            for tr in entity.possible_transitions():
-                url = entity.absolute_url(vid='statuschange', treid=tr.eid)
-                menu_items.append(self.mk_action(_(tr.name), url))
-            # don't propose to see wf if user can't pass any transition
-            if menu_items:
-                wfurl = self.build_url('cwetype/%s'%entity.e_schema, vid='workflow')
-                menu_items.append(self.mk_action(_('view workflow'), wfurl))
-            if entity.workflow_history:
-                wfurl = entity.absolute_url(vid='wfhistory')
-                menu_items.append(self.mk_action(_('view history'), wfurl))
-            box.append(BoxMenu(menu_title, menu_items))
-        return None
-
-    def linkto_url(self, entity, rtype, etype, target):
-        return self.build_url(vid='creation', etype=etype,
-                              __linkto='%s:%s:%s' % (rtype, entity.eid, target),
-                              __redirectpath=entity.rest_path(), # should not be url quoted!
-                              __redirectvid=self.req.form.get('vid', ''))
+        elif appendanyway:
+            box.append(RawBoxItem(xml_escape(submenu.label)))
 
 
 class SearchBox(BoxTemplate):
--- a/web/views/workflow.py	Mon Sep 14 18:59:17 2009 +0200
+++ b/web/views/workflow.py	Mon Sep 14 19:01:24 2009 +0200
@@ -15,7 +15,7 @@
 from logilab.common.graph import escape, GraphGenerator, DotBackend
 
 from cubicweb import Unauthorized, view
-from cubicweb.selectors import (implements, has_related_entities,
+from cubicweb.selectors import (implements, has_related_entities, one_line_rset,
                                 relation_possible, match_form_params)
 from cubicweb.interfaces import IWorkflowable
 from cubicweb.view import EntityView
@@ -110,7 +110,36 @@
     def cell_call(self, row, col, view=None):
         self.wview('wfhistory', self.rset, row=row, col=col, view=view)
 
-# workflow entity types views #################################################
+
+# workflow actions #############################################################
+
+class WorkflowActions(action.Action):
+    """fill 'workflow' sub-menu of the actions box"""
+    id = 'workflow'
+    __select__ = (action.Action.__select__ & one_line_rset() &
+                  relation_possible('in_state'))
+
+    submenu = _('workflow')
+    order = 10
+
+    def fill_menu(self, box, menu):
+        req = self.req
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
+        menu.label = u'%s: %s' % (req._('state'), entity.printable_state)
+        menu.append_anyway = True
+        for tr in entity.possible_transitions():
+            url = entity.absolute_url(vid='statuschange', treid=tr.eid)
+            menu.append(box.mk_action(req._(tr.name), url))
+        # don't propose to see wf if user can't pass any transition
+        if menu.items:
+            wfurl = entity.current_workflow.absolute_url()
+            menu.append(box.mk_action(req._('view workflow'), wfurl))
+        if entity.workflow_history:
+            wfurl = entity.absolute_url(vid='wfhistory')
+            menu.append(box.mk_action(req._('view history'), wfurl))
+
+
+# workflow entity types views ##################################################
 
 class CellView(view.EntityView):
     id = 'cell'
@@ -131,7 +160,6 @@
 
 
 class WorkflowPrimaryView(primary.PrimaryView):
-    id = 'workflow'
     __select__ = implements('Workflow')
     cache_max_age = 60*60*2 # stay in http cache for 2 hours by default