|
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.response import Response |
|
13 from pyams_form.schema import CloseButton |
|
14 |
|
15 __docformat__ = 'restructuredtext' |
|
16 |
|
17 |
|
18 # import standard library |
|
19 import json |
|
20 |
|
21 from html import unescape |
|
22 |
|
23 # import interfaces |
|
24 from pyams_form.interfaces.form import IWidgetForm |
|
25 from pyams_skin.interfaces import IPageHeader, IInnerPage |
|
26 from pyams_skin.interfaces.container import ITableElementName, ITableElementEditor |
|
27 from pyams_skin.interfaces.viewlet import IToolbarAddingMenu |
|
28 from pyams_thesaurus.interfaces.loader import IThesaurusLoader, IThesaurusUpdaterConfiguration, \ |
|
29 IThesaurusExporterConfiguration, IThesaurusExporter |
|
30 from pyams_thesaurus.interfaces.thesaurus import IThesaurusInfo, IThesaurus, IThesaurusExtracts |
|
31 from pyams_thesaurus.zmi.interfaces import IThesaurusTermsMenu, IThesaurusView |
|
32 from pyams_utils.interfaces.tree import INode, ITree |
|
33 from pyams_zmi.interfaces import IAdminView |
|
34 from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu |
|
35 from pyams_zmi.layer import IAdminLayer |
|
36 from z3c.form.interfaces import IDataExtractedEvent, DISPLAY_MODE |
|
37 |
|
38 # import packages |
|
39 from pyams_form.form import AJAXAddForm, AJAXEditForm |
|
40 from pyams_pagelet.pagelet import pagelet_config |
|
41 from pyams_skin.layer import IPyAMSLayer |
|
42 from pyams_skin.page import InnerPage |
|
43 from pyams_skin.table import DefaultElementEditorAdapter |
|
44 from pyams_skin.viewlet.menu import MenuItem |
|
45 from pyams_skin.viewlet.toolbar import ToolbarMenuItem |
|
46 from pyams_template.template import template_config |
|
47 from pyams_thesaurus.loader.config import ThesaurusUpdaterConfiguration, ThesaurusExporterConfiguration |
|
48 from pyams_thesaurus.thesaurus import Thesaurus |
|
49 from pyams_thesaurus.zmi.extract import ThesaurusExtractsTable |
|
50 from pyams_utils.adapter import adapter_config, ContextRequestAdapter, ContextRequestViewAdapter |
|
51 from pyams_utils.registry import query_utility, get_utility |
|
52 from pyams_utils.traversing import get_parent |
|
53 from pyams_utils.url import absolute_url |
|
54 from pyams_viewlet.manager import viewletmanager_config |
|
55 from pyams_viewlet.viewlet import viewlet_config |
|
56 from pyams_zmi.control_panel import UtilitiesTable |
|
57 from pyams_zmi.form import AdminDialogAddForm, AdminEditForm |
|
58 from pyramid.events import subscriber |
|
59 from pyramid.exceptions import NotFound |
|
60 from pyramid.httpexceptions import HTTPBadRequest |
|
61 from pyramid.url import resource_url |
|
62 from pyramid.view import view_config |
|
63 from z3c.form import field, button |
|
64 from zope.component.interfaces import ISite |
|
65 from zope.interface import implementer, Interface, Invalid |
|
66 |
|
67 from pyams_thesaurus import _ |
|
68 |
|
69 |
|
70 @adapter_config(context=(IThesaurus, IAdminLayer), provides=ITableElementName) |
|
71 class ThesaurusNameAdapter(ContextRequestAdapter): |
|
72 """Thesaurus name adapter""" |
|
73 |
|
74 @property |
|
75 def name(self): |
|
76 translate = self.request.localizer.translate |
|
77 return translate(_("Thesaurus: {0}")).format(self.context.name) |
|
78 |
|
79 |
|
80 @viewlet_config(name='add-thesaurus.menu', context=ISite, layer=IAdminLayer, |
|
81 view=UtilitiesTable, manager=IToolbarAddingMenu, permission='system.manage') |
|
82 class ThesaurusAddMenu(ToolbarMenuItem): |
|
83 """Thesaurus add menu""" |
|
84 |
|
85 label = _("Add thesaurus...") |
|
86 label_css_class = 'fa fa-fw fa-language' |
|
87 url = 'add-thesaurus.html' |
|
88 modal_target = True |
|
89 |
|
90 |
|
91 @pagelet_config(name='add-thesaurus.html', context=ISite, layer=IPyAMSLayer, permission='system.manage') |
|
92 class ThesaurusAddForm(AdminDialogAddForm): |
|
93 """Thesaurus add form""" |
|
94 |
|
95 title = _("Utilities") |
|
96 legend = _("Add thesaurus") |
|
97 icon_css_class = 'fa fa-fw fa-language' |
|
98 |
|
99 fields = field.Fields(IThesaurusInfo).select('name', 'title', 'subject', 'description', 'language', 'creator', |
|
100 'publisher', 'created') |
|
101 ajax_handler = 'add-thesaurus.json' |
|
102 edit_permission = None |
|
103 |
|
104 def updateWidgets(self, prefix=None): |
|
105 super(ThesaurusAddForm, self).updateWidgets(prefix) |
|
106 self.widgets['description'].label_css_class = 'input textarea' |
|
107 |
|
108 def create(self, data): |
|
109 return Thesaurus() |
|
110 |
|
111 def add(self, object): |
|
112 manager = self.context.getSiteManager() |
|
113 manager['thesaurus::{0}'.format(object.name.lower())] = object |
|
114 |
|
115 def nextURL(self): |
|
116 return absolute_url(self.context, self.request, 'utilities.html') |
|
117 |
|
118 |
|
119 @subscriber(IDataExtractedEvent, form_selector=ThesaurusAddForm) |
|
120 def handle_new_thesaurus_data_extraction(event): |
|
121 """Handle new thesaurus data extraction""" |
|
122 manager = event.form.context.getSiteManager() |
|
123 name = event.data['name'] |
|
124 if 'thesaurus::{0}'.format(name.lower()) in manager: |
|
125 event.form.widgets.errors += (Invalid(_("Specified thesaurus name is already used!")), ) |
|
126 thesaurus = query_utility(IThesaurus, name=name) |
|
127 if thesaurus is not None: |
|
128 event.form.widgets.errors += (Invalid(_("A thesaurus is already registered with this name!")), ) |
|
129 |
|
130 |
|
131 @view_config(name='add-thesaurus.json', context=ISite, request_type=IPyAMSLayer, |
|
132 permission='system.manage', renderer='json', xhr=True) |
|
133 class ThesaurusAJAXAddForm(AJAXAddForm, ThesaurusAddForm): |
|
134 """Thesaurus add form, AJAX view""" |
|
135 |
|
136 |
|
137 @adapter_config(context=(IThesaurus, IAdminLayer, Interface), provides=ITableElementEditor) |
|
138 class ThesaurusTableElementEditor(DefaultElementEditorAdapter): |
|
139 """Thesaurus table element editor""" |
|
140 |
|
141 view_name = 'properties.html' |
|
142 modal_target = False |
|
143 |
|
144 @property |
|
145 def url(self): |
|
146 return resource_url(self.context, self.request, 'admin.html#{0}'.format(self.view_name)) |
|
147 |
|
148 |
|
149 class ThesaurusHeaderAdapter(ContextRequestViewAdapter): |
|
150 """Thesaurus views header adapter""" |
|
151 |
|
152 @property |
|
153 def back_url(self): |
|
154 site = get_parent(self.context, ISite) |
|
155 return absolute_url(site, self.request, 'admin.html#utilities.html') |
|
156 |
|
157 back_target = None |
|
158 icon_class = 'fa fa-fw fa-language' |
|
159 title = _("Thesaurus management") |
|
160 |
|
161 |
|
162 @viewlet_config(name='properties.menu', layer=IAdminLayer, context=IThesaurus, manager=ISiteManagementMenu, |
|
163 permission='system.view', weight=1) |
|
164 @viewletmanager_config(name='properties.menu', layer=IAdminLayer, context=IThesaurus, provides=IPropertiesMenu) |
|
165 @implementer(IPropertiesMenu) |
|
166 class ThesaurusPropertiesMenuItem(MenuItem): |
|
167 """Thesaurus properties menu""" |
|
168 |
|
169 label = _("Properties") |
|
170 icon_class = 'fa fa-fw fa-language' |
|
171 url = '#properties.html' |
|
172 |
|
173 |
|
174 @pagelet_config(name='properties.html', context=IThesaurus, layer=IPyAMSLayer, permission='system.view') |
|
175 @implementer(IWidgetForm, IInnerPage, IThesaurusView) |
|
176 class ThesaurusPropertiesEditForm(AdminEditForm): |
|
177 """Thesaurus properties edit form""" |
|
178 |
|
179 @property |
|
180 def title(self): |
|
181 translate = self.request.localizer.translate |
|
182 return translate(_("Thesaurus: {0}")).format(self.context.name) |
|
183 |
|
184 legend = _("Update thesaurus properties") |
|
185 icon_css_class = 'fa fa-fw fa-language' |
|
186 |
|
187 fields = field.Fields(IThesaurusInfo).select('name', 'title', 'subject', 'description', 'language', 'creator', |
|
188 'publisher', 'created') |
|
189 ajax_handler = 'properties.json' |
|
190 edit_permission = 'system.manage' |
|
191 |
|
192 def updateWidgets(self, prefix=None): |
|
193 super(ThesaurusPropertiesEditForm, self).updateWidgets(prefix) |
|
194 self.widgets['name'].mode = DISPLAY_MODE |
|
195 self.widgets['description'].label_css_class = 'input textarea' |
|
196 |
|
197 |
|
198 @view_config(name='properties.json', context=IThesaurus, request_type=IPyAMSLayer, |
|
199 permission='system.manage', renderer='json', xhr=True) |
|
200 class ThesaurusPropertiesAJAXEditForm(AJAXEditForm, ThesaurusPropertiesEditForm): |
|
201 """Thesaurus properties edit form, AJAX view""" |
|
202 |
|
203 |
|
204 @adapter_config(context=(IThesaurus, IAdminLayer, ThesaurusPropertiesEditForm), provides=IPageHeader) |
|
205 class ThesaurusPropertiesEditFormHeaderAdapter(ThesaurusHeaderAdapter): |
|
206 |
|
207 subtitle = _("Thesaurus properties") |
|
208 |
|
209 |
|
210 # |
|
211 # Thesaurus terms views |
|
212 # |
|
213 |
|
214 @viewlet_config(name='terms.menu', context=IThesaurus, layer=IAdminLayer, manager=ISiteManagementMenu, |
|
215 permission='system.view', weight=10) |
|
216 @viewletmanager_config(name='terms.menu', layer=IAdminLayer, context=IThesaurus, provides=IThesaurusTermsMenu) |
|
217 @implementer(IThesaurusTermsMenu) |
|
218 class ThesaurusTermsMenuItem(MenuItem): |
|
219 """Thesaurus terms menu""" |
|
220 |
|
221 label = _("Terms") |
|
222 icon_class = 'fa fa-fw fa-tags' |
|
223 url = '#terms.html' |
|
224 |
|
225 |
|
226 @pagelet_config(name='terms.html', context=IThesaurus, layer=IPyAMSLayer, permission='system.view') |
|
227 @template_config(template='templates/terms-tree.pt', layer=IPyAMSLayer) |
|
228 @implementer(IAdminView, IThesaurusView) |
|
229 class ThesaurusTermsView(InnerPage): |
|
230 """Thesaurus terms view""" |
|
231 |
|
232 def __init__(self, context, request): |
|
233 super(ThesaurusTermsView, self).__init__(context, request) |
|
234 self.extracts = IThesaurusExtracts(context) |
|
235 self.extracts_view = ThesaurusExtractsTable(context, request) |
|
236 |
|
237 def update(self): |
|
238 super(ThesaurusTermsView, self).update() |
|
239 self.extracts_view.update() |
|
240 |
|
241 @property |
|
242 def tree(self): |
|
243 return sorted([INode(node) for node in ITree(self.context).get_root_nodes()], |
|
244 key=lambda x: x.label) |
|
245 |
|
246 @property |
|
247 def search_query_params(self): |
|
248 return json.dumps({'thesaurus_name': self.context.name}) |
|
249 |
|
250 |
|
251 @adapter_config(context=(IThesaurus, IPyAMSLayer, ThesaurusTermsView), provides=IPageHeader) |
|
252 class ThesaurusTermsHeaderAdapter(ThesaurusHeaderAdapter): |
|
253 |
|
254 subtitle = _("Thesaurus terms") |
|
255 |
|
256 |
|
257 class BaseTreeNodesView(object): |
|
258 """Base tree nodes views""" |
|
259 |
|
260 def __init__(self, request): |
|
261 self.request = request |
|
262 |
|
263 def get_nodes(self, term, result, subnodes=None): |
|
264 extracts = IThesaurusExtracts(get_parent(term, IThesaurus)) |
|
265 translate = self.request.localizer.translate |
|
266 for child in INode(term).get_children(): |
|
267 node = INode(child) |
|
268 result.append({'label': node.label.replace("'", "'"), |
|
269 'view': absolute_url(node.context, self.request, 'properties.html'), |
|
270 'css_class': node.css_class, |
|
271 'extracts': [{'name': name, |
|
272 'title': extract.name, |
|
273 'color': extract.color, |
|
274 'used': name in (node.context.extracts or ())} |
|
275 for name, extract in sorted(extracts.items(), key=lambda x: x[0])], |
|
276 'extensions': [{'title': translate(ext.label, context=self.request), |
|
277 'icon': ext.icon, |
|
278 'view': absolute_url(node.context, self.request, ext.target_view)} |
|
279 for ext in node.context.query_extensions()], |
|
280 'expand': node.has_children()}) |
|
281 if subnodes and (node.context.label in subnodes): |
|
282 nodes = result[-1]['subnodes'] = [] |
|
283 self.get_nodes(node.context, nodes, subnodes) |
|
284 |
|
285 |
|
286 @view_config(name='get-nodes.json', context=IThesaurus, request_type=IPyAMSLayer, |
|
287 permission='view', renderer='json', xhr=True) |
|
288 class ThesaurusTermNodes(BaseTreeNodesView): |
|
289 """Get thesaurus nodes""" |
|
290 |
|
291 def __call__(self): |
|
292 label = self.request.params.get('term') |
|
293 if label: |
|
294 label = unescape(label) |
|
295 term = self.request.context.terms.get(label) |
|
296 if term is None: |
|
297 raise NotFound |
|
298 result = [] |
|
299 self.get_nodes(term, result) |
|
300 return {'term': label, |
|
301 'nodes': sorted(result, key=lambda x: x['label'])} |
|
302 |
|
303 |
|
304 @view_config(name='get-parent-nodes.json', context=IThesaurus, request_type=IPyAMSLayer, |
|
305 permission='view', renderer='json', xhr=True) |
|
306 class ThesaurusTermParentNodes(BaseTreeNodesView): |
|
307 """Get thesaurus parent nodes""" |
|
308 |
|
309 def __call__(self): |
|
310 label = self.request.params.get('term') |
|
311 if label: |
|
312 label = unescape(label) |
|
313 term = self.request.context.terms.get(label) |
|
314 if term is None: |
|
315 raise NotFound |
|
316 result = [] |
|
317 parents = list(reversed(term.get_parents())) |
|
318 if parents: |
|
319 self.get_nodes(parents[0], result, [t.label for t in parents]) |
|
320 return {'term': label, |
|
321 'nodes': result, |
|
322 'parent': parents[0].label} |
|
323 else: |
|
324 return {'term': label, |
|
325 'nodes': result, |
|
326 'parent': label} |
|
327 |
|
328 |
|
329 @view_config(name='switch-extract.json', context=IThesaurus, request_type=IPyAMSLayer, |
|
330 permission='thesaurus.manage', renderer='json', xhr=True) |
|
331 def switch_term_extract(request): |
|
332 """Term extract switcher""" |
|
333 label = request.params.get('term') |
|
334 extract_name = request.params.get('extract') |
|
335 if not (label and extract_name): |
|
336 raise HTTPBadRequest("Missing arguments") |
|
337 thesaurus = request.context |
|
338 term = thesaurus.terms.get(unescape(label)) |
|
339 if term is None: |
|
340 raise HTTPBadRequest("Term not found") |
|
341 extract = IThesaurusExtracts(thesaurus).get(extract_name) |
|
342 if extract is None: |
|
343 raise HTTPBadRequest("Extract not found") |
|
344 if extract.name in (term.extracts or ()): |
|
345 extract.remove_term(term) |
|
346 else: |
|
347 extract.add_term(term) |
|
348 return {'term': term.label, |
|
349 'extract': extract.name, |
|
350 'color': extract.color, |
|
351 'used': extract.name in term.extracts} |
|
352 |
|
353 |
|
354 # |
|
355 # Terms import views |
|
356 # |
|
357 |
|
358 @viewlet_config(name='import.menu', context=IThesaurus, layer=IAdminLayer, manager=IThesaurusTermsMenu, |
|
359 permission='system.manage', weight=10) |
|
360 class ThesaurusImportMenuItem(MenuItem): |
|
361 """Thesaurus import menu""" |
|
362 |
|
363 label = _("Import terms...") |
|
364 icon_class = 'fa fa-fw fa-upload' |
|
365 url = 'import.html' |
|
366 |
|
367 modal_target = True |
|
368 |
|
369 |
|
370 class IThesaurusFormImportButtons(Interface): |
|
371 """Thesaurus import form buttons""" |
|
372 |
|
373 close = CloseButton(name='close', title=_("Close")) |
|
374 add = button.Button(name='add', title=_("Import terms")) |
|
375 |
|
376 |
|
377 @pagelet_config(name='import.html', context=IThesaurus, layer=IPyAMSLayer, permission='system.manage') |
|
378 class ThesaurusImportForm(AdminDialogAddForm): |
|
379 """Thesaurus import form""" |
|
380 |
|
381 title = _("Thesaurus") |
|
382 legend = _("Import thesaurus terms") |
|
383 icon_css_class = 'fa fa-fw fa-upload' |
|
384 |
|
385 fields = field.Fields(IThesaurusUpdaterConfiguration).select('clear', 'conflict_suffix', 'data', 'format', |
|
386 'import_synonyms', 'language', 'encoding') |
|
387 buttons = button.Buttons(IThesaurusFormImportButtons) |
|
388 |
|
389 ajax_handler = 'import.json' |
|
390 edit_permission = None |
|
391 |
|
392 def updateWidgets(self, prefix=None): |
|
393 super(ThesaurusImportForm, self).updateWidgets(prefix) |
|
394 self.widgets['language'].noValueMessage = _("-- automatic selection -- (if available)") |
|
395 self.widgets['encoding'].noValueMessage = _("-- automatic selection -- (if available)") |
|
396 |
|
397 def create(self, data): |
|
398 configuration = ThesaurusUpdaterConfiguration(data) |
|
399 loader = get_utility(IThesaurusLoader, name=configuration.format) |
|
400 thesaurus = loader.load(data['data'], configuration) |
|
401 target = self.context |
|
402 if configuration.clear: |
|
403 target.clear() |
|
404 target.merge(configuration, thesaurus) |
|
405 |
|
406 def update_content(self, content, data): |
|
407 pass |
|
408 |
|
409 def add(self, object): |
|
410 pass |
|
411 |
|
412 def nextURL(self): |
|
413 return absolute_url(self.context, self.request, 'admin.html#terms.html') |
|
414 |
|
415 |
|
416 @view_config(name='import.json', context=IThesaurus, request_type=IPyAMSLayer, |
|
417 permission='system.manage', renderer='json', xhr=True) |
|
418 class ThesaurusImportAJAXForm(AJAXAddForm, ThesaurusImportForm): |
|
419 """Thesaurus import form, AJAX view""" |
|
420 |
|
421 |
|
422 # |
|
423 # Terms export views |
|
424 # |
|
425 |
|
426 @viewlet_config(name='export.menu', context=IThesaurus, layer=IAdminLayer, manager=IThesaurusTermsMenu, |
|
427 permission='system.view', weight=15) |
|
428 class ThesaurusExportMenuItem(MenuItem): |
|
429 """Thesaurus export menu""" |
|
430 |
|
431 label = _("Export terms...") |
|
432 icon_class = 'fa fa-fw fa-download' |
|
433 url = 'export.html' |
|
434 |
|
435 modal_target = True |
|
436 |
|
437 |
|
438 class IThesaurusFormExportButtons(Interface): |
|
439 """Thesaurus export form buttons""" |
|
440 |
|
441 close = CloseButton(name='close', title=_("Close")) |
|
442 add = button.Button(name='add', title=_("Export terms")) |
|
443 |
|
444 |
|
445 @pagelet_config(name='export.html', context=IThesaurus, layer=IPyAMSLayer, permission='system.view') |
|
446 class ThesaurusExportForm(AdminDialogAddForm): |
|
447 """Thesaurus export form""" |
|
448 |
|
449 title = _("Thesaurus") |
|
450 legend = _("export thesaurus terms") |
|
451 icon_css_class = 'fa fa-fw fa-download' |
|
452 |
|
453 fields = field.Fields(IThesaurusExporterConfiguration) |
|
454 buttons = button.Buttons(IThesaurusFormExportButtons) |
|
455 |
|
456 ajax_handler = 'export.xml' |
|
457 download_target = 'download_frame' |
|
458 edit_permission = None |
|
459 |
|
460 configuration = None |
|
461 exporter = None |
|
462 |
|
463 def createAndAdd(self, data): |
|
464 configuration = self.configuration = ThesaurusExporterConfiguration(data) |
|
465 exporter = self.exporter = get_utility(IThesaurusExporter, name=configuration.format) |
|
466 return exporter.export(self.context, configuration) |
|
467 |
|
468 |
|
469 @view_config(name='export.xml', context=IThesaurus, request_type=IPyAMSLayer, permission='system.manage') |
|
470 class ThesaurusExportAJAXForm(AJAXAddForm, ThesaurusExportForm): |
|
471 """Thesaurus export form, AJAX view""" |
|
472 |
|
473 def get_ajax_output(self, changes): |
|
474 changes.seek(0) |
|
475 headers = {'Content-Disposition': 'attachment; filename="{0}"'.format(self.configuration.filename)} |
|
476 response = Response(content_type=self.exporter.handler.content_type, |
|
477 headers=headers) |
|
478 response.body_file = changes |
|
479 return response |