├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── cte_forest ├── __init__.py ├── fields.py ├── models.py └── query.py ├── cte_forest_test ├── __init__.py ├── manage.py ├── models.py ├── settings.py └── tests.py ├── docs ├── Makefile ├── advanced.rst ├── api.rst ├── basic.rst ├── conf.py ├── examples.rst ├── index.rst ├── make.bat └── technical.rst ├── requirements.docs.txt ├── requirements.txt ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | htmlcov 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | .idea 38 | 39 | # Stuff 40 | *.sw? 41 | venv 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: xenial 3 | language: python 4 | cache: pip 5 | python: 6 | - 3.8 7 | services: 8 | - postgresql 9 | jobs: 10 | fast_finish: true 11 | include: 12 | - python: 3.8 13 | env: REQ="https://github.com/django/django/archive/master.zip#egg=Django" 14 | - python: 3.8 15 | env: REQ="Django>=3.1,<3.2" 16 | - python: 3.7 17 | env: REQ="Django>=3.1,<3.2" 18 | - python: 3.6 19 | env: REQ="Django>=3.1,<3.2" 20 | - python: 3.7 21 | env: REQ="Django>=3.0,<3.1" 22 | - python: 3.6 23 | env: REQ="Django>=3.0,<3.1" 24 | - python: 3.7 25 | env: REQ="Django>=2.2,<3.0" 26 | - python: 3.6 27 | env: REQ="Django>=2.2,<3.0" 28 | - python: 3.5 29 | env: REQ="Django>=2.2,<3.0" 30 | - python: 3.7 31 | env: REQ="Django>=1.11,<2.0" 32 | - python: 3.6 33 | env: REQ="Django>=1.11,<2.0" 34 | - python: 3.5 35 | env: REQ="Django>=1.11,<2.0" 36 | - python: 2.7 37 | env: REQ="Django>=1.11,<2.0" 38 | - python: 3.8 39 | env: REQ="black flake8" 40 | install: skip 41 | script: 42 | - pip install black flake8 43 | - flake8 . 44 | - black --check cte_forest cte_forest_test 45 | allow_failures: 46 | - env: REQ="https://github.com/django/django/archive/master.zip#egg=Django" 47 | install: 48 | - pip install -U pip wheel 49 | - pip install $REQ psycopg2-binary 50 | - python setup.py install 51 | script: PYTHONWARNINGS=all PYTHONPATH=. cte_forest_test/manage.py test -v 2 52 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Change log 3 | ========== 4 | 5 | `Next version`_ 6 | ~~~~~~~~~~~~~~~ 7 | 8 | - Added Django@master to the build matrix and fixed current problems 9 | with Django 2.0. 10 | - Fixed all flake8_ warnings that are not explicitly ignored. 11 | - Added our own documentation_ build on readthedocs. 12 | - Removed documentation and tests from PyPI builds. 13 | 14 | 15 | `0.2`_ (2016-11-12) 16 | ~~~~~~~~~~~~~~~~~~~ 17 | 18 | - Fixed a few deprecation warnings and raised the minimum Django version 19 | to 1.10, thanks ``base_manager_name``. 20 | 21 | 22 | `0.1`_ (2016-09-03) 23 | ~~~~~~~~~~~~~~~~~~~ 24 | 25 | - Initial public release under the new name ``django-cte-forest``. 26 | - Added support for Django 1.8 and Python 3. 27 | - Fixed a few minor bugs related to hardcoded names for the virtual 28 | fields added by the CTE. 29 | - Added continuous integration on Travis CI. 30 | 31 | 32 | .. _0.1: https://github.com/matthiask/django-cte-forest/commit/6d89293b 33 | .. _0.2: https://github.com/matthiask/django-cte-forest/compare/0.1...0.2 34 | .. _Next version: https://github.com/matthiask/django-cte-forest/compare/0.2...master 35 | 36 | .. _flake8: https://flake8.readthedocs.io/ 37 | .. _documentation: https://django-cte-forest.readthedocs.io/ 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2001 - 2013 Alexis Petrounias < www.petrounias.org >, 2 | Copyright (c) 2016 Feinheit AG < www.feinheit.ch >, 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | Neither the name of the author nor the names of its contributors may be 17 | used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 21 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 22 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 23 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 26 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include README.rst 4 | recursive-include cte_forest/static * 5 | recursive-include cte_forest/locale * 6 | recursive-include cte_forest/templates * 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | If you want compatibility with other database engines (sqlite3, MariaDB, MySQL) 2 | and don't need all features this package offers take a look at 3 | `django-tree-queries `__ too! 4 | 5 | ================= 6 | django-cte-forest 7 | ================= 8 | 9 | .. image:: https://travis-ci.org/matthiask/django-cte-forest.svg?branch=master 10 | :target: https://travis-ci.org/matthiask/django-cte-forest 11 | 12 | django-cte-forest implements efficient adjacency list trees using Django 13 | and PostgreSQL Common Table Expressions (CTE). 14 | 15 | **Note!** This is a friendly fork of django-cte-trees_ by Alexis Petrounias 16 | which introduces support for Python 3 and recent Django versions. 17 | 18 | You'll find additional details and information in the documentation_ or 19 | on the original authors webpage on django-cte-trees_ 20 | 21 | .. _django-cte-trees: http://petrounias.org/software/django-cte-trees/ 22 | .. _documentation: https://django-cte-forest.readthedocs.io/ 23 | -------------------------------------------------------------------------------- /cte_forest/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Django CTE Trees - an experimental PostgreSQL Common Table Expressions (CTE) 35 | implementation of of Adjacency-Linked trees. 36 | """ 37 | 38 | VERSION = (0, 2, 2) 39 | __version__ = ".".join(map(str, VERSION)) 40 | -------------------------------------------------------------------------------- /cte_forest/fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Django CTE Trees Fields. 35 | """ 36 | 37 | 38 | from django.db.models import IntegerField, TextField 39 | 40 | 41 | class DepthField(IntegerField): 42 | def __init__(self): 43 | super(DepthField, self).__init__(null=True, blank=True, editable=False) 44 | 45 | 46 | class PathField(TextField): 47 | def __init__(self): 48 | super(PathField, self).__init__(null=True, blank=True, editable=False) 49 | 50 | 51 | class OrderingField(TextField): 52 | def __init__(self): 53 | super(OrderingField, self).__init__(null=True, blank=True, editable=False) 54 | -------------------------------------------------------------------------------- /cte_forest/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Django CTE Trees Models. 35 | """ 36 | from __future__ import unicode_literals 37 | 38 | from django.core.exceptions import FieldError, ImproperlyConfigured, ValidationError 39 | from django.db.models import CASCADE, ForeignKey, Manager, Model 40 | from django.core.exceptions import FieldDoesNotExist 41 | from django.utils.translation import gettext_lazy as _ 42 | 43 | from .query import CTEQuerySet 44 | 45 | 46 | class CTENodeManager(Manager): 47 | """ Custom :class:`Manager` which ensures all queries involving 48 | :class:`CTENode` objects are processed by the custom SQL compiler. 49 | Additionally, provides tree traversal queries for obtaining node 50 | children, siblings, ancestors, descendants, and roots. 51 | 52 | If your Model inherits from :class:`CTENode` and use your own custom 53 | :class:`Manager`, you must ensure the following three: 54 | 55 | 1) your :class:`Manager` inherits from :class:`CTENodeManager`, 56 | 57 | 2) if you override the :meth:`get_queryset` method in order to 58 | return a custom :class:`QuerySet`, then your `QuerySet` must also 59 | inherit from :class:`CTENodeManager.CTEQuerySet`, and 60 | 61 | 3) invoke the :meth:`_ensure_parameters` on your :class:`Manager` 62 | at least once before using a :class:`QuerySet` which inherits from 63 | :class:`CTENodeManager.CTEQuerySet`, unless you have supplied the 64 | necessary CTE node attributes on the :class:`CTENode` :class:`Model` in 65 | some other way. 66 | 67 | The methods :meth:`prepare_delete`, :meth:`prepare_delete_pharaoh`, 68 | :meth:`prepare_delete_grandmother`, and 69 | :meth:`prepare_delete_monarchy` can be directly used to prepare 70 | nodes for deletion with either the default or explicitly-specified 71 | deletion semantics. The :class:`CTENode` abstract :class:`Model` 72 | defines a :meth:`CTENode.delete` method which delegates preparation 73 | to this manager. 74 | 75 | """ 76 | 77 | # SQL CTE temporary table name. 78 | DEFAULT_TABLE_NAME = "cte" 79 | DEFAULT_CHILDREN_NAME = "children" 80 | 81 | # Tree traversal semantics. 82 | TREE_TRAVERSAL_NONE = "none" 83 | TREE_TRAVERSAL_DFS = "dfs" 84 | TREE_TRAVERSAL_BFS = "bfs" 85 | TREE_TRAVERSAL_METHODS = ( 86 | TREE_TRAVERSAL_NONE, 87 | TREE_TRAVERSAL_DFS, 88 | TREE_TRAVERSAL_BFS, 89 | ) 90 | TREE_TRAVERSAL_CHOICES = ( 91 | (TREE_TRAVERSAL_NONE, _("none")), 92 | (TREE_TRAVERSAL_DFS, _("depth first search")), 93 | (TREE_TRAVERSAL_BFS, _("breadth first search")), 94 | ) 95 | DEFAULT_TREE_TRAVERSAL = TREE_TRAVERSAL_DFS 96 | 97 | # Virtual fields. 98 | VIRTUAL_FIELD_DEPTH = "depth" 99 | VIRTUAL_FIELD_PATH = "path" 100 | VIRTUAL_FIELD_ORDERING = "ordering" 101 | 102 | # Deletion semantics. 103 | DELETE_METHOD_NONE = "none" 104 | DELETE_METHOD_PHARAOH = "pharaoh" 105 | DELETE_METHOD_GRANDMOTHER = "grandmother" 106 | DELETE_METHOD_MONARCHY = "monarchy" 107 | DELETE_METHODS = ( 108 | DELETE_METHOD_NONE, 109 | DELETE_METHOD_PHARAOH, 110 | DELETE_METHOD_GRANDMOTHER, 111 | DELETE_METHOD_MONARCHY, 112 | ) 113 | DELETE_METHOD_CHOICES = ( 114 | (DELETE_METHOD_NONE, _("none")), 115 | (DELETE_METHOD_PHARAOH, _("pharaoh (all subtree)")), 116 | (DELETE_METHOD_GRANDMOTHER, _("grandmother (move subtree up)")), 117 | (DELETE_METHOD_MONARCHY, _("monarchy (first child becomes subtree root)")), 118 | ) 119 | DEFAULT_DELETE_METHOD = DELETE_METHOD_PHARAOH 120 | 121 | # Related manager lookup should return this custom Manager in order to use 122 | # the custom QuerySet above. 123 | use_for_related_fields = True 124 | silence_use_for_related_fields_deprecation = True 125 | 126 | def _ensure_parameters(self): 127 | """ Attempts to load and verify the CTE node parameters. Will use 128 | default values for all missing parameters, and raise an exception if 129 | a parameter's value cannot be verified. This method will only 130 | perform these actions once, and set the :attr:`_parameters_checked` 131 | attribute to ``True`` upon its first success. 132 | """ 133 | 134 | if hasattr(self, "_parameters_checked"): 135 | return 136 | 137 | if ( 138 | not hasattr(self.model, "_cte_node_table") 139 | or self.model._cte_node_table is None 140 | ): 141 | setattr(self.model, "_cte_node_table", self.DEFAULT_TABLE_NAME) 142 | 143 | if ( 144 | not hasattr(self.model, "_cte_node_depth") 145 | or self.model._cte_node_depth is None 146 | ): 147 | setattr(self.model, "_cte_node_depth", self.VIRTUAL_FIELD_DEPTH) 148 | 149 | if ( 150 | not hasattr(self.model, "_cte_node_path") 151 | or self.model._cte_node_depth is None 152 | ): 153 | setattr(self.model, "_cte_node_path", self.VIRTUAL_FIELD_PATH) 154 | 155 | if ( 156 | not hasattr(self.model, "_cte_node_ordering") 157 | or self.model._cte_node_ordering is None 158 | ): 159 | setattr(self.model, "_cte_node_ordering", self.VIRTUAL_FIELD_ORDERING) 160 | 161 | if ( 162 | not hasattr(self.model, "_cte_node_traversal") 163 | or self.model._cte_node_traversal is None 164 | ): 165 | setattr(self.model, "_cte_node_traversal", self.DEFAULT_TREE_TRAVERSAL) 166 | 167 | if ( 168 | not hasattr(self.model, "_cte_node_children") 169 | or self.model._cte_node_children is None 170 | ): 171 | setattr(self.model, "_cte_node_children", self.DEFAULT_CHILDREN_NAME) 172 | 173 | if not hasattr(self.model, "_cte_node_primary_key_type"): 174 | setattr(self.model, "_cte_node_primary_key_type", None) 175 | 176 | # Determine the parent foreign key field name, either 177 | # explicitly specified, or the first foreign key to 'self'. 178 | # If we need to determine, then we set the attribute for future 179 | # reference. 180 | if ( 181 | not hasattr(self.model, "_cte_node_parent") 182 | or self.model._cte_node_parent is None 183 | ): 184 | found = False 185 | for f in self.model._meta.fields: 186 | if isinstance(f, ForeignKey): 187 | if f.remote_field.model == self.model: 188 | setattr(self.model, "_cte_node_parent", f.name) 189 | found = True 190 | if not found: 191 | raise ImproperlyConfigured( 192 | _( 193 | "CTENode must have a Foreign Key to self for the parent " 194 | "relation." 195 | ) 196 | ) 197 | 198 | try: 199 | parent_field = self.model._meta.get_field(self.model._cte_node_parent) 200 | except FieldDoesNotExist: 201 | raise ImproperlyConfigured( 202 | "".join( 203 | [ 204 | _( 205 | "CTENode._cte_node_parent must specify a Foreign Key" 206 | " to self, instead it is: " 207 | ), 208 | self.model._cte_node_parent, 209 | ] 210 | ) 211 | ) 212 | 213 | # Ensure parent relation is a Foreign Key to self. 214 | if not parent_field.remote_field.model == self.model: 215 | raise ImproperlyConfigured( 216 | "".join( 217 | [ 218 | _( 219 | "CTENode._cte_node_parent must specify a Foreign Key" 220 | " to self, instead it is: " 221 | ), 222 | self.model._cte_node_parent, 223 | ] 224 | ) 225 | ) 226 | 227 | # Record the parent field attribute name for future reference. 228 | setattr( 229 | self.model, 230 | "_cte_node_parent_attname", 231 | self.model._meta.get_field(self.model._cte_node_parent).attname, 232 | ) 233 | 234 | # Ensure traversal choice is valid. 235 | traversal_choices = [choice[0] for choice in self.TREE_TRAVERSAL_CHOICES] 236 | if self.model._cte_node_traversal not in traversal_choices: 237 | raise ImproperlyConfigured( 238 | " ".join( 239 | [ 240 | "CTENode._cte_node_traversal must be one of [", 241 | ", ".join(traversal_choices), 242 | "]; instead it is:", 243 | self.model._cte_node_traversal, 244 | ] 245 | ) 246 | ) 247 | 248 | # Ensure delete choice is valid. 249 | if ( 250 | not hasattr(self.model, "_cte_node_delete_method") 251 | or self.model._cte_node_delete_method is None 252 | ): 253 | setattr(self.model, "_cte_node_delete_method", self.DEFAULT_DELETE_METHOD) 254 | else: 255 | # Ensure specified method is valid. 256 | method_choices = [dm[0] for dm in self.DELETE_METHOD_CHOICES] 257 | if self.model._cte_node_delete_method not in method_choices: 258 | raise ImproperlyConfigured( 259 | " ".join( 260 | [ 261 | "delete method must be one of [", 262 | ", ".join(method_choices), 263 | "]; instead it is:", 264 | self.model._cte_node_delete_method, 265 | ] 266 | ) 267 | ) 268 | 269 | setattr(self, "_parameters_checked", True) 270 | 271 | def _ensure_virtual_fields(self, node): 272 | """ Attempts to read the virtual fields from the given `node` in order 273 | to ensure they exist, resulting in an early :class:`AttributeError` 274 | exception in case of missing virtual fields. This method requires 275 | several parameters, and thus invoked :meth:`_ensure_parameters` 276 | first, possibly resulting in an :class:`ImproperlyConfigured` 277 | exception being raised. 278 | 279 | :param node: the :class:`CTENode` for which to verify that the 280 | virtual fields are present. 281 | """ 282 | # Uses several _cte_node_* parameters, so ensure they exist. 283 | self._ensure_parameters() 284 | for vf in [ 285 | self.model._cte_node_depth, 286 | self.model._cte_node_path, 287 | self.model._cte_node_ordering, 288 | ]: 289 | if not hasattr(node, vf): 290 | raise FieldError( 291 | _( 292 | "CTENode objects must be loaded from the database before " 293 | "they can be used." 294 | ) 295 | ) 296 | 297 | def get_queryset(self): 298 | """ Returns a custom :class:`QuerySet` which provides the CTE 299 | functionality for all queries concerning :class:`CTENode` objects. 300 | This method overrides the default :meth:`get_queryset` method of 301 | the :class:`Manager` class. 302 | 303 | :returns: a custom :class:`QuerySet` which provides the CTE 304 | functionality for all queries concerning :class:`CTENode` 305 | objects. 306 | """ 307 | # The CTEQuerySet uses _cte_node_* attributes from the Model, so ensure 308 | # they exist. 309 | self._ensure_parameters() 310 | return CTEQuerySet(self.model, using=self._db) 311 | 312 | def roots(self): 313 | """ Returns a :class:`QuerySet` of all root :class:`CTENode` objects. 314 | 315 | :returns: a :class:`QuerySet` of all root :class:`CTENode` objects. 316 | """ 317 | # We need to read the _cte_node_parent attribute, so ensure it exists. 318 | self._ensure_parameters() 319 | # We need to construct: self.filter(parent = None) 320 | return self.filter(**{self.model._cte_node_parent: None}) 321 | 322 | def leaves(self): 323 | """ Returns a :class:`QuerySet` of all leaf nodes (nodes with no 324 | children). 325 | 326 | :return: A :class:`QuerySet` of all leaf nodes (nodes with no 327 | children). 328 | """ 329 | # We need to read the _cte_node_children attribute, so ensure it exists. 330 | self._ensure_parameters() 331 | return self.exclude( 332 | **{"%s__id__in" % self.model._cte_node_children: self.all()} 333 | ) 334 | 335 | def branches(self): 336 | """ Returns a :class:`QuerySet` of all branch nodes (nodes with at least 337 | one child). 338 | 339 | :return: A :class:`QuerySet` of all leaf nodes (nodes with at least 340 | one child). 341 | """ 342 | # We need to read the _cte_node_children attribute, so ensure it exists. 343 | self._ensure_parameters() 344 | return self.filter( 345 | **{"%s__id__in" % self.model._cte_node_children: self.all()} 346 | ).distinct() 347 | 348 | def root(self, node): 349 | """ Returns the :class:`CTENode` which is the root of the tree in which 350 | the given `node` participates (or `node` if it is a root node). This 351 | method functions through the :meth:`get` method. 352 | 353 | :param node: A :class:`CTENode` whose root is required. 354 | 355 | :return: A :class:`CTENode` which is the root of the tree in which 356 | the given `node` participates (or the given `node` if it is a 357 | root node). 358 | """ 359 | # We need to use the path virtual field, so ensure it exists. 360 | self._ensure_virtual_fields(node) 361 | return self.get(pk=getattr(node, self.model._cte_node_path)[0]) 362 | 363 | def siblings(self, node): 364 | """ Returns a :class:`QuerySet` of all siblings of a given 365 | :class:`CTENode` `node`. 366 | 367 | :param node: a :class:`CTENode` whose siblings are required. 368 | 369 | :returns: A :class:`QuerySet` of all siblings of the given `node`. 370 | """ 371 | # We need to read the _cte_node_parent* attributes, so ensure they 372 | # exist. 373 | self._ensure_parameters() 374 | # We need to construct: filter(parent = node.parent_id) 375 | return self.filter( 376 | **{ 377 | self.model._cte_node_parent: getattr( 378 | node, self.model._cte_node_parent_attname 379 | ) 380 | } 381 | ).exclude(id=node.id) 382 | 383 | def ancestors(self, node): 384 | """ Returns a :class:`QuerySet` of all ancestors of a given 385 | :class:`CTENode` `node`. 386 | 387 | :param node: A :class:`CTENode` whose ancestors are required. 388 | 389 | :returns: A :class:`QuerySet` of all ancestors of the given `node`. 390 | """ 391 | # We need to use the path virtual field, so ensure it exists. 392 | self._ensure_virtual_fields(node) 393 | return self.filter(pk__in=getattr(node, self.model._cte_node_path)[:-1]) 394 | 395 | def descendants(self, node): 396 | """ Returns a :class:`QuerySet` with all descendants for a given 397 | :class:`CTENode` `node`. 398 | 399 | :param node: the :class:`CTENode` whose descendants are required. 400 | 401 | :returns: A :class:`QuerySet` with all descendants of the given 402 | `node`. 403 | """ 404 | # We need to read the _cte_node_* attributes, so ensure they exist. 405 | self._ensure_parameters() 406 | # This is implemented in the CTE WHERE logic, so we pass a reference to 407 | # the offset CTENode to the custom QuerySet, which will process it. 408 | # Because the compiler will include the node in question in the offset, 409 | # we must exclude it here. 410 | return CTEQuerySet(self.model, using=self._db, offset=node).exclude(pk=node.pk) 411 | 412 | def is_parent_of(self, node, subject): 413 | """ Returns ``True`` if the given `node` is the parent of the given 414 | `subject` node, ``False`` otherwise. This method uses the 415 | :attr:`parent` field, and so does not perform any query. 416 | 417 | :param node: the :class:`CTENode' for which to determine whether it 418 | is a parent of the `subject`. 419 | 420 | :param subject: the :class:`CTENode` for which to determine whether 421 | its parent is the `node`. 422 | 423 | :returns: ``True`` if `node` is the parent of `subject`, ``False`` 424 | otherwise. 425 | """ 426 | return subject.parent_id == node.id 427 | 428 | def is_child_of(self, node, subject): 429 | """ Returns ``True`` if the given `node` is a child of the given 430 | `subject` node, ``False`` otherwise. This method used the 431 | :attr:`parent` field, and so does not perform any query. 432 | 433 | :param node: the :class:`CTENode' for which to determine whether it 434 | is a child of the `subject`. 435 | 436 | :param subject: the :class:`CTENode` for which to determine whether 437 | one of its children is the `node`. 438 | 439 | :returns: ``True`` if `node` is a child of `subject`, ``False`` 440 | otherwise. 441 | """ 442 | return node.parent_id == subject.id 443 | 444 | def is_sibling_of(self, node, subject): 445 | """ Returns ``True`` if the given `node` is a sibling of the given 446 | `subject` node, ``False`` otherwise. This method uses the 447 | :attr:`parent` field, and so does not perform any query. 448 | 449 | :param node: the :class:`CTENode' for which to determine whether it 450 | is a sibling of the `subject`. 451 | 452 | :param subject: the :class:`CTENode` for which to determine whether 453 | one of its siblings is the `node`. 454 | 455 | :returns: ``True`` if `node` is a sibling of `subject`, ``False`` 456 | otherwise. 457 | """ 458 | # Ensure nodes are not siblings of themselves. 459 | return not node.id == subject.id and node.parent_id == subject.parent_id 460 | 461 | def is_ancestor_of(self, node, subject): 462 | """ Returns ``True`` if the given `node` is an ancestor of the given 463 | `subject` node, ``False`` otherwise. This method uses the 464 | :attr:`path` virtual field, and so does not perform any query. 465 | 466 | :param node: the :class:`CTENode' for which to determine whether it 467 | is an ancestor of the `subject`. 468 | 469 | :param subject: the :class:`CTENode` for which to determine whether 470 | one of its ancestors is the `node`. 471 | 472 | :returns: ``True`` if `node` is an ancestor of `subject`, ``False`` 473 | otherwise. 474 | """ 475 | # We will need to use the path virtual field, so ensure it exists. 476 | self._ensure_virtual_fields(node) 477 | 478 | # Convenience check so is_ancestor_of can be combined with methods 479 | # returning nodes without the caller having to worry about a None 480 | # subject. 481 | if subject is None: 482 | return False 483 | 484 | # Optimization: a node will never be an ancestor of a root node. 485 | if getattr(subject, subject._cte_node_depth) == 1: 486 | return False 487 | 488 | # The path will either be an index of primitives, or an encoding of an 489 | # array. 490 | if type(getattr(node, node._cte_node_path)) == list: 491 | # We can slice with -1 because we know that depth > 1 from above. 492 | return node.id in getattr(subject, subject._cte_node_path)[0:-1] 493 | else: 494 | # Search for node id up to the penultimate entry in the path of the 495 | # subject, meaning we ignore the end of the path consisting of: 496 | # a) the ending closing curly brace, 497 | # b) the length of the subject id, and 498 | # c) the separator character, 499 | # therefore we look for a match ending at the length of the 500 | # subject's id string plus two (so negative index length minus two). 501 | _path = getattr(subject, subject._cte_node_path) 502 | return _path[: -len(str(subject.id)) - 2].index(str(node.id)) > 0 503 | 504 | def is_descendant_of(self, node, subject): 505 | """ Returns ``True`` if the given `node` is a descendant of the given 506 | `subject` node, ``False`` otherwise. This method uses the 507 | :attr:`path` virtual field, and so does not perform any query. 508 | 509 | :param node: the :class:`CTENode' for which to determine whether it 510 | is a descendant of the `subject`. 511 | 512 | :param subject: the :class:`CTENode` for which to determine whether 513 | one of its descendants is the `node`. 514 | 515 | :returns: ``True`` if `node` is a descendant of `subject`, ``False`` 516 | otherwise. 517 | """ 518 | # We will need to use the path virtual field, so ensure it exists. 519 | self._ensure_virtual_fields(node) 520 | 521 | # Convenience check so is_descendant_of can be combined with methods 522 | # returning nodes without the caller having to worry about a None 523 | # subject. 524 | if subject is None: 525 | return False 526 | 527 | # Optimization: a root node will never be a descendant of any node. 528 | if getattr(node, node._cte_node_depth) == 1: 529 | return False 530 | 531 | # The path will either be an index of primitives, or an encoding of an 532 | # array. 533 | if type(getattr(node, node._cte_node_path)) == list: 534 | # We can slice with -1 because we know that depth > 1 from above. 535 | return subject.id in getattr(node, node._cte_node_path)[0:-1] 536 | else: 537 | # Search for subject id up to the penultimate entry in the path of 538 | # the node, meaning we ignore the end of the path consisting of: 539 | # a) the ending closing curly brace, 540 | # b) the length of the node id, and 541 | # c) the separator character, 542 | # therefore we look for a match ending at most at the length of the 543 | # node's id string plus two (so negative index length minus two). 544 | _path = getattr(node, node._cte_node_path) 545 | return _path[: -len(str(node.id)) - 2].index(str(subject.id)) > 0 546 | 547 | def is_leaf(self, node): 548 | """ Returns ``True`` if the given `node` is a leaf (has no children), 549 | ``False`` otherwise. 550 | 551 | :param node: the :class:`CTENode` for which to determine whether it 552 | is a leaf. 553 | 554 | :return: ``True`` if `node` is a leaf, ``False`` otherwise. 555 | """ 556 | return not node.children.exists() 557 | 558 | def is_branch(self, node): 559 | """ Returns ``True`` if the given `node` is a branch (has at least one 560 | child), ``False`` otherwise. 561 | 562 | :param node: the :class:`CTENode` for which to determine whether it 563 | is a branch. 564 | 565 | :return: ``True`` if `node` is a branch, ``False`` otherwise. 566 | """ 567 | return node.children.exists() 568 | 569 | def attribute_path( 570 | self, 571 | node, 572 | attribute, 573 | missing=None, 574 | visitor=lambda node, attribute: getattr(node, attribute, None), 575 | ): 576 | """ Generates a list of values of the `attribute` of all ancestors of 577 | the given `node` (as well as the node itself). If a value is 578 | ``None``, then the optional value of `missing` is used (by default 579 | ``None``). 580 | 581 | By default, the ``getattr(node, attribute, None) or missing`` 582 | mechanism is used to obtain the value of the attribute for each 583 | node. This can be overridden by supplying a custom `visitor` 584 | function, which expects as arguments the node and the attribute, and 585 | should return an appropriate value for the required attribute. 586 | 587 | :param node: the :class:`CTENode` for which to generate the 588 | attribute path. 589 | 590 | :param attribute: the name of the attribute. 591 | 592 | :param missing: optional value to use when attribute value is None. 593 | 594 | :param visitor: optional function responsible for obtaining the 595 | attribute value from a node. 596 | 597 | :return: a list of values of the required `attribute` of the 598 | ancestor path of the given `node`. 599 | """ 600 | return [visitor(c, attribute) or missing for c in node.ancestors()] + [ 601 | visitor(node, attribute) or missing 602 | ] 603 | 604 | def as_tree(self, visitor=None, children=None): 605 | """ Recursively traverses each tree (starting from each root) in order 606 | to generate a dictionary-based tree structure of the entire forest. 607 | Each level of the forest/tree is a list of nodes, and each node 608 | consists of a dictionary representation, where the entry 609 | ``children`` (by default) consists of a list of dictionary 610 | representations of its children. 611 | 612 | Optionally, a `visitor` callback can be used, which is responsible 613 | for generating a dictionary representation of a given 614 | :class:`CTENode`. By default, the :meth:`_default_node_visitor` is 615 | used which generates a dictionary with the current node as well as 616 | structural properties. See :meth:`_default_node_visitor` for the 617 | appropriate signature of this callback. 618 | 619 | Optionally, a `children` callback can be used, which is responsible 620 | for determining which :class:`CTENode`s are children of each visited 621 | :class:`CTENode`, resulting in a key (by default ``children``) and a 622 | list of children :class:`CTENode` objects, which are then included 623 | in the dictionary representation of the currently-visited node. See 624 | :meth:`_default_node_children` for the appropriate signature of this 625 | callback. 626 | 627 | For each node visited, the :meth:`CTENode.as_tree` method is invoked 628 | along with the optional `visitor` and `children` arguments. This 629 | method, if not overridden, will delegate to :meth:`node_as_tree`, 630 | which is responsible for invoking the :meth:`visitor` and 631 | :meth:`children` methods, as well as updating the dictionary 632 | representation of the node with the representation of the children 633 | nodes. 634 | 635 | :param visitor: optional function responsible for generating the 636 | dictionary representation of a node. 637 | 638 | :param children: optional function responsible for generating a 639 | children key and list for a node. 640 | 641 | :return: a dictionary representation of the structure of the forest. 642 | """ 643 | return [ 644 | root.as_tree(visitor=visitor, children=children) for root in self.roots() 645 | ] 646 | 647 | def node_as_tree( 648 | self, 649 | node, 650 | visitor=lambda self, node: self._default_node_visitor(node), 651 | children=lambda self, node, visitor, children: self._default_node_children( 652 | node, visitor, children 653 | ), 654 | ): 655 | """ Visits a :class:`CTENode` `node` and delegates to the (optional) 656 | `visitor` callback, as well as the (optional) `children` callback, 657 | in order to generate a dictionary representation of the node along 658 | with its children nodes. 659 | 660 | :param node: the :class:`CTENode` for which to generate the 661 | representation. 662 | 663 | :param visitor: optional function responsible for generating the 664 | dictionary representation of the node. 665 | 666 | :param children: optional function responsible for generating a 667 | children key and list for the node. 668 | 669 | :return: a dictionary representation of the structure of the node 670 | and its descendant tree. 671 | """ 672 | tree = visitor(self, node) 673 | tree.update(children(self, node, visitor, children)) 674 | return tree 675 | 676 | def _default_node_visitor(self, node): 677 | """ Generates a dictionary representation of the given :class:`CTENode` 678 | `node`, which consists of the node itself under the key ``node``, as 679 | well as structural information under the keys ``depth``, ``path``, 680 | ``ordering``, ``leaf``, and ``branch``. 681 | 682 | :param node: the :class:`CTENode` for which to generate the 683 | representation. 684 | 685 | :return: a dictionary representation of the structure of the node. 686 | """ 687 | return { 688 | "depth": getattr(node, node._cte_node_depth), 689 | "path": [str(c) for c in getattr(node, node._cte_node_path)], 690 | "ordering": getattr(node, node._cte_node_ordering), 691 | "leaf": node.is_leaf(), 692 | "branch": node.is_branch(), 693 | "node": node, 694 | } 695 | 696 | def _default_node_children(self, node, visitor, children): 697 | """ Generates a key and list of children of the given :class:`CTENode` 698 | `node`, intended to be used as an update to the dictionary 699 | representation generated by the :meth:`node_as_tree` method. The key is 700 | ``children`` and the list consists of the children of the given node as 701 | determined by the `children` callback. 702 | 703 | Each child node is, in turn, visited through recursive calls to 704 | :meth:`node_as_child`, and the `visitor` and `children` parameters are 705 | passed along. 706 | 707 | :param node: the :class:`CTENode` for which to generate the children 708 | representation. 709 | 710 | :param visitor: optional function responsible for generating the 711 | dictionary representation of the node. 712 | 713 | :param children: optional function responsible for generating a 714 | children key and list for the node. 715 | 716 | :return: a key and list representation of the structure of the children 717 | of the given node. 718 | """ 719 | return { 720 | self.model._cte_node_children: [ 721 | self.node_as_tree(child, visitor=visitor, children=children) 722 | for child in node.children.all() 723 | ] 724 | } 725 | 726 | def drilldown(self, attributes, path): 727 | """ Recursively descends the tree/forest (starting from each root node) 728 | in order to find a :class:`CTENode` which corresponds to the given 729 | `path`. The path is expected to be an iterable of tuples, called 730 | path components, consisting of attribute values with which to filter 731 | through the QuerySet API. The name of the attribute to which each 732 | value corresponds is specified in `attributes`, which is expected 733 | to conform to Django's QuerySet API for the filter semantics. Each 734 | value in the path component tuple will be mapped to its 735 | corresponding attribute name before being passed to the filter 736 | method. 737 | 738 | For example, if the node model features the integer field ``x`` 739 | and the boolean field ``y``, we can drill down in the following way: 740 | 741 | drilldown(('x__gte', 'y'),[(35, True), (37, False)]) 742 | 743 | The simplest form of drilldown is to match with equality on a single 744 | attribute, such as ``name``, as in the following example: 745 | 746 | drilldown(('name',), [('path',), ('to',), ('my',), ('node',)]) 747 | 748 | Don't forget the trailing comma if specifying singleton tuples! If 749 | you need very simple, one-attribute path components, it is suggested 750 | you extend the manager with your own convenience method; the above 751 | will, for instance, become: 752 | 753 | def drilldown_by_name(self, path): 754 | return self.drilldown(('name',), 755 | [(component,) for component in path]) 756 | 757 | Failure to find the required node results in a :class:`DoesNotExist` 758 | exception being raised. 759 | 760 | An empty path will result in the first root node being returned (if 761 | at least one root node exists). 762 | """ 763 | 764 | # empty path result in first root, if present 765 | if len(path) == 0: 766 | try: 767 | return self.roots()[0] 768 | except IndexError: 769 | raise self.model.DoesNotExist 770 | 771 | # bootstrap with the first component, then iterate through the remaining 772 | # components in the path as long as each child node is found 773 | component = path[0] 774 | current = None 775 | 776 | # mapping of attribute names with values, as per QuerySet filter 777 | def attrs(component): 778 | return dict(zip(attributes, component)) 779 | 780 | # find the root corresponding to the bootstrapped initial path component 781 | try: 782 | root = self.roots().filter(**attrs(component))[0] 783 | except IndexError: 784 | raise self.model.DoesNotExist 785 | 786 | # proceed to drill down until path components are exhausted 787 | current = root 788 | for component in path[1:]: 789 | try: 790 | current = current.children.filter(**attrs(component))[0] 791 | except IndexError: 792 | raise self.model.DoesNotExist 793 | return current 794 | 795 | def prepare_delete(self, node, method, position=None, save=True): 796 | """ Prepares a given :class:`CTENode` `node` for deletion, by executing 797 | the required deletion semantics (Pharaoh, Grandmother, or Monarchy). 798 | 799 | The `method` argument can be one of the valid 800 | :const:`DELETE_METHODS` choices. If it is 801 | :const:`DELETE_METHOD_NONE` or ``None``, then the default delete 802 | method will be used (as specified from the optional 803 | :attr:`_cte_node_delete_method`). 804 | 805 | Under the :const:`DELETE_METHOD_GRANDMOTHER` and 806 | :const:`DELETE_METHOD_MONARCHY` delete semantics, descendant nodes 807 | may be moved; in this case the optional `position` can be a 808 | ``callable`` which is invoked prior to each move operation (see 809 | :meth:`move` for details). 810 | 811 | Furthermore, by default, after each move operation, sub-tree nodes 812 | which were moved will be saved through a call to :meth:`Model.save` 813 | unless `save` is ``False``. 814 | 815 | This method delegates move operations to :meth:`move`. 816 | 817 | :param node: the :class:`CTENode` to prepare for deletion. 818 | 819 | :param method: optionally, a delete method to use. 820 | 821 | :param position: optionally, a ``callable`` to invoke prior to each 822 | move operation. 823 | 824 | :param save: flag indicating whether to save after each move 825 | operation, ``True`` by default. 826 | """ 827 | # If no delete method preference is specified, use attribute. 828 | if method is None: 829 | method = node._cte_node_delete_method 830 | # If no preference specified, use default. 831 | if method == self.DELETE_METHOD_NONE: 832 | method = self.DEFAULT_DELETE_METHOD 833 | # Delegate to appropriate method. 834 | getattr(self, "prepare_delete_%s" % method)(node, position, save) 835 | 836 | def prepare_delete_pharaoh(self, node, position=None, save=True): 837 | """ Prepares a given :class:`CTENode` `node` for deletion, by executing 838 | the :const:`DELETE_METHOD_PHARAOH` semantics. 839 | 840 | This method does not perform any sub-tree reorganization, and hence 841 | no move operation, so the `position` and `save` arguments are 842 | ignored; they are present for regularity purposes with the rest of 843 | the deletion preparation methods. 844 | 845 | :param node: the :class:`CTENode` to prepare for deletion. 846 | 847 | :param position: this is ignored, but present for regularity. 848 | 849 | :param save: this is ignored, but present for regularity. 850 | """ 851 | # Django will take care of deleting the sub-tree through the reverse 852 | # Foreign Key parent relation. 853 | pass 854 | 855 | def prepare_delete_grandmother(self, node, position=None, save=True): 856 | """ Prepares a given :class:`CTENode` `node` for deletion, by executing 857 | the :const:`DELETE_METHOD_GRANDMOTHER` semantics. Descendant nodes, 858 | if present, will be moved; in this case the optional `position` can 859 | be a ``callable`` which is invoked prior to each move operation (see 860 | :meth:`move` for details). 861 | 862 | By default, after each move operation, sub-tree nodes which were 863 | moved will be saved through a call to :meth:`Model.save` unless 864 | `save` is ``False``. 865 | 866 | This method delegates move operations to :meth:`move`. 867 | 868 | :param node: the :class:`CTENode` to prepare for deletion. 869 | 870 | :param position: optionally, a ``callable`` to invoke prior to each 871 | move operation. 872 | 873 | :param save: flag indicating whether to save after each move 874 | operation, ``True`` by default. 875 | """ 876 | # Move all children to the node's parent. 877 | for child in node.children.all(): 878 | child.move(node.parent, position, save) 879 | 880 | def prepare_delete_monarchy(self, node, position=None, save=True): 881 | """ Prepares a given :class:`CTENode` `node` for deletion, by executing 882 | the :const:`DELETE_METHOD_MONARCHY` semantics. Descendant nodes, 883 | if present, will be moved; in this case the optional `position` can 884 | be a ``callable`` which is invoked prior to each move operation (see 885 | :meth:`move` for details). 886 | 887 | By default, after each move operation, sub-tree nodes which were 888 | moved will be saved through a call to :meth:`Model.save` unless 889 | `save` is ``False``. 890 | 891 | This method delegates move operations to :meth:`move`. 892 | 893 | :param node: the :class:`CTENode` to prepare for deletion. 894 | 895 | :param position: optionally, a ``callable`` to invoke prior to each 896 | move operation. 897 | 898 | :param save: flag indicating whether to save after each move 899 | operation, ``True`` by default. 900 | """ 901 | # We are going to iterate all children, even though the first child is 902 | # treated in a special way, because the query iterator may be custom, so 903 | # we will avoid using slicing children[0] and children[1:]. 904 | first = None 905 | for child in node.children.all(): 906 | if first is None: 907 | first = child 908 | first.move(node.parent, position, save) 909 | else: 910 | child.move(first, position, save) 911 | 912 | def move(self, node, destination, position=None, save=False): 913 | """ Moves the given :class:`CTENode` `node` and places it as a child 914 | node of the `destination` :class:`CTENode` (or makes it a root node 915 | if `destination` is ``None``). 916 | 917 | Optionally, `position` can be a callable which is invoked prior to 918 | placement of the `node` with the `node` and the `destination` node 919 | as the sole two arguments; this can be useful in implementing 920 | specific sibling ordering semantics. 921 | 922 | Optionally, if `save` is ``True``, after the move operation 923 | completes (after the :attr:`CTENode.parent` foreign key is updated 924 | and the `position` callable is called if present), a call to 925 | :meth:`Model.save` is made. 926 | 927 | :param destination: the destination node of this move, ``None`` 928 | denoting that the node will become a root node. 929 | 930 | :param position: optional callable invoked prior to placement for 931 | purposes of custom sibling ordering semantics. 932 | 933 | :param save: optional flag indicating whether this model's 934 | :meth:`save` method should be invoked after the move. 935 | 936 | :return: this node. 937 | """ 938 | # Allow custom positioning semantics to specify the position before 939 | # setting the parent. 940 | if position is not None: 941 | position(node, destination) 942 | node.parent = destination 943 | if save: 944 | node.save() 945 | return node 946 | 947 | 948 | class CTENode(Model): 949 | """ Abstract :class:`Model` which implements a node in a CTE tree. This 950 | model features a mandatory foreign key to the parent node (hence to 951 | ``self``), which, when ``None``, indicates a root node. Multiple nodes 952 | with a ``None`` parent results in a forest, which can be constrained 953 | either with custom SQL constraints or through application logic. 954 | 955 | It is necessary for any custom :class:`Manager` of this model to inherit 956 | from :class:`CTENodeManager`, as all functionality of the CTE tree is 957 | implemented in the manager. 958 | 959 | It is possible to manipulate individual nodes when not loaded through 960 | the custom manager, or when freshly created either through the 961 | :meth:`create` method or through the constructor, however, any operation 962 | which requires tree information (the :attr:`depth`, :attr:`path`, 963 | and :attr:`ordering` virtual fields) will not work, and any attempt to 964 | invoke such methods will result in an :class:`ImproperlyConfigured` 965 | exception being raised. 966 | 967 | Many runtime properties of nodes are specified through a set of 968 | parameters which are stored as attributes of the node class, and begin 969 | with ``_cte_node_``. Before any of these parameters are used, the 970 | manager will attempt to load and verify them, raising an 971 | :class:`ImproperlyConfigured` exception if any errors are encountered. 972 | All parameters have default values. 973 | 974 | All :class:`QuerySet` objects involving CTE nodes use the 975 | :meth:`QuerySet.extra` semantics in order to specify additional 976 | ``SELECT``, ``WHERE``, and ``ORDER_BY`` SQL semantics, therefore, they 977 | cannot be combined through the ``OR`` operator (the ``|`` operator). 978 | 979 | The following parameters can optionally be specified at the class level: 980 | 981 | * _cte_node_traversal: 982 | 983 | A string from one of :const:`TREE_TRAVERSAL_METHODS`, which 984 | specifies the default tree traversal order. If this parameters is 985 | ``None`` or :const:`TREE_TRAVERSAL_NONE`, then 986 | :const:`DEFAULT_TREE_TRAVERSAL` method is used (which is ``dfs`` 987 | for depth-first). 988 | 989 | * _cte_node_order_by: 990 | 991 | A list of strings or tuples specifying the ordering of siblings 992 | during tree traversal (in the breadth-first method, siblings are 993 | ordered depending on their parent and not the entire set of nodes at 994 | the given depth of the tree). 995 | 996 | The entries in this list can be any of the model fields, much like 997 | the entries in the :attr:`ordering` of the model's :class:`Meta` 998 | class or the arguments of the :meth:`order_by` method of 999 | :class:`QuerySet`. 1000 | 1001 | These entries may also contain the virtual field :attr:`depth`, 1002 | which cannot be used by the normal :class:`QuerySet` because Django 1003 | cannot recognize such virtual fields. 1004 | 1005 | In case of multiple entries, they must all be of the same database 1006 | type. For VARCHAR fields, their values will be cast to TEXT, unless 1007 | otherwise specified. It is possible to specify the database type 1008 | into which the ordering field values are cast by providing tuples of 1009 | the form ``(fieldname, dbtype)`` in the ordering sequence. 1010 | 1011 | Specifying cast types is necessary when combining different data 1012 | types in the ordering sequence, such as an int and a float (casting 1013 | the int into a float is probably the desired outcome in this 1014 | situation). In the worst case, TEXT can be specified for all casts. 1015 | 1016 | * _cte_node_delete_method: 1017 | 1018 | A string specifying the desired default deletion semantics, which 1019 | may be one of :const:`DELETE_METHODS`. If this parameter is missing 1020 | or ``None`` or :const:`DELETE_METHOD_NONE`, then the default 1021 | deletion semantics :const:`DEFAULT_DELETE_METHOD` will be used 1022 | (which is :const:`DELETE_METHOD_PHARAOH` or ``pharaoh`` for the 1023 | Pharaoh deletion semantics). 1024 | 1025 | * _cte_node_parent: 1026 | 1027 | A string referencing the name of the :class:`ForeignKey` field which 1028 | implements the parent relationship, typically called ``parent`` and 1029 | automatically inherited from this class. 1030 | 1031 | If this parameter is missing, and no field with the name ``parent`` 1032 | can be found, then the first :class:`ForeignKey` which relates to 1033 | this model will be used as the parent relationship field. 1034 | 1035 | * _cte_node_children: 1036 | 1037 | A string referencing the `related_name` attribute of the 1038 | :class:`ForeignKey` field which implements the parent relationship, 1039 | typically called ``parent`` (specified in 1040 | :const:`DEFAULT_CHILDREN_NAME`) and automatically 1041 | inherited from this class. 1042 | 1043 | * _cte_node_table: 1044 | 1045 | The name of the temporary table to use with the ``WITH`` CTE SQL 1046 | statement when compiling queries involving nodes. By default this is 1047 | :const:`DEFAULT_TABLE_NAME` (which is ``cte``). 1048 | 1049 | * _cte_node_primary_key_type: 1050 | 1051 | A string representing the database type of the primary key, if the 1052 | primary key is a non-standard type, and must be cast in order to be 1053 | used in the :attr:`path` or :attr:`ordering` virtual fields 1054 | (similarly to the :attr:`_cte_node_order_by` parameter above). 1055 | 1056 | A ``VARCHAR`` primary key will be automatically cast to ``TEXT``, 1057 | unless explicitly specified otherwise through this parameter. 1058 | 1059 | 1060 | * _cte_node_path, _cte_node_depth, _cte_node_ordering: 1061 | 1062 | Strings specifying the attribute names of the virtual fields 1063 | containing the path, depth, and ordering prefix of each node, by 1064 | default, respectively, :const:`VIRTUAL_FIELD_PATH` (which is 1065 | ``path``), :const:`VIRTUAL_FIELD_DEPTH` (which is ``depth``), and 1066 | :const:`VIRTUAL_FIELD_ORDERING` (which is ``ordering``). 1067 | """ 1068 | 1069 | # This ForeignKey is mandatory, however, its name can be different, as long 1070 | # as it's specified through _cte_node_parent. 1071 | _cte_node_parent = "parent" 1072 | parent = ForeignKey( 1073 | "self", on_delete=CASCADE, null=True, blank=True, related_name="children" 1074 | ) 1075 | 1076 | # This custom Manager is mandatory. 1077 | objects = CTENodeManager() 1078 | 1079 | class Meta: 1080 | abstract = True 1081 | base_manager_name = "objects" 1082 | # Prevent cycles in order to maintain tree / forest property. 1083 | unique_together = [("id", "parent")] 1084 | 1085 | def clean(self): 1086 | """ Prevents cycles in the tree. """ 1087 | super(CTENode, self).clean() 1088 | if self.parent and self.pk in getattr(self.parent, self._cte_node_path): 1089 | raise ValidationError(_("A node cannot be made a descendant of itself.")) 1090 | 1091 | def root(self): 1092 | """ Returns the CTENode which is the root of the tree in which this 1093 | node participates. 1094 | """ 1095 | return self.__class__.objects.root(self) 1096 | 1097 | def siblings(self): 1098 | """ Returns a :class:`QuerySet` of all siblings of this node. 1099 | 1100 | :returns: A :class:`QuerySet` of all siblings of this node. 1101 | """ 1102 | return self.__class__.objects.siblings(self) 1103 | 1104 | def ancestors(self): 1105 | """ Returns a :class:`QuerySet` of all ancestors of this node. 1106 | 1107 | :returns: A :class:`QuerySet` of all ancestors of this node. 1108 | """ 1109 | return self.__class__.objects.ancestors(self) 1110 | 1111 | def descendants(self): 1112 | """ Returns a :class:`QuerySet` of all descendants of this node. 1113 | 1114 | :returns: A :class:`QuerySet` of all descendants of this node. 1115 | """ 1116 | return self.__class__.objects.descendants(self) 1117 | 1118 | def is_parent_of(self, subject): 1119 | """ Returns ``True`` if this node is the parent of the given `subject` 1120 | node, ``False`` otherwise. This method uses the :attr:`parent` 1121 | field, and so does not perform any query. 1122 | 1123 | :param subject: the :class:`CTENode` for which to determine whether 1124 | its parent is this node. 1125 | 1126 | :returns: ``True`` if this node is the parent of `subject`, 1127 | ``False`` otherwise. 1128 | """ 1129 | return self.__class__.objects.is_parent_of(self, subject) 1130 | 1131 | def is_child_of(self, subject): 1132 | """ Returns ``True`` if this node is a child of the given `subject` 1133 | node, ``False`` otherwise. This method used the :attr:`parent` 1134 | field, and so does not perform any query. 1135 | 1136 | :param subject: the :class:`CTENode` for which to determine whether 1137 | one of its children is this node. 1138 | 1139 | :returns: ``True`` if this node is a child of `subject`, ``False`` 1140 | otherwise. 1141 | """ 1142 | return self.__class__.objects.is_child_of(self, subject) 1143 | 1144 | def is_sibling_of(self, subject): 1145 | """ Returns ``True`` if this node is a sibling of the given `subject` 1146 | node, ``False`` otherwise. This method uses the :attr:`parent` 1147 | field, and so does not perform any query. 1148 | 1149 | :param subject: the :class:`CTENode` for which to determine whether 1150 | one of its siblings is this node. 1151 | 1152 | :returns: ``True`` if this node is a sibling of `subject`, ``False`` 1153 | otherwise. 1154 | """ 1155 | return self.__class__.objects.is_sibling_of(self, subject) 1156 | 1157 | def is_ancestor_of(self, subject): 1158 | """ Returns ``True`` if the node is an ancestor of the given `subject` 1159 | node, ``False`` otherwise. This method uses the :attr:`path` virtual 1160 | field, and so does not perform any query. 1161 | 1162 | :param subject: the :class:`CTENode` for which to determine whether 1163 | one of its ancestors is this node. 1164 | 1165 | :returns: ``True`` if this node is an ancestor of `subject`, 1166 | ``False`` otherwise. 1167 | """ 1168 | return self.__class__.objects.is_ancestor_of(self, subject) 1169 | 1170 | def is_descendant_of(self, subject): 1171 | """ Returns ``True`` if the node is a descendant of the given `subject` 1172 | node, ``False`` otherwise. This method uses the :attr:`path` virtual 1173 | field, and so does not perform any query. 1174 | 1175 | :param subject: the :class:`CTENode` for which to determine whether 1176 | one of its descendants is this node. 1177 | 1178 | :returns: ``True`` if this node is a descendant of `subject`, 1179 | ``False`` otherwise. 1180 | """ 1181 | return self.__class__.objects.is_descendant_of(self, subject) 1182 | 1183 | def is_leaf(self): 1184 | """ Returns ``True`` if this node is a leaf (has no children), ``False`` 1185 | otherwise. 1186 | 1187 | :return: ``True`` if this node is a leaf, ``False`` otherwise. 1188 | """ 1189 | return self.__class__.objects.is_leaf(self) 1190 | 1191 | def is_branch(self): 1192 | """ Returns ``True`` if this node is a branch (has at least one child), 1193 | ``False`` otherwise. 1194 | 1195 | :return: ``True`` if this node is a branch, ``False`` otherwise. 1196 | """ 1197 | return self.__class__.objects.is_branch(self) 1198 | 1199 | def attribute_path(self, attribute, missing=None, visitor=None): 1200 | """ Generates a list of values of the `attribute` of all ancestors of 1201 | this node (as well as the node itself). If a value is ``None``, then 1202 | the optional value of `missing` is used (by default ``None``). 1203 | 1204 | By default, the ``getattr(node, attribute, None) or missing`` 1205 | mechanism is used to obtain the value of the attribute for each 1206 | node. This can be overridden by supplying a custom `visitor` 1207 | function, which expects as arguments the node and the attribute, and 1208 | should return an appropriate value for the required attribute. 1209 | 1210 | :param attribute: the name of the attribute. 1211 | 1212 | :param missing: optional value to use when attribute value is None. 1213 | 1214 | :param visitor: optional function responsible for obtaining the 1215 | attribute value from a node. 1216 | 1217 | :return: a list of values of the required `attribute` of the 1218 | ancestor path of this node. 1219 | """ 1220 | _parameters = {"node": self, "attribute": attribute} 1221 | if missing is not None: 1222 | _parameters["missing"] = missing 1223 | if visitor is not None: 1224 | _parameters["visitor"] = visitor 1225 | return self.__class__.objects.attribute_path(**_parameters) 1226 | 1227 | def as_tree(self, visitor=None, children=None): 1228 | """ Recursively traverses each tree (starting from each root) in order 1229 | to generate a dictionary-based tree structure of the entire forest. 1230 | Each level of the forest/tree is a list of nodes, and each node 1231 | consists of a dictionary representation, where the entry 1232 | ``children`` (by default) consists of a list of dictionary 1233 | representations of its children. 1234 | 1235 | See :meth:`CTENodeManager.as_tree` and 1236 | :meth:`CTENodeManager.node_as_tree` for details on how this method 1237 | works, as well as its expected arguments. 1238 | 1239 | :param visitor: optional function responsible for generating the 1240 | dictionary representation of a node. 1241 | 1242 | :param children: optional function responsible for generating a 1243 | children key and list for a node. 1244 | 1245 | :return: a dictionary representation of the structure of the forest. 1246 | """ 1247 | _parameters = {"node": self} 1248 | if visitor is not None: 1249 | _parameters["visitor"] = visitor 1250 | if children is not None: 1251 | _parameters["children"] = children 1252 | return self.__class__.objects.node_as_tree(**_parameters) 1253 | 1254 | def move(self, destination=None, position=None, save=False): 1255 | """ Moves this node and places it as a child node of the `destination` 1256 | :class:`CTENode` (or makes it a root node if `destination` is 1257 | ``None``). 1258 | 1259 | Optionally, `position` can be a callable which is invoked prior to 1260 | placement of the node with this node and the destination node as the 1261 | sole two arguments; this can be useful in implementing specific 1262 | sibling ordering semantics. 1263 | 1264 | Optionally, if `save` is ``True``, after the move operation 1265 | completes (after the :attr:`parent` foreign key is updated and the 1266 | `position` callable is called if present), a call to 1267 | :meth:`Model.save` is made. 1268 | 1269 | :param destination: the destination node of this move, ``None`` 1270 | denoting that the node will become a root node. 1271 | 1272 | :param position: optional callable invoked prior to placement for 1273 | purposes of custom sibling ordering semantics. 1274 | 1275 | :param save: optional flag indicating whether this model's 1276 | :meth:`save` method should be invoked after the move. 1277 | 1278 | :return: this node. 1279 | """ 1280 | return self.__class__.objects.move(self, destination, position, save) 1281 | 1282 | def delete(self, method=None, position=None, save=True): 1283 | """ Prepares the tree for deletion according to the deletion semantics 1284 | specified for the :class:`CTENode` Model, and then delegates to the 1285 | :class:`CTENode` superclass ``delete`` method. 1286 | 1287 | Default deletion `method` and `position` callable can be overridden 1288 | by being supplied as arguments to this method. 1289 | 1290 | :param method: optionally a particular deletion method, overriding 1291 | the default method specified for this model. 1292 | 1293 | :param position: optional callable to invoke prior to each move 1294 | operation, should the delete method require any moves. 1295 | 1296 | :param save: optional flag indicating whether this model's 1297 | :meth:`save` method should be invoked after each move operation, 1298 | should the delete method require any moves. 1299 | """ 1300 | self.__class__.objects.prepare_delete( 1301 | self, method=method, position=position, save=save 1302 | ) 1303 | return super(CTENode, self).delete() 1304 | -------------------------------------------------------------------------------- /cte_forest/query.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Django CTE Trees Query Compiler. 35 | """ 36 | from __future__ import unicode_literals 37 | 38 | import django 39 | from django.db import connections 40 | from django.db.models.query import QuerySet 41 | from django.db.models.sql import AggregateQuery, DeleteQuery, InsertQuery, UpdateQuery 42 | from django.db.models.sql.compiler import ( 43 | SQLAggregateCompiler, 44 | SQLCompiler, 45 | SQLDeleteCompiler, 46 | SQLInsertCompiler, 47 | SQLUpdateCompiler, 48 | ) 49 | from django.db.models.sql.query import Query 50 | from django.db.models.sql.where import ExtraWhere, WhereNode 51 | 52 | 53 | class CTEQuerySet(QuerySet): 54 | """ 55 | The QuerySet which ensures all CTE Node SQL compilation is processed by the 56 | CTE Compiler and has the appropriate extra syntax, selects, tables, and 57 | WHERE clauses. 58 | """ 59 | 60 | def __init__(self, model=None, query=None, using=None, offset=None, hints=None): 61 | """ 62 | Prepares a CTEQuery object by adding appropriate extras, namely the 63 | SELECT virtual fields, the WHERE clause which matches the CTE pk with 64 | the real table pk, and the tree-specific order_by parameters. If the 65 | query object has already been prepared through this phase, then it 66 | won't be prepared again. 67 | 68 | :param model: 69 | :type model: 70 | :param query: 71 | :type query: 72 | :param using: 73 | :type using: 74 | :param offset: 75 | :type offset: 76 | :return: 77 | :rtype: 78 | """ 79 | # Only create an instance of a Query if this is the first invocation in 80 | # a query chain. 81 | if query is None: 82 | query = CTEQuery(model, offset=offset) 83 | super(CTEQuerySet, self).__init__(model, query, using) 84 | 85 | def aggregate(self, *args, **kwargs): 86 | """ 87 | Returns a dictionary containing the calculations (aggregation) 88 | over the current queryset 89 | 90 | If args is present the expression is passed as a kwarg using 91 | the Aggregate object's default alias. 92 | """ 93 | if self.query.distinct_fields: 94 | raise NotImplementedError("aggregate() + distinct(fields) not implemented.") 95 | for arg in args: 96 | # The default_alias property may raise a TypeError, so we use 97 | # a try/except construct rather than hasattr in order to remain 98 | # consistent between PY2 and PY3 (hasattr would swallow 99 | # the TypeError on PY2). 100 | try: 101 | arg.default_alias 102 | except (AttributeError, TypeError): 103 | raise TypeError("Complex aggregates require an alias") 104 | kwargs[arg.default_alias] = arg 105 | 106 | if django.VERSION < (2, 0): 107 | query = self.query.clone(CTEAggregateQuery) 108 | else: 109 | query = self.query.chain(CTEAggregateQuery) 110 | for (alias, aggregate_expr) in kwargs.items(): 111 | query.add_annotation(aggregate_expr, alias, is_summary=True) 112 | if not query.annotations[alias].contains_aggregate: 113 | raise TypeError("%s is not an aggregate expression" % alias) 114 | return query.get_aggregation(self.db, kwargs.keys()) 115 | 116 | 117 | class CTEQuery(Query): 118 | """ 119 | A Query which processes SQL compilation through the CTE Compiler, as well as 120 | keeps track of extra selects, the CTE table, and the corresponding WHERE 121 | clauses. 122 | """ 123 | 124 | def __init__(self, model, where=WhereNode, offset=None): 125 | """ 126 | Delegates initialization to Django's Query, and in addition adds the 127 | necessary extra selects, table, and where clauses for the CTE. Depending 128 | on the ordering semantics specified in the CTE Node as well as any 129 | order_by queries, the final order_by of the Query is generated. In the 130 | event of an offset Node being specified (such as querying for all of the 131 | descendants of a Node), an additional appropriate WHERE clauses is also 132 | added. 133 | 134 | :param model: 135 | :type model: 136 | :param where: 137 | :type where: 138 | :param offset: 139 | :type offset: 140 | :return: 141 | :rtype: 142 | """ 143 | super(CTEQuery, self).__init__(model, where=where) 144 | # import from models here to avoid circular imports. 145 | from .models import CTENodeManager 146 | 147 | if model is not None: 148 | where = [self._generate_where(self)] 149 | # If an offset Node is specified, then only those Nodes which 150 | # contain the offset Node as a parent (in their path virtual field) 151 | # will be returned. 152 | if offset is not None: 153 | where.append( 154 | """'{id}' = ANY({cte}."{path}")""".format( 155 | cte=model._cte_node_table, 156 | path=model._cte_node_path, 157 | id=str(offset.id), 158 | ) 159 | ) 160 | order_by_prefix = [] 161 | if model._cte_node_traversal == CTENodeManager.TREE_TRAVERSAL_NONE: 162 | chosen_traversal = CTENodeManager.DEFAULT_TREE_TRAVERSAL 163 | else: 164 | chosen_traversal = model._cte_node_traversal 165 | if chosen_traversal == CTENodeManager.TREE_TRAVERSAL_DFS: 166 | order_by_prefix = [model._cte_node_ordering] 167 | if chosen_traversal == CTENodeManager.TREE_TRAVERSAL_BFS: 168 | order_by_prefix = [model._cte_node_depth, model._cte_node_ordering] 169 | # prepend correct CTE table prefix to order_by fields 170 | order_by = [ 171 | "{cte}.{field}".format(cte=model._cte_node_table, field=field) 172 | for field in order_by_prefix 173 | ] 174 | if ( 175 | hasattr(model, "_cte_node_order_by") 176 | and model._cte_node_order_by is not None 177 | and len(model._cte_node_order_by) > 0 178 | ): 179 | order_by.extend( 180 | field[0] if type(field) == tuple else field 181 | for field in model._cte_node_order_by 182 | ) 183 | # Specify the virtual fields for depth, path, and ordering; 184 | # optionally the offset Node constraint; and the desired ordering. 185 | # The CTE table will be added later by the Compiler only if the 186 | # actual query needs it. 187 | self.add_extra( 188 | select={ 189 | model._cte_node_depth: "{cte}.{depth}".format( 190 | cte=model._cte_node_table, depth=model._cte_node_depth 191 | ), 192 | model._cte_node_path: "{cte}.{path}".format( 193 | cte=model._cte_node_table, path=model._cte_node_path 194 | ), 195 | model._cte_node_ordering: "{cte}.{ordering}".format( 196 | cte=model._cte_node_table, ordering=model._cte_node_ordering 197 | ), 198 | }, 199 | select_params=None, 200 | where=where, 201 | params=None, 202 | tables=None, 203 | order_by=order_by, 204 | ) 205 | 206 | @classmethod 207 | def _generate_where(cls, query): 208 | def maybe_alias(table): 209 | if table in query.table_map: 210 | return query.table_map[table][0] 211 | return table 212 | 213 | return '{cte}."{pk}" = {table}."{pk}"'.format( 214 | cte=query.model._cte_node_table, 215 | pk=query.model._meta.pk.attname, 216 | table=maybe_alias(query.model._meta.db_table), 217 | ) 218 | 219 | @classmethod 220 | def _remove_cte_where(cls, query): 221 | where = cls._generate_where(query) 222 | for w in query.where.children: 223 | if isinstance(w, ExtraWhere): 224 | if where in w.sqls: 225 | query.where.children.remove(w) 226 | 227 | def get_compiler(self, using=None, connection=None): 228 | """ Overrides the Query method get_compiler in order to return 229 | an instance of the above custom compiler. 230 | """ 231 | # Copy the body of this method from Django except the final 232 | # return statement. We will ignore code coverage for this. 233 | if using is None and connection is None: # pragma: no cover 234 | raise ValueError("Need either using or connection") 235 | if using: 236 | connection = connections[using] 237 | # Check that the compiler will be able to execute the query 238 | for alias, aggregate in self.annotation_select.items(): 239 | connection.ops.check_expression_support(aggregate) 240 | # Instantiate the custom compiler. 241 | return { 242 | CTEUpdateQuery: CTEUpdateQueryCompiler, 243 | CTEInsertQuery: CTEInsertQueryCompiler, 244 | CTEDeleteQuery: CTEDeleteQueryCompiler, 245 | CTEAggregateQuery: CTEAggregateQueryCompiler, 246 | }.get(self.__class__, CTEQueryCompiler)(self, connection, using) 247 | 248 | if django.VERSION < (2, 0): 249 | 250 | def clone(self, klass=None, memo=None, **kwargs): 251 | """ Overrides Django's Query clone in order to return appropriate CTE 252 | compiler based on the target Query class. This mechanism is used by 253 | methods such as 'update' and '_update' in order to generate UPDATE 254 | queries rather than SELECT queries. 255 | """ 256 | klass = { 257 | UpdateQuery: CTEUpdateQuery, 258 | InsertQuery: CTEInsertQuery, 259 | DeleteQuery: CTEDeleteQuery, 260 | AggregateQuery: CTEAggregateQuery, 261 | }.get(klass, self.__class__) 262 | return super(CTEQuery, self).clone(klass, memo, **kwargs) 263 | 264 | else: 265 | 266 | def chain(self, klass=None): 267 | """ Overrides Django's Query clone in order to return appropriate CTE 268 | compiler based on the target Query class. This mechanism is used by 269 | methods such as 'update' and '_update' in order to generate UPDATE 270 | queries rather than SELECT queries. 271 | """ 272 | klass = { 273 | UpdateQuery: CTEUpdateQuery, 274 | InsertQuery: CTEInsertQuery, 275 | DeleteQuery: CTEDeleteQuery, 276 | AggregateQuery: CTEAggregateQuery, 277 | }.get(klass, self.__class__) 278 | return super(CTEQuery, self).chain(klass) 279 | 280 | 281 | class CTEUpdateQuery(UpdateQuery, CTEQuery): 282 | pass 283 | 284 | 285 | class CTEInsertQuery(InsertQuery, CTEQuery): 286 | pass 287 | 288 | 289 | class CTEDeleteQuery(DeleteQuery, CTEQuery): 290 | pass 291 | 292 | 293 | class CTEAggregateQuery(AggregateQuery, CTEQuery): 294 | pass 295 | 296 | 297 | class CTECompiler(object): 298 | 299 | CTE = """WITH RECURSIVE {cte} ("{depth}", "{path}", "{ordering}", "{pk}") AS ( 300 | 301 | SELECT 1 AS depth, 302 | array[{pk_path}] AS {path}, 303 | {order} AS {ordering}, 304 | T."{pk}" 305 | FROM {db_table} T 306 | WHERE T."{parent}" IS NULL 307 | 308 | UNION ALL 309 | 310 | SELECT {cte}.{depth} + 1 AS {depth}, 311 | {cte}.{path} || {pk_path}, 312 | {cte}.{ordering} || {order}, 313 | T."{pk}" 314 | FROM {db_table} T 315 | JOIN {cte} ON T."{parent}" = {cte}."{pk}") 316 | 317 | """ 318 | 319 | @classmethod 320 | def generate_sql(cls, connection, query, as_sql): 321 | """ 322 | 323 | :param query: 324 | :type query: 325 | :param sql: 326 | :type sql: 327 | :return: 328 | :rtype: 329 | """ 330 | # add the CTE table to the Query's extras precisely once (because we 331 | # could be combining multiple CTE Queries). 332 | if query.model._cte_node_table not in query.extra_tables: 333 | query.add_extra( 334 | select=None, 335 | select_params=None, 336 | where=None, 337 | params=None, 338 | tables=[query.model._cte_node_table], 339 | order_by=None, 340 | ) 341 | 342 | # place appropriate CTE table prefix to any order_by or extra_order_by 343 | # entries which reference virtual fields, and preserve the optional 344 | # sign. 345 | cte_columns = ( 346 | query.model._cte_node_depth, 347 | query.model._cte_node_path, 348 | query.model._cte_node_ordering, 349 | ) 350 | 351 | def maybe_prefix_order_by(order_by): 352 | return [ 353 | "{sign}{cte}.{column}".format( 354 | sign="-" if order[0] == "-" else "", 355 | cte=query.model._cte_node_table, 356 | column=order.lstrip("-"), 357 | ) 358 | if order.lstrip("-") in cte_columns 359 | else order 360 | for order in order_by 361 | ] 362 | 363 | query.order_by = maybe_prefix_order_by(query.order_by) 364 | query.extra_order_by = maybe_prefix_order_by(query.extra_order_by) 365 | 366 | # Obtain compiled SQL from Django. 367 | sql = as_sql() 368 | 369 | def maybe_cast(field): 370 | # If the ordering information specified a type to cast to, then use 371 | # this type immediately, otherwise determine whether a 372 | # variable-length character field should be cast into TEXT or if no 373 | # casting is necessary. A None type defaults to the latter. 374 | if type(field) == tuple and not field[1] is None: 375 | return 'CAST (T."%s" AS %s)' % field 376 | else: 377 | if type(field) == tuple: 378 | name = field[0] 379 | else: 380 | name = field 381 | _field = query.model._meta.get_field(name) 382 | if _field.db_type(connection).startswith("varchar"): 383 | return 'CAST (T."%s" AS TEXT)' % name 384 | else: 385 | return 'T."%s"' % name 386 | 387 | # The primary key is used in the path; in case it is of a custom type, 388 | # ensure appropriate casting is performed. This is a very rare 389 | # condition, as most types can be used directly in the path, especially 390 | # since no other fields with incompatible types are combined (with a 391 | # notable exception of VARCHAR types which must be converted to TEXT). 392 | pk_path = maybe_cast( 393 | (query.model._meta.pk.attname, query.model._cte_node_primary_key_type) 394 | ) 395 | 396 | # If no explicit ordering is specified, then use the primary key. If the 397 | # primary key is used in ordering, and it is of a type which needs 398 | # casting in order to be used in the ordering, then it is possible that 399 | # explicit casting was not specified through _cte_node_order_by because 400 | # it is expected to be specified through the _cte_node_primary_key_type 401 | # attribute. Specifying the cast type of the primary key in the 402 | # _cte_node_order_by attribute has precedence over 403 | # _cte_node_primary_key_type. 404 | if ( 405 | not hasattr(query.model, "_cte_node_order_by") 406 | or query.model._cte_node_order_by is None 407 | or len(query.model._cte_node_order_by) == 0 408 | ): 409 | order = "array[%s]" % maybe_cast( 410 | (query.model._meta.pk.attname, query.model._cte_node_primary_key_type) 411 | ) 412 | else: 413 | # Compute the ordering virtual field constructor, possibly casting 414 | # fields into a common type. 415 | order = "||".join( 416 | "array[%s]" % maybe_cast(field) 417 | for field in query.model._cte_node_order_by 418 | ) 419 | # Prepend the CTE with the ordering constructor and table 420 | # name to the SQL obtained from Django above. 421 | return ( 422 | "".join( 423 | [ 424 | cls.CTE.format( 425 | order=order, 426 | cte=query.model._cte_node_table, 427 | depth=query.model._cte_node_depth, 428 | path=query.model._cte_node_path, 429 | ordering=query.model._cte_node_ordering, 430 | parent=query.model._cte_node_parent_attname, 431 | pk=query.model._meta.pk.attname, 432 | pk_path=pk_path, 433 | db_table=query.model._meta.db_table, 434 | ), 435 | sql[0], 436 | ] 437 | ), 438 | sql[1], 439 | ) 440 | 441 | 442 | class CTEQueryCompiler(SQLCompiler): 443 | def as_sql(self, *args, **kwargs): 444 | """ 445 | Overrides the :class:`SQLCompiler` method in order to prepend the 446 | necessary CTE syntax, as well as perform pre- and post-processing, 447 | including adding the extra CTE table and WHERE clauses. 448 | 449 | :param with_limits: 450 | :type with_limits: 451 | :param with_col_aliases: 452 | :type with_col_aliases: 453 | :return: 454 | :rtype: 455 | """ 456 | 457 | def _as_sql(): 458 | return super(CTEQueryCompiler, self).as_sql(*args, **kwargs) 459 | 460 | return CTECompiler.generate_sql(self.connection, self.query, _as_sql) 461 | 462 | 463 | class CTEUpdateQueryCompiler(SQLUpdateCompiler): 464 | def as_sql(self, *args, **kwargs): 465 | """ 466 | Overrides the :class:`SQLUpdateCompiler` method in order to remove any 467 | CTE-related WHERE clauses, which are not necessary for UPDATE queries, 468 | yet may have been added if this query was cloned from a CTEQuery. 469 | 470 | :return: 471 | :rtype: 472 | """ 473 | CTEQuery._remove_cte_where(self.query) 474 | return super(self.__class__, self).as_sql(*args, **kwargs) 475 | 476 | 477 | class CTEInsertQueryCompiler(SQLInsertCompiler): 478 | def as_sql(self, *args, **kwargs): 479 | """ 480 | Overrides the :class:`SQLInsertCompiler` method in order to remove any 481 | CTE-related WHERE clauses, which are not necessary for INSERT queries, 482 | yet may have been added if this query was cloned from a CTEQuery. 483 | 484 | :return: 485 | :rtype: 486 | """ 487 | CTEQuery._remove_cte_where(self.query) 488 | return super(self.__class__, self).as_sql(*args, **kwargs) 489 | 490 | 491 | class CTEDeleteQueryCompiler(SQLDeleteCompiler): 492 | def as_sql(self, *args, **kwargs): 493 | """ 494 | Overrides the :class:`SQLDeleteCompiler` method in order to remove any 495 | CTE-related WHERE clauses, which are not necessary for DELETE queries, 496 | yet may have been added if this query was cloned from a CTEQuery. 497 | 498 | :return: 499 | :rtype: 500 | """ 501 | CTEQuery._remove_cte_where(self.query) 502 | return super(self.__class__, self).as_sql(*args, **kwargs) 503 | 504 | 505 | class CTEAggregateQueryCompiler(SQLAggregateCompiler): 506 | def as_sql(self, *args, **kwargs): 507 | """ 508 | Overrides the :class:`SQLAggregateCompiler` method in order to 509 | prepend the necessary CTE syntax, as well as perform pre- and post- 510 | processing, including adding the extra CTE table and WHERE clauses. 511 | 512 | :param qn: 513 | :type qn: 514 | :return: 515 | :rtype: 516 | """ 517 | 518 | def _as_sql(): 519 | return super(CTEAggregateQueryCompiler, self).as_sql(*args, **kwargs) 520 | 521 | return CTECompiler.generate_sql(self.connection, self.query, _as_sql) 522 | -------------------------------------------------------------------------------- /cte_forest_test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Django CTE Trees test application. Use this application in an otherwise 35 | empty Django project in order to run unit tests for the cte_forest 36 | application. 37 | """ 38 | -------------------------------------------------------------------------------- /cte_forest_test/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This document is free and open-source software, subject to the OSI-approved 5 | # BSD license below. 6 | # 7 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 13 | # * Redistributions of source code must retain the above copyright notice, this 14 | # list of conditions and the following disclaimer. 15 | # 16 | # * Redistributions in binary form must reproduce the above copyright notice, 17 | # this list of conditions and the following disclaimer in the documentation 18 | # and/or other materials provided with the distribution. 19 | # 20 | # * Neither the name of the author nor the names of its contributors may be used 21 | # to endorse or promote products derived from this software without specific 22 | # prior written permission. 23 | # 24 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | """ Dummy Django application settings so unittest and Sphinx autodoc can work. 36 | """ 37 | 38 | # Python 39 | import os 40 | import sys 41 | 42 | if __name__ == "__main__": 43 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cte_forest_test.settings") 44 | from django.core.management import execute_from_command_line 45 | 46 | execute_from_command_line(sys.argv) 47 | -------------------------------------------------------------------------------- /cte_forest_test/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Models for the Django CTE Trees test application. 35 | """ 36 | 37 | from uuid import uuid4 38 | 39 | from django.db.models import ( 40 | CASCADE, 41 | CharField, 42 | DateField, 43 | FloatField, 44 | ForeignKey, 45 | Model, 46 | PositiveIntegerField, 47 | UUIDField, 48 | ) 49 | 50 | from cte_forest.models import CTENode, CTENodeManager 51 | 52 | 53 | class SimpleNode(CTENode, Model): 54 | 55 | pass 56 | 57 | 58 | class NoneDeleteNode(CTENode, Model): 59 | 60 | _cte_node_delete_method = "none" 61 | 62 | 63 | class SimpleNodeUser(Model): 64 | 65 | node = ForeignKey(SimpleNode, on_delete=CASCADE, null=False) 66 | 67 | 68 | class NamedNode(CTENode, Model): 69 | 70 | name = CharField(max_length=128, null=False) 71 | 72 | class Meta(object): 73 | 74 | abstract = True 75 | 76 | 77 | class SimpleNamedNode(NamedNode): 78 | 79 | pass 80 | 81 | 82 | class OrderedNamedNode(NamedNode): 83 | 84 | _cte_node_order_by = ["name"] 85 | 86 | 87 | class ValueNamedNode(NamedNode): 88 | 89 | v = PositiveIntegerField() 90 | 91 | 92 | class SimpleNamedNodeUser(Model): 93 | 94 | node = ForeignKey(SimpleNamedNode, on_delete=CASCADE, null=False) 95 | 96 | name = CharField(max_length=128, null=False) 97 | 98 | 99 | class DFSOrderedNode(CTENode, Model): 100 | 101 | v = PositiveIntegerField() 102 | 103 | _cte_node_traversal = "dfs" 104 | 105 | _cte_node_order_by = ["v"] 106 | 107 | 108 | class BFSOrderedNode(CTENode, Model): 109 | 110 | v = PositiveIntegerField() 111 | 112 | _cte_node_traversal = "bfs" 113 | 114 | _cte_node_order_by = ["v"] 115 | 116 | 117 | class NoneTraversalNode(CTENode, Model): 118 | 119 | v = PositiveIntegerField() 120 | 121 | _cte_node_traversal = "none" 122 | 123 | _cte_node_order_by = ["v"] 124 | 125 | 126 | class TypeCoercionNode(CTENode, Model): 127 | 128 | name = CharField(max_length=128, null=False) 129 | 130 | v = PositiveIntegerField() 131 | 132 | _cte_node_order_by = [("v", "text"), "name"] 133 | 134 | 135 | class TypeCombinationNode(CTENode, Model): 136 | 137 | v1 = PositiveIntegerField() 138 | 139 | v2 = FloatField() 140 | 141 | _cte_node_traversal = "bfs" 142 | 143 | _cte_node_order_by = [("v1", "float"), "v2"] 144 | 145 | 146 | class ExoticTypeNode(CTENode, Model): 147 | 148 | v = DateField() 149 | 150 | y = DateField(null=True) 151 | 152 | _cte_node_order_by = ["v"] 153 | 154 | 155 | class DBTypeNode(CTENode, Model): 156 | 157 | v = UUIDField(default=uuid4) 158 | 159 | _cte_node_order_by = ["v"] 160 | 161 | 162 | class CustomPrimaryKeyNode(CTENode, Model): 163 | 164 | id = CharField(max_length=128, primary_key=True) 165 | 166 | 167 | class DBTypePrimaryKeyNode(CTENode, Model): 168 | 169 | id = UUIDField(primary_key=True, default=uuid4) 170 | 171 | 172 | class AggregationNode(CTENode, Model): 173 | 174 | price = PositiveIntegerField() 175 | 176 | 177 | class BadParameter_parent_1_Node(CTENode, Model): 178 | 179 | _cte_node_parent = "wrong" 180 | 181 | 182 | class ArbitraryNode(CTENode, Model): 183 | 184 | pass 185 | 186 | 187 | class BadParameter_parent_2_Node(CTENode, Model): 188 | 189 | wrong = ForeignKey(ArbitraryNode, on_delete=CASCADE, null=True) 190 | 191 | _cte_node_parent = "wrong" 192 | 193 | 194 | class ArbitraryModel(Model): 195 | 196 | pass 197 | 198 | 199 | class BadParameter_parent_3_Node(CTENode, Model): 200 | 201 | wrong = ForeignKey(ArbitraryModel, on_delete=CASCADE, null=True) 202 | 203 | _cte_node_parent = "wrong" 204 | 205 | 206 | class BadParameter_parent_4_Node(Model): 207 | 208 | objects = CTENodeManager() 209 | 210 | 211 | class BadParameter_traversal_Node(CTENode, Model): 212 | 213 | _cte_node_traversal = "wrong" 214 | 215 | 216 | class BadParameter_delete_Node(CTENode, Model): 217 | 218 | _cte_node_delete_method = "wrong" 219 | -------------------------------------------------------------------------------- /cte_forest_test/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Dummy Django application settings so unittest and Sphinx autodoc can work. 35 | """ 36 | 37 | DEBUG = True 38 | 39 | SECRET_KEY = "dummy" 40 | 41 | INSTALLED_APPS = ("cte_forest", "cte_forest_test") 42 | 43 | DATABASES = { 44 | "default": {"ENGINE": "django.db.backends.postgresql_psycopg2", "NAME": "dummy"} 45 | } 46 | -------------------------------------------------------------------------------- /cte_forest_test/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Unit tests for the Django CTE Trees test application. 35 | """ 36 | 37 | from datetime import date, timedelta 38 | from uuid import UUID 39 | 40 | from django.core.exceptions import FieldError, ImproperlyConfigured 41 | from django.db.models import Avg, F 42 | from django.test import TestCase 43 | 44 | from cte_forest_test.models import ( 45 | AggregationNode, 46 | BadParameter_delete_Node, 47 | BadParameter_parent_1_Node, 48 | BadParameter_parent_2_Node, 49 | BadParameter_parent_3_Node, 50 | BadParameter_parent_4_Node, 51 | BadParameter_traversal_Node, 52 | BFSOrderedNode, 53 | CustomPrimaryKeyNode, 54 | DBTypeNode, 55 | DBTypePrimaryKeyNode, 56 | DFSOrderedNode, 57 | ExoticTypeNode, 58 | NoneDeleteNode, 59 | NoneTraversalNode, 60 | OrderedNamedNode, 61 | SimpleNamedNode, 62 | SimpleNamedNodeUser, 63 | SimpleNode, 64 | SimpleNodeUser, 65 | TypeCoercionNode, 66 | TypeCombinationNode, 67 | ValueNamedNode, 68 | ) 69 | 70 | 71 | class SimpleNodeTest(TestCase): 72 | def test_node_creation(self): 73 | 74 | node = SimpleNode.objects.create() 75 | 76 | self.assertEqual(SimpleNode.objects.get().id, node.id) 77 | self.assertEqual(len(SimpleNode.objects.all()), 1) 78 | self.assertEqual(len(SimpleNode.objects.filter()), 1) 79 | self.assertEqual(SimpleNode.objects.count(), 1) 80 | 81 | def test_node_creation_save(self): 82 | 83 | node = SimpleNode() 84 | node.save() 85 | 86 | self.assertEqual(SimpleNode.objects.get().id, node.id) 87 | self.assertEqual(len(SimpleNode.objects.all()), 1) 88 | self.assertEqual(len(SimpleNode.objects.filter()), 1) 89 | self.assertEqual(SimpleNode.objects.count(), 1) 90 | 91 | def test_multiple_node_creation(self): 92 | 93 | SimpleNode.objects.create() 94 | SimpleNode.objects.create() 95 | 96 | self.assertEqual(len(SimpleNode.objects.filter()), 2) 97 | self.assertEqual(SimpleNode.objects.count(), 2) 98 | 99 | def test_node_save(self): 100 | 101 | node = SimpleNode.objects.create() 102 | node.save() 103 | 104 | self.assertEqual(node.id, SimpleNode.objects.get().id) 105 | self.assertEqual(len(SimpleNode.objects.all()), 1) 106 | self.assertEqual(len(SimpleNode.objects.filter()), 1) 107 | self.assertEqual(SimpleNode.objects.count(), 1) 108 | 109 | def test_node_delete(self): 110 | 111 | node = SimpleNode.objects.create() 112 | 113 | node.delete() 114 | 115 | self.assertEqual(len(SimpleNode.objects.filter()), 0) 116 | self.assertEqual(SimpleNode.objects.count(), 0) 117 | 118 | def test_node_delete_query(self): 119 | 120 | SimpleNode.objects.create() 121 | 122 | SimpleNode.objects.filter().delete() 123 | 124 | self.assertEqual(len(SimpleNode.objects.filter()), 0) 125 | self.assertEqual(SimpleNode.objects.count(), 0) 126 | 127 | def test_tree_structure(self): 128 | 129 | root_node = SimpleNode.objects.create() 130 | middle_node = SimpleNode.objects.create(parent=root_node) 131 | bottom_node = SimpleNode.objects.create(parent=middle_node) 132 | 133 | fresh_root_node = SimpleNode.objects.get(id=root_node.id) 134 | fresh_middle_node = SimpleNode.objects.get(id=middle_node.id) 135 | fresh_bottom_node = SimpleNode.objects.get(id=bottom_node.id) 136 | 137 | self.assertEqual(fresh_root_node.depth, 1) 138 | self.assertEqual(fresh_root_node.path, [root_node.id]) 139 | self.assertEqual(fresh_root_node.ordering, [root_node.id]) 140 | 141 | self.assertEqual(fresh_middle_node.depth, 2) 142 | self.assertEqual(fresh_middle_node.path, [root_node.id, middle_node.id]) 143 | self.assertEqual(fresh_middle_node.ordering, [root_node.id, middle_node.id]) 144 | 145 | self.assertEqual(fresh_bottom_node.depth, 3) 146 | self.assertEqual( 147 | fresh_bottom_node.path, [root_node.id, middle_node.id, bottom_node.id] 148 | ) 149 | self.assertEqual( 150 | fresh_bottom_node.ordering, [root_node.id, middle_node.id, bottom_node.id] 151 | ) 152 | 153 | def test_node_roots(self): 154 | 155 | root_node_1 = SimpleNode.objects.create() 156 | root_node_2 = SimpleNode.objects.create() 157 | SimpleNode.objects.create(parent=root_node_1) 158 | SimpleNode.objects.create(parent=root_node_2) 159 | 160 | self.assertEqual(len(SimpleNode.objects.roots()), 2) 161 | self.assertEqual(SimpleNode.objects.roots().count(), 2) 162 | 163 | root_node_pks = [root_node_1.pk, root_node_2.pk] 164 | for node in SimpleNode.objects.roots(): 165 | self.assertTrue(node.pk in root_node_pks) 166 | 167 | def test_tree_structure_leaves(self): 168 | 169 | root_node = SimpleNode.objects.create() 170 | middle_node = SimpleNode.objects.create(parent=root_node) 171 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 172 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 173 | bottom_node_3 = SimpleNode.objects.create(parent=middle_node) 174 | 175 | self.assertEqual( 176 | [bottom_node_1.id, bottom_node_2.id, bottom_node_3.id], 177 | [node.id for node in SimpleNode.objects.leaves()], 178 | ) 179 | 180 | def test_tree_structure_branches(self): 181 | 182 | root_node = SimpleNode.objects.create() 183 | middle_node = SimpleNode.objects.create(parent=root_node) 184 | SimpleNode.objects.create(parent=middle_node) 185 | SimpleNode.objects.create(parent=middle_node) 186 | SimpleNode.objects.create(parent=middle_node) 187 | 188 | self.assertEqual( 189 | {root_node.id, middle_node.id}, 190 | {node.id for node in SimpleNode.objects.branches()}, 191 | ) 192 | 193 | def test_tree_structure_parent(self): 194 | 195 | root_node = SimpleNode.objects.create() 196 | middle_node = SimpleNode.objects.create(parent=root_node) 197 | bottom_node = SimpleNode.objects.create(parent=middle_node) 198 | 199 | self.assertEqual(root_node.parent, None) 200 | self.assertEqual(middle_node.parent.id, root_node.id) 201 | self.assertEqual(bottom_node.parent.id, middle_node.id) 202 | self.assertEqual(bottom_node.parent.parent.id, root_node.id) 203 | 204 | def test_tree_structure_children(self): 205 | 206 | root_node = SimpleNode.objects.create() 207 | middle_node = SimpleNode.objects.create(parent=root_node) 208 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 209 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 210 | bottom_node_3 = SimpleNode.objects.create(parent=middle_node) 211 | 212 | self.assertEqual(root_node.children.all()[0].id, middle_node.id) 213 | 214 | self.assertEqual( 215 | [node.pk for node in middle_node.children.all()], 216 | [bottom_node_1.pk, bottom_node_2.pk, bottom_node_3.pk], 217 | ) 218 | 219 | self.assertEqual(len(bottom_node_1.children.all()), 0) 220 | 221 | def test_tree_structure_siblings(self): 222 | 223 | root_node = SimpleNode.objects.create() 224 | middle_node_1 = SimpleNode.objects.create(parent=root_node) 225 | middle_node_2 = SimpleNode.objects.create(parent=root_node) 226 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node_1) 227 | bottom_node_2_1 = SimpleNode.objects.create(parent=middle_node_2) 228 | bottom_node_2_2 = SimpleNode.objects.create(parent=middle_node_2) 229 | 230 | self.assertEqual(len(root_node.siblings()), 0) 231 | self.assertEqual(root_node.siblings().count(), 0) 232 | 233 | self.assertEqual(middle_node_1.siblings()[0].id, middle_node_2.id) 234 | self.assertEqual(middle_node_2.siblings()[0].id, middle_node_1.id) 235 | 236 | self.assertEqual(len(bottom_node_1.siblings()), 0) 237 | self.assertEqual(bottom_node_1.siblings().count(), 0) 238 | 239 | self.assertEqual(bottom_node_2_1.siblings()[0].id, bottom_node_2_2.id) 240 | self.assertEqual(bottom_node_2_2.siblings()[0].id, bottom_node_2_1.id) 241 | 242 | def test_tree_structure_descendants(self): 243 | 244 | root_node = SimpleNode.objects.create() 245 | middle_node = SimpleNode.objects.create(parent=root_node) 246 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 247 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 248 | bottom_node_3 = SimpleNode.objects.create(parent=middle_node) 249 | 250 | self.assertEqual( 251 | [node.pk for node in root_node.descendants()], 252 | [middle_node.pk, bottom_node_1.pk, bottom_node_2.pk, bottom_node_3.pk], 253 | ) 254 | 255 | self.assertEqual( 256 | [node.pk for node in middle_node.descendants()], 257 | [bottom_node_1.pk, bottom_node_2.pk, bottom_node_3.pk], 258 | ) 259 | 260 | self.assertEqual(len(bottom_node_1.descendants()), 0) 261 | 262 | def test_tree_structure_ancestors(self): 263 | 264 | root_node = SimpleNode.objects.create() 265 | middle_node = SimpleNode.objects.create(parent=root_node) 266 | bottom_node = SimpleNode.objects.create(parent=middle_node) 267 | fresh_root_node = SimpleNode.objects.get(id=root_node.id) 268 | fresh_middle_node = SimpleNode.objects.get(id=middle_node.id) 269 | fresh_bottom_node = SimpleNode.objects.get(id=bottom_node.id) 270 | 271 | self.assertEqual(len(fresh_root_node.ancestors()), 0) 272 | 273 | self.assertEqual(fresh_root_node.ancestors().count(), 0) 274 | 275 | self.assertEqual(fresh_middle_node.ancestors()[0].id, root_node.id) 276 | 277 | self.assertEqual( 278 | [node.pk for node in fresh_bottom_node.ancestors()], 279 | [root_node.id, middle_node.id], 280 | ) 281 | 282 | def test_tree_structure_root(self): 283 | 284 | root_node = SimpleNode.objects.create() 285 | middle_node = SimpleNode.objects.create(parent=root_node) 286 | bottom_node = SimpleNode.objects.create(parent=middle_node) 287 | 288 | fresh_root_node = SimpleNode.objects.get(id=root_node.id) 289 | fresh_middle_node = SimpleNode.objects.get(id=middle_node.id) 290 | fresh_bottom_node = SimpleNode.objects.get(id=bottom_node.id) 291 | 292 | self.assertEqual(fresh_root_node.root().id, root_node.id) 293 | self.assertEqual(fresh_middle_node.root().id, root_node.id) 294 | self.assertEqual(fresh_bottom_node.root().id, root_node.id) 295 | 296 | def test_tree_structure_intersection(self): 297 | 298 | root_node = SimpleNode.objects.create() 299 | middle_node = SimpleNode.objects.create(parent=root_node) 300 | SimpleNode.objects.create(parent=middle_node) 301 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 302 | SimpleNode.objects.create(parent=middle_node) 303 | 304 | # We need the path information to get ancestors. 305 | fresh_bottom_node_2 = SimpleNode.objects.get(id=bottom_node_2.id) 306 | q = fresh_bottom_node_2.ancestors() & root_node.descendants() 307 | self.assertEqual(middle_node.id, q[0].id) 308 | 309 | def test_tree_structure_is_child_of(self): 310 | 311 | root_node = SimpleNode.objects.create() 312 | middle_node = SimpleNode.objects.create(parent=root_node) 313 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 314 | SimpleNode.objects.create(parent=middle_node) 315 | SimpleNode.objects.create(parent=middle_node) 316 | 317 | self.assertTrue(middle_node.is_child_of(root_node)) 318 | self.assertFalse(bottom_node_1.is_child_of(root_node)) 319 | 320 | def test_tree_structure_is_parent_of(self): 321 | 322 | root_node = SimpleNode.objects.create() 323 | middle_node = SimpleNode.objects.create(parent=root_node) 324 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 325 | SimpleNode.objects.create(parent=middle_node) 326 | SimpleNode.objects.create(parent=middle_node) 327 | 328 | self.assertTrue(root_node.is_parent_of(middle_node)) 329 | self.assertFalse(root_node.is_parent_of(bottom_node_1)) 330 | 331 | def test_tree_structure_is_sibling_of(self): 332 | 333 | root_node = SimpleNode.objects.create() 334 | another_root_node = SimpleNode.objects.create() 335 | middle_node = SimpleNode.objects.create(parent=root_node) 336 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 337 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 338 | SimpleNode.objects.create(parent=middle_node) 339 | 340 | self.assertTrue(bottom_node_1.is_sibling_of(bottom_node_2)) 341 | self.assertTrue(bottom_node_2.is_sibling_of(bottom_node_1)) 342 | 343 | # Ensure edge case when parents are None. 344 | self.assertTrue(root_node.is_sibling_of(another_root_node)) 345 | 346 | self.assertFalse(bottom_node_1.is_sibling_of(middle_node)) 347 | self.assertFalse(bottom_node_1.is_sibling_of(root_node)) 348 | 349 | # Ensure edge case when compared to self. 350 | self.assertFalse(bottom_node_1.is_sibling_of(bottom_node_1)) 351 | self.assertFalse(middle_node.is_sibling_of(middle_node)) 352 | 353 | def test_tree_structure_is_descendant_of(self): 354 | 355 | root_node = SimpleNode.objects.create() 356 | middle_node = SimpleNode.objects.create(parent=root_node) 357 | bottom_node = SimpleNode.objects.create(parent=middle_node) 358 | 359 | fresh_root_node = SimpleNode.objects.get(id=root_node.id) 360 | fresh_middle_node = SimpleNode.objects.get(id=middle_node.id) 361 | fresh_bottom_node = SimpleNode.objects.get(id=bottom_node.id) 362 | 363 | self.assertTrue(fresh_middle_node.is_descendant_of(fresh_root_node)) 364 | self.assertFalse(fresh_root_node.is_descendant_of(fresh_middle_node)) 365 | 366 | self.assertTrue(fresh_bottom_node.is_descendant_of(fresh_root_node)) 367 | self.assertFalse(fresh_root_node.is_descendant_of(fresh_bottom_node)) 368 | 369 | self.assertTrue(fresh_bottom_node.is_descendant_of(fresh_middle_node)) 370 | self.assertFalse(fresh_middle_node.is_descendant_of(fresh_bottom_node)) 371 | 372 | self.assertFalse(fresh_root_node.is_descendant_of(fresh_root_node)) 373 | self.assertFalse(fresh_middle_node.is_descendant_of(fresh_middle_node)) 374 | self.assertFalse(fresh_bottom_node.is_descendant_of(fresh_bottom_node)) 375 | 376 | def test_tree_structure_is_descendant_of_none(self): 377 | 378 | root_node = SimpleNode.objects.create() 379 | middle_node = SimpleNode.objects.create(parent=root_node) 380 | 381 | fresh_root_node = SimpleNode.objects.get(id=root_node.id) 382 | fresh_middle_node = SimpleNode.objects.get(id=middle_node.id) 383 | 384 | self.assertTrue(fresh_middle_node.is_descendant_of(fresh_root_node)) 385 | 386 | self.assertFalse(fresh_middle_node.is_descendant_of(None)) 387 | 388 | def test_tree_structure_is_ancestor_of(self): 389 | 390 | root_node = SimpleNode.objects.create() 391 | middle_node = SimpleNode.objects.create(parent=root_node) 392 | bottom_node = SimpleNode.objects.create(parent=middle_node) 393 | 394 | fresh_root_node = SimpleNode.objects.get(id=root_node.id) 395 | fresh_middle_node = SimpleNode.objects.get(id=middle_node.id) 396 | fresh_bottom_node = SimpleNode.objects.get(id=bottom_node.id) 397 | 398 | self.assertFalse(fresh_middle_node.is_ancestor_of(fresh_root_node)) 399 | self.assertTrue(fresh_root_node.is_ancestor_of(fresh_middle_node)) 400 | 401 | self.assertFalse(fresh_bottom_node.is_ancestor_of(fresh_root_node)) 402 | self.assertTrue(fresh_root_node.is_ancestor_of(fresh_bottom_node)) 403 | 404 | self.assertFalse(fresh_bottom_node.is_ancestor_of(fresh_middle_node)) 405 | self.assertTrue(fresh_middle_node.is_ancestor_of(fresh_bottom_node)) 406 | 407 | self.assertFalse(fresh_root_node.is_ancestor_of(fresh_root_node)) 408 | self.assertFalse(fresh_middle_node.is_ancestor_of(fresh_middle_node)) 409 | self.assertFalse(fresh_bottom_node.is_ancestor_of(fresh_bottom_node)) 410 | 411 | def test_tree_structure_is_ancestor_of_none(self): 412 | 413 | root_node = SimpleNode.objects.create() 414 | middle_node = SimpleNode.objects.create(parent=root_node) 415 | 416 | fresh_root_node = SimpleNode.objects.get(id=root_node.id) 417 | fresh_middle_node = SimpleNode.objects.get(id=middle_node.id) 418 | 419 | self.assertTrue(fresh_root_node.is_ancestor_of(fresh_middle_node)) 420 | self.assertFalse(fresh_root_node.is_ancestor_of(None)) 421 | 422 | def test_tree_structure_is_leaf(self): 423 | 424 | root_node = SimpleNode.objects.create() 425 | middle_node = SimpleNode.objects.create(parent=root_node) 426 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 427 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 428 | bottom_node_3 = SimpleNode.objects.create(parent=middle_node) 429 | 430 | self.assertFalse(root_node.is_leaf()) 431 | self.assertFalse(middle_node.is_leaf()) 432 | self.assertTrue(bottom_node_1.is_leaf()) 433 | self.assertTrue(bottom_node_2.is_leaf()) 434 | self.assertTrue(bottom_node_3.is_leaf()) 435 | 436 | def test_tree_structure_is_branch(self): 437 | 438 | root_node = SimpleNode.objects.create() 439 | middle_node = SimpleNode.objects.create(parent=root_node) 440 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 441 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 442 | bottom_node_3 = SimpleNode.objects.create(parent=middle_node) 443 | 444 | self.assertTrue(root_node.is_branch()) 445 | self.assertTrue(middle_node.is_branch()) 446 | self.assertFalse(bottom_node_1.is_branch()) 447 | self.assertFalse(bottom_node_2.is_branch()) 448 | self.assertFalse(bottom_node_3.is_branch()) 449 | 450 | def test_tree_attribute_path(self): 451 | 452 | root_node = SimpleNamedNode.objects.create(name="root") 453 | middle_node = SimpleNamedNode.objects.create(parent=root_node, name="middle") 454 | SimpleNamedNode.objects.create(parent=middle_node, name="bottom") 455 | 456 | fresh_root_node = SimpleNamedNode.objects.get(name="root") 457 | fresh_middle_node = SimpleNamedNode.objects.get(name="middle") 458 | fresh_bottom_node = SimpleNamedNode.objects.get(name="bottom") 459 | 460 | self.assertEqual(["root"], fresh_root_node.attribute_path("name")) 461 | self.assertEqual(["root", "middle"], fresh_middle_node.attribute_path("name")) 462 | self.assertEqual( 463 | ["root", "middle", "bottom"], fresh_bottom_node.attribute_path("name") 464 | ) 465 | 466 | self.assertEqual(["foo"], fresh_root_node.attribute_path("bar", missing="foo")) 467 | 468 | def custom_visitor(node, attribute): 469 | value = getattr(node, attribute, None) 470 | if value == "middle": 471 | return "foo" 472 | if value == "bottom": 473 | return None 474 | return value 475 | 476 | self.assertEqual( 477 | ["root", "foo", "bar"], 478 | fresh_bottom_node.attribute_path( 479 | "name", missing="bar", visitor=custom_visitor 480 | ), 481 | ) 482 | 483 | def test_tree_structure_as_tree(self): 484 | 485 | root_node = OrderedNamedNode.objects.create(name="root") 486 | middle_node = OrderedNamedNode.objects.create(parent=root_node, name="middle") 487 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 3") 488 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 1") 489 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 2") 490 | OrderedNamedNode.objects.create(name="root other") 491 | 492 | OrderedNamedNode.objects.get(name="root") 493 | fresh_middle_node = OrderedNamedNode.objects.get(name="middle") 494 | OrderedNamedNode.objects.get(name="bottom 1") 495 | fresh_bottom_node_2 = OrderedNamedNode.objects.get(name="bottom 2") 496 | OrderedNamedNode.objects.get(name="bottom 3") 497 | OrderedNamedNode.objects.get(name="root other") 498 | 499 | # get the forest 500 | tree = OrderedNamedNode.objects.as_tree() 501 | self.assertEqual(len(tree), 2) 502 | self.assertEqual(len(tree[0]["children"]), 1) 503 | self.assertEqual(len(tree[1]["children"]), 0) 504 | self.assertEqual( 505 | tree[0]["children"][0]["children"][1]["node"].id, fresh_bottom_node_2.id 506 | ) 507 | 508 | # get a specific tree 509 | middle_tree = fresh_middle_node.as_tree() 510 | self.assertEqual(middle_tree["node"].id, middle_node.id) 511 | self.assertFalse(middle_tree["leaf"]) 512 | self.assertTrue(middle_tree["branch"]) 513 | self.assertEqual(middle_tree["depth"], 2) 514 | self.assertEqual(middle_tree["ordering"], ["root", "middle"]) 515 | self.assertEqual(middle_tree["path"], [str(root_node.id), str(middle_node.id)]) 516 | self.assertEqual(len(middle_tree["children"]), 3) 517 | self.assertEqual(middle_tree["children"][1]["node"].id, fresh_bottom_node_2.id) 518 | 519 | def test_tree_structure_as_tree_custom(self): 520 | 521 | root_node = OrderedNamedNode.objects.create(name="root") 522 | middle_node = OrderedNamedNode.objects.create(parent=root_node, name="middle") 523 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 3") 524 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 1") 525 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 2") 526 | OrderedNamedNode.objects.create(name="root other") 527 | 528 | OrderedNamedNode.objects.get(name="root") 529 | fresh_middle_node = OrderedNamedNode.objects.get(name="middle") 530 | OrderedNamedNode.objects.get(name="bottom 1") 531 | OrderedNamedNode.objects.get(name="bottom 2") 532 | fresh_bottom_node_3 = OrderedNamedNode.objects.get(name="bottom 3") 533 | OrderedNamedNode.objects.get(name="root other") 534 | 535 | def custom_visitor(manager, node): 536 | tree = manager._default_node_visitor(node) 537 | tree["person"] = tree["node"] 538 | del tree["node"] 539 | return tree 540 | 541 | def custom_children(manager, node, visitor, children): 542 | if node.name == "bottom 2": 543 | return {} 544 | return { 545 | "offspring": [ 546 | child.as_tree(visitor=visitor, children=children) 547 | for child in node.children.exclude(name="bottom 2") 548 | ] 549 | } 550 | 551 | # get the forest 552 | tree = OrderedNamedNode.objects.as_tree( 553 | visitor=custom_visitor, children=custom_children 554 | ) 555 | self.assertEqual(len(tree), 2) 556 | self.assertEqual(len(tree[0]["offspring"]), 1) 557 | self.assertEqual(len(tree[1]["offspring"]), 0) 558 | self.assertEqual( 559 | tree[0]["offspring"][0]["offspring"][1]["person"].id, fresh_bottom_node_3.id 560 | ) 561 | 562 | # get a specific tree 563 | middle_tree = fresh_middle_node.as_tree( 564 | visitor=custom_visitor, children=custom_children 565 | ) 566 | self.assertEqual(middle_tree["person"].id, middle_node.id) 567 | self.assertFalse(middle_tree["leaf"]) 568 | self.assertTrue(middle_tree["branch"]) 569 | self.assertEqual(middle_tree["depth"], 2) 570 | self.assertEqual(middle_tree["ordering"], ["root", "middle"]) 571 | self.assertEqual(middle_tree["path"], [str(root_node.id), str(middle_node.id)]) 572 | self.assertEqual(len(middle_tree["offspring"]), 2) 573 | self.assertEqual( 574 | middle_tree["offspring"][1]["person"].id, fresh_bottom_node_3.id 575 | ) 576 | 577 | def test_tree_drilldown(self): 578 | 579 | # check empty forest (no roots, empty path) 580 | self.assertRaises( 581 | SimpleNamedNode.DoesNotExist, 582 | lambda: SimpleNamedNode.objects.drilldown(("name",), []), 583 | ) 584 | 585 | root_node = SimpleNamedNode.objects.create(name="root") 586 | middle_node = SimpleNamedNode.objects.create(parent=root_node, name="middle") 587 | SimpleNamedNode.objects.create(parent=middle_node, name="bottom") 588 | 589 | fresh_root_node = SimpleNamedNode.objects.get(name="root") 590 | SimpleNamedNode.objects.get(name="middle") 591 | fresh_bottom_node = SimpleNamedNode.objects.get(name="bottom") 592 | 593 | # check missing path component 594 | self.assertRaises( 595 | SimpleNamedNode.DoesNotExist, 596 | lambda: SimpleNamedNode.objects.drilldown(("name",), [("root",), ("xxx")]), 597 | ) 598 | 599 | # check missing root component 600 | self.assertRaises( 601 | SimpleNamedNode.DoesNotExist, 602 | lambda: SimpleNamedNode.objects.drilldown(("name",), [("xxx",), ("xxx")]), 603 | ) 604 | 605 | # check success 606 | self.assertEqual( 607 | fresh_bottom_node, 608 | SimpleNamedNode.objects.drilldown( 609 | ("name",), [("root",), ("middle",), ("bottom",)] 610 | ), 611 | ) 612 | 613 | # check empty path success 614 | self.assertEqual( 615 | fresh_root_node, SimpleNamedNode.objects.drilldown(("name",), []) 616 | ) 617 | 618 | # check extraneous path component 619 | self.assertRaises( 620 | SimpleNamedNode.DoesNotExist, 621 | lambda: SimpleNamedNode.objects.drilldown( 622 | ("name",), [("root",), ("middle",), ("bottom",), ("xxx",)] 623 | ), 624 | ) 625 | 626 | def test_tree_drilldown_complex_filtering(self): 627 | 628 | root_node = ValueNamedNode.objects.create(name="root", v=5) 629 | middle_node = ValueNamedNode.objects.create( 630 | parent=root_node, name="middle", v=5 631 | ) 632 | ValueNamedNode.objects.create(parent=middle_node, name="xxx bottom 1", v=7) 633 | ValueNamedNode.objects.create(parent=middle_node, name="bottom 2", v=1) 634 | ValueNamedNode.objects.create(parent=middle_node, name="bottom 3", v=6) 635 | 636 | ValueNamedNode.objects.get(name="root") 637 | ValueNamedNode.objects.get(name="middle") 638 | ValueNamedNode.objects.get(name="xxx bottom 1") 639 | ValueNamedNode.objects.get(name="bottom 2") 640 | fresh_bottom_node_3 = ValueNamedNode.objects.get(name="bottom 3") 641 | 642 | self.assertEqual( 643 | fresh_bottom_node_3, 644 | ValueNamedNode.objects.drilldown( 645 | ("name__startswith", "v__gte"), 646 | [("root", 5), ("middle", 5), ("bottom", 5)], 647 | ), 648 | ) 649 | 650 | def test_node_delete_pharaoh(self): 651 | 652 | root_node = SimpleNode.objects.create() 653 | middle_node = SimpleNode.objects.create(parent=root_node) 654 | SimpleNode.objects.create(parent=middle_node) 655 | SimpleNode.objects.create(parent=middle_node) 656 | SimpleNode.objects.create(parent=middle_node) 657 | 658 | middle_node.delete() 659 | 660 | self.assertEqual(len(SimpleNode.objects.all()), 1) 661 | self.assertEqual(SimpleNode.objects.count(), 1) 662 | self.assertEqual(SimpleNode.objects.get().id, root_node.id) 663 | 664 | def test_node_delete_grandmother(self): 665 | 666 | root_node = SimpleNode.objects.create() 667 | middle_node = SimpleNode.objects.create(parent=root_node) 668 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 669 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 670 | bottom_node_3 = SimpleNode.objects.create(parent=middle_node) 671 | 672 | middle_node.delete(method=SimpleNode.objects.DELETE_METHOD_GRANDMOTHER) 673 | 674 | self.assertEqual(len(SimpleNode.objects.all()), 4) 675 | self.assertEqual(SimpleNode.objects.count(), 4) 676 | self.assertEqual( 677 | [node.id for node in SimpleNode.objects.all()], 678 | [root_node.id, bottom_node_1.id, bottom_node_2.id, bottom_node_3.id], 679 | ) 680 | self.assertEqual(len(root_node.children.all()), 3) 681 | self.assertEqual(root_node.children.count(), 3) 682 | self.assertEqual([node.depth for node in root_node.children.all()], [2, 2, 2]) 683 | 684 | def test_node_delete_monarchy(self): 685 | 686 | root_node = SimpleNode.objects.create() 687 | middle_node = SimpleNode.objects.create(parent=root_node) 688 | bottom_node_1 = SimpleNode.objects.create(parent=middle_node) 689 | bottom_node_2 = SimpleNode.objects.create(parent=middle_node) 690 | bottom_node_3 = SimpleNode.objects.create(parent=middle_node) 691 | 692 | middle_node.delete(method=SimpleNode.objects.DELETE_METHOD_MONARCHY) 693 | 694 | fresh_bottom_node_1 = SimpleNode.objects.get(id=bottom_node_1.id) 695 | 696 | self.assertEqual(len(SimpleNode.objects.all()), 4) 697 | self.assertEqual(SimpleNode.objects.count(), 4) 698 | self.assertEqual( 699 | [node.id for node in SimpleNode.objects.all()], 700 | [root_node.id, bottom_node_1.id, bottom_node_2.id, bottom_node_3.id], 701 | ) 702 | self.assertEqual(len(root_node.children.all()), 1) 703 | self.assertEqual(root_node.children.count(), 1) 704 | self.assertEqual([node.depth for node in root_node.children.all()], [2]) 705 | 706 | self.assertEqual(fresh_bottom_node_1.depth, 2) 707 | self.assertEqual(fresh_bottom_node_1.children.count(), 2) 708 | self.assertEqual(len(fresh_bottom_node_1.children.all()), 2) 709 | self.assertEqual( 710 | [3, 3], [node.depth for node in fresh_bottom_node_1.children.all()] 711 | ) 712 | 713 | def test_node_delete_none(self): 714 | 715 | root_node = NoneDeleteNode.objects.create() 716 | middle_node = NoneDeleteNode.objects.create(parent=root_node) 717 | NoneDeleteNode.objects.create(parent=middle_node) 718 | NoneDeleteNode.objects.create(parent=middle_node) 719 | NoneDeleteNode.objects.create(parent=middle_node) 720 | 721 | middle_node.delete() 722 | 723 | self.assertEqual(len(NoneDeleteNode.objects.all()), 1) 724 | self.assertEqual(NoneDeleteNode.objects.count(), 1) 725 | self.assertEqual(NoneDeleteNode.objects.get().id, root_node.id) 726 | 727 | def test_node_move(self): 728 | 729 | root_node = SimpleNode.objects.create() 730 | middle_node = SimpleNode.objects.create(parent=root_node) 731 | SimpleNode.objects.create(parent=middle_node) 732 | SimpleNode.objects.create(parent=middle_node) 733 | bottom_node_3 = SimpleNode.objects.create(parent=middle_node) 734 | 735 | fresh_bottom_node_3 = SimpleNode.objects.get(id=bottom_node_3.id) 736 | 737 | self.assertEqual(fresh_bottom_node_3.depth, 3) 738 | 739 | bottom_node_3.move( 740 | root_node, position=lambda node, destination: None, save=True 741 | ) 742 | 743 | fresh_bottom_node_3 = SimpleNode.objects.get(id=bottom_node_3.id) 744 | 745 | self.assertEqual(fresh_bottom_node_3.depth, 2) 746 | 747 | 748 | class SimpleNodeErrorsTest(TestCase): 749 | def test_node_parameters_parent_1(self): 750 | 751 | # _cte_node_parent points to a non-existing field. 752 | self.assertRaises( 753 | ImproperlyConfigured, BadParameter_parent_1_Node.objects.create 754 | ) 755 | 756 | def test_node_parameters_parent_2(self): 757 | 758 | # Parent Foreign Key points to different CTENode Model. 759 | self.assertRaises( 760 | ImproperlyConfigured, BadParameter_parent_2_Node.objects.create 761 | ) 762 | 763 | def test_node_parameters_parent_3(self): 764 | 765 | # Parent Foreign Key points to arbitrary Model. 766 | self.assertRaises( 767 | ImproperlyConfigured, BadParameter_parent_3_Node.objects.create 768 | ) 769 | 770 | def test_node_parameters_parent_4(self): 771 | 772 | # Parent Foreign Key missing. 773 | self.assertRaises( 774 | ImproperlyConfigured, BadParameter_parent_4_Node.objects.create 775 | ) 776 | 777 | def test_node_parameters_traversal(self): 778 | 779 | self.assertRaises( 780 | ImproperlyConfigured, BadParameter_traversal_Node.objects.create 781 | ) 782 | 783 | def test_node_parameters_delete(self): 784 | 785 | self.assertRaises(ImproperlyConfigured, BadParameter_delete_Node.objects.create) 786 | 787 | def test_node_virtual_fields(self): 788 | 789 | root_node = SimpleNode.objects.create() 790 | 791 | def read_path(node): 792 | return node.path 793 | 794 | self.assertRaises(AttributeError, read_path, root_node) 795 | self.assertRaises(FieldError, root_node.ancestors) 796 | 797 | 798 | class SimpleNodeUsageTest(TestCase): 799 | def test_node_usage(self): 800 | 801 | root_node = SimpleNode.objects.create() 802 | middle_node = SimpleNode.objects.create(parent=root_node) 803 | SimpleNode.objects.create(parent=middle_node) 804 | SimpleNode.objects.create(parent=middle_node) 805 | SimpleNode.objects.create(parent=middle_node) 806 | 807 | SimpleNodeUser.objects.create(node=root_node) 808 | 809 | self.assertEqual(SimpleNodeUser.objects.get().node.id, root_node.id) 810 | 811 | 812 | class SimpleNamedNodeTest(TestCase): 813 | def test_node_creation(self): 814 | 815 | node = SimpleNamedNode.objects.create(name="root") 816 | 817 | self.assertEqual(SimpleNamedNode.objects.get().name, node.name) 818 | 819 | def test_user_creation(self): 820 | 821 | node = SimpleNamedNode.objects.create(name="root") 822 | user = SimpleNamedNodeUser.objects.create(node=node, name="root user") 823 | 824 | self.assertEqual(user.node.name, "root") 825 | 826 | def test_node_save(self): 827 | 828 | node = SimpleNamedNode.objects.create(name="amazing") 829 | self.assertEqual(SimpleNamedNode.objects.get().name, "amazing") 830 | node.name = "so and so" 831 | node.save() 832 | self.assertEqual(SimpleNamedNode.objects.get().name, "so and so") 833 | 834 | def test_ordering(self): 835 | 836 | root_node = SimpleNamedNode.objects.create(name="root") 837 | middle_node = SimpleNamedNode.objects.create(parent=root_node, name="middle") 838 | 839 | # Create these in mixed order to test ordering by name below. 840 | SimpleNamedNode.objects.create(parent=middle_node, name="bottom 2") 841 | SimpleNamedNode.objects.create(parent=middle_node, name="bottom 3") 842 | SimpleNamedNode.objects.create(parent=middle_node, name="bottom 1") 843 | 844 | # Order should be by primary key, not name. 845 | node_names = [node.name for node in SimpleNamedNode.objects.all()] 846 | self.assertEqual( 847 | node_names, ["root", "middle", "bottom 2", "bottom 3", "bottom 1"] 848 | ) 849 | 850 | # But if we override ordering, we can get back to flat name space. 851 | flat_node_names = [ 852 | node.name for node in SimpleNamedNode.objects.all().order_by("name") 853 | ] 854 | self.assertEqual( 855 | flat_node_names, ["bottom 1", "bottom 2", "bottom 3", "middle", "root"] 856 | ) 857 | 858 | def test_node_filter_chaining(self): 859 | 860 | root_node = SimpleNamedNode.objects.create(name="root") 861 | middle_node = SimpleNamedNode.objects.create(parent=root_node, name="middle") 862 | SimpleNamedNode.objects.create(parent=middle_node, name="bottom 1") 863 | SimpleNamedNode.objects.create(parent=middle_node, name="bottom 2") 864 | SimpleNamedNode.objects.create(parent=middle_node, name="bottom 3") 865 | 866 | self.assertEqual( 867 | "bottom 1", 868 | SimpleNamedNode.objects.filter(id__gt=1) 869 | .exclude(name="bottom 3") 870 | .filter(name__in=["bottom 3", "bottom 1"])[0] 871 | .name, 872 | ) 873 | 874 | 875 | class OrderedNamedNodeTest(TestCase): 876 | def test_node_creation(self): 877 | 878 | node = OrderedNamedNode.objects.create(name="root") 879 | fresh_node = OrderedNamedNode.objects.get() 880 | 881 | self.assertEqual(fresh_node.ordering, [node.name]) 882 | 883 | def test_simple_ordering(self): 884 | 885 | root_node = OrderedNamedNode.objects.create(name="root") 886 | middle_node = OrderedNamedNode.objects.create(parent=root_node, name="middle") 887 | 888 | # Create these in mixed order to test ordering by name below. 889 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 2") 890 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 3") 891 | OrderedNamedNode.objects.create(parent=middle_node, name="bottom 1") 892 | 893 | # Order should be by path name, not primary key. 894 | node_names = [node.name for node in OrderedNamedNode.objects.all()] 895 | self.assertEqual( 896 | ["root", "middle", "bottom 1", "bottom 2", "bottom 3"], node_names 897 | ) 898 | 899 | # But if we override ordering, we can get back to flat name space. 900 | flat_node_names = [ 901 | node.name for node in OrderedNamedNode.objects.all().order_by("name") 902 | ] 903 | self.assertEqual( 904 | ["bottom 1", "bottom 2", "bottom 3", "middle", "root"], flat_node_names 905 | ) 906 | 907 | 908 | class DFSOrderedNodeTest(TestCase): 909 | def test_ordering_dfs(self): 910 | 911 | root_node = DFSOrderedNode.objects.create(v=1) 912 | # The following two have different v values, but created in the 913 | # opposite order from which we are ordering. 914 | middle_node_1 = DFSOrderedNode.objects.create(parent=root_node, v=5) 915 | middle_node_2 = DFSOrderedNode.objects.create(parent=root_node, v=4) 916 | 917 | # The following two have the same v value. 918 | bottom_node_1_1 = DFSOrderedNode.objects.create(parent=middle_node_1, v=3) 919 | bottom_node_1_2 = DFSOrderedNode.objects.create(parent=middle_node_1, v=3) 920 | 921 | # The following have different v values, but created in 'reverse' order. 922 | bottom_node_2_1 = DFSOrderedNode.objects.create(parent=middle_node_2, v=5) 923 | bottom_node_2_2 = DFSOrderedNode.objects.create(parent=middle_node_2, v=4) 924 | 925 | expected_order = [ 926 | root_node.id, 927 | middle_node_2.id, 928 | bottom_node_2_2.id, 929 | bottom_node_2_1.id, 930 | middle_node_1.id, 931 | bottom_node_1_1.id, 932 | bottom_node_1_2.id, 933 | ] 934 | 935 | self.assertEqual( 936 | expected_order, [node.id for node in DFSOrderedNode.objects.all()] 937 | ) 938 | 939 | 940 | class BFSOrderedNodeTest(TestCase): 941 | def test_ordering_bfs(self): 942 | 943 | root_node = BFSOrderedNode.objects.create(v=1) 944 | # The following two have different v values, but created in the 945 | # opposite order from which we are ordering. 946 | middle_node_1 = BFSOrderedNode.objects.create(parent=root_node, v=5) 947 | middle_node_2 = BFSOrderedNode.objects.create(parent=root_node, v=4) 948 | 949 | # The following two have the same v value. 950 | bottom_node_1_1 = BFSOrderedNode.objects.create(parent=middle_node_1, v=3) 951 | bottom_node_1_2 = BFSOrderedNode.objects.create(parent=middle_node_1, v=3) 952 | 953 | # The following have different v values, but created in 'reverse' order. 954 | bottom_node_2_1 = BFSOrderedNode.objects.create(parent=middle_node_2, v=5) 955 | bottom_node_2_2 = BFSOrderedNode.objects.create(parent=middle_node_2, v=4) 956 | 957 | expected_order = [ 958 | root_node.id, 959 | middle_node_2.id, 960 | middle_node_1.id, 961 | bottom_node_2_2.id, 962 | bottom_node_2_1.id, 963 | bottom_node_1_1.id, 964 | bottom_node_1_2.id, 965 | ] 966 | 967 | self.assertEqual( 968 | expected_order, [node.id for node in BFSOrderedNode.objects.all()] 969 | ) 970 | 971 | 972 | class TraversalNodeTest(TestCase): 973 | def test_none_traversal_parameter(self): 974 | 975 | root_node = NoneTraversalNode.objects.create(v=1) 976 | # The following two have different v values, but created in the 977 | # opposite order from which we are ordering. 978 | middle_node_1 = NoneTraversalNode.objects.create(parent=root_node, v=5) 979 | middle_node_2 = NoneTraversalNode.objects.create(parent=root_node, v=4) 980 | 981 | # The following two have the same v value. 982 | bottom_node_1_1 = NoneTraversalNode.objects.create(parent=middle_node_1, v=3) 983 | bottom_node_1_2 = NoneTraversalNode.objects.create(parent=middle_node_1, v=3) 984 | 985 | # The following have different v values, but created in 'reverse' order. 986 | bottom_node_2_1 = NoneTraversalNode.objects.create(parent=middle_node_2, v=5) 987 | bottom_node_2_2 = NoneTraversalNode.objects.create(parent=middle_node_2, v=4) 988 | 989 | expected_order = [ 990 | root_node.id, 991 | middle_node_2.id, 992 | bottom_node_2_2.id, 993 | bottom_node_2_1.id, 994 | middle_node_1.id, 995 | bottom_node_1_1.id, 996 | bottom_node_1_2.id, 997 | ] 998 | 999 | self.assertEqual( 1000 | expected_order, [node.id for node in NoneTraversalNode.objects.all()] 1001 | ) 1002 | 1003 | 1004 | class TypeCoercionNodeTest(TestCase): 1005 | def test_type_coercion(self): 1006 | 1007 | # Note: this tests in DFS. We will order on a VARCHAR, which will be 1008 | # automatically cast into TEXT, and also on an int, which we will have 1009 | # to explicitly cast into TEXT in order to combine with the VARCHAR. 1010 | 1011 | root_node = TypeCoercionNode.objects.create(v=1, name="") 1012 | # The following two have different v values, but created in the 1013 | # opposite order from which we are ordering. 1014 | middle_node_1 = TypeCoercionNode.objects.create( 1015 | parent=root_node, v=5, name="foo" 1016 | ) 1017 | middle_node_2 = TypeCoercionNode.objects.create( 1018 | parent=root_node, v=4, name="foo" 1019 | ) 1020 | 1021 | # The following two have the same v value. 1022 | bottom_node_1_1 = TypeCoercionNode.objects.create( 1023 | parent=middle_node_1, v=3, name="b" 1024 | ) 1025 | bottom_node_1_2 = TypeCoercionNode.objects.create( 1026 | parent=middle_node_1, v=3, name="a" 1027 | ) 1028 | 1029 | # The following two have the same v and name values, so the order in 1030 | # they will be returned is defined by the database. 1031 | bottom_node_2_1 = TypeCoercionNode.objects.create( 1032 | parent=middle_node_2, v=4, name="foo" 1033 | ) 1034 | bottom_node_2_2 = TypeCoercionNode.objects.create( 1035 | parent=middle_node_2, v=4, name="foo" 1036 | ) 1037 | 1038 | expected_order = [ 1039 | root_node.id, 1040 | middle_node_2.id, 1041 | bottom_node_2_2.id, 1042 | bottom_node_2_1.id, 1043 | middle_node_1.id, 1044 | bottom_node_1_2.id, 1045 | bottom_node_1_1.id, 1046 | ] 1047 | 1048 | self.assertEqual( 1049 | expected_order, [node.id for node in TypeCoercionNode.objects.all()] 1050 | ) 1051 | 1052 | 1053 | class TypeCombinationNodeTest(TestCase): 1054 | def test_type_combination(self): 1055 | 1056 | # Note: this tests in BFS. 1057 | 1058 | root_node = TypeCombinationNode.objects.create(v1=1, v2=3.2) 1059 | 1060 | # The following two have different v1 values, but created in the 1061 | # opposite order from which we are ordering. 1062 | middle_node_1 = TypeCombinationNode.objects.create( 1063 | parent=root_node, v1=5, v2=3.7 1064 | ) 1065 | middle_node_2 = TypeCombinationNode.objects.create( 1066 | parent=root_node, v1=4, v2=8.9 1067 | ) 1068 | 1069 | # The following two have the same v1 value. 1070 | bottom_node_1_1 = TypeCombinationNode.objects.create( 1071 | parent=middle_node_1, v1=3, v2=1.5 1072 | ) 1073 | bottom_node_1_2 = TypeCombinationNode.objects.create( 1074 | parent=middle_node_1, v1=3, v2=1.4 1075 | ) 1076 | 1077 | # The following two have the same v1 and v2 values, so the order in 1078 | # they will be returned is defined by the database. 1079 | bottom_node_2_1 = TypeCombinationNode.objects.create( 1080 | parent=middle_node_2, v1=4, v2=4 1081 | ) 1082 | bottom_node_2_2 = TypeCombinationNode.objects.create( 1083 | parent=middle_node_2, v1=4, v2=5 1084 | ) 1085 | 1086 | expected_order = [ 1087 | root_node.id, 1088 | middle_node_2.id, 1089 | middle_node_1.id, 1090 | bottom_node_2_1.id, 1091 | bottom_node_2_2.id, 1092 | bottom_node_1_2.id, 1093 | bottom_node_1_1.id, 1094 | ] 1095 | 1096 | self.assertEqual( 1097 | expected_order, [node.id for node in TypeCombinationNode.objects.all()] 1098 | ) 1099 | 1100 | 1101 | class ExoticTypeNodeTest(TestCase): 1102 | def test_exotic_type(self): 1103 | 1104 | root_node = ExoticTypeNode.objects.create(v=date(1982, 9, 26)) 1105 | # The following two have different v values, but created in the 1106 | # opposite order from which we are ordering. 1107 | middle_node_1 = ExoticTypeNode.objects.create( 1108 | parent=root_node, v=date(2006, 7, 7) 1109 | ) 1110 | middle_node_2 = ExoticTypeNode.objects.create( 1111 | parent=root_node, v=date(1946, 1, 6) 1112 | ) 1113 | 1114 | # The following two have the same v value. 1115 | bottom_node_1_1 = ExoticTypeNode.objects.create( 1116 | parent=middle_node_1, v=date(1924, 11, 20) 1117 | ) 1118 | bottom_node_1_2 = ExoticTypeNode.objects.create( 1119 | parent=middle_node_1, v=date(1924, 11, 20) 1120 | ) 1121 | 1122 | # The following have different v values, but created in 'reverse' order. 1123 | bottom_node_2_1 = ExoticTypeNode.objects.create( 1124 | parent=middle_node_2, v=date(1987, 10, 20) 1125 | ) 1126 | bottom_node_2_2 = ExoticTypeNode.objects.create( 1127 | parent=middle_node_2, v=date(1903, 4, 25) 1128 | ) 1129 | 1130 | expected_order = [ 1131 | root_node.id, 1132 | middle_node_2.id, 1133 | bottom_node_2_2.id, 1134 | bottom_node_2_1.id, 1135 | middle_node_1.id, 1136 | bottom_node_1_1.id, 1137 | bottom_node_1_2.id, 1138 | ] 1139 | 1140 | self.assertEqual( 1141 | expected_order, [node.id for node in ExoticTypeNode.objects.all()] 1142 | ) 1143 | 1144 | def test_date_query(self): 1145 | 1146 | node1 = ExoticTypeNode.objects.create(v=date(1982, 9, 26), y=date(1982, 9, 29)) 1147 | node2 = ExoticTypeNode.objects.create(v=date(1982, 9, 26), y=date(1982, 9, 30)) 1148 | 1149 | self.assertEqual( 1150 | list(ExoticTypeNode.objects.filter(y__gt=F("v") + timedelta(days=3))), 1151 | [node2], 1152 | ) 1153 | 1154 | self.assertEqual( 1155 | ExoticTypeNode.objects.filter(y__gt=F("v") + timedelta(days=3)).count(), 1 1156 | ) 1157 | 1158 | self.assertEqual( 1159 | ExoticTypeNode.objects.filter(y__gte=F("v") + timedelta(days=3)).count(), 2 1160 | ) 1161 | 1162 | self.assertEqual( 1163 | list(ExoticTypeNode.objects.filter(y__lte=F("v") + timedelta(days=3))), 1164 | [node1], 1165 | ) 1166 | 1167 | self.assertEqual( 1168 | list(ExoticTypeNode.objects.filter(y__lte=F("v") + timedelta(days=-4))), [] 1169 | ) 1170 | 1171 | 1172 | class DBTypeNodeTest(TestCase): 1173 | 1174 | sorted_uuids = [ 1175 | UUID("24f8aab3-43b1-4359-bbe7-532e33aa8114"), 1176 | UUID("4761ac94-9435-4fe0-9644-3e0b0da4e808"), 1177 | UUID("563a973c-74da-46f9-8be7-70b9b60ca47a"), 1178 | UUID("6309a02a-fec0-48c3-a93d-4a06a80de78b"), 1179 | UUID("75025ae9-fd6d-41d2-8cd3-c34c49267d6d"), 1180 | UUID("78e66b6f-f59c-42df-b20e-ea9855329bea"), 1181 | ] 1182 | 1183 | def test_ensure_sorted_uuids(self): 1184 | 1185 | previous = None 1186 | for u in self.sorted_uuids: 1187 | if previous is None: 1188 | previous = u 1189 | else: 1190 | self.assertTrue(previous < u) 1191 | 1192 | def test_db_type(self): 1193 | 1194 | root_node = DBTypeNode.objects.create(v=self.sorted_uuids[0]) 1195 | # The following two have different v values, but created in the 1196 | # opposite order from which we are ordering. 1197 | middle_node_1 = DBTypeNode.objects.create( 1198 | parent=root_node, v=self.sorted_uuids[2] 1199 | ) 1200 | middle_node_2 = DBTypeNode.objects.create( 1201 | parent=root_node, v=self.sorted_uuids[1] 1202 | ) 1203 | 1204 | # The following two have the same v value. 1205 | bottom_node_1_1 = DBTypeNode.objects.create( 1206 | parent=middle_node_1, v=self.sorted_uuids[3] 1207 | ) 1208 | bottom_node_1_2 = DBTypeNode.objects.create( 1209 | parent=middle_node_1, v=self.sorted_uuids[3] 1210 | ) 1211 | 1212 | # The following have different v values, but created in 'reverse' order. 1213 | bottom_node_2_1 = DBTypeNode.objects.create( 1214 | parent=middle_node_2, v=self.sorted_uuids[5] 1215 | ) 1216 | bottom_node_2_2 = DBTypeNode.objects.create( 1217 | parent=middle_node_2, v=self.sorted_uuids[4] 1218 | ) 1219 | 1220 | expected_order = [ 1221 | root_node.id, 1222 | middle_node_2.id, 1223 | bottom_node_2_2.id, 1224 | bottom_node_2_1.id, 1225 | middle_node_1.id, 1226 | bottom_node_1_1.id, 1227 | bottom_node_1_2.id, 1228 | ] 1229 | 1230 | self.assertEqual(expected_order, [node.id for node in DBTypeNode.objects.all()]) 1231 | 1232 | def test_db_type_path(self): 1233 | 1234 | root_node = DBTypeNode.objects.create(v=self.sorted_uuids[0]) 1235 | # The following two have different v values, but created in the 1236 | # opposite order from which we are ordering. 1237 | middle_node_1 = DBTypeNode.objects.create( 1238 | parent=root_node, v=self.sorted_uuids[2] 1239 | ) 1240 | DBTypeNode.objects.create(parent=root_node, v=self.sorted_uuids[1]) 1241 | 1242 | fresh_middle_node_1 = DBTypeNode.objects.get(id=middle_node_1.id) 1243 | 1244 | self.assertEqual(fresh_middle_node_1.ancestors()[0], root_node) 1245 | 1246 | def test_tree_structure_is_child_of(self): 1247 | 1248 | root_node = DBTypeNode.objects.create() 1249 | middle_node = DBTypeNode.objects.create(parent=root_node) 1250 | bottom_node_1 = DBTypeNode.objects.create(parent=middle_node) 1251 | DBTypeNode.objects.create(parent=middle_node) 1252 | DBTypeNode.objects.create(parent=middle_node) 1253 | 1254 | fresh_middle_node = DBTypeNode.objects.get(id=middle_node.id) 1255 | fresh_bottom_node_1 = DBTypeNode.objects.get(id=bottom_node_1.id) 1256 | 1257 | self.assertTrue(fresh_middle_node.is_child_of(root_node)) 1258 | self.assertFalse(fresh_bottom_node_1.is_child_of(root_node)) 1259 | 1260 | 1261 | class CustomPrimaryKeyNodeTest(TestCase): 1262 | def test_tree_structure_is_child_of(self): 1263 | 1264 | # Ensure string-based path encoding works. 1265 | 1266 | root_node = CustomPrimaryKeyNode.objects.create(id="root") 1267 | middle_node = CustomPrimaryKeyNode.objects.create(id="middle", parent=root_node) 1268 | bottom_node_1 = CustomPrimaryKeyNode.objects.create( 1269 | id="bottom 1", parent=middle_node 1270 | ) 1271 | CustomPrimaryKeyNode.objects.create(id="bottom 2", parent=middle_node) 1272 | CustomPrimaryKeyNode.objects.create(id="bottom 3", parent=middle_node) 1273 | 1274 | fresh_middle_node = CustomPrimaryKeyNode.objects.get(id=middle_node.id) 1275 | fresh_bottom_node_1 = CustomPrimaryKeyNode.objects.get(id=bottom_node_1.id) 1276 | 1277 | self.assertTrue(fresh_middle_node.is_child_of(root_node)) 1278 | self.assertFalse(fresh_bottom_node_1.is_child_of(root_node)) 1279 | 1280 | def test_tree_structure_is_descendant_of(self): 1281 | 1282 | # Ensure string-based path encoding works. 1283 | 1284 | root_node = CustomPrimaryKeyNode.objects.create(id="root") 1285 | middle_node = CustomPrimaryKeyNode.objects.create(id="middle", parent=root_node) 1286 | bottom_node_1 = CustomPrimaryKeyNode.objects.create( 1287 | id="bottom 1", parent=middle_node 1288 | ) 1289 | CustomPrimaryKeyNode.objects.create(id="bottom 2", parent=middle_node) 1290 | CustomPrimaryKeyNode.objects.create(id="bottom 3", parent=middle_node) 1291 | 1292 | fresh_root_node = CustomPrimaryKeyNode.objects.get(id=root_node.id) 1293 | fresh_middle_node = CustomPrimaryKeyNode.objects.get(id=middle_node.id) 1294 | fresh_bottom_node_1 = CustomPrimaryKeyNode.objects.get(id=bottom_node_1.id) 1295 | 1296 | self.assertTrue(fresh_bottom_node_1.is_descendant_of(fresh_root_node)) 1297 | 1298 | self.assertTrue(fresh_root_node.is_ancestor_of(fresh_bottom_node_1)) 1299 | 1300 | self.assertFalse(fresh_middle_node.is_descendant_of(fresh_bottom_node_1)) 1301 | 1302 | self.assertFalse(fresh_bottom_node_1.is_ancestor_of(fresh_middle_node)) 1303 | 1304 | 1305 | class DBTypePrimaryKeyNodeTest(TestCase): 1306 | def test_tree_structure_is_child_of(self): 1307 | 1308 | # Ensure string-based path encoding works. 1309 | 1310 | root_node = DBTypePrimaryKeyNode.objects.create() 1311 | middle_node = DBTypePrimaryKeyNode.objects.create(parent=root_node) 1312 | bottom_node_1 = DBTypePrimaryKeyNode.objects.create(parent=middle_node) 1313 | DBTypePrimaryKeyNode.objects.create(parent=middle_node) 1314 | DBTypePrimaryKeyNode.objects.create(parent=middle_node) 1315 | 1316 | fresh_middle_node = DBTypePrimaryKeyNode.objects.get(id=middle_node.id) 1317 | fresh_bottom_node_1 = DBTypePrimaryKeyNode.objects.get(id=bottom_node_1.id) 1318 | 1319 | self.assertTrue(fresh_middle_node.is_child_of(root_node)) 1320 | self.assertFalse(fresh_bottom_node_1.is_child_of(root_node)) 1321 | 1322 | def test_tree_structure_is_ancestor_of(self): 1323 | 1324 | # Ensure string-based path encoding works. 1325 | 1326 | root_node = DBTypePrimaryKeyNode.objects.create() 1327 | middle_node = DBTypePrimaryKeyNode.objects.create(parent=root_node) 1328 | bottom_node = DBTypePrimaryKeyNode.objects.create(parent=middle_node) 1329 | 1330 | fresh_root_node = DBTypePrimaryKeyNode.objects.get(id=root_node.id) 1331 | fresh_middle_node = DBTypePrimaryKeyNode.objects.get(id=middle_node.id) 1332 | fresh_bottom_node = DBTypePrimaryKeyNode.objects.get(id=bottom_node.id) 1333 | 1334 | self.assertFalse(fresh_middle_node.is_ancestor_of(fresh_root_node)) 1335 | self.assertTrue(fresh_root_node.is_ancestor_of(fresh_middle_node)) 1336 | 1337 | self.assertFalse(fresh_bottom_node.is_ancestor_of(fresh_root_node)) 1338 | self.assertTrue(fresh_root_node.is_ancestor_of(fresh_bottom_node)) 1339 | 1340 | self.assertFalse(fresh_bottom_node.is_ancestor_of(fresh_middle_node)) 1341 | self.assertTrue(fresh_middle_node.is_ancestor_of(fresh_bottom_node)) 1342 | 1343 | self.assertFalse(fresh_root_node.is_ancestor_of(fresh_root_node)) 1344 | self.assertFalse(fresh_middle_node.is_ancestor_of(fresh_middle_node)) 1345 | self.assertFalse(fresh_bottom_node.is_ancestor_of(fresh_bottom_node)) 1346 | 1347 | def test_tree_structure_is_descendant_of(self): 1348 | 1349 | # Ensure string-based path encoding works. 1350 | 1351 | root_node = DBTypePrimaryKeyNode.objects.create() 1352 | middle_node = DBTypePrimaryKeyNode.objects.create(parent=root_node) 1353 | bottom_node = DBTypePrimaryKeyNode.objects.create(parent=middle_node) 1354 | 1355 | fresh_root_node = DBTypePrimaryKeyNode.objects.get(id=root_node.id) 1356 | fresh_middle_node = DBTypePrimaryKeyNode.objects.get(id=middle_node.id) 1357 | fresh_bottom_node = DBTypePrimaryKeyNode.objects.get(id=bottom_node.id) 1358 | 1359 | self.assertTrue(fresh_middle_node.is_descendant_of(fresh_root_node)) 1360 | self.assertFalse(fresh_root_node.is_descendant_of(fresh_middle_node)) 1361 | 1362 | self.assertTrue(fresh_bottom_node.is_descendant_of(fresh_root_node)) 1363 | self.assertFalse(fresh_root_node.is_descendant_of(fresh_bottom_node)) 1364 | 1365 | self.assertTrue(fresh_bottom_node.is_descendant_of(fresh_middle_node)) 1366 | self.assertFalse(fresh_middle_node.is_descendant_of(fresh_bottom_node)) 1367 | 1368 | self.assertFalse(fresh_root_node.is_descendant_of(fresh_root_node)) 1369 | self.assertFalse(fresh_middle_node.is_descendant_of(fresh_middle_node)) 1370 | self.assertFalse(fresh_bottom_node.is_descendant_of(fresh_bottom_node)) 1371 | 1372 | 1373 | class AggregationNodeTest(TestCase): 1374 | def test_aggregation(self): 1375 | 1376 | AggregationNode.objects.create(price=100) 1377 | node2 = AggregationNode.objects.create(price=200) 1378 | AggregationNode.objects.create(price=300) 1379 | 1380 | self.assertEqual( 1381 | AggregationNode.objects.all().aggregate(Avg("price"))["price__avg"], 200 1382 | ) 1383 | 1384 | self.assertEqual( 1385 | list( 1386 | AggregationNode.objects.filter( 1387 | price=AggregationNode.objects.all().aggregate(Avg("price"))[ 1388 | "price__avg" 1389 | ] 1390 | ) 1391 | ), 1392 | [node2], 1393 | ) 1394 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoCTEForest.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoCTEForest.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoCTEForest" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoCTEForest" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/advanced.rst: -------------------------------------------------------------------------------- 1 | .. advanced: 2 | 3 | Advanced Usage 4 | ============== 5 | 6 | All imports are from:: 7 | 8 | from cte_forest.models import ... 9 | 10 | 11 | .. contents:: 12 | :local: 13 | 14 | 15 | Extending and Inheritance 16 | ------------------------- 17 | 18 | If a custom Manager is specified, it must inherit from CTENodeManager:: 19 | 20 | class CategoryManager(CTENodeManager): 21 | 22 | ... 23 | 24 | class Category(CTENode): 25 | 26 | name = CharField(max_length = 128, null = False) 27 | 28 | objects = CategoryManager() 29 | 30 | def __unicode__(self): 31 | return '%s @ %s' % (self.name, self.depth) 32 | 33 | 34 | 35 | Dummy Fields 36 | ------------ 37 | 38 | It is possible to add the fields for 'depth', 'path', and 'ordering' as normal 39 | fields to a Django model, so that mechanics of Django, third party, or your own 40 | modules find them. Doing so will create columns for them in the database, but 41 | their values will always be overridden by the materialized CTE table. They will 42 | also never be written (unless you explicitly do so) through an UPDATE query, 43 | meaning the columns will remain empty. Although this is wasteful, it may be 44 | useful or even necessary, given that Django does not cater for non-managed 45 | fields (only entire models). 46 | 47 | Therefore, the fields package provides three such fields which are automatically 48 | set to null, blank, and non-editable, and can be used as such:: 49 | 50 | 51 | from cte_forest.fields import DepthField, PathField, OrderingField 52 | 53 | class Category(CTENode): 54 | 55 | depth = DepthField() 56 | path = PathField() 57 | ordering = OrderingField() 58 | 59 | name = CharField(max_length = 128, null = False) 60 | 61 | objects = CategoryManager() 62 | 63 | def __unicode__(self): 64 | return '%s @ %s' % (self.name, self.depth) 65 | 66 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | Module cte_forest.models 2 | ============================================ 3 | 4 | .. moduleauthor:: Alexis Petrounias 5 | 6 | Any Model can be turned into a tree by inheriting from the abstract 7 | :class:`CTENode`. By default, this model will feature a :class:`ForeignKey` to 8 | `self` named `parent`, and with a `related_name` of `children`. Furthermore, it 9 | will feature a custom :class:`CTENodeManager` through the 10 | :attr:`CTENode.objects` attribute. 11 | 12 | Each instance of a :class:`CTENode` will feature three **virtual** fields, by 13 | default named :attr:`depth`, :attr:`path`, and :attr:`ordering`; these fields 14 | are populated through a custom CTE SQL query, and contain, respectively, the 15 | depth of a node (starting with root nodes of depth one), the path (an array of 16 | primary keys, or an encoded string if the primary key type requires this), and 17 | a custom ordering key (usually starting with the DFS or BFS order key, and then 18 | extended with custom ordering specified by the concrete Model). 19 | 20 | All parameters regarding the configuration of the tree are specified as 21 | attributes in the concrete Model inheriting from :class:`CTENode`, and have the 22 | form :attr:`_cte_node_*`. See below for a complete listing and interpretation of 23 | each parameter. 24 | 25 | An important caveat when using CTE queries is ordering: any CTE query which is 26 | cloned and processed through an :meth:`order_by` invocation will result in the 27 | CTE ordering of the nodes to be overridden. Therefore, if you wish to maintain 28 | proper tree ordering, you must specify any custom fields to order by through the 29 | :attr:`_cte_node_order_by` attribute (see below). Unfortunately, it is not 30 | possible to re-create tree ordering through an :meth:`order_by` invocation (see 31 | technical notes for an explanation). 32 | 33 | In case custom ordering among siblings is desired (such as using an integer, or 34 | lexicographic order of a string name, and so on), the :meth:`move` method 35 | accepts a parameter ``position`` which, if not ``None``, is expected to be a 36 | callable that is invoked with the destination and the node currently being 37 | moved as arguments, before any modification to the parent relationship is made. 38 | Thus, the :meth:`move` method delegates to this callable in order to specify or 39 | modify order-related attributes, enforce constraints, or even change the 40 | attributes of siblings (such as creating a hole in a contiguous integer total 41 | order). 42 | 43 | 44 | .. contents:: 45 | :local: 46 | 47 | ======= 48 | CTENode 49 | ======= 50 | 51 | .. autoclass:: cte_forest.models.CTENode 52 | :members: 53 | 54 | 55 | ============== 56 | CTENodeManager 57 | ============== 58 | 59 | .. autoclass:: cte_forest.models.CTENodeManager 60 | :members: 61 | 62 | -------------------------------------------------------------------------------- /docs/basic.rst: -------------------------------------------------------------------------------- 1 | .. basic: 2 | 3 | Basic Usage 4 | =========== 5 | All imports are from:: 6 | 7 | from cte_forest.models import ... 8 | 9 | 10 | .. contents:: 11 | :local: 12 | 13 | 14 | Defining a Node 15 | --------------- 16 | 17 | Define a Model which inherits from cte_forest.models.CTENode:: 18 | 19 | class Category(CTENode): 20 | 21 | name = CharField(max_length = 128, null = False) 22 | 23 | def __unicode__(self): 24 | return '%s @ %s' % (self.name, self.depth) 25 | 26 | The Category Model will now have a *parent* foreign key to *self*, as well as 27 | all the tree structure virtual fields (depth, path, ordering) and methods 28 | inherited from CTENode, and a custom Manager for performing the custom CTE 29 | queries. 30 | 31 | The following is an example usage of Category:: 32 | 33 | >>> root = Category.objects.create(name = 'root') 34 | >>> middle = Category.objects.create(name = 'middle', parent = root) 35 | >>> bottom = Category.objects.create(name = 'bottom', parent = middle) 36 | >>> print(Category.objects.all()) 37 | [, , ] 38 | 39 | See the module documentation and examples for a comprehensive guide on using the 40 | CTE Node and its manager. 41 | 42 | 43 | Ordering 44 | -------- 45 | 46 | By default, CTE Nodes are ordered based on their primary key. This means there 47 | is no guarantee of the traversal order before Nodes are created, nor when new 48 | Nodes are added (although between no changes to the tree the order remains the 49 | same). In most situations (especially test cases) automatic primary keys are 50 | generated in an ascending order, which may give the false impression of 51 | deterministic ordering! 52 | 53 | To specify a tree ordering, use the '_cte_node_order_by' parameter, which is a 54 | list of fields with which to order similar to Django's order_by. This parameter 55 | will create the 'ordering' virtual field for the CTE Node, which also supports 56 | ordering on multiple fields by supporting arrays for the 'ordering' field. For 57 | example, the following Category features an 'order' integer field:: 58 | 59 | class Category(CTENode): 60 | 61 | name = CharField(max_length = 128, null = False) 62 | 63 | order = PositiveIntegerField(null = False, default = 0) 64 | 65 | _cte_node_order_by = ('order', ) 66 | 67 | def __unicode__(self): 68 | return '%s @ %s : %s' % (self.name, self.depth, self.ordering) 69 | 70 | The following is an example usage of the ordered Category:: 71 | 72 | >>> root = Category.objects.create(name = 'root') 73 | >>> first_middle = Category.objects.create(name = 'first middle', parent = root, order = 1) 74 | >>> second_middle = Category.objects.create(name = 'second middle', parent = root, order = 2) 75 | >>> first_bottom = Category.objects.create(name = 'first bottom', parent = first_middle, order = 1) 76 | >>> second_bottom = Category.objects.create(name = 'second bottom', parent = second_middle, order = 1) 77 | >>> Category.objects.all() 78 | [, 79 | , 80 | , 81 | , 82 | ] 83 | 84 | which is a depth-first ordering. A breadth-first ordering (in this example 85 | through Django's 'order_by' query, which overrides the default ordering 86 | specified via '_cte_node_order_by') is achieved via:: 87 | 88 | >>> Category.objects.all().order_by('depth', 'order') 89 | [, 90 | , 91 | , 92 | , 93 | ] 94 | 95 | Hence, quite exotic ordering can be achieved. As a last example, a descending 96 | in-order breadth-first search, but with ascending siblings: 97 | 98 | >>> Category.objects.all().order_by('-depth', 'order') 99 | [, 100 | , 101 | , 102 | , 103 | ] 104 | 105 | and so on. 106 | 107 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # This document is free and open-source software, subject to the OSI-approved 4 | # BSD license below. 5 | # 6 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 7 | # All rights reserved. 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, this 13 | # list of conditions and the following disclaimer. 14 | # 15 | # * Redistributions in binary form must reproduce the above copyright notice, 16 | # this list of conditions and the following disclaimer in the documentation 17 | # and/or other materials provided with the distribution. 18 | # 19 | # * Neither the name of the author nor the names of its contributors may be used 20 | # to endorse or promote products derived from this software without specific 21 | # prior written permission. 22 | # 23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | """ Sphinx configuration for Django CTE Forest. 35 | """ 36 | 37 | import os 38 | import sys 39 | 40 | import django 41 | 42 | # add package root as well as dummy Django application so models can be 43 | # imported 44 | sys.path.append(os.path.abspath('..')) 45 | os.environ['DJANGO_SETTINGS_MODULE'] = 'cte_forest_test.settings' 46 | django.setup() 47 | 48 | extensions = ['sphinx.ext.autodoc'] 49 | 50 | templates_path = ['_templates'] 51 | 52 | source_suffix = '.rst' 53 | 54 | master_doc = 'index' 55 | 56 | project = u'Django CTE Forest' 57 | copyright = u'2011 - 2013 Alexis Petrounias ' 58 | 59 | version = __import__('cte_forest').__version__ 60 | release = version 61 | 62 | pygments_style = 'sphinx' 63 | 64 | html_theme = 'default' 65 | 66 | html_static_path = ['_static'] 67 | 68 | htmlhelp_basename = 'DjangoCTEForestdoc' 69 | 70 | latex_documents = [( 71 | 'index', 72 | 'DjangoCTEForest.tex', 73 | u'Django CTE Forest Documentation', 74 | u'Alexis Petrounias \\textless{}www.petrounias.org\\textgreater{}', 75 | 'manual', 76 | )] 77 | 78 | man_pages = [( 79 | 'index', 80 | 'djangocteforest', 81 | u'Django CTE Forest Documentation', 82 | [u'Alexis Petrounias '], 83 | 1, 84 | )] 85 | 86 | texinfo_documents = [( 87 | 'index', 88 | 'DjangoCTEForest', 89 | u'Django CTE Forest Documentation', 90 | u'Alexis Petrounias ', 91 | 'DjangoCTEForest', 92 | 'Experimental implementation of Adjacency-List trees for Django' 93 | ' using PostgreSQL Common Table Expressions (CTE).', 94 | 'Miscellaneous', 95 | )] 96 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. examples: 2 | 3 | Examples 4 | ============================================ 5 | 6 | *Coming soon.* 7 | 8 | 9 | .. contents:: 10 | :local: 11 | 12 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Django CTE Forest documentation master file 2 | 3 | 4 | Django CTE Forest 5 | ================= 6 | 7 | Django Adjacency-List trees using PostgreSQL Common Table Expressions (CTE). Its 8 | aim is to completely hide the management of tree structure. 9 | 10 | Although handling tree structure in a transparent way is a desirable 11 | characteristic for many applications, the currently **known limitations** of 12 | including CTE (see below) will be a show-stopper for many other applications. 13 | Unless you know beforehand that these limitations will not affect your 14 | application, this module is **not suitable** for you, and you should use an 15 | actively managed tree structure (such as 16 | `django-mptt `_ or 17 | `django-treebeard `_). 18 | 19 | 20 | *Characteristics* 21 | 22 | - **Simple**: inheriting from an abstract node model is sufficient to obtain 23 | tree functionality for any :class:`Model`. 24 | 25 | - **Seamless**: does not use :class:`RawQuerySet`, so queries using CTE can be 26 | combined with normal Django queries, and won't confuse the 27 | :class:`SQLCompiler` or other :class:`QuerySets`, including using multiple 28 | databases. 29 | 30 | - **Self-contained**: tree nodes can be manipulated without worrying about 31 | maintaining tree structure in the database. 32 | 33 | - **Single query**: all tree traversal operations can be performed through a 34 | single query, including children, siblings, ancestors, roots, and descendants. 35 | 36 | - **Powerful ordering**: supports (a subset of) normal Django ordering as well 37 | as ordering on tree structure information, including depth and path, in DFS 38 | and BFS orders. 39 | 40 | - **Multiple delete semantics**: supports Pharaoh, Grandmother, and Monarchy 41 | deletion patterns. 42 | 43 | - **Code**: unit tests, code coverage, documentation, comments. 44 | 45 | 46 | *Known limitations* 47 | 48 | - **Virtual fields not usable in external queries**: it is not yet possible to 49 | use the virtual fields which describe the tree structure (depth, path, and 50 | ordering information) in queries other than directly on the CTE Nodes. 51 | Consequently, you cannot order on these fields any Model other than the CTE 52 | Nodes themselves. See the technical notes for details. 53 | 54 | - **Cannot merge queries with OR**: because CTE relies on custom WHERE clauses 55 | added through extra, the bitwise OR operator cannot be used with query 56 | composition. 57 | 58 | - **Cannot use new Nodes without loading**: immediately after creating a CTE 59 | Node, it must be read from the database if you need to use its tree structure 60 | (depth, path, and ordering information). 61 | 62 | - **Cannot order descending**: you cannot order on structure fields (depth, 63 | path) or additional normal fields combined with structure fields in descending 64 | order. 65 | 66 | ============= 67 | Prerequisites 68 | ============= 69 | 70 | Core: 71 | 72 | - PostgreSQL >= 8.4 73 | - Python >= 2.7, >= 3.4 74 | - psycopg2 >= 2.4 75 | - Django >= 1.10 76 | 77 | 78 | ========= 79 | Obtaining 80 | ========= 81 | 82 | - Original author's website for the project: http://www.petrounias.org/software/django-cte-trees/ 83 | 84 | - Git repository on GitHub: https://github.com/matthiask/django-cte-forest/ 85 | 86 | - PyPI: https://pypi.python.org/pypi/django-cte-forest 87 | 88 | 89 | ============ 90 | Installation 91 | ============ 92 | 93 | Via setup tools:: 94 | 95 | python setup.py install 96 | 97 | Via pip and pypi:: 98 | 99 | pip install django-cte-forest 100 | 101 | 102 | Include the cte_forest module as an application in your Django project through the 103 | INSTALLED_APPS list in your settings:: 104 | 105 | INSTALLED_APPS = ( 106 | ..., 107 | 'cte_forest', 108 | ..., 109 | ) 110 | 111 | 112 | ================= 113 | Table of Contents 114 | ================= 115 | .. toctree:: 116 | :maxdepth: 6 117 | 118 | basic.rst 119 | advanced.rst 120 | examples.rst 121 | api.rst 122 | technical.rst 123 | 124 | ============= 125 | Release Notes 126 | ============= 127 | 128 | - v0.9.0 @ 3 May 2011 Initial public release. 129 | 130 | - v0.9.1 @ 19 November 2011 Added is_branch utility method to CTENode Model. 131 | 132 | - v0.9.2 @ 3 March 2012 Introduced structural operations for representing trees 133 | as dictionaries, traversing attributes and structure (visitor pattern), and 134 | 'drilldown' facility based on attribute path filtering. Added documentation 135 | and removed whitespace. 136 | 137 | - v1.0.0, 17 July 2013 Beta version 1; cleaned up package and comments, updated 138 | pypi data, added documentation, and updated Django multiple database support 139 | for compatibility with latest version. 140 | 141 | - v1.0.0, 27 July 2013 Beta version 2; several optimisations to reduce compiled 142 | query size; fixed an issue with descendants where the offset node was returned 143 | as the first descendant; introduced support for CTE table prefixing on virtual 144 | fields when used in ordering; introduced support for UPDATE queries; added 145 | documentation for ordering, further technical notes, and advanced usage. 146 | 147 | ================== 148 | Development Status 149 | ================== 150 | 151 | Actively developed and maintained since 2011. Currently used in production in 152 | proprietary projects by the author and his team, as well as other third parties. 153 | 154 | =========== 155 | Future Work 156 | =========== 157 | 158 | - Abstract models for sibling ordering semantics (integer total and partial 159 | orders, and lexicographic string orders) [high priority, easy task]. 160 | - Support for dynamic specification of traversal and ordering [normal priority, 161 | hard task]. 162 | - Support other databases (which feature CTE in some way) [low priority, normal 163 | difficulty task]. 164 | 165 | ============ 166 | Contributors 167 | ============ 168 | 169 | Written and maintained by Alexis Petrounias < http://www.petrounias.org/ > 170 | 171 | ======= 172 | License 173 | ======= 174 | 175 | Released under the OSI-approved BSD license. 176 | 177 | Copyright (c) 2011 - 2013 Alexis Petrounias < www.petrounias.org >, 178 | all rights reserved. 179 | 180 | Redistribution and use in source and binary forms, with or without modification, 181 | are permitted provided that the following conditions are met: 182 | 183 | Redistributions of source code must retain the above copyright notice, this list 184 | of conditions and the following disclaimer. 185 | 186 | Redistributions in binary form must reproduce the above copyright notice, this 187 | list of conditions and the following disclaimer in the documentation and/or 188 | other materials provided with the distribution. 189 | 190 | Neither the name of the author nor the names of its contributors may be used to 191 | endorse or promote products derived from this software without specific prior 192 | written permission. 193 | 194 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 195 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 197 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 198 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 199 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 200 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 201 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 202 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 203 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 204 | 205 | ================== 206 | Indices and tables 207 | ================== 208 | 209 | * :ref:`genindex` 210 | * :ref:`modindex` 211 | * :ref:`search` 212 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DjangoCTEForest.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DjangoCTEForest.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/technical.rst: -------------------------------------------------------------------------------- 1 | .. technical: 2 | 3 | Technical Notes 4 | =============== 5 | 6 | .. contents:: 7 | :local: 8 | 9 | ========= 10 | CTE Trees 11 | ========= 12 | 13 | See PostgreSQL WITH queries: http://www.postgresql.org/docs/devel/static/queries-with.html 14 | 15 | And the PostgreSQL wiki on CTE: http://wiki.postgresql.org/wiki/CTEReadme 16 | 17 | 18 | ============ 19 | Custom Query 20 | ============ 21 | 22 | The custom query compiler generates the following SQL:: 23 | 24 | WITH RECURSIVE {cte} ( 25 | "{depth}", "{path}", "{ordering}", "{pk}") AS ( 26 | 27 | SELECT 1 AS depth, 28 | array[{pk_path}] AS {path}, 29 | {order} AS {ordering}, 30 | T."{pk}" 31 | FROM {db_table} T 32 | WHERE T."{parent}" IS NULL 33 | 34 | UNION ALL 35 | 36 | SELECT {cte}.{depth} + 1 AS {depth}, 37 | {cte}.{path} || {pk_path}, 38 | {cte}.{ordering} || {order}, 39 | T."{pk}" 40 | FROM {db_table} T 41 | JOIN {cte} ON T."{parent}" = {cte}."{pk}") 42 | 43 | where the variables are obtained from the CTE Node parameters. 44 | 45 | ===================== 46 | Custom Query Compiler 47 | ===================== 48 | 49 | The compiler constructs the ad hoc variables which will be used in the SELECT 50 | query, synthesizes the WHERE clause, as well as the order-by parameter, and then 51 | uses the Query's 'add_extra' method. The table 'cte' is added, as well as an 52 | 'ExtraWhere' node which ensures that the primary key of the 'cte' table matches 53 | the primary key of the model's table. If the CTE recursion is to be started from 54 | an offset Node, then an ExtraWhere is also added ensuring that all Nodes which 55 | are to be returned contain the primary key of the offset Node in their 56 | materialized 'path' virtual field (hence the offset Node is also included 57 | itself, which means descendant lookups must explicitly exclude it). 58 | 59 | In order to allow Django model fields and corresponding columns for the virtual 60 | fields 'depth', 'path', and 'ordering', appropriate prefixing is ensured for 61 | all queries involving the CTE compiler. So, for example, assuming the CTE Node 62 | model features an integer field 'order', specifying that ordering should be 63 | descending breadth first search (but ascending for siblings), you would write: 64 | 65 | order_by('-depth', 'order') 66 | 67 | and the compiler would translate this to ['-"cte".depth', 'order'] because the 68 | 'depth' field is provided by the CTE query but the 'order' field is provided by 69 | the SELECT query. 70 | 71 | PostgreSQL array type is used in order to materialize the 'path' and the 72 | 'ordering' virtual fields; automatic casting is done by the compiler so primary 73 | keys and fields contributing to the order can be more 'exotic', such as a UUID 74 | primary key, converting VARCHAR to TEXT, and so on. 75 | 76 | =========== 77 | Performance 78 | =========== 79 | 80 | There is no straightforward way to compare the performance of CTE trees with 81 | alternatives (such as Django-treebeard or Django-mptt) because actively-managed 82 | tree structures perform multiple SQL queries for certain operations, as well as 83 | perform many operations in the application layer. Therefore, performance 84 | comparison will make sense depending on your application and the kinds of 85 | operations it performs on trees. 86 | 87 | Generally, each SQL query involving a node Model will create the recursive CTE 88 | temporary table, even if the virtual fields are not selected (deferred). 89 | 90 | Note that INSERT and UPDATE operations are not affected at all from the CTE 91 | compiler, and thus impose no overhead. 92 | 93 | 94 | ======= 95 | Testing 96 | ======= 97 | 98 | Use the cte_forest_test Django dummy application and module. By default, it uses 99 | a localhost database 'dummy' with user 'dummy' and password 'dummy'. 100 | -------------------------------------------------------------------------------- /requirements.docs.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | Sphinx==1.5.3 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.10 2 | psycopg2-binary 3 | coverage 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude=venv,build,docs,.tox,migrations 3 | # max-complexity=10 4 | max-line-length=88 5 | 6 | [bdist_wheel] 7 | universal = 1 8 | 9 | [coverage:run] 10 | branch = True 11 | include = 12 | *cte_forest* 13 | omit = 14 | *cte_forest_test* 15 | *migrations* 16 | *tests* 17 | *.tox* 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This document is free and open-source software, subject to the OSI-approved 5 | # BSD license below. 6 | # 7 | # Copyright (c) 2011 - 2013 Alexis Petrounias , 8 | # All rights reserved. 9 | # 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # 13 | # * Redistributions of source code must retain the above copyright notice, this 14 | # list of conditions and the following disclaimer. 15 | # 16 | # * Redistributions in binary form must reproduce the above copyright notice, 17 | # this list of conditions and the following disclaimer in the documentation 18 | # and/or other materials provided with the distribution. 19 | # 20 | # * Neither the name of the author nor the names of its contributors may be used 21 | # to endorse or promote products derived from this software without specific 22 | # prior written permission. 23 | # 24 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 28 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 29 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 32 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | 36 | import os 37 | from io import open 38 | 39 | from setuptools import find_packages, setup 40 | 41 | 42 | def read(filename): 43 | path = os.path.join(os.path.dirname(__file__), filename) 44 | with open(path, encoding='utf-8') as handle: 45 | return handle.read() 46 | 47 | 48 | setup( 49 | name='django-cte-forest', 50 | version=__import__('cte_forest').__version__, 51 | description=( 52 | 'Django Adjacency-List trees using PostgreSQL' 53 | ' Common Table Expressions (CTE).' 54 | ), 55 | long_description=read('README.rst'), 56 | maintainer='Matthias Kestenholz', 57 | maintainer_email='mk@feinheit.ch', 58 | url='https://github.com/matthiask/django-cte-forest', 59 | license='BSD License', 60 | packages=find_packages( 61 | exclude=['cte_forest_test'], 62 | ), 63 | include_package_data=True, 64 | classifiers=[ 65 | 'Development Status :: 3 - Alpha', 66 | 'Environment :: Web Environment', 67 | 'Framework :: Django', 68 | 'Intended Audience :: Developers', 69 | 'License :: OSI Approved :: BSD License', 70 | 'Operating System :: OS Independent', 71 | 'Programming Language :: Python', 72 | 'Topic :: Software Development :: Libraries :: Python Modules', 73 | ], 74 | zip_safe=False, 75 | ) 76 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [testenv] 2 | basepython = python3 3 | 4 | [testenv:docs] 5 | deps = 6 | Sphinx 7 | Django 8 | psycopg2-binary 9 | changedir = docs 10 | commands = make html 11 | skip_install = true 12 | whitelist_externals = make 13 | 14 | [testenv:style] 15 | deps = 16 | black 17 | flake8 18 | changedir = {toxinidir} 19 | commands = 20 | black cte_forest cte_forest_test 21 | flake8 . 22 | skip_install = true 23 | 24 | [testenv:tests] 25 | deps = 26 | Django 27 | coverage 28 | psycopg2-binary 29 | changedir = {toxinidir} 30 | skip_install = true 31 | setenv = 32 | PYTHONWARNINGS=all 33 | PYTHONPATH=. 34 | commands = 35 | coverage run cte_forest_test/manage.py test -v 2 {posargs} 36 | coverage html 37 | --------------------------------------------------------------------------------