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