1
- import operator
2
1
import unicodedata
3
2
import uuid
4
- from functools import reduce
5
3
6
4
from django .conf import settings
7
5
from django .contrib .auth .models import AbstractBaseUser , UserManager
8
6
from django .contrib .auth .validators import UnicodeUsernameValidator
7
+ from django .contrib .postgres .aggregates import ArrayAgg
8
+ from django .contrib .postgres .fields import ArrayField
9
+ from django .contrib .postgres .indexes import GinIndex
10
+ from django .core .exceptions import ValidationError
9
11
from django .db import models
12
+ from django .db .models import Exists , OuterRef , Q , Subquery
10
13
from django .db .models .signals import pre_save
11
14
from django .dispatch import receiver
12
15
from django .utils import timezone , translation
13
16
from django .utils .translation import gettext_lazy as _
14
17
from localized_fields .fields import LocalizedCharField , LocalizedTextField
15
- from tree_queries .models import TreeNode , TreeQuerySet
16
18
17
19
18
20
def make_uuid ():
@@ -160,59 +162,34 @@ def is_authenticated(self):
160
162
return True
161
163
162
164
163
- class ScopeQuerySet (TreeQuerySet ):
164
- # django-tree-queries sadly does not (yet?) support ancestors query
165
- # for QS - only for single nodes. So we're providing all_descendants()
166
- # and all_ancestors() queryset methods.
167
-
165
+ class ScopeQuerySet (models .QuerySet ):
168
166
def all_descendants (self , include_self = False ):
169
- """Return a QS that contains all descendants of the given QS.
167
+ """Return a QS that contains all descendants."""
168
+ expr = Q (all_parents__overlap = self .aggregate (all_pks = ArrayAgg ("pk" ))["all_pks" ])
170
169
171
- This is a workaround for django-tree-queries, which currently does
172
- not support this query (it can only do it on single nodes).
170
+ if include_self :
171
+ expr = expr | Q ( pk__in = self )
173
172
174
- This is in contrast to .descendants(), which can only give the descendants
175
- of one model instance.
176
- """
177
- descendants_q = reduce (
178
- operator .or_ ,
179
- [
180
- models .Q (
181
- pk__in = entry .descendants (include_self = include_self ).values ("pk" )
182
- )
183
- for entry in self
184
- ],
185
- models .Q (),
186
- )
187
- return self .model .objects .filter (descendants_q )
173
+ return Scope .objects .filter (expr )
188
174
189
175
def all_ancestors (self , include_self = False ):
190
- """Return a QS that contains all ancestors of the given QS.
176
+ """Return a QS that contains all ancestors."""
191
177
192
- This is a workaround for django-tree-queries, which currently does
193
- not support this query (it can only do it on single nodes).
178
+ filter_qs = self .filter (all_parents__contains = [OuterRef ("pk" )])
194
179
195
- This is in contrast to .ancestors(), which can only give the ancestors
196
- of one model instance.
197
- """
180
+ new_qs = Scope .objects .all ().annotate (_is_ancestor = Exists (Subquery (filter_qs )))
181
+ expr = Q (_is_ancestor = True )
198
182
199
- descendants_q = reduce (
200
- operator .or_ ,
201
- [
202
- models .Q (pk__in = entry .ancestors (include_self = include_self ).values ("pk" ))
203
- for entry in self
204
- ],
205
- models .Q (),
206
- )
207
- return self .model .objects .filter (descendants_q )
183
+ if include_self :
184
+ expr = expr | Q (pk__in = self )
185
+
186
+ return new_qs .filter (expr )
208
187
209
188
def all_roots (self ):
210
- return Scope .objects .all ().filter (
211
- pk__in = [scope .ancestors (include_self = True ).first ().pk for scope in self ]
212
- )
189
+ return self .all_ancestors (include_self = True ).filter (parent__isnull = True )
213
190
214
191
215
- class Scope (TreeNode , UUIDModel ):
192
+ class Scope (UUIDModel ):
216
193
name = LocalizedCharField (_ ("scope name" ), blank = False , null = False , required = False )
217
194
218
195
full_name = LocalizedCharField (
@@ -224,26 +201,67 @@ class Scope(TreeNode, UUIDModel):
224
201
)
225
202
is_active = models .BooleanField (default = True )
226
203
227
- objects = ScopeQuerySet .as_manager (with_tree_fields = True )
204
+ objects = ScopeQuerySet .as_manager ()
205
+
206
+ parent = models .ForeignKey (
207
+ "Scope" ,
208
+ null = True ,
209
+ blank = True ,
210
+ on_delete = models .CASCADE ,
211
+ related_name = "children" ,
212
+ )
213
+
214
+ all_parents = ArrayField (models .UUIDField (null = False ), default = list )
215
+
216
+ def ancestors (self , include_self = False ):
217
+ expr = Q (pk__in = self .all_parents )
218
+ if include_self :
219
+ expr = expr | Q (pk = self .pk )
220
+ return Scope .objects .all ().filter (expr )
221
+
222
+ def descendants (self , include_self = False ):
223
+ expr = Q (all_parents__contains = [self .pk ])
224
+
225
+ if include_self :
226
+ expr = expr | Q (pk = self .pk )
227
+
228
+ return Scope .objects .all ().filter (expr )
228
229
229
230
def get_root (self ):
230
- return self .ancestors (include_self = True ).first ()
231
+ if self .parent_id :
232
+ return Scope .objects .get (pk = self .all_parents [0 ])
233
+ else :
234
+ return self
231
235
232
236
def save (self , * args , ** kwargs ):
233
- # django-tree-queries does validation in TreeNode.clean(), which is not
234
- # called by DRF (only by django forms), so we have to do this here
235
- self .clean ()
237
+ self ._ensure_no_loop ()
236
238
return super ().save (* args , ** kwargs )
237
239
240
+ def _ensure_no_loop (self ):
241
+ parent = self .parent
242
+ while parent :
243
+ if parent == self :
244
+ raise ValidationError (
245
+ "A node cannot be made a descendant or parent of itself"
246
+ )
247
+ parent = parent .parent
248
+
238
249
def __str__ (self ):
239
250
return f"{ type (self ).__name__ } ({ self .full_name } , pk={ self .pk } )"
240
251
241
252
class Meta :
242
253
ordering = ["name" ]
254
+ indexes = [GinIndex (fields = ["all_parents" ])]
243
255
244
256
245
257
@receiver (pre_save , sender = Scope )
246
- def set_full_name (instance , sender , ** kwargs ):
258
+ def set_full_name_and_parents (instance , sender , ** kwargs ):
259
+ """Update the `full_name` and `all_parents` properties of the Scope.
260
+
261
+ The full name depends on the complete list of parents of the Scope.
262
+ And to ensure correct behaviour in the queries, the `all_parents`
263
+ attribute needs to be updated as well
264
+ """
247
265
if kwargs .get ("raw" ): # pragma: no cover
248
266
# Raw is set while loading fixtures. In those
249
267
# cases we don't want to mess with data that
@@ -255,6 +273,9 @@ def set_full_name(instance, sender, **kwargs):
255
273
256
274
forced_lang = settings .EMEIS_FORCE_MODEL_LOCALE .get ("scope" , None )
257
275
276
+ old_all_parents = [* instance .all_parents ]
277
+ old_full_name = {** instance .full_name }
278
+
258
279
if forced_lang :
259
280
# If scope is forced monolingual, do not fill non-forced language fields
260
281
languages = [forced_lang ]
@@ -263,25 +284,34 @@ def set_full_name(instance, sender, **kwargs):
263
284
with translation .override (lang ):
264
285
instance .full_name [lang ] = str (instance .name )
265
286
287
+ parent_ids = []
266
288
parent = instance .parent
267
289
while parent :
290
+ parent_ids .append (parent .pk )
268
291
for lang in languages :
269
292
with translation .override (lang ):
270
293
new_fullname = f"{ parent .name } { sep } { instance .full_name [lang ]} "
271
294
instance .full_name [lang ] = new_fullname
272
295
parent = parent .parent
273
296
297
+ # make it root-first
298
+ parent_ids .reverse ()
299
+ instance .all_parents = parent_ids
300
+
274
301
if forced_lang :
275
302
# Ensure only the "forced" language full_name is set, and nothing else
276
303
full_name = instance .full_name [forced_lang ]
277
304
instance .full_name .clear ()
278
305
instance .full_name [forced_lang ] = full_name
279
306
280
- # Force update of all children (recursively)
281
- for child in instance .children .all ():
282
- # save() triggers the set_full_name signal handler, which will
283
- # recurse all the way down, updating the full_name
284
- child .save ()
307
+ if old_all_parents != instance .all_parents or old_full_name != dict (
308
+ instance .full_name
309
+ ):
310
+ # Something changed - force update all children (recursively)
311
+ for child in instance .children .all ():
312
+ # save() triggers the signal handler, which will
313
+ # recurse all the way down, updating the full_name
314
+ child .save ()
285
315
286
316
287
317
class Role (SlugModel ):
0 commit comments