python-django-mptt-0.8.5/000077500000000000000000000000001275105100200152605ustar00rootroot00000000000000python-django-mptt-0.8.5/INSTALL000066400000000000000000000007541275105100200163170ustar00rootroot00000000000000Thanks for downloading Django MPTT To install, run the following command inside this directory: python setup.py install Or if you'd prefer, you can simply place the included ``mptt`` directory somewhere on your PYTHONPATH, or symlink to it from somewhere on your PYTHONPATH; this is useful if you're working from a git checkout. Requires: - Python 2.7 or newer - Django 1.8 or newer You can obtain Python from https://www.python.org/ and Django from https://www.djangoproject.com/ python-django-mptt-0.8.5/LICENSE000066400000000000000000000020771275105100200162730ustar00rootroot00000000000000Django MPTT ----------- Copyright (c) 2007, Jonathan Buchanan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-django-mptt-0.8.5/MANIFEST.in000066400000000000000000000004741275105100200170230ustar00rootroot00000000000000include INSTALL include LICENSE include MANIFEST.in include NOTES include README.rst recursive-include docs *.rst *.py Makefile recursive-include mptt *.json recursive-include tests * recursive-include mptt/templates * recursive-include mptt/locale * recursive-include mptt/static * global-exclude __pycache__ *.pyc python-django-mptt-0.8.5/NOTES000066400000000000000000000452221275105100200161000ustar00rootroot00000000000000============================= Django MPTT Development Notes ============================= This document contains notes related to use cases/reasoning behind and implementation details for Django MPTT features. I've not worked with this particular kind of hierarchical data structure before to any degree of complexity, so you can consider this my "working out" :) Reparenting ----------- Since it's not unreasonable to assume a good percentage of the people who use this application will also be using the ``django.contrib.admin`` application or ``forms.ModelForm`` to edit their data, and since in these cases only the parent field will be editable if users have allowed ``mptt.register`` to create tree fields for them, it would be nice if Django MPTT automatically took care of the tree when a ``Model`` instance has its parent changed. When the parent of a tree node is changed, its left, right, level and tree id may also be updated to keep the integrity of the tree structure intact. In this case, we'll assume the node which was changed should become the last child of its parent. The following diagram depicts a representation of a nested set which we'll base some basic reparenting examples on - hopefully, by observing the resulting tree structures, we can come up with algorithms for different reparenting scenarios:: __________________________________________________________________________ | Root 1 | | ________________________________ ________________________________ | | | Child 1.1 | | Child 1.2 | | | | ___________ ___________ | | ___________ ___________ | | | | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | | 1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14 | |________________________________| |________________________________| | |__________________________________________________________________________| Extract Root Node (Remove Parent) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the node's previous parent was not ``None`` and the node's new parent is ``None``, we need to make it into a new root. For example, were we to change Child 1.2's parent to ``None``, we should end up with the following structure:: ______________________________________ | Root 1 | | ________________________________ | | | Child 1.1 | | | | ___________ ___________ | | | | | C 1.1.1 | | C 1.1.2 | | | 1 2 3___________4 5___________6 7 8 | |________________________________| | |______________________________________| ____________________________________ | Root 2 | | _____________ _____________ | | | Child 2.1 | | Child 2.2 | | 1 2_____________3 4_____________5 6 |____________________________________| The following steps should translate an existing node and its descendants into the new format required: 1. The new root node's level will have to become ``0``, and the levels of any descendants will decrease by the same amount, so just subtract the root node's current level from all affected nodes:: new_level = current_level - new_root_node.level 2. The new root node's left value will have to become ``1``. Since the node's number of descendants hasn't changed, we can simply use the node's current left value to adjust all left and right values by the amount required:: new_left = current_left - new_root_node.left + 1 new_right = current_right - new_root_node.left + 1 3. A new tree id will be generated for the new root node, so update the node and all descendants with this value. This can be expressed as a single SQL query:: UPDATE nodes SET level = level - [NODE_LEVEL], left = left - [NODE_LEFT - 1], right = right - [NODE_LEFT - 1], tree_id = [NEW_TREE_ID] WHERE left BETWEEN [NODE_LEFT] AND [NODE_RIGHT] AND tree_id = [CURRENT_TREE_ID] Now we have to fix the original tree, as there's a hole the size of the node we just moved. We can calculate the size of the gap using the node's left and right values, updating the original tree accordingly:: UPDATE nodes SET left = left - [NODE_RIGHT - NODE_LEFT + 1] WHERE left > [NODE_LEFT] AND tree_id = [CURRENT_TREE_ID] UPDATE nodes SET right = right - [NODE_RIGHT - NODE_LEFT + 1] WHERE right > [NODE_RIGHT] AND tree_id = [CURRENT_TREE_ID] Insert Tree (Add Parent) ~~~~~~~~~~~~~~~~~~~~~~~~ If the node's previous parent was ``None`` and the node's new parent is not ``None``, we need to move the entire tree it was the root node for. First, we need to make some space for the tree to be moved into the new parent's tree. This is the same as the process used when creating a new child node, where we add ``2`` to all left and right values which are greater than or equal to the right value of the parent into which the new node will be inserted. In this case, we want to use the width of the tree we'll be inserting, which is ``right - left + 1``. For any node without children, this would be ``2``, which is why we add that amount when creating a new node. This seems like the kind of operation which could be extracted out into a reusable function at some stage. Steps to translate the node and its descendants to the new format required: 1. The node's level (``0``, as it's a root node) will have to become one greater than its new parent's level. We can add this amount to the node and all its descendants to get the correct levels:: new_level = current_level + new_parent.level + 1 2. The node's left value (``1``, as it's a root node) will have to become the current right value of its new parent (look at the diagrams above if this doesn't make sense - imagine inserting Root 2 back into Root 1). Add the difference between the node's left value and the new parent's right value to all left and right values of the node and its descendants:: new_left = current_left + new_parent.right - 1 new_right = current_right + new_parent.right - 1 3. Update the node and all descendants with the tree id of the tree they're moving to. This is a similar query to that used when creating new root nodes from existing child nodes. We can omit the left value from the ``WHERE`` statement in this case, since we'll be operating on a whole tree, but this also looks like something which could be extracted into a reusable function at some stage:: UPDATE nodes SET level = level + [PARENT_LEVEL + 1], left = left + [PARENT_RIGHT - 1], right = right + [PARENT_RIGHT - 1], tree_id = [PARENT_TREE_ID] WHERE tree_id = [CURRENT_TREE_ID] Move Within Tree (Change Parent, Same Tree) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Original Tree:: __________________________________________________________________________ | Root 1 | | ________________________________ ________________________________ | | | Child 1.1 | | Child 1.2 | | | | ___________ ___________ | | ___________ ___________ | | | | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | | 1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14 | |________________________________| |________________________________| | |__________________________________________________________________________| C 1.2.2 -> Root 1:: ____________________________________________________________________________ | Root 1 | | ________________________________ _________________ _____________ | | | Child 1.1 | | Child 1.2 | | Child 1.3 | | | | ___________ ___________ | | ___________ | | | | | | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | | | | 1 2 3___________4 5___________6 7 8 9___________10 11 12 13 14 | |________________________________| |_________________| |_____________| | |____________________________________________________________________________| |________________| | Affected area old_left = 11, new_left = 12 old_right = 12, new_right = 13 left_right_change = 1 target_left = 1, target_right = 14 all other affected lefts and rights decreased by 2 C 1.2.2 -> Child 1.1:: __________________________________________________________________________ | Root 1 | | _______________________________________________ _________________ | | | Child 1.1 | | Child 1.2 | | | | ___________ ___________ ___________ | | ___________ | | | | | C 1.1.1 | | C 1.1.2 | | C 1.1.3 | | | | C 1.2.1 | | | 1 2 3___________4 5___________6 7___________8 9 10 11__________12 13 14 | |_______________________________________________| |_________________| | |__________________________________________________________________________| |________________________________| | Affected area old_left = 11, new_left = 7 old_right = 12, new_right = 8 left_right_change = -4 target_left = 2, target_right = 7 all other affected lefts and rights increased by 2 C 1.1.2 -> Root 1:: ____________________________________________________________________________ | Root 1 | | _________________ ________________________________ _____________ | | | Child 1.1 | | Child 1.2 | | Child 1.3 | | | | ___________ | | ___________ ___________ | | | | | | | C 1.1.1 | | | | C 1.2.1 | | C 1.2.2 | | | | | 1 2 3___________4 5 6 7___________8 9___________10 11 12 13 14 | |_________________| |________________________________| |_____________| | |____________________________________________________________________________| |____________________________________________________| | Affected area old_left = 5, new_left = 12 old_right = 6, new_right = 13 left_right_change = 7 target_left = 1, target_right = 14 all other affected lefts and rights decreased by 2 Child 1.2 -> Child 1.1:: ______________________________________________________________________________ | Root 1 | | ________________________________________________________________________ | | | Child 1.1 | | | | ___________ ___________ ____________________________________ | | | | | C 1.1.1 | | C 1.1.2 | | C 1.1.3 | | | | | | | | | | _____________ _____________ | | | | | | | | | | | C 1.1.3.1 | | C 1.1.3.2 | | | | 1 2 3 4 5 6 7 8_____________9 10____________11 12 13 14 | | |___________| |___________| |____________________________________| | | | |________________________________________________________________________| | |______________________________________________________________________________| |_______________________________________| | Affected area old_left = 8, new_left = 7 old_right = 13, new_right = 12 left_right_change = -1 target_left = 2, target_right = 7 all other affected lefts and rights increased by 6 From the diagrams above, the area affected by moving a subtree within the same tree appears to be confined to the section of the tree between the subtree's lower and upper bounds of the subtree's old and new left and right values. Affected nodes which are not in the subtree being moved appear to be changed by the width of the subtree, with a sign inverse to that of the left_right_change. Node Movement ------------- For automatic reparenting, we've been making the node which has had its parent changed the last child of its new parent node but, outside of that, we may want to position a node in other ways relative to a given target node, say to make it the target node's immediate sibling on either side or its first child. Drawing those trees was pretty tedious, so we'll use this kind of tree representation from now on, as seen in the tests. In order, the fields listed are: id, parent_id, tree_id, level, left, right:: 1 - 1 0 1 14 1 2 1 1 1 2 7 2 3 2 1 2 3 4 3 4 2 1 2 5 6 4 5 1 1 1 8 13 5 6 5 1 2 9 10 6 7 5 1 2 11 12 7 Same Tree, Children ~~~~~~~~~~~~~~~~~~~ Last Child Calculation (derived from previous trees):: if target_right > right: new_left = target_right - subtree_width new_right = target_right - 1 else: new_left = target_right new_right = target_right + subtree_width - 1 Moving "up" the tree:: 1 - 1 0 1 14 1 2 1 1 1 2 9 2 7 2 1 2 3 4 => 7 3 2 1 2 5 6 3 4 2 1 2 7 8 4 5 1 1 1 10 13 5 6 5 1 2 11 12 6 node = 7 target_node = 2 left = 11, right = 12 new_left = 3, new_right = 4 target_left = 2, target_right = 7 affected area = 3 to 12 all other affected lefts and rights increased by 2 1 - 1 0 1 14 1 2 1 1 1 2 13 2 5 2 1 2 3 8 => 5 6 5 1 3 4 5 6 7 5 1 3 6 7 7 3 2 1 2 9 10 3 4 2 1 2 11 12 4 node = 5 target_node = 2 left = 8, right = 13 new_left = 3, new_right = 8 target_left = 2, target_right = 7 affected area = 3 to 13 all other affected lefts and rights increased by 6 Moving "down" the tree:: 1 - 1 0 1 14 1 2 1 1 1 2 5 2 3 2 1 2 3 4 3 5 1 1 1 6 13 5 4 5 1 2 7 8 => 4 6 5 1 2 9 10 6 7 5 1 2 11 12 7 node = 4 target_node = 5 left = 5, right = 6 new_left = 7, new_right = 8 target_left = 8, target_right = 13 affected area = 5 to 8 all other affected lefts and rights decreased by 2 1 - 1 0 1 14 1 5 1 1 1 2 13 5 2 5 1 2 3 8 => 2 3 2 1 3 4 5 3 4 2 1 3 6 7 4 6 5 1 2 9 10 6 7 5 1 2 11 12 7 node = 2 target_node = 5 left = 2, right = 9 new_left = 3, new_right = 8 target_left = 8, target_right = 13 affected area = 2 to 8 all other affected lefts and rights decreased by 6 First Child Calculation:: if target_left > left: new_left = target_left - subtree_width + 1 new_right = target_left else: new_left = target_left + 1 new_right = target_left + subtree_width Same Tree, Siblings ~~~~~~~~~~~~~~~~~~~ Moving "up" the tree:: 1 - 1 0 1 14 1 2 1 1 1 2 9 2 3 2 1 2 3 4 3 7 2 1 2 5 6 => 7 4 2 1 2 7 8 4 5 1 1 1 10 13 5 6 5 1 2 11 12 6 Left sibling: node = 7 target_node = 4 left = 11, right = 12 new_left = 5, new_right = 6 target_left = 5, target_right = 6 affected area = 5 to 12 all other affected lefts and rights increased by 2 Right sibling: node = 7 target_node = 3 left = 11, right = 12 new_left = 5, new_right = 6 target_left = 3, target_right = 4 affected area = 3 to 12 all other affected lefts and rights increased by 2 1 - 1 0 1 14 1 2 1 1 1 2 13 2 3 2 1 2 3 4 3 5 2 1 2 5 10 => 5 6 5 1 3 6 7 6 7 5 1 3 8 9 7 4 2 1 2 11 12 4 Left sibling: node = 5 target_node = 4 left = 8, right = 13 new_left = 5, new_right = 10 target_left = 5, target_right = 6 affected area = 5 to 13 all other affected lefts and rights increased by 6 Right sibling: node = 5 target_node = 3 left = 8, right = 13 new_left = 5, new_right = 10 target_left = 3, target_right = 4 affected area = 3 to 13 all other affected lefts and rights increased by 6 Moving "down" the tree:: 1 - 1 0 1 14 1 2 1 1 1 2 5 2 4 2 1 2 3 4 4 5 1 1 1 6 13 5 6 5 1 2 7 8 6 3 5 1 2 9 10 => 3 7 5 1 2 11 12 7 Left sibling: node = 3 target_node = 7 left = 3, right = 4 new_left = 9, new_right = 10 target_left = 11, target_right = 12 affected area = 4 to 10 all other affected lefts and rights decreased by 2 Right sibling: node = 3 target_node = 6 left = 3, right = 4 new_left = 9, new_right = 10 target_left = 9, target_right = 10 affected area = 4 to 10 all other affected lefts and rights decreased by 2 1 - 1 0 1 14 1 5 1 1 1 2 13 5 6 5 1 2 3 4 6 2 6 1 2 5 10 => 2 3 2 1 3 6 7 3 4 2 1 3 8 9 4 7 5 1 2 11 12 7 Left sibling: node = 2 target_node = 7 left = 2, right = 7 new_left = 5, new_right = 10 target_left = 11, target_right = 12 affected area = 2 to 10 all other affected lefts and rights decreased by 6 Right sibling: node = 2 target_node = 6 left = 2, right = 7 new_left = 5, new_right = 10 target_left = 9, target_right = 10 affected area = 2 to 10 all other affected lefts and rights decreased by 6 Derived Calculations:: Left sibling: if target_left > left: new_left = target_left - subtree_width new_right = target_left - 1 else: new_left = target_left new_right = target_left + subtree_width - 1 if target_right > right: new_left = target_right - subtree_width + 1 new_right = target_right else: new_left = target_right + 1 new_right = target_right + subtree_width python-django-mptt-0.8.5/PKG-INFO000066400000000000000000000022471275105100200163620ustar00rootroot00000000000000Metadata-Version: 1.1 Name: django-mptt Version: 0.8.5 Summary: Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances. Home-page: http://github.com/django-mptt/django-mptt Author: Craig de Stigter Author-email: craig.ds@gmail.com License: MIT License Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Utilities python-django-mptt-0.8.5/README.rst000066400000000000000000000055531275105100200167570ustar00rootroot00000000000000=========== django-mptt =========== Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances. .. image:: https://secure.travis-ci.org/django-mptt/django-mptt.png?branch=master :alt: Build Status :target: https://travis-ci.org/django-mptt/django-mptt Project home: http://github.com/django-mptt/django-mptt/ Documentation: http://django-mptt.github.io/django-mptt/ Discussion group: http://groups.google.com/group/django-mptt-dev What is Modified Preorder Tree Traversal? ========================================= MPTT is a technique for storing hierarchical data in a database. The aim is to make retrieval operations very efficient. The trade-off for this efficiency is that performing inserts and moving items around the tree is more involved, as there's some extra work required to keep the tree structure in a good state at all times. Here are a few articles about MPTT to whet your appetite and provide details about how the technique itself works: * `Trees in SQL`_ * `Storing Hierarchical Data in a Database`_ * `Managing Hierarchical Data in MySQL`_ .. _`Trees in SQL`: http://www.ibase.ru/files/articles/programming/dbmstrees/sqltrees.html .. _`Storing Hierarchical Data in a Database`: http://www.sitepoint.com/print/hierarchical-data-database .. _`Managing Hierarchical Data in MySQL`: http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/ What is ``django-mptt``? ======================== ``django-mptt`` is a reusable Django app which aims to make it easy for you to use MPTT with your own Django models. It takes care of the details of managing a database table as a tree structure and provides tools for working with trees of model instances. Requirements ------------ * Python 2.7 or 3.2+ * A supported version of Django (currently 1.8+) Feature overview ---------------- * Simple registration of models - fields required for tree structure will be added automatically. * The tree structure is automatically updated when you create or delete model instances, or change an instance's parent. * Each level of the tree is automatically sorted by a field (or fields) of your choice. * New model methods are added to each registered model for: * changing position in the tree * retrieving ancestors, siblings, descendants * counting descendants * other tree-related operations * A ``TreeManager`` manager is added to all registered models. This provides methods to: * move nodes around a tree, or into a different tree * insert a node anywhere in a tree * rebuild the MPTT fields for the tree (useful when you do bulk updates outside of django) * Form fields for tree models. * Utility functions for tree models. * Template tags and filters for rendering trees. * Admin classes for visualizing and modifying trees in Django's administration interface. python-django-mptt-0.8.5/django_mptt.egg-info/000077500000000000000000000000001275105100200212605ustar00rootroot00000000000000python-django-mptt-0.8.5/django_mptt.egg-info/PKG-INFO000066400000000000000000000022471275105100200223620ustar00rootroot00000000000000Metadata-Version: 1.1 Name: django-mptt Version: 0.8.5 Summary: Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances. Home-page: http://github.com/django-mptt/django-mptt Author: Craig de Stigter Author-email: craig.ds@gmail.com License: MIT License Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Utilities python-django-mptt-0.8.5/django_mptt.egg-info/SOURCES.txt000066400000000000000000000046141275105100200231510ustar00rootroot00000000000000INSTALL LICENSE MANIFEST.in NOTES README.rst setup.cfg setup.py django_mptt.egg-info/PKG-INFO django_mptt.egg-info/SOURCES.txt django_mptt.egg-info/dependency_links.txt django_mptt.egg-info/top_level.txt docs/Makefile docs/admin.rst docs/autogenerated.rst docs/conf.py docs/forms.rst docs/index.rst docs/install.rst docs/models.rst docs/mptt.admin.rst docs/mptt.exceptions.rst docs/mptt.fields.rst docs/mptt.forms.rst docs/mptt.managers.rst docs/mptt.models.rst docs/mptt.querysets.rst docs/mptt.rst docs/mptt.utils.rst docs/overview.rst docs/technical_details.rst docs/templates.rst docs/tutorial.rst docs/upgrade.rst docs/utilities.rst mptt/__init__.py mptt/admin.py mptt/exceptions.py mptt/fields.py mptt/forms.py mptt/managers.py mptt/models.py mptt/querysets.py mptt/settings.py mptt/signals.py mptt/utils.py mptt/locale/de/LC_MESSAGES/django.mo mptt/locale/de/LC_MESSAGES/django.po mptt/locale/dk/LC_MESSAGES/django.mo mptt/locale/dk/LC_MESSAGES/django.po mptt/locale/es/LC_MESSAGES/django.mo mptt/locale/es/LC_MESSAGES/django.po mptt/locale/es_AR/LC_MESSAGES/django.mo mptt/locale/es_AR/LC_MESSAGES/django.po mptt/locale/fr/LC_MESSAGES/django.mo mptt/locale/fr/LC_MESSAGES/django.po mptt/locale/mn/LC_MESSAGES/django.mo mptt/locale/mn/LC_MESSAGES/django.po mptt/locale/nb/LC_MESSAGES/django.mo mptt/locale/nb/LC_MESSAGES/django.po mptt/locale/pl/LC_MESSAGES/django.mo mptt/locale/pl/LC_MESSAGES/django.po mptt/locale/pt_BR/LC_MESSAGES/django.mo mptt/locale/pt_BR/LC_MESSAGES/django.po mptt/locale/ru/LC_MESSAGES/django.mo mptt/locale/ru/LC_MESSAGES/django.po mptt/static/mptt/arrow-move.png mptt/static/mptt/disclosure-down.png mptt/static/mptt/disclosure-right.png mptt/static/mptt/draggable-admin.css mptt/static/mptt/draggable-admin.js mptt/templates/admin/grappelli_mptt_change_list.html mptt/templates/admin/grappelli_mptt_change_list_results.html mptt/templates/admin/mptt_change_list.html mptt/templates/admin/mptt_change_list_results.html mptt/templatetags/__init__.py mptt/templatetags/mptt_admin.py mptt/templatetags/mptt_tags.py tests/.coveragerc tests/.gitignore tests/__init__.py tests/mydatabase tests/requirements.txt tests/runtests.sh tests/settings.py tests/myapp/__init__.py tests/myapp/admin.py tests/myapp/doctests.txt tests/myapp/models.py tests/myapp/tests.py tests/myapp/urls.py tests/myapp/fixtures/categories.json tests/myapp/fixtures/genres.json tests/myapp/fixtures/items.json tests/myapp/fixtures/persons.jsonpython-django-mptt-0.8.5/django_mptt.egg-info/dependency_links.txt000066400000000000000000000000011275105100200253260ustar00rootroot00000000000000 python-django-mptt-0.8.5/django_mptt.egg-info/top_level.txt000066400000000000000000000000051275105100200240050ustar00rootroot00000000000000mptt python-django-mptt-0.8.5/docs/000077500000000000000000000000001275105100200162105ustar00rootroot00000000000000python-django-mptt-0.8.5/docs/Makefile000066400000000000000000000110111275105100200176420ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build/docs # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-mptt.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-mptt.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-mptt" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-mptt" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." python-django-mptt-0.8.5/docs/admin.rst000066400000000000000000000105231275105100200200330ustar00rootroot00000000000000============= Admin classes ============= ``mptt.admin.MPTTModelAdmin`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is a bare-bones tree admin. All it does is enforce ordering, and indent the nodes in the tree to make a pretty tree list view. .. image:: mpttmodeladmin-genres.png :align: center :width: 26.21cm :alt: MPTTModelAdmin screenshot Usage:: from django.contrib import admin from mptt.admin import MPTTModelAdmin from myproject.myapp.models import Node admin.site.register(Node, MPTTModelAdmin) You can change the indent pixels per level globally by putting this in your settings.py:: # default is 10 pixels MPTT_ADMIN_LEVEL_INDENT = 20 If you'd like to specify the pixel amount per Model, define an ``mptt_level_indent`` attribute in your MPTTModelAdmin:: from django.contrib import admin from mptt.admin import MPTTModelAdmin from myproject.myapp.models import Node class CustomMPTTModelAdmin(MPTTModelAdmin): # specify pixel amount for this ModelAdmin only: mptt_level_indent = 20 admin.site.register(Node, CustomMPTTModelAdmin) If you'd like to specify which field should be indented, add an ``mptt_indent_field`` to your MPTTModelAdmin:: # … class CustomMPTTModelAdmin(MPTTModelAdmin): mptt_indent_field = "some_node_field" # … ``mptt.admin.DraggableMPTTAdmin`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 0.8.1 .. image:: draggablempttadmin-genres.png :align: center :width: 26.39cm :alt: DraggableMPTTAdmin screenshot This is a tree admin based on FeinCMS_ offering drag-drop functionality for moving nodes:: from django.contrib import admin from mptt.admin import DraggableMPTTAdmin from myproject.myapp.models import Node admin.site.register( Node, DraggableMPTTAdmin, list_display=( 'tree_actions', 'indented_title', # ...more fields if you feel like it... ), list_display_links=( 'indented_title', ), ) .. note:: Supported browsers include all recent versions of Firefox, Chrome, Safari and Internet Explorer (9 or better). .. warning:: Does not work well with big trees (more than a few hundred nodes, or trees deeper than 10 levels). Patches implementing lazy-loading of deep trees are very much appreciated. It is recommended that ``tree_actions`` is the first value passed to ``list_display``; this also requires you to specify ``list_display_links`` because ``tree_actions`` cannot be used as the object link field. ``indented_title`` does nothing but return the indented self-description of nodes, ``20px`` per level (or the value of ``mptt_level_indent``, see below.) ``list_per_page`` is set to 2000 by default (which effectively disables pagination for most trees). Replacing ``indented_title`` ---------------------------- If you want to replace the ``indented_title`` method with your own, we recommend using the following code:: from django.utils.html import format_html class MyDraggableMPTTAdmin(DraggableMPTTAdmin): list_display = ('tree_actions', 'something') list_display_links = ('something',) def something(self, instance): return format_html( '
{}
', instance._mpttfield('level') * self.mptt_level_indent, item.name, # Or whatever you want to put here ) something.short_description = _('something nice') For changing the indentation per node, look below. Simply replacing ``indented_title`` is insufficient because the indentation also needs to be communicated to the JavaScript code. Overriding admin templates per app or model ------------------------------------------- ``DraggableMPTTAdmin`` uses the stock admin changelist template with some CSS and JavaScript on top, so simply follow the official guide for `overriding admin templates`_. Changing the indentation of nodes --------------------------------- Simply set ``mptt_level_indent`` to a different pixel value (defaults to ``20``):: # ... class MyDraggableMPTTAdmin(DraggableMPTTAdmin): mptt_level_indent = 50 # ... .. _overriding admin templates: https://docs.djangoproject.com/en/1.9/ref/contrib/admin/#overriding-admin-templates .. _FeinCMS: https://github.com/feincms/feincms/ python-django-mptt-0.8.5/docs/autogenerated.rst000066400000000000000000000005141275105100200215710ustar00rootroot00000000000000 Autogenerated documentation =========================== These docs are generated by Sphinx from the docstrings in ``django-mptt``. They're not necessarily very helpful. You might be just as well off reading the `source code`_. .. toctree:: :maxdepth: 3 mptt .. _`source code`: http://github.com/django-mptt/django-mptt/ python-django-mptt-0.8.5/docs/conf.py000066400000000000000000000161051275105100200175120ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals # # django-mptt documentation build configuration file, created by # sphinx-quickstart on Wed Sep 8 20:11:06 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'django-mptt' copyright = '2007 - 2013, Craig de Stigter, Jonathan Buchanan and others' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version_tuple = __import__('mptt').VERSION version = ".".join(str(v) for v in version_tuple) # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'django-mpttdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'django-mptt.tex', 'django-mptt Documentation', 'Craig de Stigter', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'django-mptt', 'django-mptt Documentation', ['Craig de Stigter'], 1) ] python-django-mptt-0.8.5/docs/forms.rst000066400000000000000000000124761275105100200201020ustar00rootroot00000000000000================================== Working with trees in Django forms ================================== .. contents:: :depth: 3 Fields ====== The following custom form fields are provided in the ``mptt.forms`` package. ``TreeNodeChoiceField`` ----------------------- This is the default formfield used by ``TreeForeignKey`` A subclass of `ModelChoiceField`_ which represents the tree level of each node when generating option labels. For example, where a form which used a ``ModelChoiceField``:: category = ModelChoiceField(queryset=Category.objects.all()) ...would result in a select with the following options:: --------- Root 1 Child 1.1 Child 1.1.1 Root 2 Child 2.1 Child 2.1.1 Using a ``TreeNodeChoiceField`` instead:: category = TreeNodeChoiceField(queryset=Category.objects.all()) ...would result in a select with the following options:: Root 1 --- Child 1.1 ------ Child 1.1.1 Root 2 --- Child 2.1 ------ Child 2.1.1 The text used to indicate a tree level can by customised by providing a ``level_indicator`` argument:: category = TreeNodeChoiceField(queryset=Category.objects.all(), level_indicator=u'+--') ...which for this example would result in a select with the following options:: Root 1 +-- Child 1.1 +--+-- Child 1.1.1 Root 2 +-- Child 2.1 +--+-- Child 2.1.1 .. _`ModelChoiceField`: http://docs.djangoproject.com/en/dev/ref/forms/fields/#django.forms.ModelChoiceField ``TreeNodeMultipleChoiceField`` ------------------------------- Just like ``TreeNodeChoiceField``, but accepts more than one value. ``TreeNodePositionField`` ------------------------- A subclass of `ChoiceField`_ whose choices default to the valid arguments for the `move_to method`_. .. _`ChoiceField`: http://docs.djangoproject.com/en/dev/ref/forms/fields/#choicefield Forms ===== The following custom form is provided in the ``mptt.forms`` package. ``MoveNodeForm`` ---------------- A form which allows the user to move a given node from one location in its tree to another, with optional restriction of the nodes which are valid target nodes for the `move_to method` Fields ~~~~~~ The form contains the following fields: * ``target`` -- a ``TreeNodeChoiceField`` for selecting the target node for the node movement. Target nodes will be displayed as a single `` {% for node,structure in classifiers|tree_info:"ancestors" %} {% if node.is_child_node %} {% endif %} {% endfor %} python-django-mptt-0.8.5/docs/tutorial.rst000066400000000000000000000113141275105100200206050ustar00rootroot00000000000000 ======== Tutorial ======== The Problem =========== You've created a Django project, and you need to manage some hierarchical data. For instance you've got a bunch of hierarchical pages in a CMS, and sometimes pages are *children* of other pages Now suppose you want to show a breadcrumb on your site, like this:: Home > Products > Food > Meat > Spam > Spammy McDelicious To get all those page titles you might do something like this:: titles = [] while page: titles.append(page.title) page = page.parent That's one database query for each page in the breadcrumb, and database queries are slow. Let's do this a better way. The Solution ============ Modified Preorder Tree Traversal can be a bit daunting at first, but it's one of the best ways to solve this problem. If you want to go into the details, there's a good explanation here: `Storing Hierarchical Data in a Database`_ or `Managing Hierarchical Data in Mysql`_ tl;dr: MPTT makes most tree operations much cheaper in terms of queries. In fact all these operations take at most one query, and sometimes zero: * get descendants of a node * get ancestors of a node * get all nodes at a given level * get leaf nodes And this one takes zero queries: * count the descendants of a given node .. _`Storing Hierarchical Data in a Database`: http://www.sitepoint.com/hierarchical-data-database/ .. _`Managing Hierarchical Data in Mysql`: http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/ Enough intro. Let's get started. Getting started =============== Add ``mptt`` To ``INSTALLED_APPS`` ---------------------------------- As with most Django applications, you should add ``mptt`` to the ``INSTALLED_APPS`` in your ``settings.py`` file:: INSTALLED_APPS = ( 'django.contrib.auth', # ... 'mptt', ) Set up your model ----------------- Start with a basic subclass of MPTTModel, something like this:: from django.db import models from mptt.models import MPTTModel, TreeForeignKey class Genre(MPTTModel): name = models.CharField(max_length=50, unique=True) parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True) class MPTTMeta: order_insertion_by = ['name'] You must define a parent field which is a ``TreeForeignKey`` to ``'self'``. A ``TreeForeignKey`` is just a regular ``ForeignKey`` that renders form fields differently in the admin and a few other places. Because you're inheriting from MPTTModel, your model will also have a number of other fields: ``level``, ``lft``, ``rght``, and ``tree_id``. These fields are managed by the MPTT algorithm. Most of the time you won't need to use these fields directly. That ``MPTTMeta`` class adds some tweaks to ``django-mptt`` - in this case, just ``order_insertion_by``. This indicates the natural ordering of the data in the tree. Now create and apply the migrations to create the table in the database:: python manage.py makemigrations python manage.py migrate Create some data ---------------- Fire up a django shell:: python manage.py shell Now create some data to test:: from myapp.models import Genre rock = Genre.objects.create(name="Rock") blues = Genre.objects.create(name="Blues") Genre.objects.create(name="Hard Rock", parent=rock) Genre.objects.create(name="Pop Rock", parent=rock) Make a view ----------- This one's pretty simple for now. Add this lightweight view to your ``views.py``:: def show_genres(request): return render_to_response("genres.html", {'nodes':Genre.objects.all()}, context_instance=RequestContext(request)) And add a URL for it in ``urls.py``:: (r'^genres/$', 'myapp.views.show_genres'), Template -------- .. highlightlang:: html+django ``django-mptt`` includes some template tags for making this bit easy too. Create a template called ``genres.html`` in your template directory and put this in it:: {% load mptt_tags %}
    {% recursetree nodes %}
  • {{ node.name }} {% if not node.is_leaf_node %}
      {{ children }}
    {% endif %}
  • {% endrecursetree %}
That recursetree tag will recursively render that template fragment for all the nodes. Try it out by going to ``/genres/``. There's more; `check out the docs`_ for custom admin-site stuff, more template tags, tree rebuild functions etc. Now you can stop thinking about how to do trees, and start making a great django app! .. _`check out the docs`: http://django-mptt.github.com/django-mptt/ python-django-mptt-0.8.5/docs/upgrade.rst000066400000000000000000000201021275105100200203640ustar00rootroot00000000000000============= Upgrade notes ============= 0.8.0 =================== Dropped support for old Django versions and Python 2.6 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unsupported versions of django (1.4, 1.5, 1.6, 1.7) are no longer supported, and Python 2.6 is no longer supported. These versions of python/django no longer receive security patches. You should upgrade to Python 2.7 and Django 1.8+. Django 1.9 support has been added. 0.7.0 ===== Dropped support for Django 1.5, Added support for 1.8 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django 1.5 support has been removed since django 1.5 is not supported upstream any longer. Django 1.8 support has been added. Deprecated: Calling ``recursetree``/``cache_tree_children`` with incorrectly-ordered querysets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Previously, when given a queryset argument, ``cache_tree_children`` called ``.order_by`` to ensure that the queryset was in the correct order. In 0.7, calling ``cache_tree_children`` with an incorrectly-ordered queryset will cause a deprecation warning. In 0.8, it will raise an error. This also applies to ``recursetree``, since it calls ``cache_tree_children``. This probably doesn't affect many usages, since the default ordering for mptt models will work fine. Minor: ``TreeManager.get_queryset`` no longer provided on Django < 1.6 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django renamed ``get_query_set`` to ``get_queryset`` in Django 1.6. For backward compatibility django-mptt had both methods available for 1.4-1.5 users. This has been removed. You should use ``get_query_set`` on Django 1.4-1.5, and ``get_queryset`` if you're on 1.6+. Removed FeinCMSModelAdmin ~~~~~~~~~~~~~~~~~~~~~~~~~ Deprecated in 0.6.0, this has now been removed. 0.6.0 ===== mptt now requires Python 2.6+, and supports Python 3.2+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ mptt 0.6 drops support for both Python 2.4 and 2.5. This was done to make it easier to support Python 3, as well as support the new context managers (delay_mptt_updates and disable_mptt_updates). If you absolutely can't upgrade your Python version, you'll need to stick to mptt 0.5.5 until you can. No more implicit ``empty_label=True`` on form fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Until 0.5, ``TreeNodeChoiceField`` and ``TreeNodeMultipleChoiceField`` implicitly set ``empty_label=True``. This was around since a long time ago, for unknown reasons. It has been removed in 0.6.0 as it caused occasional headaches for users. If you were relying on this behavior, you'll need to explicitly pass ``empty_label=True`` to any of those fields you use, otherwise you will start seeing new '--------' choices appearing in them. Deprecated FeinCMSModelAdmin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you were using ``mptt.admin.FeinCMSModelAdmin``, you should switch to using ``feincms.admin.tree_editor.TreeEditor`` instead, or you'll get a loud deprecation warning. 0.4.2 to 0.5.5 ============== ``TreeManager`` is now the default manager, ``YourModel.tree`` removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In 0.5, ``TreeManager`` now behaves just like a normal django manager. If you don't override anything, you'll now get a ``TreeManager`` by default (``.objects``.) Before 0.5, ``.tree`` was the default name for the ``TreeManager``. That's been removed, so we recommend updating your code to use ``.objects``. If you don't want to update ``.tree`` to ``.objects`` everywhere just yet, you should add an explicit ``TreeManager`` to your models:: objects = tree = TreeManager() ``save(raw=True)`` keyword argument removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In earlier versions, MPTTModel.save() had a ``raw`` keyword argument. If True, the MPTT fields would not be updated during the save. This (undocumented) argument has now been removed. ``_meta`` attributes moved to ``_mptt_meta`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In 0.4, we deprecated all these attributes on model._meta. These have now been removed:: MyModel._meta.left_attr MyModel._meta.right_attr MyModel._meta.tree_id_attr MyModel._meta.level_attr MyModel._meta.tree_manager_attr MyModel._meta.parent_attr MyModel._meta.order_insertion_by If you're still using any of these, you'll need to update by simply renaming ``_meta`` to ``_mptt_meta``. Running the tests ~~~~~~~~~~~~~~~~~ Tests are now run with:: cd tests/ ./runtests.sh The previous method (``python setup.py test``) no longer works since we switched to plain distutils. 0.3 to 0.4.2 ============ Model changes ~~~~~~~~~~~~~ MPTT attributes on ``MyModel._meta`` deprecated, moved to ``MyModel._mptt_meta`` ---------------------------------------------------------------------------------- Most people won't need to worry about this, but if you're using any of the following, note that these are deprecated and will be removed in 0.5:: MyModel._meta.left_attr MyModel._meta.right_attr MyModel._meta.tree_id_attr MyModel._meta.level_attr MyModel._meta.tree_manager_attr MyModel._meta.parent_attr MyModel._meta.order_insertion_by They'll continue to work as previously for now, but you should upgrade your code if you can. Simply replace ``_meta`` with ``_mptt_meta``. Use model inheritance where possible ------------------------------------ The preferred way to do model registration in ``django-mptt`` 0.4 is via model inheritance. Suppose you start with this:: class Node(models.Model): ... mptt.register(Node, order_insertion_by=['name'], parent_attr='padre') First, Make your model a subclass of ``MPTTModel``, instead of ``models.Model``:: from mptt.models import MPTTModel class Node(MPTTModel): ... Then remove your call to ``mptt.register()``. If you were passing it keyword arguments, you should add them to an ``MPTTMeta`` inner class on the model:: class Node(MPTTModel): ... class MPTTMeta: order_insertion_by = ['name'] parent_attr = 'padre' If necessary you can still use ``mptt.register``. It was removed in 0.4.0 but restored in 0.4.2, since people reported use cases that didn't work without it.) For instance, if you need to register models where the code isn't under your control, you'll need to use ``mptt.register()``. Behind the scenes, ``mptt.register()`` in 0.4 will actually add MPTTModel to ``Node.__bases__``, thus achieving the same result as subclassing ``MPTTModel``. If you're already inheriting from something other than ``Model``, that means multiple inheritance. You're probably all upgraded at this point :) A couple more notes for more complex scenarios: More complicated scenarios ~~~~~~~~~~~~~~~~~~~~~~~~~~ What if I'm already inheriting from something? ---------------------------------------------- If your model is already a subclass of an abstract model, you should use multiple inheritance:: class Node(MPTTModel, ParentModel): ... You should always put MPTTModel as the first model base. This is because there's some complicated metaclass stuff going on behind the scenes, and if Django's model metaclass gets called before the MPTT one, strange things can happen. Isn't multiple inheritance evil? Well, maybe. However, the `Django model docs`_ don't forbid this, and as long as your other model doesn't have conflicting methods, it should be fine. .. note:: As always when dealing with multiple inheritance, approach with a bit of caution. Our brief testing says it works, but if you find that the Django internals are somehow breaking this approach for you, please `create an issue`_ with specifics. .. _`create an issue`: http://github.com/django-mptt/django-mptt/issues .. _`Django model docs`: http://docs.djangoproject.com/en/dev/topics/db/models/#multiple-inheritance Compatibility with 0.3 ---------------------- ``MPTTModel`` was added in 0.4. If you're writing a library or reusable app that needs to work with 0.3, you should use the ``mptt.register()`` function instead, as above. python-django-mptt-0.8.5/docs/utilities.rst000066400000000000000000000071701275105100200207620ustar00rootroot00000000000000================================ Utilities for working with trees ================================ .. contents:: :depth: 3 List/tree utilities =================== The ``mptt.utils`` module contains the following functions for working with and creating lists of model instances which represent trees. ``previous_current_next()`` --------------------------- From http://www.wordaligned.org/articles/zippy-triples-served-with-python Creates an iterator which returns (previous, current, next) triples, with ``None`` filling in when there is no previous or next available. This function is useful if you want to step through a tree one item at a time and you need to refer to the previous or next item in the tree. It is used in the implementation of `tree_item_iterator()`_. Required arguments ~~~~~~~~~~~~~~~~~~ ``items`` A list or other iterable item. ``tree_item_iterator()`` ------------------------ This function is used to implement the ``tree_info`` template filter, yielding two-tuples of (tree item, tree structure information ``dict``). See the ``tree_info`` documentation for more information. Required arguments ~~~~~~~~~~~~~~~~~~ ``items`` A list or iterable of model instances which represent a tree. Optional arguments ~~~~~~~~~~~~~~~~~~ ``ancestors`` Boolean. If ``True``, a list of unicode representations of the ancestors of the current node, in descending order (root node first, immediate parent last), will be added to the tree structure information ``dict` under the key ``'ancestors'``. ``drilldown_tree_for_node()`` ----------------------------- This function is used in the implementation of the ``drilldown_tree_for_node`` template tag. It creates an iterable which yields model instances representing a drilldown tree for a given node. A drilldown tree consists of a node's ancestors, itself and its immediate children, all in tree order. Optional arguments may be given to specify details of a relationship between the given node's class and another model class, for the purpose of adding related item counts to the node's children. Required arguments ~~~~~~~~~~~~~~~~~~ ``node`` A model instance which represents a node in a tree. Optional arguments ~~~~~~~~~~~~~~~~~~ ``rel_cls`` A model class which has a relationship to the node's class. ``rel_field`` The name of the field in ``rel_cls`` which holds the relationship to the node's class. ``count_attr`` The name of an attribute which should be added to each child of the node in the drilldown tree (if any), containing a count of how many instances of ``rel_cls`` are related to it through ``rel_field``. ``cumulative`` If ``True``, the count will be for items related to the child node *and* all of its descendants. Defaults to ``False``. ``get_cached_trees()`` ----------------------------- Takes a list/queryset of model objects in MPTT left (depth-first) order and caches the children and parent on each node. This allows up and down traversal through the tree without the need for further queries. Use cases include using a recursively included template or arbitrarily traversing trees. Returns a list of top-level nodes. If a single tree was provided in its entirety, the list will of course consist of just the tree's root node. Aliases to this function are also available: ``mptt.templatetags.mptt_tag.cache_tree_children`` Use for recursive rendering in templates. ``mptt.querysets.TreeQuerySet.get_cached_trees`` Useful for chaining with queries; e.g., `Node.objects.filter(**kwargs).get_cached_trees()` Required arguments ~~~~~~~~~~~~~~~~~~ ``queryset`` An iterable that consists of all nodes which are to be cached. python-django-mptt-0.8.5/mptt/000077500000000000000000000000001275105100200162445ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/__init__.py000066400000000000000000000010731275105100200203560ustar00rootroot00000000000000from __future__ import unicode_literals VERSION = (0, 8, 5) __version__ = '.'.join(str(v) for v in VERSION) def register(*args, **kwargs): """ Registers a model class as an MPTTModel, adding MPTT fields and adding MPTTModel to __bases__. This is equivalent to just subclassing MPTTModel, but works for an already-created model. """ from mptt.models import MPTTModelBase return MPTTModelBase.register(*args, **kwargs) class AlreadyRegistered(Exception): "Deprecated - don't use this anymore. It's never thrown, you don't need to catch it" python-django-mptt-0.8.5/mptt/admin.py000066400000000000000000000233541275105100200177150ustar00rootroot00000000000000from __future__ import unicode_literals import json from django import http from django.conf import settings from django.contrib.admin.actions import delete_selected from django.contrib.admin.options import ModelAdmin from django.db import IntegrityError, transaction from django.forms.utils import flatatt from django.templatetags.static import static from django.utils.encoding import force_text from django.utils.html import format_html, mark_safe from django.utils.translation import ugettext as _, ugettext_lazy from mptt.exceptions import InvalidMove from mptt.forms import MPTTAdminForm, TreeNodeChoiceField from mptt.models import MPTTModel, TreeForeignKey __all__ = ('MPTTModelAdmin', 'MPTTAdminForm', 'DraggableMPTTAdmin') IS_GRAPPELLI_INSTALLED = 'grappelli' in settings.INSTALLED_APPS class MPTTModelAdmin(ModelAdmin): """ A basic admin class that displays tree items according to their position in the tree. No extra editing functionality beyond what Django admin normally offers. """ if IS_GRAPPELLI_INSTALLED: change_list_template = 'admin/grappelli_mptt_change_list.html' else: change_list_template = 'admin/mptt_change_list.html' form = MPTTAdminForm def formfield_for_foreignkey(self, db_field, request, **kwargs): if issubclass(db_field.rel.to, MPTTModel) \ and not isinstance(db_field, TreeForeignKey) \ and db_field.name not in self.raw_id_fields: db = kwargs.get('using') limit_choices_to = db_field.get_limit_choices_to() defaults = dict( form_class=TreeNodeChoiceField, queryset=db_field.rel.to._default_manager.using( db).complex_filter(limit_choices_to), required=False) defaults.update(kwargs) kwargs = defaults return super(MPTTModelAdmin, self).formfield_for_foreignkey( db_field, request, **kwargs) def get_ordering(self, request): """ Changes the default ordering for changelists to tree-order. """ mptt_opts = self.model._mptt_meta return self.ordering or (mptt_opts.tree_id_attr, mptt_opts.left_attr) def delete_selected_tree(self, modeladmin, request, queryset): """ Deletes multiple instances and makes sure the MPTT fields get recalculated properly. (Because merely doing a bulk delete doesn't trigger the post_delete hooks.) """ # If this is True, the confirmation page has been displayed if request.POST.get('post'): n = 0 with queryset.model._tree_manager.delay_mptt_updates(): for obj in queryset: if self.has_delete_permission(request, obj): obj.delete() n += 1 obj_display = force_text(obj) self.log_deletion(request, obj, obj_display) self.message_user( request, _('Successfully deleted %(count)d items.') % {'count': n}) # Return None to display the change list page again return None else: # (ab)using the built-in action to display the confirmation page return delete_selected(self, request, queryset) def get_actions(self, request): actions = super(MPTTModelAdmin, self).get_actions(request) if 'delete_selected' in actions: actions['delete_selected'] = ( self.delete_selected_tree, 'delete_selected', _('Delete selected %(verbose_name_plural)s')) return actions class JS(object): """ Use this to insert a script tag via ``forms.Media`` containing additional attributes (such as ``id`` and ``data-*`` for CSP-compatible data injection.):: media.add_js([ JS('asset.js', { 'id': 'asset-script', 'data-the-answer': '"42"', }), ]) The rendered media tag (via ``{{ media.js }}`` or ``{{ media }}`` will now contain a script tag as follows, without line breaks:: The attributes are automatically escaped. The data attributes may now be accessed inside ``asset.js``:: var answer = document.querySelector('#asset-script').dataset.answer; """ def __init__(self, js, attrs): self.js = js self.attrs = attrs def startswith(self, _): # Masquerade as absolute path so that we are returned as-is. return True def __html__(self): return format_html( '{}"{}', static(self.js), mark_safe(flatatt(self.attrs)), ).rstrip('"') class DraggableMPTTAdmin(MPTTModelAdmin): """ The ``DraggableMPTTAdmin`` modifies the standard Django administration change list to a drag-drop enabled interface. """ change_list_template = None # Back to default list_per_page = 2000 # This will take a really long time to load. list_display = ('tree_actions', 'indented_title') # Sane defaults. list_display_links = ('indented_title',) # Sane defaults. mptt_level_indent = 20 def tree_actions(self, item): try: url = item.get_absolute_url() except Exception: # Nevermind. url = '' return format_html( '
' '
', item.pk, item._mpttfield('level'), url, ) tree_actions.short_description = '' def indented_title(self, item): """ Generate a short title for an object, indent it depending on the object's depth in the hierarchy. """ return format_html( '
{}
', item._mpttfield('level') * self.mptt_level_indent, item, ) indented_title.short_description = ugettext_lazy('title') def changelist_view(self, request, *args, **kwargs): if request.is_ajax() and request.POST.get('cmd') == 'move_node': return self._move_node(request) response = super(DraggableMPTTAdmin, self).changelist_view( request, *args, **kwargs) try: response.context_data['media'].add_css({'all': ( 'mptt/draggable-admin.css', )}) response.context_data['media'].add_js(( JS('mptt/draggable-admin.js', { 'id': 'draggable-admin-context', 'data-context': json.dumps(self._tree_context(request)), }), ),) except (AttributeError, KeyError): # Not meant for us if there is no context_data attribute (no # TemplateResponse) or no media in the context. pass return response @transaction.atomic def _move_node(self, request): position = request.POST.get('position') if position not in ('last-child', 'left', 'right'): self.message_user(request, _('Did not understand moving instruction.')) return http.HttpResponse('FAIL, unknown instruction.') queryset = self.get_queryset(request) try: cut_item = queryset.get(pk=request.POST.get('cut_item')) pasted_on = queryset.get(pk=request.POST.get('pasted_on')) except (self.model.DoesNotExist, TypeError, ValueError): self.message_user(request, _('Objects have disappeared, try again.')) return http.HttpResponse('FAIL, invalid objects.') if not self.has_change_permission(request, cut_item): self.message_user(request, _('No permission')) return http.HttpResponse('FAIL, no permission.') try: self.model._tree_manager.move_node(cut_item, pasted_on, position) except InvalidMove as e: self.message_user(request, '%s' % e) return http.HttpResponse('FAIL, invalid move.') except IntegrityError as e: self.message_user(request, _('Database error: %s') % e) raise self.message_user( request, _('%s has been successfully moved.') % cut_item) return http.HttpResponse('OK, moved.') def _tree_context(self, request): opts = self.model._meta return { 'storageName': 'tree_%s_%s_collapsed' % (opts.app_label, opts.model_name), 'treeStructure': self._build_tree_structure(self.get_queryset(request)), 'levelIndent': self.mptt_level_indent, 'messages': { 'before': _('move node before node'), 'child': _('move node to child position'), 'after': _('move node after node'), 'collapseTree': _('Collapse tree'), 'expandTree': _('Expand tree'), }, } def _build_tree_structure(self, queryset): """ Build an in-memory representation of the item tree, trying to keep database accesses down to a minimum. The returned dictionary looks like this (as json dump): {"6": [7, 8, 10] "7": [12], ... } Leaves are not included in the dictionary. """ all_nodes = {} mptt_opts = self.model._mptt_meta items = queryset.values_list( 'pk', '%s_id' % mptt_opts.parent_attr, ) for p_id, parent_id in items: all_nodes.setdefault( str(parent_id) if parent_id else 0, [], ).append(p_id) return all_nodes python-django-mptt-0.8.5/mptt/exceptions.py000066400000000000000000000006571275105100200210070ustar00rootroot00000000000000""" MPTT exceptions. """ from __future__ import unicode_literals class InvalidMove(Exception): """ An invalid node move was attempted. For example, attempting to make a node a child of itself. """ pass class CantDisableUpdates(Exception): """ User tried to disable updates on a model that doesn't support it (abstract, proxy or a multiple-inheritance subclass of an MPTTModel) """ pass python-django-mptt-0.8.5/mptt/fields.py000066400000000000000000000030701275105100200200640ustar00rootroot00000000000000""" Model fields for working with trees. """ from __future__ import unicode_literals from django.db import models from django.conf import settings from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField __all__ = ('TreeForeignKey', 'TreeOneToOneField', 'TreeManyToManyField') class TreeForeignKey(models.ForeignKey): """ Extends the foreign key, but uses mptt's ``TreeNodeChoiceField`` as the default form field. This is useful if you are creating models that need automatically generated ModelForms to use the correct widgets. """ def formfield(self, **kwargs): """ Use MPTT's ``TreeNodeChoiceField`` """ kwargs.setdefault('form_class', TreeNodeChoiceField) return super(TreeForeignKey, self).formfield(**kwargs) class TreeOneToOneField(models.OneToOneField): def formfield(self, **kwargs): kwargs.setdefault('form_class', TreeNodeChoiceField) return super(TreeOneToOneField, self).formfield(**kwargs) class TreeManyToManyField(models.ManyToManyField): def formfield(self, **kwargs): kwargs.setdefault('form_class', TreeNodeMultipleChoiceField) return super(TreeManyToManyField, self).formfield(**kwargs) # South integration if 'south' in settings.INSTALLED_APPS: # pragma: no cover from south.modelsinspector import add_introspection_rules add_introspection_rules([], ["^mptt\.fields\.TreeForeignKey"]) add_introspection_rules([], ["^mptt\.fields\.TreeOneToOneField"]) add_introspection_rules([], ["^mptt\.fields\.TreeManyToManyField"]) python-django-mptt-0.8.5/mptt/forms.py000066400000000000000000000157561275105100200177620ustar00rootroot00000000000000""" Form components for working with trees. """ from __future__ import unicode_literals from django import forms from django.forms.forms import NON_FIELD_ERRORS from django.utils.encoding import smart_text from django.utils.html import conditional_escape, mark_safe from django.utils.translation import ugettext_lazy as _ from mptt.exceptions import InvalidMove from mptt.settings import DEFAULT_LEVEL_INDICATOR __all__ = ( 'TreeNodeChoiceField', 'TreeNodeMultipleChoiceField', 'TreeNodePositionField', 'MoveNodeForm', ) # Fields ###################################################################### class TreeNodeChoiceFieldMixin(object): def __init__(self, queryset, *args, **kwargs): self.level_indicator = kwargs.pop('level_indicator', DEFAULT_LEVEL_INDICATOR) # if a queryset is supplied, enforce ordering if hasattr(queryset, 'model'): mptt_opts = queryset.model._mptt_meta queryset = queryset.order_by(mptt_opts.tree_id_attr, mptt_opts.left_attr) super(TreeNodeChoiceFieldMixin, self).__init__(queryset, *args, **kwargs) def _get_level_indicator(self, obj): level = getattr(obj, obj._mptt_meta.level_attr) return mark_safe(conditional_escape(self.level_indicator) * level) def label_from_instance(self, obj): """ Creates labels which represent the tree level of each node when generating option labels. """ level_indicator = self._get_level_indicator(obj) return mark_safe(level_indicator + ' ' + conditional_escape(smart_text(obj))) class TreeNodeChoiceField(TreeNodeChoiceFieldMixin, forms.ModelChoiceField): """A ModelChoiceField for tree nodes.""" class TreeNodeMultipleChoiceField(TreeNodeChoiceFieldMixin, forms.ModelMultipleChoiceField): """A ModelMultipleChoiceField for tree nodes.""" class TreeNodePositionField(forms.ChoiceField): """A ChoiceField for specifying position relative to another node.""" FIRST_CHILD = 'first-child' LAST_CHILD = 'last-child' LEFT = 'left' RIGHT = 'right' DEFAULT_CHOICES = ( (FIRST_CHILD, _('First child')), (LAST_CHILD, _('Last child')), (LEFT, _('Left sibling')), (RIGHT, _('Right sibling')), ) def __init__(self, *args, **kwargs): if 'choices' not in kwargs: kwargs['choices'] = self.DEFAULT_CHOICES super(TreeNodePositionField, self).__init__(*args, **kwargs) # Forms ####################################################################### class MoveNodeForm(forms.Form): """ A form which allows the user to move a given node from one location in its tree to another, with optional restriction of the nodes which are valid target nodes for the move. """ target = TreeNodeChoiceField(queryset=None) position = TreeNodePositionField() def __init__(self, node, *args, **kwargs): """ The ``node`` to be moved must be provided. The following keyword arguments are also accepted:: ``valid_targets`` Specifies a ``QuerySet`` of valid targets for the move. If not provided, valid targets will consist of everything other node of the same type, apart from the node itself and any descendants. For example, if you want to restrict the node to moving within its own tree, pass a ``QuerySet`` containing everything in the node's tree except itself and its descendants (to prevent invalid moves) and the root node (as a user could choose to make the node a sibling of the root node). ``target_select_size`` The size of the select element used for the target node. Defaults to ``10``. ``position_choices`` A tuple of allowed position choices and their descriptions. Defaults to ``TreeNodePositionField.DEFAULT_CHOICES``. ``level_indicator`` A string which will be used to represent a single tree level in the target options. """ self.node = node valid_targets = kwargs.pop('valid_targets', None) target_select_size = kwargs.pop('target_select_size', 10) position_choices = kwargs.pop('position_choices', None) level_indicator = kwargs.pop('level_indicator', None) super(MoveNodeForm, self).__init__(*args, **kwargs) opts = node._mptt_meta if valid_targets is None: valid_targets = node._tree_manager.exclude(**{ opts.tree_id_attr: getattr(node, opts.tree_id_attr), opts.left_attr + '__gte': getattr(node, opts.left_attr), opts.right_attr + '__lte': getattr(node, opts.right_attr), }) self.fields['target'].queryset = valid_targets self.fields['target'].widget.attrs['size'] = target_select_size if level_indicator: self.fields['target'].level_indicator = level_indicator if position_choices: self.fields['position'].choices = position_choices def save(self): """ Attempts to move the node using the selected target and position. If an invalid move is attempted, the related error message will be added to the form's non-field errors and the error will be re-raised. Callers should attempt to catch ``InvalidNode`` to redisplay the form with the error, should it occur. """ try: self.node.move_to(self.cleaned_data['target'], self.cleaned_data['position']) return self.node except InvalidMove as e: self.errors[NON_FIELD_ERRORS] = self.error_class(e) raise class MPTTAdminForm(forms.ModelForm): """ A form which validates that the chosen parent for a node isn't one of its descendants. """ def __init__(self, *args, **kwargs): super(MPTTAdminForm, self).__init__(*args, **kwargs) if self.instance and self.instance.pk: instance = self.instance opts = self._meta.model._mptt_meta parent_field = self.fields.get(opts.parent_attr) if parent_field: parent_qs = parent_field.queryset parent_qs = parent_qs.exclude( pk__in=instance.get_descendants( include_self=True ).values_list('pk', flat=True) ) parent_field.queryset = parent_qs def clean(self): cleaned_data = super(MPTTAdminForm, self).clean() opts = self._meta.model._mptt_meta parent = cleaned_data.get(opts.parent_attr) if self.instance and parent: if parent.is_descendant_of(self.instance, include_self=True): if opts.parent_attr not in self._errors: self._errors[opts.parent_attr] = self.error_class() self._errors[opts.parent_attr].append(_('Invalid parent')) del self.cleaned_data[opts.parent_attr] return cleaned_data python-django-mptt-0.8.5/mptt/locale/000077500000000000000000000000001275105100200175035ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/de/000077500000000000000000000000001275105100200200735ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/de/LC_MESSAGES/000077500000000000000000000000001275105100200216605ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/de/LC_MESSAGES/django.mo000066400000000000000000000056111275105100200234620ustar00rootroot00000000000000l698)r;+"2' Z f)q@:6FNLEF(Co&5:KCk9GM1-C  @6 <w 8 I M7 D I H +]      %s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.First childLast childThe model %s has already been registered.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2009-09-23 14:44+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %s Tag benötigt entweder drei, sieben oder acht Argumente%s Tag benötigt drei ArgumenteEin Element kann nicht Unterelement eines seiner Unterlemente sein.Ein Element kann nicht Unterelement von sich selbst sein.Ein Element kann nicht ein Geschwister eines seiner Unterelemente sein.Ein Element kann nicht in ein Geschwister von sich selbst umgewandelt werden.Eine ungültige Position wurde angegeben: %s.Kann ein Element, welches schon gespeichert wurde, nicht einfügen.Erstes UnterelementLetztes Unterelement%s wurde schon registriert.drilldown_tree_for_node Tag bekam ein ungültiges Modellfeld: %sdrilldown_tree_for_node tag bekam ein ungültiges Modell: %sfull_tree_for_model Tag bekam ein ungültiges Modell: %swenn '%s' acht Argumente übergeben werden, muss das fünfte 'count' seinwenn '%s' acht Argumente übergeben werden, muss das vierte 'cumulative' seinwenn '%s' acht Argumente übergeben werden, muss das achte 'in' seinwenn '%s' sieben Argumente übergeben werden, muss das vierte 'with' seinwenn '%s' sieben Argumente übergeben werden, muss das sechste 'in' seinZweites Argument für %s Tag muss 'as' seinpython-django-mptt-0.8.5/mptt/locale/de/LC_MESSAGES/django.po000066400000000000000000000077271275105100200234770ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2009-09-23 14:44+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: __init__.py:34 #, python-format msgid "The model %s has already been registered." msgstr "%s wurde schon registriert." #: forms.py:41 msgid "First child" msgstr "Erstes Unterelement" #: forms.py:42 msgid "Last child" msgstr "Letztes Unterelement" #: forms.py:43 msgid "Left sibling" msgstr "" #: forms.py:44 msgid "Right sibling" msgstr "" #: managers.py:121 msgid "Cannot insert a node which has already been saved." msgstr "Kann ein Element, welches schon gespeichert wurde, nicht einfügen." #: managers.py:306 managers.py:480 managers.py:516 managers.py:673 #, python-format msgid "An invalid position was given: %s." msgstr "Eine ungültige Position wurde angegeben: %s." #: managers.py:466 managers.py:653 msgid "A node may not be made a sibling of itself." msgstr "Ein Element kann nicht in ein Geschwister von sich selbst umgewandelt werden." #: managers.py:632 managers.py:753 msgid "A node may not be made a child of itself." msgstr "Ein Element kann nicht Unterelement von sich selbst sein." #: managers.py:634 managers.py:755 msgid "A node may not be made a child of any of its descendants." msgstr "Ein Element kann nicht Unterelement eines seiner Unterlemente sein." #: managers.py:655 msgid "A node may not be made a sibling of any of its descendants." msgstr "Ein Element kann nicht ein Geschwister eines seiner Unterelemente sein." #: templatetags/mptt_tags.py:23 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model Tag bekam ein ungültiges Modell: %s" #: templatetags/mptt_tags.py:44 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldown_tree_for_node tag bekam ein ungültiges Modell: %s" #: templatetags/mptt_tags.py:48 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldown_tree_for_node Tag bekam ein ungültiges Modellfeld: %s" #: templatetags/mptt_tags.py:72 #, python-format msgid "%s tag requires three arguments" msgstr "%s Tag benötigt drei Argumente" #: templatetags/mptt_tags.py:74 templatetags/mptt_tags.py:125 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "Zweites Argument für %s Tag muss 'as' sein" #: templatetags/mptt_tags.py:123 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s Tag benötigt entweder drei, sieben oder acht Argumente" #: templatetags/mptt_tags.py:128 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "wenn '%s' sieben Argumente übergeben werden, muss das vierte 'with' sein" #: templatetags/mptt_tags.py:130 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "wenn '%s' sieben Argumente übergeben werden, muss das sechste 'in' sein" #: templatetags/mptt_tags.py:134 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "wenn '%s' acht Argumente übergeben werden, muss das vierte 'cumulative' sein" #: templatetags/mptt_tags.py:136 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "wenn '%s' acht Argumente übergeben werden, muss das fünfte 'count' sein" #: templatetags/mptt_tags.py:138 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "wenn '%s' acht Argumente übergeben werden, muss das achte 'in' sein" python-django-mptt-0.8.5/mptt/locale/dk/000077500000000000000000000000001275105100200201015ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/dk/LC_MESSAGES/000077500000000000000000000000001275105100200216665ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/dk/LC_MESSAGES/django.mo000066400000000000000000000057051275105100200234740ustar00rootroot000000000000006 @9`);+",2O    )@:6ZFLE%FkC&59S#;)@.X#:   % CF > : P UU L N LG 0     %s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.First childLast childLeft siblingRight siblingThe model %s has already been registered.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2009-09-11 10:38+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %s tagget påkræver enten tre, syv eller otte argumenter%s tagget påkræver tre argumenterEn knude må ikke være barn af nogle af dets efterkommere.En knude må ikke være barn af sig selv.En knude må ikke være søskend med nogle af dets efterkommere.En knude må ikke være søskend til sig selv.En ugyldig position blev givet: %s.Kan ikke indsættte en knude, der allerede er blevet gemt.Første barnSidste barnVenstre søskendHøjre søskendDin model er allerede registreret %s.drilldown_tree_for_node tagget blev givet et ugyldigt modelfelt: %sdrilldown_tree_for_node tagget blev givet en ugyldig model: %sfull_tree_for_model tagget blev givet en ugyldig model: %shvis otte argumenter gives, skal det fjedre argument til %s tagget være 'count'hvis otte argumenter gives, skal det fjedre argument til %s tagget være 'cumulative'hvis otte argumenter gives, skal det syvne argument til %s tagget være 'in'hvis syv argumenter gives, skal det fjerde argument til %s tagget være 'with'hvis syv argumenter gives, skal det sjette argument til %s tagget være 'in'det andet argument til %s tagget skal være 'as'python-django-mptt-0.8.5/mptt/locale/dk/LC_MESSAGES/django.po000066400000000000000000000077611275105100200235030ustar00rootroot00000000000000# django-mptt in Danish. # django-mptt på Dansk. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Michael Lind Mortensen , 2009. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2009-09-11 10:38+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: __init__.py:34 #, python-format msgid "The model %s has already been registered." msgstr "Din model er allerede registreret %s." #: forms.py:40 msgid "First child" msgstr "Første barn" #: forms.py:41 msgid "Last child" msgstr "Sidste barn" #: forms.py:42 msgid "Left sibling" msgstr "Venstre søskend" #: forms.py:43 msgid "Right sibling" msgstr "Højre søskend" #: managers.py:121 msgid "Cannot insert a node which has already been saved." msgstr "Kan ikke indsættte en knude, der allerede er blevet gemt." #: managers.py:237 managers.py:411 managers.py:447 managers.py:604 #, python-format msgid "An invalid position was given: %s." msgstr "En ugyldig position blev givet: %s." #: managers.py:397 managers.py:584 msgid "A node may not be made a sibling of itself." msgstr "En knude må ikke være søskend til sig selv." #: managers.py:563 managers.py:684 msgid "A node may not be made a child of itself." msgstr "En knude må ikke være barn af sig selv." #: managers.py:565 managers.py:686 msgid "A node may not be made a child of any of its descendants." msgstr "En knude må ikke være barn af nogle af dets efterkommere." #: managers.py:586 msgid "A node may not be made a sibling of any of its descendants." msgstr "En knude må ikke være søskend med nogle af dets efterkommere." #: templatetags/mptt_tags.py:23 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model tagget blev givet en ugyldig model: %s" #: templatetags/mptt_tags.py:44 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldown_tree_for_node tagget blev givet en ugyldig model: %s" #: templatetags/mptt_tags.py:48 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldown_tree_for_node tagget blev givet et ugyldigt modelfelt: %s" #: templatetags/mptt_tags.py:72 #, python-format msgid "%s tag requires three arguments" msgstr "%s tagget påkræver tre argumenter" #: templatetags/mptt_tags.py:74 templatetags/mptt_tags.py:125 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "det andet argument til %s tagget skal være 'as'" #: templatetags/mptt_tags.py:123 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s tagget påkræver enten tre, syv eller otte argumenter" #: templatetags/mptt_tags.py:128 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "hvis syv argumenter gives, skal det fjerde argument til %s tagget være 'with'" #: templatetags/mptt_tags.py:130 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "hvis syv argumenter gives, skal det sjette argument til %s tagget være 'in'" #: templatetags/mptt_tags.py:134 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "hvis otte argumenter gives, skal det fjedre argument til %s tagget være 'cumulative'" #: templatetags/mptt_tags.py:136 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "hvis otte argumenter gives, skal det fjedre argument til %s tagget være 'count'" #: templatetags/mptt_tags.py:138 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "hvis otte argumenter gives, skal det syvne argument til %s tagget være 'in'" python-django-mptt-0.8.5/mptt/locale/es/000077500000000000000000000000001275105100200201125ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/es/LC_MESSAGES/000077500000000000000000000000001275105100200216775ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/es/LC_MESSAGES/django.mo000066400000000000000000000011261275105100200234760ustar00rootroot00000000000000<\p q{ k % FAdd childSuccessfully deleted %s items.View on siteProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2014-08-29 12:31+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); Añadir hijo%s registros eliminados correctamenteVer en el sitiopython-django-mptt-0.8.5/mptt/locale/es/LC_MESSAGES/django.po000066400000000000000000000070411275105100200235030ustar00rootroot00000000000000# SPANISH TRANSLATION # This file is distributed under the same license as the PACKAGE django-mptt. # Borja Fernandez 2014. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-08-29 12:31+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:105 admin.py:107 msgid "Add child" msgstr "Añadir hijo" #: admin.py:113 admin.py:115 msgid "View on site" msgstr "Ver en el sitio" #: admin.py:127 #, python-format msgid "Successfully deleted %s items." msgstr "%s registros eliminados correctamente" #: admin.py:132 #, python-format msgid "Elimina seleccionado %(verbose_name_plural)s" msgstr "" #: forms.py:62 msgid "Primer hijo" msgstr "" #: forms.py:63 msgid "Ultimo hijo" msgstr "" #: forms.py:64 msgid "Hermano izquierda" msgstr "" #: forms.py:65 msgid "Hermano derecha" msgstr "" #: forms.py:183 msgid "Padre no válido" msgstr "" #: managers.py:413 msgid "No se puede insertar un nodo que ya ha sido salvado." msgstr "" #: managers.py:625 managers.py:798 managers.py:834 managers.py:998 #, python-format msgid "Se ha proporcionado una posición no válida: %s." msgstr "" #: managers.py:784 managers.py:978 msgid "Un nodo no se puede hacer hermano de si mismo." msgstr "" #: managers.py:957 managers.py:1082 msgid "Un nodo no puede ser hijo de si mismo" msgstr "" #: managers.py:959 managers.py:1084 msgid "Un nodo no puede ser hijo de alguno de sus descendientes." msgstr "" #: managers.py:980 msgid "Un nodo no puede ser hermano de uno de sus descendientes" msgstr "" #: models.py:271 msgid "register() espera un Django model class argument" msgstr "" #: templatetags/mptt_tags.py:32 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "" #: templatetags/mptt_tags.py:56 #, python-format msgid "drilldown_tree_for_node tag modelo inválido: %s" msgstr "" #: templatetags/mptt_tags.py:63 #, python-format msgid "drilldown_tree_for_node tag campo del model inválido: %s" msgstr "" #: templatetags/mptt_tags.py:90 #, python-format msgid "%s tag requiere tres argumentos" msgstr "" #: templatetags/mptt_tags.py:92 templatetags/mptt_tags.py:147 #, python-format msgid "segundo argumento para %s tag debe ser 'as'" msgstr "" #: templatetags/mptt_tags.py:144 #, python-format msgid "%s tag requiere tres, siete o ocho argumentos" msgstr "" #: templatetags/mptt_tags.py:151 #, python-format msgid "Si se proporcionan 7 argumentos, el cuarto para %s debe ser 'with'" msgstr "" #: templatetags/mptt_tags.py:154 #, python-format msgid "Si se proporcionan siete argumentos, el sexto argumento para %s tag debe ser 'in'" msgstr "" #: templatetags/mptt_tags.py:159 #, python-format msgid "" "Si se proporcionan ocho argumentos, el cuarto argumento para %s tag debe ser 'cumulative'" msgstr "" #: templatetags/mptt_tags.py:162 #, python-format msgid "Si se proporcionan ocho argumentos, el quinto argumento para %s tag debe ser 'count'" msgstr "" #: templatetags/mptt_tags.py:165 #, python-format msgid "si se proporcionan ocho argumentos, el septimo argumento para %s tag debe ser 'in'" msgstr "" #: templatetags/mptt_tags.py:268 #, python-format msgid "El nodo %s no es el primero en orden de profundidad" msgstr "" #: templatetags/mptt_tags.py:345 #, python-format msgid "%s tag requiere un queryset" msgstr "" python-django-mptt-0.8.5/mptt/locale/es_AR/000077500000000000000000000000001275105100200204745ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/es_AR/LC_MESSAGES/000077500000000000000000000000001275105100200222615ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/es_AR/LC_MESSAGES/django.mo000066400000000000000000000056711275105100200240710ustar00rootroot000000000000006 @9`);+",2O    )@:6ZFLE%FkC&p0!9'<C*5    % #5 >Y 5 1 M RN L M J< 1     %s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.First childLast childLeft siblingRight siblingThe model %s has already been registered.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2014-08-29 12:31+0200 PO-Revision-Date: 2015-10-11 21:39-0300 Last-Translator: Gonzalo Bustos Language-Team: Spanish (Argentina) Language: es_AR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.6.10 El tag %s requiere tres, siete u ocho argumentosEl tag %s require tres argumentosUn nodo no puede ser hijo de alguno de sus descendientes.Un nodo no puede ser hijo de sí mismo.Un nodo no puede ser hermano de alguno de sus descendientes.Un nodo no puede ser hermano de sí mismo.Posición inválida: %s.No se puede insertar un nodo que ya ha sido guardado.Primer hijoÚltimo hijoHermano izquierdoHermano derechoEl modelo %s ya ha sido registrado.Campo de modelo inválido para tag drilldown_tree_for_node: %sModelo inválido para tag drilldown_tree_for_node: %sModelo inválido para tag full_tree_for_model: %sSi se proporcionan ocho argumentos, el quinto para el tag %s debe ser 'count'Si se proporcionan ocho argumentos, el cuarto para el tag %s debe ser 'cumulative'Si se proporcionan ocho argumentos, el séptimo para el tag %s debe ser 'in'Si se proporcionan siete argumentos, el cuarto para el tag %s debe ser 'with'Si se proporcionan siete argumentos, el sexto para el tag %s debe ser 'in'El segundo argumento para el tag %s debe ser 'as'python-django-mptt-0.8.5/mptt/locale/es_AR/LC_MESSAGES/django.po000066400000000000000000000076361275105100200240770ustar00rootroot00000000000000# SPANISH (ARGENTINA) TRANSLATION # This file is distributed under the same license as the PACKAGE django-mptt. # Gonzalo Bustos, 2015. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-08-29 12:31+0200\n" "PO-Revision-Date: 2015-10-11 21:39-0300\n" "Last-Translator: Gonzalo Bustos\n" "Language-Team: Spanish (Argentina)\n" "Language: es_AR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.10\n" #: __init__.py:34 #, python-format msgid "The model %s has already been registered." msgstr "El modelo %s ya ha sido registrado." #: forms.py:41 msgid "First child" msgstr "Primer hijo" #: forms.py:42 msgid "Last child" msgstr "Último hijo" #: forms.py:43 msgid "Left sibling" msgstr "Hermano izquierdo" #: forms.py:44 msgid "Right sibling" msgstr "Hermano derecho" #: managers.py:121 msgid "Cannot insert a node which has already been saved." msgstr "No se puede insertar un nodo que ya ha sido guardado." #: managers.py:306 managers.py:480 managers.py:516 managers.py:673 #, python-format msgid "An invalid position was given: %s." msgstr "Posición inválida: %s." #: managers.py:466 managers.py:653 msgid "A node may not be made a sibling of itself." msgstr "Un nodo no puede ser hermano de sí mismo." #: managers.py:632 managers.py:753 msgid "A node may not be made a child of itself." msgstr "Un nodo no puede ser hijo de sí mismo." #: managers.py:634 managers.py:755 msgid "A node may not be made a child of any of its descendants." msgstr "Un nodo no puede ser hijo de alguno de sus descendientes." #: managers.py:655 msgid "A node may not be made a sibling of any of its descendants." msgstr "Un nodo no puede ser hermano de alguno de sus descendientes." #: templatetags/mptt_tags.py:23 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "Modelo inválido para tag full_tree_for_model: %s" #: templatetags/mptt_tags.py:44 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "Modelo inválido para tag drilldown_tree_for_node: %s" #: templatetags/mptt_tags.py:48 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "Campo de modelo inválido para tag drilldown_tree_for_node: %s" #: templatetags/mptt_tags.py:72 #, python-format msgid "%s tag requires three arguments" msgstr "El tag %s require tres argumentos" #: templatetags/mptt_tags.py:74 templatetags/mptt_tags.py:125 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "El segundo argumento para el tag %s debe ser 'as'" #: templatetags/mptt_tags.py:123 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "El tag %s requiere tres, siete u ocho argumentos" #: templatetags/mptt_tags.py:128 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "Si se proporcionan siete argumentos, el cuarto para el tag %s debe ser 'with'" #: templatetags/mptt_tags.py:130 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "" "Si se proporcionan siete argumentos, el sexto para el tag %s debe ser 'in'" #: templatetags/mptt_tags.py:134 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "Si se proporcionan ocho argumentos, el cuarto para el tag %s debe ser " "'cumulative'" #: templatetags/mptt_tags.py:136 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "Si se proporcionan ocho argumentos, el quinto para el tag %s debe ser 'count'" #: templatetags/mptt_tags.py:138 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "Si se proporcionan ocho argumentos, el séptimo para el tag %s debe ser 'in'" python-django-mptt-0.8.5/mptt/locale/fr/000077500000000000000000000000001275105100200201125ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/fr/LC_MESSAGES/000077500000000000000000000000001275105100200216775ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/fr/LC_MESSAGES/django.mo000066400000000000000000000070501275105100200235000ustar00rootroot00000000000000)Z6>9^);+ *"42W'     @:`6FLEfFC&7@^f$ 0+ "\ < ) > ,% R +a ?  5  - < M *^  I ? ;$ S` X O R] N (      %(count)s %(name)s was changed successfully.%(count)s %(name)s were changed successfully.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.Add childAn invalid position was given: %s.Cannot insert a node which has already been saved.Database errorDelete selected %(verbose_name_plural)sFirst childLast childLeft siblingRight siblingSuccessfully deleted %s items.View on sitedrilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2011-07-26 19:41-0400 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %(count)s %(name)s a été modifié avec succès.%(count)s %(name)s ont été modifiés avec succès.le tag %s requiert un « queryset »le tag %s requiert trois, sept ou huit argumentsle tag %s requiert trois argumentsUne node ne peut être l'enfant d'aucun de ces déscendants.Une node ne peut être son propre enfant.Une node ne peut être voisine avec aucun de ces déscendants.Une node ne peut être voisine d'elle-même.Ajouter enfantUne position invalide à été fournie: %s.Impossible d'insérer une node qui à déjà été sauvegardéeErreur de base de donnéesSupprimer les %(verbose_name_plural)s sélectionnéesPremier enfantDernier enfantVoisin de gaucheVoisin de droite%s itèmes on été supprimé avec succèsVoir sur le sitele tag drilldown_tree_for_node à reçu un champs de modèle invalide: %sle tag drilldown_tree_for_node à reçu un modèle invalide: %sle tag full_tree_for_model à reçu un modèle invalide: %ssi huit arguments sont fournis, le cinquième argument du tag %s doit être 'count'si huit arguments sont fournis, le quatrième argument du tag %s doit être 'cumulative'si huit arguments sont fournis, le septième argument du tag %s doit être 'in'si sept arguments sont fournis, le quatrième argument du tag %s doit être 'with'si sept arguments sont fournis, le sixième argument du tag %s doit être 'in'le second argument de %s doit être 'as'python-django-mptt-0.8.5/mptt/locale/fr/LC_MESSAGES/django.po000066400000000000000000000114071275105100200235040ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2011-07-26 19:41-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: admin.py:91 msgid "Database error" msgstr "Erreur de base de données" #: admin.py:127 #, python-format msgid "%(count)s %(name)s was changed successfully." msgid_plural "%(count)s %(name)s were changed successfully." msgstr[0] "%(count)s %(name)s a été modifié avec succès." msgstr[1] "%(count)s %(name)s ont été modifiés avec succès." #: admin.py:197 admin.py:199 msgid "Add child" msgstr "Ajouter enfant" #: admin.py:205 admin.py:207 msgid "View on site" msgstr "Voir sur le site" #: admin.py:219 #, python-format msgid "Successfully deleted %s items." msgstr "%s itèmes on été supprimé avec succès" #: admin.py:224 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Supprimer les %(verbose_name_plural)s sélectionnées" #: forms.py:62 msgid "First child" msgstr "Premier enfant" #: forms.py:63 msgid "Last child" msgstr "Dernier enfant" #: forms.py:64 msgid "Left sibling" msgstr "Voisin de gauche" #: forms.py:65 msgid "Right sibling" msgstr "Voisin de droite" #: managers.py:197 msgid "Cannot insert a node which has already been saved." msgstr "Impossible d'insérer une node qui à déjà été sauvegardée" #: managers.py:373 managers.py:545 managers.py:581 managers.py:736 #, python-format msgid "An invalid position was given: %s." msgstr "Une position invalide à été fournie: %s." #: managers.py:531 managers.py:716 msgid "A node may not be made a sibling of itself." msgstr "Une node ne peut être voisine d'elle-même." #: managers.py:695 managers.py:817 msgid "A node may not be made a child of itself." msgstr "Une node ne peut être son propre enfant." #: managers.py:697 managers.py:819 msgid "A node may not be made a child of any of its descendants." msgstr "Une node ne peut être l'enfant d'aucun de ces déscendants." #: managers.py:718 msgid "A node may not be made a sibling of any of its descendants." msgstr "Une node ne peut être voisine avec aucun de ces déscendants." #: templatetags/mptt_tags.py:28 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "le tag full_tree_for_model à reçu un modèle invalide: %s" #: templatetags/mptt_tags.py:49 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "le tag drilldown_tree_for_node à reçu un modèle invalide: %s" #: templatetags/mptt_tags.py:53 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "le tag drilldown_tree_for_node à reçu un champs de modèle invalide: %s" #: templatetags/mptt_tags.py:78 #, python-format msgid "%s tag requires three arguments" msgstr "le tag %s requiert trois arguments" #: templatetags/mptt_tags.py:80 templatetags/mptt_tags.py:132 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "le second argument de %s doit être 'as'" #: templatetags/mptt_tags.py:130 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "le tag %s requiert trois, sept ou huit arguments" #: templatetags/mptt_tags.py:135 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "si sept arguments sont fournis, le quatrième argument du tag %s doit être " "'with'" #: templatetags/mptt_tags.py:137 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "" "si sept arguments sont fournis, le sixième argument du tag %s doit être 'in'" #: templatetags/mptt_tags.py:141 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "si huit arguments sont fournis, le quatrième argument du tag %s doit être " "'cumulative'" #: templatetags/mptt_tags.py:143 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "si huit arguments sont fournis, le cinquième argument du tag %s doit être " "'count'" #: templatetags/mptt_tags.py:145 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "si huit arguments sont fournis, le septième argument du tag %s doit être 'in'" #: templatetags/mptt_tags.py:296 #, python-format msgid "%s tag requires a queryset" msgstr "le tag %s requiert un « queryset »" python-django-mptt-0.8.5/mptt/locale/mn/000077500000000000000000000000001275105100200201155ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/mn/LC_MESSAGES/000077500000000000000000000000001275105100200217025ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/mn/LC_MESSAGES/django.mo000066400000000000000000000070311275105100200235020ustar00rootroot000000000000006 @9`);+",2O    )@:6ZFLE%FkC&uX6U#EyZJ e U     ;4 bp b ^6 o r lx l jR [     %s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.First childLast childLeft siblingRight siblingThe model %s has already been registered.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: Mongolian translatioin Report-Msgid-Bugs-To: POT-Creation-Date: 2009-09-23 14:44+0200 PO-Revision-Date: 2014-03-04 23:32+0800 Last-Translator: Bayarkhuu Bataa Language-Team: Bayarkhuu Bataa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Generator: Poedit 1.5.7 %s нь гурав, долоо эсвэл найман аргумент шаардана%s нь гурван аргумент шаарданаЗангилаа өөрийнхөө үр садын хүү байж болохгүй.Зангилаа өөрийнхөө хүү байж болохгүй.Зангилаа өөрийнхөө үр садын ах дүү байж болохгүй.Зангилаа өөрийнхөө ах дүү байж болохгүй.Буруу байрлал: %s.Өмнө нь орсон зангилааг дахиж оруулж болохгүй.Эхний хүүСүүлийн хүүЗүүн ах дүүсБаруун ах дүүс%s модел аль хэдийнэ бүртгэгдсэн.drilldown_tree_for_node нь буруу моделд тодорхойлогдсон байна: %sdrilldown_tree_for_node нь буруу моделд тодорхойлогдсон байна: %sfull_tree_for_model нь буруу моделд тодорхойлогдсон байна: %s%s руу найман аргумент дамжуулсан бол 5 дахь нь 'count' байх ёстой%s руу найман аргумент дамжуулсан бол 4 дэх нь 'cumulative' байх ёстой%s руу найман аргумент дамжуулсан бол 7 дахь нь 'in' байх ёстой%s руу долоон аргумент дамжуулсан бол 4 дэх нь 'with' байх ёстой%s руу долоон аргумент дамжуулсан бол 6 дэх нь 'in' байх ёстой%s руу дамжуулах хоёрдох аргумент нь 'as байх ёстой'python-django-mptt-0.8.5/mptt/locale/mn/LC_MESSAGES/django.po000066400000000000000000000110311275105100200235000ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Mongolian translation\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2009-09-23 14:44+0200\n" "PO-Revision-Date: 2014-03-04 23:32+0800\n" "Last-Translator: Bayarkhuu Bataa \n" "Language-Team: Bayarkhuu Bataa \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.5.7\n" #: __init__.py:34 #, python-format msgid "The model %s has already been registered." msgstr "%s модел аль хэдийнэ бүртгэгдсэн." #: forms.py:41 msgid "First child" msgstr "Эхний хүү" #: forms.py:42 msgid "Last child" msgstr "Сүүлийн хүү" #: forms.py:43 msgid "Left sibling" msgstr "Зүүн ах дүүс" #: forms.py:44 msgid "Right sibling" msgstr "Баруун ах дүүс" #: managers.py:121 msgid "Cannot insert a node which has already been saved." msgstr "Өмнө нь орсон зангилааг дахиж оруулж болохгүй." #: managers.py:306 managers.py:480 managers.py:516 managers.py:673 #, python-format msgid "An invalid position was given: %s." msgstr "Буруу байрлал: %s." #: managers.py:466 managers.py:653 msgid "A node may not be made a sibling of itself." msgstr "Зангилаа өөрийнхөө ах дүү байж болохгүй." #: managers.py:632 managers.py:753 msgid "A node may not be made a child of itself." msgstr "Зангилаа өөрийнхөө хүү байж болохгүй." #: managers.py:634 managers.py:755 msgid "A node may not be made a child of any of its descendants." msgstr "Зангилаа өөрийнхөө үр садын хүү байж болохгүй." #: managers.py:655 msgid "A node may not be made a sibling of any of its descendants." msgstr "Зангилаа өөрийнхөө үр садын ах дүү байж болохгүй." #: templatetags/mptt_tags.py:23 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model нь буруу моделд тодорхойлогдсон байна: %s" #: templatetags/mptt_tags.py:44 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldown_tree_for_node нь буруу моделд тодорхойлогдсон байна: %s" #: templatetags/mptt_tags.py:48 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldown_tree_for_node нь буруу моделд тодорхойлогдсон байна: %s" #: templatetags/mptt_tags.py:72 #, python-format msgid "%s tag requires three arguments" msgstr "%s нь гурван аргумент шаардана" #: templatetags/mptt_tags.py:74 templatetags/mptt_tags.py:125 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "%s руу дамжуулах хоёрдох аргумент нь 'as байх ёстой'" #: templatetags/mptt_tags.py:123 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s нь гурав, долоо эсвэл найман аргумент шаардана" #: templatetags/mptt_tags.py:128 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "%s руу долоон аргумент дамжуулсан бол 4 дэх нь 'with' байх ёстой" #: templatetags/mptt_tags.py:130 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "%s руу долоон аргумент дамжуулсан бол 6 дэх нь 'in' байх ёстой" #: templatetags/mptt_tags.py:134 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "%s руу найман аргумент дамжуулсан бол 4 дэх нь 'cumulative' байх ёстой" #: templatetags/mptt_tags.py:136 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "%s руу найман аргумент дамжуулсан бол 5 дахь нь 'count' байх ёстой" #: templatetags/mptt_tags.py:138 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "%s руу найман аргумент дамжуулсан бол 7 дахь нь 'in' байх ёстой" python-django-mptt-0.8.5/mptt/locale/nb/000077500000000000000000000000001275105100200201025ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/nb/LC_MESSAGES/000077500000000000000000000000001275105100200216675ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/nb/LC_MESSAGES/django.mo000066400000000000000000000075411275105100200234750ustar00rootroot00000000000000 +Z6$[9{);+ G"Q2t'     >cK8@:)6dFLE/FuC0&1@X= 1 9* (d < + ! 9& ` $m      b 6d A > 9 TV Y SQYO31/     %(count)s %(name)s was changed successfully.%(count)s %(name)s were changed successfully.%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.Add childAn invalid position was given: %s.Cannot insert a node which has already been saved.Database errorDelete selected %(verbose_name_plural)sFirst childInvalid parentLast childLeft siblingRight siblingSuccessfully deleted %s items.View on site`tree_manager_attr` is deprecated; just instantiate a TreeManager as a normal manager on your modelcache_tree_children was passed nodes in the wrong order!drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'register() expects a Django model class argumentsecond argument to %s tag must be 'as'Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2012-05-13 22:53+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit %(count)s %(name)s ble endret.%(count)s %(name)s ble endret.%s-taggen trenger tre, sju eller åtte argumenter%s-taggen trenger tre argumenterEn node kan ikke være barn av noen av sine etterkommere.En node kan ikke være barn av seg selv.En node kan ikke være søsken av noen av sine etterkommere.En node kan ikke være søsken av seg selv.Legg til barnEn ugyldig posisjon ble gitt: %s.Kan ikke sette inn en node som allerede har blitt lagret.DatabasefeilSlett valgte %(verbose_name_plural)sFørste barnUgyldig forelderSiste barnVenstre søskenHøyre søskenSlettet %s elementer.Vis på nettsted`tree_manager_attr` er foreldet. Bare instansier en TreeManager som en normal manager på modellencache_tree_children ble gitt noder i feil rekkefølge!drilldown_tree_for_node-taggen ble gitt et ugyldig modellfelt: %sdrilldown_tree_for_node-taggen blev gitt en ugyldig modell: %sfull_tree_for_model-taggen ble gitt en ugyldig modell: %shvis åtte argumenter er gitt, må det fjerde argumentet til %s-taggen være 'count'hvis åtte argumenter er gitt, må det fjerde argumentet til %s-taggen være 'cumulative'hvis åtte argumenter er gitt, skal det sjuende argumentet til %s-taggen være 'in'hvis sju argumenter er gitt, må det fjerde argumentet til %s-taggen være 'with'hvis sju argumenter er gitt, må det sjette argumentet til %s-taggen være 'in'register() forventer et Django-modellklasseargumentdet andre argumentet til %s-taggen må være 'as'python-django-mptt-0.8.5/mptt/locale/nb/LC_MESSAGES/django.po000066400000000000000000000126151275105100200234760ustar00rootroot00000000000000# django-mptt in Norwegian bokmål. # django-mptt på Bokmål. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Sigurd Gartmann , 2012. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-05-13 22:53+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: admin.py:96 msgid "Database error" msgstr "Databasefeil" #: admin.py:132 #, python-format msgid "%(count)s %(name)s was changed successfully." msgid_plural "%(count)s %(name)s were changed successfully." msgstr[0] "%(count)s %(name)s ble endret." msgstr[1] "%(count)s %(name)s ble endret." #: admin.py:207 admin.py:209 msgid "Add child" msgstr "Legg til barn" #: admin.py:215 admin.py:217 msgid "View on site" msgstr "Vis på nettsted" #: admin.py:229 #, python-format msgid "Successfully deleted %s items." msgstr "Slettet %s elementer." #: admin.py:234 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Slett valgte %(verbose_name_plural)s" #: forms.py:72 msgid "First child" msgstr "Første barn" #: forms.py:73 msgid "Last child" msgstr "Siste barn" #: forms.py:74 msgid "Left sibling" msgstr "Venstre søsken" #: forms.py:75 msgid "Right sibling" msgstr "Høyre søsken" #: forms.py:177 msgid "Invalid parent" msgstr "Ugyldig forelder" #: managers.py:206 msgid "Cannot insert a node which has already been saved." msgstr "Kan ikke sette inn en node som allerede har blitt lagret." #: managers.py:385 managers.py:557 managers.py:593 managers.py:748 #, python-format msgid "An invalid position was given: %s." msgstr "En ugyldig posisjon ble gitt: %s." #: managers.py:543 managers.py:728 msgid "A node may not be made a sibling of itself." msgstr "En node kan ikke være søsken av seg selv." #: managers.py:707 managers.py:829 msgid "A node may not be made a child of itself." msgstr "En node kan ikke være barn av seg selv." #: managers.py:709 managers.py:831 msgid "A node may not be made a child of any of its descendants." msgstr "En node kan ikke være barn av noen av sine etterkommere." #: managers.py:730 msgid "A node may not be made a sibling of any of its descendants." msgstr "En node kan ikke være søsken av noen av sine etterkommere." #: models.py:44 msgid "" "`tree_manager_attr` is deprecated; just instantiate a TreeManager as a " "normal manager on your model" msgstr "`tree_manager_attr` er foreldet. Bare instansier en TreeManager som en normal manager på modellen" #: models.py:199 msgid "register() expects a Django model class argument" msgstr "register() forventer et Django-modellklasseargument" #: templatetags/mptt_tags.py:28 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model-taggen ble gitt en ugyldig modell: %s" #: templatetags/mptt_tags.py:52 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldown_tree_for_node-taggen blev gitt en ugyldig modell: %s" #: templatetags/mptt_tags.py:59 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldown_tree_for_node-taggen ble gitt et ugyldig modellfelt: %s" #: templatetags/mptt_tags.py:86 #, python-format msgid "%s tag requires three arguments" msgstr "%s-taggen trenger tre argumenter" #: templatetags/mptt_tags.py:88 templatetags/mptt_tags.py:143 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "det andre argumentet til %s-taggen må være 'as'" #: templatetags/mptt_tags.py:140 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s-taggen trenger tre, sju eller åtte argumenter" #: templatetags/mptt_tags.py:147 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "hvis sju argumenter er gitt, må det fjerde argumentet til %s-taggen være " "'with'" #: templatetags/mptt_tags.py:150 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "" "hvis sju argumenter er gitt, må det sjette argumentet til %s-taggen være 'in'" #: templatetags/mptt_tags.py:155 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "hvis åtte argumenter er gitt, må det fjerde argumentet til %s-taggen være " "'cumulative'" #: templatetags/mptt_tags.py:158 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "hvis åtte argumenter er gitt, må det fjerde argumentet til %s-taggen være " "'count'" #: templatetags/mptt_tags.py:161 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "hvis åtte argumenter er gitt, skal det sjuende argumentet til %s-taggen være " "'in'" #: templatetags/mptt_tags.py:251 msgid "cache_tree_children was passed nodes in the wrong order!" msgstr "cache_tree_children ble gitt noder i feil rekkefølge!" #: templatetags/mptt_tags.py:313 #, fuzzy, python-format msgid "%s tag requires a queryset" msgstr "%s-taggen trenger tre argumenter" #~ msgid "The model %s has already been registered." #~ msgstr "Modellen %s har allerede blitt registrert." python-django-mptt-0.8.5/mptt/locale/pl/000077500000000000000000000000001275105100200201165ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/pl/LC_MESSAGES/000077500000000000000000000000001275105100200217035ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000074001275105100200235030ustar00rootroot00000000000000)Z6>9^);+ *"42W'     @:`6FLEfFC&7^u " 4 S ;O Y 8  #/ :S  %     # 3 GE B > X ^h V YWx/     %(count)s %(name)s was changed successfully.%(count)s %(name)s were changed successfully.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.Add childAn invalid position was given: %s.Cannot insert a node which has already been saved.Database errorDelete selected %(verbose_name_plural)sFirst childLast childLeft siblingRight siblingSuccessfully deleted %s items.View on sitedrilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2011-10-17 16:06+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: BARTOSZ BIAŁY Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2) %(count)s %(name)s został zmieniony.%(count)s %(name)s zostały zmienione.%(count)s %(name)s zostało zmienionych.tag %s wymaga argumentu 'queryset'tag %s wymaga trzech, siedmiu lub ośmiu argumentówtag %s wymaga trzech argumentówWęzeł nie może zostać węzłem potomnym żadnego ze swoich węzłów potomnych.Węzeł nie może zostać swoim własnym węzłem potomnym.Węzeł nie może zostać węzłem równożędnym żadnego ze swoich węzłów potomnych.Węzeł nie może zostać swoim węzłem równożędnym.Dodaj podelementPodano niewłaściwą pozycję: %s.Nie można wstawić węzła, który został już zapisany.Błąd bazy danychUsuń wybrane %(verbose_name_plural)sPierwszy podelementOstatni podelementLewy podelementPrawy podelementSkutecznie usinięto %s elementów.Pokaż na stroniedo tagu drilldown_tree_for_node przekazano niewłaściwe pole model: %sdo tagu drilldown_tree_for_node przekazano niewłaściwy model: %sdo tagu full_tree_for_model przekazano niewłaściwy model: %sjeżeli podano osiem argumentów, to piątym argumentem tagu %s musi być słowo 'count'jeżeli podano osiem argumentów, to czwartym argumentem tagu %s musi być słowo 'cumulative'jeżeli podano osiem argumentów, to siódmym argumentem tagu %s musi być słowo 'in'jeżeli podano siedem argumentów, to czwartym argumentem tagu %s musi być słowo 'with'jeżeli podano siedem argumentów, to szóstym argumentem tagu %s musi być słowo 'in'drugim argumentem tagu %s musi być słowo 'as'python-django-mptt-0.8.5/mptt/locale/pl/LC_MESSAGES/django.po000066400000000000000000000120421275105100200235040ustar00rootroot00000000000000# django-mptt in Polish. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the django-mptt package. # Bartosz Biały , 2011. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2011-10-17 16:06+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: BARTOSZ BIAŁY \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2)\n" #: admin.py:91 msgid "Database error" msgstr "Błąd bazy danych" #: admin.py:127 #, python-format msgid "%(count)s %(name)s was changed successfully." msgid_plural "%(count)s %(name)s were changed successfully." msgstr[0] "%(count)s %(name)s został zmieniony." msgstr[1] "%(count)s %(name)s zostały zmienione." msgstr[2] "%(count)s %(name)s zostało zmienionych." #: admin.py:197 admin.py:199 msgid "Add child" msgstr "Dodaj podelement" #: admin.py:205 admin.py:207 msgid "View on site" msgstr "Pokaż na stronie" #: admin.py:219 #, python-format msgid "Successfully deleted %s items." msgstr "Skutecznie usinięto %s elementów." #: admin.py:224 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Usuń wybrane %(verbose_name_plural)s" #: forms.py:65 msgid "First child" msgstr "Pierwszy podelement" #: forms.py:66 msgid "Last child" msgstr "Ostatni podelement" #: forms.py:67 msgid "Left sibling" msgstr "Lewy podelement" #: forms.py:68 msgid "Right sibling" msgstr "Prawy podelement" #: managers.py:200 msgid "Cannot insert a node which has already been saved." msgstr "Nie można wstawić węzła, który został już zapisany." #: managers.py:379 managers.py:551 managers.py:587 managers.py:742 #, python-format msgid "An invalid position was given: %s." msgstr "Podano niewłaściwą pozycję: %s." #: managers.py:537 managers.py:722 msgid "A node may not be made a sibling of itself." msgstr "Węzeł nie może zostać swoim węzłem równożędnym." #: managers.py:701 managers.py:823 msgid "A node may not be made a child of itself." msgstr "Węzeł nie może zostać swoim własnym węzłem potomnym." #: managers.py:703 managers.py:825 msgid "A node may not be made a child of any of its descendants." msgstr "Węzeł nie może zostać węzłem potomnym żadnego ze swoich węzłów potomnych." #: managers.py:724 msgid "A node may not be made a sibling of any of its descendants." msgstr "Węzeł nie może zostać węzłem równożędnym żadnego ze swoich węzłów potomnych." #: templatetags/mptt_tags.py:29 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "do tagu full_tree_for_model przekazano niewłaściwy model: %s" #: templatetags/mptt_tags.py:50 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "do tagu drilldown_tree_for_node przekazano niewłaściwy model: %s" #: templatetags/mptt_tags.py:54 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "do tagu drilldown_tree_for_node przekazano niewłaściwe pole model: %s" #: templatetags/mptt_tags.py:89 templatetags/mptt_tags.py:176 #, python-format msgid "%s tag requires three arguments" msgstr "tag %s wymaga trzech argumentów" #: templatetags/mptt_tags.py:91 templatetags/mptt_tags.py:143 #: templatetags/mptt_tags.py:179 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "drugim argumentem tagu %s musi być słowo 'as'" #: templatetags/mptt_tags.py:141 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "tag %s wymaga trzech, siedmiu lub ośmiu argumentów" #: templatetags/mptt_tags.py:146 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "jeżeli podano siedem argumentów, to czwartym argumentem tagu %s musi być słowo 'with'" #: templatetags/mptt_tags.py:148 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "jeżeli podano siedem argumentów, to szóstym argumentem tagu %s musi być słowo 'in'" #: templatetags/mptt_tags.py:152 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "jeżeli podano osiem argumentów, to czwartym argumentem tagu %s musi być słowo 'cumulative'" #: templatetags/mptt_tags.py:154 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "jeżeli podano osiem argumentów, to piątym argumentem tagu %s musi być słowo 'count'" #: templatetags/mptt_tags.py:156 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "jeżeli podano osiem argumentów, to siódmym argumentem tagu %s musi być słowo 'in'" #: templatetags/mptt_tags.py:329 #, python-format msgid "%s tag requires a queryset" msgstr "tag %s wymaga argumentu 'queryset'" python-django-mptt-0.8.5/mptt/locale/pt_BR/000077500000000000000000000000001275105100200205115ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/pt_BR/LC_MESSAGES/000077500000000000000000000000001275105100200222765ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/pt_BR/LC_MESSAGES/django.mo000066400000000000000000000056131275105100200241020ustar00rootroot000000000000006 @9`);+",2O    )@:6ZFLE%FkC&C.a :';(Py3  7$ 7\ 3 P U No O L /[     %s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.First childLast childLeft siblingRight siblingThe model %s has already been registered.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2014-08-19 16:30-300 PO-Revision-Date: 2014-08-19 16:30-300 Last-Translator: Luis Fagundes Language-Team: LANGUAGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tag %s requer três, sete ou oito argumentosA tag %s requer três argumentosUm nó não pode ser filho de nenhum de seus descendentes.Um nó não pode ser filho de si mesmo.Um nó não pode ser irmão de nenhum de seus descendentes.Um nó não pode ser irmão de si mesmo.Posição inválida: %s.Não é possível inserir um nó que já foi salvo.Primeiro filhoÚltimo filhoIrmão à esquerdaIrmão à direitaO modelo %s foi registrado.drilldown_tree_for_node recebeu um modelo inválido: %sdrilldown_tree_for_node recebeu um modelo inválido: %sfull_tree_for_model recebeu um modelo inválido: %sse oito argumentos são dados, o quinto argumento para a tag %s deve ser 'count'se oito argumentos são dados, o quarto argumento para a tag %s deve ser 'cumulative'se oito argumentos são dados, o sétimo argumento para a tag %s deve ser 'in'se sete argumentos são dados, o quarto argumento para a tag %s deve ser 'with'se sete argumentos são dados, o sexto argumento para a tag %s deve ser 'in'o segundo argumento para a tag %s deve ser 'as'python-django-mptt-0.8.5/mptt/locale/pt_BR/LC_MESSAGES/django.po000066400000000000000000000076241275105100200241110ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-08-19 16:30-300\n" "PO-Revision-Date: 2014-08-19 16:30-300\n" "Last-Translator: Luis Fagundes \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: __init__.py:34 #, python-format msgid "The model %s has already been registered." msgstr "O modelo %s foi registrado." #: forms.py:41 msgid "First child" msgstr "Primeiro filho" #: forms.py:42 msgid "Last child" msgstr "Último filho" #: forms.py:43 msgid "Left sibling" msgstr "Irmão à esquerda" #: forms.py:44 msgid "Right sibling" msgstr "Irmão à direita" #: managers.py:121 msgid "Cannot insert a node which has already been saved." msgstr "Não é possível inserir um nó que já foi salvo." #: managers.py:306 managers.py:480 managers.py:516 managers.py:673 #, python-format msgid "An invalid position was given: %s." msgstr "Posição inválida: %s." #: managers.py:466 managers.py:653 msgid "A node may not be made a sibling of itself." msgstr "Um nó não pode ser irmão de si mesmo." #: managers.py:632 managers.py:753 msgid "A node may not be made a child of itself." msgstr "Um nó não pode ser filho de si mesmo." #: managers.py:634 managers.py:755 msgid "A node may not be made a child of any of its descendants." msgstr "Um nó não pode ser filho de nenhum de seus descendentes." #: managers.py:655 msgid "A node may not be made a sibling of any of its descendants." msgstr "Um nó não pode ser irmão de nenhum de seus descendentes." #: templatetags/mptt_tags.py:23 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model recebeu um modelo inválido: %s" #: templatetags/mptt_tags.py:44 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldown_tree_for_node recebeu um modelo inválido: %s" #: templatetags/mptt_tags.py:48 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldown_tree_for_node recebeu um modelo inválido: %s" #: templatetags/mptt_tags.py:72 #, python-format msgid "%s tag requires three arguments" msgstr "A tag %s requer três argumentos" #: templatetags/mptt_tags.py:74 templatetags/mptt_tags.py:125 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "o segundo argumento para a tag %s deve ser 'as'" #: templatetags/mptt_tags.py:123 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "A tag %s requer três, sete ou oito argumentos" #: templatetags/mptt_tags.py:128 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "se sete argumentos são dados, o quarto argumento para a tag %s deve ser 'with'" #: templatetags/mptt_tags.py:130 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "se sete argumentos são dados, o sexto argumento para a tag %s deve ser 'in'" #: templatetags/mptt_tags.py:134 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "se oito argumentos são dados, o quarto argumento para a tag %s deve ser 'cumulative'" #: templatetags/mptt_tags.py:136 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "se oito argumentos são dados, o quinto argumento para a tag %s deve ser 'count'" #: templatetags/mptt_tags.py:138 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "se oito argumentos são dados, o sétimo argumento para a tag %s deve ser 'in'" python-django-mptt-0.8.5/mptt/locale/ru/000077500000000000000000000000001275105100200201315ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/ru/LC_MESSAGES/000077500000000000000000000000001275105100200217165ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/locale/ru/LC_MESSAGES/django.mo000066400000000000000000000122361275105100200235210ustar00rootroot00000000000000'T5`a69)-;W+"2 #'6&^      $ %&@L:6FLFEFC dy0&  , % K +U [ O [- O 6 d u & 9 K !@b2~!N@L3[R3N}S~]g3F-zHTDF%"  '$! &#   %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: django-mptt Report-Msgid-Bugs-To: POT-Creation-Date: 2016-03-02 12:10+0300 PO-Revision-Date: 2013-08-28 19:49+0400 Last-Translator: Rafael Kamashev Language-Team: Language: ru_RU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); %s был успешно перемещен.для %s требуется queryset%s требует три, семь или восемь аргументов%s требует три аргументаЭлемент не может быть потомком своего наследника.Элемент не может быть потомком самому себе.Элемент не может быть дочерним своему наследнику.Элемент не может быть дочерним самому себе.Была дана неверная позиция: %s.Невозможно добавить элемент, который уже был сохранён.Свернуть деревоОшибка базы данных: %sУдалить выбранное %(verbose_name_plural)sИзучите инструкцию по перемещению узлов.Развернуть деревоПервый потомокНеверный родительский узелПоследний потомокЛевый братНет доступаУзел %s имеет уровень меньше первого уровняОбъекты пропали, повторите еще раз.Правый братУспешно удалено %(count)d узлов.для drilldown_tree_for_node было дано неверное поле модели: %sдля drilldown_tree_for_node была дана неверная модель: %sдля full_tree_for_model была дана неверная модель: %sесли дано восемь аргументов, то пятый аргумент для %s должен быть 'count'если дано восемь аргументов, то четвёртый аргумент для %s должен быть 'cumulative'если дано восемь аргументов, то седьмой аргумент для %s должен быть 'in'если дано семь аргументов, то четвёртый аргумент для %s должен быть 'with'если дано семь аргументов, то шестой для %s должен быть 'in'переместить узел после узлапереместить узел до узлапереместить узел в подчиненную позициюregister() ожидает модель Django в качестве аргументавторым аргуметом для %s должен быть 'as'заголовокpython-django-mptt-0.8.5/mptt/locale/ru/LC_MESSAGES/django.po000066400000000000000000000160011275105100200235160ustar00rootroot00000000000000# django-mptt in russian # This file is distributed under the same license as the django-mptt package. # Translators: # Sergey Vishnikin , 2013. # Rafael Kamashev , 2016. msgid "" msgstr "" "Project-Id-Version: django-mptt\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-03-02 12:10+0300\n" "PO-Revision-Date: 2013-08-28 19:49+0400\n" "Last-Translator: Rafael Kamashev \n" "Language-Team: \n" "Language: ru_RU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: admin.py:80 #, python-format #| msgid "Successfully deleted %s items." msgid "Successfully deleted %(count)d items." msgstr "Успешно удалено %(count)d узлов." #: admin.py:93 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Удалить выбранное %(verbose_name_plural)s" #: admin.py:175 msgid "title" msgstr "заголовок" #: admin.py:205 msgid "Did not understand moving instruction." msgstr "Изучите инструкцию по перемещению узлов." #: admin.py:213 msgid "Objects have disappeared, try again." msgstr "Объекты пропали, повторите еще раз." #: admin.py:217 msgid "No permission" msgstr "Нет доступа" #: admin.py:226 #, python-format #| msgid "Database error" msgid "Database error: %s" msgstr "Ошибка базы данных: %s" #: admin.py:231 #, python-format msgid "%s has been successfully moved." msgstr "%s был успешно перемещен." #: admin.py:242 msgid "move node before node" msgstr "переместить узел до узла" #: admin.py:243 msgid "move node to child position" msgstr "переместить узел в подчиненную позицию" #: admin.py:244 msgid "move node after node" msgstr "переместить узел после узла" #: admin.py:245 msgid "Collapse tree" msgstr "Свернуть дерево" #: admin.py:246 msgid "Expand tree" msgstr "Развернуть дерево" #: forms.py:63 msgid "First child" msgstr "Первый потомок" #: forms.py:64 msgid "Last child" msgstr "Последний потомок" #: forms.py:65 msgid "Left sibling" msgstr "Левый брат" #: forms.py:66 msgid "Right sibling" msgstr "Правый брат" #: forms.py:184 msgid "Invalid parent" msgstr "Неверный родительский узел" #: managers.py:514 msgid "Cannot insert a node which has already been saved." msgstr "Невозможно добавить элемент, который уже был сохранён." #: managers.py:743 managers.py:916 managers.py:952 managers.py:1116 #, python-format msgid "An invalid position was given: %s." msgstr "Была дана неверная позиция: %s." #: managers.py:902 managers.py:1096 msgid "A node may not be made a sibling of itself." msgstr "Элемент не может быть дочерним самому себе." #: managers.py:1075 managers.py:1200 msgid "A node may not be made a child of itself." msgstr "Элемент не может быть потомком самому себе." #: managers.py:1077 managers.py:1202 msgid "A node may not be made a child of any of its descendants." msgstr "Элемент не может быть потомком своего наследника." #: managers.py:1098 msgid "A node may not be made a sibling of any of its descendants." msgstr "Элемент не может быть дочерним своему наследнику." #: models.py:291 msgid "register() expects a Django model class argument" msgstr "register() ожидает модель Django в качестве аргумента" #: templatetags/mptt_tags.py:31 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "для full_tree_for_model была дана неверная модель: %s" #: templatetags/mptt_tags.py:55 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "для drilldown_tree_for_node была дана неверная модель: %s" #: templatetags/mptt_tags.py:62 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "для drilldown_tree_for_node было дано неверное поле модели: %s" #: templatetags/mptt_tags.py:89 #, python-format msgid "%s tag requires three arguments" msgstr "%s требует три аргумента" #: templatetags/mptt_tags.py:91 templatetags/mptt_tags.py:146 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "вторым аргуметом для %s должен быть 'as'" #: templatetags/mptt_tags.py:143 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s требует три, семь или восемь аргументов" #: templatetags/mptt_tags.py:150 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "если дано семь аргументов, то четвёртый аргумент для %s должен быть 'with'" #: templatetags/mptt_tags.py:154 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "если дано семь аргументов, то шестой для %s должен быть 'in'" #: templatetags/mptt_tags.py:160 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "если дано восемь аргументов, то четвёртый аргумент для %s должен быть " "'cumulative'" #: templatetags/mptt_tags.py:164 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "если дано восемь аргументов, то пятый аргумент для %s должен быть 'count'" #: templatetags/mptt_tags.py:168 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "если дано восемь аргументов, то седьмой аргумент для %s должен быть 'in'" #: templatetags/mptt_tags.py:287 #, python-format msgid "%s tag requires a queryset" msgstr "для %s требуется queryset" #: utils.py:240 #, python-format msgid "Node %s not in depth-first order" msgstr "Узел %s имеет уровень меньше первого уровня" #~ msgid "%(count)s %(name)s was changed successfully." #~ msgid_plural "%(count)s %(name)s were changed successfully." #~ msgstr[0] "%(count)s %(name)s успешно изменен." #~ msgstr[1] "%(count)s %(name)s успешно изменены." #~ msgstr[2] "%(count)s %(name)s успешно изменено." #~ msgid "Add child" #~ msgstr "Добавить потомка" #~ msgid "View on site" #~ msgstr "Перейти на сайт" python-django-mptt-0.8.5/mptt/managers.py000066400000000000000000001411461275105100200204220ustar00rootroot00000000000000""" A custom manager for working with trees of objects. """ from __future__ import unicode_literals import functools import contextlib from itertools import groupby from django.db import models, connections, router from django.db.models import F, ManyToManyField, Max, Q from django.utils.translation import ugettext as _ from mptt.exceptions import CantDisableUpdates, InvalidMove from mptt.querysets import TreeQuerySet from mptt.utils import _get_tree_model from mptt.signals import node_moved __all__ = ('TreeManager',) COUNT_SUBQUERY = """( SELECT COUNT(*) FROM %(rel_table)s WHERE %(mptt_fk)s = %(mptt_table)s.%(mptt_rel_to)s )""" CUMULATIVE_COUNT_SUBQUERY = """( SELECT COUNT(*) FROM %(rel_table)s WHERE %(mptt_fk)s IN ( SELECT m2.%(mptt_rel_to)s FROM %(mptt_table)s m2 WHERE m2.%(tree_id)s = %(mptt_table)s.%(tree_id)s AND m2.%(left)s BETWEEN %(mptt_table)s.%(left)s AND %(mptt_table)s.%(right)s ) )""" COUNT_SUBQUERY_M2M = """( SELECT COUNT(*) FROM %(rel_table)s j INNER JOIN %(rel_m2m_table)s k ON j.%(rel_pk)s = k.%(rel_m2m_column)s WHERE k.%(mptt_fk)s = %(mptt_table)s.%(mptt_pk)s )""" CUMULATIVE_COUNT_SUBQUERY_M2M = """( SELECT COUNT(*) FROM %(rel_table)s j INNER JOIN %(rel_m2m_table)s k ON j.%(rel_pk)s = k.%(rel_m2m_column)s WHERE k.%(mptt_fk)s IN ( SELECT m2.%(mptt_pk)s FROM %(mptt_table)s m2 WHERE m2.%(tree_id)s = %(mptt_table)s.%(tree_id)s AND m2.%(left)s BETWEEN %(mptt_table)s.%(left)s AND %(mptt_table)s.%(right)s ) )""" def delegate_manager(method): """ Delegate method calls to base manager, if exists. """ @functools.wraps(method) def wrapped(self, *args, **kwargs): if self._base_manager: return getattr(self._base_manager, method.__name__)(*args, **kwargs) return method(self, *args, **kwargs) return wrapped class TreeManager(models.Manager.from_queryset(TreeQuerySet)): """ A manager for working with trees of objects. """ def contribute_to_class(self, model, name): super(TreeManager, self).contribute_to_class(model, name) if not model._meta.abstract: self.tree_model = _get_tree_model(model) self._base_manager = None if self.tree_model is not model: # _base_manager is the treemanager on tree_model self._base_manager = self.tree_model._tree_manager def get_queryset(self, *args, **kwargs): """ Ensures that this manager always returns nodes in tree order. """ return super(TreeManager, self).get_queryset( *args, **kwargs ).order_by( self.tree_id_attr, self.left_attr ) def _get_queryset_relatives(self, queryset, direction, include_self): """ Returns a queryset containing either the descendants ``direction == desc`` or the ancestors ``direction == asc`` of a given queryset. This function is not meant to be called directly, although there is no harm in doing so. Instead, it should be used via ``get_queryset_descendants()`` and/or ``get_queryset_ancestors()``. This function works by grouping contiguous siblings and using them to create a range that selects all nodes between the range, instead of querying for each node individually. Three variables are required when querying for ancestors or descendants: tree_id_attr, left_attr, right_attr. If we weren't using ranges and our queryset contained 100 results, the resulting SQL query would contain 300 variables. However, when using ranges, if the same queryset contained 10 sets of contiguous siblings, then the resulting SQL query should only contain 30 variables. The attributes used to create the range are completely dependent upon whether you are ascending or descending the tree. * Ascending (ancestor nodes): select all nodes whose right_attr is greater than (or equal to, if include_self = True) the smallest right_attr within the set of contiguous siblings, and whose left_attr is less than (or equal to) the largest left_attr within the set of contiguous siblings. * Descending (descendant nodes): select all nodes whose left_attr is greater than (or equal to, if include_self = True) the smallest left_attr within the set of contiguous siblings, and whose right_attr is less than (or equal to) the largest right_attr within the set of contiguous siblings. The result is the more contiguous siblings in the original queryset, the fewer SQL variables will be required to execute the query. """ assert self.model is queryset.model opts = queryset.model._mptt_meta filters = Q() e = 'e' if include_self else '' max_op = 'lt' + e min_op = 'gt' + e if direction == 'asc': max_attr = opts.left_attr min_attr = opts.right_attr elif direction == 'desc': max_attr = opts.right_attr min_attr = opts.left_attr tree_key = opts.tree_id_attr min_key = '%s__%s' % (min_attr, min_op) max_key = '%s__%s' % (max_attr, max_op) q = queryset.order_by(opts.tree_id_attr, opts.parent_attr, opts.left_attr).only( opts.tree_id_attr, opts.left_attr, opts.right_attr, min_attr, max_attr, opts.parent_attr, # These fields are used by MPTTModel.update_mptt_cached_fields() *opts.order_insertion_by ) if not q: return self.none() for group in groupby( q, key=lambda n: ( getattr(n, opts.tree_id_attr), getattr(n, opts.parent_attr + '_id'), )): next_lft = None for node in list(group[1]): tree, lft, rght, min_val, max_val = (getattr(node, opts.tree_id_attr), getattr(node, opts.left_attr), getattr(node, opts.right_attr), getattr(node, min_attr), getattr(node, max_attr)) if next_lft is None: next_lft = rght + 1 min_max = {'min': min_val, 'max': max_val} elif lft == next_lft: if min_val < min_max['min']: min_max['min'] = min_val if max_val > min_max['max']: min_max['max'] = max_val next_lft = rght + 1 elif lft != next_lft: filters |= Q(**{ tree_key: tree, min_key: min_max['min'], max_key: min_max['max'], }) min_max = {'min': min_val, 'max': max_val} next_lft = rght + 1 filters |= Q(**{ tree_key: tree, min_key: min_max['min'], max_key: min_max['max'], }) return self.filter(filters) def get_queryset_descendants(self, queryset, include_self=False): """ Returns a queryset containing the descendants of all nodes in the given queryset. If ``include_self=True``, nodes in ``queryset`` will also be included in the result. """ return self._get_queryset_relatives(queryset, 'desc', include_self) def get_queryset_ancestors(self, queryset, include_self=False): """ Returns a queryset containing the ancestors of all nodes in the given queryset. If ``include_self=True``, nodes in ``queryset`` will also be included in the result. """ return self._get_queryset_relatives(queryset, 'asc', include_self) @contextlib.contextmanager def disable_mptt_updates(self): """ Context manager. Disables mptt updates. NOTE that this context manager causes inconsistencies! MPTT model methods are not guaranteed to return the correct results. When to use this method: If used correctly, this method can be used to speed up bulk updates. This doesn't do anything clever. It *will* mess up your tree. You should follow this method with a call to ``TreeManager.rebuild()`` to ensure your tree stays sane, and you should wrap both calls in a transaction. This is best for updates that span a large part of the table. If you are doing localised changes (one tree, or a few trees) consider using ``delay_mptt_updates``. If you are making only minor changes to your tree, just let the updates happen. Transactions: This doesn't enforce any transactional behavior. You should wrap this in a transaction to ensure database consistency. If updates are already disabled on the model, this is a noop. Usage:: with transaction.atomic(): with MyNode.objects.disable_mptt_updates(): ## bulk updates. MyNode.objects.rebuild() """ # Error cases: if self.model._meta.abstract: # an abstract model. Design decision needed - do we disable # updates for all concrete models that derive from this model? I # vote no - that's a bit implicit and it's a weird use-case # anyway. Open to further discussion :) raise CantDisableUpdates( "You can't disable/delay mptt updates on %s," " it's an abstract model" % self.model.__name__ ) elif self.model._meta.proxy: # a proxy model. disabling updates would implicitly affect other # models using the db table. Caller should call this on the # manager for the concrete model instead, to make the behavior # explicit. raise CantDisableUpdates( "You can't disable/delay mptt updates on %s, it's a proxy" " model. Call the concrete model instead." % self.model.__name__ ) elif self.tree_model is not self.model: # a multiple-inheritance child of an MPTTModel. Disabling # updates may affect instances of other models in the tree. raise CantDisableUpdates( "You can't disable/delay mptt updates on %s, it doesn't" " contain the mptt fields." % self.model.__name__ ) if not self.model._mptt_updates_enabled: # already disabled, noop. yield else: self.model._set_mptt_updates_enabled(False) try: yield finally: self.model._set_mptt_updates_enabled(True) @contextlib.contextmanager def delay_mptt_updates(self): """ Context manager. Delays mptt updates until the end of a block of bulk processing. NOTE that this context manager causes inconsistencies! MPTT model methods are not guaranteed to return the correct results until the end of the context block. When to use this method: If used correctly, this method can be used to speed up bulk updates. This is best for updates in a localised area of the db table, especially if all the updates happen in a single tree and the rest of the forest is left untouched. No subsequent rebuild is necessary. ``delay_mptt_updates`` does a partial rebuild of the modified trees (not the whole table). If used indiscriminately, this can actually be much slower than just letting the updates occur when they're required. The worst case occurs when every tree in the table is modified just once. That results in a full rebuild of the table, which can be *very* slow. If your updates will modify most of the trees in the table (not a small number of trees), you should consider using ``TreeManager.disable_mptt_updates``, as it does much fewer queries. Transactions: This doesn't enforce any transactional behavior. You should wrap this in a transaction to ensure database consistency. Exceptions: If an exception occurs before the processing of the block, delayed updates will not be applied. Usage:: with transaction.atomic(): with MyNode.objects.delay_mptt_updates(): ## bulk updates. """ with self.disable_mptt_updates(): if self.model._mptt_is_tracking: # already tracking, noop. yield else: self.model._mptt_start_tracking() try: yield except Exception: # stop tracking, but discard results self.model._mptt_stop_tracking() raise results = self.model._mptt_stop_tracking() partial_rebuild = self.partial_rebuild for tree_id in results: partial_rebuild(tree_id) @property def parent_attr(self): return self.model._mptt_meta.parent_attr @property def left_attr(self): return self.model._mptt_meta.left_attr @property def right_attr(self): return self.model._mptt_meta.right_attr @property def tree_id_attr(self): return self.model._mptt_meta.tree_id_attr @property def level_attr(self): return self.model._mptt_meta.level_attr def _translate_lookups(self, **lookups): new_lookups = {} join_parts = '__'.join for k, v in lookups.items(): parts = k.split('__') new_parts = [] new_parts__append = new_parts.append for part in parts: new_parts__append(getattr(self, part + '_attr', part)) new_lookups[join_parts(new_parts)] = v return new_lookups @delegate_manager def _mptt_filter(self, qs=None, **filters): """ Like ``self.filter()``, but translates name-agnostic filters for MPTT fields. """ if qs is None: qs = self return qs.filter(**self._translate_lookups(**filters)) @delegate_manager def _mptt_update(self, qs=None, **items): """ Like ``self.update()``, but translates name-agnostic MPTT fields. """ if qs is None: qs = self return qs.update(**self._translate_lookups(**items)) def _get_connection(self, **hints): return connections[router.db_for_write(self.model, **hints)] def add_related_count(self, queryset, rel_model, rel_field, count_attr, cumulative=False): """ Adds a related item count to a given ``QuerySet`` using its ``extra`` method, for a ``Model`` class which has a relation to this ``Manager``'s ``Model`` class. Arguments: ``rel_model`` A ``Model`` class which has a relation to this `Manager``'s ``Model`` class. ``rel_field`` The name of the field in ``rel_model`` which holds the relation. ``count_attr`` The name of an attribute which should be added to each item in this ``QuerySet``, containing a count of how many instances of ``rel_model`` are related to it through ``rel_field``. ``cumulative`` If ``True``, the count will be for each item and all of its descendants, otherwise it will be for each item itself. """ connection = self._get_connection() qn = connection.ops.quote_name meta = self.model._meta mptt_field = rel_model._meta.get_field(rel_field) if isinstance(mptt_field, ManyToManyField): if cumulative: subquery = CUMULATIVE_COUNT_SUBQUERY_M2M % { 'rel_table': qn(rel_model._meta.db_table), 'rel_pk': qn(rel_model._meta.pk.column), 'rel_m2m_table': qn(mptt_field.m2m_db_table()), 'rel_m2m_column': qn(mptt_field.m2m_column_name()), 'mptt_fk': qn(mptt_field.m2m_reverse_name()), 'mptt_table': qn(self.tree_model._meta.db_table), 'mptt_pk': qn(meta.pk.column), 'tree_id': qn(meta.get_field(self.tree_id_attr).column), 'left': qn(meta.get_field(self.left_attr).column), 'right': qn(meta.get_field(self.right_attr).column), } else: subquery = COUNT_SUBQUERY_M2M % { 'rel_table': qn(rel_model._meta.db_table), 'rel_pk': qn(rel_model._meta.pk.column), 'rel_m2m_table': qn(mptt_field.m2m_db_table()), 'rel_m2m_column': qn(mptt_field.m2m_column_name()), 'mptt_fk': qn(mptt_field.m2m_reverse_name()), 'mptt_table': qn(self.tree_model._meta.db_table), 'mptt_pk': qn(meta.pk.column), } else: if cumulative: subquery = CUMULATIVE_COUNT_SUBQUERY % { 'rel_table': qn(rel_model._meta.db_table), 'mptt_fk': qn(rel_model._meta.get_field(rel_field).column), 'mptt_table': qn(self.tree_model._meta.db_table), 'mptt_rel_to': qn(mptt_field.rel.field_name), 'tree_id': qn(meta.get_field(self.tree_id_attr).column), 'left': qn(meta.get_field(self.left_attr).column), 'right': qn(meta.get_field(self.right_attr).column), } else: subquery = COUNT_SUBQUERY % { 'rel_table': qn(rel_model._meta.db_table), 'mptt_fk': qn(rel_model._meta.get_field(rel_field).column), 'mptt_table': qn(self.tree_model._meta.db_table), 'mptt_rel_to': qn(mptt_field.rel.field_name), } return queryset.extra(select={count_attr: subquery}) @delegate_manager def insert_node(self, node, target, position='last-child', save=False, allow_existing_pk=False, refresh_target=True): """ Sets up the tree state for ``node`` (which has not yet been inserted into in the database) so it will be positioned relative to a given ``target`` node as specified by ``position`` (when appropriate) it is inserted, with any neccessary space already having been made for it. A ``target`` of ``None`` indicates that ``node`` should be the last root node. If ``save`` is ``True``, ``node``'s ``save()`` method will be called before it is returned. NOTE: This is a low-level method; it does NOT respect ``MPTTMeta.order_insertion_by``. In most cases you should just set the node's parent and let mptt call this during save. """ if node.pk and not allow_existing_pk and self.filter(pk=node.pk).exists(): raise ValueError(_('Cannot insert a node which has already been saved.')) if target is None: tree_id = self._get_next_tree_id() setattr(node, self.left_attr, 1) setattr(node, self.right_attr, 2) setattr(node, self.level_attr, 0) setattr(node, self.tree_id_attr, tree_id) setattr(node, self.parent_attr, None) elif target.is_root_node() and position in ['left', 'right']: if refresh_target: # Ensure mptt values on target are not stale. target._mptt_refresh() target_tree_id = getattr(target, self.tree_id_attr) if position == 'left': tree_id = target_tree_id space_target = target_tree_id - 1 else: tree_id = target_tree_id + 1 space_target = target_tree_id self._create_tree_space(space_target) setattr(node, self.left_attr, 1) setattr(node, self.right_attr, 2) setattr(node, self.level_attr, 0) setattr(node, self.tree_id_attr, tree_id) setattr(node, self.parent_attr, None) else: setattr(node, self.left_attr, 0) setattr(node, self.level_attr, 0) if refresh_target: # Ensure mptt values on target are not stale. target._mptt_refresh() space_target, level, left, parent, right_shift = \ self._calculate_inter_tree_move_values(node, target, position) tree_id = getattr(target, self.tree_id_attr) self._create_space(2, space_target, tree_id) setattr(node, self.left_attr, -left) setattr(node, self.right_attr, -left + 1) setattr(node, self.level_attr, -level) setattr(node, self.tree_id_attr, tree_id) setattr(node, self.parent_attr, parent) if parent: self._post_insert_update_cached_parent_right(parent, right_shift) if save: node.save() return node @delegate_manager def _move_node(self, node, target, position='last-child', save=True, refresh_target=True): if self.tree_model._mptt_is_tracking: # delegate to insert_node and clean up the gaps later. return self.insert_node(node, target, position=position, save=save, allow_existing_pk=True, refresh_target=refresh_target) else: if target is None: if node.is_child_node(): self._make_child_root_node(node) elif target.is_root_node() and position in ('left', 'right'): self._make_sibling_of_root_node(node, target, position) else: if node.is_root_node(): self._move_root_node(node, target, position) else: self._move_child_node(node, target, position) def move_node(self, node, target, position='last-child'): """ Moves ``node`` relative to a given ``target`` node as specified by ``position`` (when appropriate), by examining both nodes and calling the appropriate method to perform the move. A ``target`` of ``None`` indicates that ``node`` should be turned into a root node. Valid values for ``position`` are ``'first-child'``, ``'last-child'``, ``'left'`` or ``'right'``. ``node`` will be modified to reflect its new tree state in the database. This method explicitly checks for ``node`` being made a sibling of a root node, as this is a special case due to our use of tree ids to order root nodes. NOTE: This is a low-level method; it does NOT respect ``MPTTMeta.order_insertion_by``. In most cases you should just move the node yourself by setting node.parent. """ self._move_node(node, target, position=position) node_moved.send(sender=node.__class__, instance=node, target=target, position=position) @delegate_manager def root_node(self, tree_id): """ Returns the root node of the tree with the given id. """ return self._mptt_filter(tree_id=tree_id, parent=None).get() @delegate_manager def root_nodes(self): """ Creates a ``QuerySet`` containing root nodes. """ return self._mptt_filter(parent=None) @delegate_manager def rebuild(self): """ Rebuilds all trees in the database table using `parent` link. """ opts = self.model._mptt_meta qs = self._mptt_filter(parent=None) if opts.order_insertion_by: qs = qs.order_by(*opts.order_insertion_by) pks = qs.values_list('pk', flat=True) rebuild_helper = self._rebuild_helper idx = 0 for pk in pks: idx += 1 rebuild_helper(pk, 1, idx) rebuild.alters_data = True @delegate_manager def partial_rebuild(self, tree_id): """ Partially rebuilds a tree i.e. It rebuilds only the tree with given ``tree_id`` in database table using ``parent`` link. """ opts = self.model._mptt_meta qs = self._mptt_filter(parent=None, tree_id=tree_id) if opts.order_insertion_by: qs = qs.order_by(*opts.order_insertion_by) pks = qs.values_list('pk', flat=True) if not pks: return if len(pks) > 1: raise RuntimeError( "More than one root node with tree_id %d. That's invalid," " do a full rebuild." % tree_id) self._rebuild_helper(pks[0], 1, tree_id) def _rebuild_helper(self, pk, left, tree_id, level=0): opts = self.model._mptt_meta right = left + 1 qs = self._mptt_filter(parent__pk=pk) if opts.order_insertion_by: qs = qs.order_by(*opts.order_insertion_by) child_ids = qs.values_list('pk', flat=True) rebuild_helper = self._rebuild_helper for child_id in child_ids: right = rebuild_helper(child_id, right, tree_id, level + 1) qs = self.model._default_manager.filter(pk=pk) self._mptt_update( qs, left=left, right=right, level=level, tree_id=tree_id ) return right + 1 def _post_insert_update_cached_parent_right(self, instance, right_shift, seen=None): setattr(instance, self.right_attr, getattr(instance, self.right_attr) + right_shift) attr = '_%s_cache' % self.parent_attr if hasattr(instance, attr): parent = getattr(instance, attr) if parent: if not seen: seen = set() seen.add(instance) if parent in seen: # detect infinite recursion and throw an error raise InvalidMove self._post_insert_update_cached_parent_right(parent, right_shift, seen=seen) def _calculate_inter_tree_move_values(self, node, target, position): """ Calculates values required when moving ``node`` relative to ``target`` as specified by ``position``. """ left = getattr(node, self.left_attr) level = getattr(node, self.level_attr) target_left = getattr(target, self.left_attr) target_right = getattr(target, self.right_attr) target_level = getattr(target, self.level_attr) if position == 'last-child' or position == 'first-child': if position == 'last-child': space_target = target_right - 1 else: space_target = target_left level_change = level - target_level - 1 parent = target elif position == 'left' or position == 'right': if position == 'left': space_target = target_left - 1 else: space_target = target_right level_change = level - target_level parent = getattr(target, self.parent_attr) else: raise ValueError(_('An invalid position was given: %s.') % position) left_right_change = left - space_target - 1 right_shift = 0 if parent: right_shift = 2 * (node.get_descendant_count() + 1) return space_target, level_change, left_right_change, parent, right_shift def _close_gap(self, size, target, tree_id): """ Closes a gap of a certain ``size`` after the given ``target`` point in the tree identified by ``tree_id``. """ self._manage_space(-size, target, tree_id) def _create_space(self, size, target, tree_id): """ Creates a space of a certain ``size`` after the given ``target`` point in the tree identified by ``tree_id``. """ self._manage_space(size, target, tree_id) def _create_tree_space(self, target_tree_id, num_trees=1): """ Creates space for a new tree by incrementing all tree ids greater than ``target_tree_id``. """ qs = self._mptt_filter(tree_id__gt=target_tree_id) self._mptt_update(qs, tree_id=F(self.tree_id_attr) + num_trees) self.tree_model._mptt_track_tree_insertions(target_tree_id + 1, num_trees) def _get_next_tree_id(self): """ Determines the next largest unused tree id for the tree managed by this manager. """ max_tree_id = list(self.aggregate(Max(self.tree_id_attr)).values())[0] max_tree_id = max_tree_id or 0 return max_tree_id + 1 def _inter_tree_move_and_close_gap( self, node, level_change, left_right_change, new_tree_id, parent_pk=None): """ Removes ``node`` from its current tree, with the given set of changes being applied to ``node`` and its descendants, closing the gap left by moving ``node`` as it does so. If ``parent_pk`` is ``None``, this indicates that ``node`` is being moved to a brand new tree as its root node, and will thus have its parent field set to ``NULL``. Otherwise, ``node`` will have ``parent_pk`` set for its parent field. """ connection = self._get_connection(instance=node) qn = connection.ops.quote_name opts = self.model._meta inter_tree_move_query = """ UPDATE %(table)s SET %(level)s = CASE WHEN %(left)s >= %%s AND %(left)s <= %%s THEN %(level)s - %%s ELSE %(level)s END, %(tree_id)s = CASE WHEN %(left)s >= %%s AND %(left)s <= %%s THEN %%s ELSE %(tree_id)s END, %(left)s = CASE WHEN %(left)s >= %%s AND %(left)s <= %%s THEN %(left)s - %%s WHEN %(left)s > %%s THEN %(left)s - %%s ELSE %(left)s END, %(right)s = CASE WHEN %(right)s >= %%s AND %(right)s <= %%s THEN %(right)s - %%s WHEN %(right)s > %%s THEN %(right)s - %%s ELSE %(right)s END, %(parent)s = CASE WHEN %(pk)s = %%s THEN %(new_parent)s ELSE %(parent)s END WHERE %(tree_id)s = %%s""" % { 'table': qn(self.tree_model._meta.db_table), 'level': qn(opts.get_field(self.level_attr).column), 'left': qn(opts.get_field(self.left_attr).column), 'tree_id': qn(opts.get_field(self.tree_id_attr).column), 'right': qn(opts.get_field(self.right_attr).column), 'parent': qn(opts.get_field(self.parent_attr).column), 'pk': qn(opts.pk.column), 'new_parent': parent_pk is None and 'NULL' or '%s', } left = getattr(node, self.left_attr) right = getattr(node, self.right_attr) gap_size = right - left + 1 gap_target_left = left - 1 params = [ left, right, level_change, left, right, new_tree_id, left, right, left_right_change, gap_target_left, gap_size, left, right, left_right_change, gap_target_left, gap_size, node._meta.pk.get_db_prep_value(node.pk, connection), getattr(node, self.tree_id_attr) ] if parent_pk is not None: params.insert(-1, parent_pk) cursor = connection.cursor() cursor.execute(inter_tree_move_query, params) def _make_child_root_node(self, node, new_tree_id=None): """ Removes ``node`` from its tree, making it the root node of a new tree. If ``new_tree_id`` is not specified a new tree id will be generated. ``node`` will be modified to reflect its new tree state in the database. """ left = getattr(node, self.left_attr) right = getattr(node, self.right_attr) level = getattr(node, self.level_attr) if not new_tree_id: new_tree_id = self._get_next_tree_id() left_right_change = left - 1 self._inter_tree_move_and_close_gap(node, level, left_right_change, new_tree_id) # Update the node to be consistent with the updated # tree in the database. setattr(node, self.left_attr, left - left_right_change) setattr(node, self.right_attr, right - left_right_change) setattr(node, self.level_attr, 0) setattr(node, self.tree_id_attr, new_tree_id) setattr(node, self.parent_attr, None) node._mptt_cached_fields[self.parent_attr] = None def _make_sibling_of_root_node(self, node, target, position): """ Moves ``node``, making it a sibling of the given ``target`` root node as specified by ``position``. ``node`` will be modified to reflect its new tree state in the database. Since we use tree ids to reduce the number of rows affected by tree mangement during insertion and deletion, root nodes are not true siblings; thus, making an item a sibling of a root node is a special case which involves shuffling tree ids around. """ if node == target: raise InvalidMove(_('A node may not be made a sibling of itself.')) opts = self.model._meta tree_id = getattr(node, self.tree_id_attr) target_tree_id = getattr(target, self.tree_id_attr) if node.is_child_node(): if position == 'left': space_target = target_tree_id - 1 new_tree_id = target_tree_id elif position == 'right': space_target = target_tree_id new_tree_id = target_tree_id + 1 else: raise ValueError(_('An invalid position was given: %s.') % position) self._create_tree_space(space_target) if tree_id > space_target: # The node's tree id has been incremented in the # database - this change must be reflected in the node # object for the method call below to operate on the # correct tree. setattr(node, self.tree_id_attr, tree_id + 1) self._make_child_root_node(node, new_tree_id) else: if position == 'left': if target_tree_id > tree_id: left_sibling = target.get_previous_sibling() if node == left_sibling: return new_tree_id = getattr(left_sibling, self.tree_id_attr) lower_bound, upper_bound = tree_id, new_tree_id shift = -1 else: new_tree_id = target_tree_id lower_bound, upper_bound = new_tree_id, tree_id shift = 1 elif position == 'right': if target_tree_id > tree_id: new_tree_id = target_tree_id lower_bound, upper_bound = tree_id, target_tree_id shift = -1 else: right_sibling = target.get_next_sibling() if node == right_sibling: return new_tree_id = getattr(right_sibling, self.tree_id_attr) lower_bound, upper_bound = new_tree_id, tree_id shift = 1 else: raise ValueError(_('An invalid position was given: %s.') % position) connection = self._get_connection(instance=node) qn = connection.ops.quote_name root_sibling_query = """ UPDATE %(table)s SET %(tree_id)s = CASE WHEN %(tree_id)s = %%s THEN %%s ELSE %(tree_id)s + %%s END WHERE %(tree_id)s >= %%s AND %(tree_id)s <= %%s""" % { 'table': qn(self.tree_model._meta.db_table), 'tree_id': qn(opts.get_field(self.tree_id_attr).column), } cursor = connection.cursor() cursor.execute(root_sibling_query, [tree_id, new_tree_id, shift, lower_bound, upper_bound]) setattr(node, self.tree_id_attr, new_tree_id) def _manage_space(self, size, target, tree_id): """ Manages spaces in the tree identified by ``tree_id`` by changing the values of the left and right columns by ``size`` after the given ``target`` point. """ if self.tree_model._mptt_is_tracking: self.tree_model._mptt_track_tree_modified(tree_id) else: connection = self._get_connection() qn = connection.ops.quote_name opts = self.model._meta space_query = """ UPDATE %(table)s SET %(left)s = CASE WHEN %(left)s > %%s THEN %(left)s + %%s ELSE %(left)s END, %(right)s = CASE WHEN %(right)s > %%s THEN %(right)s + %%s ELSE %(right)s END WHERE %(tree_id)s = %%s AND (%(left)s > %%s OR %(right)s > %%s)""" % { 'table': qn(self.tree_model._meta.db_table), 'left': qn(opts.get_field(self.left_attr).column), 'right': qn(opts.get_field(self.right_attr).column), 'tree_id': qn(opts.get_field(self.tree_id_attr).column), } cursor = connection.cursor() cursor.execute(space_query, [target, size, target, size, tree_id, target, target]) def _move_child_node(self, node, target, position): """ Calls the appropriate method to move child node ``node`` relative to the given ``target`` node as specified by ``position``. """ tree_id = getattr(node, self.tree_id_attr) target_tree_id = getattr(target, self.tree_id_attr) if tree_id == target_tree_id: self._move_child_within_tree(node, target, position) else: self._move_child_to_new_tree(node, target, position) def _move_child_to_new_tree(self, node, target, position): """ Moves child node ``node`` to a different tree, inserting it relative to the given ``target`` node in the new tree as specified by ``position``. ``node`` will be modified to reflect its new tree state in the database. """ left = getattr(node, self.left_attr) right = getattr(node, self.right_attr) level = getattr(node, self.level_attr) new_tree_id = getattr(target, self.tree_id_attr) space_target, level_change, left_right_change, parent, new_parent_right = \ self._calculate_inter_tree_move_values(node, target, position) tree_width = right - left + 1 # Make space for the subtree which will be moved self._create_space(tree_width, space_target, new_tree_id) # Move the subtree connection = self._get_connection(instance=node) self._inter_tree_move_and_close_gap( node, level_change, left_right_change, new_tree_id, parent._meta.pk.get_db_prep_value(parent.pk, connection)) # Update the node to be consistent with the updated # tree in the database. setattr(node, self.left_attr, left - left_right_change) setattr(node, self.right_attr, right - left_right_change) setattr(node, self.level_attr, level - level_change) setattr(node, self.tree_id_attr, new_tree_id) setattr(node, self.parent_attr, parent) node._mptt_cached_fields[self.parent_attr] = parent.pk def _move_child_within_tree(self, node, target, position): """ Moves child node ``node`` within its current tree relative to the given ``target`` node as specified by ``position``. ``node`` will be modified to reflect its new tree state in the database. """ left = getattr(node, self.left_attr) right = getattr(node, self.right_attr) level = getattr(node, self.level_attr) width = right - left + 1 tree_id = getattr(node, self.tree_id_attr) target_left = getattr(target, self.left_attr) target_right = getattr(target, self.right_attr) target_level = getattr(target, self.level_attr) if position == 'last-child' or position == 'first-child': if node == target: raise InvalidMove(_('A node may not be made a child of itself.')) elif left < target_left < right: raise InvalidMove(_('A node may not be made a child of any of its descendants.')) if position == 'last-child': if target_right > right: new_left = target_right - width new_right = target_right - 1 else: new_left = target_right new_right = target_right + width - 1 else: if target_left > left: new_left = target_left - width + 1 new_right = target_left else: new_left = target_left + 1 new_right = target_left + width level_change = level - target_level - 1 parent = target elif position == 'left' or position == 'right': if node == target: raise InvalidMove(_('A node may not be made a sibling of itself.')) elif left < target_left < right: raise InvalidMove(_('A node may not be made a sibling of any of its descendants.')) if position == 'left': if target_left > left: new_left = target_left - width new_right = target_left - 1 else: new_left = target_left new_right = target_left + width - 1 else: if target_right > right: new_left = target_right - width + 1 new_right = target_right else: new_left = target_right + 1 new_right = target_right + width level_change = level - target_level parent = getattr(target, self.parent_attr) else: raise ValueError(_('An invalid position was given: %s.') % position) left_boundary = min(left, new_left) right_boundary = max(right, new_right) left_right_change = new_left - left gap_size = width if left_right_change > 0: gap_size = -gap_size connection = self._get_connection(instance=node) qn = connection.ops.quote_name opts = self.model._meta # The level update must come before the left update to keep # MySQL happy - left seems to refer to the updated value # immediately after its update has been specified in the query # with MySQL, but not with SQLite or Postgres. move_subtree_query = """ UPDATE %(table)s SET %(level)s = CASE WHEN %(left)s >= %%s AND %(left)s <= %%s THEN %(level)s - %%s ELSE %(level)s END, %(left)s = CASE WHEN %(left)s >= %%s AND %(left)s <= %%s THEN %(left)s + %%s WHEN %(left)s >= %%s AND %(left)s <= %%s THEN %(left)s + %%s ELSE %(left)s END, %(right)s = CASE WHEN %(right)s >= %%s AND %(right)s <= %%s THEN %(right)s + %%s WHEN %(right)s >= %%s AND %(right)s <= %%s THEN %(right)s + %%s ELSE %(right)s END, %(parent)s = CASE WHEN %(pk)s = %%s THEN %%s ELSE %(parent)s END WHERE %(tree_id)s = %%s""" % { 'table': qn(self.tree_model._meta.db_table), 'level': qn(opts.get_field(self.level_attr).column), 'left': qn(opts.get_field(self.left_attr).column), 'right': qn(opts.get_field(self.right_attr).column), 'parent': qn(opts.get_field(self.parent_attr).column), 'pk': qn(opts.pk.column), 'tree_id': qn(opts.get_field(self.tree_id_attr).column), } cursor = connection.cursor() cursor.execute(move_subtree_query, [ left, right, level_change, left, right, left_right_change, left_boundary, right_boundary, gap_size, left, right, left_right_change, left_boundary, right_boundary, gap_size, node._meta.get_field(node._meta.pk.name).get_db_prep_value(node.pk, connection), parent._meta.get_field(parent._meta.pk.name).get_db_prep_value(parent.pk, connection), tree_id]) # Update the node to be consistent with the updated # tree in the database. setattr(node, self.left_attr, new_left) setattr(node, self.right_attr, new_right) setattr(node, self.level_attr, level - level_change) setattr(node, self.parent_attr, parent) node._mptt_cached_fields[self.parent_attr] = parent.pk def _move_root_node(self, node, target, position): """ Moves root node``node`` to a different tree, inserting it relative to the given ``target`` node as specified by ``position``. ``node`` will be modified to reflect its new tree state in the database. """ left = getattr(node, self.left_attr) right = getattr(node, self.right_attr) level = getattr(node, self.level_attr) tree_id = getattr(node, self.tree_id_attr) new_tree_id = getattr(target, self.tree_id_attr) width = right - left + 1 if node == target: raise InvalidMove(_('A node may not be made a child of itself.')) elif tree_id == new_tree_id: raise InvalidMove(_('A node may not be made a child of any of its descendants.')) space_target, level_change, left_right_change, parent, right_shift = \ self._calculate_inter_tree_move_values(node, target, position) # Create space for the tree which will be inserted self._create_space(width, space_target, new_tree_id) # Move the root node, making it a child node connection = self._get_connection(instance=node) qn = connection.ops.quote_name opts = self.model._meta move_tree_query = """ UPDATE %(table)s SET %(level)s = %(level)s - %%s, %(left)s = %(left)s - %%s, %(right)s = %(right)s - %%s, %(tree_id)s = %%s, %(parent)s = CASE WHEN %(pk)s = %%s THEN %%s ELSE %(parent)s END WHERE %(left)s >= %%s AND %(left)s <= %%s AND %(tree_id)s = %%s""" % { 'table': qn(self.tree_model._meta.db_table), 'level': qn(opts.get_field(self.level_attr).column), 'left': qn(opts.get_field(self.left_attr).column), 'right': qn(opts.get_field(self.right_attr).column), 'tree_id': qn(opts.get_field(self.tree_id_attr).column), 'parent': qn(opts.get_field(self.parent_attr).column), 'pk': qn(opts.pk.column), } cursor = connection.cursor() cursor.execute(move_tree_query, [ level_change, left_right_change, left_right_change, new_tree_id, node._meta.pk.get_db_prep_value(node.pk, connection), parent._meta.pk.get_db_prep_value(parent.pk, connection), left, right, tree_id]) # Update the former root node to be consistent with the updated # tree in the database. setattr(node, self.left_attr, left - left_right_change) setattr(node, self.right_attr, right - left_right_change) setattr(node, self.level_attr, level - level_change) setattr(node, self.tree_id_attr, new_tree_id) setattr(node, self.parent_attr, parent) node._mptt_cached_fields[self.parent_attr] = parent.pk python-django-mptt-0.8.5/mptt/models.py000066400000000000000000001214401275105100200201030ustar00rootroot00000000000000from __future__ import unicode_literals from functools import reduce, wraps import operator import threading from django.db import models from django.db.models.base import ModelBase from django.db.models.query import Q from django.db.models.query_utils import DeferredAttribute from django.utils import six from django.utils.translation import ugettext as _ from mptt.fields import TreeForeignKey, TreeOneToOneField, TreeManyToManyField from mptt.managers import TreeManager from mptt.signals import node_moved from mptt.utils import _get_tree_model __all__ = ( 'TreeForeignKey', 'TreeOneToOneField', 'TreeManyToManyField', 'TreeManager', 'MPTTOptions', 'MPTTModelBase', 'MPTTModel', ) class _classproperty(object): def __init__(self, getter, setter=None): self.fget = getter self.fset = setter def __get__(self, cls, owner): return self.fget(owner) def __set__(self, cls, owner, value): if not self.fset: raise AttributeError("This classproperty is read only") self.fset(owner, value) class classpropertytype(property): def __init__(self, name, bases=(), members={}): return super(classpropertytype, self).__init__( members.get('__get__'), members.get('__set__'), members.get('__delete__'), members.get('__doc__') ) classproperty = classpropertytype('classproperty') class MPTTOptions(object): """ Options class for MPTT models. Use this as an inner class called ``MPTTMeta``:: class MyModel(MPTTModel): class MPTTMeta: order_insertion_by = ['name'] parent_attr = 'myparent' """ order_insertion_by = [] left_attr = 'lft' right_attr = 'rght' tree_id_attr = 'tree_id' level_attr = 'level' parent_attr = 'parent' def __init__(self, opts=None, **kwargs): # Override defaults with options provided if opts: opts = list(opts.__dict__.items()) else: opts = [] opts.extend(list(kwargs.items())) if 'tree_manager_attr' in [opt[0] for opt in opts]: raise ValueError( "`tree_manager_attr` has been removed; you should instantiate" " a TreeManager as a normal manager on your model instead.") for key, value in opts: if key[:2] == '__': continue setattr(self, key, value) # Normalize order_insertion_by to a list if isinstance(self.order_insertion_by, six.string_types): self.order_insertion_by = [self.order_insertion_by] elif isinstance(self.order_insertion_by, tuple): self.order_insertion_by = list(self.order_insertion_by) elif self.order_insertion_by is None: self.order_insertion_by = [] def __iter__(self): return ((k, v) for k, v in self.__dict__.items() if k[0] != '_') # Helper methods for accessing tree attributes on models. def get_raw_field_value(self, instance, field_name): """ Gets the value of the given fieldname for the instance. This is not the same as getattr(). This function will return IDs for foreignkeys etc, rather than doing a database query. """ field = instance._meta.get_field(field_name) return field.value_from_object(instance) def set_raw_field_value(self, instance, field_name, value): """ Sets the value of the given fieldname for the instance. This is not the same as setattr(). This function requires an ID for a foreignkey (etc) rather than an instance. """ field = instance._meta.get_field(field_name) setattr(instance, field.attname, value) def update_mptt_cached_fields(self, instance): """ Caches (in an instance._mptt_cached_fields dict) the original values of: - parent pk - fields specified in order_insertion_by These are used in save() to determine if the relevant fields have changed, so that the MPTT fields need to be updated. """ instance._mptt_cached_fields = {} field_names = set((self.parent_attr,)) if self.order_insertion_by: for f in self.order_insertion_by: if f[0] == '-': f = f[1:] field_names.add(f) deferred_fields = instance.get_deferred_fields() for field_name in field_names: if deferred_fields: field = instance._meta.get_field(field_name) if field.attname in deferred_fields \ and field.attname not in instance.__dict__: # deferred attribute (i.e. via .only() or .defer()) # It'd be silly to cache this (that'd do a database query) # Instead, we mark it as a deferred attribute here, then # assume it hasn't changed during save(), unless it's no # longer deferred. instance._mptt_cached_fields[field_name] = DeferredAttribute continue instance._mptt_cached_fields[field_name] = self.get_raw_field_value( instance, field_name) def insertion_target_filters(self, instance, order_insertion_by): """ Creates a filter which matches suitable right siblings for ``node``, where insertion should maintain ordering according to the list of fields in ``order_insertion_by``. For example, given an ``order_insertion_by`` of ``['field1', 'field2', 'field3']``, the resulting filter should correspond to the following SQL:: field1 > %s OR (field1 = %s AND field2 > %s) OR (field1 = %s AND field2 = %s AND field3 > %s) """ fields = [] filters = [] fields__append = fields.append filters__append = filters.append and_ = operator.and_ or_ = operator.or_ for field_name in order_insertion_by: if field_name[0] == '-': field_name = field_name[1:] filter_suffix = '__lt' else: filter_suffix = '__gt' value = getattr(instance, field_name) if value is None: # node isn't saved yet. get the insertion value from pre_save. field = instance._meta.get_field(field_name) value = field.pre_save(instance, True) q = Q(**{field_name + filter_suffix: value}) filters__append(reduce(and_, [Q(**{f: v}) for f, v in fields] + [q])) fields__append((field_name, value)) return reduce(or_, filters) def get_ordered_insertion_target(self, node, parent): """ Attempts to retrieve a suitable right sibling for ``node`` underneath ``parent`` (which may be ``None`` in the case of root nodes) so that ordering by the fields specified by the node's class' ``order_insertion_by`` option is maintained. Returns ``None`` if no suitable sibling can be found. """ right_sibling = None # Optimisation - if the parent doesn't have descendants, # the node will always be its last child. if parent is None or parent.get_descendant_count() > 0: opts = node._mptt_meta order_by = opts.order_insertion_by[:] filters = self.insertion_target_filters(node, order_by) if parent: filters = filters & Q(**{opts.parent_attr: parent}) # Fall back on tree ordering if multiple child nodes have # the same values. order_by.append(opts.left_attr) else: filters = filters & Q(**{opts.parent_attr: None}) # Fall back on tree id ordering if multiple root nodes have # the same values. order_by.append(opts.tree_id_attr) queryset = node.__class__._tree_manager.db_manager(node._state.db).filter(filters).order_by(*order_by) if node.pk: queryset = queryset.exclude(pk=node.pk) try: right_sibling = queryset[:1][0] except IndexError: # No suitable right sibling could be found pass return right_sibling class MPTTModelBase(ModelBase): """ Metaclass for MPTT models """ def __new__(meta, class_name, bases, class_dict): """ Create subclasses of MPTTModel. This: - adds the MPTT fields to the class - adds a TreeManager to the model """ if class_name == 'NewBase' and class_dict == {}: return super(MPTTModelBase, meta).__new__(meta, class_name, bases, class_dict) is_MPTTModel = False try: MPTTModel except NameError: is_MPTTModel = True MPTTMeta = class_dict.pop('MPTTMeta', None) if not MPTTMeta: class MPTTMeta: pass initial_options = frozenset(dir(MPTTMeta)) # extend MPTTMeta from base classes for base in bases: if hasattr(base, '_mptt_meta'): for name, value in base._mptt_meta: if name == 'tree_manager_attr': continue if name not in initial_options: setattr(MPTTMeta, name, value) class_dict['_mptt_meta'] = MPTTOptions(MPTTMeta) super_new = super(MPTTModelBase, meta).__new__ cls = super_new(meta, class_name, bases, class_dict) cls = meta.register(cls) # see error cases in TreeManager.disable_mptt_updates for the reasoning here. cls._mptt_tracking_base = None if is_MPTTModel: bases = [cls] else: bases = [base for base in cls.mro() if issubclass(base, MPTTModel)] for base in bases: if (not (base._meta.abstract or base._meta.proxy) and base._tree_manager.tree_model is base): cls._mptt_tracking_base = base break if cls is cls._mptt_tracking_base: cls._threadlocal = threading.local() # set on first access (to make threading errors more obvious): # cls._threadlocal.mptt_delayed_tree_changes = None return cls @classmethod def register(meta, cls, **kwargs): """ For the weird cases when you need to add tree-ness to an *existing* class. For other cases you should subclass MPTTModel instead of calling this. """ if not issubclass(cls, models.Model): raise ValueError(_("register() expects a Django model class argument")) if not hasattr(cls, '_mptt_meta'): cls._mptt_meta = MPTTOptions(**kwargs) abstract = getattr(cls._meta, 'abstract', False) try: MPTTModel except NameError: # We're defining the base class right now, so don't do anything # We only want to add this stuff to the subclasses. # (Otherwise if field names are customized, we'll end up adding two # copies) pass else: if not issubclass(cls, MPTTModel): bases = list(cls.__bases__) # strip out bases that are strict superclasses of MPTTModel. # (i.e. Model, object) # this helps linearize the type hierarchy if possible for i in range(len(bases) - 1, -1, -1): if issubclass(MPTTModel, bases[i]): del bases[i] bases.insert(0, MPTTModel) cls.__bases__ = tuple(bases) if _get_tree_model(cls) is cls: # HACK: _meta.get_field() doesn't work before AppCache.ready in Django>=1.8 # ( see https://code.djangoproject.com/ticket/24231 ) # So the only way to get existing fields is using local_fields on all superclasses. existing_field_names = set() for base in cls.mro(): if hasattr(base, '_meta'): existing_field_names.update([f.name for f in base._meta.local_fields]) for key in ('left_attr', 'right_attr', 'tree_id_attr', 'level_attr'): field_name = getattr(cls._mptt_meta, key) if field_name not in existing_field_names: field = models.PositiveIntegerField(db_index=True, editable=False) field.contribute_to_class(cls, field_name) # Add a tree manager, if there isn't one already if not abstract: manager = getattr(cls, 'objects', None) if manager is None: manager = cls._default_manager._copy_to_model(cls) manager.contribute_to_class(cls, 'objects') elif manager.model != cls: # manager was inherited manager = manager._copy_to_model(cls) manager.contribute_to_class(cls, 'objects') # make sure we have a tree manager somewhere tree_manager = None if hasattr(cls._meta, 'concrete_managers'): # Django < 1.10 cls_managers = cls._meta.concrete_managers + cls._meta.abstract_managers cls_managers = [r[2] for r in cls_managers] else: cls_managers = cls._meta.managers for cls_manager in cls_managers: if isinstance(cls_manager, TreeManager): # prefer any locally defined manager (i.e. keep going if not local) if cls_manager.model is cls: tree_manager = cls_manager break if tree_manager and tree_manager.model is not cls: tree_manager = tree_manager._copy_to_model(cls) elif tree_manager is None: tree_manager = TreeManager() tree_manager.contribute_to_class(cls, '_tree_manager') # avoid using ManagerDescriptor, so instances can refer to self._tree_manager setattr(cls, '_tree_manager', tree_manager) return cls def raise_if_unsaved(func): @wraps(func) def _fn(self, *args, **kwargs): if not self.pk: raise ValueError( 'Cannot call %(function)s on unsaved %(class)s instances' % {'function': func.__name__, 'class': self.__class__.__name__} ) return func(self, *args, **kwargs) return _fn class MPTTModel(six.with_metaclass(MPTTModelBase, models.Model)): """ Base class for tree models. """ _default_manager = TreeManager() class Meta: abstract = True def __init__(self, *args, **kwargs): super(MPTTModel, self).__init__(*args, **kwargs) self._mptt_meta.update_mptt_cached_fields(self) self._tree_manager = self._tree_manager.db_manager(self._state.db) def _mpttfield(self, fieldname): translated_fieldname = getattr(self._mptt_meta, fieldname + '_attr') return getattr(self, translated_fieldname) @_classproperty def _mptt_updates_enabled(cls): if not cls._mptt_tracking_base: return True return getattr(cls._mptt_tracking_base._threadlocal, 'mptt_updates_enabled', True) # ideally this'd be part of the _mptt_updates_enabled classproperty, but it seems # that settable classproperties are very, very hard to do! suggestions please :) @classmethod def _set_mptt_updates_enabled(cls, value): assert cls is cls._mptt_tracking_base,\ "Can't enable or disable mptt updates on a non-tracking class." cls._threadlocal.mptt_updates_enabled = value @_classproperty def _mptt_is_tracking(cls): if not cls._mptt_tracking_base: return False if not hasattr(cls._threadlocal, 'mptt_delayed_tree_changes'): # happens the first time this is called from each thread cls._threadlocal.mptt_delayed_tree_changes = None return cls._threadlocal.mptt_delayed_tree_changes is not None @classmethod def _mptt_start_tracking(cls): assert cls is cls._mptt_tracking_base,\ "Can't start or stop mptt tracking on a non-tracking class." assert not cls._mptt_is_tracking, "mptt tracking is already started." cls._threadlocal.mptt_delayed_tree_changes = set() @classmethod def _mptt_stop_tracking(cls): assert cls is cls._mptt_tracking_base,\ "Can't start or stop mptt tracking on a non-tracking class." assert cls._mptt_is_tracking, "mptt tracking isn't started." results = cls._threadlocal.mptt_delayed_tree_changes cls._threadlocal.mptt_delayed_tree_changes = None return results @classmethod def _mptt_track_tree_modified(cls, tree_id): if not cls._mptt_is_tracking: return cls._threadlocal.mptt_delayed_tree_changes.add(tree_id) @classmethod def _mptt_track_tree_insertions(cls, tree_id, num_inserted): if not cls._mptt_is_tracking: return changes = cls._threadlocal.mptt_delayed_tree_changes if not num_inserted or not changes: return if num_inserted < 0: deleted = range(tree_id + num_inserted, -num_inserted) changes.difference_update(deleted) new_changes = set( (t + num_inserted if t >= tree_id else t) for t in changes) cls._threadlocal.mptt_delayed_tree_changes = new_changes @raise_if_unsaved def get_ancestors(self, ascending=False, include_self=False): """ Creates a ``QuerySet`` containing the ancestors of this model instance. This defaults to being in descending order (root ancestor first, immediate parent last); passing ``True`` for the ``ascending`` argument will reverse the ordering (immediate parent first, root ancestor last). If ``include_self`` is ``True``, the ``QuerySet`` will also include this model instance. """ if self.is_root_node(): if not include_self: return self._tree_manager.none() else: # Filter on pk for efficiency. qs = self._tree_manager.filter(pk=self.pk) else: opts = self._mptt_meta order_by = opts.left_attr if ascending: order_by = '-' + order_by left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) if not include_self: left -= 1 right += 1 qs = self._tree_manager._mptt_filter( left__lte=left, right__gte=right, tree_id=self._mpttfield('tree_id'), ) qs = qs.order_by(order_by) if hasattr(self, '_mptt_use_cached_ancestors'): # Called during or after a `recursetree` tag. # There should be cached parents up to level 0. # So we can use them to avoid doing a query at all. ancestors = [] p = self if not include_self: p = getattr(p, opts.parent_attr) while p is not None: ancestors.append(p) p = getattr(p, opts.parent_attr) ancestors.reverse() qs._result_cache = ancestors return qs @raise_if_unsaved def get_family(self): """ Returns a ``QuerySet`` containing the ancestors, the model itself and the descendants, in tree order. """ opts = self._mptt_meta left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) ancestors = Q(**{ "%s__lte" % opts.left_attr: left, "%s__gte" % opts.right_attr: right, opts.tree_id_attr: self._mpttfield('tree_id'), }) descendants = Q(**{ "%s__gte" % opts.left_attr: left, "%s__lte" % opts.left_attr: right, opts.tree_id_attr: self._mpttfield('tree_id'), }) return self._tree_manager.filter(ancestors | descendants) @raise_if_unsaved def get_children(self): """ Returns a ``QuerySet`` containing the immediate children of this model instance, in tree order. The benefit of using this method over the reverse relation provided by the ORM to the instance's children is that a database query can be avoided in the case where the instance is a leaf node (it has no children). If called from a template where the tree has been walked by the ``cache_tree_children`` filter, no database query is required. """ if hasattr(self, '_cached_children'): qs = self._tree_manager.filter(pk__in=[n.pk for n in self._cached_children]) qs._result_cache = self._cached_children return qs else: if self.is_leaf_node(): return self._tree_manager.none() return self._tree_manager._mptt_filter(parent=self) @raise_if_unsaved def get_descendants(self, include_self=False): """ Creates a ``QuerySet`` containing descendants of this model instance, in tree order. If ``include_self`` is ``True``, the ``QuerySet`` will also include this model instance. """ if self.is_leaf_node(): if not include_self: return self._tree_manager.none() else: return self._tree_manager.filter(pk=self.pk) opts = self._mptt_meta left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) if not include_self: left += 1 right -= 1 return self._tree_manager._mptt_filter( tree_id=self._mpttfield('tree_id'), left__gte=left, left__lte=right ) def get_descendant_count(self): """ Returns the number of descendants this model instance has. """ if self._mpttfield('right') is None: # node not saved yet return 0 else: return (self._mpttfield('right') - self._mpttfield('left') - 1) // 2 @raise_if_unsaved def get_leafnodes(self, include_self=False): """ Creates a ``QuerySet`` containing leafnodes of this model instance, in tree order. If ``include_self`` is ``True``, the ``QuerySet`` will also include this model instance (if it is a leaf node) """ descendants = self.get_descendants(include_self=include_self) return self._tree_manager._mptt_filter( descendants, left=(models.F(self._mptt_meta.right_attr) - 1) ) @raise_if_unsaved def get_next_sibling(self, *filter_args, **filter_kwargs): """ Returns this model instance's next sibling in the tree, or ``None`` if it doesn't have a next sibling. """ qs = self._tree_manager.filter(*filter_args, **filter_kwargs) if self.is_root_node(): qs = self._tree_manager._mptt_filter( qs, parent=None, tree_id__gt=self._mpttfield('tree_id'), ) else: qs = self._tree_manager._mptt_filter( qs, parent__pk=getattr(self, self._mptt_meta.parent_attr + '_id'), left__gt=self._mpttfield('right'), ) siblings = qs[:1] return siblings and siblings[0] or None @raise_if_unsaved def get_previous_sibling(self, *filter_args, **filter_kwargs): """ Returns this model instance's previous sibling in the tree, or ``None`` if it doesn't have a previous sibling. """ opts = self._mptt_meta qs = self._tree_manager.filter(*filter_args, **filter_kwargs) if self.is_root_node(): qs = self._tree_manager._mptt_filter( qs, parent=None, tree_id__lt=self._mpttfield('tree_id'), ) qs = qs.order_by('-' + opts.tree_id_attr) else: qs = self._tree_manager._mptt_filter( qs, parent__pk=getattr(self, opts.parent_attr + '_id'), right__lt=self._mpttfield('left'), ) qs = qs.order_by('-' + opts.right_attr) siblings = qs[:1] return siblings and siblings[0] or None @raise_if_unsaved def get_root(self): """ Returns the root node of this model instance's tree. """ if self.is_root_node() and type(self) == self._tree_manager.tree_model: return self return self._tree_manager._mptt_filter( tree_id=self._mpttfield('tree_id'), parent=None, ).get() @raise_if_unsaved def get_siblings(self, include_self=False): """ Creates a ``QuerySet`` containing siblings of this model instance. Root nodes are considered to be siblings of other root nodes. If ``include_self`` is ``True``, the ``QuerySet`` will also include this model instance. """ if self.is_root_node(): queryset = self._tree_manager._mptt_filter(parent=None) else: parent_id = getattr(self, self._mptt_meta.parent_attr + '_id') queryset = self._tree_manager._mptt_filter(parent__pk=parent_id) if not include_self: queryset = queryset.exclude(pk=self.pk) return queryset def get_level(self): """ Returns the level of this node (distance from root) """ return getattr(self, self._mptt_meta.level_attr) def insert_at(self, target, position='first-child', save=False, allow_existing_pk=False, refresh_target=True): """ Convenience method for calling ``TreeManager.insert_node`` with this model instance. """ self._tree_manager.insert_node( self, target, position, save, allow_existing_pk=allow_existing_pk, refresh_target=refresh_target) def is_child_node(self): """ Returns ``True`` if this model instance is a child node, ``False`` otherwise. """ return not self.is_root_node() def is_leaf_node(self): """ Returns ``True`` if this model instance is a leaf node (it has no children), ``False`` otherwise. """ return not self.get_descendant_count() def is_root_node(self): """ Returns ``True`` if this model instance is a root node, ``False`` otherwise. """ return getattr(self, self._mptt_meta.parent_attr + '_id') is None @raise_if_unsaved def is_descendant_of(self, other, include_self=False): """ Returns ``True`` if this model is a descendant of the given node, ``False`` otherwise. If include_self is True, also returns True if the two nodes are the same node. """ opts = self._mptt_meta if include_self and other.pk == self.pk: return True if getattr(self, opts.tree_id_attr) != getattr(other, opts.tree_id_attr): return False else: left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) return ( left > getattr(other, opts.left_attr) and right < getattr(other, opts.right_attr)) @raise_if_unsaved def is_ancestor_of(self, other, include_self=False): """ Returns ``True`` if this model is an ancestor of the given node, ``False`` otherwise. If include_self is True, also returns True if the two nodes are the same node. """ if include_self and other.pk == self.pk: return True return other.is_descendant_of(self) def move_to(self, target, position='first-child'): """ Convenience method for calling ``TreeManager.move_node`` with this model instance. NOTE: This is a low-level method; it does NOT respect ``MPTTMeta.order_insertion_by``. In most cases you should just move the node yourself by setting node.parent. """ self._tree_manager.move_node(self, target, position) def _is_saved(self, using=None): if not self.pk or self._mpttfield('tree_id') is None: return False opts = self._meta if opts.pk.rel is None: return True else: if not hasattr(self, '_mptt_saved'): manager = self.__class__._base_manager manager = manager.using(using) self._mptt_saved = manager.filter(pk=self.pk).exists() return self._mptt_saved def _get_user_field_names(self): """ Returns the list of user defined (i.e. non-mptt internal) field names. """ from django.db.models.fields import AutoField field_names = [] internal_fields = ( self._mptt_meta.left_attr, self._mptt_meta.right_attr, self._mptt_meta.tree_id_attr, self._mptt_meta.level_attr, self._mptt_meta.parent_attr) for field in self._meta.fields: if (field.name not in internal_fields) and (not isinstance(field, AutoField)) and (not field.primary_key): # noqa field_names.append(field.name) return field_names def save(self, *args, **kwargs): """ If this is a new node, sets tree fields up before it is inserted into the database, making room in the tree structure as neccessary, defaulting to making the new node the last child of its parent. It the node's left and right edge indicators already been set, we take this as indication that the node has already been set up for insertion, so its tree fields are left untouched. If this is an existing node and its parent has been changed, performs reparenting in the tree structure, defaulting to making the node the last child of its new parent. In either case, if the node's class has its ``order_insertion_by`` tree option set, the node will be inserted or moved to the appropriate position to maintain ordering by the specified field. """ do_updates = self.__class__._mptt_updates_enabled track_updates = self.__class__._mptt_is_tracking opts = self._mptt_meta if not (do_updates or track_updates): # inside manager.disable_mptt_updates(), don't do any updates. # unless we're also inside TreeManager.delay_mptt_updates() if self._mpttfield('left') is None: # we need to set *some* values, though don't care too much what. parent = getattr(self, '_%s_cache' % opts.parent_attr, None) # if we have a cached parent, have a stab at getting # possibly-correct values. otherwise, meh. if parent: left = parent._mpttfield('left') + 1 setattr(self, opts.left_attr, left) setattr(self, opts.right_attr, left + 1) setattr(self, opts.level_attr, parent._mpttfield('level') + 1) setattr(self, opts.tree_id_attr, parent._mpttfield('tree_id')) self._tree_manager._post_insert_update_cached_parent_right(parent, 2) else: setattr(self, opts.left_attr, 1) setattr(self, opts.right_attr, 2) setattr(self, opts.level_attr, 0) setattr(self, opts.tree_id_attr, 0) return super(MPTTModel, self).save(*args, **kwargs) parent_id = opts.get_raw_field_value(self, opts.parent_attr) # determine whether this instance is already in the db force_update = kwargs.get('force_update', False) force_insert = kwargs.get('force_insert', False) collapse_old_tree = None deferred_fields = self.get_deferred_fields() if force_update or (not force_insert and self._is_saved(using=kwargs.get('using'))): # it already exists, so do a move old_parent_id = self._mptt_cached_fields[opts.parent_attr] if old_parent_id is DeferredAttribute: same_order = True else: same_order = old_parent_id == parent_id if same_order and len(self._mptt_cached_fields) > 1: for field_name, old_value in self._mptt_cached_fields.items(): if old_value is DeferredAttribute and field_name not in deferred_fields: same_order = False break if old_value != opts.get_raw_field_value(self, field_name): same_order = False break if not do_updates and not same_order: same_order = True self.__class__._mptt_track_tree_modified(self._mpttfield('tree_id')) elif (not do_updates) and not same_order and old_parent_id is None: # the old tree no longer exists, so we need to collapse it. collapse_old_tree = self._mpttfield('tree_id') parent = getattr(self, opts.parent_attr) tree_id = parent._mpttfield('tree_id') left = parent._mpttfield('left') + 1 self.__class__._mptt_track_tree_modified(tree_id) setattr(self, opts.tree_id_attr, tree_id) setattr(self, opts.left_attr, left) setattr(self, opts.right_attr, left + 1) setattr(self, opts.level_attr, parent._mpttfield('level') + 1) same_order = True if not same_order: opts.set_raw_field_value(self, opts.parent_attr, old_parent_id) try: right_sibling = None if opts.order_insertion_by: right_sibling = opts.get_ordered_insertion_target( self, getattr(self, opts.parent_attr)) if parent_id is not None: parent = getattr(self, opts.parent_attr) # If we aren't already a descendant of the new parent, # we need to update the parent.rght so things like # get_children and get_descendant_count work correctly. # # parent might be None if parent_id was assigned # directly -- then we certainly do not have to update # the cached parent. update_cached_parent = parent and ( getattr(self, opts.tree_id_attr) != getattr(parent, opts.tree_id_attr) or # noqa getattr(self, opts.left_attr) < getattr(parent, opts.left_attr) or getattr(self, opts.right_attr) > getattr(parent, opts.right_attr)) if right_sibling: self._tree_manager._move_node( self, right_sibling, 'left', save=False, refresh_target=False) else: # Default movement if parent_id is None: root_nodes = self._tree_manager.root_nodes() try: rightmost_sibling = root_nodes.exclude( pk=self.pk).order_by('-' + opts.tree_id_attr)[0] self._tree_manager._move_node( self, rightmost_sibling, 'right', save=False, refresh_target=False) except IndexError: pass else: self._tree_manager._move_node( self, parent, 'last-child', save=False) if parent_id is not None and update_cached_parent: # Update rght of cached parent right_shift = 2 * (self.get_descendant_count() + 1) self._tree_manager._post_insert_update_cached_parent_right( parent, right_shift) finally: # Make sure the new parent is always # restored on the way out in case of errors. opts.set_raw_field_value(self, opts.parent_attr, parent_id) # If there were no exceptions raised then send a moved signal node_moved.send(sender=self.__class__, instance=self, target=getattr(self, opts.parent_attr)) else: opts.set_raw_field_value(self, opts.parent_attr, parent_id) if not track_updates: # When not using delayed/disabled updates, # populate update_fields with user defined model fields. # This helps preserve tree integrity when saving model on top # of a modified tree. if len(args) > 3: if not args[3]: args = list(args) args[3] = self._get_user_field_names() args = tuple(args) else: if not kwargs.get("update_fields", None): kwargs["update_fields"] = self._get_user_field_names() else: # new node, do an insert if (getattr(self, opts.left_attr) and getattr(self, opts.right_attr)): # This node has already been set up for insertion. pass else: parent = getattr(self, opts.parent_attr) right_sibling = None # if we're inside delay_mptt_updates, don't do queries to find # sibling position. instead, do default insertion. correct # positions will be found during partial rebuild later. # *unless* this is a root node. (as update tracking doesn't # handle re-ordering of trees.) if do_updates or parent is None: if opts.order_insertion_by: right_sibling = opts.get_ordered_insertion_target(self, parent) if right_sibling: self.insert_at(right_sibling, 'left', allow_existing_pk=True, refresh_target=False) if parent: # since we didn't insert into parent, we have to update parent.rght # here instead of in TreeManager.insert_node() right_shift = 2 * (self.get_descendant_count() + 1) self._tree_manager._post_insert_update_cached_parent_right( parent, right_shift) else: # Default insertion self.insert_at(parent, position='last-child', allow_existing_pk=True) try: super(MPTTModel, self).save(*args, **kwargs) finally: if collapse_old_tree is not None: self._tree_manager._create_tree_space(collapse_old_tree, -1) self._mptt_saved = True opts.update_mptt_cached_fields(self) save.alters_data = True def delete(self, *args, **kwargs): """Calling ``delete`` on a node will delete it as well as its full subtree, as opposed to reattaching all the subnodes to its parent node. There are no argument specific to a MPTT model, all the arguments will be passed directly to the django's ``Model.delete``. ``delete`` will not return anything. """ tree_width = (self._mpttfield('right') - self._mpttfield('left') + 1) target_right = self._mpttfield('right') tree_id = self._mpttfield('tree_id') self._tree_manager._close_gap(tree_width, target_right, tree_id) parent = getattr(self, '_%s_cache' % self._mptt_meta.parent_attr, None) if parent: right_shift = -self.get_descendant_count() - 2 self._tree_manager._post_insert_update_cached_parent_right(parent, right_shift) super(MPTTModel, self).delete(*args, **kwargs) delete.alters_data = True def _mptt_refresh(self): if not self.pk: return manager = type(self)._tree_manager opts = self._mptt_meta values = manager.filter(pk=self.pk).values( opts.left_attr, opts.right_attr, opts.level_attr, opts.tree_id_attr, )[0] for k, v in values.items(): setattr(self, k, v) python-django-mptt-0.8.5/mptt/querysets.py000066400000000000000000000014201275105100200206570ustar00rootroot00000000000000from django.db import models from mptt import utils class TreeQuerySet(models.query.QuerySet): def get_descendants(self, *args, **kwargs): """ Alias to `mptt.managers.TreeManager.get_queryset_descendants`. """ return self.model._tree_manager.get_queryset_descendants(self, *args, **kwargs) get_descendants.queryset_only = True def get_ancestors(self, *args, **kwargs): """ Alias to `mptt.managers.TreeManager.get_queryset_ancestors`. """ return self.model._tree_manager.get_queryset_ancestors(self, *args, **kwargs) get_ancestors.queryset_only = True def get_cached_trees(self): """ Alias to `mptt.utils.get_cached_trees`. """ return utils.get_cached_trees(self) python-django-mptt-0.8.5/mptt/settings.py000066400000000000000000000003151275105100200204550ustar00rootroot00000000000000from django.conf import settings """Default level indicator. By default is `'---'`.""" DEFAULT_LEVEL_INDICATOR = getattr(settings, 'MPTT_DEFAULT_LEVEL_INDICATOR', '---') python-django-mptt-0.8.5/mptt/signals.py000066400000000000000000000005321275105100200202560ustar00rootroot00000000000000import django.dispatch # Behaves like Djangos normal pre-/post_save signals signals with the # added arguments ``target`` and ``position`` that matches those of # ``move_to``. # If the signal is called from ``save`` it'll not be pass position. node_moved = django.dispatch.Signal(providing_args=[ 'instance', 'target', 'position' ]) python-django-mptt-0.8.5/mptt/static/000077500000000000000000000000001275105100200175335ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/static/mptt/000077500000000000000000000000001275105100200205175ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/static/mptt/arrow-move.png000066400000000000000000000007111275105100200233220ustar00rootroot00000000000000PNG  IHDR{gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs I8tIME  +9XIDAT(ϝ PЇ9x#7;t*(>+ 細3Q(`qwL+$TL)Jbr'[Q/IzI>V9;x=\\=DB,~al<ŸO38]b%tEXtdate:create2015-12-16T13:43:57+01:00%%tEXtdate:modify2015-12-16T13:43:57+01:00˙tEXtSoftwarewww.inkscape.org<IENDB`python-django-mptt-0.8.5/mptt/static/mptt/disclosure-down.png000066400000000000000000000007601275105100200243510ustar00rootroot00000000000000PNG  IHDR{gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs I8tIME  +9XIDAT(ύ!s 3my#*f2UTT\1`-}_GP]ɛW/y5Vj񝵕~ֳ@\5I'{Gyp'2XM15TmCiJ T!YJ\Տ6{JoaO4y/ W#%tEXtdate:create2015-12-16T13:43:57+01:00%%tEXtdate:modify2015-12-16T13:43:57+01:00˙tEXtSoftwarewww.inkscape.org<IENDB`python-django-mptt-0.8.5/mptt/static/mptt/disclosure-right.png000066400000000000000000000007201275105100200245130ustar00rootroot00000000000000PNG  IHDR{gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs I8tIME  +9XIDAT(ϕ1o5ozF Ε\ :JBRmdQаzLɼĻZ8_K41>*R}Bao#0W/#p5DNQ=]v aO0lu0pP*c +rUqhj%tEXtdate:create2015-12-16T13:43:57+01:00%%tEXtdate:modify2015-12-16T13:43:57+01:00˙tEXtSoftwarewww.inkscape.org<IENDB`python-django-mptt-0.8.5/mptt/static/mptt/draggable-admin.css000066400000000000000000000017631275105100200242360ustar00rootroot00000000000000.field-tree_actions { width: 50px; padding: 2px; } .field-tree_actions > div { display: inline-block; vertical-align: middle; background-repeat: no-repeat; width: 18px; height: 18px; margin: 7px 2px 0 0; } .tree-node { cursor: pointer; } .tree-node.children { background-image: url(disclosure-down.png); } .tree-node.closed { background-image: url(disclosure-right.png); } .drag-handle { background-image: url(arrow-move.png); cursor: move; } /* focus */ #result_list tbody tr:focus { background-color: #ffffcc !important; outline: 0px; } #drag-line { position: absolute; height: 3px; font-size: 0px; background-color: #417690; } #drag-line:before { content: ' '; display: block; position: absolute; height: 10px; width: 10px; background: #417690; margin: -3px 0 0 0; border-radius: 9px; } #drag-line span { display: block; position: absolute; left: 15px; bottom: 0; width: 300px; height: 18px; color: #417690; font-size: 12px; } python-django-mptt-0.8.5/mptt/static/mptt/draggable-admin.js000066400000000000000000000355571275105100200240720ustar00rootroot00000000000000/* global django */ // IE<9 lacks Array.prototype.indexOf if (!Array.prototype.indexOf) { Array.prototype.indexOf = function(needle) { for (var i=0, l=this.length; i= 0) DraggableMPTTAdmin.collapsedNodes.splice(idx, 1); } function markNodeAsCollapsed(id) { if(isExpandedNode(id)) DraggableMPTTAdmin.collapsedNodes.push(id); } function treeNode(pk) { return $('.tree-node[data-pk="' + pk + '"]'); } // toggle children function doToggle(id, show) { var children = DraggableMPTTAdmin.treeStructure[id] || []; for (var i=0; i').appendTo('body'); } $('#ghost').html(cloned).css({ 'opacity': .8, 'position': 'absolute', 'top': event.pageY, 'left': event.pageX - 30, 'width': 600 }); // check on edge of screen if(event.pageY+100 > $(window).height()+$(window).scrollTop()) { $('html,body').stop().animate({scrollTop: $(window).scrollTop()+250 }, 500); } else if(event.pageY-50 < $(window).scrollTop()) { $('html,body').stop().animate({scrollTop: $(window).scrollTop()-250 }, 500); } // check if drag-line element already exists, else append if($('#drag-line').length < 1) { $('body').append('
'); } // loop trough all rows $('tr', originalRow.parent()).each(function(index, el) { var element = $(el), top = element.offset().top, next; // check if mouse is over a row if (event.pageY >= top && event.pageY < top + rowHeight) { var targetRow = null, targetLoc = null, elementLevel = rowLevel(element); if (event.pageY >= top && event.pageY < top + rowHeight / 3) { targetRow = element; targetLoc = BEFORE; } else if (event.pageY >= top + rowHeight / 3 && event.pageY < top + rowHeight * 2 / 3) { next = element.next(); // there's no point in allowing adding children when there are some already // better move the items to the correct place right away if (!next.length || rowLevel(next) <= elementLevel) { targetRow = element; targetLoc = CHILD; } } else if (event.pageY >= top + rowHeight * 2 / 3 && event.pageY < top + rowHeight) { next = element.next(); if (!next.length || rowLevel(next) <= elementLevel) { targetRow = element; targetLoc = AFTER; } } if(targetRow) { // Positioning relative to cell containing the link var offset = targetRow.find('th').offset(); var left = offset.left + rowLevel(targetRow) * CHILD_PAD + (targetLoc == CHILD ? CHILD_PAD : 0) + 5; // Center of the circle aligns with start of link text (cell padding!) $('#drag-line').css({ 'width': resultListWidth - left, 'left': left, 'top': offset.top + (targetLoc == BEFORE ? 0 : rowHeight) }).find('span').text(DraggableMPTTAdmin.messages[targetLoc] || ''); // Store the found row and options moveTo.hovering = element; moveTo.relativeTo = targetRow; moveTo.side = targetLoc; return true; } } }); }); $('body').keydown(function(event) { if (event.which == '27') { $('#drag-line').remove(); $('#ghost').remove(); $('body').removeClass('dragging').enableSelection().unbind('mousemove').unbind('mouseup'); event.preventDefault(); } }); $('body').bind('mouseup', function() { if(moveTo.relativeTo) { var cutItem = originalRow.find('.tree-node').data('pk'); var pastedOn = moveTo.relativeTo.find('.tree-node').data('pk'); // get out early if items are the same if(cutItem != pastedOn) { var isParent = ( rowLevel(moveTo.relativeTo.next()) > rowLevel(moveTo.relativeTo)); var position = ''; // determine position if(moveTo.side == CHILD && !isParent) { position = 'last-child'; } else if (moveTo.side == BEFORE) { position = 'left'; } else { position = 'right'; } $.ajax({ complete: function() { window.location.reload(); }, data: { cmd: 'move_node', position: position, cut_item: cutItem, pasted_on: pastedOn }, headers: { 'X-CSRFToken': getCookie('csrftoken') }, method: 'POST' }); } else { $('#drag-line').remove(); $('#ghost').remove(); } $('body').removeClass('dragging').enableSelection().unbind('mousemove').unbind('mouseup'); } }); }); return this; }); /* Every time the user expands or collapses a part of the tree, we remember the current state of the tree so we can restore it on a reload. */ function storeCollapsedNodes(nodes) { window.localStorage && window.localStorage.setItem( DraggableMPTTAdmin.storageName, JSON.stringify(nodes) ); } function retrieveCollapsedNodes() { try { return JSON.parse(window.localStorage.getItem( DraggableMPTTAdmin.storageName )); } catch(e) { return null; } } function expandOrCollapseNode(item) { var show = true; if (!item.hasClass('children')) return; var itemId = item.data('pk'); if (!isExpandedNode(itemId)) { item.removeClass('closed'); markNodeAsExpanded(itemId); } else { item.addClass('closed'); show = false; markNodeAsCollapsed(itemId); } storeCollapsedNodes(DraggableMPTTAdmin.collapsedNodes); doToggle(itemId, show); } function collapseTree() { var rlist = $("#result_list"); rlist.hide(); $('tbody tr', rlist).each(function(i, el) { var marker = $('.tree-node', el); if (marker.hasClass('children')) { var itemId = marker.data('pk'); doToggle(itemId, false); marker.addClass('closed'); markNodeAsCollapsed(itemId); } }); storeCollapsedNodes(DraggableMPTTAdmin.collapsedNodes); rlist.show(); return false; } function expandTree() { var rlist = $("#result_list"); rlist.hide(); $('tbody tr', rlist).each(function(i, el) { var marker = $('.tree-node', el); if (marker.hasClass('children')) { var itemId = $('.tree-node', el).data('pk'); doToggle(itemId, true); marker.removeClass('closed'); markNodeAsExpanded(itemId); } }); storeCollapsedNodes([]); rlist.show(); return false; } var changelistTab = function(elem, event, direction) { event.preventDefault(); elem = $(elem); var ne = (direction > 0) ? elem.nextAll(':visible:first') : elem.prevAll(':visible:first'); if(ne) { elem.attr('tabindex', -1); ne.attr('tabindex', '0'); ne.focus(); } }; function keyboardNavigationHandler(event) { // On form element? Ignore. if (/textarea|select|input/i.test(event.target.nodeName)) return; // console.log('keydown', this, event.keyCode); switch (event.keyCode) { case 40: // down changelistTab(this, event, 1); break; case 38: // up changelistTab(this, event, -1); break; case 37: // left case 39: // right expandOrCollapseNode($(this).find('.tree-node')); break; case 13: // return document.location = $('a', this).attr('href'); break; default: break; } } function addObjectTool(title, handler) { var $a = $(''); $a.click(handler); $a.text(title); $a.prependTo('.object-tools').wrap('
  • '); } // Some old browsers do not support JSON.parse (the only thing we require) var jsonParse = JSON.parse || function jsonParse(sJSON) { return eval('(' + sJSON + ')'); }; DraggableMPTTAdmin = jsonParse( document.getElementById('draggable-admin-context').getAttribute('data-context')); addObjectTool(DraggableMPTTAdmin.messages.collapseTree, collapseTree); addObjectTool(DraggableMPTTAdmin.messages.expandTree, expandTree); // fire! var rlist = $("#result_list"), rlist_tbody = rlist.find('tbody'); if ($('tbody tr', rlist).length > 1) { rlist_tbody.feinTree(); rlist.find('.tree-node').on('click', function(event) { event.preventDefault(); event.stopPropagation(); expandOrCollapseNode($(this)); }); /* Enable focussing, put focus on first result, add handler for keyboard navigation */ $('tr', rlist).attr('tabindex', -1); $('tbody tr:first', rlist).attr('tabindex', 0).focus(); $('tr', rlist).keydown(keyboardNavigationHandler); DraggableMPTTAdmin.collapsedNodes = []; var storedNodes = retrieveCollapsedNodes(); if (storedNodes) { for(var i=0; i{# DIV for HTML validation #} {% for item in result_hidden_fields %}{{ item }}{% endfor %} {% endif %} {% if results %}
    {% for header in result_headers %} {% endfor %} {% for result in results %} {% if result.form.non_field_errors %} {% endif %} {% for item in result %}{{ item }}{% endfor %} {% endfor %}
    {{ header.text|capfirst }}
    {{ result.form.non_field_errors }}
    {% endif %}python-django-mptt-0.8.5/mptt/templates/admin/mptt_change_list.html000066400000000000000000000005341275105100200255460ustar00rootroot00000000000000{% extends "admin/change_list.html" %} {% load admin_list i18n mptt_admin %} {% block result_list %} {% if action_form and actions_on_top and cl.full_result_count %}{% admin_actions %}{% endif %} {% mptt_result_list cl %} {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} {% endblock %} python-django-mptt-0.8.5/mptt/templates/admin/mptt_change_list_results.html000066400000000000000000000014651275105100200273330ustar00rootroot00000000000000{% if result_hidden_fields %}
    {# DIV for HTML validation #} {% for item in result_hidden_fields %}{{ item }}{% endfor %}
    {% endif %} {% if results %}
    {% for header in result_headers %} {% endfor %} {% for result in results %} {% if result.form.non_field_errors %} {% endif %} {% for item in result %}{{ item }}{% endfor %} {% endfor %}
    {{ header.text|capfirst }}
    {{ result.form.non_field_errors }}
    {% endif %} python-django-mptt-0.8.5/mptt/templatetags/000077500000000000000000000000001275105100200207365ustar00rootroot00000000000000python-django-mptt-0.8.5/mptt/templatetags/__init__.py000066400000000000000000000000501275105100200230420ustar00rootroot00000000000000from __future__ import unicode_literals python-django-mptt-0.8.5/mptt/templatetags/mptt_admin.py000066400000000000000000000166721275105100200234600ustar00rootroot00000000000000from __future__ import unicode_literals from django.conf import settings from django.contrib.admin.templatetags.admin_list import ( result_hidden_fields, _boolean_icon, result_headers) from django.contrib.admin.utils import lookup_field, display_for_field from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.template import Library from django.utils.encoding import smart_text, force_text from django.utils.html import escape, escapejs, conditional_escape from django.utils.safestring import mark_safe from django.utils.translation import get_language_bidi register = Library() MPTT_ADMIN_LEVEL_INDENT = getattr(settings, 'MPTT_ADMIN_LEVEL_INDENT', 10) IS_GRAPPELLI_INSTALLED = True if 'grappelli' in settings.INSTALLED_APPS else False def get_empty_value_display(cl): if hasattr(cl.model_admin, 'get_empty_value_display'): return cl.model_admin.get_empty_value_display() else: # Django < 1.9 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE return EMPTY_CHANGELIST_VALUE ### # Ripped from contrib.admin's (1.3.1) items_for_result tag. # The only difference is we're indenting nodes according to their level. def mptt_items_for_result(cl, result, form): """ Generates the actual list of data. """ first = True pk = cl.lookup_opts.pk.attname # #### MPTT ADDITION START # figure out which field to indent mptt_indent_field = getattr(cl.model_admin, 'mptt_indent_field', None) if not mptt_indent_field: for field_name in cl.list_display: try: f = cl.lookup_opts.get_field(field_name) except models.FieldDoesNotExist: if (mptt_indent_field is None and field_name != 'action_checkbox'): mptt_indent_field = field_name else: # first model field, use this one mptt_indent_field = field_name break # #### MPTT ADDITION END # figure out how much to indent mptt_level_indent = getattr(cl.model_admin, 'mptt_level_indent', MPTT_ADMIN_LEVEL_INDENT) for field_name in cl.list_display: row_class = '' try: f, attr, value = lookup_field(field_name, result, cl.model_admin) except (AttributeError, ObjectDoesNotExist): result_repr = get_empty_value_display(cl) else: if f is None: if field_name == 'action_checkbox': row_class = ' class="action-checkbox"' allow_tags = getattr(attr, 'allow_tags', False) boolean = getattr(attr, 'boolean', False) if boolean: allow_tags = True result_repr = _boolean_icon(value) else: result_repr = smart_text(value) # Strip HTML tags in the resulting text, except if the # function has an "allow_tags" attribute set to True. if not allow_tags: result_repr = conditional_escape(result_repr) else: result_repr = mark_safe(result_repr) else: if isinstance(f.rel, models.ManyToOneRel): field_val = getattr(result, f.name) if field_val is None: result_repr = get_empty_value_display(cl) else: result_repr = escape(field_val) else: try: result_repr = display_for_field(value, f) except TypeError: # Changed in Django 1.9, now takes 3 arguments result_repr = display_for_field( value, f, get_empty_value_display(cl)) if isinstance(f, models.DateField)\ or isinstance(f, models.TimeField)\ or isinstance(f, models.ForeignKey): row_class = ' class="nowrap"' if force_text(result_repr) == '': result_repr = mark_safe(' ') # #### MPTT ADDITION START if field_name == mptt_indent_field: level = getattr(result, result._mptt_meta.level_attr) padding_attr = ' style="padding-%s:%spx"' % ( 'right' if get_language_bidi() else 'left', 8 + mptt_level_indent * level) else: padding_attr = '' # #### MPTT ADDITION END # If list_display_links not defined, add the link tag to the first field if (first and not cl.list_display_links) or field_name in cl.list_display_links: table_tag = {True: 'th', False: 'td'}[first] first = False url = cl.url_for_result(result) # Convert the pk to something that can be used in Javascript. # Problem cases are long ints (23L) and non-ASCII strings. if cl.to_field: attr = str(cl.to_field) else: attr = pk value = result.serializable_value(attr) result_id = escapejs(value) # #### MPTT SUBSTITUTION START yield mark_safe('<%s%s%s>
    %s' % ( table_tag, row_class, padding_attr, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, '%s'); return false;"' % result_id or ''), # noqa conditional_escape(result_repr), table_tag)) # #### MPTT SUBSTITUTION END else: # By default the fields come from ModelAdmin.list_editable, but if we pull # the fields out of the form instead of list_editable custom admins # can provide fields on a per request basis if (form and field_name in form.fields and not ( field_name == cl.model._meta.pk.name and form[cl.model._meta.pk.name].is_hidden)): bf = form[field_name] result_repr = mark_safe(force_text(bf.errors) + force_text(bf)) else: result_repr = conditional_escape(result_repr) # #### MPTT SUBSTITUTION START yield mark_safe('%s' % (row_class, padding_attr, result_repr)) # #### MPTT SUBSTITUTION END if form and not form[cl.model._meta.pk.name].is_hidden: yield mark_safe('%s' % force_text(form[cl.model._meta.pk.name])) def mptt_results(cl): if cl.formset: for res, form in zip(cl.result_list, cl.formset.forms): yield list(mptt_items_for_result(cl, res, form)) else: for res in cl.result_list: yield list(mptt_items_for_result(cl, res, None)) def mptt_result_list(cl): """ Displays the headers and data list together """ return {'cl': cl, 'result_hidden_fields': list(result_hidden_fields(cl)), 'result_headers': list(result_headers(cl)), 'results': list(mptt_results(cl))} # custom template is merely so we can strip out sortable-ness from the column headers # Based on admin/change_list_results.html (1.3.1) if IS_GRAPPELLI_INSTALLED: mptt_result_list = register.inclusion_tag( "admin/grappelli_mptt_change_list_results.html")(mptt_result_list) else: mptt_result_list = register.inclusion_tag( "admin/mptt_change_list_results.html")(mptt_result_list) python-django-mptt-0.8.5/mptt/templatetags/mptt_tags.py000066400000000000000000000235731275105100200233240ustar00rootroot00000000000000""" Template tags for working with lists of model instances which represent trees. """ from __future__ import unicode_literals from django import template from django.apps import apps from django.db.models.fields import FieldDoesNotExist from django.utils.encoding import force_text from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from mptt.utils import (drilldown_tree_for_node, get_cached_trees, tree_item_iterator) register = template.Library() # ## ITERATIVE TAGS class FullTreeForModelNode(template.Node): def __init__(self, model, context_var): self.model = model self.context_var = context_var def render(self, context): cls = apps.get_model(*self.model.split('.')) if cls is None: raise template.TemplateSyntaxError( _('full_tree_for_model tag was given an invalid model: %s') % self.model ) context[self.context_var] = cls._tree_manager.all() return '' class DrilldownTreeForNodeNode(template.Node): def __init__(self, node, context_var, foreign_key=None, count_attr=None, cumulative=False): self.node = template.Variable(node) self.context_var = context_var self.foreign_key = foreign_key self.count_attr = count_attr self.cumulative = cumulative def render(self, context): # Let any VariableDoesNotExist raised bubble up args = [self.node.resolve(context)] if self.foreign_key is not None: app_label, model_name, fk_attr = self.foreign_key.split('.') cls = apps.get_model(app_label, model_name) if cls is None: raise template.TemplateSyntaxError( _('drilldown_tree_for_node tag was given an invalid model: %s') % '.'.join([app_label, model_name]) ) try: cls._meta.get_field(fk_attr) except FieldDoesNotExist: raise template.TemplateSyntaxError( _('drilldown_tree_for_node tag was given an invalid model field: %s') % fk_attr ) args.extend([cls, fk_attr, self.count_attr, self.cumulative]) context[self.context_var] = drilldown_tree_for_node(*args) return '' @register.tag def full_tree_for_model(parser, token): """ Populates a template variable with a ``QuerySet`` containing the full tree for a given model. Usage:: {% full_tree_for_model [model] as [varname] %} The model is specified in ``[appname].[modelname]`` format. Example:: {% full_tree_for_model tests.Genre as genres %} """ bits = token.contents.split() if len(bits) != 4: raise template.TemplateSyntaxError(_('%s tag requires three arguments') % bits[0]) if bits[2] != 'as': raise template.TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0]) return FullTreeForModelNode(bits[1], bits[3]) @register.tag('drilldown_tree_for_node') def do_drilldown_tree_for_node(parser, token): """ Populates a template variable with the drilldown tree for a given node, optionally counting the number of items associated with its children. A drilldown tree consists of a node's ancestors, itself and its immediate children. For example, a drilldown tree for a book category "Personal Finance" might look something like:: Books Business, Finance & Law Personal Finance Budgeting (220) Financial Planning (670) Usage:: {% drilldown_tree_for_node [node] as [varname] %} Extended usage:: {% drilldown_tree_for_node [node] as [varname] count [foreign_key] in [count_attr] %} {% drilldown_tree_for_node [node] as [varname] cumulative count [foreign_key] in [count_attr] %} The foreign key is specified in ``[appname].[modelname].[fieldname]`` format, where ``fieldname`` is the name of a field in the specified model which relates it to the given node's model. When this form is used, a ``count_attr`` attribute on each child of the given node in the drilldown tree will contain a count of the number of items associated with it through the given foreign key. If cumulative is also specified, this count will be for items related to the child node and all of its descendants. Examples:: {% drilldown_tree_for_node genre as drilldown %} {% drilldown_tree_for_node genre as drilldown count tests.Game.genre in game_count %} {% drilldown_tree_for_node genre as drilldown cumulative count tests.Game.genre in game_count %} """ # noqa bits = token.contents.split() len_bits = len(bits) if len_bits not in (4, 8, 9): raise template.TemplateSyntaxError( _('%s tag requires either three, seven or eight arguments') % bits[0]) if bits[2] != 'as': raise template.TemplateSyntaxError( _("second argument to %s tag must be 'as'") % bits[0]) if len_bits == 8: if bits[4] != 'count': raise template.TemplateSyntaxError( _("if seven arguments are given, fourth argument to %s tag must be 'with'") % bits[0]) if bits[6] != 'in': raise template.TemplateSyntaxError( _("if seven arguments are given, sixth argument to %s tag must be 'in'") % bits[0]) return DrilldownTreeForNodeNode(bits[1], bits[3], bits[5], bits[7]) elif len_bits == 9: if bits[4] != 'cumulative': raise template.TemplateSyntaxError( _("if eight arguments are given, fourth argument to %s tag must be 'cumulative'") % bits[0]) if bits[5] != 'count': raise template.TemplateSyntaxError( _("if eight arguments are given, fifth argument to %s tag must be 'count'") % bits[0]) if bits[7] != 'in': raise template.TemplateSyntaxError( _("if eight arguments are given, seventh argument to %s tag must be 'in'") % bits[0]) return DrilldownTreeForNodeNode(bits[1], bits[3], bits[6], bits[8], cumulative=True) else: return DrilldownTreeForNodeNode(bits[1], bits[3]) @register.filter def tree_info(items, features=None): """ Given a list of tree items, produces doubles of a tree item and a ``dict`` containing information about the tree structure around the item, with the following contents: new_level ``True`` if the current item is the start of a new level in the tree, ``False`` otherwise. closed_levels A list of levels which end after the current item. This will be an empty list if the next item is at the same level as the current item. Using this filter with unpacking in a ``{% for %}`` tag, you should have enough information about the tree structure to create a hierarchical representation of the tree. Example:: {% for genre,structure in genres|tree_info %} {% if structure.new_level %}
    • {% else %}
    • {% endif %} {{ genre.name }} {% for level in structure.closed_levels %}
    {% endfor %} {% endfor %} """ kwargs = {} if features: feature_names = features.split(',') if 'ancestors' in feature_names: kwargs['ancestors'] = True return tree_item_iterator(items, **kwargs) @register.filter def tree_path(items, separator=' :: '): """ Creates a tree path represented by a list of ``items`` by joining the items with a ``separator``. Each path item will be coerced to unicode, so a list of model instances may be given if required. Example:: {{ some_list|tree_path }} {{ some_node.get_ancestors|tree_path:" > " }} """ return separator.join(force_text(i) for i in items) # ## RECURSIVE TAGS @register.filter def cache_tree_children(queryset): """ Alias to `mptt.utils.get_cached_trees`. """ return get_cached_trees(queryset) class RecurseTreeNode(template.Node): def __init__(self, template_nodes, queryset_var): self.template_nodes = template_nodes self.queryset_var = queryset_var def _render_node(self, context, node): bits = [] context.push() for child in node.get_children(): bits.append(self._render_node(context, child)) context['node'] = node context['children'] = mark_safe(''.join(bits)) rendered = self.template_nodes.render(context) context.pop() return rendered def render(self, context): queryset = self.queryset_var.resolve(context) roots = cache_tree_children(queryset) bits = [self._render_node(context, node) for node in roots] return ''.join(bits) @register.tag def recursetree(parser, token): """ Iterates over the nodes in the tree, and renders the contained block for each node. This tag will recursively render children into the template variable {{ children }}. Only one database query is required (children are cached for the whole tree) Usage:
      {% recursetree nodes %}
    • {{ node.name }} {% if not node.is_leaf_node %}
        {{ children }}
      {% endif %}
    • {% endrecursetree %}
    """ bits = token.contents.split() if len(bits) != 2: raise template.TemplateSyntaxError(_('%s tag requires a queryset') % bits[0]) queryset_var = template.Variable(bits[1]) template_nodes = parser.parse(('endrecursetree',)) parser.delete_first_token() return RecurseTreeNode(template_nodes, queryset_var) python-django-mptt-0.8.5/mptt/utils.py000066400000000000000000000237571275105100200177740ustar00rootroot00000000000000""" Utilities for working with lists of model instances which represent trees. """ from __future__ import unicode_literals import copy import csv import itertools import sys from django.utils.six import next, text_type from django.utils.six.moves import zip from django.utils.translation import ugettext as _ __all__ = ('previous_current_next', 'tree_item_iterator', 'drilldown_tree_for_node', 'get_cached_trees',) def previous_current_next(items): """ From http://www.wordaligned.org/articles/zippy-triples-served-with-python Creates an iterator which returns (previous, current, next) triples, with ``None`` filling in when there is no previous or next available. """ extend = itertools.chain([None], items, [None]) prev, cur, nex = itertools.tee(extend, 3) # Advancing an iterator twice when we know there are two items (the # two Nones at the start and at the end) will never fail except if # `items` is some funny StopIteration-raising generator. There's no point # in swallowing this exception. next(cur) next(nex) next(nex) return zip(prev, cur, nex) def tree_item_iterator(items, ancestors=False): """ Given a list of tree items, iterates over the list, generating two-tuples of the current tree item and a ``dict`` containing information about the tree structure around the item, with the following keys: ``'new_level'`` ``True`` if the current item is the start of a new level in the tree, ``False`` otherwise. ``'closed_levels'`` A list of levels which end after the current item. This will be an empty list if the next item is at the same level as the current item. If ``ancestors`` is ``True``, the following key will also be available: ``'ancestors'`` A list of unicode representations of the ancestors of the current node, in descending order (root node first, immediate parent last). For example: given the sample tree below, the contents of the list which would be available under the ``'ancestors'`` key are given on the right:: Books -> [] Sci-fi -> [u'Books'] Dystopian Futures -> [u'Books', u'Sci-fi'] """ structure = {} opts = None first_item_level = 0 for previous, current, next_ in previous_current_next(items): if opts is None: opts = current._mptt_meta current_level = getattr(current, opts.level_attr) if previous: structure['new_level'] = (getattr(previous, opts.level_attr) < current_level) if ancestors: # If the previous node was the end of any number of # levels, remove the appropriate number of ancestors # from the list. if structure['closed_levels']: structure['ancestors'] = \ structure['ancestors'][:-len(structure['closed_levels'])] # If the current node is the start of a new level, add its # parent to the ancestors list. if structure['new_level']: structure['ancestors'].append(text_type(previous)) else: structure['new_level'] = True if ancestors: # Set up the ancestors list on the first item structure['ancestors'] = [] first_item_level = current_level if next_: structure['closed_levels'] = list(range( current_level, getattr(next_, opts.level_attr), -1)) else: # All remaining levels need to be closed structure['closed_levels'] = list(range( current_level, first_item_level - 1, -1)) # Return a deep copy of the structure dict so this function can # be used in situations where the iterator is consumed # immediately. yield current, copy.deepcopy(structure) def drilldown_tree_for_node(node, rel_cls=None, rel_field=None, count_attr=None, cumulative=False): """ Creates a drilldown tree for the given node. A drilldown tree consists of a node's ancestors, itself and its immediate children, all in tree order. Optional arguments may be given to specify a ``Model`` class which is related to the node's class, for the purpose of adding related item counts to the node's children: ``rel_cls`` A ``Model`` class which has a relation to the node's class. ``rel_field`` The name of the field in ``rel_cls`` which holds the relation to the node's class. ``count_attr`` The name of an attribute which should be added to each child in the drilldown tree, containing a count of how many instances of ``rel_cls`` are related through ``rel_field``. ``cumulative`` If ``True``, the count will be for each child and all of its descendants, otherwise it will be for each child itself. """ if rel_cls and rel_field and count_attr: children = node._tree_manager.add_related_count( node.get_children(), rel_cls, rel_field, count_attr, cumulative) else: children = node.get_children() return itertools.chain(node.get_ancestors(), [node], children) def print_debug_info(qs, file=None): """ Given an mptt queryset, prints some debug information to stdout. Use this when things go wrong. Please include the output from this method when filing bug issues. """ opts = qs.model._mptt_meta writer = csv.writer(sys.stdout if file is None else file) header = ( 'pk', opts.level_attr, '%s_id' % opts.parent_attr, opts.tree_id_attr, opts.left_attr, opts.right_attr, 'pretty', ) writer.writerow(header) for n in qs.order_by('tree_id', 'lft'): level = getattr(n, opts.level_attr) row = [] for field in header[:-1]: row.append(getattr(n, field)) row.append('%s%s' % ('- ' * level, text_type(n).encode('utf-8'))) writer.writerow(row) def _get_tree_model(model_class): # Find the model that contains the tree fields. # This is a weird way of going about it, but Django doesn't let us access # the fields list to detect where the tree fields actually are, # because the app cache hasn't been loaded yet. # So, it *should* be the *last* concrete MPTTModel subclass in the mro(). bases = list(model_class.mro()) while bases: b = bases.pop() # NOTE can't use `issubclass(b, MPTTModel)` here because we can't import MPTTModel yet! # So hasattr(b, '_mptt_meta') will have to do. if hasattr(b, '_mptt_meta') and not (b._meta.abstract or b._meta.proxy): return b return None def get_cached_trees(queryset): """ Takes a list/queryset of model objects in MPTT left (depth-first) order and caches the children and parent on each node. This allows up and down traversal through the tree without the need for further queries. Use cases include using a recursively included template or arbitrarily traversing trees. NOTE: nodes _must_ be passed in the correct (depth-first) order. If they aren't, a ValueError will be raised. Returns a list of top-level nodes. If a single tree was provided in its entirety, the list will of course consist of just the tree's root node. Aliases to this function are also available: ``mptt.templatetags.mptt_tag.cache_tree_children`` Use for recursive rendering in templates. ``mptt.querysets.TreeQuerySet.get_cached_trees`` Useful for chaining with queries; e.g., `Node.objects.filter(**kwargs).get_cached_trees()` """ current_path = [] top_nodes = [] if queryset: # Get the model's parent-attribute name parent_attr = queryset[0]._mptt_meta.parent_attr root_level = None for obj in queryset: # Get the current mptt node level node_level = obj.get_level() if root_level is None: # First iteration, so set the root level to the top node level root_level = node_level if node_level < root_level: # ``queryset`` was a list or other iterable (unable to order), # and was provided in an order other than depth-first raise ValueError( _('Node %s not in depth-first order') % (type(queryset),) ) # Set up the attribute on the node that will store cached children, # which is used by ``MPTTModel.get_children`` obj._cached_children = [] # Remove nodes not in the current branch while len(current_path) > node_level - root_level: current_path.pop(-1) if node_level == root_level: # Add the root to the list of top nodes, which will be returned top_nodes.append(obj) else: # Cache the parent on the current node, and attach the current # node to the parent's list of children _parent = current_path[-1] setattr(obj, parent_attr, _parent) _parent._cached_children.append(obj) if root_level == 0: # get_ancestors() can use .parent.parent.parent... setattr(obj, '_mptt_use_cached_ancestors', True) # Add the current node to end of the current path - the last node # in the current path is the parent for the next iteration, unless # the next iteration is higher up the tree (a new branch), in which # case the paths below it (e.g., this one) will be removed from the # current path during the next iteration current_path.append(obj) return top_nodes python-django-mptt-0.8.5/setup.cfg000066400000000000000000000002301275105100200170740ustar00rootroot00000000000000[flake8] exclude = venv,.tox,docs/conf.py max-line-length = 99 [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 python-django-mptt-0.8.5/setup.py000077500000000000000000000025341275105100200170010ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import find_packages, setup setup( name='django-mptt', description='''Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances.''', version=__import__('mptt').__version__, author='Craig de Stigter', author_email='craig.ds@gmail.com', url='http://github.com/django-mptt/django-mptt', license='MIT License', packages=find_packages(exclude=['tests', 'tests.*']), include_package_data=True, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", 'Topic :: Utilities', ], ) python-django-mptt-0.8.5/tests/000077500000000000000000000000001275105100200164225ustar00rootroot00000000000000python-django-mptt-0.8.5/tests/.coveragerc000066400000000000000000000006711275105100200205470ustar00rootroot00000000000000[run] include = */mptt/* [report] exclude_lines = pragma: no cover # Don't complain about missing debug-only code: def __unicode__ def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: omit = */tests/* show_missing = True python-django-mptt-0.8.5/tests/.gitignore000066400000000000000000000000221275105100200204040ustar00rootroot00000000000000.coverage htmlcov python-django-mptt-0.8.5/tests/__init__.py000066400000000000000000000000501275105100200205260ustar00rootroot00000000000000from __future__ import unicode_literals python-django-mptt-0.8.5/tests/myapp/000077500000000000000000000000001275105100200175505ustar00rootroot00000000000000python-django-mptt-0.8.5/tests/myapp/__init__.py000066400000000000000000000000501275105100200216540ustar00rootroot00000000000000from __future__ import unicode_literals python-django-mptt-0.8.5/tests/myapp/admin.py000066400000000000000000000004261275105100200212140ustar00rootroot00000000000000from django.contrib import admin from mptt.admin import MPTTModelAdmin, DraggableMPTTAdmin from myapp.models import Category, Person class CategoryAdmin(MPTTModelAdmin): pass admin.site.register(Category, CategoryAdmin) admin.site.register(Person, DraggableMPTTAdmin) python-django-mptt-0.8.5/tests/myapp/doctests.txt000066400000000000000000001310741275105100200221470ustar00rootroot00000000000000>>> from datetime import date >>> from django.db import connection >>> from mptt.exceptions import InvalidMove >>> from myapp.models import Genre, Insert, MultiOrder, Node, OrderedInsertion, Tree, Person, Student >>> def print_tree_details(nodes): ... opts = nodes[0]._mptt_meta ... print('\n'.join(['%s %s %s %s %s %s' % \ ... (n.pk, getattr(n, '%s_id' % opts.parent_attr) or '-', ... getattr(n, opts.tree_id_attr), getattr(n, opts.level_attr), ... getattr(n, opts.left_attr), getattr(n, opts.right_attr)) \ ... for n in nodes])) >>> def reset_sequence(model): ... # This fixes test failures. For a strange reason, this snippet is ... # only necessary with PY3. Also, do not fail if the table does not ... # exist at all for some reason. ... try: ... connection.cursor().execute( ... 'UPDATE sqlite_sequence SET seq=0 WHERE name=%s', ... [model._meta.db_table]) ... except: ... pass # Creation #################################################################### # creation with a given ID >>> action = Genre.objects.create(pk=1, name='Action') >>> platformer = Genre.objects.create(name='Platformer', parent=action) >>> platformer_2d = Genre.objects.create(name='2D Platformer', parent=platformer) >>> platformer = Genre.objects.get(pk=platformer.pk) >>> platformer_3d = Genre.objects.create(name='3D Platformer', parent=platformer) >>> platformer = Genre.objects.get(pk=platformer.pk) >>> platformer_4d = Genre.objects.create(name='4D Platformer', parent=platformer) >>> rpg = Genre.objects.create(name='Role-playing Game') >>> arpg = Genre.objects.create(name='Action RPG', parent=rpg) >>> rpg = Genre.objects.get(pk=rpg.pk) >>> trpg = Genre.objects.create(name='Tactical RPG', parent=rpg) >>> print_tree_details(Genre.objects.all()) 1 - 1 0 1 10 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 6 - 2 0 1 6 7 6 2 1 2 3 8 6 2 1 4 5 # Utilities ################################################################### >>> from mptt.utils import previous_current_next, tree_item_iterator, drilldown_tree_for_node >>> for p,c,n in previous_current_next(Genre.objects.all()): ... print((p,c,n)) (None, , ) (, , ) (, , ) (, , ) (, , ) (, , ) (, , ) (, , None) >>> for i,s in tree_item_iterator(Genre.objects.all()): ... print((i, s['new_level'], list(s['closed_levels']))) (, True, []) (, True, []) (, True, []) (, False, []) (, False, [2, 1]) (, False, []) (, True, []) (, False, [1, 0]) >>> for i,s in tree_item_iterator(rpg.get_descendants()): ... print((i, s['new_level'], list(s['closed_levels']))) (, True, []) (, False, [1]) >>> for i,s in tree_item_iterator(trpg.get_descendants()): ... print((i, s['new_level'], list(s['closed_levels']))) >>> for i,s in tree_item_iterator(Genre.objects.all(), ancestors=True): ... print((i, s['new_level'], s['ancestors'], list(s['closed_levels']))) (, True, [], []) (, True, [u'Action'], []) (, True, [u'Action', u'Platformer'], []) (, False, [u'Action', u'Platformer'], []) (, False, [u'Action', u'Platformer'], [2, 1]) (, False, [], []) (, True, [u'Role-playing Game'], []) (, False, [u'Role-playing Game'], [1, 0]) >>> action = Genre.objects.get(pk=action.pk) >>> [item.name for item in drilldown_tree_for_node(action)] [u'Action', u'Platformer'] >>> platformer = Genre.objects.get(pk=platformer.pk) >>> [item.name for item in drilldown_tree_for_node(platformer)] [u'Action', u'Platformer', u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> platformer_3d = Genre.objects.get(pk=platformer_3d.pk) >>> [item.name for item in drilldown_tree_for_node(platformer_3d)] [u'Action', u'Platformer', u'3D Platformer'] # Forms ####################################################################### >>> from mptt.forms import TreeNodeChoiceField, MoveNodeForm >>> f = TreeNodeChoiceField(queryset=Genre.objects.all()) >>> print(f.widget.render("test", None)) >>> f = TreeNodeChoiceField(queryset=Genre.objects.all(), required=False) >>> print(f.widget.render("test", None)) >>> f = TreeNodeChoiceField(queryset=Genre.objects.all(), empty_label=u'None of the below') >>> print(f.widget.render("test", None)) >>> f = TreeNodeChoiceField(queryset=Genre.objects.all(), required=False, empty_label=u'None of the below') >>> print(f.widget.render("test", None)) >>> f = TreeNodeChoiceField(queryset=Genre.objects.all(), level_indicator=u'+--') >>> print(f.widget.render("test", None)) >>> form = MoveNodeForm(Genre.objects.get(pk=7)) >>> print(form['target']) >>> form = MoveNodeForm(Genre.objects.get(pk=7), level_indicator=u'+--', target_select_size=5) >>> print(form['target']) >>> form = MoveNodeForm(Genre.objects.get(pk=7), position_choices=(('left', 'left'),)) >>> print(form['position']) # TreeManager Methods ######################################################### # check that tree manager is the explicitly defined tree manager for Person >>> Person._tree_manager == Person.objects True # managers of non-abstract bases don't get inherited, so: >>> Student._tree_manager == Student.objects False >>> Student._tree_manager == Person._tree_manager False >>> Student._tree_manager.model >>> Student._tree_manager.tree_model >>> Person._tree_manager.model >>> Person._tree_manager.tree_model >>> Genre.objects.root_node(action.tree_id) >>> Genre.objects.root_node(rpg.tree_id) >>> Genre.objects.root_node(3) Traceback (most recent call last): ... DoesNotExist: Genre matching query does not exist. >>> [g.name for g in Genre.objects.root_nodes()] [u'Action', u'Role-playing Game'] >>> [g.parent for g in Genre.objects.only('parent')] [None, , , , , None, , ] # Model Instance Methods ###################################################### >>> action = Genre.objects.get(pk=action.pk) >>> [g.name for g in action.get_ancestors()] [] >>> [g.name for g in action.get_ancestors(ascending=True)] [] >>> [g.name for g in action.get_children()] [u'Platformer'] >>> [g.name for g in action.get_descendants()] [u'Platformer', u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in action.get_descendants(include_self=True)] [u'Action', u'Platformer', u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in action.get_leafnodes()] [u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in action.get_leafnodes(include_self=False)] [u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in action.get_leafnodes(include_self=True)] [u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in platformer.get_family()] [u'Action', u'Platformer', u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> action.get_descendant_count() 4 >>> action.get_previous_sibling() >>> action.get_next_sibling() >>> action.get_root() >>> [g.name for g in action.get_siblings()] [u'Role-playing Game'] >>> [g.name for g in action.get_siblings(include_self=True)] [u'Action', u'Role-playing Game'] >>> action.is_root_node() True >>> action.is_child_node() False >>> action.is_leaf_node() False >>> action.is_descendant_of(action) False >>> action.is_descendant_of(action, include_self=True) True >>> action.is_ancestor_of(action) False >>> action.is_ancestor_of(action, include_self=True) True >>> platformer = Genre.objects.get(pk=platformer.pk) >>> [g.name for g in platformer.get_ancestors()] [u'Action'] >>> [g.name for g in platformer.get_ancestors(ascending=True)] [u'Action'] >>> [g.name for g in platformer.get_children()] [u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in platformer.get_descendants()] [u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in platformer.get_descendants(include_self=True)] [u'Platformer', u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in platformer.get_leafnodes()] [u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> [g.name for g in platformer.get_leafnodes(include_self=True)] [u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> platformer.get_descendant_count() 3 >>> platformer.get_previous_sibling() >>> platformer.get_next_sibling() >>> platformer.get_root() >>> [g.name for g in platformer.get_siblings()] [] >>> [g.name for g in platformer.get_siblings(include_self=True)] [u'Platformer'] >>> platformer.is_root_node() False >>> platformer.is_child_node() True >>> platformer.is_leaf_node() False >>> action.is_descendant_of(platformer) False >>> action.is_descendant_of(platformer, include_self=True) False >>> action.is_ancestor_of(platformer) True >>> action.is_ancestor_of(platformer, include_self=True) True >>> platformer_3d = Genre.objects.get(pk=platformer_3d.pk) >>> [g.name for g in platformer_3d.get_ancestors()] [u'Action', u'Platformer'] >>> [g.name for g in platformer_3d.get_ancestors(ascending=True)] [u'Platformer', u'Action'] >>> [g.name for g in platformer_3d.get_children()] [] >>> [g.name for g in platformer_3d.get_descendants()] [] >>> [g.name for g in platformer_3d.get_descendants(include_self=True)] [u'3D Platformer'] >>> [g.name for g in platformer_3d.get_leafnodes()] [] >>> [g.name for g in platformer_3d.get_leafnodes(include_self=True)] [u'3D Platformer'] >>> platformer_3d.get_descendant_count() 0 >>> platformer_3d.get_previous_sibling() >>> platformer_3d.get_next_sibling() >>> platformer_3d.get_root() >>> [g.name for g in platformer_3d.get_siblings()] [u'2D Platformer', u'4D Platformer'] >>> [g.name for g in platformer_3d.get_siblings(include_self=True)] [u'2D Platformer', u'3D Platformer', u'4D Platformer'] >>> platformer_3d.is_root_node() False >>> platformer_3d.is_child_node() True >>> platformer_3d.is_leaf_node() True >>> action.is_descendant_of(platformer_3d) False >>> action.is_descendant_of(platformer_3d, include_self=True) False >>> action.is_ancestor_of(platformer_3d) True >>> action.is_ancestor_of(platformer_3d, include_self=True) True >>> platformer_3d.is_descendant_of(platformer_3d) False >>> platformer_3d.is_descendant_of(platformer_3d, include_self=True) True >>> platformer_3d.is_ancestor_of(platformer_3d) False >>> platformer_3d.is_ancestor_of(platformer_3d, include_self=True) True # The move_to method will be used in other tests to verify that it calls the # TreeManager correctly. ####################### # Intra-Tree Movement # ####################### >>> root = Node.objects.create() >>> c_1 = Node.objects.create(parent=root) >>> c_1_1 = Node.objects.create(parent=c_1) >>> c_1 = Node.objects.get(pk=c_1.pk) >>> c_1_2 = Node.objects.create(parent=c_1) >>> root = Node.objects.get(pk=root.pk) >>> c_2 = Node.objects.create(parent=root) >>> c_2_1 = Node.objects.create(parent=c_2) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_2_2 = Node.objects.create(parent=c_2) >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Validate exceptions are raised appropriately >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(root, root, position='first-child') Traceback (most recent call last): ... InvalidMove: A node may not be made a child of itself. >>> c_1 = Node.objects.get(pk=c_1.pk) >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> Node.objects.move_node(c_1, c_1_1, position='last-child') Traceback (most recent call last): ... InvalidMove: A node may not be made a child of any of its descendants. >>> Node.objects.move_node(root, root, position='right') Traceback (most recent call last): ... InvalidMove: A node may not be made a sibling of itself. >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1, c_1_1, position='left') Traceback (most recent call last): ... InvalidMove: A node may not be made a sibling of any of its descendants. >>> Node.objects.move_node(c_1, c_2, position='cheese') Traceback (most recent call last): ... ValueError: An invalid position was given: cheese. # Move up the tree using first-child >>> c_2_2 = Node.objects.get(pk=c_2_2.pk) >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_2_2, c_1, 'first-child') >>> print_tree_details([c_2_2]) 7 2 1 2 3 4 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 9 7 2 1 2 3 4 3 2 1 2 5 6 4 2 1 2 7 8 5 1 1 1 10 13 6 5 1 2 11 12 # Undo the move using right >>> c_2_1 = Node.objects.get(pk=c_2_1.pk) >>> c_2_2.move_to(c_2_1, 'right') >>> print_tree_details([c_2_2]) 7 5 1 2 11 12 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Move up the tree with descendants using first-child >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_2, c_1, 'first-child') >>> print_tree_details([c_2]) 5 2 1 2 3 8 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 13 5 2 1 2 3 8 6 5 1 3 4 5 7 5 1 3 6 7 3 2 1 2 9 10 4 2 1 2 11 12 # Undo the move using right >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_2, c_1, 'right') >>> print_tree_details([c_2]) 5 1 1 1 8 13 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 COVERAGE | U1 | U> | D1 | D> ------------+----+----+----+---- first-child | Y | Y | | last-child | | | | left | | | | right | | | Y | Y # Move down the tree using first-child >>> c_1_2 = Node.objects.get(pk=c_1_2.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1_2, c_2, 'first-child') >>> print_tree_details([c_1_2]) 4 5 1 2 7 8 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 5 3 2 1 2 3 4 5 1 1 1 6 13 4 5 1 2 7 8 6 5 1 2 9 10 7 5 1 2 11 12 # Undo the move using last-child >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_1_2, c_1, 'last-child') >>> print_tree_details([c_1_2]) 4 2 1 2 5 6 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Move down the tree with descendants using first-child >>> c_1 = Node.objects.get(pk=c_1.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1, c_2, 'first-child') >>> print_tree_details([c_1]) 2 5 1 2 3 8 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 5 1 1 1 2 13 2 5 1 2 3 8 3 2 1 3 4 5 4 2 1 3 6 7 6 5 1 2 9 10 7 5 1 2 11 12 # Undo the move using left >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1, c_2, 'left') >>> print_tree_details([c_1]) 2 1 1 1 2 7 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 COVERAGE | U1 | U> | D1 | D> ------------+----+----+----+---- first-child | Y | Y | Y | Y last-child | Y | | | left | | Y | | right | | | Y | Y # Move up the tree using right >>> c_2_2 = Node.objects.get(pk=c_2_2.pk) >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> Node.objects.move_node(c_2_2, c_1_1, 'right') >>> print_tree_details([c_2_2]) 7 2 1 2 5 6 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 9 3 2 1 2 3 4 7 2 1 2 5 6 4 2 1 2 7 8 5 1 1 1 10 13 6 5 1 2 11 12 # Undo the move using last-child >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_2_2, c_2, 'last-child') >>> print_tree_details([c_2_2]) 7 5 1 2 11 12 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Move up the tree with descendants using right >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> Node.objects.move_node(c_2, c_1_1, 'right') >>> print_tree_details([c_2]) 5 2 1 2 5 10 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 13 3 2 1 2 3 4 5 2 1 2 5 10 6 5 1 3 6 7 7 5 1 3 8 9 4 2 1 2 11 12 # Undo the move using last-child >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(c_2, root, 'last-child') >>> print_tree_details([c_2]) 5 1 1 1 8 13 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 COVERAGE | U1 | U> | D1 | D> ------------+----+----+----+---- first-child | Y | Y | Y | Y last-child | Y | | Y | Y left | | Y | | right | Y | Y | Y | Y # Move down the tree with descendants using left >>> c_1 = Node.objects.get(pk=c_1.pk) >>> c_2_2 = Node.objects.get(pk=c_2_2.pk) >>> Node.objects.move_node(c_1, c_2_2, 'left') >>> print_tree_details([c_1]) 2 5 1 2 5 10 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 5 1 1 1 2 13 6 5 1 2 3 4 2 5 1 2 5 10 3 2 1 3 6 7 4 2 1 3 8 9 7 5 1 2 11 12 # Undo the move using first-child >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(c_1, root, 'first-child') >>> print_tree_details([c_1]) 2 1 1 1 2 7 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Move down the tree using left >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> c_2_2 = Node.objects.get(pk=c_2_2.pk) >>> Node.objects.move_node(c_1_1, c_2_2, 'left') >>> print_tree_details([c_1_1]) 3 5 1 2 9 10 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 5 4 2 1 2 3 4 5 1 1 1 6 13 6 5 1 2 7 8 3 5 1 2 9 10 7 5 1 2 11 12 # Undo the move using left >>> c_1_2 = Node.objects.get(pk=c_1_2.pk) >>> Node.objects.move_node(c_1_1, c_1_2, 'left') >>> print_tree_details([c_1_1]) 3 2 1 2 3 4 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 COVERAGE | U1 | U> | D1 | D> ------------+----+----+----+---- first-child | Y | Y | Y | Y last-child | Y | Y | Y | Y left | Y | Y | Y | Y right | Y | Y | Y | Y I guess we're covered :) ####################### # Inter-Tree Movement # ####################### >>> new_root = Node.objects.create() >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 8 - 2 0 1 2 # Moving child nodes between trees ############################################ # Move using default (last-child) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_2.move_to(new_root) >>> print_tree_details([c_2]) 5 8 2 1 2 7 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 8 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 8 - 2 0 1 8 5 8 2 1 2 7 6 5 2 2 3 4 7 5 2 2 5 6 # Move using left >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1_1, c_2, position='left') >>> print_tree_details([c_1_1]) 3 8 2 1 2 3 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 4 2 1 2 3 4 8 - 2 0 1 10 3 8 2 1 2 3 5 8 2 1 4 9 6 5 2 2 5 6 7 5 2 2 7 8 # Move using first-child >>> c_1_2 = Node.objects.get(pk=c_1_2.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1_2, c_2, position='first-child') >>> print_tree_details([c_1_2]) 4 5 2 2 5 6 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 8 - 2 0 1 12 3 8 2 1 2 3 5 8 2 1 4 11 4 5 2 2 5 6 6 5 2 2 7 8 7 5 2 2 9 10 # Move using right >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_2, c_1, position='right') >>> print_tree_details([c_2]) 5 1 1 1 4 11 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 12 2 1 1 1 2 3 5 1 1 1 4 11 4 5 1 2 5 6 6 5 1 2 7 8 7 5 1 2 9 10 8 - 2 0 1 4 3 8 2 1 2 3 # Move using last-child >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> Node.objects.move_node(c_1_1, c_2, position='last-child') >>> print_tree_details([c_1_1]) 3 5 1 2 11 12 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 3 5 1 1 1 4 13 4 5 1 2 5 6 6 5 1 2 7 8 7 5 1 2 9 10 3 5 1 2 11 12 8 - 2 0 1 2 # Moving a root node into another tree as a child node ######################## # Validate exceptions are raised appropriately >>> Node.objects.move_node(root, c_1, position='first-child') Traceback (most recent call last): ... InvalidMove: A node may not be made a child of any of its descendants. >>> Node.objects.move_node(new_root, c_1, position='cheese') Traceback (most recent call last): ... ValueError: An invalid position was given: cheese. >>> new_root = Node.objects.get(pk=new_root.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> new_root.move_to(c_2, position='first-child') >>> print_tree_details([new_root]) 8 5 1 2 5 6 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 16 2 1 1 1 2 3 5 1 1 1 4 15 8 5 1 2 5 6 4 5 1 2 7 8 6 5 1 2 9 10 7 5 1 2 11 12 3 5 1 2 13 14 >>> new_root = Node.objects.create() >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(new_root, root, position='last-child') >>> print_tree_details([new_root]) 9 1 1 1 16 17 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 18 2 1 1 1 2 3 5 1 1 1 4 15 8 5 1 2 5 6 4 5 1 2 7 8 6 5 1 2 9 10 7 5 1 2 11 12 3 5 1 2 13 14 9 1 1 1 16 17 >>> new_root = Node.objects.create() >>> c_2_1 = Node.objects.get(pk=c_2_1.pk) >>> Node.objects.move_node(new_root, c_2_1, position='left') >>> print_tree_details([new_root]) 10 5 1 2 9 10 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 20 2 1 1 1 2 3 5 1 1 1 4 17 8 5 1 2 5 6 4 5 1 2 7 8 10 5 1 2 9 10 6 5 1 2 11 12 7 5 1 2 13 14 3 5 1 2 15 16 9 1 1 1 18 19 >>> new_root = Node.objects.create() >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(new_root, c_1, position='right') >>> print_tree_details([new_root]) 11 1 1 1 4 5 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 22 2 1 1 1 2 3 11 1 1 1 4 5 5 1 1 1 6 19 8 5 1 2 7 8 4 5 1 2 9 10 10 5 1 2 11 12 6 5 1 2 13 14 7 5 1 2 15 16 3 5 1 2 17 18 9 1 1 1 20 21 # Making nodes siblings of root nodes ######################################### # Validate exceptions are raised appropriately >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(root, root, position='left') Traceback (most recent call last): ... InvalidMove: A node may not be made a sibling of itself. >>> Node.objects.move_node(root, root, position='right') Traceback (most recent call last): ... InvalidMove: A node may not be made a sibling of itself. >>> r1 = Tree.objects.create() >>> c1_1 = Tree.objects.create(parent=r1) >>> c1_1_1 = Tree.objects.create(parent=c1_1) >>> r2 = Tree.objects.create() >>> c2_1 = Tree.objects.create(parent=r2) >>> c2_1_1 = Tree.objects.create(parent=c2_1) >>> r3 = Tree.objects.create() >>> c3_1 = Tree.objects.create(parent=r3) >>> c3_1_1 = Tree.objects.create(parent=c3_1) >>> print_tree_details(Tree.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 7 - 3 0 1 6 8 7 3 1 2 5 9 8 3 2 3 4 # Target < root node, left sibling >>> r1 = Tree.objects.get(pk=r1.pk) >>> r2 = Tree.objects.get(pk=r2.pk) >>> r2.move_to(r1, 'left') >>> print_tree_details([r2]) 4 - 1 0 1 6 >>> print_tree_details(Tree.objects.all()) 4 - 1 0 1 6 5 4 1 1 2 5 6 5 1 2 3 4 1 - 2 0 1 6 2 1 2 1 2 5 3 2 2 2 3 4 7 - 3 0 1 6 8 7 3 1 2 5 9 8 3 2 3 4 # Target > root node, left sibling >>> r3 = Tree.objects.get(pk=r3.pk) >>> r2.move_to(r3, 'left') >>> print_tree_details([r2]) 4 - 2 0 1 6 >>> print_tree_details(Tree.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 7 - 3 0 1 6 8 7 3 1 2 5 9 8 3 2 3 4 # Target < root node, right sibling >>> r1 = Tree.objects.get(pk=r1.pk) >>> r3 = Tree.objects.get(pk=r3.pk) >>> r3.move_to(r1, 'right') >>> print_tree_details([r3]) 7 - 2 0 1 6 >>> print_tree_details(Tree.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 7 - 2 0 1 6 8 7 2 1 2 5 9 8 2 2 3 4 4 - 3 0 1 6 5 4 3 1 2 5 6 5 3 2 3 4 # Target > root node, right sibling >>> r1 = Tree.objects.get(pk=r1.pk) >>> r2 = Tree.objects.get(pk=r2.pk) >>> r1.move_to(r2, 'right') >>> print_tree_details([r1]) 1 - 3 0 1 6 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 6 8 7 1 1 2 5 9 8 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 1 - 3 0 1 6 2 1 3 1 2 5 3 2 3 2 3 4 # No-op, root left sibling >>> r2 = Tree.objects.get(pk=r2.pk) >>> r2.move_to(r1, 'left') >>> print_tree_details([r2]) 4 - 2 0 1 6 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 6 8 7 1 1 2 5 9 8 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 1 - 3 0 1 6 2 1 3 1 2 5 3 2 3 2 3 4 # No-op, root right sibling >>> r1.move_to(r2, 'right') >>> print_tree_details([r1]) 1 - 3 0 1 6 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 6 8 7 1 1 2 5 9 8 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 1 - 3 0 1 6 2 1 3 1 2 5 3 2 3 2 3 4 # Child node, left sibling >>> c3_1 = Tree.objects.get(pk=c3_1.pk) >>> c3_1.move_to(r1, 'left') >>> print_tree_details([c3_1]) 8 - 3 0 1 4 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 2 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 8 - 3 0 1 4 9 8 3 1 2 3 1 - 4 0 1 6 2 1 4 1 2 5 3 2 4 2 3 4 # Child node, right sibling >>> r3 = Tree.objects.get(pk=r3.pk) >>> c1_1 = Tree.objects.get(pk=c1_1.pk) >>> c1_1.move_to(r3, 'right') >>> print_tree_details([c1_1]) 2 - 2 0 1 4 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 2 2 - 2 0 1 4 3 2 2 1 2 3 4 - 3 0 1 6 5 4 3 1 2 5 6 5 3 2 3 4 8 - 4 0 1 4 9 8 4 1 2 3 1 - 5 0 1 2 # Insertion of positioned nodes ############################################### >>> r1 = Insert.objects.create() >>> r2 = Insert.objects.create() >>> r3 = Insert.objects.create() >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 2 3 - 3 0 1 2 >>> r2 = Insert.objects.get(pk=r2.pk) >>> c1 = Insert() >>> c1 = Insert.objects.insert_node(c1, r2, save=True) >>> print_tree_details([c1]) 4 2 2 1 2 3 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 4 4 2 2 1 2 3 3 - 3 0 1 2 >>> c1.insert_at(r2) Traceback (most recent call last): ... ValueError: Cannot insert a node which has already been saved. # First child >>> r2 = Insert.objects.get(pk=r2.pk) >>> c2 = Insert() >>> c2 = Insert.objects.insert_node(c2, r2, position='first-child', save=True) >>> print_tree_details([c2]) 5 2 2 1 2 3 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 6 5 2 2 1 2 3 4 2 2 1 4 5 3 - 3 0 1 2 # Left >>> c1 = Insert.objects.get(pk=c1.pk) >>> c3 = Insert() >>> c3 = Insert.objects.insert_node(c3, c1, position='left', save=True) >>> print_tree_details([c3]) 6 2 2 1 4 5 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 8 5 2 2 1 2 3 6 2 2 1 4 5 4 2 2 1 6 7 3 - 3 0 1 2 # Right >>> c4 = Insert() >>> c4 = Insert.objects.insert_node(c4, c3, position='right', save=True) >>> print_tree_details([c4]) 7 2 2 1 6 7 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 10 5 2 2 1 2 3 6 2 2 1 4 5 7 2 2 1 6 7 4 2 2 1 8 9 3 - 3 0 1 2 # Last child >>> r2 = Insert.objects.get(pk=r2.pk) >>> c5 = Insert() >>> c5 = Insert.objects.insert_node(c5, r2, position='last-child', save=True) >>> print_tree_details([c5]) 8 2 2 1 10 11 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 12 5 2 2 1 2 3 6 2 2 1 4 5 7 2 2 1 6 7 4 2 2 1 8 9 8 2 2 1 10 11 3 - 3 0 1 2 # Left sibling of root >>> r2 = Insert.objects.get(pk=r2.pk) >>> r4 = Insert() >>> r4 = Insert.objects.insert_node(r4, r2, position='left', save=True) >>> print_tree_details([r4]) 9 - 2 0 1 2 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 9 - 2 0 1 2 2 - 3 0 1 12 5 2 3 1 2 3 6 2 3 1 4 5 7 2 3 1 6 7 4 2 3 1 8 9 8 2 3 1 10 11 3 - 4 0 1 2 # Right sibling of root >>> r2 = Insert.objects.get(pk=r2.pk) >>> r5 = Insert() >>> r5 = Insert.objects.insert_node(r5, r2, position='right', save=True) >>> print_tree_details([r5]) 10 - 4 0 1 2 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 9 - 2 0 1 2 2 - 3 0 1 12 5 2 3 1 2 3 6 2 3 1 4 5 7 2 3 1 6 7 4 2 3 1 8 9 8 2 3 1 10 11 10 - 4 0 1 2 3 - 5 0 1 2 # Last root >>> r6 = Insert() >>> r6 = Insert.objects.insert_node(r6, None, save=True) >>> print_tree_details([r6]) 11 - 6 0 1 2 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 9 - 2 0 1 2 2 - 3 0 1 12 5 2 3 1 2 3 6 2 3 1 4 5 7 2 3 1 6 7 4 2 3 1 8 9 8 2 3 1 10 11 10 - 4 0 1 2 3 - 5 0 1 2 11 - 6 0 1 2 # order_insertion_by with single criterion #################################### >>> r1 = OrderedInsertion.objects.create(name='games') # Root ordering >>> r2 = OrderedInsertion.objects.create(name='food') >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 # Same name - insert after >>> r3 = OrderedInsertion.objects.create(name='food') >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 2 1 - 3 0 1 2 >>> c1 = OrderedInsertion.objects.create(name='zoo', parent=r3) >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 4 4 3 2 1 2 3 1 - 3 0 1 2 >>> r3 = OrderedInsertion.objects.get(pk=r3.pk) >>> c2 = OrderedInsertion.objects.create(name='monkey', parent=r3) >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 6 5 3 2 1 2 3 4 3 2 1 4 5 1 - 3 0 1 2 >>> r3 = OrderedInsertion.objects.get(pk=r3.pk) >>> c3 = OrderedInsertion.objects.create(name='animal', parent=r3) >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 8 6 3 2 1 2 3 5 3 2 1 4 5 4 3 2 1 6 7 1 - 3 0 1 2 # order_insertion_by reparenting with single criterion ######################## # Root -> child >>> r1 = OrderedInsertion.objects.get(pk=r1.pk) >>> r3 = OrderedInsertion.objects.get(pk=r3.pk) >>> r1.parent = r3 >>> r1.save() >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 10 6 3 2 1 2 3 1 3 2 1 4 5 5 3 2 1 6 7 4 3 2 1 8 9 # Child -> root >>> c3 = OrderedInsertion.objects.get(pk=c3.pk) >>> c3.parent = None >>> c3.save() >>> print_tree_details(OrderedInsertion.objects.all()) 6 - 1 0 1 2 2 - 2 0 1 2 3 - 3 0 1 8 1 3 3 1 2 3 5 3 3 1 4 5 4 3 3 1 6 7 # Child -> child >>> c1 = OrderedInsertion.objects.get(pk=c1.pk) >>> c1.parent = c3 >>> c1.save() >>> print_tree_details(OrderedInsertion.objects.all()) 6 - 1 0 1 4 4 6 1 1 2 3 2 - 2 0 1 2 3 - 3 0 1 6 1 3 3 1 2 3 5 3 3 1 4 5 >>> c3 = OrderedInsertion.objects.get(pk=c3.pk) >>> c2 = OrderedInsertion.objects.get(pk=c2.pk) >>> c2.parent = c3 >>> c2.save() >>> print_tree_details(OrderedInsertion.objects.all()) 6 - 1 0 1 6 5 6 1 1 2 3 4 6 1 1 4 5 2 - 2 0 1 2 3 - 3 0 1 4 1 3 3 1 2 3 # reordering after save, original insertion in order ########################## # sibling root nodes, inserted B,C, moved C-->A >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> c = OrderedInsertion.objects.create(name='c') >>> c.name = 'a' >>> c.save() >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 # sibling non-root nodes, inserted B,C, moved C-->A >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> c = OrderedInsertion.objects.create(name='c', parent=root) >>> c.name = 'a' >>> c.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 # sibling root nodes, inserted A,B, moved A-->C >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> a = OrderedInsertion.objects.create(name='a') >>> b = OrderedInsertion.objects.create(name='b') >>> a = OrderedInsertion.objects.get(name='a') >>> a.name = 'c' >>> a.save() >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 # sibling non-root nodes, inserted A,B, moved A-->C >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> a = OrderedInsertion.objects.create(name='a', parent=root) >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> a = OrderedInsertion.objects.get(name='a') >>> a.name = 'c' >>> a.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 # reordering after save, original insertion in reverse ######################## # sibling root nodes, inserted C,B, moved C-->A >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> c = OrderedInsertion.objects.create(name='c') >>> b = OrderedInsertion.objects.create(name='b') >>> c = OrderedInsertion.objects.get(name='c') >>> c.name = 'a' >>> c.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 2 # sibling non-root nodes, inserted C,B, moved C-->A >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> c = OrderedInsertion.objects.create(name='c', parent=root) >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> c = OrderedInsertion.objects.get(name='c') >>> c.name = 'a' >>> c.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 # sibling root nodes, inserted B,A, moved A-->C >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> a = OrderedInsertion.objects.create(name='a') >>> a.name = 'c' >>> a.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 2 # sibling non-root nodes, inserted B,A, moved A-->C >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> a = OrderedInsertion.objects.create(name='a', parent=root) >>> a.name = 'c' >>> a.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 # reordering after save, when only a single instance at level exists ########## # root node with no siblings no children, inserted B, moved B-->A (forward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'a' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 2 # root node with no siblings with children, inserted B,child, moved B-->A (forward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> child = OrderedInsertion.objects.create(name='child', parent=b) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'a' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 # parented node with no siblings no children, inserted root,B, moved B-->A (forward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'a' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 # parented node with no siblings with children, inserted root,B,C, moved B-->A (forward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> c = OrderedInsertion.objects.create(name='c', parent=b) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'a' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 # root node with no siblings no children, inserted B, moved B-->C (backward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'c' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 2 # root node with no siblings with children, inserted B,child, moved B-->C (backward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> child = OrderedInsertion.objects.create(name='child', parent=b) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'c' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 # parented node with no siblings no children, inserted root,B, moved B-->C (backward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'c' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 # parented node with no siblings with children, inserted root,B,D, moved B-->C (backward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> d = OrderedInsertion.objects.create(name='d', parent=b) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'c' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> r1 = OrderedInsertion.objects.create(name='r1', parent=root) >>> r2 = OrderedInsertion.objects.create(name='r2', parent=root) >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 # Insertion of positioned nodes, multiple ordering criteria ################### >>> r1 = MultiOrder.objects.create(name='fff', size=20, date=date(2008, 1, 1)) # Root nodes - ordering by subsequent fields >>> r2 = MultiOrder.objects.create(name='fff', size=10, date=date(2009, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 >>> r3 = MultiOrder.objects.create(name='fff', size=20, date=date(2007, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 3 - 3 0 1 2 >>> r4 = MultiOrder.objects.create(name='fff', size=20, date=date(2008, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 4 - 3 0 1 2 3 - 4 0 1 2 >>> r5 = MultiOrder.objects.create(name='fff', size=20, date=date(2007, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 4 - 3 0 1 2 3 - 4 0 1 2 5 - 5 0 1 2 >>> r6 = MultiOrder.objects.create(name='aaa', size=999, date=date(2010, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 6 - 1 0 1 2 2 - 2 0 1 2 1 - 3 0 1 2 4 - 4 0 1 2 3 - 5 0 1 2 5 - 6 0 1 2 # Child nodes >>> r1 = MultiOrder.objects.get(pk=r1.pk) >>> c1 = MultiOrder.objects.create(parent=r1, name='hhh', size=10, date=date(2009, 1, 1)) >>> print_tree_details(MultiOrder.objects.filter(tree_id=r1.tree_id)) 1 - 3 0 1 4 7 1 3 1 2 3 >>> r1 = MultiOrder.objects.get(pk=r1.pk) >>> c2 = MultiOrder.objects.create(parent=r1, name='hhh', size=20, date=date(2008, 1, 1)) >>> print_tree_details(MultiOrder.objects.filter(tree_id=r1.tree_id)) 1 - 3 0 1 6 7 1 3 1 2 3 8 1 3 1 4 5 >>> r1 = MultiOrder.objects.get(pk=r1.pk) >>> c3 = MultiOrder.objects.create(parent=r1, name='hhh', size=15, date=date(2008, 1, 1)) >>> print_tree_details(MultiOrder.objects.filter(tree_id=r1.tree_id)) 1 - 3 0 1 8 7 1 3 1 2 3 9 1 3 1 4 5 8 1 3 1 6 7 >>> r1 = MultiOrder.objects.get(pk=r1.pk) >>> c4 = MultiOrder.objects.create(parent=r1, name='hhh', size=15, date=date(2008, 1, 1)) >>> print_tree_details(MultiOrder.objects.filter(tree_id=r1.tree_id)) 1 - 3 0 1 10 7 1 3 1 2 3 9 1 3 1 4 5 10 1 3 1 6 7 8 1 3 1 8 9 # Multi-table Inheritance ##################################################### (pk, parent_id or '-', tree_id, level, left, right_attr) # Create test model instances >>> jack = Person.objects.create(name='Jack') >>> jill = Student.objects.create(name='Jill', parent=jack) >>> jim = Person.objects.create(name='Jim', parent=jack) >>> jess = Student.objects.create(name='Jess') >>> jeff = Person.objects.create(name='Jeff', parent=jess) >>> jane = Student.objects.create(name='Jane', parent=jeff) >>> joe = Person.objects.create(name='Joe', parent=jane) >>> julie = Student.objects.create(name='Julie', parent=jess) >>> print_tree_details(Person.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 10 5 4 2 1 2 7 6 5 2 2 3 6 7 6 2 3 4 5 8 4 2 1 8 9 >>> print_tree_details(Student.objects.all()) 2 1 1 1 2 3 4 - 2 0 1 10 6 5 2 2 3 6 8 4 2 1 8 9 >>> jack = Person.objects.get(pk=1) >>> list(jack.get_descendants()) [, ] >>> jack.get_root() >>> jill = Person.objects.get(pk=3) >>> jill.get_root() >>> jess = Student.objects.get(pk=4) >>> list(jess.get_descendants()) [, , , ] >>> jess.get_root() >>> jeff = Person.objects.get(pk=5) >>> list(jeff.get_descendants()) [, ] >>> jeff.get_root() >>> jane = Student.objects.get(pk=6) >>> jane.get_root() >>> list(jane.get_ancestors()) [, ] python-django-mptt-0.8.5/tests/myapp/fixtures/000077500000000000000000000000001275105100200214215ustar00rootroot00000000000000python-django-mptt-0.8.5/tests/myapp/fixtures/categories.json000066400000000000000000000052521275105100200244450ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.category", "fields": { "rght": 20, "name": "PC & Video Games", "category_uuid": "6263ac21-f08b-4b44-9462-0489c56e0d3d", "parent": null, "level": 0, "lft": 1, "tree_id": 1 } }, { "pk": 2, "model": "myapp.category", "fields": { "rght": 7, "name": "Nintendo Wii", "category_uuid": "c6125cec-ef08-4095-9a07-fd5c6db50cc1", "parent": 1, "level": 1, "lft": 2, "tree_id": 1 } }, { "pk": 3, "model": "myapp.category", "fields": { "rght": 4, "name": "Games", "category_uuid": "85da870c-837a-48a5-a3e8-ab651cfdd799", "parent": 2, "level": 2, "lft": 3, "tree_id": 1 } }, { "pk": 4, "model": "myapp.category", "fields": { "rght": 6, "name": "Hardware & Accessories", "category_uuid": "840d1ccb-9ecc-48b0-bf7c-5b0f08334d3d", "parent": 2, "level": 2, "lft": 5, "tree_id": 1 } }, { "pk": 5, "model": "myapp.category", "fields": { "rght": 13, "name": "Xbox 360", "category_uuid": "b6299e26-d5e9-4dc2-9e2a-3f8033c8dfe5", "parent": 1, "level": 1, "lft": 8, "tree_id": 1 } }, { "pk": 6, "model": "myapp.category", "fields": { "rght": 10, "name": "Games", "category_uuid": "8171b0ec-f6e6-46dc-aabc-46a6399d9290", "parent": 5, "level": 2, "lft": 9, "tree_id": 1 } }, { "pk": 7, "model": "myapp.category", "fields": { "rght": 12, "name": "Hardware & Accessories", "category_uuid": "a64720cb-af69-440a-9abc-5a8f095974e4", "parent": 5, "level": 2, "lft": 11, "tree_id": 1 } }, { "pk": 8, "model": "myapp.category", "fields": { "rght": 19, "name": "PlayStation 3", "category_uuid": "01cac717-454e-48bd-8180-8a0089613bbd", "parent": 1, "level": 1, "lft": 14, "tree_id": 1 } }, { "pk": 9, "model": "myapp.category", "fields": { "rght": 16, "name": "Games", "category_uuid": "74f9c250-6e75-4152-bbc9-f7a89969c82b", "parent": 8, "level": 2, "lft": 15, "tree_id": 1 } }, { "pk": 10, "model": "myapp.category", "fields": { "rght": 18, "name": "Hardware & Accessories", "category_uuid": "a4918038-4d03-48ca-8cc3-1c28e177bed5", "parent": 8, "level": 2, "lft": 17, "tree_id": 1 } } ] python-django-mptt-0.8.5/tests/myapp/fixtures/genres.json000066400000000000000000000045011275105100200235770ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.genre", "fields": { "rght": 16, "name": "Action", "parent": null, "level": 0, "lft": 1, "tree_id": 1 } }, { "pk": 2, "model": "myapp.genre", "fields": { "rght": 9, "name": "Platformer", "parent": 1, "level": 1, "lft": 2, "tree_id": 1 } }, { "pk": 3, "model": "myapp.genre", "fields": { "rght": 4, "name": "2D Platformer", "parent": 2, "level": 2, "lft": 3, "tree_id": 1 } }, { "pk": 4, "model": "myapp.genre", "fields": { "rght": 6, "name": "3D Platformer", "parent": 2, "level": 2, "lft": 5, "tree_id": 1 } }, { "pk": 5, "model": "myapp.genre", "fields": { "rght": 8, "name": "4D Platformer", "parent": 2, "level": 2, "lft": 7, "tree_id": 1 } }, { "pk": 6, "model": "myapp.genre", "fields": { "rght": 15, "name": "Shootemup", "parent": 1, "level": 1, "lft": 10, "tree_id": 1 } }, { "pk": 7, "model": "myapp.genre", "fields": { "rght": 12, "name": "Vertical Scrolling Shootemup", "parent": 6, "level": 2, "lft": 11, "tree_id": 1 } }, { "pk": 8, "model": "myapp.genre", "fields": { "rght": 14, "name": "Horizontal Scrolling Shootemup", "parent": 6, "level": 2, "lft": 13, "tree_id": 1 } }, { "pk": 9, "model": "myapp.genre", "fields": { "rght": 6, "name": "Role-playing Game", "parent": null, "level": 0, "lft": 1, "tree_id": 2 } }, { "pk": 10, "model": "myapp.genre", "fields": { "rght": 3, "name": "Action RPG", "parent": 9, "level": 1, "lft": 2, "tree_id": 2 } }, { "pk": 11, "model": "myapp.genre", "fields": { "rght": 5, "name": "Tactical RPG", "parent": 9, "level": 1, "lft": 4, "tree_id": 2 } } ] python-django-mptt-0.8.5/tests/myapp/fixtures/items.json000066400000000000000000000006731275105100200234430ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.item", "fields": { "name": "FIFA 15", "category_fk": "b6299e26-d5e9-4dc2-9e2a-3f8033c8dfe5", "category_pk": 5 } }, { "pk": 2, "model": "myapp.item", "fields": { "name": "Halo: Reach", "category_fk": "b6299e26-d5e9-4dc2-9e2a-3f8033c8dfe5", "category_pk": 5 } } ] python-django-mptt-0.8.5/tests/myapp/fixtures/persons.json000066400000000000000000000003111275105100200240000ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.person", "fields": { "rght": 16, "name": "John Doe", "parent": null, "level": 0, "lft": 1, "tree_id": 1 } } ]python-django-mptt-0.8.5/tests/myapp/models.py000066400000000000000000000175751275105100200214240ustar00rootroot00000000000000from __future__ import unicode_literals from django.db import models from django.utils.encoding import python_2_unicode_compatible from uuid import uuid4 import mptt from mptt.fields import TreeForeignKey, TreeOneToOneField, TreeManyToManyField from mptt.models import MPTTModel from mptt.managers import TreeManager from django.db.models.query import QuerySet class CustomTreeQueryset(QuerySet): def custom_method(self): pass class CustomTreeManager(TreeManager): def get_query_set(self): return CustomTreeQueryset(model=self.model, using=self._db) def get_queryset(self): # Django 1.8 removed the fallbacks here. return CustomTreeQueryset(model=self.model, using=self._db) def get_empty_query_set(self): return self.get_queryset().none() @python_2_unicode_compatible class Category(MPTTModel): name = models.CharField(max_length=50) parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) category_uuid = models.CharField(max_length=50, unique=True, null=True) def __str__(self): return self.name def delete(self): super(Category, self).delete() delete.alters_data = True @python_2_unicode_compatible class Item(models.Model): name = models.CharField(max_length=100) category_fk = models.ForeignKey( 'Category', to_field='category_uuid', null=True, related_name='items_by_fk', on_delete=models.CASCADE) category_pk = models.ForeignKey( 'Category', null=True, related_name='items_by_pk', on_delete=models.CASCADE) def __str__(self): return self.name @python_2_unicode_compatible class Genre(MPTTModel): name = models.CharField(max_length=50, unique=True) parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) def __str__(self): return self.name class Game(models.Model): genre = TreeForeignKey(Genre, on_delete=models.CASCADE) genres_m2m = models.ManyToManyField(Genre, related_name='games_m2m') name = models.CharField(max_length=50) def __str__(self): return self.name class Insert(MPTTModel): parent = models.ForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) @python_2_unicode_compatible class MultiOrder(MPTTModel): name = models.CharField(max_length=50) size = models.PositiveIntegerField() date = models.DateField() parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) class MPTTMeta: order_insertion_by = ['name', 'size', '-date'] def __str__(self): return self.name class Node(MPTTModel): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) # To check that you can set level_attr etc to an existing field. level = models.IntegerField() class MPTTMeta: left_attr = 'does' right_attr = 'zis' level_attr = 'level' tree_id_attr = 'work' class UUIDNode(MPTTModel): parent = models.ForeignKey('self', null=True, blank=True, related_name='children') uuid = models.UUIDField(primary_key=True, default=uuid4) name = models.CharField(max_length=50) def __str__(self): return self.name @python_2_unicode_compatible class OrderedInsertion(MPTTModel): name = models.CharField(max_length=50) parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) class MPTTMeta: order_insertion_by = ['name'] def __str__(self): return self.name class Tree(MPTTModel): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) class NewStyleMPTTMeta(MPTTModel): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) class MPTTMeta(object): left_attr = 'testing' @python_2_unicode_compatible class Person(MPTTModel): name = models.CharField(max_length=50) parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) # just testing it's actually possible to override the tree manager objects = CustomTreeManager() # This line is set because of https://github.com/django-mptt/django-mptt/issues/369 _default_manager = objects def __str__(self): return self.name class Student(Person): type = models.CharField(max_length=50) @python_2_unicode_compatible class CustomPKName(MPTTModel): my_id = models.AutoField(db_column='my_custom_name', primary_key=True) name = models.CharField(max_length=50) parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', db_column="my_cusom_parent", on_delete=models.CASCADE) def __str__(self): return self.name class ReferencingModel(models.Model): fk = TreeForeignKey(Category, related_name='+', on_delete=models.CASCADE) one = TreeOneToOneField(Category, related_name='+', on_delete=models.CASCADE) m2m = TreeManyToManyField(Category, related_name='+') # for testing various types of inheritance: # 1. multi-table inheritance, with mptt fields on base class. class MultiTableInheritanceA1(MPTTModel): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) class MultiTableInheritanceA2(MultiTableInheritanceA1): name = models.CharField(max_length=50) # 2. multi-table inheritance, with mptt fields on child class. class MultiTableInheritanceB1(MPTTModel): name = models.CharField(max_length=50) class MultiTableInheritanceB2(MultiTableInheritanceB1): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) # 3. abstract models class AbstractModel(MPTTModel): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) ghosts = models.CharField(max_length=50) class Meta: abstract = True class ConcreteModel(AbstractModel): name = models.CharField(max_length=50) class AbstractConcreteAbstract(ConcreteModel): # abstract --> concrete --> abstract class Meta: abstract = True class ConcreteAbstractConcreteAbstract(ConcreteModel): # concrete --> abstract --> concrete --> abstract pass class ConcreteConcrete(ConcreteModel): # another subclass (concrete this time) of the root concrete model pass # 4. proxy models class SingleProxyModel(ConcreteModel): objects = CustomTreeManager() class Meta: proxy = True class DoubleProxyModel(SingleProxyModel): class Meta: proxy = True # 5. swappable models class SwappableModel(MPTTModel): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) class Meta: swappable = 'MPTT_SWAPPABLE_MODEL' class SwappedInModel(MPTTModel): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) name = models.CharField(max_length=50) class AutoNowDateFieldModel(MPTTModel): parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE) now = models.DateTimeField(auto_now_add=True) class MPTTMeta: order_insertion_by = ('now',) # test registering of remote model class Group(models.Model): name = models.CharField(max_length=100) TreeForeignKey( Group, blank=True, null=True, on_delete=models.CASCADE ).contribute_to_class(Group, 'parent') mptt.register(Group, order_insertion_by=('name',)) python-django-mptt-0.8.5/tests/myapp/tests.py000066400000000000000000002151301275105100200212660ustar00rootroot00000000000000from __future__ import unicode_literals import io import os import re import sys import tempfile import unittest from django import forms from django.contrib.auth.models import Group, User from django.db.models import Q from django.db.models.query_utils import DeferredAttribute from django.apps import apps from django.forms.models import modelform_factory from django.template import Template, TemplateSyntaxError, Context from django.test import RequestFactory, TestCase from django.utils.six import string_types, PY3, b, assertRaisesRegex from django.contrib.admin.views.main import ChangeList from django.contrib.admin import ModelAdmin, site try: from mock_django import mock_signal_receiver except ImportError: mock_signal_receiver = None from mptt.admin import JS from mptt.exceptions import CantDisableUpdates, InvalidMove from mptt.forms import ( MPTTAdminForm, TreeNodeChoiceField, TreeNodeMultipleChoiceField, MoveNodeForm) from mptt.models import MPTTModel from mptt.managers import TreeManager from mptt.signals import node_moved from mptt.templatetags.mptt_tags import cache_tree_children from mptt.utils import print_debug_info from myapp.models import ( Category, Item, Genre, CustomPKName, SingleProxyModel, DoubleProxyModel, ConcreteModel, OrderedInsertion, AutoNowDateFieldModel, Person, CustomTreeQueryset, Node, ReferencingModel, CustomTreeManager, UUIDNode) def get_tree_details(nodes): """ Creates pertinent tree details for the given list of nodes. The fields are: id parent_id tree_id level left right """ if hasattr(nodes, 'order_by'): nodes = list(nodes.order_by('tree_id', 'lft', 'pk')) nodes = list(nodes) opts = nodes[0]._mptt_meta return '\n'.join(['%s %s %s %s %s %s' % (n.pk, getattr(n, '%s_id' % opts.parent_attr) or '-', getattr(n, opts.tree_id_attr), getattr(n, opts.level_attr), getattr(n, opts.left_attr), getattr(n, opts.right_attr)) for n in nodes]) leading_whitespace_re = re.compile(r'^\s+', re.MULTILINE) def tree_details(text): """ Trims leading whitespace from the given text specifying tree details so triple-quoted strings can be used to provide tree details in a readable format (says who?), to be compared with the result of using the ``get_tree_details`` function. """ return leading_whitespace_re.sub('', text.rstrip()) class TreeTestCase(TestCase): def assertTreeEqual(self, tree1, tree2): if not isinstance(tree1, string_types): tree1 = get_tree_details(tree1) tree1 = tree_details(tree1) if not isinstance(tree2, string_types): tree2 = get_tree_details(tree2) tree2 = tree_details(tree2) return self.assertEqual(tree1, tree2, "\n%r\n != \n%r" % (tree1, tree2)) class DocTestTestCase(TreeTestCase): def test_run_doctest(self): class DummyStream: content = "" encoding = 'utf8' def write(self, text): self.content += text def flush(self): pass dummy_stream = DummyStream() before = sys.stdout sys.stdout = dummy_stream with open(os.path.join(os.path.dirname(__file__), 'doctests.txt')) as f: with tempfile.NamedTemporaryFile() as temp: text = f.read() if PY3: # unicode literals in the doctests screw up doctest on py3. # this is pretty icky, but I can't find any other # workarounds :( text = re.sub(r"""\bu(["\'])""", r"\1", text) temp.write(b(text)) else: temp.write(text) temp.flush() import doctest doctest.testfile( temp.name, module_relative=False, optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS, encoding='utf-8', ) sys.stdout = before content = dummy_stream.content if content: before.write(content + '\n') self.fail() # genres.json defines the following tree structure # # 1 - 1 0 1 16 action # 2 1 1 1 2 9 +-- platformer # 3 2 1 2 3 4 | |-- platformer_2d # 4 2 1 2 5 6 | |-- platformer_3d # 5 2 1 2 7 8 | +-- platformer_4d # 6 1 1 1 10 15 +-- shmup # 7 6 1 2 11 12 |-- shmup_vertical # 8 6 1 2 13 14 +-- shmup_horizontal # 9 - 2 0 1 6 rpg # 10 9 2 1 2 3 |-- arpg # 11 9 2 1 4 5 +-- trpg class ReparentingTestCase(TreeTestCase): """ Test that trees are in the appropriate state after reparenting and that reparented items have the correct tree attributes defined, should they be required for use after a save. """ fixtures = ['genres.json'] def test_new_root_from_subtree(self): shmup = Genre.objects.get(id=6) shmup.parent = None shmup.save() self.assertTreeEqual([shmup], '6 - 3 0 1 6') self.assertTreeEqual(Genre.objects.all(), """ 1 - 1 0 1 10 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 9 - 2 0 1 6 10 9 2 1 2 3 11 9 2 1 4 5 6 - 3 0 1 6 7 6 3 1 2 3 8 6 3 1 4 5 """) def test_new_root_from_leaf_with_siblings(self): platformer_2d = Genre.objects.get(id=3) platformer_2d.parent = None platformer_2d.save() self.assertTreeEqual([platformer_2d], '3 - 3 0 1 2') self.assertTreeEqual(Genre.objects.all(), """ 1 - 1 0 1 14 2 1 1 1 2 7 4 2 1 2 3 4 5 2 1 2 5 6 6 1 1 1 8 13 7 6 1 2 9 10 8 6 1 2 11 12 9 - 2 0 1 6 10 9 2 1 2 3 11 9 2 1 4 5 3 - 3 0 1 2 """) def test_new_child_from_root(self): action = Genre.objects.get(id=1) rpg = Genre.objects.get(id=9) action.parent = rpg action.save() self.assertTreeEqual([action], '1 9 2 1 6 21') self.assertTreeEqual([rpg], '9 - 2 0 1 22') self.assertTreeEqual(Genre.objects.all(), """ 9 - 2 0 1 22 10 9 2 1 2 3 11 9 2 1 4 5 1 9 2 1 6 21 2 1 2 2 7 14 3 2 2 3 8 9 4 2 2 3 10 11 5 2 2 3 12 13 6 1 2 2 15 20 7 6 2 3 16 17 8 6 2 3 18 19 """) def test_move_leaf_to_other_tree(self): shmup_horizontal = Genre.objects.get(id=8) rpg = Genre.objects.get(id=9) shmup_horizontal.parent = rpg shmup_horizontal.save() self.assertTreeEqual([shmup_horizontal], '8 9 2 1 6 7') self.assertTreeEqual([rpg], '9 - 2 0 1 8') self.assertTreeEqual(Genre.objects.all(), """ 1 - 1 0 1 14 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 6 1 1 1 10 13 7 6 1 2 11 12 9 - 2 0 1 8 10 9 2 1 2 3 11 9 2 1 4 5 8 9 2 1 6 7 """) def test_move_subtree_to_other_tree(self): shmup = Genre.objects.get(id=6) trpg = Genre.objects.get(id=11) shmup.parent = trpg shmup.save() self.assertTreeEqual([shmup], '6 11 2 2 5 10') self.assertTreeEqual([trpg], '11 9 2 1 4 11') self.assertTreeEqual(Genre.objects.all(), """ 1 - 1 0 1 10 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 9 - 2 0 1 12 10 9 2 1 2 3 11 9 2 1 4 11 6 11 2 2 5 10 7 6 2 3 6 7 8 6 2 3 8 9 """) def test_move_child_up_level(self): shmup_horizontal = Genre.objects.get(id=8) action = Genre.objects.get(id=1) shmup_horizontal.parent = action shmup_horizontal.save() self.assertTreeEqual([shmup_horizontal], '8 1 1 1 14 15') self.assertTreeEqual([action], '1 - 1 0 1 16') self.assertTreeEqual(Genre.objects.all(), """ 1 - 1 0 1 16 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 6 1 1 1 10 13 7 6 1 2 11 12 8 1 1 1 14 15 9 - 2 0 1 6 10 9 2 1 2 3 11 9 2 1 4 5 """) def test_move_subtree_down_level(self): shmup = Genre.objects.get(id=6) platformer = Genre.objects.get(id=2) shmup.parent = platformer shmup.save() self.assertTreeEqual([shmup], '6 2 1 2 9 14') self.assertTreeEqual([platformer], '2 1 1 1 2 15') self.assertTreeEqual(Genre.objects.all(), """ 1 - 1 0 1 16 2 1 1 1 2 15 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 6 2 1 2 9 14 7 6 1 3 10 11 8 6 1 3 12 13 9 - 2 0 1 6 10 9 2 1 2 3 11 9 2 1 4 5 """) def test_move_to(self): rpg = Genre.objects.get(pk=9) action = Genre.objects.get(pk=1) rpg.move_to(action) rpg.save() self.assertEqual(rpg.parent, action) def test_invalid_moves(self): # A node may not be made a child of itself action = Genre.objects.get(id=1) action.parent = action platformer = Genre.objects.get(id=2) platformer.parent = platformer self.assertRaises(InvalidMove, action.save) self.assertRaises(InvalidMove, platformer.save) # A node may not be made a child of any of its descendants platformer_4d = Genre.objects.get(id=5) action.parent = platformer_4d platformer.parent = platformer_4d self.assertRaises(InvalidMove, action.save) self.assertRaises(InvalidMove, platformer.save) # New parent is still set when an error occurs self.assertEqual(action.parent, platformer_4d) self.assertEqual(platformer.parent, platformer_4d) class ConcurrencyTestCase(TreeTestCase): """ Test that tree structure remains intact when saving nodes (without setting new parent) after tree structure has been changed. """ def setUp(self): fruit = ConcreteModel.objects.create(name="Fruit") vegie = ConcreteModel.objects.create(name="Vegie") ConcreteModel.objects.create(name="Apple", parent=fruit) ConcreteModel.objects.create(name="Pear", parent=fruit) ConcreteModel.objects.create(name="Tomato", parent=vegie) ConcreteModel.objects.create(name="Carrot", parent=vegie) # sanity check self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 3 1 1 1 2 3 4 1 1 1 4 5 2 - 2 0 1 6 5 2 2 1 2 3 6 2 2 1 4 5 """) def _modify_tree(self): fruit = ConcreteModel.objects.get(name="Fruit") vegie = ConcreteModel.objects.get(name="Vegie") vegie.move_to(fruit) def _assert_modified_tree_state(self): carrot = ConcreteModel.objects.get(id=6) self.assertTreeEqual([carrot], '6 2 1 2 5 6') self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 12 2 1 1 1 2 7 5 2 1 2 3 4 6 2 1 2 5 6 3 1 1 1 8 9 4 1 1 1 10 11 """) def test_node_save_after_tree_restructuring(self): carrot = ConcreteModel.objects.get(id=6) self._modify_tree() carrot.name = "Purple carrot" carrot.save() self._assert_modified_tree_state() def test_node_save_after_tree_restructuring_with_update_fields(self): """ Test that model is saved properly when passing update_fields """ carrot = ConcreteModel.objects.get(id=6) self._modify_tree() # update with kwargs carrot.name = "Won't change" carrot.ghosts = "Will get updated" carrot.save(update_fields=["ghosts"]) self._assert_modified_tree_state() updated_carrot = ConcreteModel.objects.get(id=6) self.assertEqual(updated_carrot.ghosts, carrot.ghosts) self.assertNotEqual(updated_carrot.name, carrot.name) # update with positional arguments carrot.name = "Will change" carrot.ghosts = "Will not be updated" carrot.save(False, False, None, ["name"]) updated_carrot = ConcreteModel.objects.get(id=6) self.assertNotEqual(updated_carrot.ghosts, carrot.ghosts) self.assertEqual(updated_carrot.name, carrot.name) def test_update_fields_positional(self): """ Test that update_fields works as a positional argument Test for https://github.com/django-mptt/django-mptt/issues/384 """ carrot = ConcreteModel.objects.get(id=6) # Why would you do it this way? Meh. carrot.save(False, False, None, None) # categories.json defines the following tree structure: # # 1 - 1 0 1 20 games # 2 1 1 1 2 7 +-- wii # 3 2 1 2 3 4 | |-- wii_games # 4 2 1 2 5 6 | +-- wii_hardware # 5 1 1 1 8 13 +-- xbox360 # 6 5 1 2 9 10 | |-- xbox360_games # 7 5 1 2 11 12 | +-- xbox360_hardware # 8 1 1 1 14 19 +-- ps3 # 9 8 1 2 15 16 |-- ps3_games # 10 8 1 2 17 18 +-- ps3_hardware class DeletionTestCase(TreeTestCase): """ Tests that the tree structure is maintained appropriately in various deletion scenarios. """ fixtures = ['categories.json'] def test_delete_root_node(self): # Add a few other roots to verify that they aren't affected Category(name='Preceding root').insert_at(Category.objects.get(id=1), 'left', save=True) Category(name='Following root').insert_at(Category.objects.get(id=1), 'right', save=True) self.assertTreeEqual(Category.objects.all(), """ 11 - 1 0 1 2 1 - 2 0 1 20 2 1 2 1 2 7 3 2 2 2 3 4 4 2 2 2 5 6 5 1 2 1 8 13 6 5 2 2 9 10 7 5 2 2 11 12 8 1 2 1 14 19 9 8 2 2 15 16 10 8 2 2 17 18 12 - 3 0 1 2 """) Category.objects.get(id=1).delete() self.assertTreeEqual( Category.objects.all(), """ 11 - 1 0 1 2 12 - 3 0 1 2 """) def test_delete_last_node_with_siblings(self): Category.objects.get(id=9).delete() self.assertTreeEqual(Category.objects.all(), """ 1 - 1 0 1 18 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 8 1 1 1 14 17 10 8 1 2 15 16 """) def test_delete_last_node_with_descendants(self): Category.objects.get(id=8).delete() self.assertTreeEqual(Category.objects.all(), """ 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 """) def test_delete_node_with_siblings(self): child = Category.objects.get(id=6) parent = child.parent self.assertEqual(parent.get_descendant_count(), 2) child.delete() self.assertTreeEqual(Category.objects.all(), """ 1 - 1 0 1 18 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 11 7 5 1 2 9 10 8 1 1 1 12 17 9 8 1 2 13 14 10 8 1 2 15 16 """) self.assertEqual(parent.get_descendant_count(), 1) parent = Category.objects.get(pk=parent.pk) self.assertEqual(parent.get_descendant_count(), 1) def test_delete_node_with_descendants_and_siblings(self): """ Regression test for Issue 23 - we used to use pre_delete, which resulted in tree cleanup being performed for every node being deleted, rather than just the node on which ``delete()`` was called. """ Category.objects.get(id=5).delete() self.assertTreeEqual(Category.objects.all(), """ 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 8 1 1 1 8 13 9 8 1 2 9 10 10 8 1 2 11 12 """) class IntraTreeMovementTestCase(TreeTestCase): pass class InterTreeMovementTestCase(TreeTestCase): pass class PositionedInsertionTestCase(TreeTestCase): pass class CustomPKNameTestCase(TreeTestCase): def setUp(self): manager = CustomPKName.objects c1 = manager.create(name="c1") manager.create(name="c11", parent=c1) manager.create(name="c12", parent=c1) c2 = manager.create(name="c2") manager.create(name="c21", parent=c2) manager.create(name="c22", parent=c2) manager.create(name="c3") def test_get_next_sibling(self): root = CustomPKName.objects.get(name="c12") sib = root.get_next_sibling() self.assertTrue(sib is None) class DisabledUpdatesTestCase(TreeTestCase): def setUp(self): self.a = ConcreteModel.objects.create(name="a") self.b = ConcreteModel.objects.create(name="b", parent=self.a) self.c = ConcreteModel.objects.create(name="c", parent=self.a) self.d = ConcreteModel.objects.create(name="d") # state is now: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 """) def test_single_proxy(self): self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(SingleProxyModel._mptt_updates_enabled) self.assertRaises( CantDisableUpdates, SingleProxyModel.objects.disable_mptt_updates().__enter__) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(SingleProxyModel._mptt_updates_enabled) with ConcreteModel.objects.disable_mptt_updates(): self.assertFalse(ConcreteModel._mptt_updates_enabled) self.assertFalse(SingleProxyModel._mptt_updates_enabled) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(SingleProxyModel._mptt_updates_enabled) def test_double_proxy(self): self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(DoubleProxyModel._mptt_updates_enabled) self.assertRaises( CantDisableUpdates, DoubleProxyModel.objects.disable_mptt_updates().__enter__) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(DoubleProxyModel._mptt_updates_enabled) with ConcreteModel.objects.disable_mptt_updates(): self.assertFalse(ConcreteModel._mptt_updates_enabled) self.assertFalse(DoubleProxyModel._mptt_updates_enabled) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(DoubleProxyModel._mptt_updates_enabled) def test_insert_child(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): # 1 query here: with self.assertNumQueries(1): ConcreteModel.objects.create(name="e", parent=self.d) # 2nd query here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 4 2 1 2 3 """) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 4 2 1 2 3 """) def test_insert_root(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 1 query here: ConcreteModel.objects.create(name="e") # 2nd query here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 5 - 0 0 1 2 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 """) self.assertTreeEqual(ConcreteModel.objects.all(), """ 5 - 0 0 1 2 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 """) def test_move_node_same_tree(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 2 queries here: # (django does a query to determine if the row is in the db yet) self.c.parent = self.b self.c.save() # 3rd query here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 2 1 1 4 5 4 - 2 0 1 2 """) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 2 1 1 4 5 4 - 2 0 1 2 """) def test_move_node_different_tree(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 1 update query self.c.parent = self.d self.c.save() # query 2 here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 4 1 1 4 5 4 - 2 0 1 2 """) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 4 1 1 4 5 4 - 2 0 1 2 """) def test_move_node_to_root(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 1 update query self.c.parent = None self.c.save() # query 2 here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 - 1 1 4 5 4 - 2 0 1 2 """) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 - 1 1 4 5 4 - 2 0 1 2 """) def test_move_root_to_child(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 1 update query self.d.parent = self.c self.d.save() # query 2 here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 2 0 1 2 """) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 2 0 1 2 """) class DelayedUpdatesTestCase(TreeTestCase): def setUp(self): self.a = ConcreteModel.objects.create(name="a") self.b = ConcreteModel.objects.create(name="b", parent=self.a) self.c = ConcreteModel.objects.create(name="c", parent=self.a) self.d = ConcreteModel.objects.create(name="d") self.z = ConcreteModel.objects.create(name="z") # state is now: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 """) def test_proxy(self): self.assertFalse(ConcreteModel._mptt_is_tracking) self.assertFalse(SingleProxyModel._mptt_is_tracking) self.assertRaises( CantDisableUpdates, SingleProxyModel.objects.delay_mptt_updates().__enter__) self.assertFalse(ConcreteModel._mptt_is_tracking) self.assertFalse(SingleProxyModel._mptt_is_tracking) with ConcreteModel.objects.delay_mptt_updates(): self.assertTrue(ConcreteModel._mptt_is_tracking) self.assertTrue(SingleProxyModel._mptt_is_tracking) self.assertFalse(ConcreteModel._mptt_is_tracking) self.assertFalse(SingleProxyModel._mptt_is_tracking) def test_double_context_manager(self): with ConcreteModel.objects.delay_mptt_updates(): self.assertTrue(ConcreteModel._mptt_is_tracking) with ConcreteModel.objects.delay_mptt_updates(): self.assertTrue(ConcreteModel._mptt_is_tracking) self.assertTrue(ConcreteModel._mptt_is_tracking) self.assertFalse(ConcreteModel._mptt_is_tracking) def test_insert_child(self): with self.assertNumQueries(8): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 1 query for target stale check, # 1 query to save node. ConcreteModel.objects.create(name="e", parent=self.d) # 3rd query here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 6 4 2 1 2 3 5 - 3 0 1 2 """) # remaining queries (4 through 8) are the partial rebuild process. self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 4 6 4 2 1 2 3 5 - 3 0 1 2 """) def test_insert_root(self): with self.assertNumQueries(3): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries required here: # (one to get the correct tree_id, then one to insert) ConcreteModel.objects.create(name="e") # 3rd query here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 6 - 4 0 1 2 """) # no partial rebuild necessary, as no trees were modified # (newly created tree is already okay) self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 6 - 4 0 1 2 """) def test_move_node_same_tree(self): with self.assertNumQueries(10): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 1 query to ensure target fields aren't stale # 1 update query self.c.parent = self.b self.c.save() # query 3 here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 2 1 2 3 4 4 - 2 0 1 2 5 - 3 0 1 2 """) # the remaining 7 queries are the partial rebuild. self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 4 - 2 0 1 2 5 - 3 0 1 2 """) def test_move_node_different_tree(self): with self.assertNumQueries(12): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries here: # 1. update the node # 2. collapse old tree since it is now empty. self.d.parent = self.c self.d.save() # query 3 here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 1 2 5 6 5 - 2 0 1 2 """) # the other 9 queries are the partial rebuild self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 3 1 1 1 4 7 4 3 1 2 5 6 5 - 2 0 1 2 """) def test_move_node_to_root(self): with self.assertNumQueries(4): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(3): # 3 queries here! # 1. find the next tree_id to move to # 2. update the tree_id on all nodes to the right of that # 3. update tree fields on self.c self.c.parent = None self.c.save() # 4th query here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 4 - 2 0 1 2 5 - 3 0 1 2 3 - 4 0 1 2 """) self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 4 - 2 0 1 2 5 - 3 0 1 2 3 - 4 0 1 2 """) def test_move_root_to_child(self): with self.assertNumQueries(12): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries here: # 1. update the node # 2. collapse old tree since it is now empty. self.d.parent = self.c self.d.save() # query 3 here: self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 1 2 5 6 5 - 2 0 1 2 """) # the remaining 9 queries are the partial rebuild. self.assertTreeEqual(ConcreteModel.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 3 1 1 1 4 7 4 3 1 2 5 6 5 - 2 0 1 2 """) class OrderedInsertionDelayedUpdatesTestCase(TreeTestCase): def setUp(self): self.c = OrderedInsertion.objects.create(name="c") self.d = OrderedInsertion.objects.create(name="d", parent=self.c) self.e = OrderedInsertion.objects.create(name="e", parent=self.c) self.f = OrderedInsertion.objects.create(name="f") self.z = OrderedInsertion.objects.create(name="z") # state is now: self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 """) def test_insert_child(self): with self.assertNumQueries(12): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 1 query here: OrderedInsertion.objects.create(name="dd", parent=self.c) # 2nd query here: self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 6 1 1 1 6 7 4 - 2 0 1 2 5 - 3 0 1 2 """) # remaining 9 queries are the partial rebuild process. self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 6 1 1 1 4 5 3 1 1 1 6 7 4 - 2 0 1 2 5 - 3 0 1 2 """) def test_insert_root(self): with self.assertNumQueries(4): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(3): # 3 queries required here: # 1. get correct tree_id (delay_mptt_updates doesn't handle # root-level ordering when using ordered insertion) # 2. increment tree_id of all following trees # 3. insert the object OrderedInsertion.objects.create(name="ee") # 4th query here: self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 6 - 2 0 1 2 4 - 3 0 1 2 5 - 4 0 1 2 """) # no partial rebuild is required self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 6 - 2 0 1 2 4 - 3 0 1 2 5 - 4 0 1 2 """) def test_move_node_same_tree(self): with self.assertNumQueries(9): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(1): # 1 update query self.e.name = 'before d' self.e.save() # query 2 here: self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 """) # the remaining 7 queries are the partial rebuild. self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 """) def test_move_node_different_tree(self): with self.assertNumQueries(12): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries here: # 1. update the node # 2. collapse old tree since it is now empty. self.f.parent = self.c self.f.name = 'dd' self.f.save() # query 3 here: self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 4 1 1 1 2 3 3 1 1 1 4 5 5 - 2 0 1 2 """) # the remaining 9 queries are the partial rebuild self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 4 1 1 1 4 5 3 1 1 1 6 7 5 - 2 0 1 2 """) def test_move_node_to_root(self): with self.assertNumQueries(4): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(3): # 3 queries here! # 1. find the next tree_id to move to # 2. update the tree_id on all nodes to the right of that # 3. update tree fields on self.c self.e.parent = None self.e.save() # query 4 here: self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 - 2 0 1 2 4 - 3 0 1 2 5 - 4 0 1 2 """) self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 - 2 0 1 2 4 - 3 0 1 2 5 - 4 0 1 2 """) def test_move_root_to_child(self): with self.assertNumQueries(12): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries here: # 1. update the node # 2. collapse old tree since it is now empty. self.f.parent = self.e self.f.save() # query 3 here: self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 1 2 5 6 5 - 2 0 1 2 """) # the remaining 9 queries are the partial rebuild. self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 3 1 1 1 4 7 4 3 1 2 5 6 5 - 2 0 1 2 """) class ManagerTests(TreeTestCase): fixtures = ['categories.json', 'genres.json', 'persons.json'] def test_all_managers_are_different(self): # all tree managers should be different. otherwise, possible infinite recursion. seen = {} for model in apps.get_models(): if not issubclass(model, MPTTModel): continue tm = model._tree_manager if id(tm) in seen: self.fail( "Tree managers for %s and %s are the same manager" % (model.__name__, seen[id(tm)].__name__)) seen[id(tm)] = model def test_all_managers_have_correct_model(self): # all tree managers should have the correct model. for model in apps.get_models(): if not issubclass(model, MPTTModel): continue self.assertEqual(model._tree_manager.model, model) def test_base_manager_infinite_recursion(self): # repeatedly calling _base_manager should eventually return None for model in apps.get_models(): if not issubclass(model, MPTTModel): continue manager = model._tree_manager for i in range(20): manager = manager._base_manager if manager is None: break else: self.fail("Detected infinite recursion in %s._tree_manager._base_manager" % model) def test_proxy_custom_manager(self): self.assertIsInstance(SingleProxyModel._tree_manager, CustomTreeManager) self.assertIsInstance(SingleProxyModel._tree_manager._base_manager, TreeManager) self.assertIsInstance(SingleProxyModel.objects, CustomTreeManager) self.assertIsInstance(SingleProxyModel.objects._base_manager, TreeManager) def test_get_queryset_descendants(self): def get_desc_names(qs, include_self=False): desc = qs.model.objects.get_queryset_descendants( qs, include_self=include_self) return list(desc.values_list('name', flat=True).order_by('name')) qs = Category.objects.filter(Q(name='Nintendo Wii') | Q(name='PlayStation 3')) self.assertEqual( get_desc_names(qs), ['Games', 'Games', 'Hardware & Accessories', 'Hardware & Accessories'], ) self.assertEqual( get_desc_names(qs, include_self=True), ['Games', 'Games', 'Hardware & Accessories', 'Hardware & Accessories', 'Nintendo Wii', 'PlayStation 3'] ) qs = Genre.objects.filter(parent=None) self.assertEqual( get_desc_names(qs), ['2D Platformer', '3D Platformer', '4D Platformer', 'Action RPG', 'Horizontal Scrolling Shootemup', 'Platformer', 'Shootemup', 'Tactical RPG', 'Vertical Scrolling Shootemup'] ) self.assertEqual( get_desc_names(qs, include_self=True), ['2D Platformer', '3D Platformer', '4D Platformer', 'Action', 'Action RPG', 'Horizontal Scrolling Shootemup', 'Platformer', 'Role-playing Game', 'Shootemup', 'Tactical RPG', 'Vertical Scrolling Shootemup'] ) def _get_anc_names(self, qs, include_self=False): anc = qs.model.objects.get_queryset_ancestors( qs, include_self=include_self) return list(anc.values_list('name', flat=True).order_by('name')) def test_get_queryset_ancestors(self): qs = Category.objects.filter(Q(name='Nintendo Wii') | Q(name='PlayStation 3')) self.assertEqual( self._get_anc_names(qs), ['PC & Video Games'] ) self.assertEqual( self._get_anc_names(qs, include_self=True), ['Nintendo Wii', 'PC & Video Games', 'PlayStation 3'] ) qs = Genre.objects.filter(parent=None) self.assertEqual(self._get_anc_names(qs), []) self.assertEqual( self._get_anc_names(qs, include_self=True), ['Action', 'Role-playing Game']) def test_get_queryset_ancestors_regression_379(self): # https://github.com/django-mptt/django-mptt/issues/379 qs = Genre.objects.all() self.assertEqual( self._get_anc_names(qs, include_self=True), list(Genre.objects.values_list('name', flat=True).order_by('name'))) def test_custom_querysets(self): """ Test that a custom manager also provides custom querysets. """ self.assertTrue(isinstance(Person.objects.all(), CustomTreeQueryset)) self.assertTrue(isinstance(Person.objects.all()[0].get_children(), CustomTreeQueryset)) self.assertTrue(hasattr(Person.objects.none(), 'custom_method')) # Check that empty querysets get custom methods self.assertTrue(hasattr(Person.objects.all()[0].get_children().none(), 'custom_method')) self.assertEqual( type(Person.objects.all()), type(Person.objects.root_nodes()) ) def test_manager_from_custom_queryset(self): """ Test that a manager created from a custom queryset works. Regression test for #378. """ TreeManager.from_queryset(CustomTreeQueryset)().contribute_to_class(Genre, 'my_manager') self.assertIsInstance(Genre.my_manager.get_queryset(), CustomTreeQueryset) def test_num_queries_on_get_queryset_descendants(self): """ Test the number of queries to access descendants is not O(n). At the moment it is O(1)+1. Ideally we should aim for O(1). """ with self.assertNumQueries(2): qs = Category.objects.get_queryset_descendants( Category.objects.all(), include_self=True) self.assertEqual(len(qs), 10) class CacheTreeChildrenTestCase(TreeTestCase): """ Tests for the ``cache_tree_children`` template filter. """ fixtures = ['categories.json'] def test_cache_tree_children_caches_parents(self): """ Ensures that each node's parent is cached by ``cache_tree_children``. """ # Ensure only 1 query is used during this test with self.assertNumQueries(1): roots = cache_tree_children(Category.objects.all()) games = roots[0] wii = games.get_children()[0] wii_games = wii.get_children()[0] # Ensure that ``wii`` is cached as ``parent`` on ``wii_games``, and # likewise for ``games`` being ``parent`` on the attached ``wii`` self.assertEqual(wii, wii_games.parent) self.assertEqual(games, wii_games.parent.parent) def test_cache_tree_children_with_invalid_ordering(self): """ Ensures that ``cache_tree_children`` fails with a ``ValueError`` when passed a list which is not in tree order. """ with self.assertNumQueries(1): with self.assertRaises(ValueError): cache_tree_children(list(Category.objects.order_by('-id'))) # Passing a list with correct ordering should work, though. with self.assertNumQueries(1): cache_tree_children(list(Category.objects.all())) # The exact ordering tuple doesn't matter, long as the nodes end up in depth-first order. cache_tree_children(Category.objects.order_by('tree_id', 'lft', 'name')) cache_tree_children(Category.objects.filter(tree_id=1).order_by('lft')) class RecurseTreeTestCase(TreeTestCase): """ Tests for the ``recursetree`` template filter. """ fixtures = ['categories.json'] template = re.sub(r'(?m)^[\s]+', '', ''' {% load mptt_tags %}
      {% recursetree nodes %}
    • {{ node.name }} {% if not node.is_leaf_node %}
        {{ children }}
      {% endif %}
    • {% endrecursetree %}
    ''') def test_leaf_html(self): html = Template(self.template).render(Context({ 'nodes': Category.objects.filter(pk=10), })).replace('\n', '') self.assertEqual(html, '
    • Hardware & Accessories
    ') def test_nonleaf_html(self): qs = Category.objects.get(pk=8).get_descendants(include_self=True) html = Template(self.template).render(Context({ 'nodes': qs, })).replace('\n', '') self.assertEqual(html, ( '
    • PlayStation 3
        ' '
      • Games
      • Hardware & Accessories
    ' )) def test_parsing_fail(self): self.assertRaises( TemplateSyntaxError, Template, '{% load mptt_tags %}{% recursetree %}{% endrecursetree %}') def test_cached_ancestors(self): template = Template(''' {% load mptt_tags %} {% recursetree nodes %} {{ node.get_ancestors|join:" > " }} {{ node.name }} {% if not node.is_leaf_node %} {{ children }} {% endif %} {% endrecursetree %} ''') with self.assertNumQueries(1): qs = Category.objects.all() template.render(Context({'nodes': qs})) class TreeInfoTestCase(TreeTestCase): fixtures = ['genres.json'] template = re.sub(r'(?m)^[\s]+', '', ''' {% load mptt_tags %} {% for node, structure in nodes|tree_info %} {% if structure.new_level %}
    • {% else %}
    • {% endif %} {{ node.pk }} {% for level in structure.closed_levels %}
    {% endfor %} {% endfor %}''') template_with_ancestors = re.sub(r'(?m)^[\s]+', '', ''' {% load mptt_tags %} {% for node, structure in nodes|tree_info:"ancestors" %} {% if structure.new_level %}
    • {% else %}
    • {% endif %} {{ node.pk }} {% for ancestor in structure.ancestors %} {% if forloop.first %}A:{% endif %} {{ ancestor }}{% if not forloop.last %},{% endif %} {% endfor %} {% for level in structure.closed_levels %}
    {% endfor %} {% endfor %}''') def test_tree_info_html(self): html = Template(self.template).render(Context({ 'nodes': Genre.objects.all(), })).replace('\n', '') self.assertEqual( html, '
    • 1
      • 2
        • 3
        • 4
        • 5
      • ' '
      • 6
        • 7
        • 8
    • 9
        ' '
      • 10
      • 11
    ') html = Template(self.template).render(Context({ 'nodes': Genre.objects.filter(**{ '%s__gte' % Genre._mptt_meta.level_attr: 1, '%s__lte' % Genre._mptt_meta.level_attr: 2, }), })).replace('\n', '') self.assertEqual( html, '
    • 2
      • 3
      • 4
      • 5
    • 6
        ' '
      • 7
      • 8
    • 10
    • 11
    ') html = Template(self.template_with_ancestors).render(Context({ 'nodes': Genre.objects.filter(**{ '%s__gte' % Genre._mptt_meta.level_attr: 1, '%s__lte' % Genre._mptt_meta.level_attr: 2, }), })).replace('\n', '') self.assertEqual( html, '
    • 2
      • 3A:Platformer
      • 4A:Platformer
      • ' '
      • 5A:Platformer
    • 6
      • 7A:Shootemup
      • ' '
      • 8A:Shootemup
    • 10
    • 11
    ') class FullTreeTestCase(TreeTestCase): fixtures = ['genres.json'] template = re.sub(r'(?m)^[\s]+', '', ''' {% load mptt_tags %} {% full_tree_for_model myapp.Genre as tree %} {% for node in tree %}{{ node.pk }},{% endfor %} ''') def test_full_tree_html(self): html = Template(self.template).render(Context({})).replace('\n', '') self.assertEqual( html, '1,2,3,4,5,6,7,8,9,10,11,') class DrilldownTreeTestCase(TreeTestCase): fixtures = ['genres.json'] template = re.sub(r'(?m)^[\s]+', '', ''' {% load mptt_tags %} {% drilldown_tree_for_node node as tree count myapp.Game.genre in game_count %} {% for n in tree %} {% ifequal n node %}[{% endifequal %} {{ n.pk }}:{{ n.game_count }} {% ifequal n node %}]{% endifequal %}{% if not forloop.last %},{% endif %} {% endfor %} ''') def render_for_node(self, pk, cumulative=False, m2m=False): template = self.template if cumulative: template = template.replace(' count ', ' cumulative count ') if m2m: template = template.replace('Game.genre', 'Game.genres_m2m') return Template(template).render(Context({ 'node': Genre.objects.get(pk=pk), })).replace('\n', '') def test_drilldown_html(self): for idx, genre in enumerate(Genre.objects.all()): for i in range(idx): game = genre.game_set.create(name='Game %s' % i) genre.games_m2m.add(game) self.assertEqual( self.render_for_node(1), '[1:],2:1,6:5') self.assertEqual( self.render_for_node(2), '1:,[2:],3:2,4:3,5:4') self.assertEqual( self.render_for_node(1, cumulative=True), '[1:],2:10,6:18') self.assertEqual( self.render_for_node(2, cumulative=True), '1:,[2:],3:2,4:3,5:4') self.assertEqual( self.render_for_node(1, m2m=True), '[1:],2:1,6:5') self.assertEqual( self.render_for_node(2, m2m=True), '1:,[2:],3:2,4:3,5:4') self.assertEqual( self.render_for_node(1, cumulative=True, m2m=True), '[1:],2:10,6:18') self.assertEqual( self.render_for_node(2, cumulative=True, m2m=True), '1:,[2:],3:2,4:3,5:4') class TestAutoNowDateFieldModel(TreeTestCase): # https://github.com/django-mptt/django-mptt/issues/175 def test_save_auto_now_date_field_model(self): a = AutoNowDateFieldModel() a.save() class RegisteredRemoteModel(TreeTestCase): def test_save_registered_model(self): g1 = Group.objects.create(name='group 1') g1.save() class TestForms(TreeTestCase): fixtures = ['categories.json'] def test_adminform_instantiation(self): # https://github.com/django-mptt/django-mptt/issues/264 c = Category.objects.get(name='Nintendo Wii') CategoryForm = modelform_factory( Category, form=MPTTAdminForm, fields=('name', 'parent'), ) self.assertTrue(CategoryForm(instance=c)) # Test that the parent field is properly limited. (queryset) form = CategoryForm({ 'name': c.name, 'parent': c.children.all()[0].pk, }, instance=c) self.assertFalse(form.is_valid()) self.assertIn( 'Select a valid choice', '%s' % form.errors) # Test that even though we remove the field queryset limit, # validation still fails. form = CategoryForm({ 'name': c.name, 'parent': c.children.all()[0].pk, }, instance=c) form.fields['parent'].queryset = Category.objects.all() self.assertFalse(form.is_valid()) self.assertIn( 'Invalid parent', '%s' % form.errors) def test_field_types(self): ReferencingModelForm = modelform_factory( ReferencingModel, exclude=('id',)) form = ReferencingModelForm() # Also check whether we have the correct form field type self.assertTrue(isinstance( form.fields['fk'], TreeNodeChoiceField)) self.assertTrue(isinstance( form.fields['one'], TreeNodeChoiceField)) self.assertTrue(isinstance( form.fields['m2m'], TreeNodeMultipleChoiceField)) def test_movenodeform(self): c = Category.objects.get(pk=2) form = MoveNodeForm(c, { 'target': '5', 'position': 'first-child', }) self.assertTrue(form.is_valid()) form.save() self.assertTreeEqual(Category.objects.all(), ''' 1 - 1 0 1 20 5 1 1 1 2 13 2 5 1 2 3 8 3 2 1 3 4 5 4 2 1 3 6 7 6 5 1 2 9 10 7 5 1 2 11 12 8 1 1 1 14 19 9 8 1 2 15 16 10 8 1 2 17 18 ''') class TestAltersData(TreeTestCase): def test_alters_data(self): node = Node() output = Template('{{ node.save }}').render(Context({ 'node': node, })) self.assertEqual(output, '') self.assertEqual(node.pk, None) node.save() self.assertNotEqual(node.pk, None) output = Template('{{ node.delete }}').render(Context({ 'node': node, })) self.assertEqual(node, Node.objects.get(pk=node.pk)) class TestDebugInfo(TreeTestCase): fixtures = ['categories.json'] def test_debug_info(self): # Currently fails either on PY2 or PY3. stream_type = io.StringIO if PY3 else io.BytesIO with stream_type() as out: print_debug_info(Category.objects.all(), file=out) output = out.getvalue() self.assertIn('1,0,,1,1,20', output) class AdminBatch(TreeTestCase): fixtures = ['categories.json'] def test_changelist(self): user = User.objects.create_superuser('admin', 'test@example.com', 'p') self.client.login(username=user.username, password='p') response = self.client.get('/admin/myapp/category/') self.assertContains( response, 'name="_selected_action"', 10) mptt_opts = Category._mptt_meta self.assertEqual( response.context['cl'].result_list.query.order_by[:2], [mptt_opts.tree_id_attr, mptt_opts.left_attr]) data = { 'action': 'delete_selected', '_selected_action': ['5', '8', '9'], } response = self.client.post('/admin/myapp/category/', data) self.assertContains(response, 'value="Yes, I\'m sure"', 1) data['post'] = 'yes' response = self.client.post('/admin/myapp/category/', data) self.assertRedirects( response, '/admin/myapp/category/') self.assertEqual(Category.objects.count(), 4) # Batch deletion has not clobbered MPTT values, because our method # delete_selected_tree has been used. self.assertTreeEqual(Category.objects.all(), ''' 1 - 1 0 1 8 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 ''') class TestUnsaved(TreeTestCase): def test_unsaved(self): for method in [ 'get_ancestors', 'get_family', 'get_children', 'get_descendants', 'get_leafnodes', 'get_next_sibling', 'get_previous_sibling', 'get_root', 'get_siblings', ]: assertRaisesRegex( self, ValueError, 'Cannot call %s on unsaved Genre instances' % method, getattr(Genre(), method)) class QuerySetTests(TreeTestCase): fixtures = ['categories.json'] def test_get_ancestors(self): self.assertEqual( [ c.pk for c in Category.objects.get(name="Nintendo Wii").get_ancestors(include_self=False)], [ c.pk for c in Category.objects.filter(name="Nintendo Wii").get_ancestors(include_self=False)], ) self.assertEqual( [ c.pk for c in Category.objects.get(name="Nintendo Wii").get_ancestors(include_self=True)], [ c.pk for c in Category.objects.filter(name="Nintendo Wii").get_ancestors(include_self=True)], ) def test_get_descendants(self): self.assertEqual( [ c.pk for c in Category.objects.get(name="Nintendo Wii").get_descendants(include_self=False)], [ c.pk for c in Category.objects.filter(name="Nintendo Wii").get_descendants(include_self=False)], ) self.assertEqual( [ c.pk for c in Category.objects.get(name="Nintendo Wii").get_descendants(include_self=True)], [ c.pk for c in Category.objects.filter(name="Nintendo Wii").get_descendants(include_self=True)], ) class TreeManagerTestCase(TreeTestCase): fixtures = ['categories.json', 'items.json'] def test_add_related_count_with_fk_to_natural_key(self): # Regression test for #284 queryset = Category.objects.filter(name='Xbox 360').order_by('id') # Test using FK that doesn't point to a primary key for c in Category.objects.add_related_count( queryset, Item, 'category_fk', 'item_count', cumulative=False): self.assertEqual(c.item_count, c.items_by_pk.count()) # Also works when using the FK that *does* point to a primary key for c in Category.objects.add_related_count( queryset, Item, 'category_pk', 'item_count', cumulative=False): self.assertEqual(c.item_count, c.items_by_pk.count()) class TestOrderedInsertionBFS(TreeTestCase): def test_insert_ordered_DFS_backwards_root_nodes(self): rock = OrderedInsertion.objects.create(name="Rock") OrderedInsertion.objects.create(name="Led Zeppelin", parent=rock) OrderedInsertion.objects.create(name="Classical") self.assertTreeEqual(OrderedInsertion.objects.all(), """ 3 - 1 0 1 2 1 - 2 0 1 4 2 1 2 1 2 3 """) def test_insert_ordered_BFS_backwards_root_nodes(self): rock = OrderedInsertion.objects.create(name="Rock") self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 2 """) OrderedInsertion.objects.create(name="Classical") self.assertTreeEqual(OrderedInsertion.objects.all(), """ 2 - 1 0 1 2 1 - 2 0 1 2 """) # This tends to fail if it uses `rock.tree_id`, which is 1, although # in the database Rock's tree_id has been updated to 2. OrderedInsertion.objects.create(name="Led Zeppelin", parent=rock) self.assertTreeEqual(OrderedInsertion.objects.all(), """ 2 - 1 0 1 2 1 - 2 0 1 4 3 1 2 1 2 3 """) def test_insert_ordered_DFS_backwards_nonroot_nodes(self): music = OrderedInsertion.objects.create(name='music') rock = OrderedInsertion.objects.create(name="Rock", parent=music) OrderedInsertion.objects.create(name="Led Zeppelin", parent=rock) OrderedInsertion.objects.create(name="Classical", parent=music) self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 4 1 1 1 2 3 2 1 1 1 4 7 3 2 1 2 5 6 """) def test_insert_ordered_BFS_backwards_nonroot_nodes(self): music = OrderedInsertion.objects.create(name='music') rock = OrderedInsertion.objects.create(name="Rock", parent=music) OrderedInsertion.objects.create(name="Classical", parent=music) self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 """) OrderedInsertion.objects.create(name="Led Zeppelin", parent=rock) self.assertTreeEqual(OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 3 1 1 1 2 3 2 1 1 1 4 7 4 2 1 2 5 6 """) class CacheChildrenTestCase(TreeTestCase): """ Tests that the queryset function `get_cached_trees` results in a minimum number of database queries. """ fixtures = ['genres.json'] def test_genre_iter(self): """ Test a query with two root nodes. """ with self.assertNumQueries(1): root_nodes = Genre.objects.all().get_cached_trees() # `get_cached_trees` should only return the root nodes self.assertEqual(len(root_nodes), 2) # Getting the children of each node should not result in db hits. with self.assertNumQueries(0): for genre in root_nodes: self.assertIsInstance(genre, Genre) for child in genre.get_children(): self.assertIsInstance(child, Genre) for child2 in child.get_children(): self.assertIsInstance(child2, Genre) @unittest.skipUnless(mock_signal_receiver, "Signals tests require mock_django installed") class Signals(TestCase): fixtures = ['categories.json'] def setUp(self): self.signal = node_moved self.wii = Category.objects.get(pk=2) self.ps3 = Category.objects.get(pk=8) def test_signal_should_not_be_sent_when_parent_hasnt_changed(self): with mock_signal_receiver(self.signal, sender=Category) as receiver: self.wii.name = 'Woo' self.wii.save() self.assertEqual(receiver.call_count, 0) def test_signal_should_not_be_sent_when_model_created(self): with mock_signal_receiver(self.signal, sender=Category) as receiver: Category.objects.create(name='Descriptive name') self.assertEqual(receiver.call_count, 0) def test_move_by_using_move_to_should_send_signal(self): with mock_signal_receiver(self.signal, sender=Category) as receiver: self.wii.move_to(self.ps3) receiver.assert_called_once_with( instance=self.wii, signal=self.signal, target=self.ps3, sender=Category, position='first-child' ) def test_move_by_changing_parent_should_send_signal(self): '''position is not set when sent from save(). I assume it would be the default(first-child) but didn't feel comfortable setting it. ''' with mock_signal_receiver(self.signal, sender=Category) as receiver: self.wii.parent = self.ps3 self.wii.save() receiver.assert_called_once_with( instance=self.wii, signal=self.signal, target=self.ps3, sender=Category ) class DeferredAttributeTests(TreeTestCase): """ Regression tests for #176 and #424 """ def setUp(self): OrderedInsertion.objects.create(name="a") def test_deferred_order_insertion_by(self): qs = OrderedInsertion.objects.defer('name') with self.assertNumQueries(1): nodes = list(qs) with self.assertNumQueries(0): self.assertTreeEqual(nodes, ''' 1 - 1 0 1 2 ''') def test_deferred_cached_field_undeferred(self): obj = OrderedInsertion.objects.defer('name').get() self.assertEqual(obj._mptt_cached_fields['name'], DeferredAttribute) with self.assertNumQueries(1): obj.name with self.assertNumQueries(3): # does a node move, since the order_insertion_by field changed obj.save() self.assertEqual(obj._mptt_cached_fields['name'], 'a') def test_deferred_cached_field_change(self): obj = OrderedInsertion.objects.defer('name').get() self.assertEqual(obj._mptt_cached_fields['name'], DeferredAttribute) with self.assertNumQueries(0): obj.name = 'b' with self.assertNumQueries(3): # does a node move, since the order_insertion_by field changed obj.save() self.assertEqual(obj._mptt_cached_fields['name'], 'b') class DraggableMPTTAdminTestCase(TreeTestCase): def setUp(self): self.user = User.objects.create_superuser( 'admin', 'test@example.com', 'p') self.client.login(username=self.user.username, password='p') def test_changelist(self): p1 = Person.objects.create(name='Franz') p2 = Person.objects.create(name='Fritz') p3 = Person.objects.create(name='Hans') self.assertNotEqual(p1._mpttfield('tree_id'), p2._mpttfield('tree_id')) response = self.client.get('/admin/myapp/person/') self.assertContains(response, 'class="drag-handle"', 3) self.assertContains(response, 'style="text-indent:0px"', 3) self.assertContains( response, 'javascript" src="/static/mptt/draggable-admin.js"' ' data-context="{"') self.assertContains( response, '}" id="draggable-admin-context">') response = self.client.post( '/admin/myapp/person/', { 'cmd': 'move_node', 'cut_item': p1.pk, 'pasted_on': p2.pk, 'position': 'last-child', }, HTTP_X_REQUESTED_WITH='XMLHttpRequest', ) self.assertEqual(response.status_code, 200) p1.refresh_from_db() p2.refresh_from_db() self.assertEqual(p1.parent, p2) self.assertTreeEqual(Person.objects.all(), """ 2 - 2 0 1 4 1 2 2 1 2 3 3 - 3 0 1 2 """) response = self.client.get('/admin/myapp/person/') self.assertContains(response, 'style="text-indent:0px"', 2) self.assertContains(response, 'style="text-indent:20px"', 1) response = self.client.post( '/admin/myapp/person/', { 'cmd': 'move_node', 'cut_item': p3.pk, 'pasted_on': p1.pk, 'position': 'left', }, HTTP_X_REQUESTED_WITH='XMLHttpRequest', ) self.assertEqual(response.status_code, 200) self.assertTreeEqual(Person.objects.all(), """ 2 - 2 0 1 6 3 2 2 1 2 3 1 2 2 1 4 5 """) response = self.client.post('/admin/myapp/person/', { 'action': 'delete_selected', '_selected_action': [1], }) self.assertContains(response, 'Are you sure?') response = self.client.post('/admin/myapp/person/', { 'action': 'delete_selected', '_selected_action': [1], 'post': 'yes', }) self.assertRedirects(response, '/admin/myapp/person/') self.assertTreeEqual(Person.objects.all(), """ 2 - 2 0 1 4 3 2 2 1 2 3 """) def test_js(self): media = forms.Media() media.add_js([ JS('asset1.js', {}), JS('asset2.js', {'id': 'something', 'answer': '"42"'}), ]) # We can test the exact representation since forms.Media has been # really stable for a long time, and JS() uses flatatt which # alphabetically sorts its attributes. self.assertEqual( '%s' % media, '\n' '' ) class UUIDPrimaryKey(TreeTestCase): def test_save_uuid_model(self): n1 = UUIDNode.objects.create(name='node') n2 = UUIDNode.objects.create(name='sub_node', parent=n1) self.assertEqual(n1.name, 'node') self.assertEqual(n1.tree_id, n2.tree_id) self.assertEqual(n2.parent, n1) def test_move_uuid_node(self): n1 = UUIDNode.objects.create(name='n1') n2 = UUIDNode.objects.create(name='n2', parent=n1) n3 = UUIDNode.objects.create(name='n3', parent=n1) self.assertEqual(list(n1.get_children()), [n2, n3]) n3.move_to(n2, 'left') self.assertEqual(list(n1.get_children()), [n3, n2]) def test_move_root_node(self): root1 = UUIDNode.objects.create(name='n1') child = UUIDNode.objects.create(name='n2', parent=root1) root2 = UUIDNode.objects.create(name='n3') self.assertEqual(list(root1.get_children()), [child]) root2.move_to(child, 'left') self.assertEqual(list(root1.get_children()), [root2, child]) def test_move_child_node(self): root1 = UUIDNode.objects.create(name='n1') child1 = UUIDNode.objects.create(name='n2', parent=root1) root2 = UUIDNode.objects.create(name='n3') child2 = UUIDNode.objects.create(name='n4', parent=root2) self.assertEqual(list(root1.get_children()), [child1]) child2.move_to(child1, 'left') self.assertEqual(list(root1.get_children()), [child2, child1]) class DirectParentAssignment(TreeTestCase): def test_assignment(self): """Regression test for #428""" n1 = Node.objects.create() n2 = Node.objects.create() n1.parent_id = n2.id n1.save() python-django-mptt-0.8.5/tests/myapp/urls.py000066400000000000000000000002461275105100200211110ustar00rootroot00000000000000from django.conf.urls import include, url from django.contrib import admin admin.autodiscover() urlpatterns = [ url(r'^admin/', include(admin.site.urls)), ] python-django-mptt-0.8.5/tests/mydatabase000066400000000000000000022500001275105100200204560ustar00rootroot00000000000000SQLite format 3@ ~- P MP G-5I#indexsqlite_autoindex_myapp_genre_1myapp_genre!!ytablemyapp_itemmyapp_itemCREATE TABLE "myapp_item" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "category_fk_id" varchar(50) REFERENCES "myapp_category" ("category_uuid"), "category_pk_id" integer REFERENCES "myapp_category" ("id") )m))tablemyapp_categorymyapp_categoryCREATE TABLE "myapp_category" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "parent_id" integer, "category_uuid" varchar(50) UNIQUE, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL );O)indexsqlite_autoindex_myapp_category_1myapp_categoryD))Ctabledjango_sessiondjango_sessionCREATE TABLE "django_session" ( "session_key" varchar(40) NOT NULL PRIMARY KEY, "session_data" text NOT NULL, "expire_date" datetime NOT NULL );O)indexsqlite_autoindex_django_session_1django_session33Ctabledjango_content_typedjango_content_typeCREATE TABLE "django_content_type" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "app_label" varchar(100) NOT NULL, "model" varchar(100) NOT NULL, UNIQUE ("app_label", "model") )EY3indexsqlite_autoindex_django_content_type_1django_content_typeJ ctableauth_userauth_user CREATE TABLE "auth_user" ( "id" integer NOT NULL PRIMARY KEY, "username" varchar(30) NOT NULL UNIQUE, "first_name" varchar(30) NOT NULL, "last_name" varchar(30) NOT NULL, "email" varchar(75) NOT NULL, "password" varchar(128) NOT NULL, "is_staff" bool NOT NULL, "is_active" bool NOT NULL, "is_superuser" bool NOT NULL, "last_login" datetime NOT NULL, "date_joined" datetime NOT NULL )1 Eindexsqlite_autoindex_auth_user_1auth_userz --'tableauth_user_groupsauth_user_groups CREATE TABLE "auth_user_groups" ( "id" integer NOT NULL PRIMARY KEY, "user_id" integer NOT NULL, "group_id" integer NOT NULL REFERENCES "auth_group" ("id"), UNIQUE ("user_id", "group_id") )? S-indexsqlite_autoindex_auth_user_groups_1auth_user_groups 'AAYtableauth_user_user_permissionsauth_user_user_permissions CREATE TABLE "auth_user_user_permissions" ( "id" integer NOT NULL PRIMARY KEY, "user_id" integer NOT NULL, "permission_id" integer NOT NULL REFERENCES "auth_permission" ("id"), UNIQUE ("user_id", "permission_id") )S gAindexsqlite_autoindex_auth_user_user_permissions_1auth_user_user_permissions !!ctableauth_groupauth_groupCREATE TABLE "auth_group" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(80) NOT NULL UNIQUE )3G!indexsqlite_autoindex_auth_group_1auth_group99Utableauth_group_permissionsauth_group_permissionsCREATE TABLE "auth_group_permissions" ( "id" integer NOT NULL PRIMARY KEY, "group_id" integer NOT NULL, "permission_id" integer NOT NULL REFERENCES "auth_permission" ("id"), UNIQUE ("group_id", "permission_id") )K_9indexsqlite_autoindex_auth_group_permissions_1auth_group_permissions++Stableauth_permissionauth_permissionCREATE TABLE "auth_permission" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "content_type_id" integer NOT NULL, "codename" varchar(100) NOT NULL, UNIQUE ("content_type_id", "codename") )=Q+indexsqlite_autoindex_auth_permission_1auth_permissionk-- tabledjango_admin_logdjango_admin_logCREATE TABLE "django_admin_log" ( "id" integer NOT NULL PRIMARY KEY, "action_time" datetime NOT NULL, "user_id" integer NOT NULL, "content_type_id" integer, "object_id" text, "object_repr" varchar(200) NOT NULL, "action_flag" smallint unsigned NOT NULL, "cxz\f g ?  g I %  [ 5 p N  e C rL&Rs.Sx-Rn](9QE;Can delete concrete concretedelete_concreteconcrete9PE;Can change concrete concretechange_concreteconcrete3O?5Can add concrete concreteadd_concreteconcrete[Ni[Can delete concrete abstract concrete abstractdelete_concreteabstractconcreteabstract[Mi[Can change concrete abstract concrete abstractchange_concreteabstractconcreteabstractULcUCan add concrete abstract concrete abstractadd_concreteabstractconcreteabstract3K?5Can delete concrete modeldelete_concretemodel3J?5Can change concrete modelchange_concretemodel-I9/Can add concrete modeladd_concretemodelIHWICan delete multi table inheritance b2delete_multitableinheritanceb2IGWICan change multi table inheritance b2change_multitableinheritanceb2CFQCCan add multi table inheritance b2add_multitableinheritanceb2IEWICan delete multi table inheritance b1delete_multitableinheritanceb1IDWICan change multi table inheritance b1change_multitableinheritanceb1CCQCCan add multi table inheritance b1add_multitableinheritanceb1IBWICan delete multi table inheritance a2delete_multitableinheritancea2IAWICan change multi table inheritance a2change_multitableinheritancea2C@QCCan add multi table inheritance a2add_multitableinheritancea2I?WICan delete multi table inheritance a1delete_multitableinheritancea1I>WICan change multi table inheritance a1change_multitableinheritancea1C=QCCan add multi table inheritance a1add_multitableinheritancea19<E;Can delete referencing modeldelete_referencingmodel9;E;Can change referencing modelchange_referencingmodel3:?5Can add referencing modeladd_referencingmodel29?3Can delete custom pk namedelete_custompkname28?3Can change custom pk namechange_custompkname,79-Can add custom pk nameadd_custompkname&61)Can delete studentdelete_student&51)Can change studentchange_student 4+#Can add studentadd_student$3/'Can delete persondelete_person$2/'Can change personchange_person1)!Can add personadd_person;0I;Can delete new style mptt metadelete_newstylempttmeta;/I;Can change new style mptt metachange_newstylempttmeta5.C5Can add new style mptt metaadd_newstylempttmeta -+#Can delete treedelete_tree ,+#Can change treechange_tree+%Can add treeadd_tree9*E;Can delete ordered insertiondelete_orderedinsertion9)E;Can change ordered insertionchange_orderedinsertion3(?5Can add ordered insertionadd_orderedinsertion '+#Can delete node delete_node &+#Can change node change_node%%Can add node add_node-$9/Can delete multi order delete_multiorder-#9/Can change multi order change_multiorder'"3)Can add multi order add_multiorder$!/'Can delete insert delete_insert$ /'Can change insert change_insert)!Can add insert add_insert +#Can delete game delete_game +#Can change game change_game%Can add game add_game"-%Can delete genre delete_genre"-%Can change genre change_genre'Can add genre add_genre +#Can delete itemdelete_item +#Can change itemchange_item%Can add itemadd_item(3+Can delete categorydelete_category(3+Can change categorychange_category"-%Can add categoryadd_category&1)Can delete sessiondelete_session&1)Can change sessionchange_session +#Can add sessionadd_session/;1Can delete content typedelete_contenttype/;1Can change content typechange_contenttype) 5+Can add content typeadd_contenttype +#Can delete userdelete_user +#Can change userchange_user %Can add useradd_user" -%Can delete groupdelete_group"-%Can change groupchange_group'Can add groupadd_group,7/Can delete permissiondelete_permission,7/Can change permissionchange_permission&1)Can add permissionadd_permission(5 +Can delete log entrydelete_logentry(5 +Can change log entrychange_logentry"/ %Can add log entryadd_log4Q ]p]J;)jTE3!  j R : +   } b D &   f K -  \ 7  _ G , oQ3eB%delete_group]%change_group\add_group["Edelete_autonowdatefieldmodelZ"Echange_autonowdatefieldmodelY?add_autonowdatefieldmodelX;delete_doubleproxymodelW;change_doubleproxymodelV5add_doubleproxymodelU;delete_singleproxymodelT;change_singleproxymodelS5add_singleproxymodelR;delete_concreteconcreteQ;change_concreteconcreteP5add_concreteconcreteO-[delete_concreteabstractconcreteabstractN-[change_concreteabstractconcreteabstractM*Uadd_concreteabstractconcreteabstractL5delete_concretemodelK5change_concretemodelJ/add_concretemodelI$Idelete_multitableinheritanceb2H$Ichange_multitableinheritanceb2G!Cadd_multitableinheritanceb2F$Idelete_multitableinheritanceb1E$Ichange_multitableinheritanceb1D!Cadd_multitableinheritanceb1C$Idelete_multitableinheritancea2B$Ichange_multitableinheritancea2A!Cadd_multitableinheritancea2@$Idelete_multitableinheritancea1?$Ichange_multitableinheritancea1>!Cadd_multitableinheritancea1=;delete_referencingmodel<;change_referencingmodel;5add_referencingmodel:3delete_custompkname93change_custompkname8-add_custompkname7)delete_student6)change_student5#add_student4'delete_person3'change_person2!add_person1;delete_newstylempttmeta0;change_newstylempttmeta/5add_newstylempttmeta.#delete_tree-#change_tree,add_tree+;delete_orderedinsertion*;change_orderedinsertion)5add_orderedinsertion(# delete_node'# change_node& add_node%/ delete_multiorder$/ change_multiorder#) add_multiorder"' delete_insert!' change_insert ! add_insert# delete_game# change_game add_game% delete_genre% change_genre add_genre#delete_item#change_itemadd_item+delete_category+change_category%add_category)delete_session)change_session#add_session1delete_contenttype1change_contenttype+add_contenttype #delete_user #change_user add_user %delete_group %change_groupadd_group/delete_permission/change_permission)add_permission +delete_logentry +change_logentry % add_logentry            vrU9%m>& | ?  O " v,1-single proxy modelmyappsingleproxymodel,1-double proxy modelmyappdoubleproxymodelgroupmyappgroup8?7auto now date field modelmyappautonowdatefieldmodel+/-concrete concretemyappconcreteconcreteMSMconcrete abstract concrete abstractmyappconcreteabstractconcreteabstract%)'concrete modelmyappconcretemodel;A;multi table inheritance b2myappmultitableinheritanceb2;A;multi table inheritance b1myappmultitableinheritanceb1;A;multi table inheritance a2myappmultitableinheritancea2;A;multi table inheritance a1myappmultitableinheritancea1+/-referencing modelmyappreferencingmodel$)%custom pk namemyappcustompknamestudentmyappstudentpersonmyappperson-3-new style mptt metamyappnewstylempttmetatreemyapptree+/-ordered insertionmyapporderedinsertion nodemyappnode #!multi ordermyappmultiorder insertmyappinsert gamemyappgame genremyappgenreitemmyappitemcategorymyappcategorysessionsessionssession(%%#content typecontenttypescontenttypeuserauthusergroupauthgroup!!permissionauthpermissionlog entryadminlogentry  / ux  JJY e9i$Y7 { /-myappsingleproxymodel-myappdoubleproxymodelmyappgroup7myappautonowdatefieldmodel-myappconcreteconcrete*Mmyappconcreteabstractconcreteabstract'myappconcretemodel!;myappmultitableinheritanceb2!;myappmultitableinheritanceb1!;myappmultitableinheritancea2!;myappmultitableinheritancea1-myappreferencingmodel%myappcustompknamemyappstudentmyappperson-myappnewstylempttmetamyapptree-myapporderedinsertionmyappnode !myappmultiorder myappinsert myappgame myappgenre myappitemmyappcategorysessionssession%#contenttypescontenttype authuserauthgroup!authpermission adminlogentry      sI % Tactical RPG  ! Action RPG  / Role-playing Game*I Horizontal Scrolling Shootemup (E Vertical Scrolling Shootemup   Shootemup ' 4D Platformer' 3D Platformer' 2D Platformer!  Platformer   Action ):_I)%Tactical RPG !Action RPG /Role-playing Game "IHorizontal Scrolling Shootemup EVertical Scrolling Shootemup Shootemup'4D Platformer'3D Platformer'2D Platformer!Platformer  Action KK )v:d  OS55I#indexsqlite_autoindex_myapp_genre_1myapp_genrek-- tabledjango_admin_logdjango_admin_logCREATE TABLE "django_admin_log" ( "id" integer NOT NULL PRIMARY KEY, "action_time" datetime NOT NULL, "user_id" integer NOT NULL, "content_type_id" integer, "object_id" text, "object_repr" varchar(200) NOT NULLk-- tabledjango_admin_logdjango_admin_logCREATE TABLE "django_admin_log" ( "id" integer NOT NULL PRIMARY KEY, "action_time" datetime NOT NULL, "user_id" integer NOT NULL, "content_type_id" integer, "object_id" text, "object_repr" varchar(200) NOT NULL, "action_flag" smallint unsigned NOT NULL, "change_message" text NOT NULL )++Stableauth_permissionauth_permissionCREATE TABLE "auth_permission" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "content_type_id" integer NOT NULL, "codename" varchar(100) NOT NULL, UNIQUE ("content_type_id", "codename") )=Q+indexsqlite_autoindex_auth_permission_1auth_permission99Utableauth_group_permissionsauth_group_permissionsCREATE TABLE "auth_group_permissions" ( "id" integer NOT NULL PRIMARY KEY, "group_id" integer NOT NULL, "permission_id" integer NOT NULL REFERENCES "auth_permission" ("id"), UNIQUE ("group_id", "permission_id") )K_9indexsqlite_autoindex_auth_group_permissions_1auth_group_permissions !!ctableauth_groupauth_groupCREATE TABLE "auth_group" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(80) NOT NULL UNIQUE )3G!indexsqlite_autoindex_auth_group_1auth_group'AAYtableauth_user_user_permissionsauth_user_user_permissions CREATE TABLE "auth_user_user_permissions" ( "id" integer NOT NULL PRIMARY KEY, "user_id" integer NOT NULL, "permission_id" integer NOT NULL REFERENCES "auth_permission" ("id"), UNIQUE ("user_id", "permission_id") )S gAindexsqlite_autoindex_auth_user_user_permissions_1auth_user_user_permissions z --'tableauth_user_groupsauth_user_groups CREATE TABLE "auth_user_groups" ( "id" integer NOT NULL PRIMARY KEY, "user_id" integer NOT NULL, "group_id" integer NOT NULL REFERENCES "auth_group" ("id"), UNIQUE ("user_id", "group_id") )? S-indexsqlite_autoindex_auth_user_groups_1auth_user_groups J ctableauth_userauth_user CREATE TABLE "auth_user" ( "id" integer NOT NULL PRIMARY KEY, "username" varchar(30) NOT NULL UNIQUE, "first_name" varchar(30) NOT NULL, "last_name" varchar(30) NOT NULL, "email" varchar(75) NOT NULL, "password" varchar(128) NOT NULL, "is_staff" bool NOT NULL, "is_active" bool NOT NULL, "is_superuser" bool NOT NULL, "last_login" datetime NOT NULL, "date_joined" datetime NOT NULL )1 Eindexsqlite_autoindex_auth_user_1auth_user33Ctabledjango_content_typedjango_content_typeCREATE TABLE "django_content_type" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "app_label" varchar(100) NOT NULL, "model" varchar(100) NOT NULL, UNIQUE ("app_label", "model") )EY3indexsqlite_autoindex_django_content_type_1django_content_typeD))Ctabledjango_sessiondjango_sessionCREATE TABLE "django_session" ( "session_key" varchar(40) NOT NULL PRIMARY KEY, "session_data" text NOT NULL, "expire_date" datetime NOT NULL );O)indexsqlite_autoindex_django_session_1django_sessionm))tablemyapp_categorymyapp_categoryCREATE TABLE "myapp_category" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "parent_id" integer, "category_uuid" varchar(50) UNIQUE, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL );O)indexsqlite_autoindex_myapp_category_1myapp_category -dq 4 C C(c"11qtablemyapp_custoC##Mtablemyapp_genremyapp_gen!!ytablemyapp_itemmyapp_itemCREATE TABLE "myapp_item" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "category_fk_id" varchar(50) REFERENCES "myapp_category" ("category_uuid"), "category_pk_id" integer REFERENCES "myapp_category" ("id") )C##Mtablemyapp_genremyapp_genreCREATE TABLE "myapp_genre" ( "id" !!ytablemyapp_itemmyapp_itemCREATE TABLE "myapp_item" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "category_fk_id" varchar(50) REFERENCES "myapp_category" ("category_uuid"), "category_pk_id" integer REFERENCES "myapp_category" ("id") )C##Mtablemyapp_genremyapp_genreCREATE TABLE "myapp_genre" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL UNIQUE, "parent_id" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL )5I#indexsqlite_autoindex_myapp_genre_1myapp_genre 773tablemyapp_game_genres_m2mmyapp_game_genres_m2mCREATE TABLE "myapp_game_genres_m2m" ( "id" integer NOT NULL PRIMARY KEY, "game_id" integer NOT NULL, "genre_id" integer NOT NULL REFERENCES "myapp_genre" ("id"), UNIQUE ("game_id", "genre_id") )I]7indexsqlite_autoindex_myapp_game_genres_m2m_1myapp_game_genres_m2mF!!Wtablemyapp_gamemyapp_gameCREATE TABLE "myapp_game" ( "id" integer NOT NULL PRIMARY KEY, "genre_id" integer NOT NULL REFERENCES "myapp_genre" ("id"), "name" varchar(50) NOT NULL )%%tablemyapp_insertmyapp_insertCREATE TABLE "myapp_insert" ( "id" integer NOT NULL PRIMARY KEY, "parent_id" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL ) --Itablemyapp_multiordermyapp_multiorderCREATE TABLE "myapp_multiorder" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "size" integer unsigned NOT NULL, "date" date NOT NULL, "parent_id" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL ) !!ctablemyapp_nodemyapp_nodeCREATE TABLE "myapp_node" ( "id" integer NOT NULL PRIMARY KEY, "parent_id" integer, "level" integer NOT NULL, "does" integer unsigned NOT NULL, "zis" integer unsigned NOT NULL, "work" integer unsigned NOT NULL )]99Utablemyapp_orderedinsertionmyapp_orderedinsertion CREATE TABLE "myapp_orderedinsertion" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "parent_id" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL )!!{tablemyapp_treemyapp_tree!CREATE TABLE "myapp_tree" ( "id" integer NOT NULL PRIMARY KEY, "parent_id" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL )@99tablemyapp_newstylempttmetamyapp_newstylempttmeta"CREATE TABLE "myapp_newstylempttmeta" ( "id" integer NOT NULL PRIMARY KEY, "parent_id" integer, "testing" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL )? %%Atablemyapp_personmyapp_person#CREATE TABLE "myapp_person" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "parent_id" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL ):!''3tablemyapp_studentmyapp_student$CREATE TABLE "myapp_student" ( "person_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "myapp_person" ("id"), "type" varchar(50) NOT NULL )               g[  ! U}d:#AAtablemyapp_referencingmodel_m2mmyapp_referencingmodel_m2m&CREAc"11qtablemyapp_custompknamemyapp_custompkname%CREATE TABLE "myapp_custompkname" ( "my_custom_name" integer NOT NULL PRIMAc"11qtablemyapp_custompknamemyapp_custompkname%CREATE TABLE "myapp_custompkname" ( "my_custom_name" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "my_cusom_parent" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL ):#AAtablemyapp_referencingmodel_m2mmyapp_referencingmodel_m2m&CREATE TABLE "myapp_referencingmodel_m2m" ( "id" integer NOT NULL PRIMARY KEY, "referencingmodel_id" integer NOT NULL, "category_id" integer NOT NULL REFERENCES "myapp_category" ("id"), UNIQUE ("referencingmodel_id", "category_id") )S$gAindexsqlite_autoindex_myapp_referencingmodel_m2m_1myapp_referencingmodel_m2m'%99?tablemyapp_referencingmodelmyapp_referencingmodel)CREATE TABLE "myapp_referencingmodel" ( "id" integer NOT NULL PRIMARY KEY, "fk_id" integer NOT NULL REFERENCES "myapp_category" ("id"), "one_id" integer NOT NULL UNIQUE REFERENCES "myapp_category" ("id") )K&_9indexsqlite_autoindex_myapp_referencingmodel_1myapp_referencingmodel*Q'GG!tablemyapp_multitableinheritancea1myapp_multitableinheritancea1+CREATE TABLE "myapp_multitableinheritancea1" ( "id" integer NOT NULL PRIMARY KEY, "parent_id" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL ) (GGtablemyapp_multitableinheritancea2myapp_multitableinheritancea2,CREATE TABLE "myapp_multitableinheritancea2" ( "multitableinheritancea1_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "myapp_multitableinheritancea1" ("id"), "name" varchar(50) NOT NULL )Y)GG1tablemyapp_multitableinheritanceb1myapp_multitableinheritanceb1-CREATE TABLE "myapp_multitableinheritanceb1" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(50) NOT NULL, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL )*GGtablemyapp_multitableinheritanceb2myapp_multitableinheritanceb2.CREATE TABLE "myapp_multitableinheritanceb2" ( "multitableinheritanceb1_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "myapp_multitableinheritanceb1" ("id"), "parent_id" integer )w+33tablemyapp_concretemodelmyapp_concretemodel/CREATE TABLE "myapp_concretemodel" ( "id" integer NOT NULL PRIMARY KEY, "parent_id" integer, "ghosts" varchar(50) NOT NULL, "name" varchar(50) NOT NULL, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL )r,YY?tablemyapp_concreteabstractconcreteabstractmyapp_concreteabstractconcreteabstract0CREATE TABLE "myapp_concreteabstractconcreteabstract" ( "concretemodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "myapp_concretemodel" ("id") )B-99tablemyapp_concreteconcretemyapp_concreteconcrete1CREATE TABLE "myapp_concreteconcrete" ( "concretemodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "myapp_concretemodel" ("id") )h.CCWtablemyapp_autonowdatefieldmodelmyapp_autonowdatefieldmodel2CREATE TABLE "myapp_autonowdatefieldmodel" ( "id" integer NOT NULL PRIMARY KEY, "parent_id" integer, "now" datetime NOT NULL, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL )=/##Atablemyapp_groupmyapp_group3CREATE TABLE "myapp_group" ( "id" integer NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "parent_id" integer, "lft" integer unsigned NOT NULL, "rght" integer unsigned NOT NULL, "tree_id" integer unsigned NOT NULL, "level" integer unsigned NOT NULL )            Q`2`> g ?  g I %  [ 5 p N  e C rL&Rs.Sx-Rn](9QE;Can delete concrete concretedelete_concreteconcrete9PE;Can change concrete concretechange_concreteconcrete3O?5Can add concrete concreteadd_concreteconcrete[Ni[Can delete concrete abstract concrete abstractdelete_concreteabstractconcreteabstract[Mi[Can change concrete abstract concrete abstractchange_concreteabstractconcreteabstractULcUCan add concrete abstract concrete abstractadd_concreteabstractconcreteabstract3K?5Can delete concrete modeldelete_concretemodel3J?5Can change concrete modelchange_concretemodel-I9/Can add concrete modeladd_concretemodelIHWICan delete multi table inheritance b2delete_multitableinheritanceb2IGWICan change multi table inheritance b2change_multitableinheritanceb2CFQCCan add multi table inheritance b2add_multitableinheritanceb2IEWICan delete multi table inheritance b1delete_multitableinheritanceb1IDWICan change multi table inheritance b1change_multitableinheritanceb1CCQCCan add multi table inheritance b1add_multitableinheritanceb1IBWICan delete multi table inheritance a2delete_multitableinheritancea2IAWICan change multi table inheritance a2change_multitableinheritancea2C@QCCan add multi table inheritance a2add_multitableinheritancea2I?WICan delete multi table inheritance a1delete_multitableinheritancea1I>WICan change multi table inheritance a1change_multitableinheritancea1C=QCCan add multi table inheritance a1add_multitableinheritancea19<E;Can delete referencing modeldelete_referencingmodel9;E;Can change referencing modelchange_referencingmodel3:?5Can add referencing modeladd_referencingmodel29?3Can delete custom pk namedelete_custompkname28?3Can change custom pk namechange_custompkname,79-Can add custom pk nameadd_custompkname&61)Can delete studentdelete_student&51)Can change studentchange_student 4+#Can add studentadd_student$3/'Can delete persondelete_person$2/'Can change personchange_person1)!Can add personadd_person;0I;Can delete new style mptt metadelete_newstylempttmeta;/I;Can change new style mptt metachange_newstylempttmeta5.C5Can add new style mptt metaadd_newstylempttmeta -+#Can delete treedelete_tree ,+#Can change treechange_tree+%Can add treeadd_tree9*E;Can delete ordered insertiondelete_orderedinsertion9)E;Can change ordered insertionchange_orderedinsertion3(?5Can add ordered insertionadd_orderedinsertion '+#Can delete node delete_node &+#Can change node change_node%%Can add node add_node-$9/Can delete multi order delete_multiorder-#9/Can change multi order change_multiorder'"3)Can add multi order add_multiorder$!/'Can delete insert delete_insert$ /'Can change insert change_insert)!Can add insert add_insert +#Can delete game delete_game +#Can change game change_game%Can add game add_game"-%Can delete genre delete_genre"-%Can change genre change_genre'Can add genre add_genre +#Can delete itemdelete_item +#Can change itemchange_item%Can add itemadd_item(3+Can delete categorydelete_category(3+Can change categorychange_category"-%Can add categoryadd_category&1)Can delete sessiondelete_session&1)Can change sessionchange_session +#Can add sessionadd_session/;1Can delete content typedelete_contenttype/;1Can change content typechange_contenttype) 5+Can add content typeadd_contenttype +#Can delete userdelete_user +#Can change userchange_user %Can add useradd_user" -%Can delete groupdelete_group"-%Can change groupchange_group'Can add groupadd_group,7/Can delete permissiondelete_permission,7/Can change permissionchange_permission&1)Can add permissionadd_permission(5 +Can delete log entrydelete_logentry(5 +Can change log entrychange_logentry"/ %Can add log entryadd_logentry lRb l"]-%Can delete groupdelete_group"\-%Can change groupchange_group['Can add groupadd_groupFZUECan delete auto now date field modeldelete_autonowdatefieldmodelFYUECan change auto now date field modelchange_autonowdatefieldmodel@XO?Can add auto now date field modeladd_autonowdatefieldmodel:WG;Can delete double proxy modeldelete_doubleproxymodel:VG;Can change double proxy modelchange_doubleproxymodel4UA5Can add double proxy modeladd_doubleproxymodel:TG;Can delete single proxy modeldelete_singleproxymodel:SG;Can change single proxy modelchange_singleproxymodel4RA5Can add single proxy modeladd_singleproxymodel   ] ztnhb\VPJD>82,& ~xrlf`ZTNHB<60*$  ]\[ZYXQPONMLWVUTSRKJIHGFEDCBA@?>=<;:9876543210/.-,+*)( ' & % $ # " !                   74;5 "  f ;  D# {djS3! indexmyapp_node_63f17a16myapp_nodeZCREATE INDEX "m0?-!indexdjango_admin_log_fbfc09f1django_admin_log6CREATE INDEX "django_admin_log_fbfc09f1" ON "django_admin_log" ("user_id")1?-1indexdjango_admin_log_e4470c6edjango_admin_log7CREATE INDEX "django_admin_log_e4470c6e" ON "django_admin_log" ("content_type_id")2=+-indexauth_permission_e4470c6eauth_permission8CREATE INDEX "auth_permission_e4470?-!indexdjango_admin_log_fbfc09f1django_admin_log6CREATE INDEX "django_admin_log_fbfc09f1" ON "django_admin_log" ("user_id")1?-1indexdjango_admin_log_e4470c6edjango_admin_log7CREATE INDEX "django_admin_log_e4470c6e" ON "django_admin_log" ("content_type_id")2=+-indexauth_permission_e4470c6eauth_permission8CREATE INDEX "auth_permission_e4470c6e" ON "auth_permission" ("content_type_id")3K9;indexauth_group_permissions_bda51c3cauth_group_permissions9CREATE INDEX "auth_group_permissions_bda51c3c" ON "auth_group_permissions" ("group_id")4K9Eindexauth_group_permissions_1e014c8fauth_group_permissions:CREATE INDEX "auth_group_permissions_1e014c8f" ON "auth_group_permissions" ("permission_id")(5SAIindexauth_user_user_permissions_fbfc09f1auth_user_user_permissions;CREATE INDEX "auth_user_user_permissions_fbfc09f1" ON "auth_user_user_permissions" ("user_id").6SAUindexauth_user_user_permissions_1e014c8fauth_user_user_permissions=CREATE INDEX "auth_user_user_permissions_1e014c8f" ON "auth_user_user_permissions" ("permission_id")7?-!indexauth_user_groups_fbfc09f1auth_user_groups>CREATE INDEX "auth_user_groups_fbfc09f1" ON "auth_user_groups" ("user_id")8?-#indexauth_user_groups_bda51c3cauth_user_groups?CREATE INDEX "auth_user_groups_bda51c3c" ON "auth_user_groups" ("group_id")|9;)!indexdjango_session_c25c2c28django_session@CREATE INDEX "django_session_c25c2c28" ON "django_session" ("expire_date")z:;)indexmyapp_category_63f17a16myapp_categoryACREATE INDEX "myapp_category_63f17a16" ON "myapp_category" ("parent_id")t;;)indexmyapp_category_42b06ff6myapp_categoryBCREATE INDEX "myapp_category_42b06ff6" ON "myapp_category" ("lft")u<;)indexmyapp_category_91543e5amyapp_categoryCCREATE INDEX "myapp_category_91543e5a" ON "myapp_category" ("rght")x=;)indexmyapp_category_efd07f28myapp_categoryDCREATE INDEX "myapp_category_efd07f28" ON "myapp_category" ("tree_id")v>;)indexmyapp_category_2a8f42e8myapp_categoryECREATE INDEX "myapp_category_2a8f42e8" ON "myapp_category" ("level")o?3!indexmyapp_item_1d121359myapp_itemFCREATE INDEX "myapp_item_1d121359" ON "myapp_item" ("category_fk_id")o@3!indexmyapp_item_dfe38ac1myapp_itemGCREATE INDEX "myapp_item_dfe38ac1" ON "myapp_item" ("category_pk_id")nA5#indexmyapp_genre_63f17a16myapp_genreHCREATE INDEX "myapp_genre_63f17a16" ON "myapp_genre" ("parent_id")hB5#indexmyapp_genre_42b06ff6myapp_genreICREATE INDEX "myapp_genre_42b06ff6" ON "myapp_genre" ("lft")iC5#indexmyapp_genre_91543e5amyapp_genreJCREATE INDEX "myapp_genre_91543e5a" ON "myapp_genre" ("rght")lD5# indexmyapp_genre_efd07f28myapp_genreKCREATE INDEX "myapp_genre_efd07f28" ON "myapp_genre" ("tree_id")jE5# indexmyapp_genre_2a8f42e8myapp_genreLCREATE INDEX "myapp_genre_2a8f42e8" ON "myapp_genre" ("level")FI75indexmyapp_game_genres_m2m_7b333d1emyapp_game_genres_m2mMCREATE INDEX "myapp_game_genres_m2m_7b333d1e" ON "myapp_game_genres_m2m" ("game_id")GI77indexmyapp_game_genres_m2m_f8d711d0myapp_game_genres_m2mNCREATE INDEX "myapp_game_genres_m2m_f8d711d0" ON "myapp_game_genres_m2m" ("genre_id")iH3! indexmyapp_game_f8d711d0myapp_gameOCREATE INDEX "myapp_game_f8d711d0" ON "myapp_game" ("genre_id")rI7%indexmyapp_insert_63f17a16myapp_insertPCREATE INDEX "myapp_insert_63f17a16" ON "myapp_insert" ("parent_id")lJ7% indexmyapp_insert_42b06ff6myapp_insertQCREATE INDEX "myapp_insert_42b06ff6" ON "myapp_insert" ("lft")mK7% indexmyapp_insert_91543e5amyapp_insertRCREATE INDEX "myapp_insert_91543e5a" ON "myapp_insert" ("rght")                                                                   xxZ]_K K  Q " ' X ^xJnj7% indexmyapp_person_2a8f42e8myap}P?-inde|O?-indexmyapp_multiorder_42b06ff6myapp_multiorderVCREATE INDEX "myapp_multiorder_42b06ff6" ON "myapp_multiorder" ("lft")}P?-indexmyapp_multiorder_91543e5amyapp_multiorderWCREATE INDEX "myapp_multiorder_91543e5a" ON "myapp_multiorder" ("rght")Q?-!indexmyapp_multiorder_efd07f28myapp_multiorderXCREATE INDEX "myapp_multiorder_efd07f28" ON "myapp_multiorder" ("tree_id")~R?-indexmyapp_multiorder_2a8f42e8myapp_multiorderYCREATE INDEX "myapp_multiorder_2a8f42e8" ON "myapp_multiorder" ("lpL7%indexmyapp_insert_efd07f28myapp_insertSCREATE INDEX "myapp_insert_efd07f28" ON "myapp_insert" ("tree_id")nM7% indexmyapp_insert_2a8f42e8myapp_insertTCREATE INDEX "myapp_insert_2a8f42e8" ON "myapp_insert" ("level")N?-%indexmyapp_multiorder_63f17a16myapp_multiorderUCREATE INDEX "myapp_multiorder_63f17a16" ON "myapp_multiorder" ("parent_id")|O?-indexmyapp_multiorder_42b06ff6myapp_multiorderVCREATE INDEX "myapp_multiorder_42b06ff6" ON "myapp_multiorder" ("lft")}P?-indexmyapp_multiorder_91543e5amyapp_multiorderWCREATE INDEX "myapp_multiorder_91543e5a" ON "myapp_multiorder" ("rght")Q?-!indexmyapp_multiorder_efd07f28myapp_multiorderXCREATE INDEX "myapp_multiorder_efd07f28" ON "myapp_multiorder" ("tree_id")~R?-indexmyapp_multiorder_2a8f42e8myapp_multiorderYCREATE INDEX "myapp_multiorder_2a8f42e8" ON "myapp_multiorder" ("level")jS3! indexmyapp_node_63f17a16myapp_nodeZCREATE INDEX "myapp_node_63f17a16" ON "myapp_node" ("parent_id")eT3!indexmyapp_node_3132601cmyapp_node[CREATE INDEX "myapp_node_3132601c" ON "myapp_node" ("does")dU3!indexmyapp_node_2669bc52myapp_node]CREATE INDEX "myapp_node_2669bc52" ON "myapp_node" ("zis")eV3!indexmyapp_node_d79af30cmyapp_node^CREATE INDEX "myapp_node_d79af30c" ON "myapp_node" ("work")WK9=indexmyapp_orderedinsertion_63f17a16myapp_orderedinsertion_CREATE INDEX "myapp_orderedinsertion_63f17a16" ON "myapp_orderedinsertion" ("parent_id")XK91indexmyapp_orderedinsertion_42b06ff6myapp_orderedinsertion`CREATE INDEX "myapp_orderedinsertion_42b06ff6" ON "myapp_orderedinsertion" ("lft")YK93indexmyapp_orderedinsertion_91543e5amyapp_orderedinsertionaCREATE INDEX "myapp_orderedinsertion_91543e5a" ON "myapp_orderedinsertion" ("rght")ZK99indexmyapp_orderedinsertion_efd07f28myapp_orderedinsertionbCREATE INDEX "myapp_orderedinsertion_efd07f28" ON "myapp_orderedinsertion" ("tree_id")[K95indexmyapp_orderedinsertion_2a8f42e8myapp_orderedinsertioncCREATE INDEX "myapp_orderedinsertion_2a8f42e8" ON "myapp_orderedinsertion" ("level")j\3! indexmyapp_tree_63f17a16myapp_treedCREATE INDEX "myapp_tree_63f17a16" ON "myapp_tree" ("parent_id")d]3!indexmyapp_tree_42b06ff6myapp_treeeCREATE INDEX "myapp_tree_42b06ff6" ON "myapp_tree" ("lft")e^3!indexmyapp_tree_91543e5amyapp_treefCREATE INDEX "myapp_tree_91543e5a" ON "myapp_tree" ("rght")h_3! indexmyapp_tree_efd07f28myapp_treegCREATE INDEX "myapp_tree_efd07f28" ON "myapp_tree" ("tree_id")f`3!indexmyapp_tree_2a8f42e8myapp_treehCREATE INDEX "myapp_tree_2a8f42e8" ON "myapp_tree" ("level")aK9=indexmyapp_newstylempttmeta_63f17a16myapp_newstylempttmetaiCREATE INDEX "myapp_newstylempttmeta_63f17a16" ON "myapp_newstylempttmeta" ("parent_id")bI97indexmyapp_newstylempttmeta_c71e76cmyapp_newstylempttmetajCREATE INDEX "myapp_newstylempttmeta_c71e76c" ON "myapp_newstylempttmeta" ("testing")cK93indexmyapp_newstylempttmeta_91543e5amyapp_newstylempttmetakCREATE INDEX "myapp_newstylempttmeta_91543e5a" ON "myapp_newstylempttmeta" ("rght")dK99indexmyapp_newstylempttmeta_efd07f28myapp_newstylempttmetalCREATE INDEX "myapp_newstylempttmeta_efd07f28" ON "myapp_newstylempttmeta" ("tree_id")eK95indexmyapp_newstylempttmeta_2a8f42e8myapp_newstylempttmetamCREATE INDEX "myapp_newstylempttmeta_2a8f42e8" ON "myapp_newstylempttmeta" ("level")rf7%indexmyapp_person_63f17a16myapp_personnCREATE INDEX "myapp_person_63f17a16" ON "myapp_person" ("parent_id")                            L-0?S R r ) H REh-UCMindexmyapp_autonowdatefieldmodel_efd07f28myapp_autonowdatefieldmodelCREATE INDEX "myapp_autonowdatefieldmodel_efd07f28" ON "myapp_autonowdatefieldmodel" ("tree_id")nC1)indexmyapp_custompkname_efd07f28myapp_custompknamevCREATE INDEX "myapp_custompkname_efd07f28" ON "myapp_custompkname" ("tree_id")oC1%indexmyapp_custompkname_2a8f42e8myapp_custompknamewCREATE INDEX "myapp_custompkname_2a8f42e8" ON "myapp_custompkname" ("level")4pSAaindexmyapp_referencingmodel_m2m_99486f13myapp_referencingmodel_m2myCREATE INDEX "myapp_referencingmodel_m2m_99486f13" ON "myapp_referencingmodel_m2m" ("referencingmodel_id"),qSAQindexmyapp_referencingmodel_m2m_42dc49bcmyapp_referencingmodel_m2mzCREATE INDEX "myapp_referencingmodel_m2m_42dc49bc" ON "myapp_referencingmodel_m2m" ("category_id")rK95indexmyapp_referencingmodel_37789993myapp_referencingmodel{CREATE INDEX "myapp_referencingmodel_3778lg7% indexmyapp_person_42b06ff6myapp_personoCREATE INDEX "myapp_person_42b06ff6" ON "myapp_person" ("lft")mh7% indexmyapp_person_91543e5amyapp_personpCREATE INDEX "myapp_person_91543e5a" ON "myapp_person" ("rght")pi7%indexmyapp_person_efd07f28myapp_personqCREATE INDEX "myapp_person_efd07f28" ON "myapp_person" ("tree_id")nj7% indexmyapp_person_2a8f42e8myapp_personrCREATE INDEX "myapp_person_2a8f42e8" ON "myapp_person" ("level")kC19indexmyapp_custompkname_b29d415bmyapp_custompknamesCREATE INDEX "myapp_custompkname_b29d415b" ON "myapp_custompkname" ("my_cusom_parent")lC1!indexmyapp_custompkname_42b06ff6myapp_custompknametCREATE INDEX "myapp_custompkname_42b06ff6" ON "myapp_custompkname" ("lft")mC1#indexmyapp_custompkname_91543e5amyapp_custompknameuCREATE INDEX "myapp_custompkname_91543e5a" ON "myapp_custompkname" ("rght")nC1)indexmyapp_custompkname_efd07f28myapp_custompknamevCREATE INDEX "myapp_custompkname_efd07f28" ON "myapp_custompkname" ("tree_id")oC1%indexmyapp_custompkname_2a8f42e8myapp_custompknamewCREATE INDEX "myapp_custompkname_2a8f42e8" ON "myapp_custompkname" ("level")4pSAaindexmyapp_referencingmodel_m2m_99486f13myapp_referencingmodel_m2myCREATE INDEX "myapp_referencingmodel_m2m_99486f13" ON "myapp_referencingmodel_m2m" ("referencingmodel_id"),qSAQindexmyapp_referencingmodel_m2m_42dc49bcmyapp_referencingmodel_m2mzCREATE INDEX "myapp_referencingmodel_m2m_42dc49bc" ON "myapp_referencingmodel_m2m" ("category_id")rK95indexmyapp_referencingmodel_37789993myapp_referencingmodel{CREATE INDEX "myapp_referencingmodel_37789993" ON "myapp_referencingmodel" ("fk_id")6sYGYindexmyapp_multitableinheritancea1_63f17a16myapp_multitableinheritancea1|CREATE INDEX "myapp_multitableinheritancea1_63f17a16" ON "myapp_multitableinheritancea1" ("parent_id")0tYGMindexmyapp_multitableinheritancea1_42b06ff6myapp_multitableinheritancea1}CREATE INDEX "myapp_multitableinheritancea1_42b06ff6" ON "myapp_multitableinheritancea1" ("lft")1uYGOindexmyapp_multitableinheritancea1_91543e5amyapp_multitableinheritancea1~CREATE INDEX "myapp_multitableinheritancea1_91543e5a" ON "myapp_multitableinheritancea1" ("rght")4vYGUindexmyapp_multitableinheritancea1_efd07f28myapp_multitableinheritancea1CREATE INDEX "myapp_multitableinheritancea1_efd07f28" ON "myapp_multitableinheritancea1" ("tree_id")3wYGQindexmyapp_multitableinheritancea1_2a8f42e8myapp_multitableinheritancea1CREATE INDEX "myapp_multitableinheritancea1_2a8f42e8" ON "myapp_multitableinheritancea1" ("level")1xYGMindexmyapp_multitableinheritanceb1_42b06ff6myapp_multitableinheritanceb1CREATE INDEX "myapp_multitableinheritanceb1_42b06ff6" ON "myapp_multitableinheritanceb1" ("lft")2yYGOindexmyapp_multitableinheritanceb1_91543e5amyapp_multitableinheritanceb1CREATE INDEX "myapp_multitableinheritanceb1_91543e5a" ON "myapp_multitableinheritanceb1" ("rght")5zYGUindexmyapp_multitableinheritanceb1_efd07f28myapp_multitableinheritanceb1CREATE INDEX "myapp_multitableinheritanceb1_efd07f28" ON "myapp_multitableinheritanceb1" ("tree_id")                        .W Y r  E Qy .k 5# indexmyapp_group_2a8f42e8myapp_groupCREATE INDEX "myapp_group_2a8f42e8" ON "myapp_group" ("level")m 5# indexmyapp_group_efd07f28myapp_groupCREATE INDEX "myapp_group_efd07f28" ON "myapp_group" ("tree_id")j 5#indexmyapp_group_91543e5amyapp_groupCREATE INDEX "myapp_group_91543e5a" ON "myapp_group" ("rght")i5#indexmyapp_group_42b06ff6myapp_groupCREATE INDEX "myapp_group_42b06ff6" ON "myapp_group" ("lft")o5#indexmyapp_group_63f17a16myapp_groupCREATE INDEX "myapp_group_63f17a16" ON "myapp_group" ("parent_id")3{YGQindexmyapp_multitableinheritanceb1_2a8f42e8myapp_multitableinheritanceb1CREATE INDEX "myapp_multitableinheritanceb1_2a8f42e8" ON "myapp_multitableinheritanceb1" ("level")7|YGYindexmyapp_multitableinheritanceb2_63f17a16myapp_multitableinheritanceb2CREATE INDEX "myapp_multitableinheritanceb2_63f17a16" ON "myapp_multitableinheritanceb2" ("parent_id")}E31indexmyapp_concretemodel_63f17a16myapp_concretemodelCREATE INDEX "myapp_concretemodel_63f17a16" ON "myapp_concretemodel" ("parent_id") ~E3%indexmyapp_concretemodel_42b06ff6myapp_concretemodelCREATE INDEX "myapp_concretemodel_42b06ff6" ON "myapp_concretemodel" ("lft") E3'indexmyapp_concretemodel_91543e5amyapp_concretemodelCREATE INDEX "myapp_concretemodel_91543e5a" ON "myapp_concretemodel" ("rght") E3-indexmyapp_concretemodel_efd07f28myapp_concretemodelCREATE INDEX "myapp_concretemodel_efd07f28" ON "myapp_concretemodel" ("tree_id") E3)indexmyapp_concretemodel_2a8f42e8myapp_concretemodelCREATE INDEX "myapp_concretemodel_2a8f42e8" ON "myapp_concretemodel" ("level")/UCQindexmyapp_autonowdatefieldmodel_63f17a16myapp_autonowdatefieldmodelCREATE INDEX "myapp_autonowdatefieldmodel_63f17a16" ON "myapp_autonowdatefieldmodel" ("parent_id"))UCEindexmyapp_autonowdatefieldmodel_42b06ff6myapp_autonowdatefieldmodelCREATE INDEX "myapp_autonowdatefieldmodel_42b06ff6" ON "myapp_autonowdatefieldmodel" ("lft")*UCGindexmyapp_autonowdatefieldmodel_91543e5amyapp_autonowdatefieldmodelCREATE INDEX "myapp_autonowdatefieldmodel_91543e5a" ON "myapp_autonowdatefieldmodel" ("rght")-UCMindexmyapp_autonowdatefieldmodel_efd07f28myapp_autonowdatefieldmodelCREATE INDEX "myapp_autonowdatefieldmodel_efd07f28" ON "myapp_autonowdatefieldmodel" ("tree_id")+UCIindexmyapp_autonowdatefieldmodel_2a8f42e8myapp_autonowdatefieldmodelCREATE INDEX "myapp_autonowdatefieldmodel_2a8f42e8" ON "myapp_autonowdatefieldmodel" ("level")     python-django-mptt-0.8.5/tests/requirements.txt000066400000000000000000000000341275105100200217030ustar00rootroot00000000000000mock-django Django coverage python-django-mptt-0.8.5/tests/runtests.sh000077500000000000000000000007511275105100200206530ustar00rootroot00000000000000#!/bin/sh set -e export PYTHONPATH="./" export DJANGO_SETTINGS_MODULE='settings' if [ `which django-admin.py` ] ; then export DJANGO_ADMIN=`which django-admin.py` else export DJANGO_ADMIN=`which django-admin` fi if [ `which coverage` ] ; then export COVERAGE='coverage run' else export COVERAGE='' fi $COVERAGE $DJANGO_ADMIN test --traceback --settings=$DJANGO_SETTINGS_MODULE --verbosity 2 --pythonpath="../" "$@" if [ `which coverage` ] ; then coverage report fi python-django-mptt-0.8.5/tests/settings.py000066400000000000000000000026641275105100200206440ustar00rootroot00000000000000from __future__ import unicode_literals import os DIRNAME = os.path.dirname(__file__) DEBUG = True DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'mydatabase' } } INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.messages', 'django.contrib.sessions', 'django.contrib.staticfiles', 'mptt', 'myapp', ) STATIC_URL = '/static/' SECRET_KEY = 'abc123' MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', ) TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] ROOT_URLCONF = 'myapp.urls' # Swappable model testing MPTT_SWAPPABLE_MODEL = 'myapp.SwappedInModel'