diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/workflow.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/workflow.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,452 @@
+# copyright 2003-2014 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 .
+"""workflow views:
+
+* IWorkflowable views and forms
+* workflow entities views (State, Transition, TrInfo)
+"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+import os
+from warnings import warn
+
+from six import add_metaclass
+
+from logilab.mtconverter import xml_escape
+from logilab.common.graph import escape
+from logilab.common.deprecation import class_deprecated
+
+from cubicweb import Unauthorized
+from cubicweb.predicates import (has_related_entities, one_line_rset,
+ relation_possible, match_form_params,
+ score_entity, is_instance, adaptable)
+from cubicweb.view import EntityView
+from cubicweb.schema import display_name
+from cubicweb.web import stdmsgs, action, component, form, action
+from cubicweb.web import formfields as ff, formwidgets as fwdgs
+from cubicweb.web.views import TmpFileViewMixin
+from cubicweb.web.views import uicfg, forms, primary, ibreadcrumbs
+from cubicweb.web.views.tabs import TabbedPrimaryView, PrimaryTab
+from cubicweb.web.views.dotgraphview import DotGraphView, DotPropsHandler
+
+_pvs = uicfg.primaryview_section
+_pvs.tag_subject_of(('Workflow', 'initial_state', '*'), 'hidden')
+_pvs.tag_object_of(('*', 'state_of', 'Workflow'), 'hidden')
+_pvs.tag_object_of(('*', 'transition_of', 'Workflow'), 'hidden')
+_pvs.tag_object_of(('*', 'wf_info_for', '*'), 'hidden')
+for rtype in ('in_state', 'by_transition', 'from_state', 'to_state'):
+ _pvs.tag_subject_of(('*', rtype, '*'), 'hidden')
+ _pvs.tag_object_of(('*', rtype, '*'), 'hidden')
+_pvs.tag_object_of(('*', 'wf_info_for', '*'), 'hidden')
+
+_abaa = uicfg.actionbox_appearsin_addmenu
+_abaa.tag_subject_of(('BaseTransition', 'condition', 'RQLExpression'), False)
+_abaa.tag_subject_of(('State', 'allowed_transition', 'BaseTransition'), False)
+_abaa.tag_object_of(('SubWorkflowExitPoint', 'destination_state', 'State'),
+ False)
+_abaa.tag_subject_of(('*', 'wf_info_for', '*'), False)
+_abaa.tag_object_of(('*', 'wf_info_for', '*'), False)
+
+_abaa.tag_object_of(('*', 'state_of', 'CWEType'), True)
+_abaa.tag_object_of(('*', 'transition_of', 'CWEType'), True)
+_abaa.tag_subject_of(('Transition', 'destination_state', '*'), True)
+_abaa.tag_object_of(('*', 'allowed_transition', 'Transition'), True)
+_abaa.tag_object_of(('*', 'destination_state', 'State'), True)
+_abaa.tag_subject_of(('State', 'allowed_transition', '*'), True)
+_abaa.tag_object_of(('State', 'state_of', 'Workflow'), True)
+_abaa.tag_object_of(('Transition', 'transition_of', 'Workflow'), True)
+_abaa.tag_object_of(('WorkflowTransition', 'transition_of', 'Workflow'), True)
+
+_afs = uicfg.autoform_section
+_affk = uicfg.autoform_field_kwargs
+
+# IWorkflowable views #########################################################
+
+class ChangeStateForm(forms.CompositeEntityForm):
+ # set dom id to ensure there is no conflict with edition form (see
+ # session_key() implementation)
+ __regid__ = domid = 'changestate'
+
+ form_renderer_id = 'base' # don't want EntityFormRenderer
+ form_buttons = [fwdgs.SubmitButton(),
+ fwdgs.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')]
+
+
+class ChangeStateFormView(form.FormViewMixIn, EntityView):
+ __regid__ = 'statuschange'
+ title = _('status change')
+ __select__ = (one_line_rset()
+ & match_form_params('treid')
+ & adaptable('IWorkflowable'))
+
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(row, col)
+ transition = self._cw.entity_from_eid(self._cw.form['treid'])
+ form = self.get_form(entity, transition)
+ self.w(u'
%s %s
\n' % (self._cw._(transition.name),
+ entity.view('oneline')))
+ msg = self._cw._('status will change from %(st1)s to %(st2)s') % {
+ 'st1': entity.cw_adapt_to('IWorkflowable').printable_state,
+ 'st2': self._cw._(transition.destination(entity).name)}
+ self.w(u'%s
\n' % msg)
+ form.render(w=self.w)
+
+ def redirectpath(self, entity):
+ return entity.rest_path()
+
+ def get_form(self, entity, transition, **kwargs):
+ # XXX used to specify both rset/row/col and entity in case implements
+ # selector (and not is_instance) is used on custom form
+ form = self._cw.vreg['forms'].select(
+ 'changestate', self._cw, entity=entity, transition=transition,
+ redirect_path=self.redirectpath(entity), **kwargs)
+ trinfo = self._cw.vreg['etypes'].etype_class('TrInfo')(self._cw)
+ trinfo.eid = next(self._cw.varmaker)
+ subform = self._cw.vreg['forms'].select('edition', self._cw, entity=trinfo,
+ mainform=False)
+ subform.field_by_name('wf_info_for', 'subject').value = entity.eid
+ trfield = subform.field_by_name('by_transition', 'subject')
+ trfield.widget = fwdgs.HiddenInput()
+ trfield.value = transition.eid
+ form.add_subform(subform)
+ return form
+
+
+class WFHistoryView(EntityView):
+ __regid__ = 'wfhistory'
+ __select__ = relation_possible('wf_info_for', role='object') & \
+ score_entity(lambda x: x.cw_adapt_to('IWorkflowable').workflow_history)
+
+ title = _('Workflow history')
+
+ def cell_call(self, row, col, view=None, title=title):
+ _ = self._cw._
+ eid = self.cw_rset[row][col]
+ sel = 'Any FS,TS,C,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._cw.vreg.schema.eschema('CWUser').has_perm(self._cw, 'read'):
+ sel += ',U,WF'
+ rql += ', WF owned_by U?'
+ headers = (_('from_state'), _('to_state'), _('comment'), _('date'),
+ _('CWUser'))
+ else:
+ sel += ',WF'
+ headers = (_('from_state'), _('to_state'), _('comment'), _('date'))
+ rql = '%s %s, X eid %%(x)s' % (sel, rql)
+ try:
+ rset = self._cw.execute(rql, {'x': eid})
+ except Unauthorized:
+ return
+ if rset:
+ if title:
+ self.w(u'%s
\n' % _(title))
+ self.wview('table', rset, headers=headers,
+ cellvids={2: 'editable-final'})
+
+
+class WFHistoryVComponent(component.EntityCtxComponent):
+ """display the workflow history for entities supporting it"""
+ __regid__ = 'wfhistory'
+ __select__ = component.EntityCtxComponent.__select__ & WFHistoryView.__select__
+ context = 'navcontentbottom'
+ title = _('Workflow history')
+
+ def render_body(self, w):
+ self.entity.view('wfhistory', w=w, title=None)
+
+
+class InContextWithStateView(EntityView):
+ """display incontext view for an entity as well as its current state"""
+ __regid__ = 'incontext-state'
+ __select__ = adaptable('IWorkflowable')
+ def entity_call(self, entity):
+ iwf = entity.cw_adapt_to('IWorkflowable')
+ self.w(u'%s [%s]' % (entity.view('incontext'), iwf.printable_state))
+
+
+# workflow actions #############################################################
+
+class WorkflowActions(action.Action):
+ """fill 'workflow' sub-menu of the actions box"""
+ __regid__ = 'workflow'
+ __select__ = (action.Action.__select__ & one_line_rset() &
+ relation_possible('in_state'))
+
+ submenu = _('workflow')
+ order = 10
+
+ def fill_menu(self, box, menu):
+ entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+ menu.label = u'%s: %s' % (self._cw._('state'),
+ entity.cw_adapt_to('IWorkflowable').printable_state)
+ menu.append_anyway = True
+ super(WorkflowActions, self).fill_menu(box, menu)
+
+ def actual_actions(self):
+ entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+ iworkflowable = entity.cw_adapt_to('IWorkflowable')
+ hastr = False
+ for tr in iworkflowable.possible_transitions():
+ url = entity.absolute_url(vid='statuschange', treid=tr.eid)
+ yield self.build_action(self._cw._(tr.name), url)
+ hastr = True
+ # don't propose to see wf if user can't pass any transition
+ if hastr:
+ wfurl = iworkflowable.current_workflow.absolute_url()
+ yield self.build_action(self._cw._('view workflow'), wfurl)
+ if iworkflowable.workflow_history:
+ wfurl = entity.absolute_url(vid='wfhistory')
+ yield self.build_action(self._cw._('view history'), wfurl)
+
+
+# workflow entity types views ##################################################
+
+_pvs = uicfg.primaryview_section
+_pvs.tag_subject_of(('Workflow', 'initial_state', '*'), 'hidden')
+_pvs.tag_object_of(('*', 'state_of', 'Workflow'), 'hidden')
+_pvs.tag_object_of(('*', 'transition_of', 'Workflow'), 'hidden')
+_pvs.tag_object_of(('*', 'default_workflow', 'Workflow'), 'hidden')
+
+_abaa = uicfg.actionbox_appearsin_addmenu
+_abaa.tag_subject_of(('BaseTransition', 'condition', 'RQLExpression'), False)
+_abaa.tag_subject_of(('State', 'allowed_transition', 'BaseTransition'), False)
+_abaa.tag_object_of(('SubWorkflowExitPoint', 'destination_state', 'State'),
+ False)
+_abaa.tag_object_of(('State', 'state_of', 'Workflow'), True)
+_abaa.tag_object_of(('BaseTransition', 'transition_of', 'Workflow'), False)
+_abaa.tag_object_of(('Transition', 'transition_of', 'Workflow'), True)
+_abaa.tag_object_of(('WorkflowTransition', 'transition_of', 'Workflow'), True)
+
+class WorkflowPrimaryView(TabbedPrimaryView):
+ __select__ = is_instance('Workflow')
+ tabs = [ _('wf_tab_info'), _('wfgraph'),]
+ default_tab = 'wf_tab_info'
+
+
+class StateInContextView(EntityView):
+ """convenience trick, State's incontext view should not be clickable"""
+ __regid__ = 'incontext'
+ __select__ = is_instance('State')
+
+ def cell_call(self, row, col):
+ self.w(xml_escape(self._cw.view('textincontext', self.cw_rset,
+ row=row, col=col)))
+
+class WorkflowTabTextView(PrimaryTab):
+ __regid__ = 'wf_tab_info'
+ __select__ = PrimaryTab.__select__ & one_line_rset() & is_instance('Workflow')
+
+ def render_entity_attributes(self, entity):
+ _ = self._cw._
+ self.w(u'%s
' % (entity.printable_value('description')))
+ self.w(u'%s%s' % (_("workflow_of").capitalize(), _(" :")))
+ html = []
+ for e in entity.workflow_of:
+ view = e.view('outofcontext')
+ if entity.eid == e.default_workflow[0].eid:
+ view += u' [%s]' % _('default_workflow')
+ html.append(view)
+ self.w(', '.join(v for v in html))
+ self.w(u'%s
' % _("Transition_plural"))
+ rset = self._cw.execute(
+ 'Any T,T,DS,T,TT ORDERBY TN WHERE T transition_of WF, WF eid %(x)s,'
+ 'T type TT, T name TN, T destination_state DS?', {'x': entity.eid})
+ self.wview('table', rset, 'null',
+ cellvids={ 1: 'trfromstates', 2: 'outofcontext', 3:'trsecurity',},
+ headers = (_('Transition'), _('from_state'),
+ _('to_state'), _('permissions'), _('type') ),
+ )
+
+
+class TransitionSecurityTextView(EntityView):
+ __regid__ = 'trsecurity'
+ __select__ = is_instance('Transition')
+
+ def cell_call(self, row, col):
+ _ = self._cw._
+ entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
+ if entity.require_group:
+ self.w(u'%s%s %s
' %
+ (_('groups'), _(" :"),
+ u', '.join((g.view('incontext') for g
+ in entity.require_group))))
+ if entity.condition:
+ self.w(u'%s%s %s
' %
+ ( _('conditions'), _(" :"),
+ u'
'.join((e.dc_title() for e
+ in entity.condition))))
+
+class TransitionAllowedTextView(EntityView):
+ __regid__ = 'trfromstates'
+ __select__ = is_instance('Transition')
+
+ def cell_call(self, row, col):
+ entity = self.cw_rset.get_entity(self.cw_row, self.cw_col)
+ self.w(u', '.join((e.view('outofcontext') for e
+ in entity.reverse_allowed_transition)))
+
+
+# workflow entity types edition ################################################
+
+def _wf_items_for_relation(req, wfeid, wfrelation, field):
+ wf = req.entity_from_eid(wfeid)
+ rschema = req.vreg.schema[field.name]
+ param = 'toeid' if field.role == 'subject' else 'fromeid'
+ return sorted((e.view('combobox'), unicode(e.eid))
+ for e in getattr(wf, 'reverse_%s' % wfrelation)
+ if rschema.has_perm(req, 'add', **{param: e.eid}))
+
+# TrInfo
+_afs.tag_subject_of(('TrInfo', 'to_state', '*'), 'main', 'hidden')
+_afs.tag_subject_of(('TrInfo', 'from_state', '*'), 'main', 'hidden')
+_afs.tag_attribute(('TrInfo', 'tr_count'), 'main', 'hidden')
+
+# BaseTransition
+# XXX * allowed_transition BaseTransition
+# XXX BaseTransition destination_state *
+
+def transition_states_vocabulary(form, field):
+ entity = form.edited_entity
+ if entity.has_eid():
+ wfeid = entity.transition_of[0].eid
+ else:
+ eids = form.linked_to.get(('transition_of', 'subject'))
+ if not eids:
+ return []
+ wfeid = eids[0]
+ return _wf_items_for_relation(form._cw, wfeid, 'state_of', field)
+
+_afs.tag_subject_of(('*', 'destination_state', '*'), 'main', 'attributes')
+_affk.tag_subject_of(('*', 'destination_state', '*'),
+ {'choices': transition_states_vocabulary})
+_afs.tag_object_of(('*', 'allowed_transition', '*'), 'main', 'attributes')
+_affk.tag_object_of(('*', 'allowed_transition', '*'),
+ {'choices': transition_states_vocabulary})
+
+# State
+
+def state_transitions_vocabulary(form, field):
+ entity = form.edited_entity
+ if entity.has_eid():
+ wfeid = entity.state_of[0].eid
+ else :
+ eids = form.linked_to.get(('state_of', 'subject'))
+ if not eids:
+ return []
+ wfeid = eids[0]
+ return _wf_items_for_relation(form._cw, wfeid, 'transition_of', field)
+
+_afs.tag_subject_of(('State', 'allowed_transition', '*'), 'main', 'attributes')
+_affk.tag_subject_of(('State', 'allowed_transition', '*'),
+ {'choices': state_transitions_vocabulary})
+
+
+# adaptaters ###################################################################
+
+class WorkflowIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = is_instance('Workflow')
+ # XXX what if workflow of multiple types?
+ def parent_entity(self):
+ return self.entity.workflow_of and self.entity.workflow_of[0] or None
+
+class WorkflowItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = is_instance('BaseTransition', 'State')
+ def parent_entity(self):
+ return self.entity.workflow
+
+class TransitionItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = is_instance('SubWorkflowExitPoint')
+ def parent_entity(self):
+ return self.entity.reverse_subworkflow_exit[0]
+
+class TrInfoIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = is_instance('TrInfo')
+ def parent_entity(self):
+ return self.entity.for_entity
+
+
+# workflow images ##############################################################
+
+class WorkflowDotPropsHandler(DotPropsHandler):
+
+ def node_properties(self, stateortransition):
+ """return default DOT drawing options for a state or transition"""
+ props = super(WorkflowDotPropsHandler, self).node_properties(stateortransition)
+ if hasattr(stateortransition, 'state_of'):
+ props['shape'] = 'box'
+ props['style'] = 'filled'
+ if stateortransition.reverse_initial_state:
+ props['fillcolor'] = '#88CC88'
+ else:
+ props['shape'] = 'ellipse'
+ return props
+
+
+class WorkflowVisitor(object):
+ 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
+ for outgoingstate in transition.potential_destinations():
+ yield transition.eid, outgoingstate.eid, transition
+
+class WorkflowGraphView(DotGraphView):
+ __regid__ = 'wfgraph'
+ __select__ = EntityView.__select__ & one_line_rset() & is_instance('Workflow')
+
+ def build_visitor(self, entity):
+ return WorkflowVisitor(entity)
+
+ def build_dotpropshandler(self):
+ return WorkflowDotPropsHandler(self._cw)
+
+
+@add_metaclass(class_deprecated)
+class TmpPngView(TmpFileViewMixin, EntityView):
+ __deprecation_warning__ = '[3.18] %(cls)s is deprecated'
+ __regid__ = 'tmppng'
+ __select__ = match_form_params('tmpfile')
+ content_type = 'image/png'
+ binary = True
+
+ def cell_call(self, row=0, col=0):
+ key = self._cw.form['tmpfile']
+ if key not in self._cw.session.data:
+ # the temp file is gone and there's nothing
+ # we can do about it
+ # we should probably write it to some well
+ # behaved place and serve it
+ return
+ tmpfile = self._cw.session.data.pop(key)
+ self.w(open(tmpfile, 'rb').read())
+ os.unlink(tmpfile)