django-fast-treenode 1.1.3__py3-none-any.whl → 2.0.1__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-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/METADATA +127 -46
- django_fast_treenode-2.0.1.dist-info/RECORD +41 -0
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/WHEEL +1 -1
- treenode/__init__.py +0 -7
- treenode/admin.py +327 -82
- treenode/apps.py +20 -3
- treenode/cache.py +231 -0
- treenode/docs/Documentation +101 -54
- treenode/forms.py +75 -19
- treenode/managers.py +260 -48
- treenode/models/__init__.py +7 -0
- treenode/models/classproperty.py +24 -0
- treenode/models/closure.py +168 -0
- treenode/models/factory.py +71 -0
- treenode/models/proxy.py +650 -0
- treenode/static/treenode/css/tree_widget.css +62 -0
- treenode/static/treenode/css/treenode_admin.css +106 -0
- treenode/static/treenode/js/tree_widget.js +161 -0
- treenode/static/treenode/js/treenode_admin.js +171 -0
- treenode/templates/admin/export_success.html +26 -0
- treenode/templates/admin/tree_node_changelist.html +11 -0
- treenode/templates/admin/tree_node_export.html +27 -0
- treenode/templates/admin/tree_node_import.html +27 -0
- treenode/templates/widgets/tree_widget.css +23 -0
- treenode/templates/widgets/tree_widget.html +21 -0
- treenode/urls.py +34 -0
- treenode/utils/__init__.py +4 -0
- treenode/utils/base36.py +35 -0
- treenode/utils/exporter.py +141 -0
- treenode/utils/importer.py +296 -0
- treenode/version.py +11 -1
- treenode/views.py +102 -2
- treenode/widgets.py +49 -27
- django_fast_treenode-1.1.3.dist-info/RECORD +0 -33
- treenode/compat.py +0 -8
- treenode/factory.py +0 -68
- treenode/models.py +0 -668
- treenode/static/select2tree/.gitkeep +0 -1
- treenode/static/select2tree/select2tree.css +0 -176
- treenode/static/select2tree/select2tree.js +0 -181
- treenode/static/treenode/css/treenode.css +0 -85
- treenode/static/treenode/js/treenode.js +0 -201
- treenode/templates/widgets/.gitkeep +0 -1
- treenode/templates/widgets/attrs.html +0 -7
- treenode/templates/widgets/options.html +0 -1
- treenode/templates/widgets/select2tree.html +0 -22
- treenode/tests.py +0 -3
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/LICENSE +0 -0
- {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/top_level.txt +0 -0
- /treenode/{docs → templates/admin}/.gitkeep +0 -0
treenode/cache.py
ADDED
@@ -0,0 +1,231 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Cache Module
|
4
|
+
|
5
|
+
This module provides a singleton-based caching system for TreeNode models.
|
6
|
+
It includes optimized key generation, cache size tracking,
|
7
|
+
and an eviction mechanism to ensure efficient memory usage.
|
8
|
+
|
9
|
+
Features:
|
10
|
+
- Singleton cache instance to prevent redundant allocations.
|
11
|
+
- Custom cache key generation using function parameters.
|
12
|
+
- Automatic cache eviction when memory limits are exceeded.
|
13
|
+
- Decorator `@cached_method` for caching method results.
|
14
|
+
|
15
|
+
Version: 2.0.0
|
16
|
+
Author: Timur Kady
|
17
|
+
Email: timurkady@yandex.com
|
18
|
+
"""
|
19
|
+
|
20
|
+
|
21
|
+
from django.core.cache import caches
|
22
|
+
from django.conf import settings
|
23
|
+
import threading
|
24
|
+
import hashlib
|
25
|
+
import json
|
26
|
+
import logging
|
27
|
+
from pympler import asizeof
|
28
|
+
|
29
|
+
from .utils.base36 import to_base36
|
30
|
+
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
|
33
|
+
|
34
|
+
# ---------------------------------------------------
|
35
|
+
# Caching
|
36
|
+
# ---------------------------------------------------
|
37
|
+
|
38
|
+
class TreeNodeCache:
|
39
|
+
"""Singleton-класс для управления кэшем TreeNode."""
|
40
|
+
|
41
|
+
_instance = None
|
42
|
+
_lock = threading.Lock()
|
43
|
+
_keys = dict()
|
44
|
+
_total_size = 0
|
45
|
+
_cache_limit = 0
|
46
|
+
|
47
|
+
def __new__(cls):
|
48
|
+
"""Create only one instance of the class (Singleton)."""
|
49
|
+
with cls._lock:
|
50
|
+
if cls._instance is None:
|
51
|
+
cls._instance = super(TreeNodeCache, cls).__new__(cls)
|
52
|
+
cls._instance._initialize()
|
53
|
+
return cls._instance
|
54
|
+
|
55
|
+
def _initialize(self):
|
56
|
+
"""Initialize cache."""
|
57
|
+
self.cache_timeout = None
|
58
|
+
limit = getattr(settings, 'TREENODE_CACHE_LIMIT', 100)*1024*1024
|
59
|
+
self._cache_limit = limit
|
60
|
+
self.cache_timeout = None
|
61
|
+
cache_name = 'treenode' if 'treenode' in settings.CACHES else 'default'
|
62
|
+
self.cache = caches[cache_name]
|
63
|
+
self._total_size = 0
|
64
|
+
self.cache.clear()
|
65
|
+
|
66
|
+
def generate_cache_key(self, label, func_name, unique_id, *args, **kwargs):
|
67
|
+
"""
|
68
|
+
Generate Cache Key.
|
69
|
+
|
70
|
+
Generates a cache key of the form:
|
71
|
+
<model_name>_<func_name>_<id>_<hash>,
|
72
|
+
where <hash> is calculated from the function parameters
|
73
|
+
(args and kwargs).
|
74
|
+
If the parameters can be serialized via JSON, use this, otherwise we
|
75
|
+
use repr to generate the string.
|
76
|
+
"""
|
77
|
+
try:
|
78
|
+
# Sort dictionary keys to ensure determinism.
|
79
|
+
params_repr = json.dumps(
|
80
|
+
(args, kwargs),
|
81
|
+
sort_keys=True,
|
82
|
+
default=str
|
83
|
+
)
|
84
|
+
except (TypeError, ValueError) as e:
|
85
|
+
# If JSON serialization fails, use repr.
|
86
|
+
params_repr = repr((args, kwargs))
|
87
|
+
logger.warning(f"Failed to serialize cache key params: {e}")
|
88
|
+
|
89
|
+
# Calculate the MD5 hash from the received string.
|
90
|
+
hash_value = hashlib.sha256(params_repr.encode("utf-8")).hexdigest()
|
91
|
+
|
92
|
+
# Forming the final key.
|
93
|
+
cache_key = f"{label}_{func_name}_{unique_id}_{hash_value}"
|
94
|
+
|
95
|
+
return cache_key
|
96
|
+
|
97
|
+
def get_obj_size(self, value):
|
98
|
+
"""Determine the size of the object in bytes."""
|
99
|
+
try:
|
100
|
+
return len(json.dumps(value).encode("utf-8"))
|
101
|
+
except (TypeError, ValueError):
|
102
|
+
return asizeof.asizeof(value)
|
103
|
+
|
104
|
+
def cache_size(self):
|
105
|
+
"""Return the total size of the cache in bytes."""
|
106
|
+
return self._total_size
|
107
|
+
|
108
|
+
def set(self, cache_key, value):
|
109
|
+
"""Push to cache."""
|
110
|
+
size = self.get_obj_size(value)
|
111
|
+
self.cache.set(cache_key, value, timeout=self.cache_timeout)
|
112
|
+
|
113
|
+
# Update cache size
|
114
|
+
if cache_key in self._keys:
|
115
|
+
self._total_size -= self._keys[cache_key]
|
116
|
+
self._keys[cache_key] = size
|
117
|
+
self._total_size += size
|
118
|
+
|
119
|
+
# Check if the limit has been exceeded
|
120
|
+
self._evict_cache()
|
121
|
+
|
122
|
+
def get(self, cache_key):
|
123
|
+
"""Get from cache."""
|
124
|
+
return self.cache.get(cache_key)
|
125
|
+
|
126
|
+
def invalidate(self, label):
|
127
|
+
"""Clear cache for a specific model only."""
|
128
|
+
prefix = f"{label}_"
|
129
|
+
keys_to_remove = [key for key in self._keys if key.startswith(prefix)]
|
130
|
+
for key in keys_to_remove:
|
131
|
+
self.cache.delete(key)
|
132
|
+
self._total_size -= self._keys.pop(key, 0)
|
133
|
+
if self._total_size < 0:
|
134
|
+
self._total_size = 0
|
135
|
+
|
136
|
+
def clear(self):
|
137
|
+
"""Full cache clearing."""
|
138
|
+
self.cache.clear()
|
139
|
+
self._keys.clear()
|
140
|
+
self._total_size = 0
|
141
|
+
|
142
|
+
def _evict_cache(self):
|
143
|
+
"""Delete old entries if the cache has exceeded the limit."""
|
144
|
+
if self._total_size <= self._cache_limit:
|
145
|
+
# If the size is within the limit, do nothing
|
146
|
+
return
|
147
|
+
|
148
|
+
if not self._keys:
|
149
|
+
self.clear()
|
150
|
+
|
151
|
+
logger.warning(f"Cache limit exceeded! Current size: \
|
152
|
+
{self._total_size}, Limit: {self._cache_limit}")
|
153
|
+
|
154
|
+
# Sort keys by insertion order (FIFO)
|
155
|
+
keys_sorted = list(self._keys.keys())
|
156
|
+
|
157
|
+
keys_to_delete = []
|
158
|
+
freed_size = 0
|
159
|
+
|
160
|
+
# Delete old keys until we reach the limit
|
161
|
+
for key in keys_sorted:
|
162
|
+
freed_size += self._keys[key]
|
163
|
+
keys_to_delete.append(key)
|
164
|
+
if self._total_size - freed_size <= self._cache_limit:
|
165
|
+
break
|
166
|
+
|
167
|
+
# Delete keys in batches (delete_many)
|
168
|
+
self.cache.delete_many(keys_to_delete)
|
169
|
+
|
170
|
+
# Update data in `_keys` and `_total_size`
|
171
|
+
for key in keys_to_delete:
|
172
|
+
self._total_size -= self._keys.pop(key, 0)
|
173
|
+
|
174
|
+
logger.info(f"Evicted {len(keys_to_delete)} keys from cache, \
|
175
|
+
freed {freed_size} bytes.")
|
176
|
+
|
177
|
+
|
178
|
+
# Create a global cache object (there is only one for the entire system)
|
179
|
+
treenode_cache = TreeNodeCache()
|
180
|
+
|
181
|
+
|
182
|
+
# ---------------------------------------------------
|
183
|
+
# Decorator
|
184
|
+
# ---------------------------------------------------
|
185
|
+
|
186
|
+
|
187
|
+
def cached_method(func):
|
188
|
+
"""
|
189
|
+
Decorate instance methods for caching.
|
190
|
+
|
191
|
+
The decorator caches the results of the decorated class or instance method.
|
192
|
+
If the cache is cleared or invalidated, the cached results will be
|
193
|
+
recalculated.
|
194
|
+
|
195
|
+
Usage:
|
196
|
+
@cached_tree_method
|
197
|
+
def model_method(self):
|
198
|
+
# Tree method logic
|
199
|
+
"""
|
200
|
+
|
201
|
+
def wrapper(self, *args, **kwargs):
|
202
|
+
# Generate a cache key.
|
203
|
+
if isinstance(self, type):
|
204
|
+
# Если self — класс, используем его имя
|
205
|
+
unique_id = to_base36(id(self))
|
206
|
+
label = getattr(self._meta, 'label', self.__name__)
|
207
|
+
else:
|
208
|
+
unique_id = getattr(self, "pk", id(self))
|
209
|
+
label = self._meta.label
|
210
|
+
|
211
|
+
cache_key = treenode_cache.generate_cache_key(
|
212
|
+
label,
|
213
|
+
func.__name__,
|
214
|
+
unique_id,
|
215
|
+
args,
|
216
|
+
kwargs
|
217
|
+
)
|
218
|
+
|
219
|
+
# Retrieving from cache
|
220
|
+
value = treenode_cache.get(cache_key)
|
221
|
+
|
222
|
+
if value is None:
|
223
|
+
value = func(self, *args, **kwargs)
|
224
|
+
|
225
|
+
# Push to cache
|
226
|
+
treenode_cache.set(cache_key, value)
|
227
|
+
return value
|
228
|
+
return wrapper
|
229
|
+
|
230
|
+
|
231
|
+
# The End
|
treenode/docs/Documentation
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
# Django-fast-treenode
|
2
2
|
__Combination of Adjacency List and Closure Table__
|
3
3
|
|
4
|
-
##
|
4
|
+
## Functions
|
5
5
|
Application for supporting tree (hierarchical) data structure in Django projects
|
6
|
-
*
|
7
|
-
*
|
8
|
-
*
|
6
|
+
* fast: the fastest of the two methods is used to process requests, combining the advantages of an **Adjacency Table** and a **Closure Table**,
|
7
|
+
* even faster: the main resource-intensive operations are **cached**; **bulk operations** are used for inserts and changes,
|
8
|
+
* synchronized: model instances in memory are automatically updated,
|
9
|
+
* compatibility: you can easily add a tree node to existing projects using TreeNode without changing the code,
|
9
10
|
* no dependencies,
|
10
|
-
* easy
|
11
|
-
* admin integration: visualization options (accordion, breadcrumbs or
|
12
|
-
* widget:
|
11
|
+
* easy setup: just extend the abstract model/model-admin,
|
12
|
+
* admin integration: visualization options (accordion, breadcrumbs or padding),
|
13
|
+
* widget: Built-in Select2-to-Tree extends Select2 to support arbitrary nesting levels.
|
13
14
|
|
14
15
|
## Debut idea
|
15
16
|
This is a modification of the reusable [django-treenode](https://github.com/fabiocaccamo/django-treenode) application developed by [Fabio Caccamo](https://github.com/fabiocaccamo).
|
@@ -44,13 +45,12 @@ You can easily find additional information on your own on the Internet.
|
|
44
45
|
|
45
46
|
## Quick start
|
46
47
|
1. Run ```pip install django-fast-treenode```
|
47
|
-
2. Add ```
|
48
|
+
2. Add ```treenode``` to ```settings.INSTALLED_APPS```
|
48
49
|
3. Make your model inherit from ```treenode.models.TreeNodeModel``` (described below)
|
49
50
|
4. Make your model-admin inherit from ```treenode.admin.TreeNodeModelAdmin``` (described below)
|
50
51
|
5. Run python manage.py makemigrations and ```python manage.py migrate```
|
51
52
|
|
52
|
-
|
53
|
-
It will automatically build a new and complete Closure Table for your tree.
|
53
|
+
For more information on migrating from the **django-treenode** package and upgrading to version 2.0, see [below](#migration-guide).
|
54
54
|
|
55
55
|
## Configuration
|
56
56
|
### `models.py`
|
@@ -116,6 +116,8 @@ CACHES = {
|
|
116
116
|
},
|
117
117
|
"treenode": {
|
118
118
|
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
119
|
+
"KEY_PREFIX": "", # This is important!
|
120
|
+
"VERSION": None, # This is important!
|
119
121
|
},
|
120
122
|
}
|
121
123
|
```
|
@@ -123,15 +125,11 @@ CACHES = {
|
|
123
125
|
|
124
126
|
```
|
125
127
|
class YoursForm(TreeNodeForm):
|
126
|
-
|
127
|
-
class Meta:
|
128
|
-
widgets = {
|
129
|
-
'tn_parent': TreeWidget(attrs={'style': 'min-width:400px'}),
|
130
|
-
}
|
128
|
+
# Your code is here
|
131
129
|
```
|
132
130
|
|
133
|
-
## Usage
|
134
131
|
|
132
|
+
## Usage
|
135
133
|
### Methods/Properties
|
136
134
|
|
137
135
|
- [`delete`](#delete)
|
@@ -157,7 +155,6 @@ class YoursForm(TreeNodeForm):
|
|
157
155
|
- [`get_last_child`](#get_last_child)
|
158
156
|
- [`get_level`](#get_level)
|
159
157
|
- [`get_order`](#get_order)
|
160
|
-
- [`get_ordered_queryset`](#get_ordered_queryset)
|
161
158
|
- [`get_parent`](#get_parent)
|
162
159
|
- [`get_parent_pk`](#get_parent_pk)
|
163
160
|
- [`set_parent`](#set_parent)
|
@@ -187,11 +184,17 @@ class YoursForm(TreeNodeForm):
|
|
187
184
|
- [`update_tree`](#update_tree)
|
188
185
|
|
189
186
|
#### `delete`
|
190
|
-
**Delete a node**
|
191
|
-
|
187
|
+
**Delete a node** provides two deletion strategies:
|
188
|
+
- **Cascade Delete (`cascade=True`)**: Removes the node along with all its descendants.
|
189
|
+
- **Reparenting (`cascade=False`)**: Moves the children of the deleted node up one level in the hierarchy before removing the node itself.
|
190
|
+
|
192
191
|
```python
|
193
|
-
|
192
|
+
node.delete(cascade=True) # Deletes node and all its descendants
|
193
|
+
node.delete(cascade=False) # Moves children up and then deletes the node
|
194
194
|
```
|
195
|
+
This ensures greater flexibility in managing tree structures while preventing orphaned nodes.
|
196
|
+
|
197
|
+
---
|
195
198
|
|
196
199
|
#### `delete_tree`
|
197
200
|
**Delete the whole tree** for the current node class:
|
@@ -313,6 +316,8 @@ obj.get_descendants_tree()
|
|
313
316
|
obj.descendants_tree
|
314
317
|
```
|
315
318
|
|
319
|
+
**Important**: In future projects, avoid using `get_descendants_tree()`. It will be removed in the next version.
|
320
|
+
|
316
321
|
#### `get_descendants_tree_display`
|
317
322
|
Get a **multiline** `string` representing the **model tree**:
|
318
323
|
```python
|
@@ -321,6 +326,8 @@ obj.get_descendants_tree_display(include_self=False, depth=None)
|
|
321
326
|
obj.descendants_tree_display
|
322
327
|
```
|
323
328
|
|
329
|
+
**Important**: In future projects, avoid using `get_descendants_tree_display()`. It will be removed in the next version.
|
330
|
+
|
324
331
|
#### `get_first_child`
|
325
332
|
Get the **first child node**:
|
326
333
|
```python
|
@@ -361,23 +368,6 @@ obj.get_order()
|
|
361
368
|
obj.order
|
362
369
|
```
|
363
370
|
|
364
|
-
#### `get_ordered_queryset`
|
365
|
-
Returns a queryset of nodes ordered by tn_priority each node.
|
366
|
-
```python
|
367
|
-
cls.get_ordered_queryset()
|
368
|
-
```
|
369
|
-
For example:
|
370
|
-
- A.1
|
371
|
-
- A.1.1
|
372
|
-
- A.1.1.1
|
373
|
-
- A.1.1.2
|
374
|
-
- A.2
|
375
|
-
- A.2.1
|
376
|
-
- ...
|
377
|
-
|
378
|
-
This method uses a lot of memory, ```RawSQL()``` and ```.extra()``` QuerySet method. Use of this method is deprecated due to concerns that Django's ```.extra()``` method **will be deprecated in the future**.
|
379
|
-
Use it only if you cannot otherwise assemble an ordered tree from an Adjacency Table and a Closure Table. In most cases, the data in one Adjacency Table is sufficient for such an assembly. You can easily find the corresponding algorithms (two-pass and one-pass) on the Internet.
|
380
|
-
|
381
371
|
#### `get_parent`
|
382
372
|
Get the **parent node**:
|
383
373
|
```python
|
@@ -561,28 +551,85 @@ obj.is_sibling_of(target_obj)
|
|
561
551
|
```python
|
562
552
|
cls.update_tree()
|
563
553
|
```
|
554
|
+
## **Cache Management**
|
555
|
+
### **Overview**
|
556
|
+
In v2.0, the caching mechanism has been improved to prevent excessive memory usage when multiple models inherit from `TreeNode`. The new system introduces **FIFO (First-In-First-Out) cache eviction**, with plans to test and integrate more advanced algorithms in future releases.
|
557
|
+
|
558
|
+
### **Key Features**
|
559
|
+
**Global Cache Limit**: The setting `TREENODE_CACHE_LIMIT` defines the maximum cache size (in MB) for all models inheriting from `TreeNode`. Default is **100MB** if not explicitly set in `settings.py`.
|
560
|
+
**settings.py**
|
561
|
+
``` python
|
562
|
+
TREENODE_CACHE_LIMIT = 100
|
563
|
+
```
|
564
|
+
**Automatic Management**: In most cases, users don’t need to manually manage cache operations.
|
565
|
+
**Manual Cache Clearing**:
|
566
|
+
- **Clear cache for a single model**: Use `clear_cache()` at the model level:
|
567
|
+
```python
|
568
|
+
MyTreeNodeModel.clear_cache()
|
569
|
+
```
|
570
|
+
- **Clear cache for all models**: Use the global `treenode_cache.clear()` method:
|
571
|
+
```python
|
572
|
+
from treenode.cache import treenode_cache
|
573
|
+
treenode_cache.clear()
|
574
|
+
```
|
575
|
+
|
576
|
+
## **Export and Import Functionality**
|
577
|
+
### **Overview**
|
578
|
+
TreeNode v2.0 includes **built-in export and import features** for easier data migration. Supported Formats: `csv`, `json`, `xlsx`, `yaml`, `tsv`
|
579
|
+
### Installation for Import/Export Features
|
580
|
+
By default, import/export functionality is **not included** to keep the package lightweight. If you need these features, install the package with:
|
581
|
+
```bash
|
582
|
+
pip install django-fast-treenode[import_export]
|
583
|
+
```
|
584
|
+
Once installed, **import/export buttons will appear** in the Django admin interface.
|
585
|
+
### **Important Considerations**
|
586
|
+
Exporting objects with M2M fields may lead to serialization issues. Some formats (e.g., CSV) do not natively support many-to-many relationships. If you encounter errors, consider exporting data in `json` or `yaml` format, which better handle nested structures.
|
587
|
+
|
588
|
+
## Migration Guide
|
589
|
+
#### Switching from `django-treenode`
|
590
|
+
The migration process from `django-treenode` is fully automated. No manual steps are required. Upon upgrading, the necessary data structures will be checked and updated automatically. In exceptional cases, you can call the update code `cls.update_tree()` manually.
|
591
|
+
|
592
|
+
|
593
|
+
#### Upgrading to `django-fast-treenode` 2.0
|
594
|
+
To upgrade to version 2.0, simply run:
|
595
|
+
```bash
|
596
|
+
pip install --upgrade django-fast-treenode
|
597
|
+
```
|
598
|
+
or
|
599
|
+
```bash
|
600
|
+
pip install django-fast-treenode[import_export]
|
601
|
+
```
|
602
|
+
After upgrading, ensure that your database schema is up to date by running:
|
603
|
+
```bash
|
604
|
+
python manage.py makemigrations
|
605
|
+
python manage.py migrate
|
606
|
+
```
|
607
|
+
This will apply any necessary database changes automatically.
|
564
608
|
|
565
|
-
## License
|
566
|
-
Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
|
567
609
|
|
568
|
-
##
|
569
|
-
|
610
|
+
## To do
|
611
|
+
These improvements aim to enhance usability, performance, and maintainability for all users of `django-fast-treenode`:
|
612
|
+
* **Cache Algorithm Optimization**: Testing and integrating more advanced cache eviction strategies.
|
613
|
+
* **Drag-and-Drop UI Enhancements**: Adding intuitive drag-and-drop functionality for tree node management.
|
614
|
+
* to be happy, to don't worry, until die.
|
570
615
|
|
571
|
-
|
616
|
+
Your wishes, objections, comments are welcome.
|
572
617
|
|
573
|
-
## Credits
|
574
|
-
This software contains, uses, including in a modified form:
|
575
|
-
* [django-treenode](https://github.com/fabiocaccamo/django-treenode) by [Fabio Caccamo](https://github.com/fabiocaccamo);
|
576
|
-
* [Select2-to-Tree](https://github.com/clivezhg/select2-to-tree) Select2 extension by [clivezhg](https://github.com/clivezhg)
|
577
618
|
|
578
|
-
|
619
|
+
# Django-fast-treenode
|
579
620
|
|
580
|
-
##
|
581
|
-
|
582
|
-
* drug-and-drop support;
|
583
|
-
* may be will restore caching;
|
584
|
-
* may be will add the ability to determine the priority of the parent by any field, for example, by creation date or alphabetical order;
|
585
|
-
* to be happy, to don't worry, until die.
|
621
|
+
## License
|
622
|
+
Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
|
586
623
|
|
624
|
+
## Cautions
|
625
|
+
**Warning**: Do not access the tree node fields directly! Most of *django-treenode* model fields have been removed as unnecessary. Now only `tn_parent` and `tn_priority` are supported and will be kept in the future.
|
587
626
|
|
588
|
-
|
627
|
+
**Risks of Direct Field Access:**
|
628
|
+
- **Database Integrity Issues**: Directly modifying fields may break tree integrity, causing inconsistent parent-child relationships.
|
629
|
+
- **Loss of Cached Data**: The caching system relies on controlled updates. Bypassing methods like `set_parent()` or `update_tree()` may lead to outdated or incorrect data.
|
630
|
+
- **Unsupported Behavior**: Future versions may change field structures or remove unnecessary fields. Relying on them directly risks breaking compatibility.
|
631
|
+
|
632
|
+
Instead, always use the **documented methods** described above or refer to the [original application documentation](https://github.com/fabiocaccamo/django-treenode).
|
633
|
+
|
634
|
+
## Credits
|
635
|
+
This software contains, uses, and includes, in a modified form, [django-treenode](https://github.com/fabiocaccamo/django-treenode) by [Fabio Caccamo](https://github.com/fabiocaccamo). Special thanks to [Mathieu Leplatre](https://blog.mathieu-leplatre.info/pages/about.html) for the advice used in writing this application.
|
treenode/forms.py
CHANGED
@@ -1,32 +1,88 @@
|
|
1
|
-
|
1
|
+
"""
|
2
|
+
TreeNode Form Module.
|
3
|
+
|
4
|
+
This module defines the TreeNodeForm class, which dynamically determines the TreeNode model.
|
5
|
+
It utilizes TreeWidget and automatically excludes the current node and its descendants
|
6
|
+
from the parent choices.
|
7
|
+
|
8
|
+
Functions:
|
9
|
+
- __init__: Initializes the form and filters out invalid parent choices.
|
10
|
+
- factory: Dynamically creates a form class for a given TreeNode model.
|
11
|
+
|
12
|
+
Version: 2.0.0
|
13
|
+
Author: Timur Kady
|
14
|
+
Email: timurkady@yandex.com
|
15
|
+
"""
|
2
16
|
|
3
17
|
from django import forms
|
4
18
|
from .widgets import TreeWidget
|
19
|
+
from django.db.models import Case, When, Value, IntegerField
|
5
20
|
|
6
21
|
|
7
22
|
class TreeNodeForm(forms.ModelForm):
|
23
|
+
"""
|
24
|
+
TreeNode Form Class.
|
25
|
+
|
26
|
+
ModelForm for dynamically determined TreeNode model.
|
27
|
+
Uses TreeWidget and excludes self and descendants from the parent choices.
|
28
|
+
"""
|
29
|
+
|
30
|
+
class Meta:
|
31
|
+
"""Meta Class."""
|
32
|
+
|
33
|
+
model = None
|
34
|
+
fields = "__all__"
|
35
|
+
widgets = {
|
36
|
+
"tn_parent": TreeWidget()
|
37
|
+
}
|
8
38
|
|
9
39
|
def __init__(self, *args, **kwargs):
|
40
|
+
"""Init Method."""
|
41
|
+
super().__init__(*args, **kwargs)
|
10
42
|
|
11
|
-
|
43
|
+
# Get the model from the form instance
|
44
|
+
# Use a model bound to a form
|
45
|
+
model = self._meta.model
|
12
46
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
if obj.pk:
|
18
|
-
exclude_pks += [obj.pk]
|
19
|
-
# tn_get_descendants_pks changed to get_descendants_pks()
|
20
|
-
# exclude_pks += split_pks(obj.get_descendants_pks())
|
21
|
-
exclude_pks += obj.get_descendants_pks()
|
47
|
+
# Проверяем наличие tn_parent и исключаем текущий узел и его потомков
|
48
|
+
if "tn_parent" in self.fields and self.instance.pk:
|
49
|
+
excluded_ids = [self.instance.pk] + list(
|
50
|
+
self.instance.get_descendants_pks())
|
22
51
|
|
23
|
-
|
24
|
-
|
52
|
+
# Sort by tn_order
|
53
|
+
queryset = model.objects.exclude(pk__in=excluded_ids)
|
54
|
+
node_list = sorted(queryset, key=lambda x: x.tn_order)
|
55
|
+
pk_list = [node.pk for node in node_list]
|
56
|
+
queryset = queryset.filter(pk__in=pk_list).order_by(
|
57
|
+
Case(*[When(pk=pk, then=Value(index))
|
58
|
+
for index, pk in enumerate(pk_list)],
|
59
|
+
default=Value(len(pk_list)),
|
60
|
+
output_field=IntegerField())
|
61
|
+
)
|
25
62
|
|
26
|
-
|
27
|
-
|
63
|
+
# Set QuerySet
|
64
|
+
self.fields["tn_parent"].queryset = queryset
|
28
65
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
66
|
+
@classmethod
|
67
|
+
def factory(cls, model):
|
68
|
+
"""
|
69
|
+
Create a form class dynamically for the given TreeNode model.
|
70
|
+
|
71
|
+
This ensures that the form works with different concrete models.
|
72
|
+
"""
|
73
|
+
class Meta:
|
74
|
+
model = model
|
75
|
+
fields = "__all__"
|
76
|
+
widgets = {
|
77
|
+
"tn_parent": TreeWidget(
|
78
|
+
attrs={
|
79
|
+
"data-autocomplete-light": "true",
|
80
|
+
"data-url": "/tree-autocomplete/",
|
81
|
+
}
|
82
|
+
)
|
83
|
+
}
|
84
|
+
|
85
|
+
return type(f"{model.__name__}Form", (cls,), {"Meta": Meta})
|
86
|
+
|
87
|
+
|
88
|
+
# The End
|