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