django-fast-treenode 2.0.10__py3-none-any.whl → 2.1.0__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.
Files changed (70) hide show
  1. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/LICENSE +2 -2
  2. django_fast_treenode-2.1.0.dist-info/METADATA +161 -0
  3. django_fast_treenode-2.1.0.dist-info/RECORD +75 -0
  4. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/WHEEL +1 -1
  5. treenode/admin/__init__.py +9 -0
  6. treenode/admin/admin.py +295 -0
  7. treenode/admin/changelist.py +65 -0
  8. treenode/admin/mixins.py +302 -0
  9. treenode/apps.py +12 -1
  10. treenode/cache.py +2 -2
  11. treenode/docs/.gitignore +0 -0
  12. treenode/docs/about.md +36 -0
  13. treenode/docs/admin.md +104 -0
  14. treenode/docs/api.md +739 -0
  15. treenode/docs/cache.md +187 -0
  16. treenode/docs/import_export.md +35 -0
  17. treenode/docs/index.md +30 -0
  18. treenode/docs/installation.md +74 -0
  19. treenode/docs/migration.md +145 -0
  20. treenode/docs/models.md +128 -0
  21. treenode/docs/roadmap.md +45 -0
  22. treenode/forms.py +33 -22
  23. treenode/managers/__init__.py +21 -0
  24. treenode/managers/adjacency.py +203 -0
  25. treenode/managers/closure.py +278 -0
  26. treenode/models/__init__.py +2 -1
  27. treenode/models/adjacency.py +343 -0
  28. treenode/models/classproperty.py +3 -0
  29. treenode/models/closure.py +39 -65
  30. treenode/models/factory.py +12 -2
  31. treenode/models/mixins/__init__.py +23 -0
  32. treenode/models/mixins/ancestors.py +65 -0
  33. treenode/models/mixins/children.py +81 -0
  34. treenode/models/mixins/descendants.py +66 -0
  35. treenode/models/mixins/family.py +63 -0
  36. treenode/models/mixins/logical.py +68 -0
  37. treenode/models/mixins/node.py +210 -0
  38. treenode/models/mixins/properties.py +156 -0
  39. treenode/models/mixins/roots.py +96 -0
  40. treenode/models/mixins/siblings.py +99 -0
  41. treenode/models/mixins/tree.py +344 -0
  42. treenode/signals.py +26 -0
  43. treenode/static/treenode/css/tree_widget.css +201 -31
  44. treenode/static/treenode/css/treenode_admin.css +48 -41
  45. treenode/static/treenode/js/tree_widget.js +269 -131
  46. treenode/static/treenode/js/treenode_admin.js +131 -171
  47. treenode/templates/admin/tree_node_changelist.html +6 -0
  48. treenode/templates/admin/tree_node_import.html +27 -9
  49. treenode/templates/admin/tree_node_import_report.html +32 -0
  50. treenode/templates/admin/treenode_ajax_rows.html +7 -0
  51. treenode/tests/tests.py +488 -0
  52. treenode/urls.py +10 -6
  53. treenode/utils/__init__.py +2 -0
  54. treenode/utils/aid.py +46 -0
  55. treenode/utils/base16.py +38 -0
  56. treenode/utils/base36.py +3 -1
  57. treenode/utils/db.py +116 -0
  58. treenode/utils/exporter.py +63 -36
  59. treenode/utils/importer.py +168 -161
  60. treenode/utils/radix.py +61 -0
  61. treenode/version.py +2 -2
  62. treenode/views.py +119 -38
  63. treenode/widgets.py +104 -40
  64. django_fast_treenode-2.0.10.dist-info/METADATA +0 -698
  65. django_fast_treenode-2.0.10.dist-info/RECORD +0 -41
  66. treenode/admin.py +0 -396
  67. treenode/docs/Documentation +0 -664
  68. treenode/managers.py +0 -281
  69. treenode/models/proxy.py +0 -650
  70. {django_fast_treenode-2.0.10.dist-info → django_fast_treenode-2.1.0.dist-info}/top_level.txt +0 -0
