django-fast-treenode 3.0.8__py3-none-any.whl → 3.2.1__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.8.dist-info → django_fast_treenode-3.2.1.dist-info}/METADATA +3 -1
- django_fast_treenode-3.2.1.dist-info/RECORD +98 -0
- treenode/admin/admin.py +5 -5
- treenode/admin/mixin.py +6 -6
- treenode/managers/managers.py +1 -1
- treenode/managers/tasks.py +19 -1
- treenode/models/mixins/__init__.py +3 -1
- treenode/models/mixins/children.py +1 -1
- treenode/models/mixins/descendants.py +1 -1
- treenode/models/mixins/node.py +4 -4
- treenode/models/mixins/roots.py +1 -1
- treenode/models/mixins/search.py +55 -0
- treenode/models/mixins/siblings.py +1 -1
- treenode/models/models.py +31 -14
- treenode/settings.py +4 -1
- treenode/static/treenode/.gitkeep +0 -0
- treenode/static/treenode/vendors/.gitkeep +0 -0
- treenode/static/treenode/vendors/jquery-ui/.gitkeep +0 -0
- treenode/templates/treenode/admin/treenode_import_export.html +3 -2
- treenode/utils/db/compiler.py +38 -79
- treenode/utils/jwt_auth.py +25 -0
- treenode/version.py +2 -2
- treenode/views/autoapi.py +95 -91
- treenode/widgets.py +2 -2
- django_fast_treenode-3.0.8.dist-info/RECORD +0 -93
- {django_fast_treenode-3.0.8.dist-info → django_fast_treenode-3.2.1.dist-info}/WHEEL +0 -0
- {django_fast_treenode-3.0.8.dist-info → django_fast_treenode-3.2.1.dist-info}/licenses/LICENSE +0 -0
- {django_fast_treenode-3.0.8.dist-info → django_fast_treenode-3.2.1.dist-info}/top_level.txt +0 -0
- /treenode/static/{css → treenode/css}/.gitkeep +0 -0
- /treenode/static/{css → treenode/css}/tree_widget.css +0 -0
- /treenode/static/{css → treenode/css}/treenode_admin.css +0 -0
- /treenode/static/{css → treenode/css}/treenode_tabs.css +0 -0
- /treenode/static/{js → treenode/js}/.gitkeep +0 -0
- /treenode/static/{js → treenode/js}/lz-string.min.js +0 -0
- /treenode/static/{js → treenode/js}/tree_widget.js +0 -0
- /treenode/static/{js → treenode/js}/treenode_admin.js +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/AUTHORS.txt +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/LICENSE.txt +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/external/jquery/jquery.js +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/index.html +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.js +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.js +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.min.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.min.css +0 -0
- /treenode/static/{vendors → treenode/vendors}/jquery-ui/package.json +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.2.1
|
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
|
@@ -57,6 +57,7 @@ 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
|
60
|
+
Requires-Dist: PyJWT>=2.0
|
60
61
|
Dynamic: author
|
61
62
|
Dynamic: home-page
|
62
63
|
Dynamic: license-file
|
@@ -126,6 +127,7 @@ At the moment, django-fast-treeenode is, if not the best, then one of the best p
|
|
126
127
|
- **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
128
|
- **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..
|
128
129
|
- **Lightweight**: All functionality is implemented within the package without heavyweight dependencies such as `djangorestframework` or `django-import-export`.
|
130
|
+
- **Optional JWT authentication**: enable token-based protection for the API with a single setting.
|
129
131
|
|
130
132
|
All this makes **Treenode Framework** a prime candidate for your needs.
|
131
133
|
|
@@ -0,0 +1,98 @@
|
|
1
|
+
django_fast_treenode-3.2.1.dist-info/licenses/LICENSE,sha256=SSYqS84FCnAW7tAxmjBKU8qAa8Jv4VGPuSSGeHwWtJE,1095
|
2
|
+
treenode/__init__.py,sha256=3z1hWpHyy4wg6uz7HCmRi9FaXYeN5CfANVpa77UIoPw,53
|
3
|
+
treenode/apps.py,sha256=QlwjNDM9rkUoWB8Vm8-OkS6lNx0-aTByuGZlu9wrQMs,1832
|
4
|
+
treenode/cache.py,sha256=2jUiiecfFxwB7QFukpU4u0FnDzGH6hNRfo6KAYvs6vM,8447
|
5
|
+
treenode/forms.py,sha256=V-upmbYSW1BbuXdSBGExHxw_j5TTUheaHvreK9tSGTE,3155
|
6
|
+
treenode/settings.py,sha256=FRGK7hl_Tnxp4sGbUNxJgEQP9niDJjLhcNBtmloOvHk,741
|
7
|
+
treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
|
8
|
+
treenode/tests.py,sha256=2uDafv3Ns6f7Vy1ekUtgYxCZEi1KRyesZDTAFhYcX-E,63
|
9
|
+
treenode/urls.py,sha256=krHvVigc_dxC0z5hEd2rgeH6th8jW7qJY3Qbia-419Y,240
|
10
|
+
treenode/version.py,sha256=2xptvV9MUEglYmGGtVaEX6WiMnY6GLv_F951eiBLmJk,220
|
11
|
+
treenode/widgets.py,sha256=3kSby7v-gpyUHmAIFZCENM8hzT1xcHJFB4op8XU6YEQ,4035
|
12
|
+
treenode/admin/__init__.py,sha256=XNEYHdF5lKb0vpdlVxdR2fxj5oUgzyx1YyCwsv0gxHw,100
|
13
|
+
treenode/admin/admin.py,sha256=cbiqlzXwK-W73DTJqvXo_abtnK1JwEwyBEOl0bmgsa4,7507
|
14
|
+
treenode/admin/changelist.py,sha256=KUYS9MaR8Ck_1xmMqupobxWKarrJEqmHuEG32CL01Bo,1662
|
15
|
+
treenode/admin/exporter.py,sha256=QE74V6W3tvwA5kCvBt1MmVlLOaWh-o8EU63cgmiwD5Q,5724
|
16
|
+
treenode/admin/importer.py,sha256=hK3D-1DZcoowGblRluGzng3n5Bf__hMsbNaIGXRpRdg,6263
|
17
|
+
treenode/admin/mixin.py,sha256=7hcjoh8W2_R2in0EorNjVTXakYP3I1mMZKIr65UsU-g,10677
|
18
|
+
treenode/managers/__init__.py,sha256=c7F9Ku9489Hv6lTpUY2nbyBlWFCXBWAkNBm4xTKcjL8,186
|
19
|
+
treenode/managers/managers.py,sha256=G1dayrqaEX5nQiKsHS_2y6o3iXKIAn66RKArHttN-kU,7174
|
20
|
+
treenode/managers/queries.py,sha256=Kepax8SDn7G5tOlPRWBCp5Oyp49O5iMITCMBoNCm_Ak,10655
|
21
|
+
treenode/managers/tasks.py,sha256=XGV7zei6LQJqQx6Ozg5lWC2A7770wn6rvWJ5jyJ_8pw,7796
|
22
|
+
treenode/models/__init__.py,sha256=iR4ksCKoayvkIWWgGk6OUGHZC3D0mzAtgdBcS2vQPBw,188
|
23
|
+
treenode/models/decorators.py,sha256=N2dcnWqSCiEXDcYCf0zVijrbGUC8kYlqOLi_GKFmECU,1457
|
24
|
+
treenode/models/factory.py,sha256=sPUSrvo1za-r6ny3B8ptwevyjO8-iUpPNrT0eSD2kvI,1786
|
25
|
+
treenode/models/models.py,sha256=ClY1LaxAikqgdLIOiuKfb8srTpj5gUcSGmB50oA6REA,12969
|
26
|
+
treenode/models/mixins/__init__.py,sha256=CAef4rue_uYeBdcswPZAOIEwsKqGLhL_XFco7JwcvdI,909
|
27
|
+
treenode/models/mixins/ancestors.py,sha256=9g-0nPHoiF_SX2kN4uDLdbWyw-TDCz1YqxLJngwTZOQ,1971
|
28
|
+
treenode/models/mixins/children.py,sha256=H0faiH24ihXCUEWGjTcjoIAKl26E8RQcQaTTkgKFz00,2397
|
29
|
+
treenode/models/mixins/descendants.py,sha256=5xoeIdRhZ94K-EMrEjGgD7KW668gyXxp8JyB3t3mFAU,2163
|
30
|
+
treenode/models/mixins/family.py,sha256=MB5kWRVvxU_xmSgCekveTP5Vhj4wJki8bU7hzn9RNLE,1673
|
31
|
+
treenode/models/mixins/logical.py,sha256=gh5wv5XZDs5GWarU6g9zKXWNwji7SE3zSVNIpywDWjw,2190
|
32
|
+
treenode/models/mixins/node.py,sha256=D3lwWsN2eRJeS99O7CaVAhabTYWA_NSY8z60zAhFAs0,8576
|
33
|
+
treenode/models/mixins/properties.py,sha256=1O6p2tfvOesBooZeOeuXi8yfEO_o-5gn_agtKqMxU-s,3945
|
34
|
+
treenode/models/mixins/roots.py,sha256=wfuf6jdkcfkEUa42n0bHD_bQDE78AEFK2ZVCeBh7FZw,4100
|
35
|
+
treenode/models/mixins/search.py,sha256=ZURelSNFT3wK3A0SnYvfHb2WnfrN_c7BIatp6ffGfsY,1652
|
36
|
+
treenode/models/mixins/siblings.py,sha256=o0Iorfe35V1A9iAKwV8CqALPuTjjcQcnEAjAdPE6haI,3067
|
37
|
+
treenode/models/mixins/tree.py,sha256=5Cew5c-NtTmz15S_ueDZ-fojHtQKZEKNxmp3W6qlC-8,14512
|
38
|
+
treenode/models/mixins/update.py,sha256=oCZkMnfT23n2n3_mnokDrtTLS_jO_lJRwG3ENkH_DE4,4948
|
39
|
+
treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
|
+
treenode/static/treenode/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
|
+
treenode/static/treenode/css/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
42
|
+
treenode/static/treenode/css/tree_widget.css,sha256=YVIHuZT_E5ck5N7wvYvuwgV_m-4Ta_uGFiNUoTw-zbk,4861
|
43
|
+
treenode/static/treenode/css/treenode_admin.css,sha256=w7yyyf4aFsJKlr_YJPVq6ap5ZvA5Co7ZCYq6ipuV958,3291
|
44
|
+
treenode/static/treenode/css/treenode_tabs.css,sha256=pbulaUEhNMQFNWXFQrRN1zMyKjoSqLAnjNygpouPUfE,879
|
45
|
+
treenode/static/treenode/js/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
46
|
+
treenode/static/treenode/js/lz-string.min.js,sha256=TAnTJQd2AlLqT9M2TU7GFjnoj9SIfwLeZnpEtLkP624,4718
|
47
|
+
treenode/static/treenode/js/tree_widget.js,sha256=2LU8IIJD5TXbWVGjCEgrj5aYnUYtwBZECzj25XLtxNc,9633
|
48
|
+
treenode/static/treenode/js/treenode_admin.js,sha256=IU3Zj2Q-WBxftl0zsjeRPrt1xOzzdEKlzGEMThi3hrk,9706
|
49
|
+
treenode/static/treenode/vendors/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
|
+
treenode/static/treenode/vendors/jquery-ui/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
|
+
treenode/static/treenode/vendors/jquery-ui/AUTHORS.txt,sha256=geZ7JfVsTJq8Wgjrcm1p56Zqjxx7PL9FkxhKiN8dQg8,14848
|
52
|
+
treenode/static/treenode/vendors/jquery-ui/LICENSE.txt,sha256=9kkCXPZcjc2FiLCTeIO6elK9ttNRKUZKNqFgLrACUPE,1818
|
53
|
+
treenode/static/treenode/vendors/jquery-ui/index.html,sha256=RSv8cA662qJdc02OX-UmpaBJoZrngoQqnNWVZNn9GQQ,24229
|
54
|
+
treenode/static/treenode/vendors/jquery-ui/jquery-ui.css,sha256=O8mmheetXPeKUa-iDk8fdxDhs096eWIi10qLJb6OXyw,17171
|
55
|
+
treenode/static/treenode/vendors/jquery-ui/jquery-ui.js,sha256=XtnlPiKuVzMh-cef76mBCNCTPqk2cIlKBgmTHfRBDog,146430
|
56
|
+
treenode/static/treenode/vendors/jquery-ui/jquery-ui.min.css,sha256=Yv2MureLOGZicbgEv28X9ft9o0SRBZ84PWIgg6h_V2E,15003
|
57
|
+
treenode/static/treenode/vendors/jquery-ui/jquery-ui.min.js,sha256=FT6F7DDm5J1Qz6B5fv3e8jnmx9QyyfR2qUeTptb73bA,70364
|
58
|
+
treenode/static/treenode/vendors/jquery-ui/jquery-ui.structure.css,sha256=392jP1trXFqS1OVv2X50px0F8w1DeTyIvvHL9wxNXi0,328
|
59
|
+
treenode/static/treenode/vendors/jquery-ui/jquery-ui.structure.min.css,sha256=cXLe7UDVCN5SvPpbb3HJdndQIk1hlWMrzyRRYO9rcLs,208
|
60
|
+
treenode/static/treenode/vendors/jquery-ui/jquery-ui.theme.css,sha256=grsB4hVvwiBxnNsG1c_ocH1YK76CExOav_dn_BsxepU,17140
|
61
|
+
treenode/static/treenode/vendors/jquery-ui/jquery-ui.theme.min.css,sha256=oW6e3DmGiKSNLzo3HCUnjes9ykGZ6gXEtl3LjiQIkMs,13708
|
62
|
+
treenode/static/treenode/vendors/jquery-ui/package.json,sha256=AvCATruWzbwY7NsfzcUR2WqPjslGO8M7hHzTj1AYDQY,2122
|
63
|
+
treenode/static/treenode/vendors/jquery-ui/external/jquery/jquery.js,sha256=eKhayi8LEQwp4NKxN-CfCh-3qOVUtJn3QNZ0TciWLP4,285314
|
64
|
+
treenode/static/treenode/vendors/jquery-ui/images/ui-icons_444444_256x240.png,sha256=YW09ms2dyfoVEOT_NEpaWXMMRLJxVIqu8hku63SoCEA,7107
|
65
|
+
treenode/static/treenode/vendors/jquery-ui/images/ui-icons_555555_256x240.png,sha256=HP6KoU5aOllbVtaudhmNOLhBIzi_0oCuxBjwyhDATOo,7105
|
66
|
+
treenode/static/treenode/vendors/jquery-ui/images/ui-icons_777620_256x240.png,sha256=NxKGIMNmb6YYZor0RuijQ7PTK5BoA9b5qXLjYTMFvXo,4615
|
67
|
+
treenode/static/treenode/vendors/jquery-ui/images/ui-icons_777777_256x240.png,sha256=TLerPghEHzRXKPUuwr0VPT1PusnYFUe1-k1QIyeXe7g,7115
|
68
|
+
treenode/static/treenode/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png,sha256=S36ETnNcCxoWxmxAkVl0P3LYEDInjxi9GnG_g5uho1c,4615
|
69
|
+
treenode/static/treenode/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png,sha256=SBgnaQEkoyko7AbSFy0lDoDzAup6bkVQ74XyenFleUM,6395
|
70
|
+
treenode/templates/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
71
|
+
treenode/templates/treenode/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
72
|
+
treenode/templates/treenode/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
73
|
+
treenode/templates/treenode/admin/treenode_ajax_rows.html,sha256=zFyPaTbSyxRjOqQ85SMv__qTIYDjEna6chYODBypDZA,224
|
74
|
+
treenode/templates/treenode/admin/treenode_changelist.html,sha256=JiUwX33w5WwegX7gM2ctw-bx9XOyXwKt9QB_6g7Dfz8,2283
|
75
|
+
treenode/templates/treenode/admin/treenode_import_export.html,sha256=bgmue5y8SwzgbFQls8DjslkX1EZmrp5enUFHqTmhUfI,2977
|
76
|
+
treenode/templates/treenode/admin/treenode_rows.html,sha256=de90wKpNdVB2C5CGnIkStBB0dWCDkmWr_pluV_NgBUs,1857
|
77
|
+
treenode/templates/treenode/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
|
78
|
+
treenode/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
79
|
+
treenode/templatetags/treenode_admin.py,sha256=5f5oqAS4zC_f0kkJRsm5MqXHjsKJXn8GslbZcbMivxg,2736
|
80
|
+
treenode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
81
|
+
treenode/utils/jwt_auth.py,sha256=LSTMuBFbH1DPJ0pVUnD_T1owb6GurpkJcCxpsXDL4HQ,845
|
82
|
+
treenode/utils/db/__init__.py,sha256=RwicAcJSI1nhIPWLdT7j9TFsgOc9834VDn9lVn54GlY,247
|
83
|
+
treenode/utils/db/compiler.py,sha256=2evMIg-bqDNbRqZub6ePNlQi9jnS_YbLe3CmOa_4XC0,2519
|
84
|
+
treenode/utils/db/db_vendor.py,sha256=4SyEHl51jVCDB3is4omHCf2bTB_QV3RvemUQYxJP5m0,930
|
85
|
+
treenode/utils/db/service.py,sha256=PF85Yhz2xUWFFCzpLYotmiNTZXXEH61rhswslSxEUds,2640
|
86
|
+
treenode/utils/db/sqlcompat.py,sha256=K71ggkKIvpdTtHQ6Y4qcbo6cj2eYiEfy6DlVBr8Po1E,4460
|
87
|
+
treenode/utils/db/sqlquery.py,sha256=KXcfKbbaBF-D134H_2DiPQtjedR79SJNXPJc0msZYEc,1938
|
88
|
+
treenode/views/__init__.py,sha256=ppxbBx51TUaKstJFpAd_DTmbKjbZGmVMLNYSpgUKnd0,111
|
89
|
+
treenode/views/autoapi.py,sha256=X7r8hgzJ9LGO1k_keh9YfLk3dzVbjW-sVte5Ddp_HVE,3771
|
90
|
+
treenode/views/autocomplete.py,sha256=ERGJT4jfCE2GUv94Ja8SgJyG1FOAyqIeB0PKH1fyU7g,1417
|
91
|
+
treenode/views/children.py,sha256=seO0SNKriRY2FJOO1oZgX42iKolXMf3EF76GrfdJZLQ,1111
|
92
|
+
treenode/views/common.py,sha256=kUN3IgMZCdNdJ2haxB9MTGcn2rctyFUAHNagzcu9wXk,594
|
93
|
+
treenode/views/crud.py,sha256=RI5rdyD4hZTszjZFThByxi_lkAeJlqbDCXFkD8iyzKE,7424
|
94
|
+
treenode/views/search.py,sha256=c_GyooT3jyoNa96bBxfoWruRN1wIw-ZGYvwGKkGojTs,1501
|
95
|
+
django_fast_treenode-3.2.1.dist-info/METADATA,sha256=EOaUVLtr2Go1Shmy1qFlVMJ2MKrjr4nkM3VPecw5dgQ,10377
|
96
|
+
django_fast_treenode-3.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
97
|
+
django_fast_treenode-3.2.1.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
98
|
+
django_fast_treenode-3.2.1.dist-info/RECORD,,
|
treenode/admin/admin.py
CHANGED
@@ -69,13 +69,13 @@ class TreeNodeModelAdmin(AdminMixin, admin.ModelAdmin):
|
|
69
69
|
"""Meta Class."""
|
70
70
|
|
71
71
|
css = {"all": (
|
72
|
-
"css/treenode_admin.css",
|
73
|
-
"vendors/jquery-ui/jquery-ui.css",
|
72
|
+
"treenode/css/treenode_admin.css",
|
73
|
+
"treenode/vendors/jquery-ui/jquery-ui.css",
|
74
74
|
)}
|
75
75
|
js = (
|
76
|
-
"vendors/jquery-ui/jquery-ui.js",
|
77
|
-
# "js/lz-string.min.js",
|
78
|
-
"js/treenode_admin.js",
|
76
|
+
"treenode/vendors/jquery-ui/jquery-ui.js",
|
77
|
+
# "treenode/js/lz-string.min.js",
|
78
|
+
"treenode/js/treenode_admin.js",
|
79
79
|
)
|
80
80
|
|
81
81
|
def __init__(self, model, admin_site):
|
treenode/admin/mixin.py
CHANGED
@@ -62,7 +62,7 @@ class AdminMixin(admin.ModelAdmin):
|
|
62
62
|
return custom_urls + default_urls
|
63
63
|
|
64
64
|
def render_changelist_rows(self, objs: list, request):
|
65
|
-
"""
|
65
|
+
"""Render rows for insert into changelist."""
|
66
66
|
list_display = list(self.get_list_display(request))
|
67
67
|
checkbox_field_name = ACTION_CHECKBOX_NAME
|
68
68
|
if checkbox_field_name not in list_display:
|
@@ -227,7 +227,7 @@ class AdminMixin(admin.ModelAdmin):
|
|
227
227
|
|
228
228
|
def import_view(self, request):
|
229
229
|
"""
|
230
|
-
|
230
|
+
Import View.
|
231
231
|
|
232
232
|
Handles file upload and initiates import processing via
|
233
233
|
TreeNodeImporter.
|
@@ -242,14 +242,14 @@ class AdminMixin(admin.ModelAdmin):
|
|
242
242
|
extension = os.path.splitext(filename)[1].lower().lstrip('.')
|
243
243
|
if extension not in ['csv', 'tsv', 'json', 'xlsx', 'yaml']:
|
244
244
|
return JsonResponse(
|
245
|
-
{"error": _(f"Invalid file format ({extension}.")},
|
245
|
+
{"error": _(f"Invalid file format ({extension}).")},
|
246
246
|
status=200
|
247
247
|
)
|
248
248
|
importer = self.TreeNodeImporter(self.model, file, extension)
|
249
249
|
importer.parse()
|
250
250
|
result = importer.import_tree()
|
251
251
|
|
252
|
-
return render(request, "admin/treenode_import_export.html", {
|
252
|
+
return render(request, "treenode/admin/treenode_import_export.html", {
|
253
253
|
"created_count": result.get("created", 0),
|
254
254
|
"updated_count": result.get("updated", 0),
|
255
255
|
"errors": result.get("errors", []),
|
@@ -258,7 +258,7 @@ class AdminMixin(admin.ModelAdmin):
|
|
258
258
|
|
259
259
|
return render(
|
260
260
|
request,
|
261
|
-
"admin/treenode_import_export.html",
|
261
|
+
"treenode/admin/treenode_import_export.html",
|
262
262
|
{"import_active": True}
|
263
263
|
)
|
264
264
|
|
@@ -284,7 +284,7 @@ class AdminMixin(admin.ModelAdmin):
|
|
284
284
|
|
285
285
|
return render(
|
286
286
|
request,
|
287
|
-
"admin/treenode_import_export.html",
|
287
|
+
"treenode/admin/treenode_import_export.html",
|
288
288
|
{"import_active": False}
|
289
289
|
)
|
290
290
|
|
treenode/managers/managers.py
CHANGED
@@ -207,7 +207,7 @@ class TreeNodeManager(models.Manager):
|
|
207
207
|
|
208
208
|
WARNING: Unsafe low-level update bypassing all TreeNode protections.
|
209
209
|
Use only when bypassing _path/_depth/priority safety checks is
|
210
|
-
|
210
|
+
intentional.
|
211
211
|
"""
|
212
212
|
return models.QuerySet(self.model, using=self.db)\
|
213
213
|
.bulk_update(*args, **kwargs)
|
treenode/managers/tasks.py
CHANGED
@@ -137,7 +137,25 @@ class TreeTaskQueue:
|
|
137
137
|
if not merged:
|
138
138
|
result_set.add(current)
|
139
139
|
|
140
|
-
|
140
|
+
if not result_set:
|
141
|
+
return []
|
142
|
+
|
143
|
+
depth_lookup = {}
|
144
|
+
if result_set:
|
145
|
+
with connection.cursor() as cursor:
|
146
|
+
format_ids = ', '.join(['%s'] * len(result_set))
|
147
|
+
sql = f"""
|
148
|
+
SELECT id, _depth
|
149
|
+
FROM {self.model._meta.db_table}
|
150
|
+
WHERE id IN ({format_ids})
|
151
|
+
"""
|
152
|
+
cursor.execute(sql, list(result_set))
|
153
|
+
for node_id, depth in cursor.fetchall():
|
154
|
+
depth_lookup[node_id] = depth
|
155
|
+
|
156
|
+
|
157
|
+
ordered_pks = sorted(result_set, key=lambda pk: (depth_lookup.get(pk, 0), pk))
|
158
|
+
return [{"mode": "update", "parent_id": pk} for pk in ordered_pks]
|
141
159
|
|
142
160
|
def _get_root_ids(self):
|
143
161
|
"""Return root node IDs."""
|
@@ -7,6 +7,7 @@ from .family import TreeNodeFamilyMixin
|
|
7
7
|
from .logical import TreeNodeLogicalMixin
|
8
8
|
from .node import TreeNodeNodeMixin
|
9
9
|
from .properties import TreeNodePropertiesMixin
|
10
|
+
from .search import TreeNodeSearchMixin
|
10
11
|
from .roots import TreeNodeRootsMixin
|
11
12
|
from .siblings import TreeNodeSiblingsMixin
|
12
13
|
from .tree import TreeNodeTreeMixin
|
@@ -16,7 +17,8 @@ from .update import RawSQLMixin
|
|
16
17
|
__all__ = [
|
17
18
|
"TreeNodeAncestorsMixin", "TreeNodeChildrenMixin", "TreeNodeFamilyMixin",
|
18
19
|
"TreeNodeDescendantsMixin", "TreeNodeLogicalMixin", "TreeNodeNodeMixin",
|
19
|
-
"
|
20
|
+
"TreeNodeSearchMixin", "TreeNodePropertiesMixin", "TreeNodeRootsMixin",
|
21
|
+
"TreeNodeSiblingsMixin", "TreeNodeTreeMixin", "RawSQLMixin"
|
20
22
|
"TreeNodeTreeMixin", "RawSQLMixin"
|
21
23
|
]
|
22
24
|
|
treenode/models/mixins/node.py
CHANGED
@@ -40,18 +40,18 @@ class TreeNodeNodeMixin(models.Model):
|
|
40
40
|
self.refresh()
|
41
41
|
return self._depth
|
42
42
|
|
43
|
-
def distance_to(self,
|
43
|
+
def distance_to(self, target):
|
44
44
|
"""Return number of edges on shortest path between two nodes."""
|
45
45
|
self_path = self.query(objects='ancestors')
|
46
|
-
|
46
|
+
target_path = target.query(objects='ancestors')
|
47
47
|
|
48
48
|
i = 0
|
49
|
-
for a, b in zip(self_path,
|
49
|
+
for a, b in zip(self_path, target_path):
|
50
50
|
if a != b:
|
51
51
|
break
|
52
52
|
i += 1
|
53
53
|
|
54
|
-
return (len(self_path) - i) + (len(
|
54
|
+
return (len(self_path) - i) + (len(target_path) - i)
|
55
55
|
|
56
56
|
def get_index(self):
|
57
57
|
"""Get the node index (self, index in node.parent.children list)."""
|
treenode/models/mixins/roots.py
CHANGED
@@ -0,0 +1,55 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
TreeNode Search Mixin
|
4
|
+
|
5
|
+
Version: 3.2.1
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
from django.db import models
|
11
|
+
|
12
|
+
|
13
|
+
class TreeNodeSearchMixin(models.Model):
|
14
|
+
"""Mixin that provides helper methods for quick node lookup."""
|
15
|
+
|
16
|
+
class Meta:
|
17
|
+
"""Mixin Meta Class."""
|
18
|
+
abstract = True
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def find_by_path(cls, path, attr="id", delimiter="/"):
|
22
|
+
"""Return a node referenced by breadcrumbs path.
|
23
|
+
|
24
|
+
The ``path`` argument may be a string or an iterable of breadcrumb
|
25
|
+
values previously produced by :py:meth:`get_breadcrumbs`.
|
26
|
+
If the path cannot be resolved, ``None`` is returned.
|
27
|
+
"""
|
28
|
+
if path is None:
|
29
|
+
return None
|
30
|
+
|
31
|
+
if not isinstance(path, (list, tuple)):
|
32
|
+
path = [p for p in str(path).strip(delimiter).split(delimiter) if p]
|
33
|
+
|
34
|
+
parent = None
|
35
|
+
for token in path:
|
36
|
+
lookup = {attr: token}
|
37
|
+
if parent is None:
|
38
|
+
qs = cls.objects.filter(parent_id__isnull=True, **lookup)
|
39
|
+
else:
|
40
|
+
qs = cls.objects.filter(parent=parent, **lookup)
|
41
|
+
parent = qs.first()
|
42
|
+
if parent is None:
|
43
|
+
return None
|
44
|
+
return parent
|
45
|
+
|
46
|
+
@classmethod
|
47
|
+
def find_in_subtree(cls, parent, value, attr="id"):
|
48
|
+
"""Search ``parent`` descendants for attribute ``attr`` equal to ``value``."""
|
49
|
+
if parent is None:
|
50
|
+
return None
|
51
|
+
prefix = parent.get_order() + "."
|
52
|
+
return cls.objects.filter(_path__startswith=prefix, **{attr: value}).first()
|
53
|
+
|
54
|
+
|
55
|
+
# The End
|
treenode/models/models.py
CHANGED
@@ -14,12 +14,12 @@ Features:
|
|
14
14
|
- Provides a caching mechanism to optimize performance.
|
15
15
|
- Includes methods for tree traversal, manipulation, and serialization.
|
16
16
|
|
17
|
+
With full support for SQL queues, deferred execution,
|
18
|
+
custom sorting, and a sleek architecture without unnecessary duplication.
|
19
|
+
|
17
20
|
Version: 3.0.7
|
18
21
|
Author: Timur Kady
|
19
22
|
Email: timurkady@yandex.com
|
20
|
-
|
21
|
-
Причём с абсолютной поддержкой SQL-очередей, deferred execution,
|
22
|
-
кастомной сортировки и крутой архитектурой без лишнего дублирования.
|
23
23
|
"""
|
24
24
|
|
25
25
|
from __future__ import annotations
|
@@ -38,13 +38,16 @@ from ..cache import treenode_cache as cache
|
|
38
38
|
from ..settings import SEGMENT_LENGTH, BASE
|
39
39
|
from ..signals import disable_signals
|
40
40
|
|
41
|
+
import logging
|
42
|
+
logger = logging.getLogger(__name__)
|
41
43
|
|
42
44
|
class TreeNodeModel(
|
43
45
|
mx.TreeNodeAncestorsMixin, mx.TreeNodeChildrenMixin,
|
44
46
|
mx.TreeNodeFamilyMixin, mx.TreeNodeDescendantsMixin,
|
45
47
|
mx.TreeNodeLogicalMixin, mx.TreeNodeNodeMixin,
|
46
|
-
mx.
|
47
|
-
mx.
|
48
|
+
mx.TreeNodeSearchMixin, mx.TreeNodePropertiesMixin,
|
49
|
+
mx.TreeNodeRootsMixin, mx.TreeNodeSiblingsMixin,
|
50
|
+
mx.TreeNodeTreeMixin, mx.RawSQLMixin,
|
48
51
|
models.Model, metaclass=TreeNodeModelBase):
|
49
52
|
"""
|
50
53
|
Abstract tree node model.
|
@@ -156,10 +159,10 @@ class TreeNodeModel(
|
|
156
159
|
# Update subtree
|
157
160
|
self._update_path(self.parent_id)
|
158
161
|
self.sqlq.flush()
|
159
|
-
#
|
162
|
+
# Clear cache
|
160
163
|
self.clear_cache()
|
161
164
|
|
162
|
-
# Saving and
|
165
|
+
# Saving and Updating methods ----------------------------------
|
163
166
|
|
164
167
|
def save(self, *args, **kwargs):
|
165
168
|
"""
|
@@ -184,6 +187,7 @@ class TreeNodeModel(
|
|
184
187
|
is_new = False
|
185
188
|
is_shift = False
|
186
189
|
is_move = False
|
190
|
+
sorting_changed = False
|
187
191
|
|
188
192
|
if self.pk:
|
189
193
|
state = self.get_db_state()
|
@@ -192,8 +196,19 @@ class TreeNodeModel(
|
|
192
196
|
is_move = self.parent_id != state["parent_id"]
|
193
197
|
if is_move:
|
194
198
|
self._meta.model.tasks.add("update", state["parent_id"])
|
199
|
+
|
200
|
+
if self.sorting_field != "priority":
|
201
|
+
old_value = self.__class__.objects.filter(
|
202
|
+
pk=self.pk
|
203
|
+
).values_list(self.sorting_field, flat=True).first()
|
204
|
+
sorting_changed = old_value != getattr(
|
205
|
+
self, self.sorting_field
|
206
|
+
)
|
195
207
|
else:
|
196
|
-
|
208
|
+
logger.error(
|
209
|
+
"TreeNodeModel save error: object with pk %s not found in DB",
|
210
|
+
self.pk,
|
211
|
+
)
|
197
212
|
else:
|
198
213
|
is_new = True
|
199
214
|
|
@@ -226,12 +241,14 @@ class TreeNodeModel(
|
|
226
241
|
pass
|
227
242
|
"""
|
228
243
|
|
229
|
-
if is_new or is_move or is_shift:
|
244
|
+
if is_new or is_move or is_shift or sorting_changed:
|
230
245
|
# Step 1: Shift siblings
|
231
246
|
if (is_new or is_move) and (self.priority is not None):
|
232
247
|
self._shift_siblings_forward()
|
233
248
|
# Step 2: Update paths for the new parent -> sqlq
|
234
|
-
self.
|
249
|
+
path_ids = self.query.get_relative_pks(objects="ancestors", include_self=True)
|
250
|
+
update_root_id = path_ids[0] if path_ids else self.parent_id
|
251
|
+
self._meta.model.tasks.add("update", update_root_id)
|
235
252
|
# Step 3: Clear model cache
|
236
253
|
self.clear_cache()
|
237
254
|
|
@@ -240,7 +257,7 @@ class TreeNodeModel(
|
|
240
257
|
disable_signals(post_save, model)):
|
241
258
|
super().save(*args, **kwargs)
|
242
259
|
|
243
|
-
if is_new or is_move or is_shift:
|
260
|
+
if is_new or is_move or is_shift or sorting_changed:
|
244
261
|
# Run sql
|
245
262
|
# self.sqlq.flush()
|
246
263
|
setattr(model, 'is_dry', True)
|
@@ -335,11 +352,11 @@ class TreeNodeModel(
|
|
335
352
|
queue.extend(model.objects.filter(parent=node))
|
336
353
|
|
337
354
|
if verbose and errors:
|
338
|
-
|
355
|
+
logger.error("Tree integrity check failed:")
|
339
356
|
for err in errors:
|
340
|
-
|
357
|
+
logger.error(" - %s", err)
|
341
358
|
elif verbose:
|
342
|
-
|
359
|
+
logger.info("Tree integrity: OK ✅")
|
343
360
|
|
344
361
|
return errors
|
345
362
|
|
treenode/settings.py
CHANGED
@@ -18,11 +18,14 @@ SEGMENT_LENGTH = getattr(settings, "TREENODE_SEGMENT_LENGTH", 3)
|
|
18
18
|
# Serialization dictionary: hexadecimal encoding, fixed segment size
|
19
19
|
SEGMENT_BASE = 16
|
20
20
|
|
21
|
-
#
|
21
|
+
# Number of children per tree node
|
22
22
|
BASE = SEGMENT_BASE ** SEGMENT_LENGTH # 4096
|
23
23
|
|
24
24
|
|
25
25
|
TREENODE_PAD_CHAR = getattr(settings, "TREENODE_PAD_CHAR", "'0'")
|
26
26
|
|
27
|
+
# Optional JWT protection for API endpoints
|
28
|
+
API_USE_JWT = getattr(settings, "TREENODE_API_USE_JWT", False)
|
29
|
+
|
27
30
|
|
28
31
|
# The End
|
File without changes
|
File without changes
|
File without changes
|
@@ -3,8 +3,9 @@
|
|
3
3
|
|
4
4
|
{% block extrastyle %}
|
5
5
|
{{ block.super }}
|
6
|
-
|
7
|
-
<link rel="stylesheet" href="{% static 'css/
|
6
|
+
{# Load built-in admin form styles #}
|
7
|
+
<link rel="stylesheet" href="{% static '/admin/css/forms.css' %}">
|
8
|
+
<link rel="stylesheet" href="{% static 'treenode/css/treenode_tabs.css' %}">
|
8
9
|
{% endblock %}
|
9
10
|
|
10
11
|
{% block content %}
|
treenode/utils/db/compiler.py
CHANGED
@@ -26,88 +26,47 @@ class TreePathCompiler:
|
|
26
26
|
|
27
27
|
@classmethod
|
28
28
|
def update_path(cls, model, parent_id=None):
|
29
|
-
"""
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
row_number_expr = f"ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
|
50
|
-
hex_expr = SQLCompat.to_hex(row_number_expr)
|
51
|
-
lpad_expr = SQLCompat.lpad(hex_expr, SEGMENT_LENGTH, "'0'")
|
29
|
+
"""Rebuild subtree using BFS so parents update before children."""
|
30
|
+
table = model._meta.db_table
|
31
|
+
sort_field = model.sorting_field
|
32
|
+
|
33
|
+
def fetch_children(pid):
|
34
|
+
if pid is None:
|
35
|
+
where = "parent_id IS NULL"
|
36
|
+
params = []
|
37
|
+
else:
|
38
|
+
where = "parent_id = %s"
|
39
|
+
params = [pid]
|
40
|
+
with connection.cursor() as cursor:
|
41
|
+
cursor.execute(
|
42
|
+
f"SELECT id FROM {table} WHERE {where} ORDER BY {sort_field}, id",
|
43
|
+
params,
|
44
|
+
)
|
45
|
+
return [row[0] for row in cursor.fetchall()]
|
46
|
+
|
47
|
+
queue = []
|
52
48
|
|
53
49
|
if parent_id is None:
|
54
|
-
|
55
|
-
|
56
|
-
SELECT
|
57
|
-
c.id,
|
58
|
-
c.parent_id,
|
59
|
-
{row_number_expr} AS new_priority,
|
60
|
-
{new_path_expr} AS new_path,
|
61
|
-
0 AS new_depth
|
62
|
-
FROM {qname} AS c
|
63
|
-
WHERE c.parent_id IS NULL
|
64
|
-
"""
|
65
|
-
params = []
|
50
|
+
for idx, node_id in enumerate(fetch_children(None)):
|
51
|
+
queue.append((node_id, "", 0, idx))
|
66
52
|
else:
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
recursive_hex_expr, SEGMENT_LENGTH, "'0'")
|
85
|
-
recursive_path_expr = SQLCompat.concat(
|
86
|
-
"t.new_path", "'.'", recursive_lpad_expr)
|
87
|
-
|
88
|
-
recursive_sql = f"""
|
89
|
-
SELECT
|
90
|
-
c.id,
|
91
|
-
c.parent_id,
|
92
|
-
{recursive_row_number_expr} AS new_priority,
|
93
|
-
{recursive_path_expr} AS new_path,
|
94
|
-
t.new_depth + 1 AS new_depth
|
95
|
-
FROM {qname} c
|
96
|
-
JOIN tree_cte t ON c.parent_id = t.id
|
97
|
-
"""
|
98
|
-
|
99
|
-
final_sql = SQLCompat.update_from(
|
100
|
-
db_table=db_table,
|
101
|
-
cte_header=cte_header,
|
102
|
-
base_sql=base_sql,
|
103
|
-
recursive_sql=recursive_sql,
|
104
|
-
update_fields=["priority", "_path", "_depth"]
|
105
|
-
)
|
106
|
-
|
107
|
-
with connection.cursor() as cursor:
|
108
|
-
# Make params read-only
|
109
|
-
params = tuple(params)
|
110
|
-
cursor.execute(final_sql, params)
|
53
|
+
parent_data = model.objects.filter(pk=parent_id).values("_path", "_depth").first()
|
54
|
+
parent_path = parent_data["_path"] if parent_data else ""
|
55
|
+
depth = (parent_data["_depth"] + 1) if parent_data else 0
|
56
|
+
for idx, node_id in enumerate(fetch_children(parent_id)):
|
57
|
+
queue.append((node_id, parent_path, depth, idx))
|
58
|
+
|
59
|
+
while queue:
|
60
|
+
node_id, base_path, depth, index = queue.pop(0)
|
61
|
+
segment = f"{index:0{SEGMENT_LENGTH}X}"
|
62
|
+
path = segment if base_path == "" else f"{base_path}.{segment}"
|
63
|
+
with connection.cursor() as cursor:
|
64
|
+
cursor.execute(
|
65
|
+
f"UPDATE {table} SET priority=%s, _path=%s, _depth=%s WHERE id=%s",
|
66
|
+
[index, path, depth, node_id],
|
67
|
+
)
|
68
|
+
for child_idx, child_id in enumerate(fetch_children(node_id)):
|
69
|
+
queue.append((child_id, path, depth + 1, child_idx))
|
111
70
|
|
112
71
|
|
113
72
|
# The End
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import jwt
|
2
|
+
from functools import wraps
|
3
|
+
|
4
|
+
from django.conf import settings
|
5
|
+
from django.http import JsonResponse
|
6
|
+
|
7
|
+
|
8
|
+
def jwt_required(view_func):
|
9
|
+
"""Ensure that request has valid JWT token."""
|
10
|
+
|
11
|
+
@wraps(view_func)
|
12
|
+
def _wrapped(request, *args, **kwargs):
|
13
|
+
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
|
14
|
+
if not auth_header.startswith("Bearer "):
|
15
|
+
return JsonResponse({"detail": "Authorization header missing"}, status=401)
|
16
|
+
token = auth_header.split(" ", 1)[1]
|
17
|
+
try:
|
18
|
+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
19
|
+
except jwt.PyJWTError:
|
20
|
+
return JsonResponse({"detail": "Invalid token"}, status=401)
|
21
|
+
request.jwt_payload = payload
|
22
|
+
return view_func(request, *args, **kwargs)
|
23
|
+
|
24
|
+
return _wrapped
|
25
|
+
|
treenode/version.py
CHANGED
treenode/views/autoapi.py
CHANGED
@@ -1,91 +1,95 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
"""
|
3
|
-
Route generator for all models inherited from TreeNodeModel
|
4
|
-
|
5
|
-
Version: 3.0.0
|
6
|
-
Author: Timur Kady
|
7
|
-
Email: timurkady@yandex.com
|
8
|
-
"""
|
9
|
-
|
10
|
-
|
11
|
-
from django.apps import apps
|
12
|
-
from django.urls import path
|
13
|
-
from django.conf import settings
|
14
|
-
from django.contrib.auth.decorators import login_required
|
15
|
-
|
16
|
-
from ..models import TreeNodeModel
|
17
|
-
from
|
18
|
-
from .
|
19
|
-
from .
|
20
|
-
from .
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
if
|
41
|
-
return
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
("
|
74
|
-
#
|
75
|
-
("<int:pk>/
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
Route generator for all models inherited from TreeNodeModel
|
4
|
+
|
5
|
+
Version: 3.0.0
|
6
|
+
Author: Timur Kady
|
7
|
+
Email: timurkady@yandex.com
|
8
|
+
"""
|
9
|
+
|
10
|
+
|
11
|
+
from django.apps import apps
|
12
|
+
from django.urls import path
|
13
|
+
from django.conf import settings
|
14
|
+
from django.contrib.auth.decorators import login_required
|
15
|
+
|
16
|
+
from ..models import TreeNodeModel
|
17
|
+
from ..settings import API_USE_JWT
|
18
|
+
from ..utils.jwt_auth import jwt_required
|
19
|
+
from .autocomplete import TreeNodeAutocompleteView
|
20
|
+
from .children import TreeChildrenView
|
21
|
+
from .crud import TreeNodeBaseAPIView
|
22
|
+
from .search import TreeSearchView
|
23
|
+
|
24
|
+
|
25
|
+
class AutoTreeAPI:
|
26
|
+
"""Auto-discover and expose TreeNode-based APIs."""
|
27
|
+
|
28
|
+
def __init__(self, base_view=TreeNodeBaseAPIView, base_url="api"):
|
29
|
+
"""Init auto-discover."""
|
30
|
+
self.base_view = base_view
|
31
|
+
self.base_url = base_url
|
32
|
+
|
33
|
+
def protect_view(self, view, model):
|
34
|
+
"""
|
35
|
+
Protect view.
|
36
|
+
|
37
|
+
Protects view with login_required if needed, based on model attribute
|
38
|
+
or global settings.
|
39
|
+
"""
|
40
|
+
if API_USE_JWT:
|
41
|
+
return jwt_required(view)
|
42
|
+
if getattr(model, 'api_login_required', None) is True:
|
43
|
+
return login_required(view)
|
44
|
+
if getattr(settings, 'TREENODE_API_LOGIN_REQUIRED', False):
|
45
|
+
return login_required(view)
|
46
|
+
return view
|
47
|
+
|
48
|
+
def discover(self):
|
49
|
+
"""Scan models and generate API urls."""
|
50
|
+
urls = [
|
51
|
+
# Admin and Widget end-points
|
52
|
+
path("widget/autocomplete/", TreeNodeAutocompleteView.as_view(), name="tree_autocomplete"), # noqa: D501
|
53
|
+
path("widget/children/", TreeChildrenView.as_view(), name="tree_children"), # noqa: D501
|
54
|
+
path("widget/search/", TreeSearchView.as_view(), name="tree_search"), # noqa: D501
|
55
|
+
]
|
56
|
+
for model in apps.get_models():
|
57
|
+
if issubclass(model, TreeNodeModel) and model is not TreeNodeModel:
|
58
|
+
model_name = model._meta.model_name
|
59
|
+
|
60
|
+
# Dynamically create an API view class for the model
|
61
|
+
api_view_class = type(
|
62
|
+
f"{model_name.capitalize()}APIView",
|
63
|
+
(self.base_view,),
|
64
|
+
{"model": model}
|
65
|
+
)
|
66
|
+
|
67
|
+
# List of API actions and their corresponding URL patterns
|
68
|
+
action_patterns = [
|
69
|
+
# List / Create
|
70
|
+
("", None, f"{model_name}-list"),
|
71
|
+
# Retrieve / Update / Delete
|
72
|
+
("<int:pk>/", None, f"{model_name}-detail"),
|
73
|
+
("tree/", {'action': 'tree'}, f"{model_name}-tree"),
|
74
|
+
# Direct children
|
75
|
+
("<int:pk>/children/", {'action': 'children'}, f"{model_name}-children"), # noqa: D501
|
76
|
+
# All descendants
|
77
|
+
("<int:pk>/descendants/", {'action': 'descendants'}, f"{model_name}-descendants"), # noqa: D501
|
78
|
+
# Ancestors + Self + Descendants
|
79
|
+
("<int:pk>/family/", {'action': 'family'}, f"{model_name}-family"), # noqa: D501
|
80
|
+
]
|
81
|
+
|
82
|
+
# Create secured view instance once
|
83
|
+
view = self.protect_view(api_view_class.as_view(), model)
|
84
|
+
|
85
|
+
# Automatically build all paths for this model
|
86
|
+
for url_suffix, extra_kwargs, route_name in action_patterns:
|
87
|
+
urls.append(
|
88
|
+
path(
|
89
|
+
f"{self.base_url}/{model_name}/{url_suffix}",
|
90
|
+
view,
|
91
|
+
extra_kwargs or {},
|
92
|
+
name=route_name
|
93
|
+
)
|
94
|
+
)
|
95
|
+
return urls
|
treenode/widgets.py
CHANGED
@@ -24,8 +24,8 @@ class TreeWidget(forms.Widget):
|
|
24
24
|
class Media:
|
25
25
|
"""Meta class to define required CSS and JS files."""
|
26
26
|
|
27
|
-
css = {"all": ("css/tree_widget.css",)}
|
28
|
-
js = ("js/tree_widget.js",)
|
27
|
+
css = {"all": ("treenode/css/tree_widget.css",)}
|
28
|
+
js = ("treenode/js/tree_widget.js",)
|
29
29
|
|
30
30
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
31
31
|
"""Build attributes for the widget."""
|
@@ -1,93 +0,0 @@
|
|
1
|
-
django_fast_treenode-3.0.8.dist-info/licenses/LICENSE,sha256=SSYqS84FCnAW7tAxmjBKU8qAa8Jv4VGPuSSGeHwWtJE,1095
|
2
|
-
treenode/__init__.py,sha256=3z1hWpHyy4wg6uz7HCmRi9FaXYeN5CfANVpa77UIoPw,53
|
3
|
-
treenode/apps.py,sha256=QlwjNDM9rkUoWB8Vm8-OkS6lNx0-aTByuGZlu9wrQMs,1832
|
4
|
-
treenode/cache.py,sha256=2jUiiecfFxwB7QFukpU4u0FnDzGH6hNRfo6KAYvs6vM,8447
|
5
|
-
treenode/forms.py,sha256=V-upmbYSW1BbuXdSBGExHxw_j5TTUheaHvreK9tSGTE,3155
|
6
|
-
treenode/settings.py,sha256=oSkcKXNVd28HXrlWZIH2VYinCMq-UdCDlX4KD0Qc_Xk,631
|
7
|
-
treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
|
8
|
-
treenode/tests.py,sha256=2uDafv3Ns6f7Vy1ekUtgYxCZEi1KRyesZDTAFhYcX-E,63
|
9
|
-
treenode/urls.py,sha256=krHvVigc_dxC0z5hEd2rgeH6th8jW7qJY3Qbia-419Y,240
|
10
|
-
treenode/version.py,sha256=5TH3IchoS9D8YEFnTIlRrjoALxMdemh1PCN0Bb9Jnss,220
|
11
|
-
treenode/widgets.py,sha256=VItPvN9XgaSRI_MZjKEmtaHDJcn2bDIQIppwKjXmYQM,4017
|
12
|
-
treenode/admin/__init__.py,sha256=XNEYHdF5lKb0vpdlVxdR2fxj5oUgzyx1YyCwsv0gxHw,100
|
13
|
-
treenode/admin/admin.py,sha256=uORuVSx-p0MYXLRVl54cBa7IAL9zicyeYTX3Cv5pBDc,7462
|
14
|
-
treenode/admin/changelist.py,sha256=KUYS9MaR8Ck_1xmMqupobxWKarrJEqmHuEG32CL01Bo,1662
|
15
|
-
treenode/admin/exporter.py,sha256=QE74V6W3tvwA5kCvBt1MmVlLOaWh-o8EU63cgmiwD5Q,5724
|
16
|
-
treenode/admin/importer.py,sha256=hK3D-1DZcoowGblRluGzng3n5Bf__hMsbNaIGXRpRdg,6263
|
17
|
-
treenode/admin/mixin.py,sha256=jOkKKt0CpknGokluW8T1YB7VhkCkaAAMYBuptbYDXAw,10648
|
18
|
-
treenode/managers/__init__.py,sha256=c7F9Ku9489Hv6lTpUY2nbyBlWFCXBWAkNBm4xTKcjL8,186
|
19
|
-
treenode/managers/managers.py,sha256=8OaFxtajyR1d7-UHyiUbifMBEF9cjfHTIEYPkYUWmt0,7166
|
20
|
-
treenode/managers/queries.py,sha256=Kepax8SDn7G5tOlPRWBCp5Oyp49O5iMITCMBoNCm_Ak,10655
|
21
|
-
treenode/managers/tasks.py,sha256=nVlGDxNnFlZS1-oU8UH_yTXJERcGrAZ9kCmKwKO5WuY,7138
|
22
|
-
treenode/models/__init__.py,sha256=iR4ksCKoayvkIWWgGk6OUGHZC3D0mzAtgdBcS2vQPBw,188
|
23
|
-
treenode/models/decorators.py,sha256=N2dcnWqSCiEXDcYCf0zVijrbGUC8kYlqOLi_GKFmECU,1457
|
24
|
-
treenode/models/factory.py,sha256=sPUSrvo1za-r6ny3B8ptwevyjO8-iUpPNrT0eSD2kvI,1786
|
25
|
-
treenode/models/models.py,sha256=n_2nCJzBOEcmh3r-GCoHV3ZiTX8Xe1ewK9SPptvI_M0,12273
|
26
|
-
treenode/models/mixins/__init__.py,sha256=aALVKMGAWbgMAeKWS6s-NF3L5FmRX96mQxtpthOX-Ec,805
|
27
|
-
treenode/models/mixins/ancestors.py,sha256=9g-0nPHoiF_SX2kN4uDLdbWyw-TDCz1YqxLJngwTZOQ,1971
|
28
|
-
treenode/models/mixins/children.py,sha256=H9iMqgucSmwLX-3O3QUj1a2PUQTmmWZ4GPPjRZ9a5E4,2399
|
29
|
-
treenode/models/mixins/descendants.py,sha256=RKowr29JUUO3E_UDvbLbMLhbn5IUkY2eh3AXfU8XSE8,2165
|
30
|
-
treenode/models/mixins/family.py,sha256=MB5kWRVvxU_xmSgCekveTP5Vhj4wJki8bU7hzn9RNLE,1673
|
31
|
-
treenode/models/mixins/logical.py,sha256=gh5wv5XZDs5GWarU6g9zKXWNwji7SE3zSVNIpywDWjw,2190
|
32
|
-
treenode/models/mixins/node.py,sha256=8kqYdFTyZK5WsSxguuX3jBo1nZehDmwIBjm38S6QJw4,8576
|
33
|
-
treenode/models/mixins/properties.py,sha256=1O6p2tfvOesBooZeOeuXi8yfEO_o-5gn_agtKqMxU-s,3945
|
34
|
-
treenode/models/mixins/roots.py,sha256=2lDjUH22NZFUMVbVZgsdtEqDoUyHhGTiaoeo9h3KRfk,4102
|
35
|
-
treenode/models/mixins/siblings.py,sha256=4XvQS7WkgolzEZdnURhIClo-VUcpuqQ-Sc7PDYKGmFw,3069
|
36
|
-
treenode/models/mixins/tree.py,sha256=5Cew5c-NtTmz15S_ueDZ-fojHtQKZEKNxmp3W6qlC-8,14512
|
37
|
-
treenode/models/mixins/update.py,sha256=oCZkMnfT23n2n3_mnokDrtTLS_jO_lJRwG3ENkH_DE4,4948
|
38
|
-
treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
39
|
-
treenode/static/css/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
40
|
-
treenode/static/css/tree_widget.css,sha256=YVIHuZT_E5ck5N7wvYvuwgV_m-4Ta_uGFiNUoTw-zbk,4861
|
41
|
-
treenode/static/css/treenode_admin.css,sha256=w7yyyf4aFsJKlr_YJPVq6ap5ZvA5Co7ZCYq6ipuV958,3291
|
42
|
-
treenode/static/css/treenode_tabs.css,sha256=pbulaUEhNMQFNWXFQrRN1zMyKjoSqLAnjNygpouPUfE,879
|
43
|
-
treenode/static/js/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
44
|
-
treenode/static/js/lz-string.min.js,sha256=TAnTJQd2AlLqT9M2TU7GFjnoj9SIfwLeZnpEtLkP624,4718
|
45
|
-
treenode/static/js/tree_widget.js,sha256=2LU8IIJD5TXbWVGjCEgrj5aYnUYtwBZECzj25XLtxNc,9633
|
46
|
-
treenode/static/js/treenode_admin.js,sha256=IU3Zj2Q-WBxftl0zsjeRPrt1xOzzdEKlzGEMThi3hrk,9706
|
47
|
-
treenode/static/vendors/jquery-ui/AUTHORS.txt,sha256=geZ7JfVsTJq8Wgjrcm1p56Zqjxx7PL9FkxhKiN8dQg8,14848
|
48
|
-
treenode/static/vendors/jquery-ui/LICENSE.txt,sha256=9kkCXPZcjc2FiLCTeIO6elK9ttNRKUZKNqFgLrACUPE,1818
|
49
|
-
treenode/static/vendors/jquery-ui/index.html,sha256=RSv8cA662qJdc02OX-UmpaBJoZrngoQqnNWVZNn9GQQ,24229
|
50
|
-
treenode/static/vendors/jquery-ui/jquery-ui.css,sha256=O8mmheetXPeKUa-iDk8fdxDhs096eWIi10qLJb6OXyw,17171
|
51
|
-
treenode/static/vendors/jquery-ui/jquery-ui.js,sha256=XtnlPiKuVzMh-cef76mBCNCTPqk2cIlKBgmTHfRBDog,146430
|
52
|
-
treenode/static/vendors/jquery-ui/jquery-ui.min.css,sha256=Yv2MureLOGZicbgEv28X9ft9o0SRBZ84PWIgg6h_V2E,15003
|
53
|
-
treenode/static/vendors/jquery-ui/jquery-ui.min.js,sha256=FT6F7DDm5J1Qz6B5fv3e8jnmx9QyyfR2qUeTptb73bA,70364
|
54
|
-
treenode/static/vendors/jquery-ui/jquery-ui.structure.css,sha256=392jP1trXFqS1OVv2X50px0F8w1DeTyIvvHL9wxNXi0,328
|
55
|
-
treenode/static/vendors/jquery-ui/jquery-ui.structure.min.css,sha256=cXLe7UDVCN5SvPpbb3HJdndQIk1hlWMrzyRRYO9rcLs,208
|
56
|
-
treenode/static/vendors/jquery-ui/jquery-ui.theme.css,sha256=grsB4hVvwiBxnNsG1c_ocH1YK76CExOav_dn_BsxepU,17140
|
57
|
-
treenode/static/vendors/jquery-ui/jquery-ui.theme.min.css,sha256=oW6e3DmGiKSNLzo3HCUnjes9ykGZ6gXEtl3LjiQIkMs,13708
|
58
|
-
treenode/static/vendors/jquery-ui/package.json,sha256=AvCATruWzbwY7NsfzcUR2WqPjslGO8M7hHzTj1AYDQY,2122
|
59
|
-
treenode/static/vendors/jquery-ui/external/jquery/jquery.js,sha256=eKhayi8LEQwp4NKxN-CfCh-3qOVUtJn3QNZ0TciWLP4,285314
|
60
|
-
treenode/static/vendors/jquery-ui/images/ui-icons_444444_256x240.png,sha256=YW09ms2dyfoVEOT_NEpaWXMMRLJxVIqu8hku63SoCEA,7107
|
61
|
-
treenode/static/vendors/jquery-ui/images/ui-icons_555555_256x240.png,sha256=HP6KoU5aOllbVtaudhmNOLhBIzi_0oCuxBjwyhDATOo,7105
|
62
|
-
treenode/static/vendors/jquery-ui/images/ui-icons_777620_256x240.png,sha256=NxKGIMNmb6YYZor0RuijQ7PTK5BoA9b5qXLjYTMFvXo,4615
|
63
|
-
treenode/static/vendors/jquery-ui/images/ui-icons_777777_256x240.png,sha256=TLerPghEHzRXKPUuwr0VPT1PusnYFUe1-k1QIyeXe7g,7115
|
64
|
-
treenode/static/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png,sha256=S36ETnNcCxoWxmxAkVl0P3LYEDInjxi9GnG_g5uho1c,4615
|
65
|
-
treenode/static/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png,sha256=SBgnaQEkoyko7AbSFy0lDoDzAup6bkVQ74XyenFleUM,6395
|
66
|
-
treenode/templates/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
67
|
-
treenode/templates/treenode/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
68
|
-
treenode/templates/treenode/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
69
|
-
treenode/templates/treenode/admin/treenode_ajax_rows.html,sha256=zFyPaTbSyxRjOqQ85SMv__qTIYDjEna6chYODBypDZA,224
|
70
|
-
treenode/templates/treenode/admin/treenode_changelist.html,sha256=JiUwX33w5WwegX7gM2ctw-bx9XOyXwKt9QB_6g7Dfz8,2283
|
71
|
-
treenode/templates/treenode/admin/treenode_import_export.html,sha256=K6L_JnhGpWgQdduXSfAJ0UuTBfSN4ylFw9w_e5tPDDE,2926
|
72
|
-
treenode/templates/treenode/admin/treenode_rows.html,sha256=de90wKpNdVB2C5CGnIkStBB0dWCDkmWr_pluV_NgBUs,1857
|
73
|
-
treenode/templates/treenode/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
|
74
|
-
treenode/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
75
|
-
treenode/templatetags/treenode_admin.py,sha256=5f5oqAS4zC_f0kkJRsm5MqXHjsKJXn8GslbZcbMivxg,2736
|
76
|
-
treenode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
77
|
-
treenode/utils/db/__init__.py,sha256=RwicAcJSI1nhIPWLdT7j9TFsgOc9834VDn9lVn54GlY,247
|
78
|
-
treenode/utils/db/compiler.py,sha256=PgD9ybS5H8OUHw1gkFBQHhnrf5HiCx8QXUMRhydwh7o,3824
|
79
|
-
treenode/utils/db/db_vendor.py,sha256=4SyEHl51jVCDB3is4omHCf2bTB_QV3RvemUQYxJP5m0,930
|
80
|
-
treenode/utils/db/service.py,sha256=PF85Yhz2xUWFFCzpLYotmiNTZXXEH61rhswslSxEUds,2640
|
81
|
-
treenode/utils/db/sqlcompat.py,sha256=K71ggkKIvpdTtHQ6Y4qcbo6cj2eYiEfy6DlVBr8Po1E,4460
|
82
|
-
treenode/utils/db/sqlquery.py,sha256=KXcfKbbaBF-D134H_2DiPQtjedR79SJNXPJc0msZYEc,1938
|
83
|
-
treenode/views/__init__.py,sha256=ppxbBx51TUaKstJFpAd_DTmbKjbZGmVMLNYSpgUKnd0,111
|
84
|
-
treenode/views/autoapi.py,sha256=IAQfhWey4z-QBuAn-QDHhpJSFCF702sHtqYOA6vK4BY,3537
|
85
|
-
treenode/views/autocomplete.py,sha256=ERGJT4jfCE2GUv94Ja8SgJyG1FOAyqIeB0PKH1fyU7g,1417
|
86
|
-
treenode/views/children.py,sha256=seO0SNKriRY2FJOO1oZgX42iKolXMf3EF76GrfdJZLQ,1111
|
87
|
-
treenode/views/common.py,sha256=kUN3IgMZCdNdJ2haxB9MTGcn2rctyFUAHNagzcu9wXk,594
|
88
|
-
treenode/views/crud.py,sha256=RI5rdyD4hZTszjZFThByxi_lkAeJlqbDCXFkD8iyzKE,7424
|
89
|
-
treenode/views/search.py,sha256=c_GyooT3jyoNa96bBxfoWruRN1wIw-ZGYvwGKkGojTs,1501
|
90
|
-
django_fast_treenode-3.0.8.dist-info/METADATA,sha256=-DOMkpuNAIruYNiRcCc7g6zGVpbWjter7IEDb9k10mg,10249
|
91
|
-
django_fast_treenode-3.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
92
|
-
django_fast_treenode-3.0.8.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
|
93
|
-
django_fast_treenode-3.0.8.dist-info/RECORD,,
|
File without changes
|
{django_fast_treenode-3.0.8.dist-info → django_fast_treenode-3.2.1.dist-info}/licenses/LICENSE
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|