Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
-------------------
Expand Down
10 changes: 9 additions & 1 deletion src/collective/documentgenerator/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
class="collective.documentgenerator.browser.converter.DocumentConvertView"
permission="zope2.View"
/>

<browser:page
for="*"
name="persistent-document-conversion"
Expand Down Expand Up @@ -93,6 +93,14 @@
permission="zope2.View"
/>

<browser:page
for="*"
name="sub-templates-usage"
class=".views.SubTemplatesUsage"
template="sub_templates_usage.pt"
permission="zope2.View"
/>

<!-- configure actionspanel -->
<configure zcml:condition="installed imio.actionspanel" package="imio.actionspanel">

Expand Down
56 changes: 56 additions & 0 deletions src/collective/documentgenerator/browser/sub_templates_usage.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
lang="en"
metal:use-macro="context/main_template/macros/master"
i18n:domain="collective.documentgenerator">
<body>

<metal:main fill-slot="main">

<h1 class="documentFirstHeading" i18n:translate="sub_templates_usage_title">Sub-templates usages</h1>

<div id="content-core">
<table class="listing" tal:define="entries view/sub_templates_usage">
<thead>
<tr>
<th i18n:translate="sub_template_usage_sub_template">Sub-template</th>
<th i18n:translate="sub_template_usage_path">Path</th>
<th i18n:translate="sub_template_usage_templates">Using templates</th>
</tr>
</thead>
<tbody>
<tal:entry repeat="entry entries">
<tr tal:condition="not:entry/groups">
<td>
<a tal:attributes="href entry/sub_template/getURL"
tal:content="entry/sub_template/Title">Sub-template</a>
</td>
<td colspan="2"><em i18n:translate="sub_template_usage_not_used">not used</em></td>
</tr>
<tr tal:repeat="group entry/groups">
<td tal:condition="repeat/group/start"
tal:attributes="rowspan python:len(entry['groups'])">
<a tal:attributes="href entry/sub_template/getURL"
tal:content="entry/sub_template/Title">Sub-template</a>
</td>
<td tal:content="group/title_path">Folder / Sub folder</td>
<td>
<ul>
<li tal:repeat="template group/templates">
<a tal:attributes="href template/absolute_url"
tal:content="template/Title">Template title</a>
</li>
</ul>
</td>
</tr>
</tal:entry>
</tbody>
</table>
</div>

</metal:main>

</body>
</html>
3 changes: 3 additions & 0 deletions src/collective/documentgenerator/browser/templates_listing.pt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<metal:main fill-slot="content-core">
<metal:content-core define-macro="content-core">

<a target="_blank" tal:attributes="href string:${context/absolute_url}/@@sub-templates-usage"
i18n:translate="sub_templates_usage_link">Sub-templates usage</a>

<tal:block define="results view/table/values;" tal:condition="results">
<div id="dg-batch"><tal:batch replace="structure view/table/renderBatch" /></div>
<tal:listing replace="structure view/table/render" />
Expand Down
18 changes: 18 additions & 0 deletions src/collective/documentgenerator/browser/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
@@ -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"])
29 changes: 29 additions & 0 deletions src/collective/documentgenerator/tests/test_viewlets.py
Original file line number Diff line number Diff line change
@@ -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'])
52 changes: 52 additions & 0 deletions src/collective/documentgenerator/utils.py
Original file line number Diff line number Diff line change
@@ -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 _
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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'):
Expand Down
9 changes: 9 additions & 0 deletions src/collective/documentgenerator/viewlets/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@
permission="zope2.View"
/>

<browser:viewlet
for="collective.documentgenerator.content.pod_template.ISubTemplate"
name="sub-template-usage"
manager="plone.app.layout.viewlets.interfaces.IBelowContentBody"
class="collective.documentgenerator.viewlets.sub_template_usage.SubTemplateUsageViewlet"
template="sub_template_usage.pt"
permission="zope2.View"
/>

</configure>
32 changes: 32 additions & 0 deletions src/collective/documentgenerator/viewlets/sub_template_usage.pt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div id="sub-template-usage"
class="portalMessage info"
tal:condition="view/available"
i18n:domain="collective.documentgenerator">

<strong i18n:translate="sub_template_used_by">
Templates using this sub-template:
</strong>
<table class="listing sub-template-usage"
tal:define="grouped view/get_using_templates_by_path">
<thead>
<tr>
<th i18n:translate="sub_template_usage_path">Path</th>
<th i18n:translate="sub_template_usage_templates">Templates</th>
</tr>
</thead>
<tbody>
<tr tal:repeat="group grouped">
<td class="sub-template-usage-path" tal:content="group/title_path">Folder / Sub folder</td>
<td>
<ul>
<li tal:repeat="template group/templates">
<a tal:attributes="href template/absolute_url"
tal:content="template/Title">Template title</a>
</li>
</ul>
</td>
</tr>
</tbody>
</table>

</div>
27 changes: 27 additions & 0 deletions src/collective/documentgenerator/viewlets/sub_template_usage.py
Original file line number Diff line number Diff line change
@@ -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())
Loading