src/pyams_thesaurus/zmi/thesaurus.py
changeset 0 47700a43ef3f
child 3 5c1931a42176
equal deleted inserted replaced
-1:000000000000 0:47700a43ef3f
       
     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("'", "&#039;"),
       
   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