|
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 from pyramid.events import subscriber |
|
13 |
|
14 __docformat__ = 'restructuredtext' |
|
15 |
|
16 |
|
17 # import standard library |
|
18 from persistent import Persistent |
|
19 |
|
20 # import interfaces |
|
21 from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget, IParagraphContainer, \ |
|
22 IParagraphFactory |
|
23 from pyams_content.component.paragraph.interfaces.milestone import IMilestone, IMilestoneContainer, \ |
|
24 IMilestoneContainerTarget, MILESTONE_CONTAINER_KEY, IMilestoneParagraph, MILESTONE_PARAGRAPH_TYPE, \ |
|
25 MILESTONE_PARAGRAPH_RENDERERS |
|
26 from pyams_content.features.checker.interfaces import IContentChecker, MISSING_VALUE, MISSING_LANG_VALUE, ERROR_VALUE |
|
27 from pyams_form.interfaces.form import IFormContextPermissionChecker |
|
28 from pyams_i18n.interfaces import II18n, II18nManager, INegotiator |
|
29 from zope.annotation import IAnnotations |
|
30 from zope.location.interfaces import ISublocations |
|
31 from zope.traversing.interfaces import ITraversable |
|
32 |
|
33 # import packages |
|
34 from pyams_catalog.utils import index_object |
|
35 from pyams_content.component.paragraph import BaseParagraph, BaseParagraphFactory, BaseParagraphContentChecker |
|
36 from pyams_content.features.checker import BaseContentChecker |
|
37 from pyams_content.features.renderer import RenderedContentMixin, IContentRenderer |
|
38 from pyams_utils.adapter import adapter_config, ContextAdapter |
|
39 from pyams_utils.registry import get_current_registry, get_utility, utility_config |
|
40 from pyams_utils.request import check_request |
|
41 from pyams_utils.traversing import get_parent |
|
42 from pyams_utils.vocabulary import vocabulary_config |
|
43 from zope.container.contained import Contained |
|
44 from zope.container.ordered import OrderedContainer |
|
45 from zope.lifecycleevent import ObjectCreatedEvent, IObjectAddedEvent, ObjectModifiedEvent, IObjectModifiedEvent, \ |
|
46 IObjectRemovedEvent |
|
47 from zope.location import locate |
|
48 from zope.interface import implementer |
|
49 from zope.schema.fieldproperty import FieldProperty |
|
50 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm |
|
51 |
|
52 from pyams_content import _ |
|
53 |
|
54 |
|
55 # |
|
56 # Milestone class and adapters |
|
57 # |
|
58 |
|
59 @implementer(IMilestone) |
|
60 class Milestone(Persistent, Contained): |
|
61 """Milestone persistent class""" |
|
62 |
|
63 visible = FieldProperty(IMilestone['visible']) |
|
64 title = FieldProperty(IMilestone['title']) |
|
65 label = FieldProperty(IMilestone['label']) |
|
66 anchor = FieldProperty(IMilestone['anchor']) |
|
67 |
|
68 |
|
69 @adapter_config(context=IMilestone, provides=IFormContextPermissionChecker) |
|
70 class MilestonePermissionChecker(ContextAdapter): |
|
71 """Milestone permission checker""" |
|
72 |
|
73 @property |
|
74 def edit_permission(self): |
|
75 content = get_parent(self.context, IMilestoneContainerTarget) |
|
76 return IFormContextPermissionChecker(content).edit_permission |
|
77 |
|
78 |
|
79 @subscriber(IObjectAddedEvent, context_selector=IMilestone) |
|
80 def handle_added_milestone(event): |
|
81 """Handle added milestone""" |
|
82 content = get_parent(event.object, IMilestoneContainerTarget) |
|
83 if content is not None: |
|
84 get_current_registry().notify(ObjectModifiedEvent(content)) |
|
85 |
|
86 |
|
87 @subscriber(IObjectModifiedEvent, context_selector=IMilestone) |
|
88 def handle_modified_milestone(event): |
|
89 """Handle modified milestone""" |
|
90 content = get_parent(event.object, IMilestoneContainerTarget) |
|
91 if content is not None: |
|
92 get_current_registry().notify(ObjectModifiedEvent(content)) |
|
93 |
|
94 |
|
95 @subscriber(IObjectRemovedEvent, context_selector=IMilestone) |
|
96 def handle_removed_milestone(event): |
|
97 """Handle removed milestone""" |
|
98 content = get_parent(event.object, IMilestoneContainerTarget) |
|
99 if content is not None: |
|
100 get_current_registry().notify(ObjectModifiedEvent(content)) |
|
101 |
|
102 |
|
103 @adapter_config(context=IMilestone, provides=IContentChecker) |
|
104 class MilestoneContentChecker(BaseContentChecker): |
|
105 """Milestone content checker""" |
|
106 |
|
107 @property |
|
108 def label(self): |
|
109 request = check_request() |
|
110 return II18n(self.context).query_attribute('title', request=request) |
|
111 |
|
112 def inner_check(self, request): |
|
113 output = [] |
|
114 translate = request.localizer.translate |
|
115 manager = get_parent(self.context, II18nManager) |
|
116 if manager is not None: |
|
117 langs = manager.get_languages() |
|
118 else: |
|
119 negotiator = get_utility(INegotiator) |
|
120 langs = (negotiator.server_language, ) |
|
121 i18n = II18n(self.context) |
|
122 for lang in langs: |
|
123 for attr in ('title', 'label'): |
|
124 value = i18n.get_attribute(attr, lang, request) |
|
125 if not value: |
|
126 field_title = translate(IMilestone[attr].title) |
|
127 if len(langs) == 1: |
|
128 output.append(translate(MISSING_VALUE).format(field=field_title)) |
|
129 else: |
|
130 output.append(translate(MISSING_LANG_VALUE).format(field=field_title, lang=lang)) |
|
131 field_title = translate(IMilestone['anchor'].title) |
|
132 if not self.context.anchor: |
|
133 output.append(translate(MISSING_VALUE).format(field=field_title)) |
|
134 else: |
|
135 target = get_parent(self.context, IParagraphContainerTarget) |
|
136 if target is not None: |
|
137 container = IParagraphContainer(target) |
|
138 paragraph = container.get(self.context.anchor) |
|
139 if paragraph is None: |
|
140 output.append(translate(ERROR_VALUE).format(field=field_title, |
|
141 message=translate(_("Selected paragraph is missing")))) |
|
142 elif not paragraph.visible: |
|
143 output.append(translate(ERROR_VALUE).format(field=field_title, |
|
144 message=translate(_("Selected paragraph is not " |
|
145 "visible")))) |
|
146 return output |
|
147 |
|
148 |
|
149 # |
|
150 # Milestones container classes and adapters |
|
151 # |
|
152 |
|
153 @implementer(IMilestoneContainer) |
|
154 class MilestoneContainer(OrderedContainer): |
|
155 """Milestones container""" |
|
156 |
|
157 last_id = 1 |
|
158 |
|
159 def append(self, value, notify=True): |
|
160 key = str(self.last_id) |
|
161 if not notify: |
|
162 # pre-locate milestone item to avoid multiple notifications |
|
163 locate(value, self, key) |
|
164 self[key] = value |
|
165 self.last_id += 1 |
|
166 if not notify: |
|
167 # make sure that milestone item is correctly indexed |
|
168 index_object(value) |
|
169 |
|
170 def get_visible_items(self): |
|
171 return filter(lambda x: IMilestone(x).visible, self.values()) |
|
172 |
|
173 |
|
174 @adapter_config(context=IMilestoneContainerTarget, provides=IMilestoneContainer) |
|
175 def milestone_container_factory(target): |
|
176 """Milestone container factory""" |
|
177 annotations = IAnnotations(target) |
|
178 container = annotations.get(MILESTONE_CONTAINER_KEY) |
|
179 if container is None: |
|
180 container = annotations[MILESTONE_CONTAINER_KEY] = MilestoneContainer() |
|
181 get_current_registry().notify(ObjectCreatedEvent(container)) |
|
182 locate(container, target, '++milestones++') |
|
183 return container |
|
184 |
|
185 |
|
186 @adapter_config(name='milestones', context=IMilestoneContainerTarget, provides=ITraversable) |
|
187 class MilestoneContainerNamespace(ContextAdapter): |
|
188 """Milestones container ++milestones++ namespace""" |
|
189 |
|
190 def traverse(self, name, furtherpaath=None): |
|
191 return IMilestoneContainer(self.context) |
|
192 |
|
193 |
|
194 @adapter_config(name='milestones', context=IMilestoneContainerTarget, provides=ISublocations) |
|
195 class MilestoneContainerSublocations(ContextAdapter): |
|
196 """Milestones container sub-locations adapter""" |
|
197 |
|
198 def sublocations(self): |
|
199 return IMilestoneContainer(self.context).values() |
|
200 |
|
201 |
|
202 @adapter_config(name='milestones', context=IMilestoneContainerTarget, provides=IContentChecker) |
|
203 class MilestoneContainerContentChecker(BaseContentChecker): |
|
204 """Milestones container content checker""" |
|
205 |
|
206 label = _("Milestones") |
|
207 sep = '\n' |
|
208 weight = 200 |
|
209 |
|
210 def inner_check(self, request): |
|
211 output = [] |
|
212 registry = request.registry |
|
213 for milestone in IMilestoneContainer(self.context).values(): |
|
214 if not milestone.visible: |
|
215 continue |
|
216 for name, checker in sorted(registry.getAdapters((milestone, ), IContentChecker), |
|
217 key=lambda x: x[1].weight): |
|
218 output.append('- {0} :'.format(II18n(milestone).query_attribute('title', request=request))) |
|
219 output.append(checker.get_check_output(request)) |
|
220 return output |
|
221 |
|
222 |
|
223 @implementer(IMilestoneParagraph) |
|
224 class MilestoneParagraph(RenderedContentMixin, BaseParagraph): |
|
225 """Milestones paragraph""" |
|
226 |
|
227 icon_class = 'fa-arrows-h' |
|
228 icon_hint = _("Milestones") |
|
229 |
|
230 renderer = FieldProperty(IMilestoneParagraph['renderer']) |
|
231 |
|
232 |
|
233 @utility_config(name=MILESTONE_PARAGRAPH_TYPE, provides=IParagraphFactory) |
|
234 class MilestoneParagraphFactory(BaseParagraphFactory): |
|
235 """Milestones paragraph factory""" |
|
236 |
|
237 name = _("Milestones paragraph") |
|
238 content_type = MilestoneParagraph |
|
239 |
|
240 |
|
241 @adapter_config(context=IMilestoneParagraph, provides=IContentChecker) |
|
242 class MilestoneParagraphContentChecker(BaseParagraphContentChecker): |
|
243 """Milestones paragraph content checker""" |
|
244 |
|
245 @property |
|
246 def label(self): |
|
247 request = check_request() |
|
248 translate = request.localizer.translate |
|
249 return II18n(self.context).query_attribute('title', request) or \ |
|
250 '({0})'.format(translate(self.context.icon_hint).lower()) |
|
251 |
|
252 def inner_check(self, request): |
|
253 output = [] |
|
254 translate = request.localizer.translate |
|
255 manager = get_parent(self.context, II18nManager) |
|
256 if manager is not None: |
|
257 langs = manager.get_languages() |
|
258 else: |
|
259 negotiator = get_utility(INegotiator) |
|
260 langs = (negotiator.server_language, ) |
|
261 i18n = II18n(self.context) |
|
262 for lang in langs: |
|
263 value = i18n.get_attribute('title', lang, request) |
|
264 if not value: |
|
265 field_title = translate(IMilestoneParagraph['title'].title) |
|
266 if len(langs) == 1: |
|
267 output.append(translate(MISSING_VALUE).format(field=field_title)) |
|
268 else: |
|
269 output.append(translate(MISSING_LANG_VALUE).format(field=field_title, lang=lang)) |
|
270 return output |
|
271 |
|
272 |
|
273 @vocabulary_config(name=MILESTONE_PARAGRAPH_RENDERERS) |
|
274 class MilestoneParagraphRendererVocabulary(SimpleVocabulary): |
|
275 """Milestones paragraph renderers vocabulary""" |
|
276 |
|
277 def __init__(self, context=None): |
|
278 request = check_request() |
|
279 translate = request.localizer.translate |
|
280 registry = request.registry |
|
281 if not IMilestoneParagraph.providedBy(context): |
|
282 context = MilestoneParagraph() |
|
283 terms = [SimpleTerm(name, title=translate(adapter.label)) |
|
284 for name, adapter in sorted(registry.getAdapters((context, request), IContentRenderer), |
|
285 key=lambda x: x[1].weight)] |
|
286 super(MilestoneParagraphRendererVocabulary, self).__init__(terms) |