django-fast-treenode 3.2.0__tar.gz → 3.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {django_fast_treenode-3.2.0/django_fast_treenode.egg-info → django_fast_treenode-3.2.1}/PKG-INFO +1 -1
  2. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1/django_fast_treenode.egg-info}/PKG-INFO +1 -1
  3. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/django_fast_treenode.egg-info/SOURCES.txt +1 -0
  4. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/api.md +23 -0
  5. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/migration.md +1 -1
  6. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/pyproject.toml +1 -1
  7. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/setup.py +1 -1
  8. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/tests/test_suite.py +11 -0
  9. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/managers/tasks.py +19 -1
  10. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/__init__.py +3 -1
  11. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/children.py +1 -1
  12. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/descendants.py +1 -1
  13. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/roots.py +1 -1
  14. django_fast_treenode-3.2.1/treenode/models/mixins/search.py +55 -0
  15. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/siblings.py +1 -1
  16. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/models.py +17 -5
  17. django_fast_treenode-3.2.1/treenode/utils/db/compiler.py +72 -0
  18. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/version.py +2 -2
  19. django_fast_treenode-3.2.0/treenode/utils/db/compiler.py +0 -113
  20. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/LICENSE +0 -0
  21. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/MANIFEST.in +0 -0
  22. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/README.md +0 -0
  23. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/django_fast_treenode.egg-info/dependency_links.txt +0 -0
  24. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/django_fast_treenode.egg-info/requires.txt +0 -0
  25. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/django_fast_treenode.egg-info/top_level.txt +0 -0
  26. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/.gitignore +0 -0
  27. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/.nojekyll +0 -0
  28. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/about.md +0 -0
  29. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/admin.md +0 -0
  30. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/apifirst.md +0 -0
  31. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/cache.md +0 -0
  32. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/customization.md +0 -0
  33. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/dnd.md +0 -0
  34. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/import_export.md +0 -0
  35. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/index.md +0 -0
  36. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/insert-after.jpg +0 -0
  37. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/insert-as-child.jpg +0 -0
  38. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/installation.md +0 -0
  39. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/models.md +0 -0
  40. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/requirements.txt +0 -0
  41. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/roadmap.md +0 -0
  42. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/docs/using.md +0 -0
  43. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/setup.cfg +0 -0
  44. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/__init__.py +0 -0
  45. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/admin/__init__.py +0 -0
  46. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/admin/admin.py +0 -0
  47. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/admin/changelist.py +0 -0
  48. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/admin/exporter.py +0 -0
  49. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/admin/importer.py +0 -0
  50. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/admin/mixin.py +0 -0
  51. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/apps.py +0 -0
  52. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/cache.py +0 -0
  53. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/forms.py +0 -0
  54. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/managers/__init__.py +0 -0
  55. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/managers/managers.py +0 -0
  56. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/managers/queries.py +0 -0
  57. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/__init__.py +0 -0
  58. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/decorators.py +0 -0
  59. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/factory.py +0 -0
  60. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/ancestors.py +0 -0
  61. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/family.py +0 -0
  62. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/logical.py +0 -0
  63. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/node.py +0 -0
  64. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/properties.py +0 -0
  65. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/tree.py +0 -0
  66. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/models/mixins/update.py +0 -0
  67. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/settings.py +0 -0
  68. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/signals.py +0 -0
  69. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/.gitkeep +0 -0
  70. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/.gitkeep +0 -0
  71. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/css/.gitkeep +0 -0
  72. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/css/tree_widget.css +0 -0
  73. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/css/treenode_admin.css +0 -0
  74. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/css/treenode_tabs.css +0 -0
  75. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/js/.gitkeep +0 -0
  76. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/js/lz-string.min.js +0 -0
  77. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/js/tree_widget.js +0 -0
  78. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/js/treenode_admin.js +0 -0
  79. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/.gitkeep +0 -0
  80. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/.gitkeep +0 -0
  81. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/AUTHORS.txt +0 -0
  82. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/LICENSE.txt +0 -0
  83. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/external/jquery/jquery.js +0 -0
  84. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_444444_256x240.png +0 -0
  85. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_555555_256x240.png +0 -0
  86. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_777620_256x240.png +0 -0
  87. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_777777_256x240.png +0 -0
  88. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_cc0000_256x240.png +0 -0
  89. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/images/ui-icons_ffffff_256x240.png +0 -0
  90. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/index.html +0 -0
  91. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.css +0 -0
  92. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.js +0 -0
  93. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.min.css +0 -0
  94. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.min.js +0 -0
  95. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.structure.css +0 -0
  96. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.structure.min.css +0 -0
  97. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.theme.css +0 -0
  98. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/jquery-ui.theme.min.css +0 -0
  99. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/static/treenode/vendors/jquery-ui/package.json +0 -0
  100. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templates/.gitkeep +0 -0
  101. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templates/treenode/.gitkeep +0 -0
  102. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templates/treenode/admin/.gitkeep +0 -0
  103. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templates/treenode/admin/treenode_ajax_rows.html +0 -0
  104. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templates/treenode/admin/treenode_changelist.html +0 -0
  105. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templates/treenode/admin/treenode_import_export.html +0 -0
  106. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templates/treenode/admin/treenode_rows.html +0 -0
  107. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templates/treenode/widgets/tree_widget.html +0 -0
  108. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templatetags/__init__.py +0 -0
  109. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/templatetags/treenode_admin.py +0 -0
  110. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/tests.py +0 -0
  111. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/urls.py +0 -0
  112. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/utils/__init__.py +0 -0
  113. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/utils/db/__init__.py +0 -0
  114. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/utils/db/db_vendor.py +0 -0
  115. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/utils/db/service.py +0 -0
  116. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/utils/db/sqlcompat.py +0 -0
  117. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/utils/db/sqlquery.py +0 -0
  118. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/utils/jwt_auth.py +0 -0
  119. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/views/__init__.py +0 -0
  120. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/views/autoapi.py +0 -0
  121. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/views/autocomplete.py +0 -0
  122. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/views/children.py +0 -0
  123. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/views/common.py +0 -0
  124. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/views/crud.py +0 -0
  125. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/views/search.py +0 -0
  126. {django_fast_treenode-3.2.0 → django_fast_treenode-3.2.1}/treenode/widgets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-fast-treenode