treenode/utils/db.py ADDED
@@ -0,0 +1,116 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ DB Vendor Utility Module
4
+
5
+ This module provides a utility function for converting integers
6
+ to Base36 string representation.
7
+
8
+ Features:
9
+ - Converts integers into a more compact Base36 format.
10
+ - Maintains lexicographic order when padded with leading zeros.
11
+ - Supports negative numbers.
12
+
13
+ Version: 2.1.0
14
+ Author: Timur Kady
15
+ Email: timurkady@yandex.com
16
+ """
17
+
18
+ import logging
19
+ from django.apps import apps
20
+ from django.db import connection
21
+
22
+ from ..models import TreeNodeModel
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def create_indexes(model):
28
+ """Create indexes for the descendants of TreeNodeModel."""
29
+ vendor = connection.vendor
30
+ sender = "Django Fast TeeNode"
31
+ table = model._meta.db_table
32
+
33
+ with connection.cursor() as cursor:
34
+ if vendor == "postgresql":
35
+ cursor.execute(
36
+ "SELECT indexname FROM pg_indexes WHERE tablename = %s AND indexname = %s;",
37
+ [table, f"idx_{table}_btree"]
38
+ )
39
+ if not cursor.fetchone():
40
+ cursor.execute(
41
+ f"CREATE INDEX idx_{table}_btree ON {table} USING BTREE (id);"
42
+ )
43
+ logger.info(f"{sender}: GIN index for table {table} created.")
44
+
45
+ # Если существует первичный ключ, выполняем кластеризацию
46
+ cursor.execute(
47
+ "SELECT relname FROM pg_class WHERE relname = %s;",
48
+ [f"{table}_pkey"]
49
+ )
50
+ if cursor.fetchone():
51
+ cursor.execute(f"CLUSTER {table} USING {table}_pkey;")
52
+ logger.info(f"{sender}: Table {table} is clustered.")
53
+
54
+ elif vendor == "mysql":
55
+ cursor.execute("SHOW TABLE STATUS WHERE Name = %s;", [table])
56
+ columns = [col[0] for col in cursor.description]
57
+ row = cursor.fetchone()
58
+ if row:
59
+ table_status = dict(zip(columns, row))
60
+ engine = table_status.get("Engine", "").lower()
61
+ if engine != "innodb":
62
+ cursor.execute(f"ALTER TABLE {table} ENGINE = InnoDB;")
63
+ logger.info(
64
+ f"{sender}: Table {table} has been converted to InnoDB."
65
+ )
66
+
67
+ elif vendor in ["microsoft", "oracle"]:
68
+ if vendor == "microsoft":
69
+ cursor.execute(
70
+ "SELECT name FROM sys.indexes WHERE name = %s AND object_id = OBJECT_ID(%s);",
71
+ [f"idx_{table}_cluster", table]
72
+ )
73
+ else:
74
+ cursor.execute(
75
+ "SELECT index_name FROM user_indexes WHERE index_name = %s;",
76
+ [f"IDX_{table.upper()}_CLUSTER"]
77
+ )
78
+ if not cursor.fetchone():
79
+ cursor.execute(
80
+ f"CREATE CLUSTERED INDEX idx_{table}_cluster ON {table} (id);")
81
+ logger.info(
82
+ f"{sender}: CLUSTERED index for table {table} created."
83
+ )
84
+
85
+ elif vendor == "sqlite":
86
+ # Kick those on SQLite
87
+ logger.warning(
88
+ f"{sender} Unable to create GIN and CLUSTER indexes for SQLite."
89
+ )
90
+ else:
91
+ logger.warning(
92
+ f"{sender}: Unknown vendor. Index creation cancelled."
93
+ )
94
+
95
+
96
+ def post_migrate_update(sender, **kwargs):
97
+ """Update indexes and tn_closure field only when necessary."""
98
+ # Перебираем все зарегистрированные модели
99
+ for model in apps.get_models():
100
+ # Check that the model inherits from TreeNodeModel and
101
+ # is not abstract
102
+ if issubclass(model, TreeNodeModel) and not model._meta.abstract:
103
+ # Create GIN and CLUSTER indexrs
104
+ create_indexes(model)
105
+ # Get ClosureModel
106
+ closure_model = model.closure_model
107
+ # Check node counts
108
+ al_count = model.objects.exists()
109
+ cl_counts = closure_model.objects.exclude(node=None).exists()
110
+
111
+ if al_count and not cl_counts:
112
+ # Call update_tree()
113
+ model.update_tree()
114
+
115
+
116
+ # The End
@@ -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,80 @@ 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
195
+
196
+ # The End