web/views/workflow.py
branchtls-sprint
changeset 1091 b5e253c0dd13
parent 984 536e421b082b
child 1132 96752791c2b6
equal deleted inserted replaced
1090:a99dc223c583 1091:b5e253c0dd13
       
     1 """workflow views:
       
     2 
       
     3 * IWorkflowable views and forms
       
     4 * workflow entities views (State, Transition, TrInfo)
       
     5 
       
     6 :organization: Logilab
       
     7 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     8 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     9 """
       
    10 __docformat__ = "restructuredtext en"
       
    11 
       
    12 from logilab.mtconverter import html_escape
       
    13 from logilab.common.graph import escape, GraphGenerator, DotBackend
       
    14 
       
    15 from cubicweb import Unauthorized, view
       
    16 from cubicweb.selectors import (implements, has_related_entities,
       
    17                                 relation_possible, match_form_params)
       
    18 from cubicweb.interfaces import IWorkflowable
       
    19 from cubicweb.web import stdmsgs, action, component, form
       
    20 from cubicweb.web.formfields import StringField,  RichTextField
       
    21 from cubicweb.web.formwidgets import HiddenInput
       
    22 from cubicweb.web.views import TmpFileViewMixin
       
    23 
       
    24 
       
    25 # IWorkflowable views #########################################################
       
    26 
       
    27 class ChangeStateForm(form.EntityFieldsForm):
       
    28     id = 'changestate'
       
    29     
       
    30     __method = StringField(name='__method', initial='set_state', widget=HiddenInput)
       
    31     state = StringField(widget=HiddenInput, eidparam=True)
       
    32     trcomment = RichTextField(eidparam=True)
       
    33 
       
    34     def form_buttons(self):
       
    35         return [self.button_ok(label=stdmsgs.YES,
       
    36                                tabindex=self.req.next_tabindex()),
       
    37                 self.button_cancel(label=stdmsgs.NO,
       
    38                                    tabindex=self.req.next_tabindex())]
       
    39 
       
    40         
       
    41 class ChangeStateFormView(view.EntityView):
       
    42     id = 'statuschange'
       
    43     title = _('status change')
       
    44     __select__ = implements(IWorkflowable) & match_form_params('treid')
       
    45 
       
    46     def cell_call(self, row, col):
       
    47         entity = self.entity(row, col)
       
    48         eid = entity.eid
       
    49         state = entity.in_state[0]
       
    50         transition = self.req.eid_rset(self.req.form['treid']).get_entity(0, 0)
       
    51         dest = transition.destination()
       
    52         _ = self.req._
       
    53         form = self.vreg.select_object('forms', 'changestate', self.req, self.rset, row=row, col=col,
       
    54                                        entity=entity, redirect_path=self.redirectpath(entity))
       
    55         self.w(form.error_message())
       
    56         self.w(u'<h4>%s %s</h4>\n' % (_(transition.name), entity.view('oneline')))
       
    57         msg = _('status will change from %(st1)s to %(st2)s') % {
       
    58             'st1': _(state.name),
       
    59             'st2': _(dest.name)}
       
    60         self.w(u'<p>%s</p>\n' % msg)
       
    61         self.w(form.form_render(state=dest.eid, trcomment=u''))
       
    62 
       
    63     def redirectpath(self, entity):
       
    64         return entity.rest_path()
       
    65 
       
    66 
       
    67 class WFHistoryVComponent(component.EntityVComponent):
       
    68     """display the workflow history for entities supporting it"""
       
    69     id = 'wfhistory'
       
    70     __select__ = (component.EntityVComponent.__select__
       
    71                   & relation_possible('wf_info_for', role='object'))
       
    72     context = 'navcontentbottom'
       
    73     title = _('Workflow history')
       
    74 
       
    75     def cell_call(self, row, col, view=None):
       
    76         _ = self.req._
       
    77         eid = self.rset[row][col]
       
    78         sel = 'Any FS,TS,WF,D'
       
    79         rql = ' ORDERBY D DESC WHERE WF wf_info_for X,'\
       
    80               'WF from_state FS, WF to_state TS, WF comment C,'\
       
    81               'WF creation_date D'
       
    82         if self.vreg.schema.eschema('EUser').has_perm(self.req, 'read'):
       
    83             sel += ',U,C'
       
    84             rql += ', WF owned_by U?'
       
    85             displaycols = range(5)
       
    86             headers = (_('from_state'), _('to_state'), _('comment'), _('date'),
       
    87                        _('EUser'))            
       
    88         else:
       
    89             sel += ',C'
       
    90             displaycols = range(4)
       
    91             headers = (_('from_state'), _('to_state'), _('comment'), _('date'))
       
    92         rql = '%s %s, X eid %%(x)s' % (sel, rql)
       
    93         try:
       
    94             rset = self.req.execute(rql, {'x': eid}, 'x')
       
    95         except Unauthorized:
       
    96             return
       
    97         if rset:
       
    98             self.wview('table', rset, title=_(self.title), displayactions=False,
       
    99                        displaycols=displaycols, headers=headers)
       
   100 
       
   101 
       
   102 # workflow entity types views #################################################
       
   103 
       
   104 class CellView(view.EntityView):
       
   105     id = 'cell'
       
   106     __select__ = implements('TrInfo')
       
   107     
       
   108     def cell_call(self, row, col, cellvid=None):
       
   109         self.w(self.entity(row, col).printable_value('comment'))
       
   110 
       
   111 
       
   112 class StateInContextView(view.EntityView):
       
   113     """convenience trick, State's incontext view should not be clickable"""
       
   114     id = 'incontext'
       
   115     __select__ = implements('State')
       
   116     
       
   117     def cell_call(self, row, col):
       
   118         self.w(html_escape(self.view('textincontext', self.rset,
       
   119                                      row=row, col=col)))
       
   120 
       
   121 
       
   122 # workflow images #############################################################
       
   123         
       
   124 class ViewWorkflowAction(action.Action):
       
   125     id = 'workflow'
       
   126     __select__ = implements('EEType') & has_related_entities('state_of', 'object')
       
   127     
       
   128     category = 'mainactions'
       
   129     title = _('view workflow')
       
   130     def url(self):
       
   131         entity = self.rset.get_entity(self.row or 0, self.col or 0)
       
   132         return entity.absolute_url(vid='workflow')
       
   133 
       
   134         
       
   135 class EETypeWorkflowView(view.EntityView):
       
   136     id = 'workflow'
       
   137     __select__ = implements('EEType')
       
   138     cache_max_age = 60*60*2 # stay in http cache for 2 hours by default 
       
   139     
       
   140     def cell_call(self, row, col, **kwargs):
       
   141         entity = self.entity(row, col)
       
   142         self.w(u'<h1>%s</h1>' % (self.req._('workflow for %s')
       
   143                                  % display_name(self.req, entity.name)))
       
   144         self.w(u'<img src="%s" alt="%s"/>' % (
       
   145             html_escape(entity.absolute_url(vid='ewfgraph')),
       
   146             html_escape(self.req._('graphical workflow for %s') % entity.name)))
       
   147 
       
   148 
       
   149 class WorkflowDotPropsHandler(object):
       
   150     def __init__(self, req):
       
   151         self._ = req._
       
   152         
       
   153     def node_properties(self, stateortransition):
       
   154         """return default DOT drawing options for a state or transition"""
       
   155         props = {'label': stateortransition.name, 
       
   156                  'fontname': 'Courier'}
       
   157         if hasattr(stateortransition, 'state_of'):
       
   158             props['shape'] = 'box'
       
   159             props['style'] = 'filled'
       
   160             if stateortransition.reverse_initial_state:
       
   161                 props['color'] = '#88CC88'
       
   162         else:
       
   163             props['shape'] = 'ellipse'
       
   164             descr = []
       
   165             tr = stateortransition
       
   166             if tr.require_group:
       
   167                 descr.append('%s %s'% (
       
   168                     self._('groups:'),
       
   169                     ','.join(g.name for g in tr.require_group)))
       
   170             if tr.condition:
       
   171                 descr.append('%s %s'% (self._('condition:'), tr.condition))
       
   172             if descr:
       
   173                 props['label'] += escape('\n'.join(descr))
       
   174         return props
       
   175     
       
   176     def edge_properties(self, transition, fromstate, tostate):
       
   177         return {'label': '', 'dir': 'forward',
       
   178                 'color': 'black', 'style': 'filled'}
       
   179 
       
   180 
       
   181 class WorkflowVisitor:
       
   182     def __init__(self, entity):
       
   183         self.entity = entity
       
   184 
       
   185     def nodes(self):
       
   186         for state in self.entity.reverse_state_of:
       
   187             state.complete()
       
   188             yield state.eid, state
       
   189             
       
   190         for transition in self.entity.reverse_transition_of:
       
   191             transition.complete()
       
   192             yield transition.eid, transition
       
   193             
       
   194     def edges(self):
       
   195         for transition in self.entity.reverse_transition_of:
       
   196             for incomingstate in transition.reverse_allowed_transition:
       
   197                 yield incomingstate.eid, transition.eid, transition
       
   198             yield transition.eid, transition.destination().eid, transition
       
   199 
       
   200 
       
   201 class EETypeWorkflowImageView(TmpFileViewMixin, view.EntityView):
       
   202     id = 'ewfgraph'
       
   203     content_type = 'image/png'
       
   204     __select__ = implements('EEType')
       
   205     
       
   206     def _generate(self, tmpfile):
       
   207         """display schema information for an entity"""
       
   208         entity = self.entity(self.row, self.col)
       
   209         visitor = WorkflowVisitor(entity)
       
   210         prophdlr = WorkflowDotPropsHandler(self.req)
       
   211         generator = GraphGenerator(DotBackend('workflow', 'LR',
       
   212                                               ratio='compress', size='30,12'))
       
   213         return generator.generate(visitor, prophdlr, tmpfile)
       
   214