django-fast-treenode 3.0.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-fast-treenode
3
- Version: 3.0.1
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,4 +1,4 @@
1
- django_fast_treenode-3.0.1.dist-info/licenses/LICENSE,sha256=T0evsb7y-63fg18ovdNSx3wwWWRwyluQvN9J4zFSvfE,1093
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
3
  treenode/apps.py,sha256=QlwjNDM9rkUoWB8Vm8-OkS6lNx0-aTByuGZlu9wrQMs,1832
4
4
  treenode/cache.py,sha256=2jUiiecfFxwB7QFukpU4u0FnDzGH6hNRfo6KAYvs6vM,8447
@@ -7,8 +7,8 @@ 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=syIkHJEd3wcrPMwV3J-wvH-4jqj7npbU__LrW9HHGFM,222
11
- treenode/widgets.py,sha256=VItPvN9XgaSRI_MZjKEmtaHDJcn2bDIQIppwKjXmYQM,4017
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=rO-zTaIj9EQt1jrQF0lbAQeGgdvHs29gpKR6mfUX2d4,9237
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=2iL3ofbr4KjCeBId7gotDBBLUU8Z2FvLMHtPjPYUUUc,1972
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=jvrPE26dDDaPzcMqpQPx3JWtjKrgSyI5F8aa4-2RRzc,3854
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=tXFgaVy0IavdJ6FAMW7kKW0JyEAm-z_qka8XGWzzM6Q,1649
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=fWuSKCj_m0tqqDWTCscX0r2Kh34-cMunm5pFdEQuu_A,6267
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.1.dist-info/METADATA,sha256=BTFwZ8cmmk_97wYl0eLsnXpQLbF8Si_JaG5DaI736rU,10245
88
- django_fast_treenode-3.0.1.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
89
- django_fast_treenode-3.0.1.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
90
- django_fast_treenode-3.0.1.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.0)
2
+ Generator: setuptools (80.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
+
@@ -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
- from_path = path + '.'
36
- to_path = path + '/'
37
- options = {'_path__gte': from_path, '_path__lt': to_path}
38
- if depth:
39
- options["_depth__lt"] = depth
40
- queryset = self._meta.model.objects.filter(**options)
41
-
42
- if include_self:
43
- return self._meta.model.objects.filter(pk=self.pk) | queryset
44
- else:
45
- return queryset
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):
@@ -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.0.0
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: D501
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 {db_table} AS c
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 {db_table} c
74
- JOIN {db_table} p ON c.parent_id = p.id
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" # noqa: D501
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(recursive_hex_expr, SEGMENT_LENGTH, "'0'") # noqa: D501
82
- recursive_path_expr = SQLCompat.concat("t.new_path", "'.'", recursive_lpad_expr) # noqa: D501
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 {db_table} c
95
+ FROM {qname} c
92
96
  JOIN tree_cte t ON c.parent_id = t.id
93
97
  """
94
98
 
95
- final_sql = f"""
96
- WITH RECURSIVE tree_cte {cte_header} AS (
97
- {base_sql}
98
- UNION ALL
99
- {recursive_sql}
100
- )
101
- UPDATE {db_table} AS orig
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
- cursor.execute(final_sql.format(sort_expr=sort_expr), params)
108
+ # Make params read-only
109
+ params = tuple(params)
110
+ cursor.execute(final_sql, params)
112
111
 
113
112
 
114
113
  # The End
@@ -17,11 +17,12 @@ Instead of LPAD(...)
17
17
  old: LPAD(...)
18
18
  new: SQLCompat.lpad(...)
19
19
 
20
- Version: 3.0.0
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 (f"substr(replace(hex(zeroblob({length})), '00', {char}), "
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
@@ -4,10 +4,9 @@ TreeNode Version Module
4
4
 
5
5
  This module defines the current version of the TreeNode package.
6
6
 
7
- Version: 3.0.1
7
+ Version: 3.0.2
8
8
  Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
-
13
- __version__ = '3.0.1'
12
+ __version__ = '3.0.2'
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 Exception as e:
132
- return HttpResponseBadRequest(str(e))
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
- return HttpResponseNotFound("Node not found.")
160
- except Exception as e:
161
- return HttpResponseBadRequest(str(e))
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 as e:
206
- return HttpResponseBadRequest(str(e))
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