Skip to content

Commit c031e2d

Browse files
authored
Merge pull request #1402 from chris34/refactor-metafilter-macro
Refactor metafilter macro
2 parents 7d382cc + d9fa1d6 commit c031e2d

File tree

4 files changed

+504
-47
lines changed

4 files changed

+504
-47
lines changed

ChangeLog.rst

+25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ Inyoka Changelog
2424
🔒 Security
2525
-----------
2626

27+
Unreleased 1.42.3 (2025-MM-DD)
28+
==============================
29+
30+
✨ New features
31+
---------------
32+
33+
🏗 Changes
34+
----------
35+
36+
* Reworked ``MetaFilter`` in wiki to yield more accurate results
37+
38+
🗑 Deprecations
39+
--------------
40+
41+
🔥 Removals
42+
-----------
43+
44+
🐛 Fixes
45+
--------
46+
47+
* Planet, sync task: Skip blog on ``IncompleteRead``
48+
49+
🔒 Security
50+
-----------
51+
2752

2853
1.42.2 (2025-03-07)
2954
===================

inyoka/wiki/macros.py

+74-47
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111
import operator
1212

1313
from django.conf import settings
14+
from django.db.models import FilteredRelation, Q
1415
from django.utils.translation import gettext as _
1516

1617
from inyoka.markup import macros, nodes
17-
from inyoka.markup.parsertools import MultiMap, flatten_iterator
1818
from inyoka.markup.templates import expand_page_template
1919
from inyoka.markup.utils import simple_filter
2020
from inyoka.utils.imaging import parse_dimensions
2121
from inyoka.utils.text import get_pagetitle, join_pagename, normalize_pagename
2222
from inyoka.utils.urls import href, is_safe_domain, urlencode
23-
from inyoka.wiki.models import MetaData, Page, is_privileged_wiki_page
23+
from inyoka.wiki.models import Page, is_privileged_wiki_page
2424
from inyoka.wiki.signals import build_picture_node
2525
from inyoka.wiki.views import fetch_real_target
2626

@@ -216,8 +216,22 @@ def build_node(self, context, format):
216216

217217

