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.
Files changed (50) hide show
  1. {django_fast_treenode-1.1.2.dist-info → django_fast_treenode-2.0.0.dist-info}/METADATA +156 -44
  2. django_fast_treenode-2.0.0.dist-info/RECORD +41 -0
  3. {django_fast_treenode-1.1.2.dist-info → django_fast_treenode-2.0.0.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 +130 -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.2.dist-info/RECORD +0 -33
  35. treenode/compat.py +0 -8
  36. treenode/factory.py +0 -68
  37. treenode/models.py +0 -669
  38. treenode/static/select2tree/.gitkeep +0 -1
  39. treenode/static/select2tree/select2tree.css +0 -176
  40. treenode/static/select2tree/select2tree.js +0 -171
  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.2.dist-info → django_fast_treenode-2.0.0.dist-info}/LICENSE +0 -0
  49. {django_fast_treenode-1.1.2.dist-info → django_fast_treenode-2.0.0.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,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
- ## Usage
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** 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):
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
- obj.delete(cascade=True)
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
- ## License
566
- Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
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
- ## 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.
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
- ## 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)
649
+ # Django-fast-treenode
650
+ ## License
651
+ Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
577
652
 
578
- Special thanks to [Mathieu Leplatre](https://blog.mathieu-leplatre.info/pages/about.html) for the advice used in writing this application
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
- ## 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.
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
- Your wishes, objections, comments are welcome.
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
- # -*- 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