|
1 # |
|
2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net> |
|
3 # All Rights Reserved. |
|
4 # |
|
5 # This software is subject to the provisions of the Zope Public License, |
|
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
|
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
|
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
|
10 # FOR A PARTICULAR PURPOSE. |
|
11 # |
|
12 |
|
13 __docformat__ = 'restructuredtext' |
|
14 |
|
15 |
|
16 # import standard library |
|
17 |
|
18 # import interfaces |
|
19 from zope.annotation.interfaces import IAttributeAnnotatable |
|
20 from zope.interface.interfaces import IObjectEvent, ObjectEvent |
|
21 |
|
22 # import packages |
|
23 from pyams_security.schema import Principal |
|
24 from zope.interface import implementer, invariant, Interface, Attribute, Invalid |
|
25 from zope.schema import Choice, Datetime, Set, TextLine, Text, List, Object, Int |
|
26 |
|
27 from pyams_workflow import _ |
|
28 |
|
29 |
|
30 MANUAL = 0 |
|
31 AUTOMATIC = 1 |
|
32 SYSTEM = 2 |
|
33 |
|
34 |
|
35 class InvalidTransitionError(Exception): |
|
36 """Base transition error""" |
|
37 |
|
38 def __init__(self, source): |
|
39 self.source = source |
|
40 |
|
41 def __str__(self): |
|
42 return 'source: "%s"' % self.source |
|
43 |
|
44 |
|
45 class NoTransitionAvailableError(InvalidTransitionError): |
|
46 """Exception raised when there is not available transition""" |
|
47 |
|
48 def __init__(self, source, destination): |
|
49 super(NoTransitionAvailableError, self).__init__(source) |
|
50 self.destination = destination |
|
51 |
|
52 def __str__(self): |
|
53 return 'source: "%s" destination: "%s"' % (self.source, self.destination) |
|
54 |
|
55 |
|
56 class AmbiguousTransitionError(InvalidTransitionError): |
|
57 """Exception raised when required transition is ambiguous""" |
|
58 |
|
59 def __init__(self, source, destination): |
|
60 super(AmbiguousTransitionError, self).__init__(source) |
|
61 self.destination = destination |
|
62 |
|
63 def __str__(self): |
|
64 return 'source: "%s" destination: "%s"' % (self.source, self.destination) |
|
65 |
|
66 |
|
67 class VersionError(Exception): |
|
68 """Versions management error""" |
|
69 |
|
70 |
|
71 class ConditionFailedError(Exception): |
|
72 """Exception raised when transition condition failed""" |
|
73 |
|
74 |
|
75 class IWorkflowTransitionEvent(IObjectEvent): |
|
76 """Workflow transition event interface""" |
|
77 |
|
78 source = Attribute('Original state or None if initial state') |
|
79 |
|
80 destination = Attribute('New state') |
|
81 |
|
82 transition = Attribute('Transition that was fired or None if initial state') |
|
83 |
|
84 comment = Attribute('Comment that went with state transition') |
|
85 |
|
86 |
|
87 @implementer(IWorkflowTransitionEvent) |
|
88 class WorkflowTransitionEvent(ObjectEvent): |
|
89 """Workflow transition event""" |
|
90 |
|
91 def __init__(self, object, source, destination, transition, comment): |
|
92 super(WorkflowTransitionEvent, self).__init__(object) |
|
93 self.source = source |
|
94 self.destination = destination |
|
95 self.transition = transition |
|
96 self.comment = comment |
|
97 |
|
98 |
|
99 class IWorkflowVersionTransitionEvent(IWorkflowTransitionEvent): |
|
100 """Workflow version transition event interface""" |
|
101 |
|
102 old_object = Attribute('Old version of object') |
|
103 |
|
104 |
|
105 @implementer(IWorkflowVersionTransitionEvent) |
|
106 class WorkflowVersionTransitionEvent(WorkflowTransitionEvent): |
|
107 """Workflow version transition event""" |
|
108 |
|
109 def __init__(self, object, old_object, source, destination, transition, comment): |
|
110 super(WorkflowVersionTransitionEvent, self).__init__(object, source, destination, transition, comment) |
|
111 self.old_object = old_object |
|
112 |
|
113 |
|
114 class IWorkflow(Interface): |
|
115 """Defines workflow in the form of transition objects. |
|
116 |
|
117 Defined as a utility. |
|
118 """ |
|
119 |
|
120 published_states = Set(title="Published states") |
|
121 |
|
122 def initialize(self): |
|
123 """Do any needed initialization. |
|
124 |
|
125 Such as initialization with the workflow versions system. |
|
126 """ |
|
127 |
|
128 def refresh(self, transitions): |
|
129 """Refresh workflow completely with new transitions.""" |
|
130 |
|
131 def get_transitions(self, source): |
|
132 """Get all transitions from source""" |
|
133 |
|
134 def get_transition(self, source, transition_id): |
|
135 """Get transition with transition_id given source state. |
|
136 |
|
137 If the transition is invalid from this source state, |
|
138 an InvalidTransitionError is raised. |
|
139 """ |
|
140 |
|
141 def get_transition_by_id(self, transition_id): |
|
142 """Get transition with transition_id""" |
|
143 |
|
144 |
|
145 class IWorkflowInfo(Interface): |
|
146 """Get workflow info about workflowed object, and drive workflow. |
|
147 |
|
148 Defined as an adapter. |
|
149 """ |
|
150 |
|
151 def set_initial_state(self, state, comment=None): |
|
152 """Set initial state for the context object. |
|
153 |
|
154 Fires a transition event. |
|
155 """ |
|
156 |
|
157 def fire_transition(self, transition_id, comment=None, side_effect=None, check_security=True): |
|
158 """Fire a transition for the context object. |
|
159 |
|
160 There's an optional comment parameter that contains some |
|
161 opaque object that offers a comment about the transition. |
|
162 This is useful for manual transitions where users can motivate |
|
163 their actions. |
|
164 |
|
165 There's also an optional side effect parameter which should |
|
166 be a callable which receives the object undergoing the transition |
|
167 as the parameter. This could do an editing action of the newly |
|
168 transitioned workflow object before an actual transition event is |
|
169 fired. |
|
170 |
|
171 If check_security is set to False, security is not checked |
|
172 and an application can fire a transition no matter what the |
|
173 user's permission is. |
|
174 """ |
|
175 |
|
176 def fire_transition_toward(self, state, comment=None, side_effect=None, check_security=True): |
|
177 """Fire transition toward state. |
|
178 |
|
179 Looks up a manual transition that will get to the indicated |
|
180 state. |
|
181 |
|
182 If no such transition is possible, NoTransitionAvailableError will |
|
183 be raised. |
|
184 |
|
185 If more than one manual transitions are possible, |
|
186 AmbiguousTransitionError will be raised. |
|
187 """ |
|
188 |
|
189 def fire_transition_for_versions(self, state, transition_id, comment=None): |
|
190 """Fire a transition for all versions in a state""" |
|
191 |
|
192 def fire_automatic(self): |
|
193 """Fire automatic transitions if possible by condition""" |
|
194 |
|
195 def has_version(self, state): |
|
196 """Return true if a version exists in given state""" |
|
197 |
|
198 def get_manual_transition_ids(self): |
|
199 """Returns list of valid manual transitions. |
|
200 |
|
201 These transitions have to have a condition that's True. |
|
202 """ |
|
203 |
|
204 def get_manual_transition_ids_toward(self, state): |
|
205 """Returns list of manual transitions towards state""" |
|
206 |
|
207 def get_automatic_transition_ids(self): |
|
208 """Returns list of possible automatic transitions. |
|
209 |
|
210 Condition is not checked. |
|
211 """ |
|
212 |
|
213 def has_automatic_transitions(self): |
|
214 """Return true if there are possible automatic outgoing transitions. |
|
215 |
|
216 Condition is not checked. |
|
217 """ |
|
218 |
|
219 |
|
220 class IWorkflowStateHistoryItem(Interface): |
|
221 """Workflow state history item""" |
|
222 |
|
223 date = Datetime(title="State change datetime", |
|
224 required=True) |
|
225 |
|
226 source_version = Int(title="Source version ID", |
|
227 required=False) |
|
228 |
|
229 source_state = TextLine(title="Transition source state", |
|
230 required=False) |
|
231 |
|
232 target_state = TextLine(title="Transition target state", |
|
233 required=True) |
|
234 |
|
235 transition = TextLine(title="Transition name", |
|
236 required=True) |
|
237 |
|
238 principal = Principal(title="Transition principal", |
|
239 required=False) |
|
240 |
|
241 comment = Text(title="Transition comment", |
|
242 required=False) |
|
243 |
|
244 |
|
245 class IWorkflowState(Interface): |
|
246 """Store state on workflowed objects. |
|
247 |
|
248 Defined as an adapter. |
|
249 """ |
|
250 |
|
251 version_id = Attribute("Version ID") |
|
252 |
|
253 state = Attribute("Version state") |
|
254 |
|
255 history = List(title="Workflow states history", |
|
256 value_type=Object(schema=IWorkflowStateHistoryItem)) |
|
257 |
|
258 |
|
259 class IWorkflowVersions(Interface): |
|
260 """Interface to get information about versions of content in workflow""" |
|
261 |
|
262 last_version_id = Attribute("Last version ID") |
|
263 |
|
264 def get_version(self, version_id): |
|
265 """Get version matching given id""" |
|
266 |
|
267 def get_versions(self, state=None): |
|
268 """Get all versions of object known for this (optional) state""" |
|
269 |
|
270 def add_version(self, content, state): |
|
271 """Return new unique version id""" |
|
272 |
|
273 def set_state(self, version_id, state): |
|
274 """Set new state for given version""" |
|
275 |
|
276 def has_version(self, state): |
|
277 """Return true if a version exists with the specific workflow state""" |
|
278 |
|
279 def remove_version(self, version_id): |
|
280 """Remove version with given ID""" |
|
281 |
|
282 |
|
283 class IWorkflowManagedContent(IAttributeAnnotatable): |
|
284 """Workflow managed content""" |
|
285 |
|
286 content_class = Attribute("Content class") |
|
287 |
|
288 workflow_name = Choice(title=_("Workflow name"), |
|
289 description=_("Name of workflow utility managing this content"), |
|
290 required=True, |
|
291 vocabulary='PyAMS workflows') |
|
292 |
|
293 view_permission = Choice(title=_("View permission"), |
|
294 description=_("This permission will be required to display content"), |
|
295 vocabulary='PyAMS permissions', |
|
296 required=False) |
|
297 |
|
298 |
|
299 class IWorkflowPublicationSupport(IAttributeAnnotatable): |
|
300 """Workflow publication support""" |
|
301 |
|
302 |
|
303 class IWorkflowVersion(IWorkflowPublicationSupport): |
|
304 """Workflow content version marker interface""" |
|
305 |
|
306 |
|
307 class IWorkflowPublicationInfo(Interface): |
|
308 """Workflow content publication info""" |
|
309 |
|
310 state_date = Datetime(title=_("State date"), |
|
311 description=_("Date at which the current state was applied"), |
|
312 readonly=True) |
|
313 |
|
314 state_principal = Principal(title=_("State principal"), |
|
315 description=_("ID of the principal which defined current state")) |
|
316 |
|
317 publication_date = Datetime(title=_("Publication date"), |
|
318 description=_("Last date at which content was accepted for publication"), |
|
319 required=False) |
|
320 |
|
321 first_publication_date = Datetime(title=_("First publication date"), |
|
322 description=_("First date at which content was accepted for publication"), |
|
323 required=False) |
|
324 |
|
325 publication_effective_date = Datetime(title=_("Publication start date"), |
|
326 description=_("Date from which content will be visible"), |
|
327 required=False) |
|
328 |
|
329 publication_expiration_date = Datetime(title=_("Publication end date"), |
|
330 description=_("Date past which content will not be visible"), |
|
331 required=False) |
|
332 |
|
333 @invariant |
|
334 def check_expiration_date(self): |
|
335 if self.publication_expiration_date is not None: |
|
336 if self.publication_effective_date is None: |
|
337 raise Invalid(_("Can't define publication end date without publication start date!")) |
|
338 if self.publication_effective_date >= self.publication_expiration_date: |
|
339 raise Invalid(_("Publication end date must be defined after publication start date!")) |
|
340 |
|
341 def reset(self): |
|
342 """Reset all publication info (used by clone features)""" |
|
343 |
|
344 def is_published(self): |
|
345 """Is the content published?""" |
|
346 |
|
347 def is_visible(self, request=None): |
|
348 """Is the content visible?""" |
|
349 |
|
350 |
|
351 class IWorkflowCommentInfo(Interface): |
|
352 """Workflow comment info""" |
|
353 |
|
354 comment = Text(title=_("Comment"), |
|
355 description=_("Comment associated with this operation"), |
|
356 required=False) |