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.
Files changed (55) hide show
  1. {django_fast_treenode-3.0.8.dist-info → django_fast_treenode-3.2.1.dist-info}/METADATA +3 -1
  2. django_fast_treenode-3.2.1.dist-info/RECORD +98 -0
  3. treenode/admin/admin.py +5 -5
  4. treenode/admin/mixin.py +6 -6
  5. treenode/managers/managers.py +1 -1
  6. treenode/managers/tasks.py +19 -1
  7. treenode/models/mixins/__init__.py +3 -1
  8. treenode/models/mixins/children.py +1 -1
  9. treenode/models/mixins/descendants.py +1 -1
  10. treenode/models/mixins/node.py +4 -4
  11. treenode/models/mixins/roots.py +1 -1
  12. treenode/models/mixins/search.py +55 -0
  13. treenode/models/mixins/siblings.py +1 -1
  14. treenode/models/models.py +31 -14
  15. treenode/settings.py +4 -1
  16. treenode/static/treenode/.gitkeep +0 -0
  17. treenode/static/treenode/vendors/.gitkeep +0 -0
  18. treenode/static/treenode/vendors/jquery-ui/.gitkeep +0 -0
  19. treenode/templates/treenode/admin/treenode_import_export.html +3 -2
  20. treenode/utils/db/compiler.py +38 -79
  21. treenode/utils/jwt_auth.py +25 -0
  22. treenode/version.py +2 -2
  23. treenode/views/autoapi.py +95 -91
  24. treenode/widgets.py +2 -2
  25. django_fast_treenode-3.0.8.dist-info/RECORD +0 -93
  26. {django_fast_treenode-3.0.8.dist-info → django_fast_treenode-3.2.1.dist-info}/WHEEL +0 -0
  27. {django_fast_treenode-3.0.8.dist-info → django_fast_treenode-3.2.1.dist-info}/licenses/LICENSE +0 -0
  28. {django_fast_treenode-3.0.8.dist-info → django_fast_treenode-3.2.1.dist-info}/top_level.txt +0 -0
  29. /treenode/static/{css → treenode/css}/.gitkeep +0 -0
  30. /treenode/static/{css → treenode/css}/tree_widget.css +0 -0
  31. /treenode/static/{css → treenode/css}/treenode_admin.css +0 -0
  32. /treenode/static/{css → treenode/css}/treenode_tabs.css +0 -0
  33. /treenode/static/{js → treenode/js}/.gitkeep +0 -0
  34. /treenode/static/{js → treenode/js}/lz-string.min.js +0 -0
  35. /treenode/static/{js → treenode/js}/tree_widget.js +0 -0
  36. /treenode/static/{js → treenode/js}/treenode_admin.js +0 -0
  37. /treenode/static/{vendors → treenode/vendors}/jquery-ui/AUTHORS.txt +0 -0
  38. /treenode/static/{vendors → treenode/vendors}/jquery-ui/LICENSE.txt +0 -0
  39. /treenode/static/{vendors → treenode/vendors}/jquery-ui/external/jquery/jquery.js +0 -0
  40. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  41. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  42. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  43. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  44. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  45. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  46. /treenode/static/{vendors → treenode/vendors}/jquery-ui/index.html +0 -0
  47. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.css +0 -0
  48. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.js +0 -0
  49. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.css +0 -0
  50. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.js +0 -0
  51. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.css +0 -0
  52. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.min.css +0 -0
  53. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.css +0 -0
  54. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.min.css +0 -0
  55. /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.0.8
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
- """Rander rows for incert to changelist."""
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
- Impoern View.
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
 
