django-fast-treenode 2.0.10__py3-none-any.whl → 2.0.11__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.
treenode/models/proxy.py CHANGED
@@ -13,7 +13,7 @@ Features:
13
13
  - Provides a caching mechanism for performance optimization.
14
14
  - Includes methods for tree traversal, manipulation, and serialization.
15
15
 
16
- Version: 2.0.0
16
+ Version: 2.0.11
17
17
  Author: Timur Kady
18
18
  Email: timurkady@yandex.com
19
19
  """
@@ -131,14 +131,14 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
131
131
 
132
132
  def get_ancestors_queryset(self, include_self=True, depth=None):
133
133
  """Get the ancestors queryset (ordered from parent to root)."""
134
- return self.closure_model.get_ancestors_queryset(
135
- self, include_self, depth)
134
+ ancestors_pks = self.get_ancestors_pks(include_self, depth)
135
+ result = self._meta.model.objects.filter(pk__in=ancestors_pks)
136
+ return result
136
137
 
137
138
  def get_ancestors(self, include_self=True, depth=None):
138
139
  """Get a list with all ancestors (ordered from root to self/parent)."""
139
- return list(
140
- self.get_ancestors_queryset(include_self, depth).iterator()
141
- )
140
+ queryset = self.get_ancestors_queryset(include_self, depth)
141
+ return list(queryset.iterator())
142
142
 
143
143
  def get_ancestors_count(self, include_self=True, depth=None):
144
144
  """Get the ancestors count."""
@@ -146,8 +146,8 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
146
146
 
147
147
  def get_ancestors_pks(self, include_self=True, depth=None):
148
148
  """Get the ancestors pks list."""
149
- qs = self.get_ancestors_queryset(include_self, depth).only('pk')
150
- return [ch.pk for ch in qs] if qs else []
149
+ pks = self.closure_model.get_ancestors_pks(self, include_self, depth)
150
+ return pks
151
151
 
152
152
  # Children --------------------
153
153
 
@@ -172,15 +172,14 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
172
172
 
173
173
  def get_descendants_queryset(self, include_self=False, depth=None):
174
174
  """Get the descendants queryset."""
175
- return self.closure_model.get_descendants_queryset(
176
- self, include_self, depth
177
- )
175
+ descendants_pks = self.get_descendants_pks(include_self, depth)
176
+ result = self._meta.model.objects.filter(pk__in=descendants_pks)
177
+ return result
178
178
 
179
179
  def get_descendants(self, include_self=False, depth=None):
180
180
  """Get a list containing all descendants."""
181
- return list(
182
- self.get_descendants_queryset(include_self, depth).iterator()
183
- )
181
+ queryset = self.get_descendants_queryset(include_self, depth).iterator()
182
+ return list(queryset)
184
183
 
185
184
  def get_descendants_count(self, include_self=False, depth=None):
186
185
  """Get the descendants count."""
@@ -188,8 +187,8 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
188
187
 
189
188
  def get_descendants_pks(self, include_self=False, depth=None):
190
189
  """Get the descendants pks list."""
191
- qs = self.get_descendants_queryset(include_self, depth)
192
- return [ch.pk for ch in qs] if qs else []
190
+ pks = self.closure_model.get_descendants_pks(self, include_self, depth)
191
+ return pks
193
192
 
194
193
  # Siblings --------------------
195
194
 
@@ -216,9 +215,16 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
216
215
 
217
216
  # -----------------------------
218
217
 
219
- def get_breadcrumbs(self, attr=None):
218
+ def get_breadcrumbs(self, attr='pk'):
220
219
  """Get the breadcrumbs to current node (self, included)."""
221
- return self.closure_model.get_breadcrumbs(self, attr)
220
+ queryset = self.get_ancestors_queryset(include_self=True)
221
+
222
+ breadcrumbs = [
223
+ getattr(item, attr)
224
+ if hasattr(item, attr) else None
225
+ for item in queryset
226
+ ]
227
+ return breadcrumbs
222
228
 
223
229
  def get_depth(self):
224
230
  """Get the node depth (self, how many levels of descendants)."""
@@ -238,7 +244,7 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
238
244
 
239
245
  def get_order(self):
240
246
  """Return the materialized path."""
241
- path = self.closure_model.get_breadcrumbs(self, attr='tn_priority')
247
+ path = self.get_breadcrumbs(attr='tn_priority')
242
248
  segments = [to_base36(i).rjust(6, '0') for i in path]
243
249
  return ''.join(segments)
244
250
 
@@ -252,8 +258,18 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
252
258
 
253
259
  def get_path(self, prefix='', suffix='', delimiter='.', format_str=''):
254
260
  """Return Materialized Path of node."""
255
- path = self.closure_model.get_path(self, delimiter, format_str)
256
- return prefix+path+suffix
261
+ priorities = self.get_breadcrumbs(attr='tn_priority')
262
+ # Проверяем, что список не пуст
263
+ if not priorities or all(p is None for p in priorities):
264
+ return prefix + suffix
265
+
266
+ str_ = "{%s}" % format_str
267
+ path = delimiter.join([
268
+ str_.format(p)
269
+ for p in priorities
270
+ if p is not None
271
+ ])
272
+ return prefix + path + suffix
257
273
 
258
274
  @cached_method
259
275
  def get_parent(self):
@@ -369,7 +385,9 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
369
385
 
370
386
  # --- 2. Check mode _-------------------------------------------------
371
387
  # If the object already exists in the DB, we'll extract its old parent
372
- if not is_new:
388
+ if is_new:
389
+ force_insert = True
390
+ else:
373
391
  ql = model.objects.filter(pk=self.pk).values_list(
374
392
  'tn_parent',
375
393
  'tn_priority').first()
@@ -395,7 +413,8 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
395
413
 
396
414
  # If the parent has changed, we move it
397
415
  if (old_parent != self.tn_parent):
398
- closure_model.move_node(self)
416
+ subtree_nodes = self.get_descendants(include_self=True)
417
+ self.closure_model.move_node(subtree_nodes)
399
418
 
400
419
  # --- 5. Update siblings ---------------------------------------------
401
420
  if is_new or is_move:
@@ -582,7 +601,7 @@ class TreeNodeModel(models.Model, metaclass=TreeFactory):
582
601
  # Save changes
583
602
  model = self._meta.model
584
603
  with transaction.atomic():
585
- model.objects.bulk_update(sorted_siblings, ('tn_priority',), 1000)
604
+ model.objects.bulk_update(sorted_siblings, ('tn_priority',))
586
605
  super().save(update_fields=['tn_priority'])
587
606
  model.clear_cache()
588
607
 
@@ -1,7 +1,20 @@
1
1
  {% extends "admin/base_site.html" %}
2
+ {% load static %}
3
+
4
+ {% block extrahead %}
5
+ <link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}">
6
+ {% endblock %}
7
+
2
8
  {% block content %}
3
9
  <div class="module">
4
10
  <h2>Import Data</h2>
11
+
12
+ {% if message %}
13
+ <div class="successnote">
14
+ <p>{{ message }}</p>
15
+ </div>
16
+ {% endif %}
17
+
5
18
  {% if errors %}
6
19
  <div class="errornote">
7
20
  <p>Errors occurred while importing the data:</p>
@@ -12,16 +25,21 @@
12
25
  </ul>
13
26
  </div>
14
27
  {% endif %}
15
- <form method="post" enctype="multipart/form-data">
28
+
29
+ <form id="importForm" method="post" enctype="multipart/form-data" style="margin: 20px 0;">
16
30
  {% csrf_token %}
17
- <div>
18
- <label for="file">Choose file:</label>
19
- <input type="file" name="file" id="file" required>
20
- </div>
21
- <div class="form-group" style="margin-top: 35px;">
22
- <button type="submit" class="button">Import</button>
23
- <a href=".." class="button cancel">Cancel</a>
31
+ <fieldset class="module aligned">
32
+ <div class="form-row field-tn_parent">
33
+ <label for="file">Choose file:</label>
34
+ <input type="file" name="file" id="file" required>
35
+ </div>
36
+ </fieldset>
37
+
38
+ <div class="submit-row" style="margin-top: 35px;">
39
+ <input id="importBtn" type="submit" value="Import" name="_save">
40
+ <input type="button" value="Cancel" class="button cancel" onclick="window.location.href='..';">
24
41
  </div>
25
42
  </form>
43
+
26
44
  </div>
27
- {% endblock %}
45
+ {% endblock %}
@@ -0,0 +1,32 @@
1
+ {% extends "admin/base_site.html" %}
2
+ {% block content %}
3
+ <div class="module">
4
+ <h2>Import Results</h2>
5
+
6
+ <ul class="messagelist">
7
+ <li class="success"><strong>Created:</strong> {{ created_count }} records</li>
8
+ <li class="success"><strong>Updated:</strong> {{ updated_count }} records</li>
9
+ </ul>
10
+
11
+ {% if errors %}
12
+ <div class="errornote">
13
+ <p>Errors occurred while importing the data:</p>
14
+ <ul>
15
+ {% for error in errors %}
16
+ <li>{{ error }}</li>
17
+ {% endfor %}
18
+ </ul>
19
+ </div>
20
+ {% endif %}
21
+
22
+ <div style="margin-top: 20px;">
23
+ <button type="button" class="button" onclick="redirectAfterImport()">Finish</button>
24
+ </div>
25
+ </div>
26
+
27
+ <script>
28
+ function redirectAfterImport() {
29
+ window.location.href = window.location.pathname.replace("import/", "") + "?import_done=1";
30
+ }
31
+ </script>
32
+ {% endblock %}
@@ -12,7 +12,7 @@ Features:
12
12
  - Provides optimized data extraction for QuerySets.
13
13
  - Generates downloadable files with appropriate HTTP responses.
14
14
 
15
- Version: 2.0.10
15
+ Version: 2.0.11
16
16
  Author: Timur Kady
17
17
  Email: timurkady@yandex.com
18
18
  """
