Skip to content

Commit a550aef

Browse files
authored
Merge pull request #34 from collective/statistics
Related Media Statistics with purge method
2 parents 779cc6a + 8a943ed commit a550aef

4 files changed

Lines changed: 168 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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
from zope.intid.interfaces import IIntIds
2323

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

2630

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