|
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 PUBLISH_CONTENT_PERMISSION, MANAGE_SITE_ROOT_PERMISSION, WEBMASTER_ROLE, \ |
|
21 PILOT_ROLE, MANAGER_ROLE, OWNER_ROLE, READER_ROLE, MANAGE_CONTENT_PERMISSION, CREATE_CONTENT_PERMISSION |
|
22 from pyams_content.shared.common.interfaces import IWfSharedContentRoles |
|
23 from pyams_content.workflow.interfaces import IBasicWorkflow |
|
24 from pyams_content.shared.common.interfaces import IManagerRestrictions |
|
25 from pyams_security.interfaces import IRoleProtectedObject |
|
26 from pyams_workflow.interfaces import IWorkflowStateLabel, IWorkflowState, IWorkflow, IWorkflowPublicationInfo, \ |
|
27 IWorkflowVersions, IWorkflowInfo, ObjectClonedEvent |
|
28 |
|
29 # import packages |
|
30 from pyams_utils.adapter import adapter_config, ContextAdapter |
|
31 from pyams_utils.date import format_datetime |
|
32 from pyams_utils.registry import utility_config, get_current_registry |
|
33 from pyams_utils.request import check_request |
|
34 from pyams_workflow.workflow import Transition, Workflow |
|
35 from zope.copy import copy |
|
36 from zope.interface import implementer |
|
37 from zope.location import locate |
|
38 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm |
|
39 |
|
40 from pyams_content import _ |
|
41 |
|
42 |
|
43 DRAFT = 'draft' |
|
44 PUBLISHED = 'published' |
|
45 ARCHIVED = 'archived' |
|
46 DELETED = 'deleted' |
|
47 |
|
48 STATES_IDS = (DRAFT, |
|
49 PUBLISHED, |
|
50 ARCHIVED, |
|
51 DELETED) |
|
52 |
|
53 STATES_LABELS = (_("Draft"), |
|
54 _("Published"), |
|
55 _("Archived"), |
|
56 _("Deleted")) |
|
57 |
|
58 STATES_VOCABULARY = SimpleVocabulary([SimpleTerm(STATES_IDS[i], title=t) |
|
59 for i, t in enumerate(STATES_LABELS)]) |
|
60 |
|
61 STATES_HEADERS = {DRAFT: _("draft created"), |
|
62 PUBLISHED: _("published"), |
|
63 ARCHIVED: _("archived")} |
|
64 |
|
65 UPDATE_STATES = (DRAFT, ) |
|
66 '''Default state available to contributors in update mode''' |
|
67 |
|
68 READONLY_STATES = (ARCHIVED, DELETED) |
|
69 '''Retired and archived contents can't be modified''' |
|
70 |
|
71 PROTECTED_STATES = (PUBLISHED, ) |
|
72 '''Protected states are available to webmasters in update mode''' |
|
73 |
|
74 MANAGER_STATES = () |
|
75 '''No custom state available to managers!''' |
|
76 |
|
77 VISIBLE_STATES = PUBLISHED_STATES = (PUBLISHED, ) |
|
78 |
|
79 WAITING_STATES = () |
|
80 |
|
81 RETIRED_STATES = () |
|
82 |
|
83 ARCHIVED_STATES = (ARCHIVED, ) |
|
84 |
|
85 |
|
86 # |
|
87 # Workflow conditions |
|
88 # |
|
89 |
|
90 def can_manage_content(wf, context): |
|
91 """Check if a manager can handle content""" |
|
92 request = check_request() |
|
93 # grant access to webmaster |
|
94 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context): |
|
95 return True |
|
96 # local content managers can manage content |
|
97 principal_id = request.principal.id |
|
98 if principal_id in context.managers: |
|
99 return True |
|
100 # shared tool managers can manage content if restrictions apply |
|
101 restrictions = IManagerRestrictions(context).get_restrictions(principal_id) |
|
102 return restrictions and restrictions.check_access(context, permission=PUBLISH_CONTENT_PERMISSION, request=request) |
|
103 |
|
104 |
|
105 def can_create_new_version(wf, context): |
|
106 """Check if we can create a new version""" |
|
107 # can't create new version when previous draft already exists |
|
108 versions = IWorkflowVersions(context) |
|
109 if versions.has_version(DRAFT): |
|
110 return False |
|
111 request = check_request() |
|
112 # grant access to webmaster |
|
113 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context): |
|
114 return True |
|
115 # grant access to owner, creator and local contributors |
|
116 principal_id = request.principal.id |
|
117 if principal_id in context.owner | {context.creator} | context.contributors: |
|
118 return True |
|
119 # grant access to local content managers |
|
120 if principal_id in context.managers: |
|
121 return True |
|
122 # grant access to shared tool managers if restrictions apply |
|
123 restrictions = IManagerRestrictions(context).get_restrictions(principal_id) |
|
124 return restrictions and restrictions.check_access(context, permission=CREATE_CONTENT_PERMISSION, request=request) |
|
125 |
|
126 |
|
127 def can_delete_version(wf, context): |
|
128 """Check if we can delete a draft version""" |
|
129 request = check_request() |
|
130 # grant access to webmaster |
|
131 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, context): |
|
132 return True |
|
133 # grant access to owner, creator and local contributors |
|
134 principal_id = request.principal.id |
|
135 if principal_id in context.owner | {context.creator} | context.contributors: |
|
136 return True |
|
137 # grant access to local content managers |
|
138 if principal_id in context.managers: |
|
139 return True |
|
140 # grant access to shared tool managers if restrictions apply |
|
141 restrictions = IManagerRestrictions(context).get_restrictions(principal_id) |
|
142 return restrictions and restrictions.check_access(context, permission=MANAGE_CONTENT_PERMISSION, request=request) |
|
143 |
|
144 |
|
145 # |
|
146 # Workflow actions |
|
147 # |
|
148 |
|
149 def publish_action(wf, context): |
|
150 """Publish version""" |
|
151 request = check_request() |
|
152 translate = request.localizer.translate |
|
153 publication_info = IWorkflowPublicationInfo(context) |
|
154 publication_info.publication_date = datetime.utcnow() |
|
155 publication_info.publisher = request.principal.id |
|
156 version_id = IWorkflowState(context).version_id |
|
157 for version in IWorkflowVersions(context).get_versions((PUBLISHED, )): |
|
158 if version is not context: |
|
159 IWorkflowInfo(version).fire_transition_toward(ARCHIVED, |
|
160 comment=translate(_("Published version {0}")).format(version_id)) |
|
161 |
|
162 |
|
163 def archive_action(wf, context): |
|
164 """Remove readers when a content is archived""" |
|
165 roles = IWfSharedContentRoles(context, None) |
|
166 if roles is not None: |
|
167 IRoleProtectedObject(context).revoke_role(READER_ROLE, roles.readers) |
|
168 |
|
169 |
|
170 def clone_action(wf, context): |
|
171 """Create new version""" |
|
172 result = copy(context) |
|
173 locate(result, context.__parent__) |
|
174 registry = get_current_registry() |
|
175 registry.notify(ObjectClonedEvent(result, context)) |
|
176 return result |
|
177 |
|
178 |
|
179 def delete_action(wf, context): |
|
180 """Delete draft version, and parent if single version""" |
|
181 versions = IWorkflowVersions(context) |
|
182 versions.remove_version(IWorkflowState(context).version_id) |
|
183 |
|
184 |
|
185 # |
|
186 # Workflow transitions |
|
187 # |
|
188 |
|
189 init = Transition(transition_id='init', |
|
190 title=_("Initialize"), |
|
191 source=None, |
|
192 destination=DRAFT, |
|
193 history_label=_("Draft creation")) |
|
194 |
|
195 draft_to_published = Transition(transition_id='draft_to_published', |
|
196 title=_("Publish"), |
|
197 source=DRAFT, |
|
198 destination=PUBLISHED, |
|
199 permission=PUBLISH_CONTENT_PERMISSION, |
|
200 condition=can_manage_content, |
|
201 action=publish_action, |
|
202 menu_css_class='fa fa-fw fa-thumbs-o-up', |
|
203 view_name='wf-publish.html', |
|
204 history_label=_("Content published"), |
|
205 notify_roles={WEBMASTER_ROLE, PILOT_ROLE, MANAGER_ROLE, OWNER_ROLE}, |
|
206 notify_message=_("published the content « {0} »"), |
|
207 order=1) |
|
208 |
|
209 published_to_archived = Transition(transition_id='published_to_archived', |
|
210 title=_("Archive content"), |
|
211 source=PUBLISHED, |
|
212 destination=ARCHIVED, |
|
213 permission=PUBLISH_CONTENT_PERMISSION, |
|
214 condition=can_manage_content, |
|
215 action=archive_action, |
|
216 menu_css_class='fa fa-fw fa-archive', |
|
217 view_name='wf-archive.html', |
|
218 history_label=_("Content archived"), |
|
219 notify_roles={WEBMASTER_ROLE, PILOT_ROLE, MANAGER_ROLE, OWNER_ROLE}, |
|
220 notify_message=_("archived content « {0} »"), |
|
221 order=2) |
|
222 |
|
223 published_to_draft = Transition(transition_id='published_to_draft', |
|
224 title=_("Create new version"), |
|
225 source=PUBLISHED, |
|
226 destination=DRAFT, |
|
227 permission=CREATE_CONTENT_PERMISSION, |
|
228 condition=can_create_new_version, |
|
229 action=clone_action, |
|
230 menu_css_class='fa fa-fw fa-file-o', |
|
231 view_name='wf-clone.html', |
|
232 history_label=_("New version created"), |
|
233 order=3) |
|
234 |
|
235 archived_to_draft = Transition(transition_id='archived_to_draft', |
|
236 title=_("Create new version"), |
|
237 source=ARCHIVED, |
|
238 destination=DRAFT, |
|
239 permission=CREATE_CONTENT_PERMISSION, |
|
240 condition=can_create_new_version, |
|
241 action=clone_action, |
|
242 menu_css_class='fa fa-fw fa-file-o', |
|
243 view_name='wf-clone.html', |
|
244 history_label=_("New version created"), |
|
245 order=4) |
|
246 |
|
247 delete = Transition(transition_id='delete', |
|
248 title=_("Delete version"), |
|
249 source=DRAFT, |
|
250 destination=DELETED, |
|
251 permission=MANAGE_CONTENT_PERMISSION, |
|
252 condition=can_delete_version, |
|
253 action=delete_action, |
|
254 menu_css_class='fa fa-fw fa-trash', |
|
255 view_name='wf-delete.html', |
|
256 history_label=_("Version deleted"), |
|
257 order=99) |
|
258 |
|
259 wf_transitions = {init, |
|
260 draft_to_published, |
|
261 published_to_archived, |
|
262 published_to_draft, |
|
263 archived_to_draft, |
|
264 delete} |
|
265 |
|
266 |
|
267 @implementer(IBasicWorkflow) |
|
268 class BasicWorkflow(Workflow): |
|
269 """PyAMS basic workflow""" |
|
270 |
|
271 |
|
272 @adapter_config(context=IBasicWorkflow, provides=IWorkflowStateLabel) |
|
273 class WorkflowStateLabelAdapter(ContextAdapter): |
|
274 """Generic state label adapter""" |
|
275 |
|
276 @staticmethod |
|
277 def get_label(content, request=None, format=True): |
|
278 if request is None: |
|
279 request = check_request() |
|
280 translate = request.localizer.translate |
|
281 state = IWorkflowState(content) |
|
282 header = STATES_HEADERS.get(state.state) |
|
283 if header is not None: |
|
284 state_label = translate(header) |
|
285 if format: |
|
286 state_label = translate(_('{state} {date}')).format(state=state_label, |
|
287 date=format_datetime(state.state_date)) |
|
288 else: |
|
289 state_label = translate(_("Unknown state")) |
|
290 return state_label |
|
291 |
|
292 |
|
293 @adapter_config(name=DRAFT, context=IBasicWorkflow, provides=IWorkflowStateLabel) |
|
294 class DraftWorkflowStateLabelAdapter(ContextAdapter): |
|
295 """Draft state label adapter""" |
|
296 |
|
297 @staticmethod |
|
298 def get_label(content, request=None, format=True): |
|
299 if request is None: |
|
300 request = check_request() |
|
301 translate = request.localizer.translate |
|
302 state = IWorkflowState(content) |
|
303 if len(state.history) <= 2: |
|
304 header = STATES_HEADERS.get(state.state) |
|
305 if header is not None: |
|
306 if state.version_id == 1: |
|
307 state_label = translate(header) |
|
308 else: |
|
309 state_label = translate(_("new version created")) |
|
310 else: |
|
311 state_label = translate(_("Unknown state")) |
|
312 else: |
|
313 state_label = translate(_('publication refused')) |
|
314 if format: |
|
315 state_label = translate(_('{state} {date}')).format(state=state_label, |
|
316 date=format_datetime(state.state_date)) |
|
317 return state_label |
|
318 |
|
319 |
|
320 wf = BasicWorkflow(wf_transitions, |
|
321 states=STATES_VOCABULARY, |
|
322 initial_state=DRAFT, |
|
323 update_states=UPDATE_STATES, |
|
324 readonly_states=READONLY_STATES, |
|
325 protected_states=PROTECTED_STATES, |
|
326 manager_states=MANAGER_STATES, |
|
327 published_states=VISIBLE_STATES, |
|
328 waiting_states=WAITING_STATES, |
|
329 retired_states=RETIRED_STATES, |
|
330 archived_states=ARCHIVED_STATES, |
|
331 auto_retired_state=ARCHIVED) |
|
332 |
|
333 |
|
334 @utility_config(name='PyAMS basic workflow', provides=IWorkflow) |
|
335 class WorkflowUtility(object): |
|
336 """PyAMS basic workflow utility |
|
337 |
|
338 This is a basic workflow implementation for PyAMS contents. |
|
339 It only implements three states which are *draft*, *published* and *archived*. |
|
340 """ |
|
341 |
|
342 def __new__(cls): |
|
343 return wf |