@@ -23,6 +23,7 @@ import json
23
23
  import yaml
24
24
  import xlsxwriter
25
25
  import numpy as np
26
+ import uuid
26
27
  from io import BytesIO
27
28
  from django.http import HttpResponse
28
29
  import logging
@@ -43,6 +44,7 @@ class TreeNodeExporter:
43
44
  self.queryset = queryset
44
45
  self.filename = filename
45
46
  self.fields = [field.name for field in queryset.model._meta.fields]
47
+ self.fields = self.get_ordered_fields()
46
48
 
47
49
  def export(self, format):
48
50
  """Determine the export format and calls the corresponding method."""
@@ -60,7 +62,9 @@ class TreeNodeExporter:
60
62
  def process_complex_fields(self, record):
61
63
  """Convert complex fields (lists, dictionaries) into JSON strings."""
62
64
  for key, value in record.items():
63
- if isinstance(value, (list, dict)):
65
+ if isinstance(value, uuid.UUID):
66
+ record[key] = str(value)
67
+ elif isinstance(value, (list, dict)):
64
68
  try:
65
69
  record[key] = json.dumps(value, ensure_ascii=False)
66
70
  except Exception as e:
@@ -68,11 +72,24 @@ class TreeNodeExporter:
68
72
  record[key] = None
