django-fast-treenode 2.1.2__tar.gz → 2.1.3__tar.gz
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-2.1.2 → django_fast_treenode-2.1.3}/MANIFEST.in +1 -0
- {django_fast_treenode-2.1.2/django_fast_treenode.egg-info → django_fast_treenode-2.1.3}/PKG-INFO +5 -1
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/README.md +4 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3/django_fast_treenode.egg-info}/PKG-INFO +5 -1
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/django_fast_treenode.egg-info/SOURCES.txt +1 -1
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/about.md +19 -12
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/admin.md +6 -2
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/api.md +25 -20
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/cache.md +7 -0
- django_fast_treenode-2.1.3/docs/index.md +95 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/migration.md +1 -1
- django_fast_treenode-2.1.3/docs/roadmap.md +63 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/pyproject.toml +2 -1
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/setup.cfg +1 -1
- django_fast_treenode-2.1.2/treenode/tests/tests.py → django_fast_treenode-2.1.3/tests/test_suite.py +58 -50
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/cache.py +1 -1
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/node.py +1 -1
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/version.py +1 -1
- django_fast_treenode-2.1.2/docs/index.md +0 -30
- django_fast_treenode-2.1.2/docs/roadmap.md +0 -45
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/LICENSE +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/django_fast_treenode.egg-info/dependency_links.txt +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/django_fast_treenode.egg-info/requires.txt +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/django_fast_treenode.egg-info/top_level.txt +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/.gitignore +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/.nojekyll +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/import_export.md +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/installation.md +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/models.md +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/docs/requirements.txt +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/setup.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/__init__.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/admin/__init__.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/admin/admin.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/admin/changelist.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/admin/mixins.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/apps.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/forms.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/managers/__init__.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/managers/adjacency.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/managers/closure.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/__init__.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/adjacency.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/classproperty.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/closure.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/factory.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/__init__.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/ancestors.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/children.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/descendants.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/family.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/logical.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/properties.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/roots.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/siblings.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/tree.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/signals.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/.gitkeep +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/.gitkeep +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/css/.gitkeep +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/css/tree_widget.css +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/css/treenode_admin.css +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/js/.gitkeep +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/js/tree_widget.js +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/js/treenode_admin.js +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/.gitkeep +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/admin/.gitkeep +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/admin/export_success.html +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/admin/tree_node_changelist.html +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/admin/tree_node_export.html +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/admin/tree_node_import.html +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/admin/tree_node_import_report.html +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/admin/treenode_ajax_rows.html +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/widgets/tree_widget.css +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/widgets/tree_widget.html +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/urls.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/utils/__init__.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/utils/aid.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/utils/base16.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/utils/base36.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/utils/db.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/utils/exporter.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/utils/importer.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/utils/radix.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/views.py +0 -0
- {django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/widgets.py +0 -0
{django_fast_treenode-2.1.2/django_fast_treenode.egg-info → django_fast_treenode-2.1.3}/PKG-INFO
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 2.1.
|
3
|
+
Version: 2.1.3
|
4
4
|
Summary: Application for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
6
|
Author: Timur Kady
|
@@ -63,6 +63,10 @@ Requires-Dist: xlsxwriter; extra == "import-export"
|
|
63
63
|
# Django-fast-treenode
|
64
64
|
**Combining Adjacency List and Closure Table for Optimal Performance**
|
65
65
|
|
66
|
+
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
67
|
+
[](https://django-fast-treenode.readthedocs.io/)
|
68
|
+
[](https://pypi.org/project/django-fast-treenode/)
|
69
|
+
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
66
70
|
|
67
71
|
**Django Fast TreeNode** is a high-performance Django application for working with tree structures, combining **Adjacency List** and **Closure Table** models. Each **TreeNodeModel** instance maintains two synchronized tables, enabling most operations to be performed with a single database query.
|
68
72
|
|
@@ -1,6 +1,10 @@
|
|
1
1
|
# Django-fast-treenode
|
2
2
|
**Combining Adjacency List and Closure Table for Optimal Performance**
|
3
3
|
|
4
|
+
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
5
|
+
[](https://django-fast-treenode.readthedocs.io/)
|
6
|
+
[](https://pypi.org/project/django-fast-treenode/)
|
7
|
+
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
4
8
|
|
5
9
|
**Django Fast TreeNode** is a high-performance Django application for working with tree structures, combining **Adjacency List** and **Closure Table** models. Each **TreeNodeModel** instance maintains two synchronized tables, enabling most operations to be performed with a single database query.
|
6
10
|
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3/django_fast_treenode.egg-info}/PKG-INFO
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 2.1.
|
3
|
+
Version: 2.1.3
|
4
4
|
Summary: Application for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
6
|
Author: Timur Kady
|
@@ -63,6 +63,10 @@ Requires-Dist: xlsxwriter; extra == "import-export"
|
|
63
63
|
# Django-fast-treenode
|
64
64
|
**Combining Adjacency List and Closure Table for Optimal Performance**
|
65
65
|
|
66
|
+
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
67
|
+
[](https://django-fast-treenode.readthedocs.io/)
|
68
|
+
[](https://pypi.org/project/django-fast-treenode/)
|
69
|
+
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
66
70
|
|
67
71
|
**Django Fast TreeNode** is a high-performance Django application for working with tree structures, combining **Adjacency List** and **Closure Table** models. Each **TreeNodeModel** instance maintains two synchronized tables, enabling most operations to be performed with a single database query.
|
68
72
|
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/django_fast_treenode.egg-info/SOURCES.txt
RENAMED
@@ -22,6 +22,7 @@ docs/migration.md
|
|
22
22
|
docs/models.md
|
23
23
|
docs/requirements.txt
|
24
24
|
docs/roadmap.md
|
25
|
+
tests/test_suite.py
|
25
26
|
treenode/__init__.py
|
26
27
|
treenode/apps.py
|
27
28
|
treenode/cache.py
|
@@ -72,7 +73,6 @@ treenode/templates/admin/tree_node_import_report.html
|
|
72
73
|
treenode/templates/admin/treenode_ajax_rows.html
|
73
74
|
treenode/templates/widgets/tree_widget.css
|
74
75
|
treenode/templates/widgets/tree_widget.html
|
75
|
-
treenode/tests/tests.py
|
76
76
|
treenode/utils/__init__.py
|
77
77
|
treenode/utils/aid.py
|
78
78
|
treenode/utils/base16.py
|
@@ -1,31 +1,36 @@
|
|
1
1
|
## About the project
|
2
2
|
### The Debut Idea
|
3
|
+
|
3
4
|
The idea of this package belongs to **[Fabio Caccamo](https://github.com/fabiocaccamo)**. His idea was to use the **Adjacency List** method to store the data tree. The most probable and time-consuming requests are calculated in advance and stored in the database. Also, most requests are cached. As a result, query processing is carried out in one call to the database or without it at all.
|
4
5
|
|
5
6
|
The original application **[django-treenode](https://github.com/fabiocaccamo/django-treenode)** has significant advantages over other analogues, and indeed, is one of the best implementations of support for hierarchical structures for Django.
|
6
7
|
|
7
8
|
However, this application has a number of undeniable shortcomings:
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
|
10
|
+
- the selected pre-calculations scheme entails high costs for adding a new element;
|
11
|
+
- inserting new elements uses signals, which leads to failures when using bulk-operations;
|
12
|
+
- the problem of ordering elements by priority inside the parent node has not been resolved.
|
11
13
|
|
12
14
|
That is, an excellent debut idea, in my humble opinion, should have been improved.
|
13
15
|
|
14
16
|
### The Development of the Idea
|
17
|
+
|
15
18
|
My idea was to solve these problems by combining the **Adjacency List** with the **Closure Table**. Main advantages:
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
|
20
|
+
- the Closure Model is generated automatically;
|
21
|
+
- maintained compatibility with the original package at the level of documented functions;
|
22
|
+
- most requests are satisfied in one call to the database;
|
23
|
+
- inserting a new element takes two calls to the database without signals usage;
|
24
|
+
- bulk-operations are supported;
|
25
|
+
- the cost of creating a new dependency is reduced many times;
|
26
|
+
- useful functionality added for some methods (e.g. the `include_self=False` and `depth` parameters has been added to functions that return lists/querysets);
|
27
|
+
- additionally, the package includes a tree view widget for the `tn_parent` field in the change form.
|
24
28
|
|
25
29
|
Of course, at large levels of nesting, the use of the Closure Table leads to an increase in resource costs. However, the combined approach still outperforms both the original application and other available Django solutions in terms of performance, especially in large trees with over 100k nodes.
|
26
30
|
|
27
31
|
### The Theory
|
28
32
|
You can get a basic understanding of what is a **Closure Table** from:
|
33
|
+
|
29
34
|
* [presentation](https://www.slideshare.net/billkarwin/models-for-hierarchical-data) by **Bill Karwin**;
|
30
35
|
* [article](https://dirtsimple.org/2010/11/simplest-way-to-do-tree-based-queries.html) by blogger **Dirt Simple**;
|
31
36
|
* [article](https://towardsdatascience.com/closure-table-pattern-to-model-hierarchies-in-nosql-c1be6a87e05b) by **Andriy Zabavskyy**.
|
@@ -33,4 +38,6 @@ You can get a basic understanding of what is a **Closure Table** from:
|
|
33
38
|
You can easily find additional information on your own on the Internet.
|
34
39
|
|
35
40
|
### Our days
|
36
|
-
Over the course of development, the package has undergone significant improvements, with a strong emphasis on performance optimization, database efficiency, and seamless integration with Django’s admin interface.
|
41
|
+
Over the course of development, the package has undergone significant improvements, with a strong emphasis on performance optimization, database efficiency, and seamless integration with Django’s admin interface.
|
42
|
+
|
43
|
+
The introduction of a hybrid model combining **Adjacency List** and **Closure Table** has substantially reduced query overhead, improved scalability, and enhanced flexibility when managing hierarchical data. These advancements have made the package not only a powerful but also a practical solution for working with large tree structures. Moving forward, the project will continue to evolve, focusing on refining caching mechanisms, expanding compatibility with Django’s ecosystem, and introducing further optimizations to ensure maximum efficiency and ease of use.
|
@@ -25,6 +25,7 @@ class CategoryAdmin(TreeNodeModelAdmin):
|
|
25
25
|
The tree structure in the admin panel **loads dynamically as nodes are expanded**. This allows handling **large datasets** efficiently, preventing performance issues.
|
26
26
|
|
27
27
|
You can choose from three display modes:
|
28
|
+
|
28
29
|
- **`TREENODE_DISPLAY_MODE_ACCORDION` (default)**
|
29
30
|
Expands/collapses nodes dynamically.
|
30
31
|
- **`TREENODE_DISPLAY_MODE_BREADCRUMBS`**
|
@@ -43,10 +44,11 @@ The search bar helps quickly locate nodes within large trees. As you type, **an
|
|
43
44
|
|
44
45
|
#### Using TreeNodeForm
|
45
46
|
If you need to customize forms for tree-based models, inherit from `TreeNodeForm`. It provides:
|
47
|
+
|
46
48
|
- A **custom tree widget** for selecting parent nodes.
|
47
49
|
- Automatic **exclusion of self and descendants** from the parent selection to prevent circular references.
|
48
50
|
|
49
|
-
#####
|
51
|
+
##### forms.py:
|
50
52
|
```python
|
51
53
|
from treenode.forms import TreeNodeForm
|
52
54
|
from .models import Category
|
@@ -59,6 +61,7 @@ class CategoryForm(TreeNodeForm):
|
|
59
61
|
```
|
60
62
|
|
61
63
|
Key Considerations:
|
64
|
+
|
62
65
|
- This form automatically ensures that **a node cannot be its own parent**.
|
63
66
|
- It uses **`TreeWidget`**, a custom hierarchical dropdown for selecting parent nodes.
|
64
67
|
- If you need a form for another tree-based model, use the **dynamic factory method**:
|
@@ -75,7 +78,7 @@ This method ensures that the form correctly associates with different tree model
|
|
75
78
|
#### The TreeWidget Class
|
76
79
|
The `TreeWidget` class is a **custom Select2-like widget** that enables hierarchical selection in forms. While it is used inside the Django admin panel by default, it can **also be used in regular forms** outside the admin panel.
|
77
80
|
|
78
|
-
#####
|
81
|
+
##### widgets.py
|
79
82
|
|
80
83
|
```python
|
81
84
|
from django import forms
|
@@ -91,6 +94,7 @@ class CategorySelectionForm(forms.Form):
|
|
91
94
|
```
|
92
95
|
|
93
96
|
Important Notes:
|
97
|
+
|
94
98
|
- **Requires jQuery**: The widget relies on AJAX requests, so ensure jQuery is available when using it outside Django’s admin.
|
95
99
|
- **Dynamically Fetches Data**: It loads the tree structure asynchronously, preventing performance issues with large datasets.
|
96
100
|
- **Customizable Data Source**: The `data-url` attribute can be adjusted to fetch tree data from a custom endpoint.
|
@@ -94,6 +94,7 @@ These methods are designed to manage **direct child nodes** within the tree stru
|
|
94
94
|
obj.add_child(position=None, **kwargs)
|
95
95
|
```
|
96
96
|
`position` specifies the order position of the object being added in the list of children of this node. It can be `'first-child'`, `'last-child'`, `'sorted-child'`, or an integer value.
|
97
|
+
|
97
98
|
The `**kwargs` parameters contain the object creation data that will be passed to the inherited node model. Instead of passing the object creation data, you can pass an already created (but not yet saved) model instance to insert into the tree using the `instance` keyword.
|
98
99
|
|
99
100
|
```python
|
@@ -201,10 +202,10 @@ obj.get_family()
|
|
201
202
|
### Node Utility Methods
|
202
203
|
This set of methods helps manage node-related operations such as:
|
203
204
|
|
204
|
-
-
|
205
|
-
-
|
206
|
-
-
|
207
|
-
-
|
205
|
+
- Breadcrumbs generation
|
206
|
+
- Depth, index, and level calculations
|
207
|
+
- Materialized path retrieval for sorting
|
208
|
+
- Dynamic node positioning within the tree
|
208
209
|
|
209
210
|
#### get_breadcrumbs
|
210
211
|
Returns the **breadcrumbs** to current node (included):
|
@@ -246,19 +247,20 @@ obj.insert_at(target, position='first-child', save=False)
|
|
246
247
|
```
|
247
248
|
|
248
249
|
Parameters:
|
250
|
+
|
249
251
|
- `target`: еhe target node relative to which this node will be placed.
|
250
252
|
- `position`: the position, relative to the target node, where the current node object will be moved to, can be one of:
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
253
|
+
- `first-root`: the node will be the first root node;
|
254
|
+
- `last-root`: the node will be the last root node;
|
255
|
+
- `sorted-root`: the new node will be moved after sorting by the treenode_sort_field field;
|
256
|
+
- `first-sibling`: the node will be the new leftmost sibling of the target node;
|
257
|
+
- `left-sibling`: the node will take the target node’s place, which will be moved to the target position with shifting follows nodes;
|
258
|
+
- `right-sibling`: the node will be moved to the position after the target node;
|
259
|
+
- `last-sibling`: the node will be the new rightmost sibling of the target node;
|
260
|
+
- `sorted-sibling`: the new node will be moved after sorting by the treenode_sort_field field;
|
261
|
+
- first-child: the node will be the first child of the target node;
|
262
|
+
- last-child: the node will be the new rightmost child of the target
|
263
|
+
- sorted-child: the new node will be moved after sorting by the treenode_sort_field field.
|
262
264
|
- `save` : if `save=true`, the node will be saved in the tree. Otherwise, the method will return a model instance with updated fields: parent field and position in sibling list.
|
263
265
|
|
264
266
|
Before using this method, the model instance must be correctly created with all required fields defined. If the model has required fields, then simply creating an object and calling insert_at() will not work, because Django will raise an exception.
|
@@ -268,9 +270,11 @@ Moves the model instance relative to the target node and sets its position (if n
|
|
268
270
|
```python
|
269
271
|
obj.move_to(target, position=0)
|
270
272
|
```
|
273
|
+
|
271
274
|
Parameters:
|
272
|
-
|
273
|
-
- `
|
275
|
+
|
276
|
+
- `target`: the target node relative to which this node will be placed.
|
277
|
+
- `position`: the position, relative to the target node, where the current node object will be moved to. For detals see [insert_at](#insert_at) method.
|
274
278
|
|
275
279
|
#### get_path
|
276
280
|
Returns Materialized Path of node. The materialized path is constructed by recording the position of each node within its parent's list of children, tracing this sequence back through all its ancestors.
|
@@ -538,6 +542,7 @@ Something like this will be returned:
|
|
538
542
|
All nodes are ordered by materialized path.
|
539
543
|
|
540
544
|
This can be used with a template like this:
|
545
|
+
|
541
546
|
```html
|
542
547
|
{% for item, info in annotated_list %}
|
543
548
|
{% if info.open %}
|
@@ -546,7 +551,7 @@ This can be used with a template like this:
|
|
546
551
|
</li><li>
|
547
552
|
{% endif %}
|
548
553
|
|
549
|
-
{{ item }}
|
554
|
+
{{ item }}
|
550
555
|
|
551
556
|
{% for close in info.close %}
|
552
557
|
</li></ul>
|
@@ -663,7 +668,7 @@ Returns the children pks list. See [`get_children_pks()`](#get_children_pks) met
|
|
663
668
|
#### obj.depth
|
664
669
|
Returns the node depth. See [`get_depth()`](#get_depth) method.
|
665
670
|
|
666
|
-
|
671
|
+
#### obj.descendants:
|
667
672
|
Returns a list containing all descendants (itself is not included). See [`get_descendants()`](#get_descendants) method.
|
668
673
|
|
669
674
|
#### obj.descendants_count
|
@@ -693,7 +698,7 @@ Returns node parent pk. See [`get_parent_pk()`](#get_parent_pk) method.
|
|
693
698
|
#### obj.priority
|
694
699
|
Returns node oder position (priority). See [`get_priority()`](#get_priority) method.
|
695
700
|
|
696
|
-
|
701
|
+
#### cls.roots
|
697
702
|
Returns a list with all root nodes. See [`get_roots()`](#get_roots) method.
|
698
703
|
|
699
704
|
#### obj.root
|
@@ -27,6 +27,7 @@ TREENODE_CACHE_LIMIT = 100
|
|
27
27
|
**Automatic Management**. In most cases, users don’t need to manually manage cache operations.All methods that somehow change the state of models reset the tree cache automatically.
|
28
28
|
|
29
29
|
**Manual Cache Clearing**. If for some reason you need to reset the cache, you can do it in two ways:
|
30
|
+
|
30
31
|
- **Clear cache for a single model**: Use `clear_cache()` at the model level:
|
31
32
|
```python
|
32
33
|
MyTreeNodeModel.clear_cache()
|
@@ -116,6 +117,7 @@ treenode_cache.clear()
|
|
116
117
|
This completely resets the cache, removing **all stored values**.
|
117
118
|
|
118
119
|
Best Practices:
|
120
|
+
|
119
121
|
- **Always use `generate_cache_key()`** instead of hardcoding cache keys to ensure consistency.
|
120
122
|
- **Use `invalidate()` instead of `clear()`** when targeting a specific model’s cache.
|
121
123
|
- **Apply `@cached_method` wisely**, ensuring it is used **only for** `TreeNodeModel`-based methods to avoid conflicts.
|
@@ -137,6 +139,7 @@ The cache is now divided into two queues:
|
|
137
139
|
- **LRU (20%-30%)** – for frequently accessed objects (Least Recently Used).
|
138
140
|
|
139
141
|
How it works:
|
142
|
+
|
140
143
|
- If tree queries are evenly distributed, the cache behaves like a standard FIFO.
|
141
144
|
- If certain nodes receive frequent queries, those records automatically move to LRU, reducing database load.
|
142
145
|
|
@@ -145,10 +148,12 @@ How it works:
|
|
145
148
|
Each new entry initially goes into **FIFO**. If accessed **more than N times**, it moves to **LRU**, where it remains longer.
|
146
149
|
|
147
150
|
**How N is determined:**
|
151
|
+
|
148
152
|
- A threshold is set based on **mathematical statistics**: N = 1 / sqrt(2).
|
149
153
|
- An absolute threshold limit is added.
|
150
154
|
|
151
155
|
**LRU Behavior:**
|
156
|
+
|
152
157
|
- When accessed, a record moves to the top of the list.
|
153
158
|
- New records are also placed at the top.
|
154
159
|
- If LRU reaches capacity, **the evicted record returns to FIFO** instead of being deleted.
|
@@ -162,11 +167,13 @@ When data is modified or deleted, the cache **automatically resets** to prevent
|
|
162
167
|
To automatically clear outdated records, an **adaptive mechanism** is used. Instead of a static TTL, the **DTT parameter** is dynamically calculated based on **Poisson distribution**.
|
163
168
|
|
164
169
|
How DTT is calculated:
|
170
|
+
|
165
171
|
1. Compute the **average interval between queries (T)**: ``` T = (1/N) * Σ (t_i - t_(i-1)), i=1...N```
|
166
172
|
2. Store **averaged value** `ΣΔt / N`
|
167
173
|
3. Set **DTT = 3T**, which removes **95% of infrequent queries**.
|
168
174
|
|
169
175
|
**Why this is better than a fixed TTL:**
|
176
|
+
|
170
177
|
- If queries are rare → DTT **increases**, preventing premature deletions.
|
171
178
|
- If queries are frequent → DTT **decreases**, accelerating cache clearing.
|
172
179
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# Django Fast TreeNode
|
2
|
+
**A Powerful Solution for Hierarchical Data Management**
|
3
|
+
|
4
|
+
Welcome to the **django-fast-treenode** documentation!
|
5
|
+
|
6
|
+
## Overview
|
7
|
+
|
8
|
+
**Django Fast TreeNode** is a specialized Django package designed to efficiently handle hierarchical data structures. It leverages **combining Adjacency List and Closure Table** to optimize querying and management of tree-like structures, making it an excellent choice for applications that require fast traversal, retrieval, and manipulation of nested relationships.
|
9
|
+
|
10
|
+
The package is particularly useful in scenarios such as:
|
11
|
+
|
12
|
+
- **Categories and taxonomies**: Manage product categories, tags, and classification systems.
|
13
|
+
- **Menus and navigation**: Create tree-like menus and nested navigation structures.
|
14
|
+
- **Forums and comments**: Store threaded discussions and nested comment chains.
|
15
|
+
- **Geographical data**: Represent administrative divisions, regions, and areas of influence.
|
16
|
+
- **Organizational and Business Structures**: Model company hierarchies, business processes, employees and departments.
|
17
|
+
|
18
|
+
**django-fast-treenode** models show excellent **performance** and **stability** in all applications.
|
19
|
+
|
20
|
+
## Key Features
|
21
|
+
|
22
|
+
1. **Efficient Tree Operations**:
|
23
|
+
* Supports fast retrieval of ancestors, descendants, and siblings.
|
24
|
+
* Uses a **closure table approach** for optimal query performance.
|
25
|
+
|
26
|
+
2. **Django ORM Integration**:
|
27
|
+
* Extends Django’s `models.Model`, making it easy to integrate into existing applications.
|
28
|
+
* Custom manager (`TreeNodeManager`) provides useful tree-related query optimizations.
|
29
|
+
|
30
|
+
|
31
|
+
3. **Query Optimization via Closure Table**
|
32
|
+
* Instead of naive recursive queries, the package precomputes relationships in a separate closure table.This allows you to run constant-time queries for retrieval using a **single database query**:
|
33
|
+
* Ancestors (`get_ancestors()`),
|
34
|
+
* Depth (`det_depth()`),
|
35
|
+
* Descendants (`get_descendants()`),
|
36
|
+
* Family (`get_family()`),
|
37
|
+
* Materialized Path(`get_breadcrumbs()` and `get_path()`),
|
38
|
+
* Roots (`get_root()`),
|
39
|
+
* Siblings (`get_siblings()`) and so on.
|
40
|
+
|
41
|
+
4. **Automatic Ordering and Priority Management**:
|
42
|
+
* Nodes can be assigned priority values for custom sorting.
|
43
|
+
* Provides automatic ordering based on a **materialized path**.
|
44
|
+
|
45
|
+
5. **Admin Interface Enhancements**:
|
46
|
+
* Supports multiple tree display modes in the Django Admin:
|
47
|
+
* **Indentation mode** (classic hierarchical view)
|
48
|
+
* **Breadcrumbs mode** (for easy navigation)
|
49
|
+
* **Accordion mode** (collapsible structures)
|
50
|
+
* Uses a **custom admin widget** (`TreeWidget`) to enhance usability.
|
51
|
+
|
52
|
+
|
53
|
+
6. **Caching for Performance**:
|
54
|
+
* Uses Django’s caching framework to optimize frequently accessed tree operations.
|
55
|
+
* Cached tree methods reduce redundant computations.
|
56
|
+
|
57
|
+
|
58
|
+
7. **Bulk Operations Support**:
|
59
|
+
* Implements efficient **bulk creation** of nodes.
|
60
|
+
* Provides methods for **batch updates** and **tree rebuilding**.
|
61
|
+
|
62
|
+
By leveraging combining with Closure Tables, it offers superior performance compared to traditional tree structures.
|
63
|
+
|
64
|
+
## Use Cases and Benefits
|
65
|
+
|
66
|
+
### When to Use Django Fast TreeNode?
|
67
|
+
- **You need a nested structure with frequent reads**: Closure tables provide **fast lookups** compared to recursive Common Table Expressions (CTEs).
|
68
|
+
- **You want an easy-to-use Django model**: The package **extends Django ORM** and integrates seamlessly.
|
69
|
+
- **You require hierarchical display in Django Admin**: The **custom admin integration** makes managing trees much easier.
|
70
|
+
|
71
|
+
### Benefits Over Other Approaches
|
72
|
+
- **Better than adjacency lists** (which require multiple queries for deep trees).
|
73
|
+
- **More efficient than recursive queries** (which can be slow on large datasets).
|
74
|
+
- **Scalable for large trees** (by leveraging precomputed paths and caching).
|
75
|
+
|
76
|
+
Django Fast TreeNode is a powerful and efficient package for managing hierarchical data in Django applications. Its seamless ORM integration and Django Admin support make it a great choice for developers looking to implement **fast, scalable** tree-based data structures.
|
77
|
+
|
78
|
+
## Contents
|
79
|
+
- [About the Project](about.md)
|
80
|
+
- [Installation, Configuration, and Fine-Tuning](installation.md)
|
81
|
+
- [Model Inheritance and Extensions](models.md)
|
82
|
+
- [Working with Admin Classes](admin.md)
|
83
|
+
- [API Reference](api.md)
|
84
|
+
- [Import & Export](import_export.md)
|
85
|
+
- [Caching and Cache Management](cache.md)
|
86
|
+
- [Migration and Upgrade Guide](migration.md)
|
87
|
+
- [Roadmap](roadmap.md)
|
88
|
+
|
89
|
+
## Links
|
90
|
+
- [Issues](https://github.com/TimurKady/django-fast-treenode/issues)
|
91
|
+
- [Pull Requests](https://github.com/TimurKady/django-fast-treenode/pulls)
|
92
|
+
- [Discussions](https://github.com/TimurKady/django-fast-treenode/discussions)
|
93
|
+
|
94
|
+
## License
|
95
|
+
Released under the [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
|
@@ -111,7 +111,7 @@ The `django-fast-treenode` package contains the full set of methods you are used
|
|
111
111
|
|
112
112
|
|**django-treebeard** | **django-fast-treenode** |**Features of use**|
|
113
113
|
|---------------------|----------------------|----------------------|
|
114
|
-
| The `pos` parameter in `add_sibling()` and `move()` methods | The parameter `pos` has the name `position` | • The `position` parameter in `django-fast-treenode` always consists of two parts separated by
|
114
|
+
| The `pos` parameter in `add_sibling()` and `move()` methods | The parameter `pos` has the name `position` | • The `position` parameter in `django-fast-treenode` always consists of two parts separated by <br>`-`: the first part determines the insertion method (_first, left, right, last, sorted_), the second — the placement type (_child, sibling_). <br> • Instead of a string format, you can also pass position as an integer indicating the exact position of the node in the list.|
|
115
115
|
|`get_siblings()`, `get_ancestors()` end ect. | Similar methods have parameters `include_self` and `depth` |• The `include_self` parameter specifies whether to include the node itself in the selection. <br> • The `depth` parameter specifies the depth of the search. |
|
116
116
|
|`move(target, pos)` metiod| The method `move()` has the name `move_to(target, position)` | - |
|
117
117
|
|
@@ -0,0 +1,63 @@
|
|
1
|
+
## Roadmap
|
2
|
+
|
3
|
+
### Last Update
|
4
|
+
The latest version provides optimized database operations, an improved caching mechanism, and enhanced integration capabilities, making it a **robust and efficient choice** for handling tree structures.
|
5
|
+
|
6
|
+
**Version 2.1 – Compatibility and Optimization**
|
7
|
+
Reducing dependencies and simplifying migration from other libraries.
|
8
|
+
|
9
|
+
- **Expanding functionality** to simplify migration from other tree packages.
|
10
|
+
- Introducing node **filtering and search with AJAX**.
|
11
|
+
- **Optimizing query performance** by reducing query complexity and improving indexing strategies.
|
12
|
+
- **Reducing database load** from the `TreeNodeAdmin` when working with large trees.
|
13
|
+
- Removing `numpy` in favor of lighter alternatives.
|
14
|
+
- Removing `Select2` in favor of light custom select2-like implementation.
|
15
|
+
|
16
|
+
|
17
|
+
### Planned Roadmap for Future Versions
|
18
|
+
The **django-fast-treenode** package will continue to evolve from its original concept—combining the benefits of the **Adjacency List** and **Closure Table** models—into a high-performance solution for managing and visualizing hierarchical data in Django projects. The focus is on **speed, usability, and flexibility**.
|
19
|
+
|
20
|
+
#### Version 2.2 – Performance Enhancements
|
21
|
+
Focusing on optimizing query efficiency, reducing database load, and improving API interactions.
|
22
|
+
|
23
|
+
- **Optimized Query Execution**: Minimize query overhead by restructuring SQL operations, reducing redundant lookups, and introducing batched operations for bulk inserts.
|
24
|
+
- **API-First CRUD Implementation**: Introduce full Create, Read, Update, and Delete (CRUD) operations for tree structures, ensuring seamless API-based interaction with hierarchical data.
|
25
|
+
- **Efficient Serialization**: Develop a lightweight tree serialization format optimized for API responses, reducing payload size while preserving structural integrity.
|
26
|
+
- **Advanced Node Filtering & Search**: Implement AJAX-based filtering and search mechanisms in Django Admin and API endpoints to enhance usability and response time.
|
27
|
+
|
28
|
+
#### Version 2.3 – Drag-and-Drop and Admin UI Improvements
|
29
|
+
Improving the usability and management of hierarchical structures in Django Admin.
|
30
|
+
|
31
|
+
- **Drag-and-Drop Node Reordering**: Introduce interactive drag-and-drop functionality for reordering nodes within the tree structure directly in Django Admin.
|
32
|
+
- **Hierarchical Sorting Strategies**: Enable various sorting methods, including manual ordering, weight-based prioritization, and hybrid approaches that combine automatic and manual sorting.
|
33
|
+
- **Admin Panel Enhancements**: Expand Django Admin capabilities for tree structures, including better visualization, inline node editing, and bulk actions.
|
34
|
+
|
35
|
+
#### Version 3.0 – Third-Generation Cache Management System
|
36
|
+
Introducing a more advanced caching system to improve scalability and efficiency.
|
37
|
+
|
38
|
+
- **Two-Level FIFO/LRU Cache**: Implement a hybrid caching mechanism combining First-In-First-Out (FIFO) and Least Recently Used (LRU) strategies to optimize cache retention for tree nodes.
|
39
|
+
- **Multi-Process Cache Synchronization**: Ensure cache consistency across different execution environments (WSGI, Gunicorn, Uvicorn) with a distributed synchronization mechanism.
|
40
|
+
- **Background Synchronization**: Introduce delayed closure table updates via Celery or RQ, preventing blocking operations while maintaining data consistency.
|
41
|
+
|
42
|
+
#### Version 4.0 – Asynchronous Operations
|
43
|
+
Refactoring the package to support fully asynchronous operations for non-blocking execution.
|
44
|
+
|
45
|
+
- **Asynchronous API Execution**: Convert existing synchronous operations to asynchronous, leveraging `async/await` for improved performance.
|
46
|
+
- **Async Database Support**: Implement async-friendly database operations compatible with Django’s evolving asynchronous ORM.
|
47
|
+
- **Optimized Tree Node Caching**: Shift from caching precomputed query results to caching raw tree nodes, reducing recomputation overhead and improving retrieval speed.
|
48
|
+
- **Asynchronous Testing**: Expand the test suite to cover async behavior and the new caching mechanism under concurrent loads.
|
49
|
+
- **Documentation Update**: Revise and expand documentation to reflect changes in the asynchronous execution model and best practices for implementation.
|
50
|
+
|
51
|
+
#### Version 5.0 – Beyond Django ORM
|
52
|
+
Decoupling tree structure management from Django’s ORM to increase flexibility and adaptability.
|
53
|
+
|
54
|
+
- **Multi-Backend Storage Support**: Introduce support for alternative storage engines beyond Django ORM, such as SQLAlchemy, custom PostgreSQL functions, and other database frameworks.
|
55
|
+
- **Redis Integration for In-Memory Trees**: Implement an optional Redis-based tree storage system, allowing high-speed in-memory hierarchy operations.
|
56
|
+
- **JSON-Based Storage Option**: Enable lightweight embedded tree storage using JSON structures, facilitating easier use in API-driven and microservice architectures.
|
57
|
+
- **ORM-Agnostic API Layer**: Design an API-first approach that allows tree structures to function independently from Django models, making the package usable in broader contexts.
|
58
|
+
|
59
|
+
So, each milestone is designed to improve performance, scalability, and flexibility, ensuring that the package remains relevant for modern web applications, API-driven architectures, and high-performance data processing environments support.
|
60
|
+
|
61
|
+
Stay tuned for updates!
|
62
|
+
|
63
|
+
Your wishes, objections, and comments are welcome.
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "django-fast-treenode"
|
7
|
-
version = "2.1.
|
7
|
+
version = "2.1.3"
|
8
8
|
description = "Application for supporting tree (hierarchical) data structure in Django projects"
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [{ name = "Timur Kady", email = "timurkady@yandex.com" }]
|
@@ -48,3 +48,4 @@ Homepage = "https://github.com/TimurKady/django-fast-treenode"
|
|
48
48
|
Documentation = "https://django-fast-treenode.readthedocs.io/"
|
49
49
|
Source = "https://github.com/TimurKady/django-fast-treenode"
|
50
50
|
Issues = "https://github.com/TimurKady/django-fast-treenode/issues"
|
51
|
+
|
django_fast_treenode-2.1.2/treenode/tests/tests.py → django_fast_treenode-2.1.3/tests/test_suite.py
RENAMED
@@ -1,9 +1,18 @@
|
|
1
|
+
import os
|
2
|
+
import django
|
1
3
|
import time
|
2
4
|
import traceback
|
5
|
+
from django.db import models
|
3
6
|
from django.test import TestCase, TransactionTestCase
|
4
|
-
from
|
7
|
+
from . import settings
|
8
|
+
from .models import TestModel
|
5
9
|
|
6
10
|
|
11
|
+
if "DJANGO_SETTINGS_MODULE" not in os.environ:
|
12
|
+
os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings"
|
13
|
+
|
14
|
+
django.setup()
|
15
|
+
|
7
16
|
class BasicOperationsTest(TestCase):
|
8
17
|
"""
|
9
18
|
Tests basic operations with nodes: insertion (using three methods), deletion,
|
@@ -16,7 +25,7 @@ class BasicOperationsTest(TestCase):
|
|
16
25
|
|
17
26
|
# 1. Insertion via node.save()
|
18
27
|
try:
|
19
|
-
node1 =
|
28
|
+
node1 = TestModel(name='Node via save()')
|
20
29
|
node1.save()
|
21
30
|
if not node1.pk:
|
22
31
|
errors.append("Insertion via save() did not assign a PK.")
|
@@ -25,7 +34,7 @@ class BasicOperationsTest(TestCase):
|
|
25
34
|
|
26
35
|
# 2. Insertion via objects.create()
|
27
36
|
try:
|
28
|
-
node2 =
|
37
|
+
node2 = TestModel.objects.create(name='Node via create()')
|
29
38
|
if not node1.pk:
|
30
39
|
errors.append(
|
31
40
|
"Insertion via objects.create() did not assign a PK.")
|
@@ -36,11 +45,11 @@ class BasicOperationsTest(TestCase):
|
|
36
45
|
# 3. Alternative method (insert_at or get_or_create)
|
37
46
|
try:
|
38
47
|
# For demonstration, use node1 as the parent if the insert_at method exists
|
39
|
-
if hasattr(
|
40
|
-
node3 =
|
48
|
+
if hasattr(TestModel, 'insert_at'):
|
49
|
+
node3 = TestModel(name='Node via insert_at()')
|
41
50
|
node3.insert_at(node1)
|
42
|
-
elif hasattr(
|
43
|
-
node3, created =
|
51
|
+
elif hasattr(TestModel.objects, 'get_or_create'):
|
52
|
+
node3, created = TestModel.objects.get_or_create(
|
44
53
|
name='Node via get_or_create()', defaults={'tn_parent': node1})
|
45
54
|
else:
|
46
55
|
errors.append(
|
@@ -52,8 +61,8 @@ class BasicOperationsTest(TestCase):
|
|
52
61
|
# Checking access methods (e.g. get_children)
|
53
62
|
try:
|
54
63
|
# Create a small tree
|
55
|
-
root =
|
56
|
-
child =
|
64
|
+
root = TestModel.objects.create(name='Root')
|
65
|
+
child = TestModel.objects.create(name='Child', tn_parent=root)
|
57
66
|
# If the model defines a get_children() method, check its functionality
|
58
67
|
if hasattr(root, 'get_children'):
|
59
68
|
children = root.get_children()
|
@@ -62,7 +71,7 @@ class BasicOperationsTest(TestCase):
|
|
62
71
|
"Method get_children() did not return the added child.")
|
63
72
|
else:
|
64
73
|
# If the method does not exist, perform a simple filter by parent
|
65
|
-
children =
|
74
|
+
children = TestModel.objects.filter(tn_parent=root)
|
66
75
|
if child not in children:
|
67
76
|
errors.append(
|
68
77
|
"Filter by parent did not return the added child.")
|
@@ -99,25 +108,24 @@ class DeepTreePerformanceTest(TransactionTestCase):
|
|
99
108
|
try:
|
100
109
|
# Insertion of the root node
|
101
110
|
start = time.time()
|
102
|
-
root =
|
111
|
+
root = TestModel.objects.create(name="Deep Root")
|
103
112
|
end = time.time()
|
104
113
|
timings[0] = end - start
|
105
114
|
nodes.append(root)
|
106
115
|
|
107
116
|
current_parent = root
|
108
117
|
|
109
|
-
for level in range(1,
|
118
|
+
for level in range(1, 101):
|
110
119
|
start = time.time()
|
111
|
-
new_node =
|
120
|
+
new_node = TestModel.objects.create(
|
112
121
|
name=f"Node Level {level}", tn_parent=current_parent)
|
113
122
|
end = time.time()
|
114
123
|
timings[level] = end - start
|
115
124
|
nodes.append(new_node)
|
116
125
|
current_parent = new_node # the next node will be a child of the one just created
|
117
126
|
|
118
|
-
# Generate report for levels: 0, 1, 10, 20, 30, 40, 50, 70, 80, 90, 100
|
119
|
-
levels_to_report = [0, 1, 10, 20, 30,
|
120
|
-
40, 50, 70, 80, 90, 100, 150, 200]
|
127
|
+
# Generate report for levels: 0, 1, 10, 20, 30, 40, 50, 70, 80, 90, 100
|
128
|
+
levels_to_report = [0, 1, 10, 20, 30,40, 50, 70, 80, 90, 100]
|
121
129
|
# to avoid division by zero
|
122
130
|
root_time = timings[0] if timings[0] > 0 else 1e-6
|
123
131
|
print("Deep tree - node insertion times:")
|
@@ -154,11 +162,11 @@ class WideTreePerformanceTest(TransactionTestCase):
|
|
154
162
|
# For 100 trees
|
155
163
|
for root_index in range(1, 101):
|
156
164
|
tree_start = time.time()
|
157
|
-
root =
|
165
|
+
root = TestModel.objects.create(name=f"Wide Root {root_index}")
|
158
166
|
current_parent = root
|
159
167
|
# Create 5 levels of nesting for each root
|
160
168
|
for level in range(1, 6):
|
161
|
-
new_node =
|
169
|
+
new_node = TestModel.objects.create(
|
162
170
|
name=f"Node {root_index}-{level}", tn_parent=current_parent)
|
163
171
|
current_parent = new_node
|
164
172
|
tree_end = time.time()
|
@@ -190,29 +198,29 @@ class MassInsertionTest(TestCase):
|
|
190
198
|
for i in range(50, 76):
|
191
199
|
# 1. Insertion via save()
|
192
200
|
try:
|
193
|
-
node =
|
201
|
+
node = TestModel(name=f"Mass Node {i} via save()")
|
194
202
|
node.save()
|
195
203
|
except Exception as e:
|
196
204
|
errors.append(f"Error inserting node {i} via save(): {e}")
|
197
205
|
|
198
206
|
# 2. Insertion via objects.create()
|
199
207
|
try:
|
200
|
-
|
208
|
+
TestModel.objects.create(name=f"Mass Node {i} via create()")
|
201
209
|
except Exception as e:
|
202
210
|
errors.append(f"Error inserting node {i} via create(): {e}")
|
203
211
|
|
204
212
|
# 3. Alternative method (insert_at or get_or_create)
|
205
213
|
try:
|
206
214
|
# Take the first node in the database or create a parent
|
207
|
-
parent =
|
215
|
+
parent = TestModel.objects.first()
|
208
216
|
if not parent:
|
209
|
-
parent =
|
217
|
+
parent = TestModel.objects.create(name="Default Parent")
|
210
218
|
|
211
|
-
if hasattr(
|
212
|
-
node =
|
219
|
+
if hasattr(TestModel, 'insert_at'):
|
220
|
+
node = TestModel(name=f"Mass Node {i} via insert_at()")
|
213
221
|
node.insert_at(parent)
|
214
|
-
elif hasattr(
|
215
|
-
|
222
|
+
elif hasattr(TestModel.objects, 'get_or_create'):
|
223
|
+
TestModel.objects.get_or_create(
|
216
224
|
name=f"Mass Node {i} via get_or_create()", defaults={'tn_parent': parent})
|
217
225
|
else:
|
218
226
|
errors.append(
|
@@ -246,20 +254,20 @@ class NodeDeletionTest(TestCase):
|
|
246
254
|
errors = []
|
247
255
|
print("\n=== NodeDeletionTest ===")
|
248
256
|
try:
|
249
|
-
parent =
|
250
|
-
child1 =
|
251
|
-
child2 =
|
257
|
+
parent = TestModel.objects.create(name="Deletion Parent")
|
258
|
+
child1 = TestModel.objects.create(name="Child 1", tn_parent=parent)
|
259
|
+
child2 = TestModel.objects.create(name="Child 2", tn_parent=parent)
|
252
260
|
|
253
261
|
# Delete child1
|
254
262
|
child1.delete()
|
255
|
-
remaining_children =
|
263
|
+
remaining_children = TestModel.objects.filter(tn_parent=parent)
|
256
264
|
if child1 in remaining_children:
|
257
265
|
errors.append(
|
258
266
|
"The deleted child1 is still present among the parent's children.")
|
259
267
|
|
260
268
|
# Delete the parent and check cascading deletion
|
261
269
|
parent.delete()
|
262
|
-
if
|
270
|
+
if TestModel.objects.filter(pk=child2.pk).exists():
|
263
271
|
errors.append(
|
264
272
|
"Child2 was not deleted after the parent was deleted (cascading deletion was expected).")
|
265
273
|
except Exception as e:
|
@@ -288,17 +296,17 @@ class NodeMovingTest(TestCase):
|
|
288
296
|
errors = []
|
289
297
|
print("\n=== NodeMovingTest ===")
|
290
298
|
try:
|
291
|
-
parent1 =
|
292
|
-
parent2 =
|
293
|
-
child =
|
299
|
+
parent1 = TestModel.objects.create(name="Original Parent")
|
300
|
+
parent2 = TestModel.objects.create(name="New Parent")
|
301
|
+
child = TestModel.objects.create(
|
294
302
|
name="Movable Child", tn_parent=parent1)
|
295
303
|
|
296
304
|
# Moving the node
|
297
305
|
child.set_parent(parent2)
|
298
306
|
child.save()
|
299
307
|
|
300
|
-
children1 =
|
301
|
-
children2 =
|
308
|
+
children1 = TestModel.objects.filter(tn_parent=parent1)
|
309
|
+
children2 = TestModel.objects.filter(tn_parent=parent2)
|
302
310
|
if child in children1:
|
303
311
|
errors.append(
|
304
312
|
"The node is still present in the original parent's children after moving.")
|
@@ -330,10 +338,10 @@ class NodeUpdateTest(TestCase):
|
|
330
338
|
errors = []
|
331
339
|
print("\n=== NodeUpdateTest ===")
|
332
340
|
try:
|
333
|
-
node =
|
341
|
+
node = TestModel.objects.create(name="Original Name")
|
334
342
|
node.name = "Updated Name"
|
335
343
|
node.save()
|
336
|
-
updated_node =
|
344
|
+
updated_node = TestModel.objects.get(pk=node.pk)
|
337
345
|
if updated_node.name != "Updated Name":
|
338
346
|
errors.append("The node's name was not updated correctly.")
|
339
347
|
except Exception as e:
|
@@ -362,12 +370,12 @@ class DataIntegrityTest(TestCase):
|
|
362
370
|
errors = []
|
363
371
|
print("\n=== DataIntegrityTest ===")
|
364
372
|
try:
|
365
|
-
root =
|
366
|
-
child1 =
|
373
|
+
root = TestModel.objects.create(name="Integrity Root")
|
374
|
+
child1 = TestModel.objects.create(
|
367
375
|
name="Integrity Child 1", tn_parent=root)
|
368
|
-
child2 =
|
376
|
+
child2 = TestModel.objects.create(
|
369
377
|
name="Integrity Child 2", tn_parent=root)
|
370
|
-
grandchild =
|
378
|
+
grandchild = TestModel.objects.create(
|
371
379
|
name="Integrity Grandchild", tn_parent=child1)
|
372
380
|
|
373
381
|
# Move child2 under child1
|
@@ -381,7 +389,7 @@ class DataIntegrityTest(TestCase):
|
|
381
389
|
grandchild.delete()
|
382
390
|
|
383
391
|
# Verify relationships
|
384
|
-
root_children =
|
392
|
+
root_children = TestModel.objects.filter(tn_parent=root)
|
385
393
|
if child1 not in root_children:
|
386
394
|
errors.append(
|
387
395
|
"Child1 not found among the root node's children.")
|
@@ -389,12 +397,12 @@ class DataIntegrityTest(TestCase):
|
|
389
397
|
errors.append(
|
390
398
|
"Child2 is present among the root node's children after moving.")
|
391
399
|
|
392
|
-
child1_children =
|
400
|
+
child1_children = TestModel.objects.filter(tn_parent=child1)
|
393
401
|
if child2 not in child1_children:
|
394
402
|
errors.append(
|
395
403
|
"Child2 not found among child1's children after moving.")
|
396
404
|
|
397
|
-
if
|
405
|
+
if TestModel.objects.filter(name="Integrity Grandchild").exists():
|
398
406
|
errors.append("Grandchild still exists after deletion.")
|
399
407
|
except Exception as e:
|
400
408
|
errors.append("Exception in DataIntegrityTest: " + str(e))
|
@@ -421,14 +429,14 @@ class LargeVolumeTest(TransactionTestCase):
|
|
421
429
|
errors = []
|
422
430
|
print("\n=== LargeVolumeTest ===")
|
423
431
|
try:
|
424
|
-
initial_count =
|
432
|
+
initial_count = TestModel.objects.count()
|
425
433
|
for i in range(1000):
|
426
434
|
try:
|
427
|
-
|
435
|
+
TestModel.objects.create(name=f"Large Node {i}")
|
428
436
|
except Exception as e:
|
429
437
|
errors.append(
|
430
438
|
f"Error inserting node {i} in large data volume: {e}")
|
431
|
-
final_count =
|
439
|
+
final_count = TestModel.objects.count()
|
432
440
|
if final_count - initial_count != 1000:
|
433
441
|
errors.append(
|
434
442
|
f"Expected 1000 new nodes, but got {final_count - initial_count}.")
|
@@ -459,7 +467,7 @@ class InvalidDataTest(TestCase):
|
|
459
467
|
try:
|
460
468
|
try:
|
461
469
|
# Attempt to create a node with invalid data
|
462
|
-
|
470
|
+
TestModel.objects.create(name=None)
|
463
471
|
errors.append(
|
464
472
|
"Creating a node with None for name did not raise an exception as expected.")
|
465
473
|
except Exception:
|
@@ -468,7 +476,7 @@ class InvalidDataTest(TestCase):
|
|
468
476
|
|
469
477
|
# Additionally, one can test creating a node with an empty string if that is not allowed
|
470
478
|
try:
|
471
|
-
node =
|
479
|
+
node = TestModel(name="")
|
472
480
|
node.save()
|
473
481
|
# If an empty string is allowed, then no error should be recorded
|
474
482
|
except Exception as e:
|
@@ -23,7 +23,7 @@ class TreeNodeNodeMixin(models.Model):
|
|
23
23
|
abstract = True
|
24
24
|
|
25
25
|
@cached_method
|
26
|
-
def get_breadcrumbs(self, attr='
|
26
|
+
def get_breadcrumbs(self, attr='id'):
|
27
27
|
"""Optimized breadcrumbs retrieval with direct cache check."""
|
28
28
|
try:
|
29
29
|
self._meta.get_field(attr)
|
@@ -1,30 +0,0 @@
|
|
1
|
-
# Django Fast TreeNode
|
2
|
-
|
3
|
-
Welcome to the **django-fast-treenode** documentation!
|
4
|
-
|
5
|
-
## Key Features
|
6
|
-
**django-fast-treenode** is a high-performance solution for managing tree structures in Django.
|
7
|
-
|
8
|
-
- **Fast processing** even with deep trees
|
9
|
-
- **Intuitive API** for seamless integration
|
10
|
-
- **Django Admin support** for easy management
|
11
|
-
- **Optimized caching** for enhanced performance
|
12
|
-
|
13
|
-
## Contents
|
14
|
-
- [About the Project](about.md)
|
15
|
-
- [Installation, Configuration, and Fine-Tuning](installation.md)
|
16
|
-
- [Model Inheritance and Extensions](models.md)
|
17
|
-
- [Working with Admin Classes](admin.md)
|
18
|
-
- [API Reference](api.md)
|
19
|
-
- [Import & Export](import_export.md)
|
20
|
-
- [Caching and Cache Management](cache.md)
|
21
|
-
- [Migration and Upgrade Guide](migration.md)
|
22
|
-
- [Roadmap](roadmap.md)
|
23
|
-
|
24
|
-
## Links
|
25
|
-
- [Issues](https://github.com/TimurKady/django-fast-treenode/issues)
|
26
|
-
- [Pull Requests](https://github.com/TimurKady/django-fast-treenode/pulls)
|
27
|
-
- [Discussions](https://github.com/TimurKady/django-fast-treenode/discussions)
|
28
|
-
|
29
|
-
## License
|
30
|
-
Released under the [MIT License](https://github.com/TimurKady/django-fast-treenode/blob/main/LICENSE).
|
@@ -1,45 +0,0 @@
|
|
1
|
-
## Roadmap
|
2
|
-
The latest version provides optimized database operations, an improved caching mechanism, and enhanced integration capabilities, making it a **robust and efficient choice** for handling tree structures.
|
3
|
-
|
4
|
-
But the **django-fast-treenode** package will continue to evolve from its original concept—combining the benefits of the **Adjacency List** and **Closure Table** models—into a high-performance solution for managing and visualizing hierarchical data in Django projects. The focus is on **speed, usability, and flexibility**.
|
5
|
-
|
6
|
-
In future versions, I plan to implement:
|
7
|
-
|
8
|
-
### **Version 2.1 – Compatibility and Optimization**
|
9
|
-
Reducing dependencies and simplifying migration from other libraries.
|
10
|
-
- **Expanding functionality** to simplify migration from other tree packages.
|
11
|
-
- Introducing node **filtering and search with AJAX**.
|
12
|
-
- **Optimizing query performance** by reducing query complexity and improving indexing strategies.
|
13
|
-
- **Reducing database load** from the `TreeNodeAdmin` when working with large trees.
|
14
|
-
- Removing `numpy` in favor of lighter alternatives.
|
15
|
-
|
16
|
-
### **Version 2.2 – Caching Improvements**
|
17
|
-
Speeding up tree operations and reducing database load.
|
18
|
-
- **Implementing a more efficient caching algorithm** to optimize performance and reduce recomputation.
|
19
|
-
- **Refining cache expiration strategies** for better memory management.
|
20
|
-
|
21
|
-
### **Version 2.3 – Performance Enhancements**
|
22
|
-
- **Reducing query overhead** and optimizing bulk operations for large datasets.
|
23
|
-
- **Django REST Framework (DRF)**: Adding initial integration for efficient tree-based API handling.
|
24
|
-
- **Serialization**: Developing lightweight tree serialization for API-first projects.
|
25
|
-
- **Enhancing node filtering and search** with AJAX-driven interfaces.
|
26
|
-
|
27
|
-
### **Version 2.4 – Drag-and-Drop and Admin UI Improvements**
|
28
|
-
- **Drag-and-Drop**: Adding drag-and-drop support for node sorting in Django admin.
|
29
|
-
- **Advanced Sorting**: Adding multiple sorting strategies for tree nodes, including manual ordering and hierarchical priority rules.
|
30
|
-
- **Admin Panel Enhancements**: Expanding the functionality of the Django admin interface for managing hierarchical data more efficiently.
|
31
|
-
|
32
|
-
### **Version 3.0 – Asynchronous operations and Fourth-Generation Cache Management System**
|
33
|
-
- **Asynchronous operations** support, ensuring efficient working and data processing in non-blocking environments.
|
34
|
-
- **Shifting from caching computed results to directly caching tree nodes**, reducing recomputation overhead and improving cache efficiency.
|
35
|
-
- **Reworking Adjacency List and Closure Table managers** for a new caching system.
|
36
|
-
- **Enhancing cache consistency** across multiple processes (WSGI, Gunicorn, Uvicorn) with a global synchronization mechanism.
|
37
|
-
|
38
|
-
### **Version 4.0 – Moving Beyond Django ORM**
|
39
|
-
Enabling tree structures to function without a strict dependency on Django ORM while maintaining compatibility.
|
40
|
-
- **Introducing support for various storage backends**, allowing greater flexibility.
|
41
|
-
- **Adding compatibility with Redis for in-memory tree storage** and JSON-based trees for lightweight embedded storage.
|
42
|
-
- **Expanding usage in API-first projects**, enabling tree structures to work independently from Django models.
|
43
|
-
|
44
|
-
Stay tuned for updates!
|
45
|
-
Your wishes, objections, and comments are welcome.
|
File without changes
|
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/django_fast_treenode.egg-info/requires.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/__init__.py
RENAMED
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/ancestors.py
RENAMED
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/children.py
RENAMED
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/descendants.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/properties.py
RENAMED
File without changes
|
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/models/mixins/siblings.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/css/.gitkeep
RENAMED
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/js/.gitkeep
RENAMED
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/static/treenode/js/tree_widget.js
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{django_fast_treenode-2.1.2 → django_fast_treenode-2.1.3}/treenode/templates/widgets/tree_widget.css
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|