Added glossary support features
authorThierry Florac <thierry.florac@onf.fr>
Fri, 28 Dec 2018 10:45:11 +0100
changeset 310 9e2e4c39f732
parent 309 cf5d41f5e210
child 311 0c9d115b227c
Added glossary support features
src/pyams_default_theme/features/glossary/__init__.py
src/pyams_default_theme/features/glossary/sitemap.py
src/pyams_default_theme/features/glossary/templates/glossary-sitemap.pt
src/pyams_default_theme/features/glossary/templates/glossary-term.pt
src/pyams_default_theme/features/glossary/templates/glossary.pt
src/pyams_default_theme/features/thesaurus/templates/glossary.pt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/features/glossary/__init__.py	Fri Dec 28 10:45:11 2018 +0100
@@ -0,0 +1,168 @@
+#
+# 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+import logging
+import pickle
+
+from pyramid.decorator import reify
+from pyramid.exceptions import NotFound
+from pyramid.interfaces import IRequest
+from pyramid.view import view_config
+from zope.interface import implementer
+
+from pyams_cache.beaker import get_cache
+from pyams_content.component.theme import ITagsManager
+from pyams_content.features.glossary import get_glossary_automaton, GLOSSARY_CACHE_REGION, GLOSSARY_CACHE_NAME, \
+    GLOSSARY_CACHE_KEY
+from pyams_default_theme.features.thesaurus.interfaces import IThesaurusTermRenderer
+from pyams_default_theme.page import BaseIndexPage
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.interfaces import IDialog
+from pyams_skin.layer import IPyAMSUserLayer
+from pyams_template.template import template_config
+from pyams_thesaurus.interfaces.thesaurus import IThesaurus
+from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyams_utils.interfaces import VIEW_PERMISSION
+from pyams_utils.interfaces.text import IHTMLRenderer
+from pyams_utils.registry import query_utility
+
+logger = logging.getLogger("PyAMS (default theme)")
+
+
+#
+# Full glossary terms page
+#
+
+@pagelet_config(name='get-glossary.html', layer=IPyAMSUserLayer, permission=VIEW_PERMISSION)
+@template_config(template='templates/glossary.pt', layer=IPyAMSUserLayer)
+class GlossaryView(BaseIndexPage):
+    """Glossary view"""
+
+    @reify
+    def thesaurus(self):
+        tags_manager = ITagsManager(self.request.root)
+        if not tags_manager.enable_glossary:
+            raise NotFound("Glossary access is not defined.")
+        thesaurus = query_utility(IThesaurus, name=tags_manager.glossary_thesaurus_name)
+        if thesaurus is None:
+            raise NotFound("Glossary thesaurus can't be found.")
+        return thesaurus
+
+    @property
+    def title(self):
+        return self.thesaurus.title
+
+    def update(self):
+        super(GlossaryView, self).update()
+        self.request.context = self.thesaurus
+
+
+#
+# Single glossary term views
+#
+
+@template_config(template='templates/glossary-term.pt', layer=IPyAMSUserLayer)
+class BaseGlossaryTermView(object):
+    """Base glossary term view"""
+
+    @reify
+    def thesaurus(self):
+        tags_manager = ITagsManager(self.request.root)
+        if not tags_manager.enable_glossary:
+            raise NotFound("Glossary access is not defined.")
+        thesaurus = query_utility(IThesaurus, name=tags_manager.glossary_thesaurus_name)
+        if thesaurus is None:
+            raise NotFound("Glossary thesaurus can't be found.")
+        return thesaurus
+
+    @property
+    def title(self):
+        return self.thesaurus.title
+
+    @reify
+    def term(self):
+        term = self.request.params.get('term')
+        if not term:
+            raise NotFound("No glossary term defined.")
+        term = self.thesaurus.terms.get(term)
+        if term is None:
+            raise NotFound("Can't find specified term.")
+        return term
+
+    @property
+    def renderers(self):
+        registry = self.request.registry
+        for name, renderer in sorted(registry.getAdapters((self.term, self.request, self), IThesaurusTermRenderer),
+                                     key=lambda x: x[1].weight):
+            renderer.update()
+            yield renderer
+
+    @property
+    def is_dialog(self):
+        return IDialog.providedBy(self)
+
+
+@pagelet_config(name='get-glossary-term-page.html', layer=IPyAMSUserLayer, permission=VIEW_PERMISSION)
+class GlossaryTermPage(BaseGlossaryTermView, BaseIndexPage):
+    """Full-page glossary term view"""
+
+    def update(self):
+        super(GlossaryTermPage, self).update()
+        self.request.context = self.term
+
+
+@pagelet_config(name='get-glossary-term.html', layer=IPyAMSUserLayer, permission=VIEW_PERMISSION)
+@implementer(IDialog)
+class GlossaryTermView(BaseGlossaryTermView):
+    """Dialog glossary term view"""
+
+    dialog_class = 'modal-large'
+
+
+@adapter_config(name='glossary', context=(str, IRequest), provides=IHTMLRenderer)
+class GlossaryHTMLRenderer(ContextRequestAdapter):
+    """Glossary HTML renderer"""
+
+    def render(self):
+        source = self.context
+        glossary_cache = get_cache(GLOSSARY_CACHE_REGION, GLOSSARY_CACHE_NAME)
+        try:
+            logger.debug("Loading glossary automaton...")
+            automaton = glossary_cache.get_value(GLOSSARY_CACHE_KEY)
+        except KeyError:
+            logger.debug("Automaton not found, loading new one...")
+            automaton = get_glossary_automaton(self.request.root)
+        else:
+            automaton = pickle.loads(automaton)
+        if automaton is None:
+            logger.debug("Missing automaton, skipping HTML conversion")
+            return source
+        logger.debug("Automaton loaded with {} terms!".format(len(automaton)))
+        found, last_found_index = set(), 0
+        marker = '<span class="thesaurus-term">{}</span>'
+        marker_length = len(marker) - 2
+        for position, text in automaton.iter(self.context):
+            if (text in found) or (position <= last_found_index):
+                continue
+            logger.debug("Found term '{}' at position {}".format(text, position))
+            count = len(found)
+            offset = marker_length * count
+            start_offset = position + offset - len(text)
+            if source[start_offset] in '<>':
+                logger.debug("Already tagged term, skipping...")
+                continue
+            source = source[0:start_offset + 1] + marker.format(text) + source[position + offset + 1:]
+            found.add(text)
+            last_found_index = position
+        return source
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/features/glossary/sitemap.py	Fri Dec 28 10:45:11 2018 +0100
@@ -0,0 +1,49 @@
+#
+# Copyright (c) 2008-2018 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.
+#
+
+__docformat__ = 'restructuredtext'
+
+from pyramid.view import view_config
+
+from pyams_content.component.theme import ITagsManager
+from pyams_content.root import ISiteRoot
+from pyams_default_theme.features.sitemap import ISitemapExtension
+from pyams_skin.layer import IPyAMSUserLayer
+from pyams_thesaurus.interfaces.thesaurus import IThesaurus
+from pyams_utils.adapter import ContextRequestAdapter, adapter_config
+from pyams_utils.registry import query_utility
+
+
+@adapter_config(name='glossary', context=(ISiteRoot, IPyAMSUserLayer), provides=ISitemapExtension)
+class SiteRootGlossarySitemap(ContextRequestAdapter):
+    """Site root glossary sitemap extension"""
+
+    @property
+    def source(self):
+        tags_manager = ITagsManager(self.context)
+        if tags_manager.enable_glossary:
+            thesaurus = query_utility(IThesaurus, name=tags_manager.glossary_thesaurus_name)
+            if thesaurus is not None:
+                return thesaurus
+
+
+@view_config(name='sitemap.xml', context=IThesaurus, request_type=IPyAMSUserLayer,
+             renderer='templates/glossary-sitemap.pt')
+class GlossarySitemapView(object):
+    """Glossary sitemap view"""
+
+    def __init__(self, request):
+        self.request = request
+
+    def __call__(self):
+        self.request.response.content_type = 'text/xml'
+        return {}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/features/glossary/templates/glossary-sitemap.pt	Fri Dec 28 10:45:11 2018 +0100
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+		tal:define="url tales:absolute_url(request.root)">
+	<tal:loop repeat="term context.terms.values()">
+		<url tal:condition="term.status == 'published'">
+			<loc>${url}/get-glossary-term-page.html?term=${term.label}</loc>
+			<lastmod>${tales:timestamp(term, 'iso')}</lastmod>
+		</url>
+	</tal:loop>
+</urlset>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/features/glossary/templates/glossary-term.pt	Fri Dec 28 10:45:11 2018 +0100
@@ -0,0 +1,31 @@
+<div class="modal-content" i18n:domain="pyams_default_theme"
+	 tal:define="term view.term">
+	<div class="modal-header">
+		<button type="button" class="close"
+				data-dismiss="modal" aria-hidden="true">
+			<i class="fa fa-fw fa-times-circle"></i>
+		</button>
+		<h3 class="modal-title">${view.title}</h3>
+	</div>
+	<div class="modal-body no-padding">
+		<div class="ams-form">
+			<div class="modal-viewport">
+				<fieldset>
+					<legend>
+						<i></i>
+						${term.alt or term.label}
+					</legend>
+					<div class="clearfix"></div>
+					<tal:loop repeat="renderer view.renderers">
+						${structure:renderer.render()}
+					</tal:loop>
+				</fieldset>
+			</div>
+			<footer tal:condition="view.is_dialog">
+				<button type="button" data-dismiss="modal"
+						class="btn close-widget closebutton-field"
+						value="Cancel" i18n:translate="">Close</button>
+			</footer>
+		</div>
+	</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_default_theme/features/glossary/templates/glossary.pt	Fri Dec 28 10:45:11 2018 +0100
@@ -0,0 +1,24 @@
+<div class="modal-content" i18n:domain="pyams_default_theme">
+	<div class="modal-header">
+		<h3 class="modal-title">${view.title}</h3>
+	</div>
+	<div class="modal-body no-padding">
+		<div class="ams-form">
+			<div class="modal-viewport">
+				<div class="clearfix spacer"></div>
+				<div class="col-md-12"
+					 tal:define="description view.thesaurus.description"
+					 tal:condition="description">
+					${structure:tales:html(description, 'markdown')}
+				</div>
+				<div class="clearfix spacer"></div>
+				<tal:loop repeat="term view.thesaurus.terms.values()">
+					<div class="col-md-3 col-sm-4 col-xs-6" tal:condition="term.status == 'published'">
+						<span class="thesaurus-term">${term.alt or term.label}</span>
+					</div>
+				</tal:loop>
+				<div class="clearfix spacer"></div>
+			</div>
+		</div>
+	</div>
+</div>
--- a/src/pyams_default_theme/features/thesaurus/templates/glossary.pt	Fri Dec 28 10:44:50 2018 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-<div class="modal-content" i18n:domain="pyams_default_theme"
-	 tal:define="term view.term">
-	<div class="modal-header">
-		<button type="button" class="close"
-				data-dismiss="modal" aria-hidden="true">
-			<i class="fa fa-fw fa-times-circle"></i>
-		</button>
-		<h3 class="modal-title"></h3>
-	</div>
-	<div class="modal-body no-padding">
-		<div class="modal-viewport">
-			<fieldset>
-				<legend>
-					<i></i>
-					${term.alt or term.label}
-				</legend>
-				<div class="clearfix"></div>
-				<tal:loop repeat="renderer view.renderers">
-					${structure:renderer.render()}
-				</tal:loop>
-			</fieldset>
-		</div>
-		<footer>
-			<button type="button" data-dismiss="modal"
-					class="btn close-widget closebutton-field"
-					value="Cancel" i18n:translate="">Close</button>
-		</footer>
-	</div>
-</div>