218218
class FilterByMetaData(macros.Macro):
219-
"""
220-
Filter pages by their metadata
219+
"""Filter pages by their metadata.
220+
221+
One filter has the form `key: value`.
222+
Multiple filters can be combined with a `;` in the sense of a `and`.
223+
Value can contain multiple values seperated by `,` in the sense of `or`.
224+
225+
``NOT`` at the start of a value negates the filter.
226+
It can be used (multiple times) only at the start of a filter.
227+
Additionally, only one value can be used with ``NOT``.
228+
229+
Examples:
230+
231+
`[[FilterByMetaData("tag: baz")]]` lists pages that have the tag baz
232+
`[[FilterByMetaData("X-Link: foo,bar")]]` lists pages that have a link to `foo` _or_ `bar`
233+
`[[FilterByMetaData("X-Link: foo; X-Link: bar")]]` lists pages that have a link to `foo` _and_ `bar`
234+
`[[FilterByMetaData("X-Link: NOT gras")]]` lists pages that not contain a link to gras
221235
"""
222236

223237
names = ('FilterByMetaData', 'MetaFilter')
@@ -230,57 +244,70 @@ class FilterByMetaData(macros.Macro):
230244
def __init__(self, filters):
231245
self.filters = [x.strip() for x in filters.split(';')]
232246

247+
def _is_allowed_key(self, key: str) -> bool:
248+
"""
249+
Returns True, if this is an allowed metadata key.
250+
251+
Via this method the queryable metadata keys can be restricted.
252+
Disallowed is for example the key `X-Behave`.
253+
"""
254+
return key in {'tags', 'tag', 'X-Redirect', 'X-Link', 'X-Attach', 'getestet'}
255+
233256
def build_node(self, context, format):
234-
mapping = []
257+
pages = Page.objects.all()
258+
235259
for part in self.filters:
236-
# TODO: Can we do something else instead of skipping?
237-
if ':' not in part:
238-
continue
239-
key = part.split(':')[0].strip()
240-
values = [x.strip() for x in part.split(':')[1].split(',')]
241-
mapping.extend([(key, x) for x in values])
242-
mapping = MultiMap(mapping)
243-
244-
pages = set()
245-
246-
for key in list(mapping.keys()):
247-
values = list(flatten_iterator(mapping[key]))
248-
includes = [x for x in values if not x.startswith('NOT ')]
249-
kwargs = {'key': key, 'value__in': includes}
250-
q = MetaData.objects.select_related('page').filter(**kwargs)
251-
res = {
252-
x.page
253-
for x in q
254-
if not is_privileged_wiki_page(x.page.name)
255-
}
256-
pages = pages.union(res)
257-
258-
# filter the pages with `AND`
259-
res = set()
260-
for key in list(mapping.keys()):
261-
for page in pages:
262-
e = [x[4:] for x in mapping[key] if x.startswith('NOT ')]
263-
i = [x for x in mapping[key] if not x.startswith('NOT ')]
264-
exclude = False
265-
for val in set(page.metadata[key]):
266-
if val in e:
267-
exclude = True
268-
if not exclude and set(page.metadata[key]) == set(i):
269-
res.add(page)
270-
271-
names = [p.name for p in res]
272-
names = sorted(names, key=lambda s: s.lower())
273-
274-
if not names:
260+
if part.count(":") != 1:
261+
return nodes.error_box(_('No result'),
262+
_('Invalid filter syntax. Query: %(query)s') % {
263+
'query': '; '.join(self.filters)})
264+
265+
key, values = part.split(":")
266+
267+
values = [v.strip() for v in values.split(",")]
268+
if values[0].startswith("NOT") and len(values) > 1:
269+
return nodes.error_box(_('No result'),
270+
_('Invalid filter syntax. Query: %(query)s') % {
271+
'query': '; '.join(self.filters)})
272+
273+
key = key.strip()
274+
if not self._is_allowed_key(key):
275+
return nodes.error_box(_('No result'),
276+
_('Invalid filter key %(key)s. Query: %(query)s') % {
277+
'key': key,
278+
'query': '; '.join(self.filters)})
279+
280+
if values[0].startswith("NOT"):
281+
exclude_value = values[0].removeprefix("NOT ")
282+
283+
normalized_key = key.lower().replace("-", "")
284+
pages = pages.annotate(**{
285+
f'filtered_{normalized_key}': FilteredRelation(
286+
'metadata',
287+
condition=Q(metadata__key=key),
288+
)}
289+
).exclude(**{f'filtered_{normalized_key}__value': exclude_value})
290+
else:
291+
includes = set(values)
292+
pages = pages.filter(metadata__key=key, metadata__value__in=includes)
293+
294+
# exclude privileged pages
295+
for prefix in settings.WIKI_PRIVILEGED_PAGES:
296+
pages = pages.exclude(name__startswith=prefix)
297+
298+
pages = pages.distinct()
299+
page_names = pages.values_list('name', flat=True)
300+
301+
if not page_names:
275302
return nodes.error_box(_('No result'),
276303
_('The metadata filter has found no results. Query: %(query)s') % {
277304
'query': '; '.join(self.filters)})
278305

279306
# build the node
280307
result = nodes.List('unordered')
281-
for page in names:
282-
title = [nodes.Text(get_pagetitle(page))]
283-
link = nodes.InternalLink(page, title, force_existing=True)
308+
for p in page_names:
309+
title = [nodes.Text(get_pagetitle(p))]
310+
link = nodes.InternalLink(p, title, force_existing=True)
284311
result.children.append(nodes.ListItem([link]))
285312

286313
return result

0 commit comments

Comments
 (0)