django-fast-treenode 3.0.2__py3-none-any.whl → 3.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_fast_treenode-3.0.2.dist-info → django_fast_treenode-3.0.4.dist-info}/METADATA +24 -25
- {django_fast_treenode-3.0.2.dist-info → django_fast_treenode-3.0.4.dist-info}/RECORD +25 -25
- {django_fast_treenode-3.0.2.dist-info → django_fast_treenode-3.0.4.dist-info}/WHEEL +1 -1
- treenode/admin/admin.py +1 -0
- treenode/admin/mixin.py +1 -1
- treenode/managers/queries.py +6 -18
- treenode/managers/tasks.py +80 -42
- treenode/static/vendors/jquery-ui/AUTHORS.txt +384 -384
- treenode/static/vendors/jquery-ui/LICENSE.txt +43 -43
- treenode/static/vendors/jquery-ui/external/jquery/jquery.js +10716 -10716
- treenode/static/vendors/jquery-ui/index.html +297 -297
- treenode/static/vendors/jquery-ui/jquery-ui.css +438 -438
- treenode/static/vendors/jquery-ui/jquery-ui.js +5222 -5222
- treenode/static/vendors/jquery-ui/jquery-ui.min.css +6 -6
- treenode/static/vendors/jquery-ui/jquery-ui.min.js +5 -5
- treenode/static/vendors/jquery-ui/jquery-ui.structure.css +16 -16
- treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css +4 -4
- treenode/static/vendors/jquery-ui/jquery-ui.theme.css +439 -439
- treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css +4 -4
- treenode/static/vendors/jquery-ui/package.json +82 -82
- treenode/utils/db/sqlcompat.py +33 -1
- treenode/utils/db/sqlquery.py +24 -0
- treenode/version.py +2 -2
- {django_fast_treenode-3.0.2.dist-info → django_fast_treenode-3.0.4.dist-info}/licenses/LICENSE +0 -0
- {django_fast_treenode-3.0.2.dist-info → django_fast_treenode-3.0.4.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 3.0.
|
3
|
+
Version: 3.0.4
|
4
4
|
Summary: Treenode Framework for supporting tree (hierarchical) data structure in Django projects
|
5
5
|
Home-page: https://django-fast-treenode.readthedocs.io/
|
6
6
|
Author: Timur Kady
|
@@ -53,7 +53,7 @@ Classifier: Operating System :: OS Independent
|
|
53
53
|
Requires-Python: >=3.9
|
54
54
|
Description-Content-Type: text/markdown
|
55
55
|
License-File: LICENSE
|
56
|
-
Requires-Dist: Django>=
|
56
|
+
Requires-Dist: Django>=5.0
|
57
57
|
Requires-Dist: msgpack>=1.0.0
|
58
58
|
Requires-Dist: openpyxl>=3.0.0
|
59
59
|
Requires-Dist: pyyaml>=5.1
|
@@ -62,16 +62,16 @@ Dynamic: home-page
|
|
62
62
|
Dynamic: license-file
|
63
63
|
Dynamic: requires-python
|
64
64
|
|
65
|
-
#
|
65
|
+
# Treenode Framework
|
66
66
|
**A hybrid open-source framework for working with trees in Django**
|
67
67
|
|
68
68
|
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
69
69
|
[](https://django-fast-treenode.readthedocs.io/)
|
70
|
-
[](https://pypi.org/project/django-fast-
|
70
|
+
[](https://pypi.org/project/django-fast-Treenode/)
|
71
71
|
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
72
72
|
[](https://github.com/sponsors/TimurKady)
|
73
73
|
|
74
|
-
## About The
|
74
|
+
## About The Treenode Framework
|
75
75
|
### Overview
|
76
76
|
|
77
77
|
**Treenode Framework** is an advanced tree management system for Django applications.It is designed to handle large-scale, deeply nested, and highly dynamic tree structures while maintaining excellent performance, data integrity, and ease of use.
|
@@ -87,7 +87,7 @@ Its core philosophy: **maximum scalability, minimum complexity**.
|
|
87
87
|
|
88
88
|
### Key Features
|
89
89
|
#### Common operations
|
90
|
-
The `django-fast-
|
90
|
+
The `django-fast-Treenode` package supports all the basic operations needed to work with tree structures:
|
91
91
|
|
92
92
|
- Extracting **ancestors** (queryset, list, pks, count);
|
93
93
|
- Extracting **children** (queryset, list, pks, count);
|
@@ -118,10 +118,10 @@ Typical applications include:
|
|
118
118
|
|
119
119
|
In all these domains, scalable and fast tree management is not a luxury — it's a necessity.
|
120
120
|
|
121
|
-
### Why
|
121
|
+
### Why Treenode Framework?
|
122
122
|
At the moment, django-fast-treeenode is, if not the best, then one of the best packages for working with tree data under Djangjo.
|
123
123
|
|
124
|
-
- **High performance**: [tests show](docs/about.md#benchmark-tests) that on trees of 5k-10k nodes with a nesting depth of 500-600 levels, **Treenode Framework** (`django-fast-
|
124
|
+
- **High performance**: [tests show](docs/about.md#benchmark-tests) that on trees of 5k-10k nodes with a nesting depth of 500-600 levels, **Treenode Framework** (`django-fast-Treenode`) shows **performance 4-7 times better** than the main popular packages.
|
125
125
|
- **Flexible API**: today contains the widest set of methods for working with a tree in comparison with other packages.
|
126
126
|
- **Convenient administration**: the admin panel interface was developed taking into account the experience of using other packages. It provides convenience and intuitiveness with ease of programming.
|
127
127
|
- **Scalability**: **Treenode Framework** suitable for solving simple problems such as menus, directories, parsing arithmetic expressions, as well as complex problems such as program optimization, image layout, multi-step decision making problems, or machine learning..
|
@@ -134,7 +134,7 @@ To get started quickly, you need to follow these steps:
|
|
134
134
|
|
135
135
|
- Simply install the package via `pip`:
|
136
136
|
```sh
|
137
|
-
pip install django-fast-
|
137
|
+
pip install django-fast-Treenode
|
138
138
|
```
|
139
139
|
- Once installed, add `'treenode'` to your `INSTALLED_APPS` in **settings.py**:
|
140
140
|
```python {title="settings.py"}
|
@@ -147,9 +147,9 @@ To get started quickly, you need to follow these steps:
|
|
147
147
|
|
148
148
|
- Open **models.py** and create your own tree class:
|
149
149
|
```
|
150
|
-
from
|
150
|
+
from Treenode.models import TreenodeModel
|
151
151
|
|
152
|
-
class MyTree(
|
152
|
+
class MyTree(TreenodeModel):
|
153
153
|
name = models.CharField(max_length=255)
|
154
154
|
display_field = "name"
|
155
155
|
```
|
@@ -157,11 +157,11 @@ To get started quickly, you need to follow these steps:
|
|
157
157
|
- Open **admin.py** and create a model for the admin panel
|
158
158
|
```
|
159
159
|
from django.contrib import admin
|
160
|
-
from
|
161
|
-
from .models import
|
160
|
+
from Treenode.admin import TreenodeModelAdmin
|
161
|
+
from .models import MyTree
|
162
162
|
|
163
|
-
@admin.register(
|
164
|
-
class
|
163
|
+
@admin.register(MyTree)
|
164
|
+
class MyTreeAdmin(TreenodeModelAdmin):
|
165
165
|
list_display = ("name",)
|
166
166
|
search_fields = ("name",)
|
167
167
|
```
|
@@ -176,25 +176,24 @@ To get started quickly, you need to follow these steps:
|
|
176
176
|
```sh
|
177
177
|
python manage.py runserver
|
178
178
|
```
|
179
|
-
|
180
179
|
Everything is ready, enjoy 🎉!
|
181
180
|
|
182
181
|
## Documentation
|
183
|
-
Full documentation is available at **[ReadTheDocs](https://django-fast-
|
182
|
+
Full documentation is available at **[ReadTheDocs](https://django-fast-Treenode.readthedocs.io/)**.
|
184
183
|
|
185
184
|
Quick access links:
|
186
|
-
* [Installation, configuration and fine tuning](https://django-fast-
|
187
|
-
* [Model Inheritance and Extensions](https://django-fast-
|
188
|
-
* [Working with Admin Classes](https://django-fast-
|
189
|
-
* [API Reference](https://django-fast-
|
190
|
-
* [Import & Export](https://django-fast-
|
191
|
-
* [Caching and working with cache](https://django-fast-
|
192
|
-
* [Migration and upgrade guide](https://django-fast-
|
185
|
+
* [Installation, configuration and fine tuning](https://django-fast-Treenode.readthedocs.io/installation/)
|
186
|
+
* [Model Inheritance and Extensions](https://django-fast-Treenode.readthedocs.io/models/)
|
187
|
+
* [Working with Admin Classes](https://django-fast-Treenode.readthedocs.io/admin/)
|
188
|
+
* [API Reference](https://django-fast-Treenode.readthedocs.io/api/)
|
189
|
+
* [Import & Export](https://django-fast-Treenode.readthedocs.io/import_export/)
|
190
|
+
* [Caching and working with cache](https://django-fast-Treenode.readthedocs.io/cache/)
|
191
|
+
* [Migration and upgrade guide](https://django-fast-Treenode.readthedocs.io/migration/)
|
193
192
|
|
194
193
|
Your wishes, objections, comments are welcome.
|
195
194
|
|
196
195
|
## License
|
197
|
-
Released under [MIT License](https://github.com/TimurKady/django-fast-
|
196
|
+
Released under [MIT License](https://github.com/TimurKady/django-fast-Treenode/blob/main/LICENSE).
|
198
197
|
|
199
198
|
## Credits
|
200
199
|
Thanks to everyone who contributed to the development and testing of this package, as well as the Django community for their inspiration and support.
|
@@ -1,4 +1,4 @@
|
|
1
|
-
django_fast_treenode-3.0.
|
1
|
+
django_fast_treenode-3.0.4.dist-info/licenses/LICENSE,sha256=SSYqS84FCnAW7tAxmjBKU8qAa8Jv4VGPuSSGeHwWtJE,1095
|
2
2
|
treenode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
treenode/apps.py,sha256=QlwjNDM9rkUoWB8Vm8-OkS6lNx0-aTByuGZlu9wrQMs,1832
|
4
4
|
treenode/cache.py,sha256=2jUiiecfFxwB7QFukpU4u0FnDzGH6hNRfo6KAYvs6vM,8447
|
@@ -7,18 +7,18 @@ treenode/settings.py,sha256=oSkcKXNVd28HXrlWZIH2VYinCMq-UdCDlX4KD0Qc_Xk,631
|
|
7
7
|
treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
|
8
8
|
treenode/tests.py,sha256=2uDafv3Ns6f7Vy1ekUtgYxCZEi1KRyesZDTAFhYcX-E,63
|
9
9
|
treenode/urls.py,sha256=krHvVigc_dxC0z5hEd2rgeH6th8jW7qJY3Qbia-419Y,240
|
10
|
-
treenode/version.py,sha256=
|
10
|
+
treenode/version.py,sha256=agtMVvI22kmKiOiAyXlZeP14ej675T1Yd8F0FzHfXXk,220
|
11
11
|
treenode/widgets.py,sha256=61ed16bVqb1_R97jekDrbKS5pDVK4nzXSDwL3CDBYEk,4075
|
12
12
|
treenode/admin/__init__.py,sha256=XNEYHdF5lKb0vpdlVxdR2fxj5oUgzyx1YyCwsv0gxHw,100
|
13
|
-
treenode/admin/admin.py,sha256=
|
13
|
+
treenode/admin/admin.py,sha256=9_GC6JjWRiG_G84royRGAr10BSNIAj4N_33JXdzP8IY,7733
|
14
14
|
treenode/admin/changelist.py,sha256=KUYS9MaR8Ck_1xmMqupobxWKarrJEqmHuEG32CL01Bo,1662
|
15
15
|
treenode/admin/exporter.py,sha256=QE74V6W3tvwA5kCvBt1MmVlLOaWh-o8EU63cgmiwD5Q,5724
|
16
16
|
treenode/admin/importer.py,sha256=hK3D-1DZcoowGblRluGzng3n5Bf__hMsbNaIGXRpRdg,6263
|
17
|
-
treenode/admin/mixin.py,sha256=
|
17
|
+
treenode/admin/mixin.py,sha256=o83zTETOjvdHRjGR2pGthsY_RO7GVEimNUISgqqOBLs,10642
|
18
18
|
treenode/managers/__init__.py,sha256=c7F9Ku9489Hv6lTpUY2nbyBlWFCXBWAkNBm4xTKcjL8,186
|
19
19
|
treenode/managers/managers.py,sha256=8OaFxtajyR1d7-UHyiUbifMBEF9cjfHTIEYPkYUWmt0,7166
|
20
|
-
treenode/managers/queries.py,sha256=
|
21
|
-
treenode/managers/tasks.py,sha256=
|
20
|
+
treenode/managers/queries.py,sha256=zZYeDVFXl-Hro9ubv0_zuN7XaVXQhyeeFx_GVZSMi30,10273
|
21
|
+
treenode/managers/tasks.py,sha256=b8deUAbCpD1Yov-PpjKNAx49rL4fejkGf64ih5JziF0,7138
|
22
22
|
treenode/models/__init__.py,sha256=iR4ksCKoayvkIWWgGk6OUGHZC3D0mzAtgdBcS2vQPBw,188
|
23
23
|
treenode/models/decorators.py,sha256=N2dcnWqSCiEXDcYCf0zVijrbGUC8kYlqOLi_GKFmECU,1457
|
24
24
|
treenode/models/factory.py,sha256=sPUSrvo1za-r6ny3B8ptwevyjO8-iUpPNrT0eSD2kvI,1786
|
@@ -44,19 +44,19 @@ treenode/static/js/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
44
44
|
treenode/static/js/lz-string.min.js,sha256=TAnTJQd2AlLqT9M2TU7GFjnoj9SIfwLeZnpEtLkP624,4718
|
45
45
|
treenode/static/js/tree_widget.js,sha256=2LU8IIJD5TXbWVGjCEgrj5aYnUYtwBZECzj25XLtxNc,9633
|
46
46
|
treenode/static/js/treenode_admin.js,sha256=fkxAj50QxwpPVdQxDNxQZ5vR0goUt53h8FzOv_fjxBQ,15945
|
47
|
-
treenode/static/vendors/jquery-ui/AUTHORS.txt,sha256=
|
48
|
-
treenode/static/vendors/jquery-ui/LICENSE.txt,sha256=
|
49
|
-
treenode/static/vendors/jquery-ui/index.html,sha256=
|
50
|
-
treenode/static/vendors/jquery-ui/jquery-ui.css,sha256=
|
51
|
-
treenode/static/vendors/jquery-ui/jquery-ui.js,sha256=
|
52
|
-
treenode/static/vendors/jquery-ui/jquery-ui.min.css,sha256=
|
53
|
-
treenode/static/vendors/jquery-ui/jquery-ui.min.js,sha256=
|
54
|
-
treenode/static/vendors/jquery-ui/jquery-ui.structure.css,sha256=
|
55
|
-
treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css,sha256=
|
56
|
-
treenode/static/vendors/jquery-ui/jquery-ui.theme.css,sha256=
|
57
|
-
treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css,sha256=
|
58
|
-
treenode/static/vendors/jquery-ui/package.json,sha256=
|
59
|
-
treenode/static/vendors/jquery-ui/external/jquery/jquery.js,sha256=
|
47
|
+
treenode/static/vendors/jquery-ui/AUTHORS.txt,sha256=7eSHeezBdJ7Qlq0sbIvY4VRvqpMwLCAsm5ur1GqFBV8,15232
|
48
|
+
treenode/static/vendors/jquery-ui/LICENSE.txt,sha256=apBrQ--OzcBj7ylvGUYGVfV3jIaoB2ZPm63a1EU0Hko,1861
|
49
|
+
treenode/static/vendors/jquery-ui/index.html,sha256=fFm161yOy1n-_mJRFXB5oOYki_8Dlv-4ym3UZlrBiFw,24526
|
50
|
+
treenode/static/vendors/jquery-ui/jquery-ui.css,sha256=Q5bjan1qvGRnmfsMY79xzgnzXgNfHpKITyFwDwYDXTE,17609
|
51
|
+
treenode/static/vendors/jquery-ui/jquery-ui.js,sha256=wo5jyZcNCjgTOWkHAG5I-AHnELJBiMT7PB86Hkn981w,151652
|
52
|
+
treenode/static/vendors/jquery-ui/jquery-ui.min.css,sha256=FS9BPkDJmv5x5xNd4OpSxisTNOEUUC3cry5lZo9TXNU,15009
|
53
|
+
treenode/static/vendors/jquery-ui/jquery-ui.min.js,sha256=ymdFCtss_PVmZcIYetqzjnF_jiQur3VPdv4bjqIPA74,70369
|
54
|
+
treenode/static/vendors/jquery-ui/jquery-ui.structure.css,sha256=Sc38kpjVG4BhkslXC-wnctVgd7RO4NAVnNinveoLumU,344
|
55
|
+
treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css,sha256=3Hh94kZQ7IgAV86zPR-eYJheU0GvPFXXa0ZHldtjim4,212
|
56
|
+
treenode/static/vendors/jquery-ui/jquery-ui.theme.css,sha256=0cqVKFAVS9NOQyqhyhpanAO_AQnoPsX715VK0iIMGDE,17579
|
57
|
+
treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css,sha256=TdVk-kqTdbRzjVI7AfPMWaIJG2pOIlnZk4hUAW3bl3E,13712
|
58
|
+
treenode/static/vendors/jquery-ui/package.json,sha256=bDQqA98dfsEi83upFeZ8RipVtAraPRoxZMxcv244oQo,2204
|
59
|
+
treenode/static/vendors/jquery-ui/external/jquery/jquery.js,sha256=6440qEDaqjKqrIVfk4x21neDBVsbef6XUR5dUCKBv_E,296030
|
60
60
|
treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png,sha256=YW09ms2dyfoVEOT_NEpaWXMMRLJxVIqu8hku63SoCEA,7107
|
61
61
|
treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png,sha256=HP6KoU5aOllbVtaudhmNOLhBIzi_0oCuxBjwyhDATOo,7105
|
62
62
|
treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png,sha256=NxKGIMNmb6YYZor0RuijQ7PTK5BoA9b5qXLjYTMFvXo,4615
|
@@ -75,8 +75,8 @@ treenode/utils/db/__init__.py,sha256=RwicAcJSI1nhIPWLdT7j9TFsgOc9834VDn9lVn54GlY
|
|
75
75
|
treenode/utils/db/compiler.py,sha256=PgD9ybS5H8OUHw1gkFBQHhnrf5HiCx8QXUMRhydwh7o,3824
|
76
76
|
treenode/utils/db/db_vendor.py,sha256=4SyEHl51jVCDB3is4omHCf2bTB_QV3RvemUQYxJP5m0,930
|
77
77
|
treenode/utils/db/service.py,sha256=PF85Yhz2xUWFFCzpLYotmiNTZXXEH61rhswslSxEUds,2640
|
78
|
-
treenode/utils/db/sqlcompat.py,sha256=
|
79
|
-
treenode/utils/db/sqlquery.py,sha256=
|
78
|
+
treenode/utils/db/sqlcompat.py,sha256=FTgEMu3kQ1c96nB1sUW-SFOmYEvdl8JlD9HvsRj_dO0,5704
|
79
|
+
treenode/utils/db/sqlquery.py,sha256=jHI0gLkdJCD6ulOenO4b0aSjyAxtJ4ZGvsQa0i3Aj8E,2958
|
80
80
|
treenode/views/__init__.py,sha256=ppxbBx51TUaKstJFpAd_DTmbKjbZGmVMLNYSpgUKnd0,111
|
81
81
|
treenode/views/autoapi.py,sha256=o75e8IFsogbhZN_rbx3BKVnoruD96nWelnC5UzOqUDw,3628
|
82
82
|
treenode/views/autocomplete.py,sha256=Z7cBnC4Ihdyxm8zlbnG6CkZdVkM3TOTWRpw5mdhaIVA,1469
|
@@ -84,7 +84,7 @@ treenode/views/children.py,sha256=bygXaEBExxG3zIPL34_PYHLFFIqlQU2naqPIlyQ6e-s,11
|
|
84
84
|
treenode/views/common.py,sha256=mrmr40R91XVbMWcz5GZT-OjpnQ87F7XQZxu1W6rqpqI,617
|
85
85
|
treenode/views/crud.py,sha256=RI5rdyD4hZTszjZFThByxi_lkAeJlqbDCXFkD8iyzKE,7424
|
86
86
|
treenode/views/search.py,sha256=c_GyooT3jyoNa96bBxfoWruRN1wIw-ZGYvwGKkGojTs,1501
|
87
|
-
django_fast_treenode-3.0.
|
88
|
-
django_fast_treenode-3.0.
|
89
|
-
django_fast_treenode-3.0.
|
90
|
-
django_fast_treenode-3.0.
|
87
|
+
django_fast_treenode-3.0.4.dist-info/METADATA,sha256=NbOy_OHnyPjubLhbssbpdM2jwGiModfoiE_CPNvQpM8,10249
|
88
|
+
django_fast_treenode-3.0.4.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
89
|
+
django_fast_treenode-3.0.4.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
90
|
+
django_fast_treenode-3.0.4.dist-info/RECORD,,
|
treenode/admin/admin.py
CHANGED
treenode/admin/mixin.py
CHANGED
@@ -84,7 +84,7 @@ class AdminMixin(admin.ModelAdmin):
|
|
84
84
|
value = field(obj)
|
85
85
|
field_name = getattr(field, "__name__", "field")
|
86
86
|
else:
|
87
|
-
attr, value = lookup_field(field, obj, self)
|
87
|
+
r, attr, value = lookup_field(field, obj, self)
|
88
88
|
field_name = field
|
89
89
|
|
90
90
|
row_data.append(value)
|
treenode/managers/queries.py
CHANGED
@@ -5,13 +5,14 @@ Low-level SQL Query Manager.
|
|
5
5
|
Encapsulates all logic to retrieve related primary keys based on relationships
|
6
6
|
(e.g., ancestors, children, descendants, siblings, family, root) using raw SQL.
|
7
7
|
|
8
|
-
Version: 3.0.
|
8
|
+
Version: 3.0.4
|
9
9
|
Author: Timur Kady
|
10
10
|
Email: timurkady@yandex.com
|
11
11
|
"""
|
12
12
|
|
13
13
|
|
14
14
|
from django.db import connection
|
15
|
+
from ..utils.db.sqlcompat import SQLCompat
|
15
16
|
|
16
17
|
|
17
18
|
class TreeQuery:
|
@@ -32,19 +33,6 @@ class TreeQuery:
|
|
32
33
|
cursor.execute(sql, params)
|
33
34
|
return cursor.fetchall()
|
34
35
|
|
35
|
-
def wrap_union_all(self, queries):
|
36
|
-
"""
|
37
|
-
Combine multiple SQL queries using UNION ALL.
|
38
|
-
|
39
|
-
Each query is a tuple: (sql, params).
|
40
|
-
Returns a tuple: (combined_sql, combined_params).
|
41
|
-
"""
|
42
|
-
union_query = " UNION ALL ".join(f"({q[0]})" for q in queries)
|
43
|
-
combined_params = []
|
44
|
-
for q in queries:
|
45
|
-
combined_params.extend(q[1])
|
46
|
-
return union_query, combined_params
|
47
|
-
|
48
36
|
def order_by(self, sql, order_by_clause):
|
49
37
|
"""Wrap the SQL in an outer query to enforce ordering."""
|
50
38
|
return f"SELECT * FROM ({sql}) AS combined ORDER BY {order_by_clause}"
|
@@ -76,7 +64,7 @@ class TreeQuery:
|
|
76
64
|
if include_self:
|
77
65
|
sql2 = f"SELECT id, priority FROM {self.db_table} WHERE id = %s"
|
78
66
|
params2 = [self.node.pk]
|
79
|
-
combined_sql, combined_params =
|
67
|
+
combined_sql, combined_params = SQLCompat.wrap_union_all(
|
80
68
|
[(sql1, params1), (sql2, params2)])
|
81
69
|
sql = self.order_by(combined_sql, "priority")
|
82
70
|
return sql, combined_params
|
@@ -115,7 +103,7 @@ class TreeQuery:
|
|
115
103
|
FROM {self.db_table}
|
116
104
|
WHERE id = %s
|
117
105
|
"""
|
118
|
-
union_sql, union_params =
|
106
|
+
union_sql, union_params = SQLCompat.wrap_union_all([
|
119
107
|
(base_sql, params),
|
120
108
|
(sql_self, [self.node.pk])
|
121
109
|
])
|
@@ -148,7 +136,7 @@ class TreeQuery:
|
|
148
136
|
|
149
137
|
if include_self:
|
150
138
|
sql_self = f"SELECT id, _depth, priority FROM {self.db_table} WHERE id = %s" # noqa: D501
|
151
|
-
union_sql, union_params =
|
139
|
+
union_sql, union_params = SQLCompat.wrap_union_all(
|
152
140
|
[(base_sql, params), (sql_self, [self.node.pk])])
|
153
141
|
else:
|
154
142
|
union_sql, union_params = base_sql, params
|
@@ -198,7 +186,7 @@ class TreeQuery:
|
|
198
186
|
if include_self:
|
199
187
|
sql_self = f"SELECT id, _depth, priority FROM {self.db_table} WHERE id = %s" # noqa: D501
|
200
188
|
queries.append((sql_self, [self.node.pk]))
|
201
|
-
combined_sql, combined_params =
|
189
|
+
combined_sql, combined_params = SQLCompat.wrap_union_all(queries)
|
202
190
|
combined_sql = self.order_by(combined_sql, "_depth, priority")
|
203
191
|
return combined_sql, combined_params
|
204
192
|
|
treenode/managers/tasks.py
CHANGED
@@ -2,24 +2,16 @@
|
|
2
2
|
"""
|
3
3
|
TreeNode TaskQuery manager
|
4
4
|
|
5
|
-
Version: 3.0.
|
5
|
+
Version: 3.0.4
|
6
6
|
Author: Timur Kady
|
7
7
|
Email: timurkady@yandex.com
|
8
8
|
"""
|
9
9
|
|
10
|
-
|
10
|
+
import atexit
|
11
|
+
from django.db import connection, transaction
|
11
12
|
|
12
13
|
from ..utils.db import TreePathCompiler
|
13
14
|
|
14
|
-
'''
|
15
|
-
try:
|
16
|
-
profile
|
17
|
-
except NameError:
|
18
|
-
def profile(func):
|
19
|
-
"""Profile."""
|
20
|
-
return func
|
21
|
-
'''
|
22
|
-
|
23
15
|
|
24
16
|
class TreeTaskQueue:
|
25
17
|
"""TreeTaskQueue Class."""
|
@@ -28,31 +20,93 @@ class TreeTaskQueue:
|
|
28
20
|
"""Init the task query."""
|
29
21
|
self.model = model
|
30
22
|
self.queue = []
|
23
|
+
self._running = False
|
24
|
+
|
25
|
+
# Register the execution queue when the interpreter exits
|
26
|
+
atexit.register(self._atexit_run)
|
27
|
+
|
28
|
+
def _atexit_run(self):
|
29
|
+
"""Run queue on interpreter exit if pending tasks exist."""
|
30
|
+
if self.queue and not self._running:
|
31
|
+
try:
|
32
|
+
self.run()
|
33
|
+
except Exception as e:
|
34
|
+
# Don't crash on completion, just log
|
35
|
+
print(f"[TreeTaskQueue] Error during atexit: {e}")
|
31
36
|
|
32
37
|
def add(self, mode, parent_id):
|
33
|
-
"""Add task to the
|
38
|
+
"""Add task to the queue.
|
39
|
+
|
40
|
+
Parameters:
|
41
|
+
mode (str): Task type (currently only "update").
|
42
|
+
parent_id (int|None): ID of parent node to update from (None = full tree).
|
43
|
+
"""
|
34
44
|
self.queue.append({"mode": mode, "parent_id": parent_id})
|
35
45
|
|
36
46
|
def run(self):
|
37
|
-
"""Run task queue.
|
47
|
+
"""Run task queue.
|
48
|
+
|
49
|
+
This method collects all queued tasks, optimizes them, and performs
|
50
|
+
a recursive rebuild of tree paths and depths using SQL. Locks the
|
51
|
+
required rows before running.
|
52
|
+
|
53
|
+
Uses Django's `transaction.atomic()` to ensure that any recursive CTE
|
54
|
+
execution or SAVEPOINT creation works properly under PostgreSQL.
|
55
|
+
"""
|
38
56
|
if len(self.queue) == 0:
|
39
57
|
return
|
58
|
+
|
59
|
+
self._running = True
|
40
60
|
try:
|
41
61
|
optimized = self._optimize()
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
62
|
+
if not optimized:
|
63
|
+
return
|
64
|
+
|
65
|
+
parent_ids = [t["parent_id"] for t in optimized if t["parent_id"] is not None]
|
66
|
+
|
67
|
+
with transaction.atomic():
|
68
|
+
if any(t["parent_id"] is None for t in optimized):
|
69
|
+
try:
|
70
|
+
with connection.cursor() as cursor:
|
71
|
+
cursor.execute(
|
72
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL FOR UPDATE NOWAIT"
|
73
|
+
)
|
74
|
+
except Exception as e:
|
75
|
+
print(f"[TreeTaskQueue] Skipped (root locked): {e}")
|
76
|
+
return
|
77
|
+
else:
|
78
|
+
try:
|
79
|
+
with connection.cursor() as cursor:
|
80
|
+
for parent_id in parent_ids:
|
81
|
+
cursor.execute(
|
82
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE id = %s FOR UPDATE NOWAIT",
|
83
|
+
[parent_id],
|
84
|
+
)
|
85
|
+
except Exception as e:
|
86
|
+
print(f"[TreeTaskQueue] Skipped (parent locked): {e}")
|
87
|
+
return
|
88
|
+
|
89
|
+
for task in optimized:
|
90
|
+
if task["mode"] == "update":
|
91
|
+
TreePathCompiler.update_path(
|
92
|
+
model=self.model,
|
93
|
+
parent_id=task["parent_id"]
|
94
|
+
)
|
95
|
+
|
96
|
+
except Exception as e:
|
97
|
+
print(f"[TreeTaskQueue] Error in run: {e}")
|
98
|
+
connection.rollback()
|
49
99
|
finally:
|
50
100
|
self.queue.clear()
|
51
101
|
self._running = False
|
52
102
|
|
53
|
-
# @profile
|
54
103
|
def _optimize(self):
|
55
|
-
"""Return optimized task queue (ID-only logic).
|
104
|
+
"""Return optimized task queue (ID-only logic).
|
105
|
+
|
106
|
+
Attempts to merge redundant or overlapping subtree updates into
|
107
|
+
the minimal set of unique parent IDs that need to be rebuilt.
|
108
|
+
If it finds a common root, it returns a single task for full rebuild.
|
109
|
+
"""
|
56
110
|
result_set = set()
|
57
111
|
id_set = set()
|
58
112
|
|
@@ -60,8 +114,6 @@ class TreeTaskQueue:
|
|
60
114
|
if task["mode"] == "update":
|
61
115
|
parent_id = task["parent_id"]
|
62
116
|
if parent_id is None:
|
63
|
-
# If we are already updating the entire tree, then
|
64
|
-
# the remaining tasks are meaningless # noqa: D501
|
65
117
|
return [{"mode": "update", "parent_id": None}]
|
66
118
|
else:
|
67
119
|
id_set.add(parent_id)
|
@@ -74,8 +126,6 @@ class TreeTaskQueue:
|
|
74
126
|
for other in id_list[:]:
|
75
127
|
ancestor = self._get_common_ancestor(current, other)
|
76
128
|
if ancestor is not None:
|
77
|
-
# If the common ancestor is the root, then we update
|
78
|
-
# the entire tree
|
79
129
|
if ancestor in self._get_root_ids():
|
80
130
|
return [{"mode": "update", "parent_id": None}]
|
81
131
|
if ancestor not in id_set:
|
@@ -87,36 +137,25 @@ class TreeTaskQueue:
|
|
87
137
|
if not merged:
|
88
138
|
result_set.add(current)
|
89
139
|
|
90
|
-
return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)]
|
140
|
+
return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)]
|
91
141
|
|
92
142
|
def _get_root_ids(self):
|
93
143
|
"""Return root node IDs."""
|
94
144
|
with connection.cursor() as cursor:
|
95
145
|
cursor.execute(
|
96
|
-
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL")
|
146
|
+
f"SELECT id FROM {self.model._meta.db_table} WHERE parent_id IS NULL")
|
97
147
|
return [row[0] for row in cursor.fetchall()]
|
98
148
|
|
99
149
|
def _get_parent_id(self, node_id):
|
100
150
|
"""Return parent ID for a given node."""
|
101
151
|
with connection.cursor() as cursor:
|
102
152
|
cursor.execute(
|
103
|
-
f"SELECT parent_id FROM {self.model._meta.db_table} WHERE id = %s", [node_id])
|
153
|
+
f"SELECT parent_id FROM {self.model._meta.db_table} WHERE id = %s", [node_id])
|
104
154
|
row = cursor.fetchone()
|
105
155
|
return row[0] if row else None
|
106
156
|
|
107
|
-
'''
|
108
|
-
def _get_ancestor_path(self, node_id):
|
109
|
-
"""Return list of ancestor IDs including the node itself."""
|
110
|
-
path = []
|
111
|
-
while node_id is not None:
|
112
|
-
path.append(node_id)
|
113
|
-
node_id = self._get_parent_id(node_id)
|
114
|
-
return path[::-1] # root to leaf
|
115
|
-
'''
|
116
|
-
|
117
|
-
# @profile
|
118
157
|
def _get_ancestor_path(self, node_id):
|
119
|
-
"""Return list of ancestor IDs including the node itself, using recursive SQL."""
|
158
|
+
"""Return list of ancestor IDs including the node itself, using recursive SQL."""
|
120
159
|
table = self.model._meta.db_table
|
121
160
|
|
122
161
|
sql = f"""
|
@@ -140,7 +179,6 @@ class TreeTaskQueue:
|
|
140
179
|
|
141
180
|
return [row[0] for row in rows]
|
142
181
|
|
143
|
-
# @profile
|
144
182
|
def _get_common_ancestor(self, id1, id2):
|
145
183
|
"""Return common ancestor ID between two nodes."""
|
146
184
|
path1 = self._get_ancestor_path(id1)
|