69
73
  return record
70
74
 
75
+ def get_ordered_fields(self):
76
+ """Return fields in the desired order.
77
+
78
+ Order: id, tn_parent, tn_priority, then the rest.
79
+ """
80
+ required_fields = ["id", "tn_parent", "tn_priority"]
81
+ other_fields = [
82
+ field for field in self.fields if field not in required_fields]
83
+ return required_fields + other_fields
84
+
71
85
  def get_sorted_queryset(self):
72
- """Sort queryset by tn_order."""
73
- queryset_list = list(self.queryset)
74
- tn_orders = np.array([obj.tn_order for obj in queryset_list])
75
- return [queryset_list[int(i)] for i in np.argsort(tn_orders)]
86
+ """Quick sort queryset by tn_order."""
87
+ queryset = self.queryset
88
+ tn_orders = np.array([obj.tn_order for obj in queryset])
89
+ sorted_indices = np.argsort(tn_orders)
90
+ queryset_list = list(queryset.iterator())
91
+ result = [queryset_list[int(idx)] for idx in sorted_indices]
92
+ return result
76
93
 
77
94
  def get_data(self):
78
95
  """Return a list of data from QuerySet as dictionaries."""
@@ -100,70 +117,78 @@ class TreeNodeExporter:
100
117
  return data
101
118
 
102
119
  def to_csv(self):
103
- """Export to CSV with proper attachment handling."""
104
- response = HttpResponse(content_type="text/csv")
120
+ """Export to CSV with proper UTF-8 encoding."""
121
+ response = HttpResponse(content_type="text/csv; charset=utf-8")
105
122
  response["Content-Disposition"] = f'attachment; filename="{self.filename}.csv"'
