diff --git a/inyoka/wiki/macros.py b/inyoka/wiki/macros.py index d937129cd..20afe396f 100644 --- a/inyoka/wiki/macros.py +++ b/inyoka/wiki/macros.py @@ -10,9 +10,12 @@ import itertools import operator +from django.db.models import Q from django.conf import settings from django.utils.translation import gettext as _ +from functools import reduce + from inyoka.markup import macros, nodes from inyoka.markup.parsertools import MultiMap, flatten_iterator from inyoka.markup.templates import expand_page_template @@ -216,8 +219,16 @@ def build_node(self, context, format): class FilterByMetaData(macros.Macro): - """ - Filter pages by their metadata + """Filter pages by their metadata. + + The metadata given can be combined with keywords to customize + the filtering. The ``NOT`` keyword negates the filter. The ``EXACT`` + keyword uses direct matching and does not accept any other value + for the metadata key. Examples: + + X-Link: mardi; tag: gras + X-Link: mardi, gras + tag: EXACT mardi; X-Link: NOT gras """ names = ('FilterByMetaData', 'MetaFilter') @@ -233,42 +244,40 @@ def __init__(self, filters): def build_node(self, context, format): mapping = [] for part in self.filters: - # TODO: Can we do something else instead of skipping? - if ':' not in part: - continue - key = part.split(':')[0].strip() - values = [x.strip() for x in part.split(':')[1].split(',')] - mapping.extend([(key, x) for x in values]) + if part.count(":") != 1: + return nodes.error_box(_('No result'), + _('Invalid filter syntax. Query: %(query)s') % { + 'query': '; '.join(self.filters)}) + key, values = part.split(":") + values = values.split(",") + mapping.extend([(key.strip(), x.strip()) for x in values]) mapping = MultiMap(mapping) - pages = set() - + pages_query = Q() + exclude_query = Q() for key in list(mapping.keys()): values = list(flatten_iterator(mapping[key])) - includes = [x for x in values if not x.startswith('NOT ')] - kwargs = {'key': key, 'value__in': includes} - q = MetaData.objects.select_related('page').filter(**kwargs) - res = { - x.page - for x in q - if not is_privileged_wiki_page(x.page.name) - } - pages = pages.union(res) - - # filter the pages with `AND` - res = set() - for key in list(mapping.keys()): - for page in pages: - e = [x[4:] for x in mapping[key] if x.startswith('NOT ')] - i = [x for x in mapping[key] if not x.startswith('NOT ')] - exclude = False - for val in set(page.metadata[key]): - if val in e: - exclude = True - if not exclude and set(page.metadata[key]) == set(i): - res.add(page) - - names = [p.name for p in res] + includes = {x for x in values if not x.startswith(('NOT ', 'EXACT',))} + exclude_values = set() + kwargs = {'key': key} + if values[0].startswith("EXACT"): + exact_value = values[0].removeprefix("EXACT ") + kwargs['value__exact'] = exact_value + elif values[0].startswith("NOT"): + exclude_values.add(values[0].removeprefix("NOT ")) + else: + kwargs['value__in'] = includes + pages_query |= Q(**kwargs) + exclude_query |= Q(**{"key": key, "value__in": exclude_values}) + + exclude_privileged_query = Q() + for prefix in settings.WIKI_PRIVILEGED_PAGES: + exclude_privileged_query |= Q(**{"page__name__startswith": prefix}) + + pages = MetaData.objects.select_related('page').filter(pages_query).exclude( + exclude_query).exclude(exclude_privileged_query) + + names = {p.page.name for p in pages} names = sorted(names, key=lambda s: s.lower()) if not names: