django-fast-treenode 1.1.3__py3-none-any.whl → 2.0.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/METADATA +156 -46
  2. django_fast_treenode-2.0.0.dist-info/RECORD +41 -0
  3. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/WHEEL +1 -1
  4. treenode/__init__.py +0 -7
  5. treenode/admin.py +327 -82
  6. treenode/apps.py +20 -3
  7. treenode/cache.py +231 -0
  8. treenode/docs/Documentation +130 -54
  9. treenode/forms.py +75 -19
  10. treenode/managers.py +260 -48
  11. treenode/models/__init__.py +7 -0
  12. treenode/models/classproperty.py +24 -0
  13. treenode/models/closure.py +168 -0
  14. treenode/models/factory.py +71 -0
  15. treenode/models/proxy.py +650 -0
  16. treenode/static/treenode/css/tree_widget.css +62 -0
  17. treenode/static/treenode/css/treenode_admin.css +106 -0
  18. treenode/static/treenode/js/tree_widget.js +161 -0
  19. treenode/static/treenode/js/treenode_admin.js +171 -0
  20. treenode/templates/admin/export_success.html +26 -0
  21. treenode/templates/admin/tree_node_changelist.html +11 -0
  22. treenode/templates/admin/tree_node_export.html +27 -0
  23. treenode/templates/admin/tree_node_import.html +27 -0
  24. treenode/templates/widgets/tree_widget.css +23 -0
  25. treenode/templates/widgets/tree_widget.html +21 -0
  26. treenode/urls.py +34 -0
  27. treenode/utils/__init__.py +4 -0
  28. treenode/utils/base36.py +35 -0
  29. treenode/utils/exporter.py +141 -0
  30. treenode/utils/importer.py +296 -0
  31. treenode/version.py +11 -1
  32. treenode/views.py +102 -2
  33. treenode/widgets.py +49 -27
  34. django_fast_treenode-1.1.3.dist-info/RECORD +0 -33
  35. treenode/compat.py +0 -8
  36. treenode/factory.py +0 -68
  37. treenode/models.py +0 -668
  38. treenode/static/select2tree/.gitkeep +0 -1
  39. treenode/static/select2tree/select2tree.css +0 -176
  40. treenode/static/select2tree/select2tree.js +0 -181
  41. treenode/static/treenode/css/treenode.css +0 -85
  42. treenode/static/treenode/js/treenode.js +0 -201
  43. treenode/templates/widgets/.gitkeep +0 -1
  44. treenode/templates/widgets/attrs.html +0 -7
  45. treenode/templates/widgets/options.html +0 -1
  46. treenode/templates/widgets/select2tree.html +0 -22
  47. treenode/tests.py +0 -3
  48. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/LICENSE +0 -0
  49. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.0.dist-info}/top_level.txt +0 -0
  50. /treenode/{docs → templates/admin}/.gitkeep +0 -0
treenode/managers.py CHANGED
@@ -1,69 +1,281 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- TreeNode Managers Module
3
+ Managers and QuerySets
4
4
 
