|
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 pyams_utils.interfaces import VIEW_PERMISSION |
|
20 from zope.annotation.interfaces import IAttributeAnnotatable |
|
21 from zope.interface.interfaces import IObjectEvent, ObjectEvent |
|
22 from zope.lifecycleevent.interfaces import IObjectCreatedEvent |
|
23 |
|
24 # import packages |
|
25 from pyams_security.schema import Principal |
|
26 from zope.interface import implementer, invariant, Interface, Attribute, Invalid |
|
27 from zope.lifecycleevent import ObjectCreatedEvent |
|
28 from zope.schema import Choice, Datetime, Set, TextLine, Text, List, Object, Int, Bool |
|
29 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm |
|
30 |
|
31 from pyams_workflow import _ |
|
32 |
|
33 |
|
34 MANUAL = 0 |
|
35 AUTOMATIC = 1 |
|
36 SYSTEM = 2 |
|
37 |
|
38 |
|
39 class InvalidTransitionError(Exception): |
|
40 """Base transition error""" |
|
41 |
|
42 def __init__(self, source): |
|
43 self.source = source |
|
44 |
|
45 def __str__(self): |
|
46 return 'source: "%s"' % self.source |
|
47 |
|
48 |
|
49 class NoTransitionAvailableError(InvalidTransitionError): |
|
50 """Exception raised when there is not available transition""" |
|
51 |
|
52 def __init__(self, source, destination): |
|
53 super(NoTransitionAvailableError, self).__init__(source) |
|
54 self.destination = destination |
|
55 |
|
56 def __str__(self): |
|
57 return 'source: "%s" destination: "%s"' % (self.source, self.destination) |
|
58 |
|
59 |
|
60 class AmbiguousTransitionError(InvalidTransitionError): |
|
61 """Exception raised when required transition is ambiguous""" |
|
62 |
|
63 def __init__(self, source, destination): |
|
64 super(AmbiguousTransitionError, self).__init__(source) |
|
65 self.destination = destination |
|
66 |
|
67 def __str__(self): |
|
68 return 'source: "%s" destination: "%s"' % (self.source, self.destination) |
|
69 |
|
70 |
|
71 class VersionError(Exception): |
|
72 """Versions management error""" |
|
73 |
|
74 |
|
75 class ConditionFailedError(Exception): |
|
76 """Exception raised when transition condition failed""" |
|
77 |
|
78 |
|
79 class IWorkflowTransitionEvent(IObjectEvent): |
|
80 """Workflow transition event interface""" |
|
81 |
|
82 wokflow = Attribute("Workflow utility") |
|
83 |
|
84 principal = Attribute("Event principal") |
|
85 |
|
86 source = Attribute('Original state or None if initial state') |
|
87 |
|
88 destination = Attribute('New state') |
|
89 |
|
90 transition = Attribute('Transition that was fired or None if initial state') |
|
91 |
|
92 comment = Attribute('Comment that went with state transition') |
|
93 |
|
94 |
|
95 @implementer(IWorkflowTransitionEvent) |
|
96 class WorkflowTransitionEvent(ObjectEvent): |
|
97 """Workflow transition event""" |
|
98 |
|
99 def __init__(self, object, workflow, principal, source, destination, transition, comment): |
|
100 super(WorkflowTransitionEvent, self).__init__(object) |
|
101 self.workflow = workflow |
|
102 self.principal = principal |
|
103 self.source = source |
|
104 self.destination = destination |
|
105 self.transition = transition |
|
106 self.comment = comment |
|
107 |
|
108 |
|
109 class IWorkflowVersionTransitionEvent(IWorkflowTransitionEvent): |
|
110 """Workflow version transition event interface""" |
|
111 |
|
112 old_object = Attribute('Old version of object') |
|
113 |
|
114 |
|
115 @implementer(IWorkflowVersionTransitionEvent) |
|
116 class WorkflowVersionTransitionEvent(WorkflowTransitionEvent): |
|
117 """Workflow version transition event""" |
|
118 |
|
119 def __init__(self, object, workflow, principal, old_object, source, destination, transition, comment): |
|
120 super(WorkflowVersionTransitionEvent, self).__init__(object, workflow, principal, source, |
|
121 destination, transition, comment) |
|
122 self.old_object = old_object |
|
123 |
|
124 |
|
125 class IObjectClonedEvent(IObjectCreatedEvent): |
|
126 """Object cloned event interface""" |
|
127 |
|
128 source = Attribute("Cloned object source") |
|
129 |
|
130 |
|
131 @implementer(IObjectClonedEvent) |
|
132 class ObjectClonedEvent(ObjectCreatedEvent): |
|
133 """Object cloned event""" |
|
134 |
|
135 def __init__(self, object, source): |
|
136 super(ObjectClonedEvent, self).__init__(object) |
|
137 self.source = source |
|
138 |
|
139 |
|
140 class IWorkflow(Interface): |
|
141 """Defines workflow in the form of transition objects. |
|
142 |
|
143 Defined as a utility. |
|
144 """ |
|
145 |
|
146 initial_state = Attribute("Initial state") |
|
147 |
|
148 update_states = Set(title="Updatable states", |
|
149 description="States of contents which are updatable by standard contributors") |
|
150 |
|
151 readonly_states = Set(title="Read-only states", |
|
152 description="States of contents which can't be modified by anybody") |
|
153 |
|
154 protected_states = Set(title="Protected states", |
|
155 description="States of contents which can only be modified by site administrators") |
|
156 |
|
157 manager_states = Set(title="Manager states", |
|
158 description="States of contents which can be modified by site administrators and content " |
|
159 "managers") |
|
160 |
|
161 published_states = Set(title="Published states", |
|
162 description="States of contents which are published") |
|
163 |
|
164 visible_states = Set(title="Visible staets", |
|
165 description="States of contents which are visible in front-office") |
|
166 |
|
167 waiting_states = Set(title="Waiting states", |
|
168 description="States of contents waiting for action") |
|
169 |
|
170 retired_states = Set(title="Retired states", |
|
171 description="States of contents which are retired but not yet archived") |
|
172 |
|
173 auto_retired_state = Attribute("Auto-retired state") |
|
174 |
|
175 def initialize(self): |
|
176 """Do any needed initialization. |
|
177 |
|
178 Such as initialization with the workflow versions system. |
|
179 """ |
|
180 |
|
181 def refresh(self, transitions): |
|
182 """Refresh workflow completely with new transitions.""" |
|
183 |
|
184 def get_state_label(self, state): |
|
185 """Get given state label""" |
|
186 |
|
187 def get_transitions(self, source): |
|
188 """Get all transitions from source""" |
|
189 |
|
190 def get_transition(self, source, transition_id): |
|
191 """Get transition with transition_id given source state. |
|
192 |
|
193 If the transition is invalid from this source state, |
|
194 an InvalidTransitionError is raised. |
|
195 """ |
|
196 |
|
197 def get_transition_by_id(self, transition_id): |
|
198 """Get transition with transition_id""" |
|
199 |
|
200 |
|
201 class IWorkflowInfo(Interface): |
|
202 """Get workflow info about workflowed object, and drive workflow. |
|
203 |
|
204 Defined as an adapter. |
|
205 """ |
|
206 |
|
207 def set_initial_state(self, state, comment=None): |
|
208 """Set initial state for the context object. |
|
209 |
|
210 Fires a transition event. |
|
211 """ |
|
212 |
|
213 def fire_transition(self, transition_id, comment=None, side_effect=None, check_security=True, principal=None): |
|
214 """Fire a transition for the context object. |
|
215 |
|
216 There's an optional comment parameter that contains some |
|
217 opaque object that offers a comment about the transition. |
|
218 This is useful for manual transitions where users can motivate |
|
219 their actions. |
|
220 |
|
221 There's also an optional side effect parameter which should |
|
222 be a callable which receives the object undergoing the transition |
|
223 as the parameter. This could do an editing action of the newly |
|
224 transitioned workflow object before an actual transition event is |
|
225 fired. |
|
226 |
|
227 If check_security is set to False, security is not checked |
|
228 and an application can fire a transition no matter what the |
|
229 user's permission is. |
|
230 """ |
|
231 |
|
232 def fire_transition_toward(self, state, comment=None, side_effect=None, check_security=True, principal=None): |
|
233 """Fire transition toward state. |
|
234 |
|
235 Looks up a manual transition that will get to the indicated |
|
236 state. |
|
237 |
|
238 If no such transition is possible, NoTransitionAvailableError will |
|
239 be raised. |
|
240 |
|
241 If more than one manual transitions are possible, |
|
242 AmbiguousTransitionError will be raised. |
|
243 """ |
|
244 |
|
245 def fire_transition_for_versions(self, state, transition_id, comment=None, principal=None): |
|
246 """Fire a transition for all versions in a state""" |
|
247 |
|
248 def fire_automatic(self): |
|
249 """Fire automatic transitions if possible by condition""" |
|
250 |
|
251 def has_version(self, state): |
|
252 """Return true if a version exists in given state""" |
|
253 |
|
254 def get_manual_transition_ids(self): |
|
255 """Returns list of valid manual transitions. |
|
256 |
|
257 These transitions have to have a condition that's True. |
|
258 """ |
|
259 |
|
260 def get_manual_transition_ids_toward(self, state): |
|
261 """Returns list of manual transitions towards state""" |
|
262 |
|
263 def get_automatic_transition_ids(self): |
|
264 """Returns list of possible automatic transitions. |
|
265 |
|
266 Condition is not checked. |
|
267 """ |
|
268 |
|
269 def has_automatic_transitions(self): |
|
270 """Return true if there are possible automatic outgoing transitions. |
|
271 |
|
272 Condition is not checked. |
|
273 """ |
|
274 |
|
275 |
|
276 class IWorkflowStateHistoryItem(Interface): |
|
277 """Workflow state history item""" |
|
278 |
|
279 date = Datetime(title="State change datetime", |
|
280 required=True) |
|
281 |
|
282 source_version = Int(title="Source version ID", |
|
283 required=False) |
|
284 |
|
285 source_state = TextLine(title="Transition source state", |
|
286 required=False) |
|
287 |
|
288 target_state = TextLine(title="Transition target state", |
|
289 required=True) |
|
290 |
|
291 transition_id = TextLine(title="Transition ID", |
|
292 required=True) |
|
293 |
|
294 transition = TextLine(title="Transition name", |
|
295 required=True) |
|
296 |
|
297 principal = Principal(title="Transition principal", |
|
298 required=False) |
|
299 |
|
300 comment = Text(title="Transition comment", |
|
301 required=False) |
|
302 |
|
303 |
|
304 class IWorkflowState(Interface): |
|
305 """Store state on workflowed objects. |
|
306 |
|
307 Defined as an adapter. |
|
308 """ |
|
309 |
|
310 version_id = Int(title=_("Version ID")) |
|
311 |
|
312 state = TextLine(title=_("Version state")) |
|
313 |
|
314 state_date = Datetime(title=_("State date"), |
|
315 description=_("Date at which the current state was applied")) |
|
316 |
|
317 state_principal = Principal(title=_("State principal"), |
|
318 description=_("ID of the principal which defined current state")) |
|
319 |
|
320 state_urgency = Bool(title=_("Urgent request?"), |
|
321 required=True, |
|
322 default=False) |
|
323 |
|
324 history = List(title="Workflow states history", |
|
325 value_type=Object(schema=IWorkflowStateHistoryItem)) |
|
326 |
|
327 def get_first_state_date(self, states): |
|
328 """Get first date at which given state was set""" |
|
329 |
|
330 |
|
331 class IWorkflowVersions(Interface): |
|
332 """Interface to get information about versions of content in workflow""" |
|
333 |
|
334 last_version_id = Attribute("Last version ID") |
|
335 |
|
336 def get_version(self, version_id): |
|
337 """Get version matching given id""" |
|
338 |
|
339 def get_versions(self, states=None, sort=False, reverse=False): |
|
340 """Get all versions of object known for this (optional) state""" |
|
341 |
|
342 def get_last_versions(self, count=1): |
|
343 """Get last versions of this object. Set count=0 to get all versions.""" |
|
344 |
|
345 def add_version(self, content, state, principal=None): |
|
346 """Return new unique version id""" |
|
347 |
|
348 def set_state(self, version_id, state, principal=None): |
|
349 """Set new state for given version""" |
|
350 |
|
351 def has_version(self, state): |
|
352 """Return true if a version exists with the specific workflow state""" |
|
353 |
|
354 def remove_version(self, version_id, state='deleted', comment=None, principal=None): |
|
355 """Remove version with given ID""" |
|
356 |
|
357 |
|
358 class IWorkflowStateLabel(Interface): |
|
359 """Workflow state label adapter interface""" |
|
360 |
|
361 def get_label(self, content, request=None, format=True): |
|
362 """Get state label for given content""" |
|
363 |
|
364 |
|
365 class IWorkflowManagedContent(IAttributeAnnotatable): |
|
366 """Workflow managed content""" |
|
367 |
|
368 content_class = Attribute("Content class") |
|
369 |
|
370 workflow_name = Choice(title=_("Workflow name"), |
|
371 description=_("Name of workflow utility managing this content"), |
|
372 required=True, |
|
373 vocabulary='PyAMS workflows') |
|
374 |
|
375 view_permission = Choice(title=_("View permission"), |
|
376 description=_("This permission will be required to display content"), |
|
377 vocabulary='PyAMS permissions', |
|
378 default=VIEW_PERMISSION, |
|
379 required=False) |
|
380 |
|
381 |
|
382 class IWorkflowPublicationSupport(IAttributeAnnotatable): |
|
383 """Workflow publication support""" |
|
384 |
|
385 |
|
386 class IWorkflowVersion(IWorkflowPublicationSupport): |
|
387 """Workflow content version marker interface""" |
|
388 |
|
389 |
|
390 class IWorkflowTransitionInfo(Interface): |
|
391 """Workflow transition info""" |
|
392 |
|
393 transition_id = TextLine(title=_("Transition ID"), |
|
394 required=True) |
|
395 |
|
396 |
|
397 DISPLAY_FIRST_VERSION = 'first' |
|
398 DISPLAY_CURRENT_VERSION = 'current' |
|
399 |
|
400 VERSION_DISPLAY = {DISPLAY_FIRST_VERSION: _("Display first version date"), |
|
401 DISPLAY_CURRENT_VERSION: _("Display current version date")} |
|
402 |
|
403 VERSION_DISPLAY_VOCABULARY = SimpleVocabulary([SimpleTerm(v, title=t) |
|
404 for v, t in VERSION_DISPLAY.items()]) |
|
405 |
|
406 |
|
407 class IWorkflowPublicationInfo(Interface): |
|
408 """Workflow content publication info""" |
|
409 |
|
410 publication_date = Datetime(title=_("Publication date"), |
|
411 description=_("Last date at which content was accepted for publication"), |
|
412 required=False) |
|
413 |
|
414 publisher = Principal(title=_("Publisher"), |
|
415 description=_("Name of the manager who published the document"), |
|
416 required=False) |
|
417 |
|
418 publication = TextLine(title=_("Publication"), |
|
419 description=_("Last publication date and actor"), |
|
420 required=False, |
|
421 readonly=True) |
|
422 |
|
423 first_publication_date = Datetime(title=_("First publication date"), |
|
424 description=_("First date at which content was accepted for publication"), |
|
425 required=False) |
|
426 |
|
427 publication_effective_date = Datetime(title=_("Publication start date"), |
|
428 description=_("Date from which content will be visible"), |
|
429 required=False) |
|
430 |
|
431 push_end_date = Datetime(title=_("Push end date"), |
|
432 description=_("Some contents can be pushed by components to front-office pages; if you " |
|
433 "set a date here, this content will not be pushed anymore passed this " |
|
434 "date, but will still be available via search engine or direct links"), |
|
435 required=False) |
|
436 |
|
437 push_end_date_index = Attribute("Push end date value used by catalog indexes") |
|
438 |
|
439 @invariant |
|
440 def check_push_end_date(self): |
|
441 if self.push_end_date is not None: |
|
442 if self.publication_effective_date is None: |
|
443 raise Invalid(_("Can't define push end date without publication start date!")) |
|
444 if self.publication_effective_date >= self.push_end_date: |
|
445 raise Invalid(_("Push end date must be defined after publication start date!")) |
|
446 if self.publication_expiration_date is not None: |
|
447 if self.publication_expiration_date < self.push_end_date: |
|
448 raise Invalid(_("Push end date must be null or defined before publication end date!")) |
|
449 |
|
450 publication_expiration_date = Datetime(title=_("Publication end date"), |
|
451 description=_("Date past which content will not be visible"), |
|
452 required=False) |
|
453 |
|
454 @invariant |
|
455 def check_expiration_date(self): |
|
456 if self.publication_expiration_date is not None: |
|
457 if self.publication_effective_date is None: |
|
458 raise Invalid(_("Can't define publication end date without publication start date!")) |
|
459 if self.publication_effective_date >= self.publication_expiration_date: |
|
460 raise Invalid(_("Publication end date must be defined after publication start date!")) |
|
461 |
|
462 displayed_publication_date = Choice(title=_("Displayed publication date"), |
|
463 description=_("The matching date will be displayed in front-office"), |
|
464 vocabulary='PyAMS content publication date', |
|
465 default=DISPLAY_FIRST_VERSION, |
|
466 required=True) |
|
467 |
|
468 visible_publication_date = Attribute("Visible publication date") |
|
469 |
|
470 def reset(self, complete=True): |
|
471 """Reset all publication info (used by clone features) |
|
472 |
|
473 If 'complete' argument is True, all date fields are reset; otherwise, push and publication end dates are |
|
474 preserved in new versions. |
|
475 """ |
|
476 |
|
477 def is_published(self, check_parent=True): |
|
478 """Is the content published?""" |
|
479 |
|
480 def is_visible(self, request=None, check_parent=True): |
|
481 """Is the content visible?""" |
|
482 |
|
483 |
|
484 class IWorkflowRequestUrgencyInfo(Interface): |
|
485 """Workflow request urgency info""" |
|
486 |
|
487 urgent_request = Bool(title=_("Urgent request?"), |
|
488 description=_("Please use this option only when really needed..."), |
|
489 required=True, |
|
490 default=False) |
|
491 |
|
492 |
|
493 class IWorkflowCommentInfo(Interface): |
|
494 """Workflow comment info""" |
|
495 |
|
496 comment = Text(title=_("Comment"), |
|
497 description=_("Comment associated with this operation"), |
|
498 required=False) |
|
499 |
|
500 |
|
501 class IWorkflowManagementTask(Interface): |
|
502 """Workflow management task marker interface""" |