3
- Version: 3.2.0
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-fast-treenode
3
- Version: 3.2.0
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
@@ -61,6 +61,7 @@ treenode/models/mixins/logical.py
61
61
  treenode/models/mixins/node.py
62
62
  treenode/models/mixins/properties.py
63
63
  treenode/models/mixins/roots.py
64
+ treenode/models/mixins/search.py
64
65
  treenode/models/mixins/siblings.py
65
66
  treenode/models/mixins/tree.py
66
67
  treenode/models/mixins/update.py
@@ -11,6 +11,7 @@ The API is divided into several logical groups, each serving a specific purpose:
11
11
  - **[Descendant Methods](#descendant-methods)** – Work with entire subtrees of nodes.
12
12
  - **[Family Methods](#family-methods)** – Retrieve and analyze relationships within a node's family (ancestors, siblings, descendants).
13
13
  - **[Node Utility Methods](#node-utility-methods)** – Additional methods for retrieving node order, paths, levels, and priorities.
14
+ - **[Search Methods](#search-methods)** – Quick node lookup utilities.
14
15
  - **[Root Node Methods](#root-node-methods)** – Manage and retrieve root nodes of trees.
15
16
  - **[Sibling Methods](#sibling-methods)** – Handle relationships between sibling nodes.
16
17
  - **[Tree Methods](#tree-methods)** – Serialize and manipulate the entire tree structure, including JSON export/import.
@@ -351,6 +352,28 @@ obj.get_root_pk()
351
352
 
352
353
  ---
353
354
 
355
+ ### Search Methods
356
+ These methods provide convenient helpers for finding nodes by
357
+ breadcrumb paths or within a subtree.
358
+
359
+ #### find_by_path
360
+ Find a node by a path previously generated via `get_breadcrumbs`.
361
+ ```python
362
+ cls.find_by_path("root/A/D", attr="name", delimiter="/")
363
+ ```
364
+
365
+ Returns the matching node or `None`.
366
+
367
+ #### find_in_subtree
368
+ Search for a node among the descendants of a parent node.
369
+ ```python
370
+ cls.find_in_subtree(parent, "D", attr="name")
371
+ ```
372
+
373
+ Returns the first found node or `None`.
374
+
375
+ ---
376
+
354
377
  ### Root Node Methods
355
378
  These methods allow managing **root nodes** efficiently. They provide retrieval, counting, and manipulation of the first and last root nodes in the tree.
356
379
 
@@ -77,7 +77,7 @@ python manage.py shell
77
77
  Then execute:
78
78
 
79
79
  ```python
80
- YourTreeNodeModel.rebuild()
80
+ YourTreeNodeModel.update_tree()
81
81
  ```
82
82
 
83
83
  !!! note
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "django-fast-treenode"
7
- version = "3.2.0"
7
+ version = "3.2.1"
8
8
  description = "Treenode Framework for supporting tree (hierarchical) data structure in Django projects"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Timur Kady", email = "timurkady@yandex.com" }]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='django-fast-treenode',
5
- version='3.2.0',
5
+ version='3.2.1',
6
6
  description='Treenode Framework for supporting tree (hierarchical) data structure in Django projects',
7
7
  long_description=open('README.md', encoding='utf-8').read(),
8
8
  long_description_content_type='text/markdown',
@@ -93,3 +93,14 @@ class TreeNodeModelTests(TestCase):
93
93
  qs = TestModel.objects.filter(pk__in=[self.a.pk, self.c.pk]).all()
94
94
  self.assertFalse(TestModel.objects.filter(pk=self.a.pk).exists())
95
95
  self.assertTrue(TestModel.objects.filter(pk=self.c.pk).exists())
96
+
97
+ # --- 6. Search ----------------------------------------------------------
98
+
99
+ def test_find_by_path(self):
100
+ path = "/".join(self.d.get_breadcrumbs(attr="name"))
101
+ node = TestModel.find_by_path(path, attr="name", delimiter="/")
102
+ self.assertEqual(node, self.d)
103
+
104
+ def test_find_in_subtree(self):
105
+ node = TestModel.find_in_subtree(self.a, "D", attr="name")
106
+ self.assertEqual(node, self.d)
@@ -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
@@ -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
@@ -45,8 +45,9 @@ class TreeNodeModel(
45
45
  mx.TreeNodeAncestorsMixin, mx.TreeNodeChildrenMixin,
46
46
  mx.TreeNodeFamilyMixin, mx.TreeNodeDescendantsMixin,
47
47
  mx.TreeNodeLogicalMixin, mx.TreeNodeNodeMixin,
48
- mx.TreeNodePropertiesMixin, mx.TreeNodeRootsMixin,
49
- mx.TreeNodeSiblingsMixin, mx.TreeNodeTreeMixin, mx.RawSQLMixin,
48
+ mx.TreeNodeSearchMixin, mx.TreeNodePropertiesMixin,
49
+ mx.TreeNodeRootsMixin, mx.TreeNodeSiblingsMixin,
50
+ mx.TreeNodeTreeMixin, mx.RawSQLMixin,
50
51
  models.Model, metaclass=TreeNodeModelBase):
51
52
  """
52
53
  Abstract tree node model.
@@ -186,6 +187,7 @@ class TreeNodeModel(
186
187
  is_new = False
187
188
  is_shift = False
188
189
  is_move = False
190
+ sorting_changed = False
189
191
 
190
192
  if self.pk:
191
193
  state = self.get_db_state()
@@ -194,6 +196,14 @@ class TreeNodeModel(
194
196
  is_move = self.parent_id != state["parent_id"]
195
197
  if is_move:
196
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
+ )
197
207
  else:
198
208
  logger.error(
199
209
  "TreeNodeModel save error: object with pk %s not found in DB",
@@ -231,12 +241,14 @@ class TreeNodeModel(
231
241
  pass
232
242
  """
233
243
 
234
- if is_new or is_move or is_shift:
244
+ if is_new or is_move or is_shift or sorting_changed:
235
245
  # Step 1: Shift siblings
236
246
  if (is_new or is_move) and (self.priority is not None):
237
247
  self._shift_siblings_forward()
238
248
  # Step 2: Update paths for the new parent -> sqlq
239
- 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)
240
252
  # Step 3: Clear model cache
241
253
  self.clear_cache()
242
254
 
@@ -245,7 +257,7 @@ class TreeNodeModel(
245
257
  disable_signals(post_save, model)):
246
258
  super().save(*args, **kwargs)
247
259
 
248
- if is_new or is_move or is_shift:
260
+ if is_new or is_move or is_shift or sorting_changed:
249
261
  # Run sql
250
262
  # self.sqlq.flush()
251
263
  setattr(model, 'is_dry', True)
@@ -0,0 +1,72 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Tree update task compiler class.
4
+
5
+ Compiles tasks to low-level SQL to update the materialized path (_path), depth
6
+ (_depth), and node order (priority) when they are shifted or moved.
7
+
8
+ Version: 3.1.0
9
+ Author: Timur Kady
10
+ Email: timurkady@yandex.com
11
+ """
12
+
13
+ from django.db import connection
14
+
15
+ from ...settings import SEGMENT_LENGTH
16
+ from .sqlcompat import SQLCompat
17
+
18
+
19
+ class TreePathCompiler:
20
+ """
21
+ Tree Task compiler class.
22
+
23
+ Efficient, ORM-free computation of _path, _depth and priority
24
+ for tree structures based on Materialized Path.
25
+ """
26
+
27
+ @classmethod
28
+ def update_path(cls, model, parent_id=None):
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 = []
48
+
49
+ if parent_id is None:
50
+ for idx, node_id in enumerate(fetch_children(None)):
51
+ queue.append((node_id, "", 0, idx))
52
+ else:
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))
70
+
71
+
72
+ # The End
@@ -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.2.0
7
+ Version: 3.2.1
8
8
  Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
- __version__ = '3.2.0'
12
+ __version__ = '3.2.1'
@@ -1,113 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- Tree update task compiler class.
4
-
5
- Compiles tasks to low-level SQL to update the materialized path (_path), depth
6
- (_depth), and node order (priority) when they are shifted or moved.
7
-
8
- Version: 3.1.0
9
- Author: Timur Kady
10
- Email: timurkady@yandex.com
11
- """
12
-
13
- from django.db import connection
14
-
15
- from ...settings import SEGMENT_LENGTH
16
- from .sqlcompat import SQLCompat
17
-
18
-
19
- class TreePathCompiler:
20
- """
21
- Tree Task compiler class.
22
-
23
- Efficient, ORM-free computation of _path, _depth and priority
24
- for tree structures based on Materialized Path.
25
- """
26
-
27
- @classmethod
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'")
52
-
53
- 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 = []
66
- 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)
111
-
112
-
113
- # The End