1
1
"""
2
- A class to model hierarchies of objects following Directed Acyclic Graph structure.
2
+ A set of model classes to model hierarchies of objects following Directed Acyclic Graph structure.
3
3
4
- The graph traversal queries use Postgresql's recursive CTEs to fetch an entire tree
5
- of related node ids in a single query. These queries also topologically sort the ids
6
- by generation.
4
+ The graph traversal queries use Postgresql's recursive CTEs to fetch an entire tree of related node ids in a single
5
+ query. These queries also topologically sort the ids by generation.
7
6
"""
8
7
9
8
from django .apps import apps
18
17
19
18
class NodeManager (models .Manager ):
20
19
def roots (self , node = None ):
21
- """Returns a Queryset of all root Nodes, or optionally, the roots of a select node"""
20
+ """
21
+ Returns a Queryset of all root nodes (nodes with no parents) in the Node model. If a node instance is specified,
22
+ returns only the roots for that node.
23
+ """
22
24
if node is not None :
23
25
return node .roots ()
24
26
return self .filter (parents__isnull = True )
25
27
26
28
def leaves (self , node = None ):
27
- """Returns a Queryset of all leaf Nodes, or optionally, the leaves of a select node"""
29
+ """
30
+ Returns a Queryset of all leaf nodes (nodes with no children) in the Node model. If a node instance is
31
+ specified, returns only the leaves for that node.
32
+ """
28
33
if node is not None :
29
34
return node .leaves ()
30
35
return self .filter (children__isnull = True )
@@ -72,6 +77,7 @@ def ordered_queryset_from_pks(self, pks):
72
77
return _ordered_filter (self .__class__ .objects , "pk" , pks )
73
78
74
79
def add_child (self , child , ** kwargs ):
80
+ """Provided with a Node instance, attaches that instance as a child to the current Node instance"""
75
81
kwargs .update ({"parent" : self , "child" : child })
76
82
77
83
disable_circular_check = kwargs .pop ("disable_circular_check" , False )
@@ -81,7 +87,10 @@ def add_child(self, child, **kwargs):
81
87
return cls .save (disable_circular_check = disable_circular_check , allow_duplicate_edges = allow_duplicate_edges )
82
88
83
89
def remove_child (self , child , delete_node = False ):
84
- """Removes the edge connecting this node to child, and optionally deletes the child node as well"""
90
+ """
91
+ Removes the edge connecting this node to the provided child Node instance, and optionally deletes the child
92
+ node as well
93
+ """
85
94
if child in self .children .all ():
86
95
self .children .through .objects .filter (parent = self , child = child ).delete ()
87
96
if delete_node :
@@ -92,6 +101,7 @@ def remove_child(self, child, delete_node=False):
92
101
child .delete ()
93
102
94
103
def add_parent (self , parent , * args , ** kwargs ):
104
+ """Provided with a Node instance, attaches the current instance as a child to the provided Node instance"""
95
105
return parent .add_child (self , ** kwargs )
96
106
97
107
def remove_parent (self , parent , delete_node = False ):
@@ -106,44 +116,54 @@ def remove_parent(self, parent, delete_node=False):
106
116
parent .delete ()
107
117
108
118
def ancestors_raw (self , ** kwargs ):
119
+ """Returns a raw QuerySet of all nodes in connected paths in a rootward direction"""
109
120
return AncestorQuery (instance = self , ** kwargs ).raw_queryset ()
110
121
111
122
def ancestors (self , ** kwargs ):
123
+ """Returns a QuerySet of all nodes in connected paths in a rootward direction"""
112
124
pks = [item .pk for item in self .ancestors_raw (** kwargs )]
113
125
return self .ordered_queryset_from_pks (pks )
114
126
115
127
def ancestors_count (self ):
128
+ """Returns an integer number representing the total number of ancestor nodes"""
116
129
return self .ancestors ().count ()
117
130
118
131
def self_and_ancestors (self , ** kwargs ):
132
+ """Returns a QuerySet of all nodes in connected paths in a rootward direction, prepending with self"""
119
133
pks = [self .pk ] + [item .pk for item in self .ancestors_raw (** kwargs )][::- 1 ]
120
134
return self .ordered_queryset_from_pks (pks )
121
135
122
136
def ancestors_and_self (self , ** kwargs ):
137
+ """Returns a QuerySet of all nodes in connected paths in a rootward direction, appending with self"""
123
138
pks = [item .pk for item in self .ancestors_raw (** kwargs )] + [self .pk ]
124
139
return self .ordered_queryset_from_pks (pks )
125
140
126
141
def descendants_raw (self , ** kwargs ):
142
+ """Returns a raw QuerySet of all nodes in connected paths in a leafward direction"""
127
143
return DescendantQuery (instance = self , ** kwargs ).raw_queryset ()
128
144
129
145
def descendants (self , ** kwargs ):
146
+ """Returns a QuerySet of all nodes in connected paths in a leafward direction"""
130
147
pks = [item .pk for item in self .descendants_raw (** kwargs )]
131
148
return self .ordered_queryset_from_pks (pks )
132
149
133
150
def descendants_count (self ):
151
+ """Returns an integer number representing the total number of descendant nodes"""
134
152
return self .descendants ().count ()
135
153
136
154
def self_and_descendants (self , ** kwargs ):
155
+ """Returns a QuerySet of all nodes in connected paths in a leafward direction, prepending with self"""
137
156
pks = [self .pk ] + [item .pk for item in self .descendants_raw (** kwargs )]
138
157
return self .ordered_queryset_from_pks (pks )
139
158
140
159
def descendants_and_self (self , ** kwargs ):
160
+ """Returns a QuerySet of all nodes in connected paths in a leafward direction, appending with self"""
141
161
pks = [item .pk for item in self .descendants_raw (** kwargs )] + [self .pk ]
142
162
return self .ordered_queryset_from_pks (pks )
143
163
144
164
def clan (self , ** kwargs ):
145
165
"""
146
- Returns a queryset with all ancestors, self, and all descendants
166
+ Returns a QuerySet with all ancestors nodes , self, and all descendant nodes
147
167
"""
148
168
pks = (
149
169
[item .pk for item in self .ancestors_raw (** kwargs )]
@@ -153,22 +173,23 @@ def clan(self, **kwargs):
153
173
return self .ordered_queryset_from_pks (pks )
154
174
155
175
def clan_count (self ):
176
+ """Returns an integer number representing the total number of clan nodes"""
156
177
return self .clan ().count ()
157
178
158
179
def siblings (self ):
159
- # Returns all nodes that share a parent with this node
180
+ """ Returns a QuerySet of all nodes that share a parent with this node, excluding self"""
160
181
return self .siblings_with_self ().exclude (pk = self .pk )
161
182
162
183
def siblings_count (self ):
163
- # Returns count of all nodes that share a parent with this node
184
+ """ Returns count of all nodes that share a parent with this node"""
164
185
return self .siblings ().count ()
165
186
166
187
def siblings_with_self (self ):
167
- # Returns all nodes that share a parent with this node and self
188
+ """ Returns a QuerySet of all nodes that share a parent with this node and self"""
168
189
return self .__class__ .objects .filter (parents__in = self .parents .all ()).distinct ()
169
190
170
191
def partners (self ):
171
- # Returns all nodes that share a child with this node
192
+ """ Returns a QuerySet of all nodes that share a child with this node"""
172
193
return self .partners_with_self ().exclude (pk = self .pk )
173
194
174
195
def partners_count (self ):
@@ -200,12 +221,21 @@ def path_raw(self, ending_node, directional=True, **kwargs):
200
221
return path
201
222
202
223
def path_exists (self , ending_node , ** kwargs ):
224
+ """
225
+ Given an ending Node instance, returns a boolean value determining whether there is a path from the current
226
+ Node instance to the ending Node instance
227
+ """
203
228
try :
204
229
return len (list (self .path_raw (ending_node , ** kwargs ))) >= 1
205
230
except NodeNotReachableException :
206
231
return False
207
232
208
233
def path (self , ending_node , ** kwargs ):
234
+ """
235
+ Returns a QuerySet of the shortest path from self to ending node, optionally in either direction.
236
+ The resulting Queryset is sorted from root-side, toward leaf-side, regardless of the relative position of
237
+ starting and ending nodes.
238
+ """
209
239
pks = [item .pk for item in self .path_raw (ending_node , ** kwargs )]
210
240
return self .ordered_queryset_from_pks (pks )
211
241
@@ -220,68 +250,85 @@ def distance(self, ending_node, **kwargs):
220
250
221
251
def is_root (self ):
222
252
"""
223
- Check if has children and not ancestors
253
+ Returns True if the current Node instance has children, but no parents
224
254
"""
225
255
return bool (self .children .exists () and not self .parents .exists ())
226
256
227
257
def is_leaf (self ):
228
258
"""
229
- Check if has ancestors and not children
259
+ Returns True if the current Node instance has parents, but no children
230
260
"""
231
261
return bool (self .parents .exists () and not self .children .exists ())
232
262
233
263
def is_island (self ):
234
264
"""
235
- Check if has no ancestors nor children
265
+ Returns True if the current Node instance has no parents nor children
236
266
"""
237
267
return bool (not self .children .exists () and not self .parents .exists ())
238
268
239
269
def is_ancestor_of (self , ending_node , ** kwargs ):
270
+ """
271
+ Provided an ending_node Node instance, returns True if the current Node instance and is an ancestor of the
272
+ provided Node instance
273
+ """
240
274
try :
241
275
return len (self .path_raw (ending_node , ** kwargs )) >= 1
242
276
except NodeNotReachableException :
243
277
return False
244
278
245
279
def is_descendant_of (self , ending_node , ** kwargs ):
280
+ """
281
+ Provided an ending_node Node instance, returns True if the current Node instance and is a descendant of the
282
+ provided Node instance
283
+ """
246
284
return (
247
285
not self .is_ancestor_of (ending_node , ** kwargs )
248
286
and len (self .path_raw (ending_node , directional = False , ** kwargs )) >= 1
249
287
)
250
288
251
289
def is_sibling_of (self , ending_node ):
290
+ """
291
+ Provided an ending_node Node instance, returns True if the provided Node instance and the current Node
292
+ instance share a parent Node
293
+ """
252
294
return ending_node in self .siblings ()
253
295
254
296
def is_partner_of (self , ending_node ):
297
+ """
298
+ Provided an ending_node Node instance, returns True if the provided Node instance and the current Node
299
+ instance share a child Node
300
+ """
255
301
return ending_node in self .partners ()
256
302
257
303
def node_depth (self ):
258
- # Depth from furthest root
304
+ """Returns an integer representing the depth of this Node instance from furthest root"""
259
305
# ToDo: Implement
260
306
pass
261
307
262
308
def connected_graph_raw (self , ** kwargs ):
263
- # Gets all nodes connected in any way to this node
309
+ """Returns a raw QuerySet of all nodes connected in any way to the current Node instance"""
264
310
return ConnectedGraphQuery (instance = self , ** kwargs ).raw_queryset ()
265
311
266
312
def connected_graph (self , ** kwargs ):
313
+ """Returns a QuerySet of all nodes connected in any way to the current Node instance"""
267
314
pks = [item .pk for item in self .connected_graph_raw (** kwargs )]
268
315
return self .ordered_queryset_from_pks (pks )
269
316
270
317
def descendants_tree (self ):
271
318
"""
272
- Returns a tree-like structure with descendants
273
- # ToDo: Modify to use CTE
319
+ Returns a tree-like structure with descendants for the current Node
274
320
"""
321
+ # ToDo: Modify to use CTE
275
322
tree = {}
276
323
for child in self .children .all ():
277
324
tree [child ] = child .descendants_tree ()
278
325
return tree
279
326
280
327
def ancestors_tree (self ):
281
328
"""
282
- Returns a tree-like structure with ancestors
283
- # ToDo: Modify to use CTE
329
+ Returns a tree-like structure with ancestors for the current Node
284
330
"""
331
+ # ToDo: Modify to use CTE
285
332
tree = {}
286
333
for parent in self .parents .all ():
287
334
tree [parent ] = parent .ancestors_tree ()
@@ -300,9 +347,9 @@ def _roots(self, ancestors_tree):
300
347
301
348
def roots (self ):
302
349
"""
303
- Returns roots nodes, if any
304
- # ToDo: Modify to use CTE
350
+ Returns a QuerySet of all root nodes, if any, for the current Node
305
351
"""
352
+ # ToDo: Modify to use CTE
306
353
ancestors_tree = self .ancestors_tree ()
307
354
roots = set ()
308
355
for ancestor in ancestors_tree :
@@ -324,9 +371,9 @@ def _leaves(self, descendants_tree):
324
371
325
372
def leaves (self ):
326
373
"""
327
- Returns leaves nodes, if any
328
- # ToDo: Modify to use CTE
374
+ Returns a QuerySet of all leaf nodes, if any, for the current Node
329
375
"""
376
+ # ToDo: Modify to use CTE
330
377
descendants_tree = self .descendants_tree ()
331
378
leaves = set ()
332
379
for descendant in descendants_tree :
@@ -337,29 +384,27 @@ def leaves(self):
337
384
338
385
def descendants_edges (self ):
339
386
"""
340
- Returns a queryset of descendants edges
341
-
342
- ToDo: Perform topological sort
387
+ Returns a QuerySet of descendant Edge instances for the current Node
343
388
"""
389
+ # ToDo: Perform topological sort
344
390
return edge_model .objects .filter (
345
391
parent__in = self .self_and_descendants (),
346
392
child__in = self .self_and_descendants (),
347
393
)
348
394
349
395
def ancestors_edges (self ):
350
396
"""
351
- Returns a queryset of ancestors edges
352
-
353
- ToDo: Perform topological sort
397
+ Returns a QuerySet of ancestor Edge instances for the current Node
354
398
"""
399
+ # ToDo: Perform topological sort
355
400
return edge_model .objects .filter (
356
401
parent__in = self .self_and_ancestors (),
357
402
child__in = self .self_and_ancestors (),
358
403
)
359
404
360
405
def clan_edges (self ):
361
406
"""
362
- Returns a queryset of all edges associated with a given node
407
+ Returns a QuerySet of all Edge instances associated with a given node
363
408
"""
364
409
return self .ancestors_edges () | self .descendants_edges ()
365
410
@@ -378,44 +423,46 @@ def duplicate_edge_checker(parent, child):
378
423
379
424
class EdgeManager (models .Manager ):
380
425
def from_nodes_queryset (self , nodes_queryset ):
381
- """Provided a queryset of nodes, returns all edges where a parent and child
382
- node are within the queryset of nodes."""
426
+ """
427
+ Provided a QuerySet of nodes, returns a QuerySet of all Edge instances where a parent and child Node are within
428
+ the QuerySet of nodes
429
+ """
383
430
return _ordered_filter (self .model .objects , ["parent" , "child" ], nodes_queryset )
384
431
385
432
def descendants (self , node , ** kwargs ):
386
433
"""
387
- Returns a queryset of all edges descended from the given node
434
+ Returns a QuerySet of all Edge instances descended from the given Node instance
388
435
"""
389
436
return _ordered_filter (self .model .objects , "parent" , node .self_and_descendants (** kwargs ))
390
437
391
438
def ancestors (self , node , ** kwargs ):
392
439
"""
393
- Returns a queryset of all edges which are ancestors of the given node
440
+ Returns a QuerySet of all Edge instances which are ancestors of the given Node instance
394
441
"""
395
442
return _ordered_filter (self .model .objects , "child" , node .ancestors_and_self (** kwargs ))
396
443
397
444
def clan (self , node , ** kwargs ):
398
445
"""
399
- Returns a queryset of all edges for ancestors, self, and descendants
446
+ Returns a QuerySet of all Edge instances for ancestors, self, and descendants
400
447
"""
401
448
return self .from_nodes_queryset (node .clan (** kwargs ))
402
449
403
450
def path (self , start_node , end_node , ** kwargs ):
404
451
"""
405
- Returns a queryset of all edges for the shortest path from start_node to end_node
452
+ Returns a QuerySet of all Edge instances for the shortest path from start_node to end_node
406
453
"""
407
454
return self .from_nodes_queryset (start_node .path (end_node , ** kwargs ))
408
455
409
456
def validate_route (self , edges , ** kwargs ):
410
457
"""
411
- Given a list or set of edges , verify that they result in a contiguous route
458
+ Given a list or set of Edge instances , verify that they result in a contiguous route
412
459
"""
413
460
# ToDo: Implement
414
461
pass
415
462
416
463
def sort (self , edges , ** kwargs ):
417
464
"""
418
- Given a list or set of edges , sort them from root-side to leaf-side
465
+ Given a list or set of Edge instances , sort them from root-side to leaf-side
419
466
"""
420
467
# ToDo: Implement
421
468
pass
0 commit comments