diff --git a/CHANGES.rst b/CHANGES.rst index a6a48184..46b8ad48 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,8 +4,10 @@ Changelog 3.47.2 (unreleased) ------------------- -- Nothing changed yet. - +- Added viewlet on SubTemplate to show its use in merge_templates field. + [sgeulette] +- Added `sub-templates-usage` view listing every sub-template usage. + [sgeulette] 3.47.1 (2026-03-26) ------------------- diff --git a/src/collective/documentgenerator/browser/configure.zcml b/src/collective/documentgenerator/browser/configure.zcml index a811769f..b4356cde 100644 --- a/src/collective/documentgenerator/browser/configure.zcml +++ b/src/collective/documentgenerator/browser/configure.zcml @@ -45,7 +45,7 @@ class="collective.documentgenerator.browser.converter.DocumentConvertView" permission="zope2.View" /> - + + + diff --git a/src/collective/documentgenerator/browser/sub_templates_usage.pt b/src/collective/documentgenerator/browser/sub_templates_usage.pt new file mode 100644 index 00000000..eafd48df --- /dev/null +++ b/src/collective/documentgenerator/browser/sub_templates_usage.pt @@ -0,0 +1,56 @@ + + + + + +

Sub-templates usages

+ +
+ + + + + + + + + + + + + + + + + + + + + +
Sub-templatePathUsing templates
+ Sub-template + not used
+ Sub-template + Folder / Sub folder + +
+
+ +
+ + + diff --git a/src/collective/documentgenerator/browser/templates_listing.pt b/src/collective/documentgenerator/browser/templates_listing.pt index ce3e5886..73b0e1e9 100644 --- a/src/collective/documentgenerator/browser/templates_listing.pt +++ b/src/collective/documentgenerator/browser/templates_listing.pt @@ -10,6 +10,9 @@ + Sub-templates usage +
diff --git a/src/collective/documentgenerator/browser/views.py b/src/collective/documentgenerator/browser/views.py index 44870cbe..5aeb3e08 100644 --- a/src/collective/documentgenerator/browser/views.py +++ b/src/collective/documentgenerator/browser/views.py @@ -7,6 +7,8 @@ from collective.documentgenerator.content.pod_template import MailingLoopTemplate from collective.documentgenerator.content.pod_template import SubTemplate from collective.documentgenerator.content.style_template import IStyleTemplate +from collective.documentgenerator.utils import get_pod_templates_using +from collective.documentgenerator.utils import group_templates_by_path from collective.documentgenerator.utils import translate as _ from OFS.interfaces import IOrderedContainer from plone import api @@ -94,6 +96,22 @@ def __call__(self, local_search=None, search_depth=None): return self.index() +class SubTemplatesUsage(BrowserView): + """Overview listing, for each sub-template, the POD templates that use + it in their 'merge_templates' field, grouped by the path they live in.""" + + def sub_templates_usage(self): + """Return a list of {'sub_template': brain, 'groups': [...]} entries, one + per sub-template (including unused ones), ordered by title. 'groups' is + the per-path grouping produced for the dedicated viewlet.""" + catalog = api.portal.get_tool("portal_catalog") + result = [] + for brain in catalog(portal_type="SubTemplate", sort_on="sortable_title"): + templates = get_pod_templates_using(brain.getObject()) + result.append({"sub_template": brain, "groups": group_templates_by_path(templates)}) + return result + + class DisplayChildrenPodTemplateProvider(ContentProviderBase): template = ViewPageTemplateFile("children_pod_template.pt") diff --git a/src/collective/documentgenerator/tests/test_sub_templates_usage_view.py b/src/collective/documentgenerator/tests/test_sub_templates_usage_view.py new file mode 100644 index 00000000..746f09f3 --- /dev/null +++ b/src/collective/documentgenerator/tests/test_sub_templates_usage_view.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from collective.documentgenerator.testing import POD_TEMPLATE_INTEGRATION + +import unittest + + +class TestSubTemplatesUsageView(unittest.TestCase): + """Test the 'sub-templates-usage' view. + + The demo profile creates a single 'sub_template' used by + 'test_template_multiple' and 'test_template_bis', both in 'podtemplates'. + """ + + layer = POD_TEMPLATE_INTEGRATION + + def setUp(self): + self.view = self.layer["portal"].restrictedTraverse("@@sub-templates-usage") + + def test_sub_templates_usage(self): + entries = self.view.sub_templates_usage() + # one entry per sub-template (here the single demo 'sub_template') + self.assertEqual([e["sub_template"].getId for e in entries], ["sub_template"]) + # both using templates share the same folder -> one group, sorted by title + groups = entries[0]["groups"] + self.assertEqual(len(groups), 1) + self.assertEqual([t.getId() for t in groups[0]["templates"]], ["test_template_bis", "test_template_multiple"]) diff --git a/src/collective/documentgenerator/tests/test_viewlets.py b/src/collective/documentgenerator/tests/test_viewlets.py new file mode 100644 index 00000000..f55f40b1 --- /dev/null +++ b/src/collective/documentgenerator/tests/test_viewlets.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +from collective.documentgenerator.testing import POD_TEMPLATE_INTEGRATION +from collective.documentgenerator.viewlets.sub_template_usage import SubTemplateUsageViewlet + +import unittest + + +class TestSubTemplateUsageViewlet(unittest.TestCase): + """Test the 'sub-template-usage' viewlet on a SubTemplate. + + The demo profile creates a 'sub_template' used by 'test_template_multiple' + and 'test_template_bis', both in the 'podtemplates' folder. + """ + + layer = POD_TEMPLATE_INTEGRATION + + def setUp(self): + portal = self.layer['portal'] + sub_template = portal.podtemplates.sub_template + self.viewlet = SubTemplateUsageViewlet(sub_template, portal.REQUEST, None, None) + + def test_available_and_using_templates_grouped_by_path(self): + self.assertTrue(self.viewlet.available()) + groups = self.viewlet.get_using_templates_by_path() + # both templates live in the same folder -> a single group, sorted by title + self.assertEqual(len(groups), 1) + self.assertEqual([t.getId() for t in groups[0]['templates']], + ['test_template_bis', 'test_template_multiple']) diff --git a/src/collective/documentgenerator/utils.py b/src/collective/documentgenerator/utils.py index 578acd2f..41fd55e9 100644 --- a/src/collective/documentgenerator/utils.py +++ b/src/collective/documentgenerator/utils.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from Acquisition import aq_inner +from Acquisition import aq_parent from appy.bin.odfclean import Cleaner from appy.pod.renderer import Renderer from collective.documentgenerator import _ @@ -184,6 +186,55 @@ def get_site_root_relative_path(obj): ) +def title_path(obj): + """Return a breadcrumb-like path made of the Title of each level between the + site root (excluded) and ``obj`` (included).""" + portal_path = '/'.join(api.portal.get().getPhysicalPath()) + titles = [] + current = aq_inner(obj) + while current is not None: + current_path = '/'.join(current.getPhysicalPath()) + if current_path == portal_path or not current_path.startswith(portal_path): + break + titles.append(safe_unicode(current.Title())) + current = aq_parent(aq_inner(current)) + return u' / '.join(reversed(titles)) + + +def get_pod_templates_using(sub_template): + """Return the list of templates referencing ``sub_template`` in their + 'merge_templates' field.""" + from collective.documentgenerator.content.pod_template import IConfigurablePODTemplate + catalog = getToolByName(sub_template, 'portal_catalog') + uid = sub_template.UID() + templates = [] + for brain in catalog(object_provides=IConfigurablePODTemplate.__identifier__): + template = brain.getObject() + for line in getattr(template, 'merge_templates', None) or []: + if line.get('template') == uid: + templates.append(template) + break + return templates + + +def group_templates_by_path(templates): + """Group ``templates`` by their container, returning a list of + {'path', 'title_path', 'templates'} dicts sorted by path then by title.""" + grouped = {} + for template in templates: + parent = aq_parent(aq_inner(template)) + path = get_site_root_relative_path(parent) + grouped.setdefault(path, {'title_path': title_path(parent), 'templates': []}) + grouped[path]['templates'].append(template) + result = [] + for path in sorted(grouped): + group = grouped[path] + group['templates'].sort(key=lambda t: safe_unicode(t.Title()).lower()) + result.append({'path': path, 'title_path': group['title_path'], + 'templates': group['templates']}) + return result + + def temporary_file_name(suffix=''): tmp_dir = os.getenv('CUSTOM_TMP', None) if tmp_dir and not os.path.exists(tmp_dir): @@ -286,6 +337,7 @@ def convert_file(afile, fmt="pdf", renderer=False, gen_context=None, delete_temp :param renderer: whether to use appy.pod Renderer or converter script. Default to False. :param gen_context: generation context dict passed to renderer :param delete_temp_files: + :return: converted file content """ if renderer: if not afile.filename.endswith('.odt'): diff --git a/src/collective/documentgenerator/viewlets/configure.zcml b/src/collective/documentgenerator/viewlets/configure.zcml index 095d07ff..00e54ef0 100644 --- a/src/collective/documentgenerator/viewlets/configure.zcml +++ b/src/collective/documentgenerator/viewlets/configure.zcml @@ -13,4 +13,13 @@ permission="zope2.View" /> + +
diff --git a/src/collective/documentgenerator/viewlets/sub_template_usage.pt b/src/collective/documentgenerator/viewlets/sub_template_usage.pt new file mode 100644 index 00000000..116e53c8 --- /dev/null +++ b/src/collective/documentgenerator/viewlets/sub_template_usage.pt @@ -0,0 +1,32 @@ +
+ + + Templates using this sub-template: + + + + + + + + + + + + + + +
PathTemplates
Folder / Sub folder + +
+ +
diff --git a/src/collective/documentgenerator/viewlets/sub_template_usage.py b/src/collective/documentgenerator/viewlets/sub_template_usage.py new file mode 100644 index 00000000..e67e841b --- /dev/null +++ b/src/collective/documentgenerator/viewlets/sub_template_usage.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from collective.documentgenerator.content.pod_template import ISubTemplate +from collective.documentgenerator.utils import get_pod_templates_using +from collective.documentgenerator.utils import group_templates_by_path +from plone.app.layout.viewlets import ViewletBase +from plone.memoize.view import memoize + + +class SubTemplateUsageViewlet(ViewletBase): + """Display the POD templates that use the current sub-template in their + 'merge_templates' field, grouped by the folder path they live in.""" + + def available(self): + return ISubTemplate.providedBy(self.context) and bool(self.get_using_templates()) + + @memoize + def get_using_templates(self): + """Return the list of templates referencing the current sub-template + in their 'merge_templates' field.""" + return get_pod_templates_using(self.context) + + def get_using_templates_by_path(self): + """Return a list of {'path', 'title_path', 'templates'} groups of the + templates referencing the current sub-template, grouped by their + container and sorted by path then by title.""" + return group_templates_by_path(self.get_using_templates())