5
+ This module defines custom managers and query sets for the TreeNode model.
6
+ It includes optimized bulk operations for handling hierarchical data
7
+ using the Closure Table approach.
8
+
9
+ Features:
10
+ - `ClosureQuerySet` and `ClosureModelManager` for managing closure records.
11
+ - `TreeNodeQuerySet` and `TreeNodeModelManager` for adjacency model operations.
12
+ - Optimized `bulk_create` and `bulk_update` methods with atomic transactions.
13
+
14
+ Version: 2.0.0
15
+ Author: Timur Kady
16
+ Email: timurkady@yandex.com
5
17
  """
6
18
 
7
- from django.db import models
8
- from django.db.models import Case, When, Value
19
+
20
+ from django.db import models, transaction
21
+
22
+
23
+ # ----------------------------------------------------------------------------
24
+ # Closere Model
25
+ # ----------------------------------------------------------------------------
26
+
27
+
28
+ class ClosureQuerySet(models.QuerySet):
29
+ """QuerySet для ClosureModel."""
30
+
31
+ @transaction.atomic
32
+ def bulk_create(self, objs, batch_size=1000):
33
+ """
34
+ Insert new nodes in bulk.
35
+
36
+ For newly created AdjacencyModel objects:
37
+ 1. Create self-referential records (parent=child, depth=0).
38
+ 2. Build ancestors for each node based on tn_parent.
39
+ """
40
+ # --- 1. Create self-referential closure records for each object.
41
+ self_closure_records = []
42
+ for item in objs:
43
+ self_closure_records.append(
44
+ self.model(parent=item, child=item, depth=0)
45
+ )
46
+ super().bulk_create(self_closure_records, batch_size=batch_size)
47
+
48
+ # --- 2. Preparing closure for parents
49
+ parent_ids = {node.tn_parent.pk for node in objs if node.tn_parent}
50
+ parent_closures = list(
51
+ self.filter(child__in=parent_ids)
52
+ .values('child', 'parent', 'depth')
53
+ )
54
+ # Pack them into a dict
55
+ parent_closures_dict = {}
56
+ for pc in parent_closures:
57
+ parent_id = pc['child']
58
+ parent_closures_dict.setdefault(parent_id, []).append(pc)
59
+
60
+ # --- 3. Get closure records for the objs themselves
61
+ node_ids = [node.pk for node in objs]
62
+ child_closures = list(
63
+ self.filter(parent__in=node_ids)
64
+ .values('parent', 'child', 'depth')
65
+ )
66
+ child_closures_dict = {}
67
+ for cc in child_closures:
68
+ parent_id = cc['parent']
69
+ child_closures_dict.setdefault(parent_id, []).append(cc)
70
+
71
+ # --- 4. Collecting new links
72
+ new_records = []
73
+ for node in objs:
74
+ if not node.tn_parent:
75
+ continue
76
+ # parent closure
77
+ parents = parent_closures_dict.get(node.tn_parent.pk, [])
78
+ # closure of descendants
79
+ # (often this is only the node itself, depth=0, but there can be
80
+ # nested ones)
81
+ children = child_closures_dict.get(node.pk, [])
82
+ # Combine parents x children
83
+ for p in parents:
84
+ for c in children:
85
+ new_records.append(self.model(
86
+ parent_id=p['parent'],
87
+ child_id=c['child'],
88
+ depth=p['depth'] + c['depth'] + 1
89
+ ))
90
+ # --- 5. bulk_create
91
+ result = []
92
+ if new_records:
93
+ result = super().bulk_create(new_records, batch_size=batch_size)
94
+ self.model.clear_cache()
95
+ return result
96
+
97
+ @transaction.atomic
98
+ def bulk_update(self, objs, fields=None, batch_size=1000):
99
+ """
100
+ Update the records in the closure table for the list of updated nodes.
101
+
102
+ For each node whose tn_parent has changed, the closure records
103
+ for its entire subtree are recalculated:
104
+ 1. The ancestor chain of the new parent is selected.
105
+ 2. The subtree (with closure records) of the updated node is selected.
106
+ 3. For each combination (ancestor, descendant), a new depth is
107
+ calculated.
108
+ 4. Old "dangling" records (those where the descendant has a link to
109
+ a non-subtree) are removed.
110
+ 5. New records are inserted using the bulk_create method.
111
+ """
112
+ if not objs:
113
+ return
114
+
115
+ # --- 1. We obtain chains of ancestors for new parents.
116
+ parent_ids = {node.tn_parent.pk for node in objs if node.tn_parent}
117
+ parent_closures = list(
118
+ self.filter(child__in=parent_ids).values(
119
+ 'child',
120
+ 'parent',
121
+ 'depth'
122
+ )
123
+ )
124
+ # We collect in a dictionary: key is the parent ID (tn_parent),
125
+ # value is the list of records.
126
+ parent_closures_dict = {}
127
+ for pc in parent_closures:
128
+ parent_closures_dict.setdefault(pc['child'], []).append(pc)
129
+
130
+ # --- 2. Obtain closing records for the subtrees of the
131
+ # nodes being updated.
132
+ updated_ids = [node.pk for node in objs]
133
+ subtree_closures = list(self.filter(parent__in=updated_ids).values(
134
+ 'parent',
135
+ 'child',
136
+ 'depth'
137
+ ))
138
+ # Group by ID of the node being updated (i.e. by parent in the
139
+ # closing record)
140
+ subtree_closures_dict = {}
141
+ for sc in subtree_closures:
142
+ subtree_closures_dict.setdefault(sc['parent'], []).append(sc)
143
+
144
+ # --- 3. Construct new close records for each updated node with
145
+ # a new parent.
146
+ new_records = []
147
+ for node in objs:
148
+ # If the node has become root (tn_parent=None), then there are
149
+ # no additional connections with ancestors.
150
+ if not node.tn_parent:
151
+ continue
152
+ # From the closing chain of the new parent we get a list of its
153
+ # ancestors
154
+ p_closures = parent_closures_dict.get(node.tn_parent.pk, [])
155
+ # From the node subtree we get the closing records
156
+ s_closures = subtree_closures_dict.get(node.pk, [])
157
+ # If for some reason the subtree entries are missing, we will
158
+ # ensure that there is a custom entry.
159
+ if not s_closures:
160
+ s_closures = [{
161
+ 'parent': node.pk,
162
+ 'child': node.pk,
163
+ 'depth': 0
164
+ }]
165
+ # Combine: for each ancestor of the new parent and for each
166
+ # descendant from the subtree
167
+ for p in p_closures:
168
+ for s in s_closures:
169
+ new_depth = p['depth'] + s['depth'] + 1
170
+ new_records.append(
171
+ self.model(
172
+ parent_id=p['parent'],
173
+ child_id=s['child'],
174
+ depth=new_depth
175
+ )
176
+ )
177
+
178
+ # --- 4. Remove old closing records so that there are no "tails".
179
+ # For each updated node, calculate a subset of IDs related to its
180
+ # subtree
181
+ for node in objs:
182
+ subtree_ids = set()
183
+ # Be sure to include the node itself (its self-link should
184
+ # already be there)
185
+ subtree_ids.add(node.pk)
186
+ for sc in subtree_closures_dict.get(node.pk, []):
187
+ subtree_ids.add(sc['child'])
188
+ # Remove records where the child belongs to the subtree, but the
189
+ # parent is not included in it.
190
+ self.filter(child_id__in=subtree_ids).exclude(
191
+ parent_id__in=subtree_ids).delete()
192
+
193
+ # --- 5. Insert new closing records in bulk.
194
+ if new_records:
195
+ super().bulk_create(new_records, batch_size=batch_size)
196
+ self.model.clear_cache()
197
+ return new_records
198
+
199
+
200
+ class ClosureModelManager(models.Manager):
201
+ """ClosureModel Manager."""
202
+
203
+ def get_queryset(self):
204
+ """get_queryset method."""
205
+ return ClosureQuerySet(self.model, using=self._db)
206
+
207
+ def bulk_create(self, objs, batch_size=1000):
208
+ """Create objects in bulk."""
209
+ self.model.clear_cache()
210
+ return self.get_queryset().bulk_create(objs, batch_size=batch_size)
211
+
212
+ def bulk_update(self, objs, fields=None, batch_size=1000):
213
+ """Move nodes in ClosureModel."""
214
+ self.model.clear_cache()
215
+ return self.get_queryset().bulk_update(
216
+ objs, fields, batch_size=batch_size
217
+ )
218
+
219
+ # ----------------------------------------------------------------------------
220
+ # TreeNode Model
221
+ # ----------------------------------------------------------------------------
9
222
 
10
223
 
11
224
  class TreeNodeQuerySet(models.QuerySet):
12
- """TreeNode Manager QuerySet Class"""
225
+ """TreeNodeModel QuerySet."""
13
226
 
14
227
  def __init__(self, model=None, query=None, using=None, hints=None):
228
+ """Init."""
15
229
  self.closure_model = model.closure_model
16
230
  super().__init__(model, query, using, hints)
17
231
 
18
- def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
232
+ @transaction.atomic
233
+ def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
234
+ """
235
+ Bulk create.
236
+
237
+ Method of bulk creation objects with updating and processing of
238
+ the Closuse Model.
239
+ """
240
+ # Regular bulk_create for TreeNodeModel
19
241
  objs = super().bulk_create(objs, batch_size, ignore_conflicts)
242
+ # Call ClosureModel to insert closure records
243
+ self.closure_model.objects.bulk_create(objs, batch_size=batch_size)
244
+ # Возвращаем результат
245
+ self.model.clear_cache()
246
+ return self.filter(pk__in=[obj.pk for obj in objs])
20
247
 
21
- objs = self.model.closure_model.bulk_create([
22
- self.model.closure_model(
23
- parent=item,
24
- child=item,
25
- depth=0
26
- )
27
- ] for item in objs)
248
+ @transaction.atomic
249
+ def bulk_update(self, objs, fields, batch_size=1000, **kwargs):
250
+ """."""
251
+ closure_model = self.model.closure_model
252
+ if 'tn_parent' in fields:
253
+ # Попросим ClosureModel обработать move
254
+ closure_model.objects.bulk_update(objs)
255
+ result = super().bulk_update(objs, fields, batch_size, **kwargs)
256
+ closure_model.clear_cache()
257
+ return result
28
258
 
29
- for node in objs:
30
- qs = self.model.closure_model.objects.all()
31
- parents = qs.filter(child=node.tn_parent).values('parent', 'depth')
32
- children = qs.filter(parent=node).values('child', 'depth')
33
- objects = [
34
- self.model.closure_model(
35
- parent_id=p['parent'],
36
- child_id=c['child'],
37
- depth=p['depth'] + c['depth'] + 1
38
- )
39
- for p in parents
40
- for c in children
41
- ]
42
- node._closure_model.objects.bulk_create(objects)
43
-
44
- self.model._update_orders()
45
- return objs
46
-
47
-
48
- class TreeNodeManager(models.Manager):
49
- """TreeNode Manager Class"""
50
259
 
51
- def get_queryset(self):
52
- """
53
- Forms a QuerySet ordered by the materialized path.
260
+ class TreeNodeModelManager(models.Manager):
261
+ """TreeNodeModel Manager."""
262
+
263
+ def bulk_create(self, objs, batch_size=1000, ignore_conflicts=False):
54
264
  """
265
+ Bulk Create.
55
266
 
56
- qs = TreeNodeQuerySet(self.model, using=self._db)
57
- node_list = sorted([node for node in qs], key=lambda x: x.tn_order)
58
- pk_list = [node.pk for node in node_list]
59
-
60
- # Retrieve the queryset with the desired ordering
61
- return qs.filter(pk__in=pk_list).order_by(
62
- Case(*[When(pk=pk, then=Value(ordering))
63
- for ordering, pk in enumerate(pk_list)],
64
- default=Value(len(pk_list)),
65
- output_field=models.IntegerField(),
66
- )
267
+ Override bulk_create for the adjacency model.
268
+ Here we first clear the cache, then delegate the creation via our
269
+ custom QuerySet.
270
+ """
271
+ self.model.clear_cache()
272
+ return self.get_queryset().bulk_create(
273
+ objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts
67
274
  )
