|
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 from uuid import uuid4 |
|
19 |
|
20 # import interfaces |
|
21 from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION, MANAGE_CONTENT_PERMISSION, CREATE_CONTENT_PERMISSION, \ |
|
22 PUBLISH_CONTENT_PERMISSION |
|
23 from pyams_content.shared.common.interfaces import IWfSharedContent, ISharedContent, ISharedTool, IManagerRestrictions |
|
24 from pyams_form.interfaces.form import IFormContextPermissionChecker, IWidgetsPrefixViewletsManager |
|
25 from pyams_i18n.interfaces import II18n, II18nManager |
|
26 from pyams_sequence.interfaces import ISequentialIntIds, ISequentialIdInfo |
|
27 from pyams_skin.interfaces import IContentTitle |
|
28 from pyams_skin.interfaces.container import ITable, ITableElementEditor |
|
29 from pyams_skin.interfaces.viewlet import IContextActions, IMenuHeader |
|
30 from pyams_skin.layer import IPyAMSLayer |
|
31 from pyams_utils.interfaces import FORBIDDEN_PERMISSION |
|
32 from pyams_workflow.interfaces import IWorkflowVersions, IWorkflowInfo, IWorkflowState, IWorkflowCommentInfo, IWorkflow |
|
33 from pyams_zmi.interfaces.menu import ISiteManagementMenu |
|
34 from zope.dublincore.interfaces import IZopeDublinCore |
|
35 |
|
36 # import packages |
|
37 from pyams_form.form import AJAXAddForm |
|
38 from pyams_form.schema import CloseButton |
|
39 from pyams_pagelet.pagelet import pagelet_config |
|
40 from pyams_skin.page import DefaultPageHeaderAdapter |
|
41 from pyams_skin.table import DefaultElementEditorAdapter |
|
42 from pyams_skin.viewlet.toolbar import ToolbarMenuItem |
|
43 from pyams_template.template import template_config |
|
44 from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextAdapter, ContextRequestAdapter |
|
45 from pyams_utils.registry import get_utility |
|
46 from pyams_utils.request import check_request |
|
47 from pyams_utils.traversing import get_parent |
|
48 from pyams_utils.url import absolute_url |
|
49 from pyams_viewlet.viewlet import viewlet_config, Viewlet |
|
50 from pyams_workflow.versions import WorkflowHistoryItem |
|
51 from pyams_zmi.form import AdminDialogAddForm |
|
52 from pyramid.view import view_config |
|
53 from z3c.form import field, button |
|
54 from zope.copy import copy |
|
55 from zope.interface import Interface |
|
56 from zope.lifecycleevent import ObjectCreatedEvent |
|
57 from zope.location import locate |
|
58 |
|
59 from pyams_content import _ |
|
60 |
|
61 |
|
62 class SharedContentAddForm(AdminDialogAddForm): |
|
63 """Shared content add form""" |
|
64 |
|
65 @property |
|
66 def title(self): |
|
67 return II18n(self.context).query_attribute('title', request=self.request) |
|
68 |
|
69 icon_css_class = 'fa fa-fw fa-plus' |
|
70 fields = field.Fields(IWfSharedContent).select('title', 'description') |
|
71 |
|
72 ajax_handler = 'add-shared-content.json' |
|
73 edit_permission = CREATE_CONTENT_PERMISSION |
|
74 |
|
75 def updateWidgets(self, prefix=None): |
|
76 super(SharedContentAddForm, self).updateWidgets(prefix) |
|
77 self.widgets['description'].label_css_class = 'textarea' |
|
78 |
|
79 def create(self, data): |
|
80 return self.context.shared_content_factory.content_class() |
|
81 |
|
82 def update_content(self, content, data): |
|
83 # generic content update |
|
84 changes = super(SharedContentAddForm, self).update_content(content, data) |
|
85 content.creator = self.request.principal.id |
|
86 content.owner = self.request.principal.id |
|
87 content.short_name = content.title.copy() |
|
88 # init content languages |
|
89 languages = II18nManager(self.context).languages |
|
90 if languages: |
|
91 II18nManager(content).languages = languages.copy() |
|
92 return changes |
|
93 |
|
94 def add(self, wf_content): |
|
95 content = self.context.shared_content_factory() |
|
96 self.request.registry.notify(ObjectCreatedEvent(content)) |
|
97 uuid = self.__uuid = str(uuid4()) |
|
98 self.context[uuid] = content |
|
99 IWorkflowVersions(content).add_version(wf_content, None) |
|
100 IWorkflowInfo(wf_content).fire_transition('init') |
|
101 |
|
102 def nextURL(self): |
|
103 return absolute_url(self.context, self.request, '{0}/++versions++/1/@@admin.html'.format(self.__uuid)) |
|
104 |
|
105 |
|
106 class SharedContentAJAXAddForm(AJAXAddForm): |
|
107 """Shared event add form, JSON renderer""" |
|
108 |
|
109 def get_ajax_output(self, changes): |
|
110 return {'status': 'redirect', |
|
111 'location': self.nextURL()} |
|
112 |
|
113 |
|
114 @viewlet_config(name='wf-create-message', context=Interface, layer=IPyAMSLayer, view=SharedContentAddForm, |
|
115 manager=IWidgetsPrefixViewletsManager, weight=20) |
|
116 @template_config(template='templates/wf-create-message.pt') |
|
117 class SharedContentAddFormMessage(Viewlet): |
|
118 """Shared content add form info message""" |
|
119 |
|
120 |
|
121 # |
|
122 # Edit adapters and views |
|
123 # |
|
124 |
|
125 @adapter_config(context=IWfSharedContent, provides=IFormContextPermissionChecker) |
|
126 class WfSharedContentPermissionChecker(ContextAdapter): |
|
127 """Shared content form permission checker""" |
|
128 |
|
129 @property |
|
130 def edit_permission(self): |
|
131 workflow = IWorkflow(self.context) |
|
132 state = IWorkflowState(self.context).state |
|
133 if state in workflow.readonly_states: # access forbidden to all for archived contents |
|
134 return FORBIDDEN_PERMISSION |
|
135 elif state in workflow.protected_states: # webmaster can update published contents |
|
136 return MANAGE_SITE_ROOT_PERMISSION |
|
137 else: |
|
138 request = check_request() |
|
139 if request.has_permission(MANAGE_SITE_ROOT_PERMISSION, self.context): # webmaster access |
|
140 return MANAGE_SITE_ROOT_PERMISSION |
|
141 if state in workflow.manager_states: # restricted manager access |
|
142 if request.principal.id in self.context.managers: |
|
143 return PUBLISH_CONTENT_PERMISSION |
|
144 restrictions = IManagerRestrictions(self.context).get_restrictions(request.principal) |
|
145 if restrictions and restrictions.check_access(self.context, |
|
146 permission=PUBLISH_CONTENT_PERMISSION, |
|
147 request=request): |
|
148 return PUBLISH_CONTENT_PERMISSION |
|
149 else: |
|
150 if request.principal.id in self.context.owner | self.context.contributors | self.context.managers: |
|
151 return MANAGE_CONTENT_PERMISSION |
|
152 restrictions = IManagerRestrictions(self.context).get_restrictions(request.principal) |
|
153 if restrictions and restrictions.check_access(self.context, |
|
154 permission=MANAGE_CONTENT_PERMISSION, |
|
155 request=request): |
|
156 return MANAGE_CONTENT_PERMISSION |
|
157 return FORBIDDEN_PERMISSION |
|
158 |
|
159 |
|
160 class WfSharedContentPermissionMixin(object): |
|
161 """Shared content permission checker""" |
|
162 |
|
163 @property |
|
164 def permission(self): |
|
165 content = get_parent(self.context, IWfSharedContent) |
|
166 if content is not None: |
|
167 return IFormContextPermissionChecker(content).edit_permission |
|
168 |
|
169 |
|
170 @adapter_config(context=(IWfSharedContent, ISiteManagementMenu), provides=IMenuHeader) |
|
171 class WfSharedContentSiteManagementMenuHeader(ContextRequestAdapter): |
|
172 """Shared content site management menu header adapter""" |
|
173 |
|
174 header = _("Manage this content") |
|
175 |
|
176 |
|
177 @adapter_config(context=(IWfSharedContent, IPyAMSLayer, Interface), provides=IContentTitle) |
|
178 class WfSharedContentTitleAdapter(ContextRequestViewAdapter): |
|
179 """Shared content title adapter""" |
|
180 |
|
181 @property |
|
182 def title(self): |
|
183 return II18n(self.context).query_attribute('title', request=self.request) |
|
184 |
|
185 |
|
186 class WfSharedContentHeaderAdapter(DefaultPageHeaderAdapter): |
|
187 """Shared content header adapter""" |
|
188 |
|
189 @property |
|
190 def back_url(self): |
|
191 shared_tool = get_parent(self.context, ISharedTool) |
|
192 return absolute_url(shared_tool, self.request, 'admin.html#dashboard.html') |
|
193 |
|
194 back_target = None |
|
195 icon_class = 'fa fa-fw fa-edit' |
|
196 |
|
197 |
|
198 @adapter_config(context=(IWfSharedContent, IPyAMSLayer, ITable), provides=ITableElementEditor) |
|
199 class WfSharedContentElementEditor(DefaultElementEditorAdapter): |
|
200 """Shared content element editor""" |
|
201 |
|
202 view_name = 'admin.html' |
|
203 modal_target = False |
|
204 |
|
205 |
|
206 # |
|
207 # Duplication menus and views |
|
208 # |
|
209 |
|
210 @viewlet_config(name='duplication.menu', context=IWfSharedContent, layer=IPyAMSLayer, |
|
211 view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION, weight=1) |
|
212 class WfSharedContentDuplicateMenu(ToolbarMenuItem): |
|
213 """Shared content duplication menu item""" |
|
214 |
|
215 label = _("Duplicate content...") |
|
216 label_css_class = 'fa fa-fw fa-files-o' |
|
217 |
|
218 url = 'duplicate.html' |
|
219 modal_target = True |
|
220 |
|
221 |
|
222 class ISharedContentDuplicateButtons(Interface): |
|
223 """Shared content duplication form buttons""" |
|
224 |
|
225 close = CloseButton(name='close', title=_("Cancel")) |
|
226 duplicate = button.Button(name='duplicate', title=_("Duplicate content")) |
|
227 |
|
228 |
|
229 @pagelet_config(name='duplicate.html', context=IWfSharedContent, layer=IPyAMSLayer, |
|
230 permission=CREATE_CONTENT_PERMISSION) |
|
231 class WfSharedContentDuplicateForm(AdminDialogAddForm): |
|
232 """Shared content duplicate form""" |
|
233 |
|
234 legend = _("Duplicate content") |
|
235 fields = field.Fields(IWorkflowCommentInfo) |
|
236 buttons = button.Buttons(ISharedContentDuplicateButtons) |
|
237 |
|
238 ajax_handler = 'duplicate.json' |
|
239 edit_permission = CREATE_CONTENT_PERMISSION |
|
240 |
|
241 def updateWidgets(self, prefix=None): |
|
242 super(WfSharedContentDuplicateForm, self).updateWidgets(prefix) |
|
243 self.widgets['comment'].label_css_class = 'textarea' |
|
244 |
|
245 def updateActions(self): |
|
246 super(WfSharedContentDuplicateForm, self).updateActions() |
|
247 if 'duplicate' in self.actions: |
|
248 self.actions['duplicate'].addClass('btn-primary') |
|
249 |
|
250 def createAndAdd(self, data): |
|
251 registry = self.request.registry |
|
252 # initialize new content |
|
253 content = get_parent(self.context, ISharedContent) |
|
254 new_content = content.__class__() |
|
255 registry.notify(ObjectCreatedEvent(new_content)) |
|
256 container = get_parent(content, ISharedTool) |
|
257 container[str(uuid4())] = new_content |
|
258 # initialize new version |
|
259 new_version = copy(self.context) |
|
260 registry.notify(ObjectCreatedEvent(new_version)) |
|
261 locate(new_version, self.context.__parent__) # locate new version for traversing to work... |
|
262 new_version.creator = self.request.principal.id |
|
263 new_version.owner = self.request.principal.id |
|
264 new_version.modifiers = set() |
|
265 # store new version |
|
266 translate = self.request.localizer.translate |
|
267 workflow = get_utility(IWorkflow, name=new_content.workflow_name) |
|
268 sequence = get_utility(ISequentialIntIds, name=new_content.sequence_name) |
|
269 IWorkflowVersions(new_content).add_version(new_version, workflow.initial_state) |
|
270 history = WorkflowHistoryItem(date=datetime.utcnow(), |
|
271 source_state=translate(workflow.states.getTerm(IWorkflowState( |
|
272 self.context).state).title), |
|
273 transition=translate(_("Duplicate content ({oid})")).format( |
|
274 oid=sequence.get_short_oid(ISequentialIdInfo(content).oid, |
|
275 content.sequence_prefix)), |
|
276 target_state=translate(workflow.states.getTerm(workflow.initial_state).title), |
|
277 principal=self.request.principal.id, |
|
278 comment=data.get('comment')) |
|
279 state = IWorkflowState(new_version) |
|
280 state.history.clear() |
|
281 state.history.append(history) |
|
282 return new_version |
|
283 |
|
284 |
|
285 @view_config(name='duplicate.json', context=IWfSharedContent, request_type=IPyAMSLayer, |
|
286 permission=CREATE_CONTENT_PERMISSION, renderer='json', xhr=True) |
|
287 class WfSharedContentDuplicateAJAXForm(AJAXAddForm, WfSharedContentDuplicateForm): |
|
288 """Shared content duplicate form, JSON renderer""" |
|
289 |
|
290 def get_ajax_output(self, changes): |
|
291 return {'status': 'redirect', |
|
292 'location': absolute_url(changes, self.request, 'admin.html')} |
|
293 |
|
294 |
|
295 @viewlet_config(name='wf-duplicate-message', context=IWfSharedContent, layer=IPyAMSLayer, |
|
296 view=WfSharedContentDuplicateForm, manager=IWidgetsPrefixViewletsManager, weight=20) |
|
297 @template_config(template='templates/wf-duplicate-message.pt') |
|
298 class WfSharedContentDuplicateFormMessage(Viewlet): |
|
299 """Shared content add form info message""" |
|
300 |
|
301 |
|
302 # |
|
303 # Custom columns mixins |
|
304 # |
|
305 |
|
306 class WfModifiedContentColumnMixin(object): |
|
307 """Shared content modified column mixin""" |
|
308 |
|
309 def renderCell(self, item): |
|
310 value = self.getValue(item) |
|
311 content = get_parent(item, IWfSharedContent) |
|
312 if content is not None: |
|
313 if IWorkflowState(content).version_id > 1: |
|
314 item_dc = IZopeDublinCore(item) |
|
315 if item_dc.modified and (item_dc.modified > IZopeDublinCore(content).created): |
|
316 translate = self.request.localizer.translate |
|
317 value += '<i class="fa fa-fw fa-circle txt-color-orange pull-right hint" title="{0}" ' \ |
|
318 'data-ams-hint-gravity="e"></i>'.format(translate(_("Created or modified in this version"))) |
|
319 return value |