Skip to content

Commit 9217321

Browse files
committed
RM statistics with purge method
1 parent 779cc6a commit 9217321

4 files changed

Lines changed: 167 additions & 10 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,8 @@ Changelog
55
## 3.8.1 (unreleased)
66

77

8-
Breaking changes:
9-
10-
- *add item here*
11-
12-
New features:
13-
14-
- *add item here*
15-
16-
Bug fixes:
8+
- Related Media Statistics view including a purge method to cleanup unused media. @petschki
179

18-
- *add item here*
1910

2011

2112
## 3.8.0 (2025-12-01)

src/collective/behavior/relatedmedia/browser.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
from zope.intid.interfaces import IIntIds
2323

2424
import json
25+
import logging
26+
27+
logger = logging.getLogger(__name__)
2528

2629

2730
class RelatedBaseView(BrowserView):
@@ -257,3 +260,89 @@ def __call__(self):
257260
status="done",
258261
)
259262
)
263+
264+
265+
class StatisticsView(BrowserView):
266+
def __call__(self):
267+
self.stats = {}
268+
269+
if self.request.get("REQUEST_METHOD", "GET") == "POST" and self.request.get(
270+
"form.submitted"
271+
):
272+
media_root = get_media_root(self.context)
273+
self.media_root_path = media_root.absolute_url_path()
274+
items = self.context.portal_catalog.unrestrictedSearchResults(
275+
path=self.media_root_path, portal_type=["Image", "File"]
276+
)
277+
_all = len(items)
278+
filter_keyword = self.request.get("filter", "").lower()
279+
unrelated = []
280+
relation_stats = {}
281+
purge = self.request.get("purge_unrelated", False)
282+
283+
for idx, it in enumerate(items, 1):
284+
obj = it.getObject()
285+
match = True
286+
287+
logger.info(f"Processing {idx}/{_all}: {obj.absolute_url()}")
288+
289+
if filter_keyword:
290+
match = (
291+
filter_keyword in obj.title_or_id().lower()
292+
or filter_keyword in obj.absolute_url()
293+
)
294+
295+
relations = api.relation.get(target=obj, unrestricted=True)
296+
297+
if not relations and match:
298+
299+
if purge:
300+
media_root.manage_delObjects([obj.getId()])
301+
logger.warning(f"Purged unrelated media: {obj.absolute_url()}")
302+
continue
303+
304+
unrelated.append(
305+
dict(
306+
source=dict(
307+
title=obj.title_or_id(), url=obj.absolute_url_path()
308+
),
309+
target=dict(title=None, url=None),
310+
)
311+
)
312+
313+
for relation in relations:
314+
315+
if relation.from_attribute not in relation_stats:
316+
relation_stats[relation.from_attribute] = []
317+
318+
source_obj = relation.from_object
319+
320+
if filter_keyword and not match:
321+
# check if source or target matches the filter keyword
322+
match = (
323+
filter_keyword in source_obj.title_or_id().lower()
324+
or filter_keyword in source_obj.absolute_url()
325+
)
326+
327+
if match:
328+
relation_stats[relation.from_attribute].append(
329+
dict(
330+
source=dict(
331+
title=obj.title_or_id(),
332+
url=obj.absolute_url_path(),
333+
),
334+
target=dict(
335+
title=source_obj.title_or_id(),
336+
url=source_obj.absolute_url_path(),
337+
),
338+
)
339+
)
340+
341+
relation_stats["unrelated"] = unrelated
342+
343+
self.stats = {
344+
"relations": relation_stats,
345+
"total": _all,
346+
}
347+
348+
return self.index()

src/collective/behavior/relatedmedia/configure.zcml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@
186186
name="related_images_filter"
187187
/>
188188

189+
<browser:page
190+
name="relatedmedia-statistics"
191+
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
192+
class=".browser.StatisticsView"
193+
template="statistics.pt"
194+
permission="cmf.ManagePortal"
195+
layer=".interfaces.ICollectiveBehaviorRelatedmediaLayer"
196+
/>
197+
189198
<include file="upgrades.zcml" />
190199

191200
</configure>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:i18n="http://xml.zope.org/namespaces/i18n"
2+
xmlns:metal="http://xml.zope.org/namespaces/metal" xmlns:tal="http://xml.zope.org/namespaces/tal"
3+
metal:use-macro="context/@@main_template/macros/master" lang="en" xml:lang="en"
4+
i18n:domain="collective.behavior.relatedmedia">
5+
6+
<body>
7+
<metal:main fill-slot="main" tal:define="stats view/stats">
8+
<h1>RelatedMedia Statistics</h1>
9+
10+
<form action="./@@relatedmedia-statistics" method="post">
11+
<div class="mb-3">
12+
<label>Filter keyword (Source/Target Title/Description)</label>
13+
<input type="text" name="filter" class="form-control" value="${request/filter|string:}" />
14+
</div>
15+
<div>
16+
<button class="btn btn-secondary" type="submit">Load Statistics</button>
17+
</div>
18+
<input type="hidden" name="form.submitted" value="1" />
19+
</form>
20+
21+
<tal:if condition="stats">
22+
23+
<p>Total number of media in ${view/media_root_path}: ${stats/total}</p>
24+
25+
<div class="accordion" id="accordionStats">
26+
<div class="accordion-item" tal:repeat="attr stats/relations">
27+
<h2 class="accordion-header">
28+
<button class="accordion-button" type="button" data-bs-toggle="collapse"
29+
data-bs-target="#collapse${attr}" aria-expanded="true" aria-controls="collapse${attr}">
30+
${attr} (${python:len(stats['relations'][attr])})
31+
</button>
32+
</h2>
33+
<div id="collapse${attr}" class="accordion-collapse collapse" data-bs-parent="#accordionStats">
34+
<div class="accordion-body">
35+
<table class="w-100 table table-condensed">
36+
<thead>
37+
<tr>
38+
<th class="w-50">Source</th>
39+
<th>Target</th>
40+
</tr>
41+
</thead>
42+
<tbody>
43+
<tr tal:repeat="rel python:stats['relations'][attr]">
44+
<td>${rel/source/title}<br /><a href="${rel/source/url}" target="_blank">
45+
<small>${rel/source/url}</small></a></td>
46+
<td>${rel/target/title}<br /><a href="${rel/target/url}" target="_blank">
47+
<small>${rel/target/url}</small></a></td>
48+
</tr>
49+
</tbody>
50+
</table>
51+
</div>
52+
</div>
53+
</div>
54+
</div>
55+
56+
<div class="mt-4">
57+
<form action="./@@relatedmedia-statistics" method="post">
58+
<input type="hidden" name="purge_unrelated" value="1" />
59+
<input type="hidden" name="form.submitted" value="1" />
60+
<button class="btn btn-danger" type="submit" onclick="javascript:return(confirm('Are you sure?'));">Purge unused/unrelated Media</button>
61+
</form>
62+
</div>
63+
</tal:if>
64+
65+
</metal:main>
66+
</body>
67+
68+
</html>

0 commit comments

Comments
 (0)