@@ -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
- inte
210
+ intentional.
211
211
  """
212
212
  return models.QuerySet(self.model, using=self.db)\
213
213
  .bulk_update(*args, **kwargs)
@@ -137,7 +137,25 @@ class TreeTaskQueue:
137
137
  if not merged:
138
138
  result_set.add(current)
139
139
 
140
- return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)]
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
- "TreeNodePropertiesMixin", "TreeNodeRootsMixin", "TreeNodeSiblingsMixin",
20
+ "TreeNodeSearchMixin", "TreeNodePropertiesMixin", "TreeNodeRootsMixin",
21
+ "TreeNodeSiblingsMixin", "TreeNodeTreeMixin", "RawSQLMixin"
20
22
  "TreeNodeTreeMixin", "RawSQLMixin"
21
23
  ]
22
24
 
@@ -86,4 +86,4 @@ class TreeNodeChildrenMixin(models.Model):
86
86
  """Get the last child node or None if it has no children."""
87
87
  return self.get_children_queryset().last()
88
88
 
89
- # The End
89
+ # The End
@@ -74,4 +74,4 @@ class TreeNodeDescendantsMixin(models.Model):
74
74
  )
75
75
 
76
76
 
77
- # The End
77
+ # The End
@@ -40,18 +40,18 @@ class TreeNodeNodeMixin(models.Model):
40
40
  self.refresh()
41
41
  return self._depth
42
42
 
43
- def distance_to(self, targer):
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
- targer_path = targer.query(objects='ancestors')
46
+ target_path = target.query(objects='ancestors')
47
47
 
48
48
  i = 0
49
- for a, b in zip(self_path, targer_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(targer_path) - i)
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)."""
@@ -137,4 +137,4 @@ class TreeNodeRootsMixin(models.Model):
137
137
  cursor.execute(query, params)
138
138
 
139
139
 
140
- # The End
140
+ # The End
@@ -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
@@ -99,4 +99,4 @@ class TreeNodeSiblingsMixin(models.Model):
99
99
  qs = self._meta.model.objects.filter(parent_id=self._parent_id)
100
100
  return qs.last()
101
101
 
102
- # The End
102
+ # 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.TreeNodePropertiesMixin, mx.TreeNodeRootsMixin,
47
- mx.TreeNodeSiblingsMixin, mx.TreeNodeTreeMixin, mx.RawSQLMixin,
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
- # Clead cache
162
+ # Clear cache
160
163
  self.clear_cache()
161
164
 
162
- # Saving and Udating methods ----------------------------------
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
- print("TreeNodeModel error: oject not found in DB! WTF, MF!")
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._meta.model.tasks.add("update", self.parent_id)
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
- print("Tree integrity check failed:")
355
+ logger.error("Tree integrity check failed:")
339
356
  for err in errors:
340
- print(" -", err)
357
+ logger.error(" - %s", err)
341
358
  elif verbose:
342
- print("Tree integrity: OK ✅")
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
- # Nubber children per one tree node
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
- <link rel="stylesheet" href="{% static 'admin/css/forms.css' %}">
7
- <link rel="stylesheet" href="{% static 'css/treenode_tabs.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 %}
@@ -26,88 +26,47 @@ class TreePathCompiler:
26
26
 
27
27
  @classmethod
28
28
  def update_path(cls, model, parent_id=None):