68
275
 
69
- # End
276
+ def get_queryset(self):
277
+ """Return a QuerySet that sorts by 'tn_parent' and 'tn_priority'."""
278
+ queryset = TreeNodeQuerySet(self.model, using=self._db)
279
+ return queryset.order_by('tn_parent', 'tn_priority')
280
+
281
+ # The End
@@ -0,0 +1,7 @@
1
+ from .proxy import TreeNodeModel
2
+
3
+
4
+ __all__ = ["TreeNodeModel",]
5
+
6
+
7
+ # The End
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Class Property Decorator
4
+
5
+ This module provides a `classproperty` decorator that allows defining
6
+ read-only class-level properties.
7
+
8
+ Features:
9
+ - Enables class-level properties similar to instance properties.
10
+ - Uses a custom descriptor for property-like behavior.
11
+
12
+ """
13
+
14
+
15
+ class classproperty(object):
16
+ """Classproperty class."""
17
+
18
+ def __init__(self, getter):
19
+ """Init."""
20
+ self.getter = getter
21
+
22
+ def __get__(self, instance, owner):
23
+ """Get."""
24
+ return self.getter(owner)
@@ -0,0 +1,168 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Closure Model
4
+
5
+ This module defines the Closure Table implementation for hierarchical
6
+ data storage in the TreeNode model. It supports efficient queries for
7
+ retrieving ancestors, descendants, breadcrumbs, and tree depth.
8
+
9
+ Features:
10
+ - Uses a Closure Table for efficient tree operations.
11
+ - Implements cached queries for improved performance.
12
+ - Provides bulk operations for inserting, moving, and deleting nodes.
13
+
14
+ Version: 2.0.0
15
+ Author: Timur Kady
16
+ Email: timurkady@yandex.com
17
+ """
18
+
19
+
20
+ from django.db import models, transaction
21
+
22
+ from ..managers import ClosureModelManager
23
+ from ..cache import cached_method, treenode_cache
24
+
25
+
26
+ class ClosureModel(models.Model):
27
+ """
28
+ Model for Closure Table.
29
+
30
+ Implements hierarchy storage using the Closure Table method.
31
+ """
32
+
33
+ parent = models.ForeignKey(
34
+ 'TreeNodeModel',
35
+ related_name='children_set',
36
+ on_delete=models.CASCADE,
37
+ )
38
+
39
+ child = models.ForeignKey(
40
+ 'TreeNodeModel',
41
+ related_name='parents_set',
42
+ on_delete=models.CASCADE,
43
+ )
44
+
45
+ depth = models.PositiveIntegerField()
46
+
47
+ objects = ClosureModelManager()
48
+
49
+ class Meta:
50
+ """Meta Class."""
51
+
52
+ abstract = True
53
+ unique_together = (("parent", "child"),)
54
+ indexes = [
55
+ models.Index(fields=["parent", "child"]),
56
+ ]
57
+
58
+ def __str__(self):
59
+ """Display information about a class object."""
60
+ return f"{self.parent} — {self.child} — {self.depth}"
61
+
62
+ # ----------- Methods of working with tree structure ----------- #
63
+
64
+ @classmethod
65
+ def clear_cache(cls):
66
+ """Clear cache for this model only."""
67
+ treenode_cache.invalidate(cls._meta.label)
68
+
69
+ @classmethod
70
+ @cached_method
71
+ def get_ancestors_queryset(cls, node, include_self=True, depth=None):
72
+ """Get the ancestors QuerySet (ordered from root to parent)."""
73
+ filters = {"child__pk": node.pk}
74
+ if depth is not None:
75
+ filters["depth__lte"] = depth
76
+ qs = cls.objects.all().filter(**filters)
77
+ if not include_self:
78
+ qs = qs.exclude(parent=node, child=node)
79
+ return qs
80
+
81
+ @classmethod
82
+ @cached_method
83
+ def get_breadcrumbs(cls, node, attr=None):
84
+ """Get the breadcrumbs to current node (included)."""
85
+ qs = cls.get_ancestors_queryset(
86
+ node, include_self=True).order_by('-depth')
87
+ if attr:
88
+ return [getattr(item.parent, attr)
89
+ if hasattr(item.parent, attr) else None for item in qs]
90
+ else:
91
+ return [item.parent.pk for item in qs]
92
+
93
+ @classmethod
94
+ @cached_method
95
+ def get_descendants_queryset(cls, node, include_self=False, depth=None):
96
+ """Get the descendants QuerySet (ordered from parent to leaf)."""
97
+ filters = {"parent__pk": node.pk}
98
+ if depth is not None:
99
+ filters["depth__lte"] = depth
100
+ qs = cls.objects.all().filter(**filters).select_related('parent')
101
+ if not include_self:
102
+ qs = qs.exclude(parent=node, child=node)
103
+ return qs
104
+
105
+ @classmethod
106
+ @cached_method
107
+ def get_root(cls, node):
108
+ """Get the root node for the given node."""
109
+ return cls.objects.filter(child=node).values_list(
110
+ "parent", flat=True).order_by('-depth').first()
111
+
112
+ @classmethod
113
+ @cached_method
114
+ def get_depth(cls, node):
115
+ """Get the node depth (how deep the node is in the tree)."""
116
+ result = cls.objects.filter(child__pk=node.pk).aggregate(
117
+ models.Max("depth")
118
+ )["depth__max"]
119
+ return result if result is not None else 0
120
+
121
+ @classmethod
122
+ @cached_method
123
+ def get_level(cls, node):
124
+ """Get the node level (starting from 1)."""
125
+ return cls.objects.filter(child__pk=node.pk).aggregate(
126
+ models.Max("depth"))["depth__max"] + 1
127
+
128
+ @classmethod
129
+ @cached_method
130
+ def get_path(cls, node, delimiter='.', format_str=""):
131
+ """Return Materialized Path of node."""
132
+ str_ = "{%s}" % format_str
133
+ priorities = cls.get_breadcrumbs(node, attr='tn_priority')
134
+ path = delimiter.join([str_.format(p) for p in priorities])
135
+ return path
136
+
137
+ @classmethod
138
+ @transaction.atomic
139
+ def insert_node(cls, node):
140
+ """Add a node to a Closure table."""
141
+ # Call bulk_create passing a single object
142
+ cls.objects.bulk_create([node], batch_size=1000)
143
+ # Clear cache
144
+ cls.clear_cache()
145
+
146
+ @classmethod
147
+ @transaction.atomic
148
+ def move_node(cls, node):
149
+ """Move a node (and its subtree) to a new parent."""
150
+ # Call bulk_update passing a single object
151
+ cls.objects.bulk_update([node], batch_size=1000)
152
+ # Clear cache
153
+ cls.clear_cache()
154
+
155
+ @classmethod
156
+ @transaction.atomic
157
+ def delete_all(cls):
158
+ """Clear the Closure Table."""
159
+ # Clear cache
160
+ cls.clear_cache()
161
+ cls.objects.all().delete()
162
+
163
+ def save(self, force_insert=False, *args, **kwargs):
164
+ """Save method."""
165
+ super().save(force_insert, *args, **kwargs)
166
+ self._meta.model.clear_cache()
167
+
168
+ # The End
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Factory for Closure Table
4
+
5
+ This module provides a metaclass `TreeFactory` that automatically binds
6
+ a model to a Closure Table for hierarchical data storage.
7
+
8
+ Features:
9
+ - Ensures non-abstract, non-proxy models get a corresponding Closure Table.
10
+ - Dynamically creates and assigns a Closure Model for each TreeNodeModel.
11
+ - Facilitates the management of hierarchical relationships.
12
+
13
+ Version: 2.0.0
14
+ Author: Timur Kady
15
+ Email: timurkady@yandex.com
16
+ """
17
+
18
+
19
+ import sys
20
+ from django.db import models
21
+ from .closure import ClosureModel # Используем готовый ClosureModel
22
+
23
+
24
+ class TreeFactory(models.base.ModelBase):
25
+ """
26
+ Metaclass for binding a model to a Closure Table.
27
+
28
+ For each non-abstract, non-proxy, and "top" (without parents) model,
29
+ assigns the `ClosureModel` as the closure table.
30
+ """
31
+
32
+ def __init__(cls, name, bases, dct):
33
+ """Class initialization.
34
+
35
+ We check that the model:
36
+ - is not abstract
37
+ - is not a proxy
38
+ - is not a child
39
+ and only then assign the ClosureModel.
40
+ """
41
+ super().__init__(name, bases, dct)
42
+
43
+ if (cls._meta.abstract or cls._meta.proxy or
44
+ cls._meta.get_parent_list()):
45
+ return
46
+
47
+ closure_name = f"{cls._meta.object_name}ClosureModel"
48
+ if getattr(cls, "closure_model", None) is not None:
49
+ return
50
+
51
+ fields = {
52
+ "parent": models.ForeignKey(
53
+ cls._meta.model,
54
+ related_name="children_set",
55
+ on_delete=models.CASCADE
56
+ ),
57
+
58
+ "child": models.ForeignKey(
59
+ cls._meta.model,
60
+ related_name="parents_set",
61
+ on_delete=models.CASCADE,
62
+ ),
63
+
64
+ "__module__": cls.__module__
65
+ }
66
+ closure_model = type(closure_name, (ClosureModel,), fields)
67
+ setattr(sys.modules[cls.__module__], closure_name, closure_model)
68
+
69
+ cls.closure_model = closure_model
70
+
71
+ # The End