Skip to content

Commit a5cf400

Browse files
committed
RM statistics with purge method
1 parent 620688e commit a5cf400

4 files changed

Lines changed: 168 additions & 1 deletion

File tree

CHANGES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Changelog
55
## 3.6.8 (unreleased)
66

77

8-
- Nothing changed yet.
8+
- Related Media Statistics view including a purge method to cleanup unused media.
9+
[petschki]
910

1011

1112
## 3.6.7 (2025-03-11)

collective/behavior/relatedmedia/browser.py

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

2626
import json
27+
import logging
28+
29+
logger = logging.getLogger(__name__)
2730

2831

2932
class RelatedBaseView(BrowserView):
@@ -276,3 +279,89 @@ def render(self):
276279
if not IViewView.providedBy(self.view):
277280
return ""
278281
return super().render()
282+
283+
284+
class StatisticsView(BrowserView):
285+
def __call__(self):
286+
self.stats = {}
287+
288+
if self.request.get("REQUEST_METHOD", "GET") == "POST" and self.request.get(
289+
"form.submitted"
290+
):
291+
media_root = get_media_root(self.context)
292+
self.media_root_path = media_root.absolute_url_path()
293+
items = self.context.portal_catalog.unrestrictedSearchResults(
294+
path=self.media_root_path, portal_type=["Image", "File"]
295+
)
296+
_all = len(items)
297+
filter_keyword = self.request.get("filter", "").lower()
298+
unrelated = []
299+
relation_stats = {}
300+
purge = self.request.get("purge_unrelated", False)
301+
302+
for idx, it in enumerate(items, 1):
303+
obj = it.getObject()
304+
match = True
305+
306+
logger.info(f"Processing {idx}/{_all}: {obj.absolute_url()}")
307+
308+
if filter_keyword:
309+
match = (
310+
filter_keyword in obj.title_or_id().lower()
311+
or filter_keyword in obj.absolute_url()
312+
)
313+
314+
relations = api.relation.get(target=obj, unrestricted=True)
315+
316+
if not relations and match:
317+
318+
if purge:
319+
media_root.manage_delObjects([obj.getId()])
320+
logger.warning(f"Purged unrelated media: {obj.absolute_url()}")
321+
continue
322+
323+
unrelated.append(
324+
dict(
325+
source=dict(
326+
title=obj.title_or_id(), url=obj.absolute_url_path()
327+
),
328+
target=dict(title=None, url=None),
329+
)
330+
)
331+
332+
for relation in relations:
333+
334+
if relation.from_attribute not in relation_stats:
335+
relation_stats[relation.from_attribute] = []
336+
337+
source_obj = relation.from_object
338+
339+
if filter_keyword and not match:
340+
# check if source or target matches the filter keyword
341+
match = (
342+
filter_keyword in source_obj.title_or_id().lower()
343+
or filter_keyword in source_obj.absolute_url()
344+
)
345+
346+
if match:
347+
relation_stats[relation.from_attribute].append(
348+
dict(
349+
source=dict(
350+
title=obj.title_or_id(),
351+
url=obj.absolute_url_path(),
352+
),
353+
target=dict(
354+
title=source_obj.title_or_id(),
355+
url=source_obj.absolute_url_path(),
356+
),
357+
)
358+
)
359+
360+
relation_stats["unrelated"] = unrelated
361+
362+
self.stats = {
363+
"relations": relation_stats,
364+
"total": _all,
365+
}
366+
367+
return self.index()

collective/behavior/relatedmedia/configure.zcml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@
197197
name="related_images_filter"
198198
/>
199199

200+
<browser:page
201+
name="relatedmedia-statistics"
202+
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
203+
class=".browser.StatisticsView"
204+
template="statistics.pt"
205+
permission="cmf.ManagePortal"
206+
layer=".interfaces.ICollectiveBehaviorRelatedmediaLayer"
207+
/>
208+
200209
<include file="upgrades.zcml" />
201210

202211
</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)