web/views/workflow.py
branchtls-sprint
changeset 1091 b5e253c0dd13
parent 984 536e421b082b
child 1132 96752791c2b6
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/workflow.py	Thu Mar 12 16:29:00 2009 +0100
@@ -0,0 +1,214 @@
+"""workflow views:
+
+* IWorkflowable views and forms
+* workflow entities views (State, Transition, TrInfo)
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.mtconverter import html_escape
+from logilab.common.graph import escape, GraphGenerator, DotBackend
+
+from cubicweb import Unauthorized, view
+from cubicweb.selectors import (implements, has_related_entities,
+                                relation_possible, match_form_params)
+from cubicweb.interfaces import IWorkflowable
+from cubicweb.web import stdmsgs, action, component, form
+from cubicweb.web.formfields import StringField,  RichTextField
+from cubicweb.web.formwidgets import HiddenInput
+from cubicweb.web.views import TmpFileViewMixin
+
+
+# IWorkflowable views #########################################################
+
+class ChangeStateForm(form.EntityFieldsForm):
+    id = 'changestate'
+    
+    __method = StringField(name='__method', initial='set_state', widget=HiddenInput)
+    state = StringField(widget=HiddenInput, eidparam=True)
+    trcomment = RichTextField(eidparam=True)
+
+    def form_buttons(self):
+        return [self.button_ok(label=stdmsgs.YES,
+                               tabindex=self.req.next_tabindex()),
+                self.button_cancel(label=stdmsgs.NO,
+                                   tabindex=self.req.next_tabindex())]
+
+        
+class ChangeStateFormView(view.EntityView):
+    id = 'statuschange'
+    title = _('status change')
+    __select__ = implements(IWorkflowable) & match_form_params('treid')
+
+    def cell_call(self, row, col):
+        entity = self.entity(row, col)
+        eid = entity.eid
+        state = entity.in_state[0]
+        transition = self.req.eid_rset(self.req.form['treid']).get_entity(0, 0)
+        dest = transition.destination()
+        _ = self.req._
+        form = self.vreg.select_object('forms', 'changestate', self.req, self.rset, row=row, col=col,
+                                       entity=entity, redirect_path=self.redirectpath(entity))
+        self.w(form.error_message())
+        self.w(u'<h4>%s %s</h4>\n' % (_(transition.name), entity.view('oneline')))
+        msg = _('status will change from %(st1)s to %(st2)s') % {
+            'st1': _(state.name),
+            'st2': _(dest.name)}
+        self.w(u'<p>%s</p>\n' % msg)
+        self.w(form.form_render(state=dest.eid, trcomment=u''))
+
+    def redirectpath(self, entity):
+        return entity.rest_path()
+
+
+class WFHistoryVComponent(component.EntityVComponent):
+    """display the workflow history for entities supporting it"""
+    id = 'wfhistory'
+    __select__ = (component.EntityVComponent.__select__
+                  & relation_possible('wf_info_for', role='object'))
+    context = 'navcontentbottom'
+    title = _('Workflow history')
+
+    def cell_call(self, row, col, view=None):
+        _ = self.req._
+        eid = self.rset[row][col]
+        sel = 'Any FS,TS,WF,D'
+        rql = ' ORDERBY D DESC WHERE WF wf_info_for X,'\
+              'WF from_state FS, WF to_state TS, WF comment C,'\
+              'WF creation_date D'
+        if self.vreg.schema.eschema('EUser').has_perm(self.req, 'read'):
+            sel += ',U,C'
+            rql += ', WF owned_by U?'
+            displaycols = range(5)
+            headers = (_('from_state'), _('to_state'), _('comment'), _('date'),
+                       _('EUser'))            
+        else:
+            sel += ',C'
+            displaycols = range(4)
+            headers = (_('from_state'), _('to_state'), _('comment'), _('date'))
+        rql = '%s %s, X eid %%(x)s' % (sel, rql)
+        try:
+            rset = self.req.execute(rql, {'x': eid}, 'x')
+        except Unauthorized:
+            return
+        if rset:
+            self.wview('table', rset, title=_(self.title), displayactions=False,
+                       displaycols=displaycols, headers=headers)
+
+
+# workflow entity types views #################################################
+
+class CellView(view.EntityView):
+    id = 'cell'
+    __select__ = implements('TrInfo')
+    
+    def cell_call(self, row, col, cellvid=None):
+        self.w(self.entity(row, col).printable_value('comment'))
+
+
+class StateInContextView(view.EntityView):
+    """convenience trick, State's incontext view should not be clickable"""
+    id = 'incontext'
+    __select__ = implements('State')
+    
+    def cell_call(self, row, col):
+        self.w(html_escape(self.view('textincontext', self.rset,
+                                     row=row, col=col)))
+
+
+# workflow images #############################################################
+        
+class ViewWorkflowAction(action.Action):
+    id = 'workflow'
+    __select__ = implements('EEType') & has_related_entities('state_of', 'object')
+    
+    category = 'mainactions'
+    title = _('view workflow')
+    def url(self):
+        entity = self.rset.get_entity(self.row or 0, self.col or 0)
+        return entity.absolute_url(vid='workflow')
+
+        
+class EETypeWorkflowView(view.EntityView):
+    id = 'workflow'
+    __select__ = implements('EEType')
+    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
+    
+    def cell_call(self, row, col, **kwargs):
+        entity = self.entity(row, col)
+        self.w(u'<h1>%s</h1>' % (self.req._('workflow for %s')
+                                 % display_name(self.req, entity.name)))
+        self.w(u'<img src="%s" alt="%s"/>' % (
+            html_escape(entity.absolute_url(vid='ewfgraph')),
+            html_escape(self.req._('graphical workflow for %s') % entity.name)))
+
+
+class WorkflowDotPropsHandler(object):
+    def __init__(self, req):
+        self._ = req._
+        
+    def node_properties(self, stateortransition):
+        """return default DOT drawing options for a state or transition"""
+        props = {'label': stateortransition.name, 
+                 'fontname': 'Courier'}
+        if hasattr(stateortransition, 'state_of'):
+            props['shape'] = 'box'
+            props['style'] = 'filled'
+            if stateortransition.reverse_initial_state:
+                props['color'] = '#88CC88'
+        else:
+            props['shape'] = 'ellipse'
+            descr = []
+            tr = stateortransition
+            if tr.require_group:
+                descr.append('%s %s'% (
+                    self._('groups:'),
+                    ','.join(g.name for g in tr.require_group)))
+            if tr.condition:
+                descr.append('%s %s'% (self._('condition:'), tr.condition))
+            if descr:
+                props['label'] += escape('\n'.join(descr))
+        return props
+    
+    def edge_properties(self, transition, fromstate, tostate):
+        return {'label': '', 'dir': 'forward',
+                'color': 'black', 'style': 'filled'}
+
+
+class WorkflowVisitor:
+    def __init__(self, entity):
+        self.entity = entity
+
+    def nodes(self):
+        for state in self.entity.reverse_state_of:
+            state.complete()
+            yield state.eid, state
+            
+        for transition in self.entity.reverse_transition_of:
+            transition.complete()
+            yield transition.eid, transition
+            
+    def edges(self):
+        for transition in self.entity.reverse_transition_of:
+            for incomingstate in transition.reverse_allowed_transition:
+                yield incomingstate.eid, transition.eid, transition
+            yield transition.eid, transition.destination().eid, transition
+
+
+class EETypeWorkflowImageView(TmpFileViewMixin, view.EntityView):
+    id = 'ewfgraph'
+    content_type = 'image/png'
+    __select__ = implements('EEType')
+    
+    def _generate(self, tmpfile):
+        """display schema information for an entity"""
+        entity = self.entity(self.row, self.col)
+        visitor = WorkflowVisitor(entity)
+        prophdlr = WorkflowDotPropsHandler(self.req)
+        generator = GraphGenerator(DotBackend('workflow', 'LR',
+                                              ratio='compress', size='30,12'))
+        return generator.generate(visitor, prophdlr, tmpfile)
+