29
- """
30
- Rebuild subtree starting from parent_id.
31
-
32
- If parent_id=None, then the whole tree is rebuilt.
33
- Uses only fields: parent_id and id. All others (priority, _path,
34
- _depth) are recalculated.
35
- """
36
- db_table = model._meta.db_table
37
- # Will eliminate the risk if the user names the model order or user.
38
- qname = connection.ops.quote_name(db_table)
39
-
40
- sorting_field = model.sorting_field
41
- sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa: D5017
42
- sort_expr = ", ".join([
43
- f"c.{field}" if "." not in field else field
44
- for field in sorting_fields
45
- ])
46
-
47
- cte_header = "(id, parent_id, new_priority, new_path, new_depth)"
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
- new_path_expr = lpad_expr
55
- base_sql = f"""
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
- path_expr = SQLCompat.concat("p._path", "'.'", lpad_expr)
68
- base_sql = f"""
69
- SELECT
70
- c.id,
71
- c.parent_id,
72
- {row_number_expr} AS new_priority,
73
- {path_expr} AS new_path,
74
- p._depth + 1 AS new_depth
75
- FROM {qname} c
76
- JOIN {qname} p ON c.parent_id = p.id
77
- WHERE p.id = %s
78
- """
79
- params = [parent_id]
80
-
81
- recursive_row_number_expr = f"ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1"
82
- recursive_hex_expr = SQLCompat.to_hex(recursive_row_number_expr)
83
- recursive_lpad_expr = SQLCompat.lpad(
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
@@ -4,9 +4,9 @@ TreeNode Version Module
4
4
 
5
5
  This module defines the current version of the TreeNode package.
6
6
 
7
- Version: 3.0.8
7
+ Version: 3.2.1
8
8
  Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
- __version__ = '3.0.8'
12
+ __version__ = '3.2.1'
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 .autocomplete import TreeNodeAutocompleteView
18
- from .children import TreeChildrenView
19
- from .search import TreeSearchView
20
- from .crud import TreeNodeBaseAPIView
21
-
22
-
23
- class AutoTreeAPI:
24
- """Auto-discover and expose TreeNode-based APIs."""
25
-
26
- def __init__(self, base_view=TreeNodeBaseAPIView, base_url="api"):
27
- """Init auto-discover."""
28
- self.base_view = base_view
29
- self.base_url = base_url
30
-
31
- def protect_view(self, view, model):
32
- """
33
- Protect view.
34
-
35
- Protects view with login_required if needed, based on model attribute
36
- or global settings.
37
- """
38
- if getattr(model, 'api_login_required', None) is True:
39
- return login_required(view)
40
- if getattr(settings, 'TREENODE_API_LOGIN_REQUIRED', False):
41
- return login_required(view)
42
- return view
43
-
44
- def discover(self):
45
- """Scan models and generate API urls."""
46
- urls = [
47
- # Admin and Widget end-points
48
- path("widget/autocomplete/", TreeNodeAutocompleteView.as_view(), name="tree_autocomplete"), # noqa: D501
49
- path("widget/children/", TreeChildrenView.as_view(), name="tree_children"), # noqa: D501
50
- path("widget/search/", TreeSearchView.as_view(), name="tree_search"), # noqa: D501
51
- ]
52
- for model in apps.get_models():
53
- if issubclass(model, TreeNodeModel) and model is not TreeNodeModel:
54
- model_name = model._meta.model_name
55
-
56
- # Dynamically create an API view class for the model
57
- api_view_class = type(
58
- f"{model_name.capitalize()}APIView",
59
- (self.base_view,),
60
- {"model": model}
61
- )
62
-
63
- # List of API actions and their corresponding URL patterns
64
- action_patterns = [
65
- # List / Create
66
- ("", None, f"{model_name}-list"),
67
- # Retrieve / Update / Delete
68
- ("<int:pk>/", None, f"{model_name}-detail"),
69
- ("tree/", {'action': 'tree'}, f"{model_name}-tree"),
70
- # Direct children
71
- ("<int:pk>/children/", {'action': 'children'}, f"{model_name}-children"), # noqa: D501
72
- # All descendants
73
- ("<int:pk>/descendants/", {'action': 'descendants'}, f"{model_name}-descendants"), # noqa: D501
74
- # Ancestors + Self + Descendants
75
- ("<int:pk>/family/", {'action': 'family'}, f"{model_name}-family"), # noqa: D501
76
- ]
77
-
78
- # Create secured view instance once
79
- view = self.protect_view(api_view_class.as_view(), model)
80
-
81
- # Automatically build all paths for this model
82
- for url_suffix, extra_kwargs, route_name in action_patterns:
83
- urls.append(
84
- path(
85
- f"{self.base_url}/{model_name}/{url_suffix}",
86
- view,
87
- extra_kwargs or {},
88
- name=route_name
89
- )
90
- )
91
- return urls
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes