11
11
import operator
12
12
13
13
from django .conf import settings
14
+ from django .db .models import FilteredRelation , Q
14
15
from django .utils .translation import gettext as _
15
16
16
17
from inyoka .markup import macros , nodes
17
- from inyoka .markup .parsertools import MultiMap , flatten_iterator
18
18
from inyoka .markup .templates import expand_page_template
19
19
from inyoka .markup .utils import simple_filter
20
20
from inyoka .utils .imaging import parse_dimensions
21
21
from inyoka .utils .text import get_pagetitle , join_pagename , normalize_pagename
22
22
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
24
24
from inyoka .wiki .signals import build_picture_node
25
25
from inyoka .wiki .views import fetch_real_target
26
26
@@ -216,8 +216,22 @@ def build_node(self, context, format):
216
216
217
217
218
218
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
221
235
"""
222
236
223
237
names = ('FilterByMetaData' , 'MetaFilter' )
@@ -230,57 +244,70 @@ class FilterByMetaData(macros.Macro):
230
244
def __init__ (self , filters ):
231
245
self .filters = [x .strip () for x in filters .split (';' )]
232
246
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
+
233
256
def build_node (self , context , format ):
234
- mapping = []
257
+ pages = Page .objects .all ()
258
+
235
259
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 :
275
302
return nodes .error_box (_ ('No result' ),
276
303
_ ('The metadata filter has found no results. Query: %(query)s' ) % {
277
304
'query' : '; ' .join (self .filters )})
278
305
279
306
# build the node
280
307
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 )
284
311
result .children .append (nodes .ListItem ([link ]))
285
312
286
313
return result
0 commit comments