django-fast-treenode 1.1.2__py3-none-any.whl → 2.0.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {django_fast_treenode-1.1.2.dist-info → django_fast_treenode-2.0.0.dist-info}/METADATA +156 -44
- django_fast_treenode-2.0.0.dist-info/RECORD +41 -0
- {django_fast_treenode-1.1.2.dist-info → django_fast_treenode-2.0.0.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 +130 -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.2.dist-info/RECORD +0 -33
- treenode/compat.py +0 -8
- treenode/factory.py +0 -68
- treenode/models.py +0 -669
- treenode/static/select2tree/.gitkeep +0 -1
- treenode/static/select2tree/select2tree.css +0 -176
- treenode/static/select2tree/select2tree.js +0 -171
- 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.2.dist-info → django_fast_treenode-2.0.0.dist-info}/LICENSE +0 -0
- {django_fast_treenode-1.1.2.dist-info → django_fast_treenode-2.0.0.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,42 @@ 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
|
-
##
|
131
|
+
## Updating to django-fast-treenode 2.X
|
132
|
+
### Overview
|
133
|
+
If you are upgrading from a previous version, you need to follow these steps to ensure compatibility and proper functionality.
|
134
|
+
|
135
|
+
### Update Process
|
136
|
+
1. **Upgrade the package**
|
137
|
+
Run the following command to install the latest version:
|
138
|
+
|
139
|
+
```bash
|
140
|
+
pip install --upgrade django-fast-treenode
|
141
|
+
```
|
142
|
+
|
143
|
+
2. **Apply database migrations**
|
144
|
+
After upgrading, you need to apply the necessary database migrations:
|
145
|
+
|
146
|
+
```bash
|
147
|
+
python manage.py makemigrations
|
148
|
+
python manage.py migrate
|
149
|
+
```
|
150
|
+
|
151
|
+
3. **Restart the application**
|
152
|
+
Finally, restart your Django application to apply all changes:
|
153
|
+
|
154
|
+
```bash
|
155
|
+
python manage.py runserver
|
156
|
+
```
|
157
|
+
|
158
|
+
**Important Notes**: Failing to apply migrations (`migrate`) after upgrading may lead to errors when interacting with tree nodes.
|
134
159
|
|
160
|
+
By following these steps, you will ensure a smooth transition to the latest version of django-fast-treenode without data inconsistencies.
|
161
|
+
|
162
|
+
|
163
|
+
## Usage
|
135
164
|
### Methods/Properties
|
136
165
|
|
137
166
|
- [`delete`](#delete)
|
@@ -157,7 +186,6 @@ class YoursForm(TreeNodeForm):
|
|
157
186
|
- [`get_last_child`](#get_last_child)
|
158
187
|
- [`get_level`](#get_level)
|
159
188
|
- [`get_order`](#get_order)
|
160
|
-
- [`get_ordered_queryset`](#get_ordered_queryset)
|
161
189
|
- [`get_parent`](#get_parent)
|
162
190
|
- [`get_parent_pk`](#get_parent_pk)
|
163
191
|
- [`set_parent`](#set_parent)
|
@@ -187,11 +215,17 @@ class YoursForm(TreeNodeForm):
|
|
187
215
|
- [`update_tree`](#update_tree)
|
188
216
|
|
189
217
|
#### `delete`
|
190
|
-
**Delete a node**
|
191
|
-
|
218
|
+
**Delete a node** provides two deletion strategies:
|
219
|
+
- **Cascade Delete (`cascade=True`)**: Removes the node along with all its descendants.
|
220
|
+
- **Reparenting (`cascade=False`)**: Moves the children of the deleted node up one level in the hierarchy before removing the node itself.
|
221
|
+
|
192
222
|
```python
|
193
|
-
|
223
|
+
node.delete(cascade=True) # Deletes node and all its descendants
|
224
|
+
node.delete(cascade=False) # Moves children up and then deletes the node
|
194
225
|
```
|
226
|
+
This ensures greater flexibility in managing tree structures while preventing orphaned nodes.
|
227
|
+
|
228
|
+
---
|
195
229
|
|
196
230
|
#### `delete_tree`
|
197
231
|
**Delete the whole tree** for the current node class:
|
@@ -313,6 +347,8 @@ obj.get_descendants_tree()
|
|
313
347
|
obj.descendants_tree
|
314
348
|
```
|
315
349
|
|
350
|
+
**Important**: In future projects, avoid using `get_descendants_tree()`. It will be removed in the next version.
|
351
|
+
|
316
352
|
#### `get_descendants_tree_display`
|
317
353
|
Get a **multiline** `string` representing the **model tree**:
|
318
354
|
```python
|
@@ -321,6 +357,8 @@ obj.get_descendants_tree_display(include_self=False, depth=None)
|
|
321
357
|
obj.descendants_tree_display
|
322
358
|
```
|
323
359
|
|
360
|
+
**Important**: In future projects, avoid using `get_descendants_tree_display()`. It will be removed in the next version.
|
361
|
+
|
324
362
|
#### `get_first_child`
|
325
363
|
Get the **first child node**:
|
326
364
|
```python
|
@@ -361,23 +399,6 @@ obj.get_order()
|
|
361
399
|
obj.order
|
362
400
|
```
|
363
401
|
|
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
402
|
#### `get_parent`
|
382
403
|
Get the **parent node**:
|
383
404
|
```python
|
@@ -561,28 +582,83 @@ obj.is_sibling_of(target_obj)
|
|
561
582
|
```python
|
562
583
|
cls.update_tree()
|
563
584
|
```
|
585
|
+
## **Cache Management**
|
586
|
+
### **Overview**
|
587
|
+
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.
|
588
|
+
|
589
|
+
### **Key Features**
|
590
|
+
**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`.
|
591
|
+
**settings.py**
|
592
|
+
``` python
|
593
|
+
TREENODE_CACHE_LIMIT = 100
|
594
|
+
```
|
595
|
+
**Automatic Management**: In most cases, users don’t need to manually manage cache operations.
|
596
|
+
**Manual Cache Clearing**:
|
597
|
+
- **Clear cache for a single model**: Use `clear_cache()` at the model level:
|
598
|
+
```python
|
599
|
+
MyTreeNodeModel.clear_cache()
|
600
|
+
```
|
601
|
+
- **Clear cache for all models**: Use the global `treenode_cache.clear()` method:
|
602
|
+
```python
|
603
|
+
from treenode.cache import treenode_cache
|
604
|
+
treenode_cache.clear()
|
605
|
+
```
|
606
|
+
|
607
|
+
## **Export and Import Functionality**
|
608
|
+
### **Overview**
|
609
|
+
TreeNode v2.0 includes **built-in export and import features** for easier data migration. Supported Formats: `csv`, `json`, `xlsx`, `yaml`, `tsv`
|
610
|
+
### Installation for Import/Export Features
|
611
|
+
By default, import/export functionality is **not included** to keep the package lightweight. If you need these features, install the package with:
|
612
|
+
```bash
|
613
|
+
pip install django-fast-treenode[import_export]
|
614
|
+
```
|
615
|
+
Once installed, **import/export buttons will appear** in the Django admin interface.
|
616
|
+
### **Important Considerations**
|
617
|
+
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.
|
618
|
+
|
619
|
+
## Migration Guide
|
620
|
+
#### Switching from `django-treenode`
|
621
|
+
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.
|
622
|
+
|
623
|
+
|
624
|
+
#### Upgrading to `django-fast-treenode` 2.0
|
625
|
+
To upgrade to version 2.0, simply run:
|
626
|
+
```bash
|
627
|
+
pip install --upgrade django-fast-treenode
|
628
|
+
```
|
629
|
+
or
|
630
|
+
```bash
|
631
|
+
pip install django-fast-treenode[import_export]
|
632
|
+
```
|
633
|
+
After upgrading, ensure that your database schema is up to date by running:
|
634
|
+
```bash
|
635
|
+
python manage.py makemigrations
|
636
|
+
python manage.py migrate
|
637
|
+
```
|
638
|
+
This will apply any necessary database changes automatically.
|
564
639
|
|
565
|
-
##
|
566
|
-
|
640
|
+
## To do
|
641
|
+
These improvements aim to enhance usability, performance, and maintainability for all users of `django-fast-treenode`:
|
642
|
+
* **Cache Algorithm Optimization**: Testing and integrating more advanced cache eviction strategies.
|
643
|
+
* **Drag-and-Drop UI Enhancements**: Adding intuitive drag-and-drop functionality for tree node management.
|
644
|
+
* to be happy, to don't worry, until die.
|
567
645
|
|
568
|
-
|
569
|
-
The code provided is intended for testing by developers and is not recommended for use in production projects. Only general tests were carried out. The risk of using the code lies entirely with you.
|
646
|
+
Your wishes, objections, comments are welcome.
|
570
647
|
|
571
|
-
Don't access treenode fields directly! Most of them have been removed as unnecessary. Use functions documented in the [source application](https://github.com/fabiocaccamo/django-treenode).
|
572
648
|
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
* [Select2-to-Tree](https://github.com/clivezhg/select2-to-tree) Select2 extension by [clivezhg](https://github.com/clivezhg)
|
649
|
+
# Django-fast-treenode
|
650
|
+
## License
|
651
|
+
Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
|
577
652
|
|
578
|
-
|
653
|
+
## Cautions
|
654
|
+
**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.
|
579
655
|
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
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.
|
656
|
+
**Risks of Direct Field Access:**
|
657
|
+
- **Database Integrity Issues**: Directly modifying fields may break tree integrity, causing inconsistent parent-child relationships.
|
658
|
+
- **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.
|
659
|
+
- **Unsupported Behavior**: Future versions may change field structures or remove unnecessary fields. Relying on them directly risks breaking compatibility.
|
586
660
|
|
661
|
+
Instead, always use the **documented methods** described above or refer to the [original application documentation](https://github.com/fabiocaccamo/django-treenode).
|
587
662
|
|
588
|
-
|
663
|
+
## Credits
|
664
|
+
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
|