django-fast-treenode 3.0.0__py3-none-any.whl → 3.0.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.0.0.dist-info → django_fast_treenode-3.0.2.dist-info}/METADATA +2 -1
- {django_fast_treenode-3.0.0.dist-info → django_fast_treenode-3.0.2.dist-info}/RECORD +13 -13
- {django_fast_treenode-3.0.0.dist-info → django_fast_treenode-3.0.2.dist-info}/WHEEL +1 -1
- {django_fast_treenode-3.0.0.dist-info → django_fast_treenode-3.0.2.dist-info}/licenses/LICENSE +1 -0
- treenode/apps.py +1 -1
- treenode/managers/queries.py +43 -0
- treenode/models/mixins/descendants.py +17 -11
- treenode/utils/db/compiler.py +24 -25
- treenode/utils/db/sqlcompat.py +91 -6
- treenode/version.py +2 -3
- treenode/views/crud.py +31 -10
- treenode/widgets.py +2 -0
- {django_fast_treenode-3.0.0.dist-info → django_fast_treenode-3.0.2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 3.0.
|
3
|
+
Version: 3.0.2
|
4
4
|
Summary: Treenode Framework for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
6
|
Author: Timur Kady
|
@@ -27,6 +27,7 @@ License: MIT License
|
|
27
27
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
28
28
|
SOFTWARE.
|
29
29
|
|
30
|
+
|
30
31
|
Project-URL: Homepage, https://github.com/TimurKady/django-fast-treenode
|
31
32
|
Project-URL: Documentation, https://django-fast-treenode.readthedocs.io/
|
32
33
|
Project-URL: Source, https://github.com/TimurKady/django-fast-treenode
|
@@ -1,14 +1,14 @@
|
|
1
|
-
django_fast_treenode-3.0.
|
1
|
+
django_fast_treenode-3.0.2.dist-info/licenses/LICENSE,sha256=SSYqS84FCnAW7tAxmjBKU8qAa8Jv4VGPuSSGeHwWtJE,1095
|
2
2
|
treenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
-
treenode/apps.py,sha256=
|
3
|
+
treenode/apps.py,sha256=QlwjNDM9rkUoWB8Vm8-OkS6lNx0-aTByuGZlu9wrQMs,1832
|
4
4
|
treenode/cache.py,sha256=2jUiiecfFxwB7QFukpU4u0FnDzGH6hNRfo6KAYvs6vM,8447
|
5
5
|
treenode/forms.py,sha256=V-upmbYSW1BbuXdSBGExHxw_j5TTUheaHvreK9tSGTE,3155
|
6
6
|
treenode/settings.py,sha256=oSkcKXNVd28HXrlWZIH2VYinCMq-UdCDlX4KD0Qc_Xk,631
|
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=
|
11
|
-
treenode/widgets.py,sha256=
|
10
|
+
treenode/version.py,sha256=TN9nZNpy8llm9fqIJAWYPQd8TagW5fxnW0I69gWD-oY,220
|
11
|
+
treenode/widgets.py,sha256=61ed16bVqb1_R97jekDrbKS5pDVK4nzXSDwL3CDBYEk,4075
|
12
12
|
treenode/admin/__init__.py,sha256=XNEYHdF5lKb0vpdlVxdR2fxj5oUgzyx1YyCwsv0gxHw,100
|
13
13
|
treenode/admin/admin.py,sha256=xhf6tT5Ydn6i_upUDA1A6TN_muNndCko8gzM_fNIex4,7707
|
14
14
|
treenode/admin/changelist.py,sha256=KUYS9MaR8Ck_1xmMqupobxWKarrJEqmHuEG32CL01Bo,1662
|
@@ -17,7 +17,7 @@ treenode/admin/importer.py,sha256=hK3D-1DZcoowGblRluGzng3n5Bf__hMsbNaIGXRpRdg,62
|
|
17
17
|
treenode/admin/mixin.py,sha256=yXcSpBEfoMYT7tuAbHhGbuqlVQkCR5RizW2bNWJ0QNM,10639
|
18
18
|
treenode/managers/__init__.py,sha256=c7F9Ku9489Hv6lTpUY2nbyBlWFCXBWAkNBm4xTKcjL8,186
|
19
19
|
treenode/managers/managers.py,sha256=8OaFxtajyR1d7-UHyiUbifMBEF9cjfHTIEYPkYUWmt0,7166
|
20
|
-
treenode/managers/queries.py,sha256=
|
20
|
+
treenode/managers/queries.py,sha256=Kepax8SDn7G5tOlPRWBCp5Oyp49O5iMITCMBoNCm_Ak,10655
|
21
21
|
treenode/managers/tasks.py,sha256=gqZfqeOrdPtTJZ1q83aCFh6htfxP0XMTOkd1KWL9PVU,5227
|
22
22
|
treenode/models/__init__.py,sha256=iR4ksCKoayvkIWWgGk6OUGHZC3D0mzAtgdBcS2vQPBw,188
|
23
23
|
treenode/models/decorators.py,sha256=N2dcnWqSCiEXDcYCf0zVijrbGUC8kYlqOLi_GKFmECU,1457
|
@@ -26,7 +26,7 @@ treenode/models/models.py,sha256=4S4SFFSxuIlY1DjuGW2T_bJaOos9chp1cuJ5k4_4gPg,122
|
|
26
26
|
treenode/models/mixins/__init__.py,sha256=aALVKMGAWbgMAeKWS6s-NF3L5FmRX96mQxtpthOX-Ec,805
|
27
27
|
treenode/models/mixins/ancestors.py,sha256=9g-0nPHoiF_SX2kN4uDLdbWyw-TDCz1YqxLJngwTZOQ,1971
|
28
28
|
treenode/models/mixins/children.py,sha256=H9iMqgucSmwLX-3O3QUj1a2PUQTmmWZ4GPPjRZ9a5E4,2399
|
29
|
-
treenode/models/mixins/descendants.py,sha256=
|
29
|
+
treenode/models/mixins/descendants.py,sha256=RKowr29JUUO3E_UDvbLbMLhbn5IUkY2eh3AXfU8XSE8,2165
|
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=8kqYdFTyZK5WsSxguuX3jBo1nZehDmwIBjm38S6QJw4,8576
|
@@ -72,19 +72,19 @@ treenode/templates/admin/treenode_rows.html,sha256=S1XtZXWMUdfgjDoKG7OJwZg81a_5I
|
|
72
72
|
treenode/templates/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
|
73
73
|
treenode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
74
74
|
treenode/utils/db/__init__.py,sha256=RwicAcJSI1nhIPWLdT7j9TFsgOc9834VDn9lVn54GlY,247
|
75
|
-
treenode/utils/db/compiler.py,sha256=
|
75
|
+
treenode/utils/db/compiler.py,sha256=PgD9ybS5H8OUHw1gkFBQHhnrf5HiCx8QXUMRhydwh7o,3824
|
76
76
|
treenode/utils/db/db_vendor.py,sha256=4SyEHl51jVCDB3is4omHCf2bTB_QV3RvemUQYxJP5m0,930
|
77
77
|
treenode/utils/db/service.py,sha256=PF85Yhz2xUWFFCzpLYotmiNTZXXEH61rhswslSxEUds,2640
|
78
|
-
treenode/utils/db/sqlcompat.py,sha256=
|
78
|
+
treenode/utils/db/sqlcompat.py,sha256=K71ggkKIvpdTtHQ6Y4qcbo6cj2eYiEfy6DlVBr8Po1E,4460
|
79
79
|
treenode/utils/db/sqlquery.py,sha256=KXcfKbbaBF-D134H_2DiPQtjedR79SJNXPJc0msZYEc,1938
|
80
80
|
treenode/views/__init__.py,sha256=ppxbBx51TUaKstJFpAd_DTmbKjbZGmVMLNYSpgUKnd0,111
|
81
81
|
treenode/views/autoapi.py,sha256=o75e8IFsogbhZN_rbx3BKVnoruD96nWelnC5UzOqUDw,3628
|
82
82
|
treenode/views/autocomplete.py,sha256=Z7cBnC4Ihdyxm8zlbnG6CkZdVkM3TOTWRpw5mdhaIVA,1469
|
83
83
|
treenode/views/children.py,sha256=bygXaEBExxG3zIPL34_PYHLFFIqlQU2naqPIlyQ6e-s,1152
|
84
84
|
treenode/views/common.py,sha256=mrmr40R91XVbMWcz5GZT-OjpnQ87F7XQZxu1W6rqpqI,617
|
85
|
-
treenode/views/crud.py,sha256=
|
85
|
+
treenode/views/crud.py,sha256=RI5rdyD4hZTszjZFThByxi_lkAeJlqbDCXFkD8iyzKE,7424
|
86
86
|
treenode/views/search.py,sha256=c_GyooT3jyoNa96bBxfoWruRN1wIw-ZGYvwGKkGojTs,1501
|
87
|
-
django_fast_treenode-3.0.
|
88
|
-
django_fast_treenode-3.0.
|
89
|
-
django_fast_treenode-3.0.
|
90
|
-
django_fast_treenode-3.0.
|
87
|
+
django_fast_treenode-3.0.2.dist-info/METADATA,sha256=Q5sQAXL8xAh-5T9r36G5IsxzvFiQJhK0UxG8xm-xC4o,10255
|
88
|
+
django_fast_treenode-3.0.2.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
|
89
|
+
django_fast_treenode-3.0.2.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
90
|
+
django_fast_treenode-3.0.2.dist-info/RECORD,,
|
treenode/apps.py
CHANGED
treenode/managers/queries.py
CHANGED
@@ -84,6 +84,48 @@ class TreeQuery:
|
|
84
84
|
sql = self.order_by(sql1, "priority")
|
85
85
|
return sql, params1
|
86
86
|
|
87
|
+
def get_descendants(self, include_self, depth):
|
88
|
+
"""
|
89
|
+
Build SQL for the 'descendants'.
|
90
|
+
|
91
|
+
Relationship using startswith-like logic.
|
92
|
+
Avoids locale-dependent string comparison issues by relying on
|
93
|
+
_path LIKE 'xxx.%'.
|
94
|
+
"""
|
95
|
+
like_pattern = self.node._path + '.%' # emulate startswith
|
96
|
+
|
97
|
+
base_sql = f"""
|
98
|
+
SELECT id, _depth, priority
|
99
|
+
FROM {self.db_table}
|
100
|
+
WHERE _path LIKE %s
|
101
|
+
"""
|
102
|
+
params = [like_pattern]
|
103
|
+
|
104
|
+
if depth is not None:
|
105
|
+
depth_val = getattr(self.node, "_depth", None)
|
106
|
+
if depth_val is None:
|
107
|
+
depth_val = type(self.node).objects.values_list(
|
108
|
+
"_depth", flat=True).get(pk=self.node.pk)
|
109
|
+
base_sql += " AND _depth <= %s"
|
110
|
+
params.append(depth_val + depth)
|
111
|
+
|
112
|
+
if include_self:
|
113
|
+
sql_self = f"""
|
114
|
+
SELECT id, _depth, priority
|
115
|
+
FROM {self.db_table}
|
116
|
+
WHERE id = %s
|
117
|
+
"""
|
118
|
+
union_sql, union_params = self.wrap_union_all([
|
119
|
+
(base_sql, params),
|
120
|
+
(sql_self, [self.node.pk])
|
121
|
+
])
|
122
|
+
else:
|
123
|
+
union_sql, union_params = base_sql, params
|
124
|
+
|
125
|
+
union_sql = self.order_by(union_sql, "_depth, priority")
|
126
|
+
return union_sql, union_params
|
127
|
+
|
128
|
+
'''
|
87
129
|
def get_descendants(self, include_self, depth):
|
88
130
|
"""
|
89
131
|
Build SQL for the 'descendants' relationship.
|
@@ -113,6 +155,7 @@ class TreeQuery:
|
|
113
155
|
|
114
156
|
union_sql = self.order_by(union_sql, "_depth, priority")
|
115
157
|
return union_sql, union_params
|
158
|
+
'''
|
116
159
|
|
117
160
|
def get_ancestors(self, include_self):
|
118
161
|
"""
|
@@ -32,17 +32,23 @@ class TreeNodeDescendantsMixin(models.Model):
|
|
32
32
|
def get_descendants_queryset(self, include_self=False, depth=None):
|
33
33
|
"""Get the descendants queryset."""
|
34
34
|
path = self.get_order() # calls refresh and gets the current _path
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
35
|
+
|
36
|
+
# from_path = path + '.'
|
37
|
+
# to_path = path + '/'
|
38
|
+
|
39
|
+
# options = {'_path__gte': from_path, '_path__lt': to_path}
|
40
|
+
# if depth:
|
41
|
+
# options["_depth__lt"] = depth
|
42
|
+
# queryset = self._meta.model.objects.filter(**options)
|
43
|
+
# if include_self:
|
44
|
+
# return self._meta.model.objects.filter(pk=self.pk) | queryset
|
45
|
+
# else:
|
46
|
+
# return queryset
|
47
|
+
|
48
|
+
suffix = "" if include_self else '.'
|
49
|
+
path += suffix
|
50
|
+
queryset = self._meta.model.objects.filter(_path__startswith=path)
|
51
|
+
return queryset
|
46
52
|
|
47
53
|
@cached_method
|
48
54
|
def get_descendants_pks(self, include_self=False, depth=None):
|
treenode/utils/db/compiler.py
CHANGED
@@ -5,7 +5,7 @@ Tree update task compiler class.
|
|
5
5
|
Compiles tasks to low-level SQL to update the materialized path (_path), depth
|
6
6
|
(_depth), and node order (priority) when they are shifted or moved.
|
7
7
|
|
8
|
-
Version: 3.
|
8
|
+
Version: 3.1.0
|
9
9
|
Author: Timur Kady
|
10
10
|
Email: timurkady@yandex.com
|
11
11
|
"""
|
@@ -34,9 +34,11 @@ class TreePathCompiler:
|
|
34
34
|
_depth) are recalculated.
|
35
35
|
"""
|
36
36
|
db_table = model._meta.db_table
|
37
|
+
# Will eliminate the risk if the user names the model order or user.
|
38
|
+
qname = connection.ops.quote_name(db_table)
|
37
39
|
|
38
40
|
sorting_field = model.sorting_field
|
39
|
-
sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa:
|
41
|
+
sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa: D5017
|
40
42
|
sort_expr = ", ".join([
|
41
43
|
f"c.{field}" if "." not in field else field
|
42
44
|
for field in sorting_fields
|
@@ -44,7 +46,7 @@ class TreePathCompiler:
|
|
44
46
|
|
45
47
|
cte_header = "(id, parent_id, new_priority, new_path, new_depth)"
|
46
48
|
|
47
|
-
row_number_expr = "ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
|
49
|
+
row_number_expr = f"ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
|
48
50
|
hex_expr = SQLCompat.to_hex(row_number_expr)
|
49
51
|
lpad_expr = SQLCompat.lpad(hex_expr, SEGMENT_LENGTH, "'0'")
|
50
52
|
|
@@ -57,7 +59,7 @@ class TreePathCompiler:
|
|
57
59
|
{row_number_expr} AS new_priority,
|
58
60
|
{new_path_expr} AS new_path,
|
59
61
|
0 AS new_depth
|
60
|
-
FROM {
|
62
|
+
FROM {qname} AS c
|
61
63
|
WHERE c.parent_id IS NULL
|
62
64
|
"""
|
63
65
|
params = []
|
@@ -70,16 +72,18 @@ class TreePathCompiler:
|
|
70
72
|
{row_number_expr} AS new_priority,
|
71
73
|
{path_expr} AS new_path,
|
72
74
|
p._depth + 1 AS new_depth
|
73
|
-
FROM {
|
74
|
-
JOIN {
|
75
|
+
FROM {qname} c
|
76
|
+
JOIN {qname} p ON c.parent_id = p.id
|
75
77
|
WHERE p.id = %s
|
76
78
|
"""
|
77
79
|
params = [parent_id]
|
78
80
|
|
79
|
-
recursive_row_number_expr = "ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1"
|
81
|
+
recursive_row_number_expr = f"ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1"
|
80
82
|
recursive_hex_expr = SQLCompat.to_hex(recursive_row_number_expr)
|
81
|
-
recursive_lpad_expr = SQLCompat.lpad(
|
82
|
-
|
83
|
+
recursive_lpad_expr = SQLCompat.lpad(
|
84
|
+
recursive_hex_expr, SEGMENT_LENGTH, "'0'")
|
85
|
+
recursive_path_expr = SQLCompat.concat(
|
86
|
+
"t.new_path", "'.'", recursive_lpad_expr)
|
83
87
|
|
84
88
|
recursive_sql = f"""
|
85
89
|
SELECT
|
@@ -88,27 +92,22 @@ class TreePathCompiler:
|
|
88
92
|
{recursive_row_number_expr} AS new_priority,
|
89
93
|
{recursive_path_expr} AS new_path,
|
90
94
|
t.new_depth + 1 AS new_depth
|
91
|
-
FROM {
|
95
|
+
FROM {qname} c
|
92
96
|
JOIN tree_cte t ON c.parent_id = t.id
|
93
97
|
"""
|
94
98
|
|
95
|
-
final_sql =
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
SET
|
103
|
-
priority = t.new_priority,
|
104
|
-
_path = t.new_path,
|
105
|
-
_depth = t.new_depth
|
106
|
-
FROM tree_cte t
|
107
|
-
WHERE orig.id = t.id;
|
108
|
-
"""
|
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
|
+
)
|
109
106
|
|
110
107
|
with connection.cursor() as cursor:
|
111
|
-
|
108
|
+
# Make params read-only
|
109
|
+
params = tuple(params)
|
110
|
+
cursor.execute(final_sql, params)
|
112
111
|
|
113
112
|
|
114
113
|
# The End
|
treenode/utils/db/sqlcompat.py
CHANGED
@@ -17,11 +17,12 @@ Instead of LPAD(...)
|
|
17
17
|
old: LPAD(...)
|
18
18
|
new: SQLCompat.lpad(...)
|
19
19
|
|
20
|
-
Version: 3.
|
20
|
+
Version: 3.1.0
|
21
21
|
Author: Timur Kady
|
22
22
|
Email: timurkady@yandex.com
|
23
23
|
"""
|
24
24
|
|
25
|
+
from django.db import connection
|
25
26
|
from .db_vendor import is_mysql, is_mariadb, is_sqlite, is_mssql
|
26
27
|
from ...settings import TREENODE_PAD_CHAR
|
27
28
|
|
@@ -42,19 +43,103 @@ class SQLCompat:
|
|
42
43
|
|
43
44
|
@staticmethod
|
44
45
|
def to_hex(value):
|
45
|
-
"""Convert integer to hexadecimal string."""
|
46
|
+
"""Convert integer to uppercase hexadecimal string."""
|
46
47
|
if is_sqlite():
|
47
|
-
return f"printf('%x', {value})"
|
48
|
+
return f"UPPER(printf('%x', {value}))"
|
49
|
+
elif is_mysql() or is_mariadb():
|
50
|
+
return f"UPPER(CONV({value}, 10, 16))"
|
48
51
|
else:
|
49
|
-
return f"TO_HEX({value})"
|
52
|
+
return f"UPPER(TO_HEX({value}))"
|
50
53
|
|
51
54
|
@staticmethod
|
52
55
|
def lpad(value, length, char=TREENODE_PAD_CHAR):
|
53
56
|
"""Pad string to the specified length."""
|
54
57
|
if is_sqlite():
|
55
|
-
return
|
56
|
-
f"1, {length} - length({value})) || {value}")
|
58
|
+
return f"printf('%0{length}s', {value})"
|
57
59
|
else:
|
58
60
|
return f"LPAD({value}, {length}, {char})"
|
59
61
|
|
62
|
+
@staticmethod
|
63
|
+
def update_from(db_table, cte_header, base_sql, recursive_sql, update_fields):
|
64
|
+
"""
|
65
|
+
Generate final SQL for updating via recursive CTE.
|
66
|
+
|
67
|
+
PostgreSQL uses UPDATE ... FROM.
|
68
|
+
Other engines use vendor-specific strategies.
|
69
|
+
"""
|
70
|
+
qt = connection.ops.quote_name(db_table)
|
71
|
+
def qf(f): return connection.ops.quote_name(f)
|
72
|
+
|
73
|
+
cte_alias = {
|
74
|
+
"priority": "new_priority",
|
75
|
+
"_path": "new_path",
|
76
|
+
"_depth": "new_depth",
|
77
|
+
}
|
78
|
+
|
79
|
+
if connection.vendor == "postgresql":
|
80
|
+
set_clause = ", ".join(
|
81
|
+
f"{qf(f)} = t.{cte_alias.get(f, f)}" for f in update_fields
|
82
|
+
)
|
83
|
+
return f"""
|
84
|
+
WITH RECURSIVE tree_cte {cte_header} AS (
|
85
|
+
{base_sql}
|
86
|
+
UNION ALL
|
87
|
+
{recursive_sql}
|
88
|
+
)
|
89
|
+
UPDATE {qt} AS orig
|
90
|
+
SET {set_clause}
|
91
|
+
FROM tree_cte t
|
92
|
+
WHERE orig.id = t.id;
|
93
|
+
"""
|
94
|
+
|
95
|
+
elif connection.vendor in {"microsoft", "mssql"}:
|
96
|
+
set_clause = ", ".join(
|
97
|
+
f"{qt}.{f} = t.{f}" for f in update_fields
|
98
|
+
)
|
99
|
+
return f"""
|
100
|
+
WITH tree_cte {cte_header} AS (
|
101
|
+
{base_sql}
|
102
|
+
UNION ALL
|
103
|
+
{recursive_sql}
|
104
|
+
)
|
105
|
+
UPDATE orig
|
106
|
+
SET {set_clause}
|
107
|
+
FROM {qt} AS orig
|
108
|
+
JOIN tree_cte t ON orig.id = t.id;
|
109
|
+
"""
|
110
|
+
|
111
|
+
elif connection.vendor == "oracle":
|
112
|
+
set_clause = ", ".join(
|
113
|
+
f"orig.{f} = t.{f}" for f in update_fields
|
114
|
+
)
|
115
|
+
return f"""
|
116
|
+
WITH tree_cte {cte_header} AS (
|
117
|
+
{base_sql}
|
118
|
+
UNION ALL
|
119
|
+
{recursive_sql}
|
120
|
+
)
|
121
|
+
MERGE INTO {qt} orig
|
122
|
+
USING tree_cte t
|
123
|
+
ON (orig.id = t.id)
|
124
|
+
WHEN MATCHED THEN UPDATE SET
|
125
|
+
{set_clause};
|
126
|
+
"""
|
127
|
+
|
128
|
+
else:
|
129
|
+
set_clause = ", ".join(
|
130
|
+
f"{qf(f)} = (SELECT t.{f} FROM tree_cte t WHERE t.id = {qt}.id)"
|
131
|
+
for f in update_fields
|
132
|
+
)
|
133
|
+
where_clause = f"id IN (SELECT id FROM tree_cte)"
|
134
|
+
return f"""
|
135
|
+
WITH RECURSIVE tree_cte {cte_header} AS (
|
136
|
+
{base_sql}
|
137
|
+
UNION ALL
|
138
|
+
{recursive_sql}
|
139
|
+
)
|
140
|
+
UPDATE {qt}
|
141
|
+
SET {set_clause}
|
142
|
+
WHERE {where_clause};
|
143
|
+
"""
|
144
|
+
|
60
145
|
# The End
|
treenode/version.py
CHANGED
treenode/views/crud.py
CHANGED
@@ -10,14 +10,17 @@ Email: timurkady@yandex.com
|
|
10
10
|
"""
|
11
11
|
|
12
12
|
import json
|
13
|
-
|
13
|
+
import logging
|
14
|
+
from django.core.exceptions import ValidationError
|
14
15
|
from django.forms.models import model_to_dict
|
15
16
|
from django.http import (
|
16
17
|
JsonResponse, HttpResponseBadRequest, HttpResponseNotFound,
|
17
18
|
)
|
18
|
-
|
19
|
+
from django.utils.translation import gettext_lazy as _
|
19
20
|
from django.views import View
|
20
21
|
|
22
|
+
logger = logging.getLogger("treenode.views.crud")
|
23
|
+
|
21
24
|
|
22
25
|
class TreeNodeBaseAPIView(View):
|
23
26
|
"""Simple API View for TreeNode-based models."""
|
@@ -128,8 +131,14 @@ class TreeNodeBaseAPIView(View):
|
|
128
131
|
obj.full_clean()
|
129
132
|
obj.save()
|
130
133
|
return JsonResponse(model_to_dict(obj), status=201)
|
131
|
-
except
|
132
|
-
|
134
|
+
except ValidationError as ve:
|
135
|
+
# give the client clear field errors
|
136
|
+
return JsonResponse({"errors": ve.message_dict}, status=400)
|
137
|
+
except Exception:
|
138
|
+
# log full information for development
|
139
|
+
logger.exception("Failed to create node")
|
140
|
+
return JsonResponse(
|
141
|
+
{"error": _("Failed to create node")}, status=400)
|
133
142
|
|
134
143
|
def put(self, request, pk):
|
135
144
|
"""
|
@@ -155,10 +164,16 @@ class TreeNodeBaseAPIView(View):
|
|
155
164
|
obj.full_clean()
|
156
165
|
obj.save()
|
157
166
|
return JsonResponse(model_to_dict(obj))
|
167
|
+
except ValidationError as ve:
|
168
|
+
return JsonResponse({"errors": ve.message_dict}, status=400)
|
158
169
|
except self.model.DoesNotExist:
|
159
|
-
|
160
|
-
|
161
|
-
|
170
|
+
# give the client clear field errors
|
171
|
+
return HttpResponseNotFound(_("Node not found (pk={pk})."))
|
172
|
+
except Exception:
|
173
|
+
# log full information for development
|
174
|
+
logger.exception("Error replacing node %s", pk)
|
175
|
+
return JsonResponse(
|
176
|
+
{"error": _("Failed to update node")}, status=400)
|
162
177
|
|
163
178
|
def patch(self, request, pk):
|
164
179
|
"""
|
@@ -200,10 +215,16 @@ class TreeNodeBaseAPIView(View):
|
|
200
215
|
"id": obj.pk,
|
201
216
|
"cascade": cascade
|
202
217
|
})
|
218
|
+
except ValidationError as ve:
|
219
|
+
# give the client clear field errors|
|
220
|
+
return JsonResponse({"errors": ve.message_dict}, status=400)
|
203
221
|
except self.model.DoesNotExist:
|
204
|
-
return HttpResponseNotFound("Node not found.")
|
205
|
-
except Exception
|
206
|
-
|
222
|
+
return HttpResponseNotFound(_("Node not found (pk={pk})."))
|
223
|
+
except Exception:
|
224
|
+
# log full information for development
|
225
|
+
logger.exception("Error deleting node %s", pk)
|
226
|
+
return JsonResponse(
|
227
|
+
{"error": _("Error deleting node")}, status=400)
|
207
228
|
|
208
229
|
|
209
230
|
# The End
|
treenode/widgets.py
CHANGED
@@ -21,6 +21,8 @@ from django.utils.translation import gettext_lazy as _
|
|
21
21
|
class TreeWidget(forms.Widget):
|
22
22
|
"""Custom widget for hierarchical tree selection."""
|
23
23
|
|
24
|
+
template_name = "django/forms/widgets/select.html"
|
25
|
+
|
24
26
|
class Media:
|
25
27
|
"""Meta class to define required CSS and JS files."""
|
26
28
|
|
File without changes
|