123
+ response.write("\ufeff") # Добавляем BOM для Excel
124
+
106
125
  writer = csv.DictWriter(response, fieldnames=self.fields)
107
126
  writer.writeheader()
108
- writer.writerows(self.get_data())
127
+ for row in self.get_data():
128
+ writer.writerow({key: str(value)
129
+ for key, value in row.items()}) # Приводим к строкам
130
+
109
131
  return response
110
132
 
111
133
  def to_json(self):
112
- """Export to JSON with UUID serialization handling."""
113
- response = HttpResponse(content_type="application/octet-stream")
134
+ """Export to JSON with proper UTF-8 encoding."""
135
+ response = HttpResponse(content_type="application/json; charset=utf-8")
114
136
  response["Content-Disposition"] = f'attachment; filename="{self.filename}.json"'
115
- json.dump(
116
- self.get_data(),
117
- response,
118
- ensure_ascii=False,
119
- indent=4,
120
- default=str
121
- )
137
+ json_str = json.dumps(self.get_data(), ensure_ascii=False, indent=4)
138
+ response.write(json_str)
122
139
  return response
123
140
 
124
141
  def to_xlsx(self):
125
- """Export to XLSX."""
142
+ """Export to XLSX with UTF-8 encoding."""
126
143
  response = HttpResponse(
127
144
  content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
128
145
  )
129
146
  response["Content-Disposition"] = f'attachment; filename="{self.filename}.xlsx"'
130
147
 
131
- data = self.get_data()
132
148
  output = BytesIO()
133
149
  workbook = xlsxwriter.Workbook(output)
134
150
  worksheet = workbook.add_worksheet()
135
151
 
136
- # Записываем заголовки
137
- headers = list(data[0].keys()) if data else []
152
+ # Заголовки
153
+ headers = list(self.fields)
138
154
  for col_num, header in enumerate(headers):
139
155
  worksheet.write(0, col_num, header)
140
156
 
141
- # Записываем строки данных
142
- for row_num, row in enumerate(data, start=1):
157
+ # Данные
158
+ for row_num, row in enumerate(self.get_data(), start=1):
143
159
  for col_num, key in enumerate(headers):
144
- worksheet.write(row_num, col_num, row[key])
160
+ worksheet.write(
161
+ row_num,
162
+ col_num,
163
+ str(row[key]) if row[key] is not None else ""
164
+ )
145
165
 
146
166
  workbook.close()
147
167
  output.seek(0)
148
- return response.write(output.read())
168
+ response.write(output.read())
169
+ return response
149
170
 
150
171
  def to_yaml(self):
151
- """Export to YAML with proper attachment handling."""
152
- response = HttpResponse(content_type="application/octet-stream")
172
+ """Export to YAML with proper UTF-8 encoding."""
173
+ response = HttpResponse(
174
+ content_type="application/x-yaml; charset=utf-8")
153
175
  response["Content-Disposition"] = f'attachment; filename="{self.filename}.yaml"'
154
- yaml_str = yaml.dump(self.get_data(), allow_unicode=True)
176
+ yaml_str = yaml.dump(
177
+ self.get_data(), allow_unicode=True, default_flow_style=False)
155
178
  response.write(yaml_str)
156
179
  return response
157
180
 
158
181
  def to_tsv(self):
159
- """Export to TSV with proper attachment handling."""
160
- response = HttpResponse(content_type="application/octet-stream")
182
+ """Export to TSV with UTF-8 encoding."""
183
+ response = HttpResponse(
184
+ content_type="text/tab-separated-values; charset=utf-8")
161
185
  response["Content-Disposition"] = f'attachment; filename="{self.filename}.tsv"'
186
+ response.write("\ufeff") # Добавляем BOM
187
+
162
188
  writer = csv.DictWriter(
163
- response,
164
- fieldnames=self.fields,
165
- delimiter=" "
166
- )
189
+ response, fieldnames=self.fields, delimiter="\t")
167
190
  writer.writeheader()
168
- writer.writerows(self.get_data())
191
+ for row in self.get_data():
192
+ writer.writerow({key: str(value) for key, value in row.items()})
193
+
169
194
  return response