"""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)