django-fast-treenode 1.1.3__py3-none-any.whl → 2.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/METADATA +127 -46
  2. django_fast_treenode-2.0.1.dist-info/RECORD +41 -0
  3. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/WHEEL +1 -1
  4. treenode/__init__.py +0 -7
  5. treenode/admin.py +327 -82
  6. treenode/apps.py +20 -3
  7. treenode/cache.py +231 -0
  8. treenode/docs/Documentation +101 -54
  9. treenode/forms.py +75 -19
  10. treenode/managers.py +260 -48
  11. treenode/models/__init__.py +7 -0
  12. treenode/models/classproperty.py +24 -0
  13. treenode/models/closure.py +168 -0
  14. treenode/models/factory.py +71 -0
  15. treenode/models/proxy.py +650 -0
  16. treenode/static/treenode/css/tree_widget.css +62 -0
  17. treenode/static/treenode/css/treenode_admin.css +106 -0
  18. treenode/static/treenode/js/tree_widget.js +161 -0
  19. treenode/static/treenode/js/treenode_admin.js +171 -0
  20. treenode/templates/admin/export_success.html +26 -0
  21. treenode/templates/admin/tree_node_changelist.html +11 -0
  22. treenode/templates/admin/tree_node_export.html +27 -0
  23. treenode/templates/admin/tree_node_import.html +27 -0
  24. treenode/templates/widgets/tree_widget.css +23 -0
  25. treenode/templates/widgets/tree_widget.html +21 -0
  26. treenode/urls.py +34 -0
  27. treenode/utils/__init__.py +4 -0
  28. treenode/utils/base36.py +35 -0
  29. treenode/utils/exporter.py +141 -0
  30. treenode/utils/importer.py +296 -0
  31. treenode/version.py +11 -1
  32. treenode/views.py +102 -2
  33. treenode/widgets.py +49 -27
  34. django_fast_treenode-1.1.3.dist-info/RECORD +0 -33
  35. treenode/compat.py +0 -8
  36. treenode/factory.py +0 -68
  37. treenode/models.py +0 -668
  38. treenode/static/select2tree/.gitkeep +0 -1
  39. treenode/static/select2tree/select2tree.css +0 -176
  40. treenode/static/select2tree/select2tree.js +0 -181
  41. treenode/static/treenode/css/treenode.css +0 -85
  42. treenode/static/treenode/js/treenode.js +0 -201
  43. treenode/templates/widgets/.gitkeep +0 -1
  44. treenode/templates/widgets/attrs.html +0 -7
  45. treenode/templates/widgets/options.html +0 -1
  46. treenode/templates/widgets/select2tree.html +0 -22
  47. treenode/tests.py +0 -3
  48. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/LICENSE +0 -0
  49. {django_fast_treenode-1.1.3.dist-info → django_fast_treenode-2.0.1.dist-info}/top_level.txt +0 -0
  50. /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
@@ -1,15 +1,16 @@
1
1
  # Django-fast-treenode
2
2
  __Combination of Adjacency List and Closure Table__
3
3
 
4
- ## Features
4
+ ## Functions
5
5
  Application for supporting tree (hierarchical) data structure in Django projects
6
- * faster,
7
- * synced: in-memory model instances are automatically updated,
8
- * compatibility: you can easily add treenode to existing projects,
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 configuration: just extend the abstract model / model-admin,
11
- * admin integration: visualization options (accordion, breadcrumbs or indentation),
12
- * widget: build-in Select2-to-Tree extends Select2 to support arbitrary level of nesting.
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 ```django-fast-treenode``` to ```settings.INSTALLED_APPS```
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
- When updating an existing project, simply call ```cls.update_tree()``` function once.
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** if `cascade=True` (default behaviour), children and descendants will be deleted too,
191
- otherwise children's parent will be set to `None` (then children become roots):
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
- obj.delete(cascade=True)
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
- ## Cautions
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.
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
- 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).
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
- Special thanks to [Mathieu Leplatre](https://blog.mathieu-leplatre.info/pages/about.html) for the advice used in writing this application
619
+ # Django-fast-treenode
579
620
 
580
- ## To do
581
- Future plans:
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
- Your wishes, objections, comments are welcome.
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
- # -*- coding: utf-8 -*-
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
- super(TreeNodeForm, self).__init__(*args, **kwargs)
43
+ # Get the model from the form instance
44
+ # Use a model bound to a form
45
+ model = self._meta.model
12
46
 
13
- if 'tn_parent' not in self.fields:
14
- return
15
- exclude_pks = []
16
- obj = self.instance
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
- # Cheaged to "legal" call
24
- manager = obj._meta.model.objects
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
- self.fields['tn_parent'].queryset = manager.prefetch_related(
27
- 'tn_children').exclude(pk__in=exclude_pks)
63
+ # Set QuerySet
64
+ self.fields["tn_parent"].queryset = queryset
28
65
 
29
- class Meta:
30
- widgets = {
31
- 'tn_parent': TreeWidget(attrs={'style': 'min-width:400px'}),
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