django-fast-treenode 3.2.0__py3-none-any.whl → 3.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_fast_treenode-3.2.0.dist-info → django_fast_treenode-3.2.2.dist-info}/METADATA +1 -1
- {django_fast_treenode-3.2.0.dist-info → django_fast_treenode-3.2.2.dist-info}/RECORD +15 -14
- treenode/managers/tasks.py +19 -1
- treenode/models/mixins/__init__.py +3 -1
- treenode/models/mixins/children.py +1 -1
- treenode/models/mixins/descendants.py +1 -1
- treenode/models/mixins/roots.py +1 -1
- treenode/models/mixins/search.py +55 -0
- treenode/models/mixins/siblings.py +1 -1
- treenode/models/models.py +17 -5
- treenode/utils/db/compiler.py +38 -79
- treenode/version.py +2 -2
- {django_fast_treenode-3.2.0.dist-info → django_fast_treenode-3.2.2.dist-info}/WHEEL +0 -0
- {django_fast_treenode-3.2.0.dist-info → django_fast_treenode-3.2.2.dist-info}/licenses/LICENSE +0 -0
- {django_fast_treenode-3.2.0.dist-info → django_fast_treenode-3.2.2.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
django_fast_treenode-3.2.
|
1
|
+
django_fast_treenode-3.2.2.dist-info/licenses/LICENSE,sha256=SSYqS84FCnAW7tAxmjBKU8qAa8Jv4VGPuSSGeHwWtJE,1095
|
2
2
|
treenode/__init__.py,sha256=3z1hWpHyy4wg6uz7HCmRi9FaXYeN5CfANVpa77UIoPw,53
|
3
3
|
treenode/apps.py,sha256=QlwjNDM9rkUoWB8Vm8-OkS6lNx0-aTByuGZlu9wrQMs,1832
|
4
4
|
treenode/cache.py,sha256=2jUiiecfFxwB7QFukpU4u0FnDzGH6hNRfo6KAYvs6vM,8447
|
@@ -7,7 +7,7 @@ treenode/settings.py,sha256=FRGK7hl_Tnxp4sGbUNxJgEQP9niDJjLhcNBtmloOvHk,741
|
|
7
7
|
treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
|
8
8
|
treenode/tests.py,sha256=2uDafv3Ns6f7Vy1ekUtgYxCZEi1KRyesZDTAFhYcX-E,63
|
9
9
|
treenode/urls.py,sha256=krHvVigc_dxC0z5hEd2rgeH6th8jW7qJY3Qbia-419Y,240
|
10
|
-
treenode/version.py,sha256=
|
10
|
+
treenode/version.py,sha256=q3WMjIC19sBswfw551UFRBugqrkG-f-MMq_yCDGtncA,220
|
11
11
|
treenode/widgets.py,sha256=3kSby7v-gpyUHmAIFZCENM8hzT1xcHJFB4op8XU6YEQ,4035
|
12
12
|
treenode/admin/__init__.py,sha256=XNEYHdF5lKb0vpdlVxdR2fxj5oUgzyx1YyCwsv0gxHw,100
|
13
13
|
treenode/admin/admin.py,sha256=cbiqlzXwK-W73DTJqvXo_abtnK1JwEwyBEOl0bmgsa4,7507
|
@@ -18,21 +18,22 @@ treenode/admin/mixin.py,sha256=7hcjoh8W2_R2in0EorNjVTXakYP3I1mMZKIr65UsU-g,10677
|
|
18
18
|
treenode/managers/__init__.py,sha256=c7F9Ku9489Hv6lTpUY2nbyBlWFCXBWAkNBm4xTKcjL8,186
|
19
19
|
treenode/managers/managers.py,sha256=G1dayrqaEX5nQiKsHS_2y6o3iXKIAn66RKArHttN-kU,7174
|
20
20
|
treenode/managers/queries.py,sha256=Kepax8SDn7G5tOlPRWBCp5Oyp49O5iMITCMBoNCm_Ak,10655
|
21
|
-
treenode/managers/tasks.py,sha256=
|
21
|
+
treenode/managers/tasks.py,sha256=XGV7zei6LQJqQx6Ozg5lWC2A7770wn6rvWJ5jyJ_8pw,7796
|
22
22
|
treenode/models/__init__.py,sha256=iR4ksCKoayvkIWWgGk6OUGHZC3D0mzAtgdBcS2vQPBw,188
|
23
23
|
treenode/models/decorators.py,sha256=N2dcnWqSCiEXDcYCf0zVijrbGUC8kYlqOLi_GKFmECU,1457
|
24
24
|
treenode/models/factory.py,sha256=sPUSrvo1za-r6ny3B8ptwevyjO8-iUpPNrT0eSD2kvI,1786
|
25
|
-
treenode/models/models.py,sha256=
|
26
|
-
treenode/models/mixins/__init__.py,sha256=
|
25
|
+
treenode/models/models.py,sha256=ClY1LaxAikqgdLIOiuKfb8srTpj5gUcSGmB50oA6REA,12969
|
26
|
+
treenode/models/mixins/__init__.py,sha256=CAef4rue_uYeBdcswPZAOIEwsKqGLhL_XFco7JwcvdI,909
|
27
27
|
treenode/models/mixins/ancestors.py,sha256=9g-0nPHoiF_SX2kN4uDLdbWyw-TDCz1YqxLJngwTZOQ,1971
|
28
|
-
treenode/models/mixins/children.py,sha256=
|
29
|
-
treenode/models/mixins/descendants.py,sha256=
|
28
|
+
treenode/models/mixins/children.py,sha256=H0faiH24ihXCUEWGjTcjoIAKl26E8RQcQaTTkgKFz00,2397
|
29
|
+
treenode/models/mixins/descendants.py,sha256=5xoeIdRhZ94K-EMrEjGgD7KW668gyXxp8JyB3t3mFAU,2163
|
30
30
|
treenode/models/mixins/family.py,sha256=MB5kWRVvxU_xmSgCekveTP5Vhj4wJki8bU7hzn9RNLE,1673
|
31
31
|
treenode/models/mixins/logical.py,sha256=gh5wv5XZDs5GWarU6g9zKXWNwji7SE3zSVNIpywDWjw,2190
|
32
32
|
treenode/models/mixins/node.py,sha256=D3lwWsN2eRJeS99O7CaVAhabTYWA_NSY8z60zAhFAs0,8576
|
33
33
|
treenode/models/mixins/properties.py,sha256=1O6p2tfvOesBooZeOeuXi8yfEO_o-5gn_agtKqMxU-s,3945
|
34
|
-
treenode/models/mixins/roots.py,sha256=
|
35
|
-
treenode/models/mixins/
|
34
|
+
treenode/models/mixins/roots.py,sha256=wfuf6jdkcfkEUa42n0bHD_bQDE78AEFK2ZVCeBh7FZw,4100
|
35
|
+
treenode/models/mixins/search.py,sha256=ZURelSNFT3wK3A0SnYvfHb2WnfrN_c7BIatp6ffGfsY,1652
|
36
|
+
treenode/models/mixins/siblings.py,sha256=o0Iorfe35V1A9iAKwV8CqALPuTjjcQcnEAjAdPE6haI,3067
|
36
37
|
treenode/models/mixins/tree.py,sha256=5Cew5c-NtTmz15S_ueDZ-fojHtQKZEKNxmp3W6qlC-8,14512
|
37
38
|
treenode/models/mixins/update.py,sha256=oCZkMnfT23n2n3_mnokDrtTLS_jO_lJRwG3ENkH_DE4,4948
|
38
39
|
treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -79,7 +80,7 @@ treenode/templatetags/treenode_admin.py,sha256=5f5oqAS4zC_f0kkJRsm5MqXHjsKJXn8Gs
|
|
79
80
|
treenode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
80
81
|
treenode/utils/jwt_auth.py,sha256=LSTMuBFbH1DPJ0pVUnD_T1owb6GurpkJcCxpsXDL4HQ,845
|
81
82
|
treenode/utils/db/__init__.py,sha256=RwicAcJSI1nhIPWLdT7j9TFsgOc9834VDn9lVn54GlY,247
|
82
|
-
treenode/utils/db/compiler.py,sha256=
|
83
|
+
treenode/utils/db/compiler.py,sha256=2evMIg-bqDNbRqZub6ePNlQi9jnS_YbLe3CmOa_4XC0,2519
|
83
84
|
treenode/utils/db/db_vendor.py,sha256=4SyEHl51jVCDB3is4omHCf2bTB_QV3RvemUQYxJP5m0,930
|
84
85
|
treenode/utils/db/service.py,sha256=PF85Yhz2xUWFFCzpLYotmiNTZXXEH61rhswslSxEUds,2640
|
85
86
|
treenode/utils/db/sqlcompat.py,sha256=K71ggkKIvpdTtHQ6Y4qcbo6cj2eYiEfy6DlVBr8Po1E,4460
|
@@ -91,7 +92,7 @@ treenode/views/children.py,sha256=seO0SNKriRY2FJOO1oZgX42iKolXMf3EF76GrfdJZLQ,11
|
|
91
92
|
treenode/views/common.py,sha256=kUN3IgMZCdNdJ2haxB9MTGcn2rctyFUAHNagzcu9wXk,594
|
92
93
|
treenode/views/crud.py,sha256=RI5rdyD4hZTszjZFThByxi_lkAeJlqbDCXFkD8iyzKE,7424
|
93
94
|
treenode/views/search.py,sha256=c_GyooT3jyoNa96bBxfoWruRN1wIw-ZGYvwGKkGojTs,1501
|
94
|
-
django_fast_treenode-3.2.
|
95
|
-
django_fast_treenode-3.2.
|
96
|
-
django_fast_treenode-3.2.
|
97
|
-
django_fast_treenode-3.2.
|
95
|
+
django_fast_treenode-3.2.2.dist-info/METADATA,sha256=Wa6Z3_h5nGmUHsqGAcNPCwlAtwLsEUDsPXgOWn3hdmw,10377
|
96
|
+
django_fast_treenode-3.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
97
|
+
django_fast_treenode-3.2.2.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
98
|
+
django_fast_treenode-3.2.2.dist-info/RECORD,,
|
treenode/managers/tasks.py
CHANGED
@@ -137,7 +137,25 @@ class TreeTaskQueue:
|
|
137
137
|
if not merged:
|
138
138
|
result_set.add(current)
|
139
139
|
|
140
|
-
|
140
|
+
if not result_set:
|
141
|
+
return []
|
142
|
+
|
143
|
+
depth_lookup = {}
|
144
|
+
if result_set:
|
145
|
+
with connection.cursor() as cursor:
|
146
|
+
format_ids = ', '.join(['%s'] * len(result_set))
|
147
|
+
sql = f"""
|
148
|
+
SELECT id, _depth
|
149
|
+
FROM {self.model._meta.db_table}
|
150
|
+
WHERE id IN ({format_ids})
|
151
|
+
"""
|
152
|
+
cursor.execute(sql, list(result_set))
|
153
|
+
for node_id, depth in cursor.fetchall():
|
154
|
+
depth_lookup[node_id] = depth
|
155
|
+
|
156
|
+
|
157
|
+
ordered_pks = sorted(result_set, key=lambda pk: (depth_lookup.get(pk, 0), pk))
|
158
|
+
return [{"mode": "update", "parent_id": pk} for pk in ordered_pks]
|
141
159
|
|
142
160
|
def _get_root_ids(self):
|
143
161
|
"""Return root node IDs."""
|
@@ -7,6 +7,7 @@ from .family import TreeNodeFamilyMixin
|
|
7
7
|
from .logical import TreeNodeLogicalMixin
|
8
8
|
from .node import TreeNodeNodeMixin
|
9
9
|
from .properties import TreeNodePropertiesMixin
|
10
|
+
from .search import TreeNodeSearchMixin
|
10
11
|
from .roots import TreeNodeRootsMixin
|
11
12
|
from .siblings import TreeNodeSiblingsMixin
|
12
13
|
from .tree import TreeNodeTreeMixin
|
@@ -16,7 +17,8 @@ from .update import RawSQLMixin
|
|
16
17
|
__all__ = [
|
17
18
|
"TreeNodeAncestorsMixin", "TreeNodeChildrenMixin", "TreeNodeFamilyMixin",
|
18
19
|
"TreeNodeDescendantsMixin", "TreeNodeLogicalMixin", "TreeNodeNodeMixin",
|
19
|
-
"
|
20
|
+
"TreeNodeSearchMixin", "TreeNodePropertiesMixin", "TreeNodeRootsMixin",
|
21
|
+
"TreeNodeSiblingsMixin", "TreeNodeTreeMixin", "RawSQLMixin"
|
20
22
|
"TreeNodeTreeMixin", "RawSQLMixin"
|
21
23
|
]
|
22
24
|
|
treenode/models/mixins/roots.py
CHANGED
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Search Mixin
|
4
|
+
|
5
|
+
Version: 3.2.1
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models
|
11
|
+
|
12
|
+
|
13
|
+
class TreeNodeSearchMixin(models.Model):
|
14
|
+
"""Mixin that provides helper methods for quick node lookup."""
|
15
|
+
|
16
|
+
class Meta:
|
17
|
+
"""Mixin Meta Class."""
|
18
|
+
abstract = True
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def find_by_path(cls, path, attr="id", delimiter="/"):
|
22
|
+
"""Return a node referenced by breadcrumbs path.
|
23
|
+
|
24
|
+
The ``path`` argument may be a string or an iterable of breadcrumb
|
25
|
+
values previously produced by :py:meth:`get_breadcrumbs`.
|
26
|
+
If the path cannot be resolved, ``None`` is returned.
|
27
|
+
"""
|
28
|
+
if path is None:
|
29
|
+
return None
|
30
|
+
|
31
|
+
if not isinstance(path, (list, tuple)):
|
32
|
+
path = [p for p in str(path).strip(delimiter).split(delimiter) if p]
|
33
|
+
|
34
|
+
parent = None
|
35
|
+
for token in path:
|
36
|
+
lookup = {attr: token}
|
37
|
+
if parent is None:
|
38
|
+
qs = cls.objects.filter(parent_id__isnull=True, **lookup)
|
39
|
+
else:
|
40
|
+
qs = cls.objects.filter(parent=parent, **lookup)
|
41
|
+
parent = qs.first()
|
42
|
+
if parent is None:
|
43
|
+
return None
|
44
|
+
return parent
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def find_in_subtree(cls, parent, value, attr="id"):
|
48
|
+
"""Search ``parent`` descendants for attribute ``attr`` equal to ``value``."""
|
49
|
+
if parent is None:
|
50
|
+
return None
|
51
|
+
prefix = parent.get_order() + "."
|
52
|
+
return cls.objects.filter(_path__startswith=prefix, **{attr: value}).first()
|
53
|
+
|
54
|
+
|
55
|
+
# The End
|
treenode/models/models.py
CHANGED
@@ -45,8 +45,9 @@ class TreeNodeModel(
|
|
45
45
|
mx.TreeNodeAncestorsMixin, mx.TreeNodeChildrenMixin,
|
46
46
|
mx.TreeNodeFamilyMixin, mx.TreeNodeDescendantsMixin,
|
47
47
|
mx.TreeNodeLogicalMixin, mx.TreeNodeNodeMixin,
|
48
|
-
mx.
|
49
|
-
mx.
|
48
|
+
mx.TreeNodeSearchMixin, mx.TreeNodePropertiesMixin,
|
49
|
+
mx.TreeNodeRootsMixin, mx.TreeNodeSiblingsMixin,
|
50
|
+
mx.TreeNodeTreeMixin, mx.RawSQLMixin,
|
50
51
|
models.Model, metaclass=TreeNodeModelBase):
|
51
52
|
"""
|
52
53
|
Abstract tree node model.
|
@@ -186,6 +187,7 @@ class TreeNodeModel(
|
|
186
187
|
is_new = False
|
187
188
|
is_shift = False
|
188
189
|
is_move = False
|
190
|
+
sorting_changed = False
|
189
191
|
|
190
192
|
if self.pk:
|
191
193
|
state = self.get_db_state()
|
@@ -194,6 +196,14 @@ class TreeNodeModel(
|
|
194
196
|
is_move = self.parent_id != state["parent_id"]
|
195
197
|
if is_move:
|
196
198
|
self._meta.model.tasks.add("update", state["parent_id"])
|
199
|
+
|
200
|
+
if self.sorting_field != "priority":
|
201
|
+
old_value = self.__class__.objects.filter(
|
202
|
+
pk=self.pk
|
203
|
+
).values_list(self.sorting_field, flat=True).first()
|
204
|
+
sorting_changed = old_value != getattr(
|
205
|
+
self, self.sorting_field
|
206
|
+
)
|
197
207
|
else:
|
198
208
|
logger.error(
|
199
209
|
"TreeNodeModel save error: object with pk %s not found in DB",
|
@@ -231,12 +241,14 @@ class TreeNodeModel(
|
|
231
241
|
pass
|
232
242
|
"""
|
233
243
|
|
234
|
-
if is_new or is_move or is_shift:
|
244
|
+
if is_new or is_move or is_shift or sorting_changed:
|
235
245
|
# Step 1: Shift siblings
|
236
246
|
if (is_new or is_move) and (self.priority is not None):
|
237
247
|
self._shift_siblings_forward()
|
238
248
|
# Step 2: Update paths for the new parent -> sqlq
|
239
|
-
self.
|
249
|
+
path_ids = self.query.get_relative_pks(objects="ancestors", include_self=True)
|
250
|
+
update_root_id = path_ids[0] if path_ids else self.parent_id
|
251
|
+
self._meta.model.tasks.add("update", update_root_id)
|
240
252
|
# Step 3: Clear model cache
|
241
253
|
self.clear_cache()
|
242
254
|
|
@@ -245,7 +257,7 @@ class TreeNodeModel(
|
|
245
257
|
disable_signals(post_save, model)):
|
246
258
|
super().save(*args, **kwargs)
|
247
259
|
|
248
|
-
if is_new or is_move or is_shift:
|
260
|
+
if is_new or is_move or is_shift or sorting_changed:
|
249
261
|
# Run sql
|
250
262
|
# self.sqlq.flush()
|
251
263
|
setattr(model, 'is_dry', True)
|
treenode/utils/db/compiler.py
CHANGED
@@ -26,88 +26,47 @@ class TreePathCompiler:
|
|
26
26
|
|
27
27
|
@classmethod
|
28
28
|
def update_path(cls, model, parent_id=None):
|
29
|
-
"""
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
row_number_expr = f"ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
|
50
|
-
hex_expr = SQLCompat.to_hex(row_number_expr)
|
51
|
-
lpad_expr = SQLCompat.lpad(hex_expr, SEGMENT_LENGTH, "'0'")
|
29
|
+
"""Rebuild subtree using BFS so parents update before children."""
|
30
|
+
table = model._meta.db_table
|
31
|
+
sort_field = model.sorting_field
|
32
|
+
|
33
|
+
def fetch_children(pid):
|
34
|
+
if pid is None:
|
35
|
+
where = "parent_id IS NULL"
|
36
|
+
params = []
|
37
|
+
else:
|
38
|
+
where = "parent_id = %s"
|
39
|
+
params = [pid]
|
40
|
+
with connection.cursor() as cursor:
|
41
|
+
cursor.execute(
|
42
|
+
f"SELECT id FROM {table} WHERE {where} ORDER BY {sort_field}, id",
|
43
|
+
params,
|
44
|
+
)
|
45
|
+
return [row[0] for row in cursor.fetchall()]
|
46
|
+
|
47
|
+
queue = []
|
52
48
|
|
53
49
|
if parent_id is None:
|
54
|
-
|
55
|
-
|
56
|
-
SELECT
|
57
|
-
c.id,
|
58
|
-
c.parent_id,
|
59
|
-
{row_number_expr} AS new_priority,
|
60
|
-
{new_path_expr} AS new_path,
|
61
|
-
0 AS new_depth
|
62
|
-
FROM {qname} AS c
|
63
|
-
WHERE c.parent_id IS NULL
|
64
|
-
"""
|
65
|
-
params = []
|
50
|
+
for idx, node_id in enumerate(fetch_children(None)):
|
51
|
+
queue.append((node_id, "", 0, idx))
|
66
52
|
else:
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
recursive_hex_expr, SEGMENT_LENGTH, "'0'")
|
85
|
-
recursive_path_expr = SQLCompat.concat(
|
86
|
-
"t.new_path", "'.'", recursive_lpad_expr)
|
87
|
-
|
88
|
-
recursive_sql = f"""
|
89
|
-
SELECT
|
90
|
-
c.id,
|
91
|
-
c.parent_id,
|
92
|
-
{recursive_row_number_expr} AS new_priority,
|
93
|
-
{recursive_path_expr} AS new_path,
|
94
|
-
t.new_depth + 1 AS new_depth
|
95
|
-
FROM {qname} c
|
96
|
-
JOIN tree_cte t ON c.parent_id = t.id
|
97
|
-
"""
|
98
|
-
|
99
|
-
final_sql = SQLCompat.update_from(
|
100
|
-
db_table=db_table,
|
101
|
-
cte_header=cte_header,
|
102
|
-
base_sql=base_sql,
|
103
|
-
recursive_sql=recursive_sql,
|
104
|
-
update_fields=["priority", "_path", "_depth"]
|
105
|
-
)
|
106
|
-
|
107
|
-
with connection.cursor() as cursor:
|
108
|
-
# Make params read-only
|
109
|
-
params = tuple(params)
|
110
|
-
cursor.execute(final_sql, params)
|
53
|
+
parent_data = model.objects.filter(pk=parent_id).values("_path", "_depth").first()
|
54
|
+
parent_path = parent_data["_path"] if parent_data else ""
|
55
|
+
depth = (parent_data["_depth"] + 1) if parent_data else 0
|
56
|
+
for idx, node_id in enumerate(fetch_children(parent_id)):
|
57
|
+
queue.append((node_id, parent_path, depth, idx))
|
58
|
+
|
59
|
+
while queue:
|
60
|
+
node_id, base_path, depth, index = queue.pop(0)
|
61
|
+
segment = f"{index:0{SEGMENT_LENGTH}X}"
|
62
|
+
path = segment if base_path == "" else f"{base_path}.{segment}"
|
63
|
+
with connection.cursor() as cursor:
|
64
|
+
cursor.execute(
|
65
|
+
f"UPDATE {table} SET priority=%s, _path=%s, _depth=%s WHERE id=%s",
|
66
|
+
[index, path, depth, node_id],
|
67
|
+
)
|
68
|
+
for child_idx, child_id in enumerate(fetch_children(node_id)):
|
69
|
+
queue.append((child_id, path, depth + 1, child_idx))
|
111
70
|
|
112
71
|
|
113
72
|
# The End
|
treenode/version.py
CHANGED
File without changes
|
{django_fast_treenode-3.2.0.dist-info → django_fast_treenode-3.2.2.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
File without changes
|