src/pyams_thesaurus/zmi/thesaurus.py
changeset 0 47700a43ef3f
child 3 5c1931a42176
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_thesaurus/zmi/thesaurus.py	Tue Apr 14 17:52:05 2015 +0200
@@ -0,0 +1,479 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyramid.response import Response
+from pyams_form.schema import CloseButton
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+
+from html import unescape
+
+# import interfaces
+from pyams_form.interfaces.form import IWidgetForm
+from pyams_skin.interfaces import IPageHeader, IInnerPage
+from pyams_skin.interfaces.container import ITableElementName, ITableElementEditor
+from pyams_skin.interfaces.viewlet import IToolbarAddingMenu
+from pyams_thesaurus.interfaces.loader import IThesaurusLoader, IThesaurusUpdaterConfiguration, \
+    IThesaurusExporterConfiguration, IThesaurusExporter
+from pyams_thesaurus.interfaces.thesaurus import IThesaurusInfo, IThesaurus, IThesaurusExtracts
+from pyams_thesaurus.zmi.interfaces import IThesaurusTermsMenu, IThesaurusView
+from pyams_utils.interfaces.tree import INode, ITree
+from pyams_zmi.interfaces import IAdminView
+from pyams_zmi.interfaces.menu import ISiteManagementMenu, IPropertiesMenu
+from pyams_zmi.layer import IAdminLayer
+from z3c.form.interfaces import IDataExtractedEvent, DISPLAY_MODE
+
+# import packages
+from pyams_form.form import AJAXAddForm, AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.layer import IPyAMSLayer
+from pyams_skin.page import InnerPage
+from pyams_skin.table import DefaultElementEditorAdapter
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_template.template import template_config
+from pyams_thesaurus.loader.config import ThesaurusUpdaterConfiguration, ThesaurusExporterConfiguration
+from pyams_thesaurus.thesaurus import Thesaurus
+from pyams_thesaurus.zmi.extract import ThesaurusExtractsTable
+from pyams_utils.adapter import adapter_config, ContextRequestAdapter, ContextRequestViewAdapter
+from pyams_utils.registry import query_utility, get_utility
+from pyams_utils.traversing import get_parent
+from pyams_utils.url import absolute_url
+from pyams_viewlet.manager import viewletmanager_config
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.control_panel import UtilitiesTable
+from pyams_zmi.form import AdminDialogAddForm, AdminEditForm
+from pyramid.events import subscriber
+from pyramid.exceptions import NotFound
+from pyramid.httpexceptions import HTTPBadRequest
+from pyramid.url import resource_url
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.component.interfaces import ISite
+from zope.interface import implementer, Interface, Invalid
+
+from pyams_thesaurus import _
+
+
+@adapter_config(context=(IThesaurus, IAdminLayer), provides=ITableElementName)
+class ThesaurusNameAdapter(ContextRequestAdapter):
+    """Thesaurus name adapter"""
+
+    @property
+    def name(self):
+        translate = self.request.localizer.translate
+        return translate(_("Thesaurus: {0}")).format(self.context.name)
+
+
+@viewlet_config(name='add-thesaurus.menu', context=ISite, layer=IAdminLayer,
+                view=UtilitiesTable, manager=IToolbarAddingMenu, permission='system.manage')
+class ThesaurusAddMenu(ToolbarMenuItem):
+    """Thesaurus add menu"""
+
+    label = _("Add thesaurus...")
+    label_css_class = 'fa fa-fw fa-language'
+    url = 'add-thesaurus.html'
+    modal_target = True
+
+
+@pagelet_config(name='add-thesaurus.html', context=ISite, layer=IPyAMSLayer, permission='system.manage')
+class ThesaurusAddForm(AdminDialogAddForm):
+    """Thesaurus add form"""
+
+    title = _("Utilities")
+    legend = _("Add thesaurus")
+    icon_css_class = 'fa fa-fw fa-language'
+
+    fields = field.Fields(IThesaurusInfo).select('name', 'title', 'subject', 'description', 'language', 'creator',
+                                                 'publisher', 'created')
+    ajax_handler = 'add-thesaurus.json'
+    edit_permission = None
+
+    def updateWidgets(self, prefix=None):
+        super(ThesaurusAddForm, self).updateWidgets(prefix)
+        self.widgets['description'].label_css_class = 'input textarea'
+
+    def create(self, data):
+        return Thesaurus()
+
+    def add(self, object):
+        manager = self.context.getSiteManager()
+        manager['thesaurus::{0}'.format(object.name.lower())] = object
+
+    def nextURL(self):
+        return absolute_url(self.context, self.request, 'utilities.html')
+
+
+@subscriber(IDataExtractedEvent, form_selector=ThesaurusAddForm)
+def handle_new_thesaurus_data_extraction(event):
+    """Handle new thesaurus data extraction"""
+    manager = event.form.context.getSiteManager()
+    name = event.data['name']
+    if 'thesaurus::{0}'.format(name.lower()) in manager:
+        event.form.widgets.errors += (Invalid(_("Specified thesaurus name is already used!")), )
+    thesaurus = query_utility(IThesaurus, name=name)
+    if thesaurus is not None:
+        event.form.widgets.errors += (Invalid(_("A thesaurus is already registered with this name!")), )
+
+
+@view_config(name='add-thesaurus.json', context=ISite, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class ThesaurusAJAXAddForm(AJAXAddForm, ThesaurusAddForm):
+    """Thesaurus add form, AJAX view"""
+
+
+@adapter_config(context=(IThesaurus, IAdminLayer, Interface), provides=ITableElementEditor)
+class ThesaurusTableElementEditor(DefaultElementEditorAdapter):
+    """Thesaurus table element editor"""
+
+    view_name = 'properties.html'
+    modal_target = False
+
+    @property
+    def url(self):
+        return resource_url(self.context, self.request, 'admin.html#{0}'.format(self.view_name))
+
+
+class ThesaurusHeaderAdapter(ContextRequestViewAdapter):
+    """Thesaurus views header adapter"""
+
+    @property
+    def back_url(self):
+        site = get_parent(self.context, ISite)
+        return absolute_url(site, self.request, 'admin.html#utilities.html')
+
+    back_target = None
+    icon_class = 'fa fa-fw fa-language'
+    title = _("Thesaurus management")
+
+
+@viewlet_config(name='properties.menu', layer=IAdminLayer, context=IThesaurus, manager=ISiteManagementMenu,
+                permission='system.view', weight=1)
+@viewletmanager_config(name='properties.menu', layer=IAdminLayer, context=IThesaurus, provides=IPropertiesMenu)
+@implementer(IPropertiesMenu)
+class ThesaurusPropertiesMenuItem(MenuItem):
+    """Thesaurus properties menu"""
+
+    label = _("Properties")
+    icon_class = 'fa fa-fw fa-language'
+    url = '#properties.html'
+
+
+@pagelet_config(name='properties.html', context=IThesaurus, layer=IPyAMSLayer, permission='system.view')
+@implementer(IWidgetForm, IInnerPage, IThesaurusView)
+class ThesaurusPropertiesEditForm(AdminEditForm):
+    """Thesaurus properties edit form"""
+
+    @property
+    def title(self):
+        translate = self.request.localizer.translate
+        return translate(_("Thesaurus: {0}")).format(self.context.name)
+
+    legend = _("Update thesaurus properties")
+    icon_css_class = 'fa fa-fw fa-language'
+
+    fields = field.Fields(IThesaurusInfo).select('name', 'title', 'subject', 'description', 'language', 'creator',
+                                                 'publisher', 'created')
+    ajax_handler = 'properties.json'
+    edit_permission = 'system.manage'
+
+    def updateWidgets(self, prefix=None):
+        super(ThesaurusPropertiesEditForm, self).updateWidgets(prefix)
+        self.widgets['name'].mode = DISPLAY_MODE
+        self.widgets['description'].label_css_class = 'input textarea'
+
+
+@view_config(name='properties.json', context=IThesaurus, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class ThesaurusPropertiesAJAXEditForm(AJAXEditForm, ThesaurusPropertiesEditForm):
+    """Thesaurus properties edit form, AJAX view"""
+
+
+@adapter_config(context=(IThesaurus, IAdminLayer, ThesaurusPropertiesEditForm), provides=IPageHeader)
+class ThesaurusPropertiesEditFormHeaderAdapter(ThesaurusHeaderAdapter):
+
+    subtitle = _("Thesaurus properties")
+
+
+#
+# Thesaurus terms views
+#
+
+@viewlet_config(name='terms.menu', context=IThesaurus, layer=IAdminLayer, manager=ISiteManagementMenu,
+                permission='system.view', weight=10)
+@viewletmanager_config(name='terms.menu', layer=IAdminLayer, context=IThesaurus, provides=IThesaurusTermsMenu)
+@implementer(IThesaurusTermsMenu)
+class ThesaurusTermsMenuItem(MenuItem):
+    """Thesaurus terms menu"""
+
+    label = _("Terms")
+    icon_class = 'fa fa-fw fa-tags'
+    url = '#terms.html'
+
+
+@pagelet_config(name='terms.html', context=IThesaurus, layer=IPyAMSLayer, permission='system.view')
+@template_config(template='templates/terms-tree.pt', layer=IPyAMSLayer)
+@implementer(IAdminView, IThesaurusView)
+class ThesaurusTermsView(InnerPage):
+    """Thesaurus terms view"""
+
+    def __init__(self, context, request):
+        super(ThesaurusTermsView, self).__init__(context, request)
+        self.extracts = IThesaurusExtracts(context)
+        self.extracts_view = ThesaurusExtractsTable(context, request)
+
+    def update(self):
+        super(ThesaurusTermsView, self).update()
+        self.extracts_view.update()
+
+    @property
+    def tree(self):
+        return sorted([INode(node) for node in ITree(self.context).get_root_nodes()],
+                      key=lambda x: x.label)
+
+    @property
+    def search_query_params(self):
+        return json.dumps({'thesaurus_name': self.context.name})
+
+
+@adapter_config(context=(IThesaurus, IPyAMSLayer, ThesaurusTermsView), provides=IPageHeader)
+class ThesaurusTermsHeaderAdapter(ThesaurusHeaderAdapter):
+
+    subtitle = _("Thesaurus terms")
+
+
+class BaseTreeNodesView(object):
+    """Base tree nodes views"""
+
+    def __init__(self, request):
+        self.request = request
+
+    def get_nodes(self, term, result, subnodes=None):
+        extracts = IThesaurusExtracts(get_parent(term, IThesaurus))
+        translate = self.request.localizer.translate
+        for child in INode(term).get_children():
+            node = INode(child)
+            result.append({'label': node.label.replace("'", "&#039;"),
+                           'view': absolute_url(node.context, self.request, 'properties.html'),
+                           'css_class': node.css_class,
+                           'extracts': [{'name': name,
+                                         'title': extract.name,
+                                         'color': extract.color,
+                                         'used': name in (node.context.extracts or ())}
+                                        for name, extract in sorted(extracts.items(), key=lambda x: x[0])],
+                           'extensions': [{'title': translate(ext.label, context=self.request),
+                                           'icon': ext.icon,
+                                           'view': absolute_url(node.context, self.request, ext.target_view)}
+                                          for ext in node.context.query_extensions()],
+                           'expand': node.has_children()})
+            if subnodes and (node.context.label in subnodes):
+                nodes = result[-1]['subnodes'] = []
+                self.get_nodes(node.context, nodes, subnodes)
+
+
+@view_config(name='get-nodes.json', context=IThesaurus, request_type=IPyAMSLayer,
+             permission='view', renderer='json', xhr=True)
+class ThesaurusTermNodes(BaseTreeNodesView):
+    """Get thesaurus nodes"""
+
+    def __call__(self):
+        label = self.request.params.get('term')
+        if label:
+            label = unescape(label)
+        term = self.request.context.terms.get(label)
+        if term is None:
+            raise NotFound
+        result = []
+        self.get_nodes(term, result)
+        return {'term': label,
+                'nodes': sorted(result, key=lambda x: x['label'])}
+
+
+@view_config(name='get-parent-nodes.json', context=IThesaurus, request_type=IPyAMSLayer,
+             permission='view', renderer='json', xhr=True)
+class ThesaurusTermParentNodes(BaseTreeNodesView):
+    """Get thesaurus parent nodes"""
+
+    def __call__(self):
+        label = self.request.params.get('term')
+        if label:
+            label = unescape(label)
+        term = self.request.context.terms.get(label)
+        if term is None:
+            raise NotFound
+        result = []
+        parents = list(reversed(term.get_parents()))
+        if parents:
+            self.get_nodes(parents[0], result, [t.label for t in parents])
+            return {'term': label,
+                    'nodes': result,
+                    'parent': parents[0].label}
+        else:
+            return {'term': label,
+                    'nodes': result,
+                    'parent': label}
+
+
+@view_config(name='switch-extract.json', context=IThesaurus, request_type=IPyAMSLayer,
+             permission='thesaurus.manage', renderer='json', xhr=True)
+def switch_term_extract(request):
+    """Term extract switcher"""
+    label = request.params.get('term')
+    extract_name = request.params.get('extract')
+    if not (label and extract_name):
+        raise HTTPBadRequest("Missing arguments")
+    thesaurus = request.context
+    term = thesaurus.terms.get(unescape(label))
+    if term is None:
+        raise HTTPBadRequest("Term not found")
+    extract = IThesaurusExtracts(thesaurus).get(extract_name)
+    if extract is None:
+        raise HTTPBadRequest("Extract not found")
+    if extract.name in (term.extracts or ()):
+        extract.remove_term(term)
+    else:
+        extract.add_term(term)
+    return {'term': term.label,
+            'extract': extract.name,
+            'color': extract.color,
+            'used': extract.name in term.extracts}
+
+
+#
+# Terms import views
+#
+
+@viewlet_config(name='import.menu', context=IThesaurus, layer=IAdminLayer, manager=IThesaurusTermsMenu,
+                permission='system.manage', weight=10)
+class ThesaurusImportMenuItem(MenuItem):
+    """Thesaurus import menu"""
+
+    label = _("Import terms...")
+    icon_class = 'fa fa-fw fa-upload'
+    url = 'import.html'
+
+    modal_target = True
+
+
+class IThesaurusFormImportButtons(Interface):
+    """Thesaurus import form buttons"""
+
+    close = CloseButton(name='close', title=_("Close"))
+    add = button.Button(name='add', title=_("Import terms"))
+
+
+@pagelet_config(name='import.html', context=IThesaurus, layer=IPyAMSLayer, permission='system.manage')
+class ThesaurusImportForm(AdminDialogAddForm):
+    """Thesaurus import form"""
+
+    title = _("Thesaurus")
+    legend = _("Import thesaurus terms")
+    icon_css_class = 'fa fa-fw fa-upload'
+
+    fields = field.Fields(IThesaurusUpdaterConfiguration).select('clear', 'conflict_suffix', 'data', 'format',
+                                                                 'import_synonyms', 'language', 'encoding')
+    buttons = button.Buttons(IThesaurusFormImportButtons)
+
+    ajax_handler = 'import.json'
+    edit_permission = None
+
+    def updateWidgets(self, prefix=None):
+        super(ThesaurusImportForm, self).updateWidgets(prefix)
+        self.widgets['language'].noValueMessage = _("-- automatic selection -- (if available)")
+        self.widgets['encoding'].noValueMessage = _("-- automatic selection -- (if available)")
+
+    def create(self, data):
+        configuration = ThesaurusUpdaterConfiguration(data)
+        loader = get_utility(IThesaurusLoader, name=configuration.format)
+        thesaurus = loader.load(data['data'], configuration)
+        target = self.context
+        if configuration.clear:
+            target.clear()
+        target.merge(configuration, thesaurus)
+
+    def update_content(self, content, data):
+        pass
+
+    def add(self, object):
+        pass
+
+    def nextURL(self):
+        return absolute_url(self.context, self.request, 'admin.html#terms.html')
+
+
+@view_config(name='import.json', context=IThesaurus, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class ThesaurusImportAJAXForm(AJAXAddForm, ThesaurusImportForm):
+    """Thesaurus import form, AJAX view"""
+
+
+#
+# Terms export views
+#
+
+@viewlet_config(name='export.menu', context=IThesaurus, layer=IAdminLayer, manager=IThesaurusTermsMenu,
+                permission='system.view', weight=15)
+class ThesaurusExportMenuItem(MenuItem):
+    """Thesaurus export menu"""
+
+    label = _("Export terms...")
+    icon_class = 'fa fa-fw fa-download'
+    url = 'export.html'
+
+    modal_target = True
+
+
+class IThesaurusFormExportButtons(Interface):
+    """Thesaurus export form buttons"""
+
+    close = CloseButton(name='close', title=_("Close"))
+    add = button.Button(name='add', title=_("Export terms"))
+
+
+@pagelet_config(name='export.html', context=IThesaurus, layer=IPyAMSLayer, permission='system.view')
+class ThesaurusExportForm(AdminDialogAddForm):
+    """Thesaurus export form"""
+
+    title = _("Thesaurus")
+    legend = _("export thesaurus terms")
+    icon_css_class = 'fa fa-fw fa-download'
+
+    fields = field.Fields(IThesaurusExporterConfiguration)
+    buttons = button.Buttons(IThesaurusFormExportButtons)
+
+    ajax_handler = 'export.xml'
+    download_target = 'download_frame'
+    edit_permission = None
+
+    configuration = None
+    exporter = None
+
+    def createAndAdd(self, data):
+        configuration = self.configuration = ThesaurusExporterConfiguration(data)
+        exporter = self.exporter = get_utility(IThesaurusExporter, name=configuration.format)
+        return exporter.export(self.context, configuration)
+
+
+@view_config(name='export.xml', context=IThesaurus, request_type=IPyAMSLayer, permission='system.manage')
+class ThesaurusExportAJAXForm(AJAXAddForm, ThesaurusExportForm):
+    """Thesaurus export form, AJAX view"""
+
+    def get_ajax_output(self, changes):
+        changes.seek(0)
+        headers = {'Content-Disposition': 'attachment; filename="{0}"'.format(self.configuration.filename)}
+        response = Response(content_type=self.exporter.handler.content_type,
+                            headers=headers)
+        response.body_file = changes
+        return response