django-fast-treenode 3.0.7__py3-none-any.whl → 3.2.0__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 (56) hide show
  1. {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/METADATA +3 -1
  2. django_fast_treenode-3.2.0.dist-info/RECORD +97 -0
  3. treenode/admin/admin.py +109 -131
  4. treenode/admin/mixin.py +6 -6
  5. treenode/managers/managers.py +1 -1
  6. treenode/managers/queries.py +1 -1
  7. treenode/managers/tasks.py +1 -1
  8. treenode/models/mixins/node.py +4 -4
  9. treenode/models/models.py +14 -9
  10. treenode/settings.py +4 -1
  11. treenode/static/treenode/.gitkeep +0 -0
  12. treenode/static/{css → treenode/css}/treenode_admin.css +21 -0
  13. treenode/static/treenode/js/treenode_admin.js +322 -0
  14. treenode/static/treenode/vendors/.gitkeep +0 -0
  15. treenode/static/treenode/vendors/jquery-ui/.gitkeep +0 -0
  16. treenode/templates/treenode/admin/treenode_changelist.html +61 -15
  17. treenode/templates/treenode/admin/treenode_import_export.html +3 -2
  18. treenode/templates/treenode/admin/treenode_rows.html +37 -40
  19. treenode/templatetags/treenode_admin.py +57 -14
  20. treenode/utils/jwt_auth.py +25 -0
  21. treenode/version.py +2 -2
  22. treenode/views/autoapi.py +5 -1
  23. treenode/views/autocomplete.py +52 -52
  24. treenode/views/children.py +41 -41
  25. treenode/views/common.py +23 -23
  26. treenode/widgets.py +2 -2
  27. django_fast_treenode-3.0.7.dist-info/RECORD +0 -93
  28. treenode/static/js/treenode_admin.js +0 -531
  29. {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/WHEEL +0 -0
  30. {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/licenses/LICENSE +0 -0
  31. {django_fast_treenode-3.0.7.dist-info → django_fast_treenode-3.2.0.dist-info}/top_level.txt +0 -0
  32. /treenode/static/{css → treenode/css}/.gitkeep +0 -0
  33. /treenode/static/{css → treenode/css}/tree_widget.css +0 -0
  34. /treenode/static/{css → treenode/css}/treenode_tabs.css +0 -0
  35. /treenode/static/{js → treenode/js}/.gitkeep +0 -0
  36. /treenode/static/{js → treenode/js}/lz-string.min.js +0 -0
  37. /treenode/static/{js → treenode/js}/tree_widget.js +0 -0
  38. /treenode/static/{vendors → treenode/vendors}/jquery-ui/AUTHORS.txt +0 -0
  39. /treenode/static/{vendors → treenode/vendors}/jquery-ui/LICENSE.txt +0 -0
  40. /treenode/static/{vendors → treenode/vendors}/jquery-ui/external/jquery/jquery.js +0 -0
  41. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  42. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  43. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  44. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  45. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  46. /treenode/static/{vendors → treenode/vendors}/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  47. /treenode/static/{vendors → treenode/vendors}/jquery-ui/index.html +0 -0
  48. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.css +0 -0
  49. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.js +0 -0
  50. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.css +0 -0
  51. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.min.js +0 -0
  52. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.css +0 -0
  53. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.structure.min.css +0 -0
  54. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.css +0 -0
  55. /treenode/static/{vendors → treenode/vendors}/jquery-ui/jquery-ui.theme.min.css +0 -0
  56. /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.7
3
+ Version: 3.2.0
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,97 @@
1
+ django_fast_treenode-3.2.0.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=7TedO4TEmiuyAVChT2JQ953v0toWdhTtfA4d5VIsuCE,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=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=93AxBPQseV3TaKhYoX173HMgV1wIFDjwGg9YVBvtPNQ,12335
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=D3lwWsN2eRJeS99O7CaVAhabTYWA_NSY8z60zAhFAs0,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/treenode/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
+ treenode/static/treenode/css/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
41
+ treenode/static/treenode/css/tree_widget.css,sha256=YVIHuZT_E5ck5N7wvYvuwgV_m-4Ta_uGFiNUoTw-zbk,4861
42
+ treenode/static/treenode/css/treenode_admin.css,sha256=w7yyyf4aFsJKlr_YJPVq6ap5ZvA5Co7ZCYq6ipuV958,3291
43
+ treenode/static/treenode/css/treenode_tabs.css,sha256=pbulaUEhNMQFNWXFQrRN1zMyKjoSqLAnjNygpouPUfE,879
44
+ treenode/static/treenode/js/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
45
+ treenode/static/treenode/js/lz-string.min.js,sha256=TAnTJQd2AlLqT9M2TU7GFjnoj9SIfwLeZnpEtLkP624,4718
46
+ treenode/static/treenode/js/tree_widget.js,sha256=2LU8IIJD5TXbWVGjCEgrj5aYnUYtwBZECzj25XLtxNc,9633
47
+ treenode/static/treenode/js/treenode_admin.js,sha256=IU3Zj2Q-WBxftl0zsjeRPrt1xOzzdEKlzGEMThi3hrk,9706
48
+ treenode/static/treenode/vendors/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ treenode/static/treenode/vendors/jquery-ui/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
+ treenode/static/treenode/vendors/jquery-ui/AUTHORS.txt,sha256=geZ7JfVsTJq8Wgjrcm1p56Zqjxx7PL9FkxhKiN8dQg8,14848
51
+ treenode/static/treenode/vendors/jquery-ui/LICENSE.txt,sha256=9kkCXPZcjc2FiLCTeIO6elK9ttNRKUZKNqFgLrACUPE,1818
52
+ treenode/static/treenode/vendors/jquery-ui/index.html,sha256=RSv8cA662qJdc02OX-UmpaBJoZrngoQqnNWVZNn9GQQ,24229
53
+ treenode/static/treenode/vendors/jquery-ui/jquery-ui.css,sha256=O8mmheetXPeKUa-iDk8fdxDhs096eWIi10qLJb6OXyw,17171
54
+ treenode/static/treenode/vendors/jquery-ui/jquery-ui.js,sha256=XtnlPiKuVzMh-cef76mBCNCTPqk2cIlKBgmTHfRBDog,146430
55
+ treenode/static/treenode/vendors/jquery-ui/jquery-ui.min.css,sha256=Yv2MureLOGZicbgEv28X9ft9o0SRBZ84PWIgg6h_V2E,15003
56
+ treenode/static/treenode/vendors/jquery-ui/jquery-ui.min.js,sha256=FT6F7DDm5J1Qz6B5fv3e8jnmx9QyyfR2qUeTptb73bA,70364
57
+ treenode/static/treenode/vendors/jquery-ui/jquery-ui.structure.css,sha256=392jP1trXFqS1OVv2X50px0F8w1DeTyIvvHL9wxNXi0,328
58
+ treenode/static/treenode/vendors/jquery-ui/jquery-ui.structure.min.css,sha256=cXLe7UDVCN5SvPpbb3HJdndQIk1hlWMrzyRRYO9rcLs,208
59
+ treenode/static/treenode/vendors/jquery-ui/jquery-ui.theme.css,sha256=grsB4hVvwiBxnNsG1c_ocH1YK76CExOav_dn_BsxepU,17140
60
+ treenode/static/treenode/vendors/jquery-ui/jquery-ui.theme.min.css,sha256=oW6e3DmGiKSNLzo3HCUnjes9ykGZ6gXEtl3LjiQIkMs,13708
61
+ treenode/static/treenode/vendors/jquery-ui/package.json,sha256=AvCATruWzbwY7NsfzcUR2WqPjslGO8M7hHzTj1AYDQY,2122
62
+ treenode/static/treenode/vendors/jquery-ui/external/jquery/jquery.js,sha256=eKhayi8LEQwp4NKxN-CfCh-3qOVUtJn3QNZ0TciWLP4,285314
63
+ treenode/static/treenode/vendors/jquery-ui/images/ui-icons_444444_256x240.png,sha256=YW09ms2dyfoVEOT_NEpaWXMMRLJxVIqu8hku63SoCEA,7107
64
+ treenode/static/treenode/vendors/jquery-ui/images/ui-icons_555555_256x240.png,sha256=HP6KoU5aOllbVtaudhmNOLhBIzi_0oCuxBjwyhDATOo,7105
65
+ treenode/static/treenode/vendors/jquery-ui/images/ui-icons_777620_256x240.png,sha256=NxKGIMNmb6YYZor0RuijQ7PTK5BoA9b5qXLjYTMFvXo,4615
66
+ treenode/static/treenode/vendors/jquery-ui/images/ui-icons_777777_256x240.png,sha256=TLerPghEHzRXKPUuwr0VPT1PusnYFUe1-k1QIyeXe7g,7115
67
+ treenode/static/treenode/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png,sha256=S36ETnNcCxoWxmxAkVl0P3LYEDInjxi9GnG_g5uho1c,4615
68
+ treenode/static/treenode/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png,sha256=SBgnaQEkoyko7AbSFy0lDoDzAup6bkVQ74XyenFleUM,6395
69
+ treenode/templates/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
70
+ treenode/templates/treenode/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
71
+ treenode/templates/treenode/admin/.gitkeep,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
72
+ treenode/templates/treenode/admin/treenode_ajax_rows.html,sha256=zFyPaTbSyxRjOqQ85SMv__qTIYDjEna6chYODBypDZA,224
73
+ treenode/templates/treenode/admin/treenode_changelist.html,sha256=JiUwX33w5WwegX7gM2ctw-bx9XOyXwKt9QB_6g7Dfz8,2283
74
+ treenode/templates/treenode/admin/treenode_import_export.html,sha256=bgmue5y8SwzgbFQls8DjslkX1EZmrp5enUFHqTmhUfI,2977
75
+ treenode/templates/treenode/admin/treenode_rows.html,sha256=de90wKpNdVB2C5CGnIkStBB0dWCDkmWr_pluV_NgBUs,1857
76
+ treenode/templates/treenode/widgets/tree_widget.html,sha256=GKcCU-B2FkkJ2BSOuXOw9e_PdYTtADcvyITEXqOlZ9Y,723
77
+ treenode/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
+ treenode/templatetags/treenode_admin.py,sha256=5f5oqAS4zC_f0kkJRsm5MqXHjsKJXn8GslbZcbMivxg,2736
79
+ treenode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
+ treenode/utils/jwt_auth.py,sha256=LSTMuBFbH1DPJ0pVUnD_T1owb6GurpkJcCxpsXDL4HQ,845
81
+ treenode/utils/db/__init__.py,sha256=RwicAcJSI1nhIPWLdT7j9TFsgOc9834VDn9lVn54GlY,247
82
+ treenode/utils/db/compiler.py,sha256=PgD9ybS5H8OUHw1gkFBQHhnrf5HiCx8QXUMRhydwh7o,3824
83
+ treenode/utils/db/db_vendor.py,sha256=4SyEHl51jVCDB3is4omHCf2bTB_QV3RvemUQYxJP5m0,930
84
+ treenode/utils/db/service.py,sha256=PF85Yhz2xUWFFCzpLYotmiNTZXXEH61rhswslSxEUds,2640
85
+ treenode/utils/db/sqlcompat.py,sha256=K71ggkKIvpdTtHQ6Y4qcbo6cj2eYiEfy6DlVBr8Po1E,4460
86
+ treenode/utils/db/sqlquery.py,sha256=KXcfKbbaBF-D134H_2DiPQtjedR79SJNXPJc0msZYEc,1938
87
+ treenode/views/__init__.py,sha256=ppxbBx51TUaKstJFpAd_DTmbKjbZGmVMLNYSpgUKnd0,111
88
+ treenode/views/autoapi.py,sha256=X7r8hgzJ9LGO1k_keh9YfLk3dzVbjW-sVte5Ddp_HVE,3771
89
+ treenode/views/autocomplete.py,sha256=ERGJT4jfCE2GUv94Ja8SgJyG1FOAyqIeB0PKH1fyU7g,1417
90
+ treenode/views/children.py,sha256=seO0SNKriRY2FJOO1oZgX42iKolXMf3EF76GrfdJZLQ,1111
91
+ treenode/views/common.py,sha256=kUN3IgMZCdNdJ2haxB9MTGcn2rctyFUAHNagzcu9wXk,594
92
+ treenode/views/crud.py,sha256=RI5rdyD4hZTszjZFThByxi_lkAeJlqbDCXFkD8iyzKE,7424
93
+ treenode/views/search.py,sha256=c_GyooT3jyoNa96bBxfoWruRN1wIw-ZGYvwGKkGojTs,1501
94
+ django_fast_treenode-3.2.0.dist-info/METADATA,sha256=q3lF9MZCoD6ZJO9DuApkCEwIEMqPCr75zjvUWOpZakw,10377
95
+ django_fast_treenode-3.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
96
+ django_fast_treenode-3.2.0.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
97
+ django_fast_treenode-3.2.0.dist-info/RECORD,,
treenode/admin/admin.py CHANGED
@@ -2,7 +2,26 @@
2
2
  """
3
3
  TreeNode Admin Model Class
4
4
 
5
- Version: 3.0.7
5
+ Modified admin panel for django-fast-treenode. Solves the following problems:
6
+ - Set list_per_page = 10000 to display all elements at once.
7
+ - Hidden standard pagination via CSS
8
+ - Disabled counting the total number of elements to speed up loading
9
+ - Accordion works regardless of the display mode
10
+ Two modes are supported:
11
+ - Indented - with indents and icons
12
+ - Breadcrumbs - with breadcrumbs
13
+ All modes have links to editing objects.
14
+
15
+ - Expand buttons for nodes with children
16
+
17
+ Additional features:
18
+ - Control panel with "Expand All" / "Collapse All" buttons
19
+ - Saving the state of the tree between page transitions
20
+ - Smooth animations when expanding/collapsing
21
+ - Counting the total number of nodes in the tree
22
+ - Recursive hiding of grandchildren when collapsing the parent
23
+
24
+ Version: 3.1.0
6
25
  Author: Timur Kady
7
26
  Email: timurkady@yandex.com
8
27
  """
@@ -10,13 +29,11 @@ Email: timurkady@yandex.com
10
29
 
11
30
  from django.contrib import admin
12
31
  from django.db import models
13
- from django.http import HttpResponseRedirect
14
32
  from django.urls import reverse
15
33
  from django.utils.html import escape
16
34
  from django.utils.safestring import mark_safe
17
35
  from django.utils.translation import gettext_lazy as _
18
36
 
19
- from .changelist import TreeNodeChangeList
20
37
  from .mixin import AdminMixin
21
38
  from ..forms import TreeNodeForm
22
39
  from ..widgets import TreeWidget
@@ -34,14 +51,12 @@ class TreeNodeModelAdmin(AdminMixin, admin.ModelAdmin):
34
51
  # Режимы отображения
35
52
  TREENODE_DISPLAY_MODE_ACCORDION = 'accordion'
36
53
  TREENODE_DISPLAY_MODE_BREADCRUMBS = 'breadcrumbs'
37
- TREENODE_DISPLAY_MODE_INDENTATION = 'indentation'
38
54
  treenode_display_mode = TREENODE_DISPLAY_MODE_ACCORDION
39
55
 
40
56
  form = TreeNodeForm
41
57
  importer_class = None
42
58
  exporter_class = None
43
59
  ordering = []
44
- list_per_page = 1000
45
60
 
46
61
  formfield_overrides = {
47
62
  models.ForeignKey: {'widget': TreeWidget()},
@@ -54,13 +69,13 @@ class TreeNodeModelAdmin(AdminMixin, admin.ModelAdmin):
54
69
  """Meta Class."""
55
70
 
56
71
  css = {"all": (
57
- "css/treenode_admin.css",
58
- "vendors/jquery-ui/jquery-ui.css",
72
+ "treenode/css/treenode_admin.css",
73
+ "treenode/vendors/jquery-ui/jquery-ui.css",
59
74
  )}
60
75
  js = (
61
- "vendors/jquery-ui/jquery-ui.js",
62
- # "js/lz-string.min.js",
63
- "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",
64
79
  )
65
80
 
66
81
  def __init__(self, model, admin_site):
@@ -89,96 +104,69 @@ class TreeNodeModelAdmin(AdminMixin, admin.ModelAdmin):
89
104
 
90
105
  toggle.short_description = _("Expand")
91
106
 
92
- def _get_treenode_field_display(self, request, obj):
93
- """Return HTML for the tree node in list view."""
94
- level = obj.get_depth()
95
- edit_url = reverse(
96
- f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
97
- args=[obj.pk]
98
- )
107
+ def get_changelist(self, request, **kwargs):
108
+ """Get changelist."""
109
+ ChangeList = super().get_changelist(request, **kwargs)
99
110
 
100
- if self.treenode_display_mode == self.TREENODE_DISPLAY_MODE_ACCORDION:
101
- icon = "📄 " if obj.is_leaf() else "📁 "
102
- content = (
103
- f'<span style="padding-left: {level * 1.5}em;">'
104
- f'{icon}<a href="{edit_url}">{str(obj)}</a></span>'
105
- )
106
- elif self.treenode_display_mode == self.TREENODE_DISPLAY_MODE_BREADCRUMBS:
107
- breadcrumbs = obj.get_ancestors(include_self=True)
108
- model_label = obj._meta.app_label
109
- model_name = obj._meta.model_name
110
- href = f"admin:{model_label}_{model_name}_change"
111
-
112
- display_attr = getattr(obj, 'display_field', 'id')
113
-
114
- records = [
115
- (
116
- getattr(item, display_attr, item.pk),
117
- reverse(href, args=[item.pk]),
118
- )
119
- for item in breadcrumbs
120
- ]
121
-
122
- content = " / ".join([
123
- f'<a href="{url}">{escape(label)}</a>'
124
- for label, url in records
125
- ])
126
-
127
- elif self.treenode_display_mode == self.TREENODE_DISPLAY_MODE_INDENTATION: # noqa
128
- indent = "&mdash;" * level
129
- content = f'{indent}<a href="{edit_url}">{str(obj)}</a>'
130
- else:
131
- content = f'<a href="{edit_url}">{str(obj)}</a>'
132
-
133
- html = (
134
- f'<div class="treenode-wrapper" '
135
- f'data-treenode-pk="{obj.pk}" '
136
- f'data-treenode-depth="{level}" '
137
- f'data-treenode-parent="{obj.parent_id or ""}">'
138
- f'<span class="treenode-content">{content}</span>'
139
- f'</div>'
140
- )
141
- return mark_safe(html)
111
+ class NoPaginationChangeList(ChangeList):
112
+ """Suppress pagination."""
142
113
 
143
- def get_list_display(self, request):
144
- """Generate list_display dynamically with tree-aware columns."""
145
- # Define callable that replaces display field with tree field
146
- def treenode_field(obj):
147
- return self._get_treenode_field_display(request, obj)
148
- treenode_field.short_description = self.model._meta.verbose_name
114
+ def get_results(self, request):
115
+ """Get result."""
116
+ super().get_results(request)
117
+ self.paginator.show_all = True
118
+ self.result_count = len(self.result_list)
119
+ self.full_result_count = len(self.result_list)
120
+ self.can_show_all = False
121
+ self.multi_page = False
122
+ self.actions = self.model_admin.get_actions(request)
149
123
 
150
- display_field = getattr(self.model, 'display_field', '__str__')
124
+ return NoPaginationChangeList
151
125
 
152
- user_list_display = list(super().get_list_display(request))
153
- # If the list is empty or only contains __str__, replace it entirely
154
- if not user_list_display or user_list_display == ['__str__']:
155
- user_list_display = [treenode_field]
156
- else:
157
- try:
158
- pos = user_list_display.index(display_field)
159
- user_list_display.pop(pos)
160
- user_list_display.insert(pos, treenode_field)
161
- except ValueError:
162
- user_list_display.insert(0, treenode_field)
126
+ def get_changelist_instance(self, request):
127
+ """
128
+ Get changelist instance.
163
129
 
164
- return (self.drag, self.toggle) + tuple(user_list_display)
130
+ Make sure our custom ChangeList is used without pagination.
131
+ """
132
+ ChangeList = self.get_changelist(request)
165
133
 
166
- def get_list_display_links(self, request, list_display):
167
- """Get display list links."""
168
- return ('treenode_field',)
134
+ return ChangeList(
135
+ request,
136
+ self.model,
137
+ self.get_list_display(request),
138
+ self.get_list_display_links(
139
+ request,
140
+ self.get_list_display(request)
141
+ ),
142
+ self.get_list_filter(request),
143
+ self.date_hierarchy,
144
+ self.search_fields,
145
+ self.list_select_related,
146
+ self.list_per_page,
147
+ self.list_max_show_all,
148
+ self.list_editable,
149
+ self,
150
+ sortable_by=self.get_sortable_by(request),
151
+ search_help_text=self.get_search_help_text(request),
152
+ )
169
153
 
170
154
  def get_queryset(self, request):
171
- """By default: only root nodes, unless searching or editing."""
155
+ """Get queryset."""
172
156
  qs = super().get_queryset(request)
157
+ return qs.select_related('parent')\
158
+ .prefetch_related('children')\
159
+ .order_by('_path')
173
160
 
174
- if request.GET.get("q"):
175
- return qs
161
+ def get_list_display(self, request):
162
+ """Get list_display."""
163
+ def treenode_field(obj):
164
+ return self._get_treenode_field_display(request, obj)
176
165
 
177
- resolved = request.resolver_match
178
- if resolved and resolved.url_name.endswith("_change"):
179
- return qs
166
+ description = str(self.model._meta.verbose_name)
167
+ treenode_field.short_description = description
180
168
 
181
- return qs.filter(parent__isnull=True)
169
+ return (self.drag, self.toggle, treenode_field)
182
170
 
183
171
  def get_form(self, request, obj=None, **kwargs):
184
172
  """Get Form method."""
@@ -189,56 +177,46 @@ class TreeNodeModelAdmin(AdminMixin, admin.ModelAdmin):
189
177
 
190
178
  def get_search_fields(self, request):
191
179
  """Get search fields."""
192
- return [getattr(self.model, 'treenode_display_field', 'id')]
193
-
194
- def get_changelist(self, request, **kwargs):
195
- """Get ChangeList Class."""
196
- return TreeNodeChangeList
197
-
198
- def get_ordering(self, request):
199
- """Get ordering."""
200
- return None
201
-
202
- def changelist_view(self, request, extra_context=None):
203
- """Changelist View."""
204
- extra_context = extra_context or {}
205
- # TODO
206
- extra_context['import_export_enabled'] = self.import_export
207
- extra_context['num_sorted_fields'] = len(self.get_ordering(request) or []) # noqa: D501
208
-
209
- response = super().changelist_view(request, extra_context=extra_context)
210
-
211
- # If response is a redirect, then there is no point in updating
212
- # ChangeList
213
- if isinstance(response, HttpResponseRedirect):
214
- return response
180
+ return [getattr(self.model, 'display_field', 'id') or 'id']
215
181
 
216
- ChangeListClass = self.get_changelist(request)
217
- cl = ChangeListClass(
218
- request,
219
- self.model,
220
- self.list_display,
221
- self.list_display_links,
222
- self.list_filter,
223
- self.date_hierarchy,
224
- self.search_fields,
225
- self.list_select_related,
226
- self.list_per_page,
227
- self.list_max_show_all,
228
- self.list_editable,
229
- self,
230
- self.get_sortable_by(request),
231
- self.get_search_help_text(request),
182
+ def _get_treenode_field_display(self, request, obj):
183
+ """
184
+ Generate HTML to display tree nodes.
185
+
186
+ Depending on the selected display mode (accordion or breadcrumbs),
187
+ do the following:
188
+ - For accordion mode: add indents and icons.
189
+ - For breadcrumbs mode: display breadcrumb path.
190
+ """
191
+ level = obj.get_depth()
192
+ display_field = getattr(obj, "display_field", None)
193
+ edit_url = reverse(
194
+ f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
195
+ args=[obj.pk]
232
196
  )
197
+ icon = ""
198
+ text = ""
199
+ padding = ""
200
+ closing = ""
201
+
202
+ if self.treenode_display_mode == self.TREENODE_DISPLAY_MODE_ACCORDION:
203
+ icon = "📄 " if obj.is_leaf() else "📁 "
204
+ text = getattr(obj, display_field, str(obj))
205
+ padding = f'<span style="padding-left: {level * 1.5}em;">'
206
+ closing = "</span>"
207
+ elif self.treenode_display_mode == self.TREENODE_DISPLAY_MODE_BREADCRUMBS: # noqa
208
+ if display_field:
209
+ breadcrumbs = obj.get_breadcrumbs(attr=display_field)
210
+ else:
211
+ breadcrumbs = [str(item) for item in obj.get_ancestors()]
233
212
 
234
- cl.get_results(request)
235
- cl.result_list = self.render_changelist_rows(cl.result_list, request)
213
+ text = "/" + "/".join([escape(label) for label in breadcrumbs])
236
214
 
237
- return response
215
+ content = f'{padding}{icon}<a href="{edit_url}">{escape(text)}</a>{closing}' # noqa
216
+ return mark_safe(content)
238
217
 
239
218
  def get_list_per_page(self, request):
240
219
  """Get list per page."""
241
220
  return 999999
242
221
 
243
-
244
222
  # The End
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)
@@ -5,7 +5,7 @@ Low-level SQL Query Manager.
5
5
  Encapsulates all logic to retrieve related primary keys based on relationships
6
6
  (e.g., ancestors, children, descendants, siblings, family, root) using raw SQL.
7
7
 
8
- Version: 3.0.7
8
+ Version: 3.0.0
9
9
  Author: Timur Kady
10
10
  Email: timurkady@yandex.com
11
11
  """
@@ -2,7 +2,7 @@
2
2
  """
3
3
  TreeNode TaskQuery manager
4
4
 
5
- Version: 3.0.4
5
+ Version: 3.0.1
6
6
  Author: Timur Kady
7
7
  Email: timurkady@yandex.com
8
8
  """
@@ -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)."""
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,6 +38,8 @@ 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,
@@ -156,10 +158,10 @@ class TreeNodeModel(
156
158
  # Update subtree
157
159
  self._update_path(self.parent_id)
158
160
  self.sqlq.flush()
159
- # Clead cache
161
+ # Clear cache
160
162
  self.clear_cache()
161
163
 
162
- # Saving and Udating methods ----------------------------------
164
+ # Saving and Updating methods ----------------------------------
163
165
 
164
166
  def save(self, *args, **kwargs):
165
167
  """
@@ -193,7 +195,10 @@ class TreeNodeModel(
193
195
  if is_move:
194
196
  self._meta.model.tasks.add("update", state["parent_id"])
195
197
  else:
196
- print("TreeNodeModel error: oject not found in DB! WTF, MF!")
198
+ logger.error(
199
+ "TreeNodeModel save error: object with pk %s not found in DB",
200
+ self.pk,
201
+ )
197
202
  else:
198
203
  is_new = True
199
204
 
@@ -335,11 +340,11 @@ class TreeNodeModel(
335
340
  queue.extend(model.objects.filter(parent=node))
336
341
 
337
342
  if verbose and errors:
338
- print("Tree integrity check failed:")
343
+ logger.error("Tree integrity check failed:")
339
344
  for err in errors:
340
- print(" -", err)
345
+ logger.error(" - %s", err)
341
346
  elif verbose:
342
- print("Tree integrity: OK ✅")
347
+ logger.info("Tree integrity: OK ✅")
343
348
 
344
349
  return errors
345
350
 
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
@@ -66,6 +66,27 @@ Email: timurkady@yandex.com
66
66
  opacity: 1.0;
67
67
  }
68
68
 
69
+ .treenode-toolbar{
70
+ display: flex;
71
+ }
72
+
73
+ .treenode-toolbar {
74
+ margin: 15px 0px;
75
+ }
76
+
77
+ .treenode-button {
78
+ padding: 5px !important;
79
+ margin-left: 15px !important;
80
+ }
81
+
82
+ tr.treenode-hidden {
83
+ display: none;
84
+ }
85
+
86
+ td.action-checkbox{
87
+ text-align: center;
88
+ }
89
+
69
90
  .dark-theme .treenode-toggle {
70
91
  color: #ccc;
71
92
  background-color: #444;