|
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 from datetime import datetime |
|
18 |
|
19 # import interfaces |
|
20 from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION, MANAGE_CONTENT_PERMISSION, PUBLISH_CONTENT_PERMISSION, \ |
|
21 CREATE_CONTENT_PERMISSION |
|
22 from pyams_content.shared.common.interfaces import IWfSharedContentRoles, IManagerRestrictions |
|
23 from pyams_content.workflow.interfaces import IContentWorkflow |
|
24 from pyams_security.interfaces import IRoleProtectedObject, ISecurityManager |
|
25 from pyams_workflow.interfaces import IWorkflow, AUTOMATIC, IWorkflowPublicationInfo, SYSTEM, IWorkflowVersions, \ |
|
26 IWorkflowState, ObjectClonedEvent, IWorkflowInfo, IWorkflowStateLabel |
|
27 |
|
28 # import packages |
|
29 from pyams_utils.adapter import adapter_config, ContextAdapter |
|
30 from pyams_utils.registry import utility_config, get_utility |
|
31 from pyams_utils.request import check_request |
|
32 from pyams_workflow.workflow import Transition, Workflow |
|
33 from pyramid.threadlocal import get_current_registry |
|
34 from zope.copy import copy |
|
35 from zope.interface import implementer |
|
36 from zope.location import locate |
|
37 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm |
|
38 |
|
39 from pyams_content import _ |
|
40 |
|
41 |
|
42 # |
|
43 # Workflow states |
|
44 # |
|
45 |
|
46 DRAFT = 'draft' |
|
47 PROPOSED = 'proposed' |
|
48 CANCELED = 'canceled' |
|
49 REFUSED = 'refused' |
|
50 PUBLISHED = 'published' |
|
51 RETIRING = 'retiring' |
|
52 RETIRED = 'retired' |
|
53 ARCHIVING = 'archiving' |
|
54 ARCHIVED = 'archived' |
|
55 DELETED = 'deleted' |
|
56 |
|
57 STATES_IDS = (DRAFT, |
|
58 PROPOSED, |
|
59 CANCELED, |
|
60 REFUSED, |
|
61 PUBLISHED, |
|
62 RETIRING, |
|
63 RETIRED, |
|
64 ARCHIVING, |
|
65 ARCHIVED, |
|
66 DELETED) |
|
67 |
|
68 UPDATE_STATES = (DRAFT, RETIRED) |
|
69 |
|
70 READONLY_STATES = (ARCHIVED, DELETED) |
|
71 |
|
72 PROTECTED_STATES = (PUBLISHED, RETIRING, ARCHIVING) |
|
73 |
|
74 MANAGER_STATES = (PROPOSED, ) |
|
75 |
|
76 VISIBLE_STATES = PUBLISHED_STATES = (PUBLISHED, RETIRING) |
|
77 |
|
78 WAITING_STATES = (PROPOSED, RETIRING, ARCHIVING) |
|
79 |
|
80 RETIRED_STATES = (RETIRED, ARCHIVING) |
|
81 |
|
82 STATES_LABELS = (_("Draft"), |
|
83 _("Proposed"), |
|
84 _("Canceled"), |
|
85 _("Refused"), |
|
86 _("Published"), |
|
87 _("Retiring"), |
|
88 _("Retired"), |
|
89 _("Archiving"), |
|
90 _("Archived"), |
|
91 _("Deleted")) |
|
92 |
|
93 STATES_HEADERS = {DRAFT: _("draft created by {principal}"), |
|
94 PROPOSED: _("publication requested by {principal}"), |
|
95 PUBLISHED: _("published by {principal}"), |
|
96 RETIRING: _("retiring requested by {principal}"), |
|
97 RETIRED: _("retired by {principal}"), |
|
98 ARCHIVING: _("archiving requested by {principal}"), |
|
99 ARCHIVED: _("archived by {principal}")} |
|
100 |
|
101 STATES_VOCABULARY = SimpleVocabulary([SimpleTerm(STATES_IDS[i], title=t) |
|
102 for i, t in enumerate(STATES_LABELS)]) |
|
103 |
|
104 |
|
105 # |
|
106 # Workflow conditions |
|
107 # |
|
108 |
|
109 def can_propose_content(wf, context): |
|
110 """Check if a content can be proposed""" |
|
111 versions = IWorkflowVersions(context) |
|
112 if versions.has_version(PROPOSED): |
|
113 return False |
|
114 request = check_request() |
|
115 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context): |
|
116 return True |
|
117 if request.principal.id in context.owner | {context.creator} | context.contributors: |
|
118 return True |
|
119 return False |
|
120 |
|
121 |
|
122 def can_backdraft_content(wf, context): |
|
123 """Check if content can return to DRAFT state""" |
|
124 return IWorkflowPublicationInfo(context).publication_date is None |
|
125 |
|
126 |
|
127 def can_retire_content(wf, context): |
|
128 """Check if already published content can return to RETIRED state""" |
|
129 return IWorkflowPublicationInfo(context).publication_date is not None |
|
130 |
|
131 |
|
132 def can_create_new_version(wf, context): |
|
133 """Check if we can create a new version""" |
|
134 versions = IWorkflowVersions(context) |
|
135 if (versions.has_version(DRAFT) or |
|
136 versions.has_version(PROPOSED) or |
|
137 versions.has_version(CANCELED) or |
|
138 versions.has_version(REFUSED)): |
|
139 return False |
|
140 request = check_request() |
|
141 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context): |
|
142 return True |
|
143 if request.principal.id in context.owner | {context.creator} | context.contributors: |
|
144 return True |
|
145 return False |
|
146 |
|
147 |
|
148 def can_delete_version(wf, context): |
|
149 """Check if we can delete a draft version""" |
|
150 request = check_request() |
|
151 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context): |
|
152 return True |
|
153 return request.principal.id in context.owner | {context.creator} | context.contributors |
|
154 |
|
155 |
|
156 def can_manage_content(wf, context): |
|
157 """Check if a manager can handle content""" |
|
158 request = check_request() |
|
159 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context): |
|
160 return True |
|
161 if request.principal.id in context.managers: |
|
162 return True |
|
163 restrictions = IManagerRestrictions(context).get_restrictions(request.principal.id) |
|
164 return restrictions and restrictions.check_access(context, permission=PUBLISH_CONTENT_PERMISSION, request=request) |
|
165 |
|
166 |
|
167 def can_cancel_operation(wf, context): |
|
168 """Check if we can cancel a request""" |
|
169 request = check_request() |
|
170 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context): |
|
171 return True |
|
172 if request.principal.id in context.owner | {context.creator} | context.contributors: |
|
173 return True |
|
174 return request.principal.id == IWorkflowState(context).state_principal |
|
175 |
|
176 |
|
177 # |
|
178 # Workflow actions |
|
179 # |
|
180 |
|
181 def publish_action(wf, context): |
|
182 """Publish version""" |
|
183 IWorkflowPublicationInfo(context).publication_date = datetime.utcnow() |
|
184 translate = check_request().localizer.translate |
|
185 version_id = IWorkflowState(context).version_id |
|
186 for version in IWorkflowVersions(context).get_versions((PUBLISHED, RETIRING, RETIRED, ARCHIVING)): |
|
187 if version is not context: |
|
188 IWorkflowInfo(version).fire_transition_toward('archived', |
|
189 comment=translate(_("Published version {0}")).format(version_id)) |
|
190 |
|
191 |
|
192 def archive_action(wf, context): |
|
193 """Remove readers when a content is archived""" |
|
194 roles = IWfSharedContentRoles(context, None) |
|
195 if roles is not None: |
|
196 IRoleProtectedObject(context).revoke_role('pyams.Reader', roles.readers) |
|
197 |
|
198 |
|
199 def clone_action(wf, context): |
|
200 """Create new version""" |
|
201 result = copy(context) |
|
202 locate(result, context.__parent__) |
|
203 registry = get_current_registry() |
|
204 registry.notify(ObjectClonedEvent(result, context)) |
|
205 return result |
|
206 |
|
207 |
|
208 def delete_action(wf, context): |
|
209 """Delete draft version, and parent if single version""" |
|
210 versions = IWorkflowVersions(context) |
|
211 versions.remove_version(IWorkflowState(context).version_id) |
|
212 |
|
213 |
|
214 # |
|
215 # Workflow transitions |
|
216 # |
|
217 |
|
218 init = Transition(transition_id='init', |
|
219 title=_("Initialize"), |
|
220 source=None, |
|
221 destination=DRAFT, |
|
222 history_label=_("Draft creation")) |
|
223 |
|
224 draft_to_proposed = Transition(transition_id='draft_to_proposed', |
|
225 title=_("Propose publication"), |
|
226 source=DRAFT, |
|
227 destination=PROPOSED, |
|
228 permission=MANAGE_CONTENT_PERMISSION, |
|
229 condition=can_propose_content, |
|
230 menu_css_class='fa fa-fw fa-question', |
|
231 view_name='wf-propose.html', |
|
232 history_label=_("Publication request"), |
|
233 next_step=_("content managers authorized to take charge of your content are going to " |
|
234 "be notified of your request."), |
|
235 order=1) |
|
236 |
|
237 retired_to_proposed = Transition(transition_id='retired_to_proposed', |
|
238 title=_("Propose publication"), |
|
239 source=RETIRED, |
|
240 destination=PROPOSED, |
|
241 permission=MANAGE_CONTENT_PERMISSION, |
|
242 condition=can_propose_content, |
|
243 menu_css_class='fa fa-fw fa-question', |
|
244 view_name='wf-propose.html', |
|
245 history_label=_("Publication request"), |
|
246 next_step=_("content managers authorized to take charge of your content are going to " |
|
247 "be notified of your request."), |
|
248 order=1) |
|
249 |
|
250 proposed_to_canceled = Transition(transition_id='proposed_to_canceled', |
|
251 title=_("Cancel publication request"), |
|
252 source=PROPOSED, |
|
253 destination=CANCELED, |
|
254 permission=MANAGE_CONTENT_PERMISSION, |
|
255 condition=can_cancel_operation, |
|
256 menu_css_class='fa fa-fw fa-mail-reply', |
|
257 view_name='wf-cancel-propose.html', |
|
258 history_label=_("Publication request canceled"), |
|
259 order=2) |
|
260 |
|
261 canceled_to_draft = Transition(transition_id='canceled_to_draft', |
|
262 title=_("Reset canceled publication to draft"), |
|
263 source=CANCELED, |
|
264 destination=DRAFT, |
|
265 trigger=AUTOMATIC, |
|
266 history_label=_("State reset to 'draft' (automatic)"), |
|
267 condition=can_backdraft_content) |
|
268 |
|
269 canceled_to_retired = Transition(transition_id='canceled_to_retired', |
|
270 title=_("Reset canceled publication to retired"), |
|
271 source=CANCELED, |
|
272 destination=RETIRED, |
|
273 trigger=AUTOMATIC, |
|
274 history_label=_("State reset to 'retired' (automatic)"), |
|
275 condition=can_retire_content) |
|
276 |
|
277 proposed_to_refused = Transition(transition_id='proposed_to_refused', |
|
278 title=_("Refuse publication"), |
|
279 source=PROPOSED, |
|
280 destination=REFUSED, |
|
281 permission=PUBLISH_CONTENT_PERMISSION, |
|
282 condition=can_manage_content, |
|
283 menu_css_class='fa fa-fw fa-thumbs-o-down', |
|
284 view_name='wf-refuse.html', |
|
285 history_label=_("Publication refused"), |
|
286 order=3) |
|
287 |
|
288 refused_to_draft = Transition(transition_id='refused_to_draft', |
|
289 title=_("Reset refused publication to draft"), |
|
290 source=REFUSED, |
|
291 destination=DRAFT, |
|
292 trigger=AUTOMATIC, |
|
293 history_label=_("State reset to 'draft' (automatic)"), |
|
294 condition=can_backdraft_content) |
|
295 |
|
296 refused_to_retired = Transition(transition_id='refused_to_retired', |
|
297 title=_("Reset refused publication to retired"), |
|
298 source=REFUSED, |
|
299 destination=RETIRED, |
|
300 trigger=AUTOMATIC, |
|
301 history_label=_("State reset to 'refused' (automatic)"), |
|
302 condition=can_retire_content) |
|
303 |
|
304 proposed_to_published = Transition(transition_id='proposed_to_published', |
|
305 title=_("Publish content"), |
|
306 source=PROPOSED, |
|
307 destination=PUBLISHED, |
|
308 permission=PUBLISH_CONTENT_PERMISSION, |
|
309 condition=can_manage_content, |
|
310 action=publish_action, |
|
311 menu_css_class='fa fa-fw fa-thumbs-o-up', |
|
312 view_name='wf-publish.html', |
|
313 history_label=_("Content published"), |
|
314 order=4) |
|
315 |
|
316 published_to_retiring = Transition(transition_id='published_to_retiring', |
|
317 title=_("Request retiring"), |
|
318 source=PUBLISHED, |
|
319 destination=RETIRING, |
|
320 permission=MANAGE_CONTENT_PERMISSION, |
|
321 menu_css_class='fa fa-fw fa-pause', |
|
322 view_name='wf-retiring.html', |
|
323 history_label=_("Retire request"), |
|
324 next_step=_("content managers authorized to take charge of your content are going " |
|
325 "to be notified of your request."), |
|
326 order=7) |
|
327 |
|
328 retiring_to_published = Transition(transition_id='retiring_to_published', |
|
329 title=_("Cancel retiring request"), |
|
330 source=RETIRING, |
|
331 destination=PUBLISHED, |
|
332 permission=MANAGE_CONTENT_PERMISSION, |
|
333 condition=can_cancel_operation, |
|
334 menu_css_class='fa fa-fw fa-mail-reply', |
|
335 view_name='wf-cancel-retiring.html', |
|
336 history_label=_("Retire request canceled"), |
|
337 order=8) |
|
338 |
|
339 retiring_to_retired = Transition(transition_id='retiring_to_retired', |
|
340 title=_("Retire content"), |
|
341 source=RETIRING, |
|
342 destination=RETIRED, |
|
343 permission=PUBLISH_CONTENT_PERMISSION, |
|
344 condition=can_manage_content, |
|
345 menu_css_class='fa fa-fw fa-stop', |
|
346 view_name='wf-retire.html', |
|
347 history_label=_("Content retired"), |
|
348 order=9) |
|
349 |
|
350 retired_to_archiving = Transition(transition_id='retired_to_archiving', |
|
351 title=_("Request archive"), |
|
352 source=RETIRED, |
|
353 destination=ARCHIVING, |
|
354 permission=MANAGE_CONTENT_PERMISSION, |
|
355 menu_css_class='fa fa-fw fa-archive', |
|
356 view_name='wf-archiving.html', |
|
357 history_label=_("Archive request"), |
|
358 next_step=_("content managers authorized to take charge of your content are going to " |
|
359 "be notified of your request."), |
|
360 order=10) |
|
361 |
|
362 archiving_to_retired = Transition(transition_id='archiving_to_retired', |
|
363 title=_("Cancel archiving request"), |
|
364 source=ARCHIVING, |
|
365 destination=RETIRED, |
|
366 permission=MANAGE_CONTENT_PERMISSION, |
|
367 condition=can_cancel_operation, |
|
368 menu_css_class='fa fa-fw fa-mail-reply', |
|
369 view_name='wf-cancel-archiving.html', |
|
370 history_label=_("Archive request canceled"), |
|
371 order=11) |
|
372 |
|
373 archiving_to_archived = Transition(transition_id='archiving_to_archived', |
|
374 title=_("Archive content"), |
|
375 source=ARCHIVING, |
|
376 destination=ARCHIVED, |
|
377 permission=PUBLISH_CONTENT_PERMISSION, |
|
378 condition=can_manage_content, |
|
379 action=archive_action, |
|
380 menu_css_class='fa fa-fw fa-archive', |
|
381 view_name='wf-archive.html', |
|
382 history_label=_("Content archived"), |
|
383 order=12) |
|
384 |
|
385 published_to_archived = Transition(transition_id='published_to_archived', |
|
386 title=_("Archive published content"), |
|
387 source=PUBLISHED, |
|
388 destination=ARCHIVED, |
|
389 trigger=SYSTEM, |
|
390 history_label=_("Content archived after version publication"), |
|
391 action=archive_action) |
|
392 |
|
393 retiring_to_archived = Transition(transition_id='retiring_to_archived', |
|
394 title=_("Archive retiring content"), |
|
395 source=RETIRING, |
|
396 destination=ARCHIVED, |
|
397 trigger=SYSTEM, |
|
398 history_label=_("Content archived after version publication"), |
|
399 action=archive_action) |
|
400 |
|
401 retired_to_archived = Transition(transition_id='retired_to_archived', |
|
402 title=_("Archive retired content"), |
|
403 source=RETIRED, |
|
404 destination=ARCHIVED, |
|
405 trigger=SYSTEM, |
|
406 history_label=_("Content archived after version publication"), |
|
407 action=archive_action) |
|
408 |
|
409 published_to_draft = Transition(transition_id='published_to_draft', |
|
410 title=_("Create new version"), |
|
411 source=PUBLISHED, |
|
412 destination=DRAFT, |
|
413 permission=CREATE_CONTENT_PERMISSION, |
|
414 condition=can_create_new_version, |
|
415 action=clone_action, |
|
416 menu_css_class='fa fa-fw fa-file-o', |
|
417 view_name='wf-clone.html', |
|
418 history_label=_("New version created"), |
|
419 order=13) |
|
420 |
|
421 retiring_to_draft = Transition(transition_id='retiring_to_draft', |
|
422 title=_("Create new version"), |
|
423 source=RETIRING, |
|
424 destination=DRAFT, |
|
425 permission=CREATE_CONTENT_PERMISSION, |
|
426 condition=can_create_new_version, |
|
427 action=clone_action, |
|
428 menu_css_class='fa fa-fw fa-file-o', |
|
429 view_name='wf-clone.html', |
|
430 history_label=_("New version created"), |
|
431 order=14) |
|
432 |
|
433 retired_to_draft = Transition(transition_id='retired_to_draft', |
|
434 title=_("Create new version"), |
|
435 source=RETIRED, |
|
436 destination=DRAFT, |
|
437 permission=CREATE_CONTENT_PERMISSION, |
|
438 condition=can_create_new_version, |
|
439 action=clone_action, |
|
440 menu_css_class='fa fa-fw fa-file-o', |
|
441 view_name='wf-clone.html', |
|
442 history_label=_("New version created"), |
|
443 order=15) |
|
444 |
|
445 archiving_to_draft = Transition(transition_id='archiving_to_draft', |
|
446 title=_("Create new version"), |
|
447 source=ARCHIVING, |
|
448 destination=DRAFT, |
|
449 permission=CREATE_CONTENT_PERMISSION, |
|
450 condition=can_create_new_version, |
|
451 action=clone_action, |
|
452 menu_css_class='fa fa-fw fa-file-o', |
|
453 view_name='wf-clone.html', |
|
454 history_label=_("New version created"), |
|
455 order=16) |
|
456 |
|
457 archived_to_draft = Transition(transition_id='archived_to_draft', |
|
458 title=_("Create new version"), |
|
459 source=ARCHIVED, |
|
460 destination=DRAFT, |
|
461 permission=CREATE_CONTENT_PERMISSION, |
|
462 condition=can_create_new_version, |
|
463 action=clone_action, |
|
464 menu_css_class='fa fa-fw fa-file-o', |
|
465 view_name='wf-clone.html', |
|
466 history_label=_("New version created"), |
|
467 order=17) |
|
468 |
|
469 delete = Transition(transition_id='delete', |
|
470 title=_("Delete version"), |
|
471 source=DRAFT, |
|
472 destination=DELETED, |
|
473 permission=MANAGE_CONTENT_PERMISSION, |
|
474 condition=can_delete_version, |
|
475 action=delete_action, |
|
476 menu_css_class='fa fa-fw fa-trash', |
|
477 view_name='wf-delete.html', |
|
478 history_label=_("Version deleted"), |
|
479 order=18) |
|
480 |
|
481 wf_transitions = [init, |
|
482 draft_to_proposed, |
|
483 retired_to_proposed, |
|
484 proposed_to_canceled, |
|
485 canceled_to_draft, |
|
486 canceled_to_retired, |
|
487 proposed_to_refused, |
|
488 refused_to_draft, |
|
489 refused_to_retired, |
|
490 proposed_to_published, |
|
491 published_to_retiring, |
|
492 retiring_to_published, |
|
493 retiring_to_retired, |
|
494 retired_to_archiving, |
|
495 archiving_to_retired, |
|
496 published_to_archived, |
|
497 retiring_to_archived, |
|
498 retired_to_archived, |
|
499 archiving_to_archived, |
|
500 published_to_draft, |
|
501 retiring_to_draft, |
|
502 retired_to_draft, |
|
503 archiving_to_draft, |
|
504 archived_to_draft, |
|
505 delete] |
|
506 |
|
507 |
|
508 @implementer(IContentWorkflow) |
|
509 class ContentWorkflow(Workflow): |
|
510 """PyAMS default content workflow""" |
|
511 |
|
512 |
|
513 @adapter_config(context=IContentWorkflow, provides=IWorkflowStateLabel) |
|
514 class WorkflowStateLabelAdapter(ContextAdapter): |
|
515 """Generic state label adapter""" |
|
516 |
|
517 @staticmethod |
|
518 def get_label(content, request=None, format=True): |
|
519 if request is None: |
|
520 request = check_request() |
|
521 translate = request.localizer.translate |
|
522 security = get_utility(ISecurityManager) |
|
523 state = IWorkflowState(content) |
|
524 state_label = translate(STATES_HEADERS[state.state]) |
|
525 if format: |
|
526 state_label = state_label.format(principal=security.get_principal(state.state_principal).title) |
|
527 return state_label |
|
528 |
|
529 |
|
530 @adapter_config(name=DRAFT, context=IContentWorkflow, provides=IWorkflowStateLabel) |
|
531 class DraftWorkflowStateLabelAdapter(ContextAdapter): |
|
532 """Draft state label adapter""" |
|
533 |
|
534 @staticmethod |
|
535 def get_label(content, request=None, format=True): |
|
536 if request is None: |
|
537 request = check_request() |
|
538 translate = request.localizer.translate |
|
539 security = get_utility(ISecurityManager) |
|
540 state = IWorkflowState(content) |
|
541 if len(state.history) == 1: |
|
542 state_label = translate(STATES_HEADERS[state.state]) |
|
543 else: |
|
544 state_label = translate(_('publication refused by {principal}')) |
|
545 if format: |
|
546 state_label = state_label.format(principal=security.get_principal(state.state_principal).title) |
|
547 return state_label |
|
548 |
|
549 |
|
550 wf = ContentWorkflow(wf_transitions, |
|
551 states=STATES_VOCABULARY, |
|
552 initial_state=DRAFT, |
|
553 update_states=UPDATE_STATES, |
|
554 readonly_states=READONLY_STATES, |
|
555 protected_states=PROTECTED_STATES, |
|
556 manager_states=MANAGER_STATES, |
|
557 published_states=VISIBLE_STATES, |
|
558 waiting_states=WAITING_STATES, |
|
559 retired_states=RETIRED_STATES) |
|
560 |
|
561 |
|
562 @utility_config(name='PyAMS default workflow', provides=IWorkflow) |
|
563 class WorkflowUtility(object): |
|
564 """PyAMS default workflow utility""" |
|
565 |
|
566 def __new__(cls): |
|
567 return wf |