pax_global_header00006660000000000000000000000064122002774460014516gustar00rootroot0000000000000052 comment=cc92ea6591e25776d0bb50f618b5f3ce94716b2f django-mptt-0.6.0/000077500000000000000000000000001220027744600137455ustar00rootroot00000000000000django-mptt-0.6.0/.gitignore000066400000000000000000000000411220027744600157300ustar00rootroot00000000000000*.pyc *.egg-info build dist .tox django-mptt-0.6.0/.travis.yml000066400000000000000000000004261220027744600160600ustar00rootroot00000000000000language: python python: - "3.3" - "3.2" - "2.7" - "2.6" # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -r requirements.txt # command to run tests, e.g. python setup.py test script: cd tests/ && ./runtests.sh django-mptt-0.6.0/INSTALL000066400000000000000000000007521220027744600150020ustar00rootroot00000000000000Thanks 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.5 or newer - Django 1.2 or newer You can obtain Python from http://www.python.org/ and Django from http://www.djangoproject.com/ django-mptt-0.6.0/LICENSE000066400000000000000000000020771220027744600147600ustar00rootroot00000000000000Django 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. django-mptt-0.6.0/MANIFEST.in000066400000000000000000000003731220027744600155060ustar00rootroot00000000000000include 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 * django-mptt-0.6.0/NOTES000066400000000000000000000452221220027744600145650ustar00rootroot00000000000000============================= 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 django-mptt-0.6.0/README.rst000066400000000000000000000052661220027744600154450ustar00rootroot00000000000000=========== 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 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/devinfo/DBMSTrees/sqltrees.html .. _`Storing Hierarchical Data in a Database`: http://www.sitepoint.com/print/hierarchical-data-database .. _`Managing Hierarchical Data in MySQL`: http://mirror.neu.edu.cn/mysql/tech-resources/articles/hierarchical-data.html 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.6+ (with experimental support for python 3.2+) Django 1.4.2+ 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. django-mptt-0.6.0/docs/000077500000000000000000000000001220027744600146755ustar00rootroot00000000000000django-mptt-0.6.0/docs/Makefile000066400000000000000000000114751220027744600163450ustar00rootroot00000000000000# 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 "renaming sphinx dirs so they're okay for gh-pages" sed -i -s -e 's/"_static\//"static\//g' $(BUILDDIR)/html/*.html mv $(BUILDDIR)/html/_static/ $(BUILDDIR)/html/static sed -i -s -e 's/"_sources\//"sources\//g' $(BUILDDIR)/html/*.html mv $(BUILDDIR)/html/_sources/ $(BUILDDIR)/html/sources @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." django-mptt-0.6.0/docs/admin.rst000066400000000000000000000031371220027744600165230ustar00rootroot00000000000000============= Admin classes ============= .. versionadded:: 0.4 To add admin functionality to your tree model, use one of the ``ModelAdmin`` classes in ``django-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. 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) # speficfy 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" # … FeinCMSModelAdmin ----------------- This requires `FeinCMS`_ to be installed. In addition to enforcing ordering and indenting the nodes, it has: - Javascript expand/collapse for parent nodes - Javascript widgets for moving nodes on the list view page .. _`FeinCMS`: http://www.feincms.org/ django-mptt-0.6.0/docs/autogenerated.rst000066400000000000000000000005171220027744600202610ustar00rootroot00000000000000 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/ django-mptt-0.6.0/docs/conf.py000066400000000000000000000160651220027744600162040ustar00rootroot00000000000000# -*- 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 - 2011, 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) ] django-mptt-0.6.0/docs/forms.rst000066400000000000000000000124621220027744600165620ustar00rootroot00000000000000================================== 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.tree.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.tree.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.tree.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 %} django-mptt-0.6.0/docs/tutorial.rst000066400000000000000000000111621220027744600172730ustar00rootroot00000000000000 ======== 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') 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 your table in the database:: python manage.py syncdb 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/ django-mptt-0.6.0/docs/upgrade.rst000066400000000000000000000144301220027744600170600ustar00rootroot00000000000000============= Upgrade notes ============= 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. django-mptt-0.6.0/docs/utilities.rst000066400000000000000000000053541220027744600174510ustar00rootroot00000000000000================================ 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``. django-mptt-0.6.0/mptt/000077500000000000000000000000001220027744600147315ustar00rootroot00000000000000django-mptt-0.6.0/mptt/__init__.py000066400000000000000000000016231220027744600170440ustar00rootroot00000000000000from __future__ import unicode_literals VERSION = (0, 6, 0) # NOTE: This method was removed in 0.4.0, but restored in 0.4.2 after use-cases were # reported that were impossible by merely subclassing MPTTModel. 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) # Also removed in 0.4.0 but restored in 0.4.2, otherwise this 0.3-compatibility code will break: # if hasattr(mptt, 'register'): # try: # mptt.register(...) # except mptt.AlreadyRegistered: # pass class AlreadyRegistered(Exception): "Deprecated - don't use this anymore. It's never thrown, you don't need to catch it" django-mptt-0.6.0/mptt/admin.py000066400000000000000000000120371220027744600163760ustar00rootroot00000000000000from __future__ import unicode_literals import django import warnings from django.conf import settings from django.contrib.admin.views.main import ChangeList from django.contrib.admin.options import ModelAdmin from django.utils.translation import ugettext as _ from mptt.forms import MPTTAdminForm, TreeNodeChoiceField __all__ = ('MPTTChangeList', 'MPTTModelAdmin', 'MPTTAdminForm') IS_GRAPPELLI_INSTALLED = 'grappelli' in settings.INSTALLED_APPS class MPTTChangeList(ChangeList): def get_query_set(self, request): qs = super(MPTTChangeList, self).get_query_set(request) # always order by (tree_id, left) tree_id = qs.model._mptt_meta.tree_id_attr left = qs.model._mptt_meta.left_attr return qs.order_by(tree_id, left) 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): from mptt.models import MPTTModel, TreeForeignKey if issubclass(db_field.rel.to, MPTTModel) \ and not isinstance(db_field, TreeForeignKey) \ and not db_field.name in self.raw_id_fields: defaults = dict(form_class=TreeNodeChoiceField, queryset=db_field.rel.to.objects.all(), required=False) defaults.update(kwargs) kwargs = defaults return super(MPTTModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) def get_changelist(self, request, **kwargs): """ Returns the ChangeList class for use on the changelist page. """ return MPTTChangeList if getattr(settings, 'MPTT_USE_FEINCMS', True): _feincms_tree_editor = None try: from feincms.admin.tree_editor import TreeEditor as _feincms_tree_editor except ImportError: pass if _feincms_tree_editor is not None: __all__ = tuple(list(__all__) + ['FeinCMSModelAdmin']) class FeinCMSModelAdmin(_feincms_tree_editor): """ A ModelAdmin to add changelist tree view and editing capabilities. Requires FeinCMS to be installed. """ form = MPTTAdminForm def __init__(self, *args, **kwargs): warnings.warn( "mptt.admin.FeinCMSModelAdmin has been deprecated, use " "feincms.admin.tree_editor.TreeEditor instead.", UserWarning, ) super(FeinCMSModelAdmin, self).__init__(*args, **kwargs) def _actions_column(self, obj): actions = super(FeinCMSModelAdmin, self)._actions_column(obj) # compatibility with Django 1.4 admin images (issue #191): # https://docs.djangoproject.com/en/1.4/releases/1.4/#django-contrib-admin if django.VERSION >= (1, 4): admin_img_prefix = "%sadmin/img/" % settings.STATIC_URL else: admin_img_prefix = "%simg/admin/" % settings.ADMIN_MEDIA_PREFIX actions.insert(0, '%s' % ( self.model._mptt_meta.parent_attr, obj.pk, _('Add child'), admin_img_prefix, _('Add child'))) if hasattr(obj, 'get_absolute_url'): actions.insert(0, '%s' % ( obj.get_absolute_url(), _('View on site'), admin_img_prefix, _('View on site'))) return actions 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.) """ n = 0 for obj in queryset: obj.delete() n += 1 self.message_user(request, _("Successfully deleted %s items.") % n) def get_actions(self, request): actions = super(FeinCMSModelAdmin, 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 django-mptt-0.6.0/mptt/exceptions.py000066400000000000000000000006571220027744600174740ustar00rootroot00000000000000""" 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 django-mptt-0.6.0/mptt/fields.py000066400000000000000000000027751220027744600165640ustar00rootroot00000000000000""" Model fields for working with trees. """ from __future__ import unicode_literals __all__ = ('TreeForeignKey', 'TreeOneToOneField', 'TreeManyToManyField') from django.db import models from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField 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 try: 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"]) except ImportError: pass django-mptt-0.6.0/mptt/forms.py000066400000000000000000000157551220027744600164460ustar00rootroot00000000000000""" 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.forms.util import ErrorList try: from django.utils.encoding import smart_text except ImportError: from django.utils.encoding import smart_unicode as smart_text from django.utils.html import conditional_escape, mark_safe from django.utils.translation import ugettext_lazy as _ from mptt.exceptions import InvalidMove __all__ = ('TreeNodeChoiceField', 'TreeNodeMultipleChoiceField', 'TreeNodePositionField', 'MoveNodeForm') # Fields ###################################################################### class TreeNodeChoiceFieldMixin(object): def __init__(self, queryset, *args, **kwargs): self.level_indicator = kwargs.pop('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'].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] = ErrorList(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[opts.parent_attr] 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] = forms.util.ErrorList() self._errors[opts.parent_attr].append(_('Invalid parent')) del self.cleaned_data[opts.parent_attr] return cleaned_data django-mptt-0.6.0/mptt/locale/000077500000000000000000000000001220027744600161705ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/de/000077500000000000000000000000001220027744600165605ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/de/LC_MESSAGES/000077500000000000000000000000001220027744600203455ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/de/LC_MESSAGES/django.mo000066400000000000000000000056111220027744600221470ustar00rootroot00000000000000l698)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' seindjango-mptt-0.6.0/mptt/locale/de/LC_MESSAGES/django.po000066400000000000000000000077271220027744600221640ustar00rootroot00000000000000# 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" django-mptt-0.6.0/mptt/locale/dk/000077500000000000000000000000001220027744600165665ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/dk/LC_MESSAGES/000077500000000000000000000000001220027744600203535ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/dk/LC_MESSAGES/django.mo000066400000000000000000000057051220027744600221610ustar00rootroot000000000000006 @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'django-mptt-0.6.0/mptt/locale/dk/LC_MESSAGES/django.po000066400000000000000000000077611220027744600221700ustar00rootroot00000000000000# 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'" django-mptt-0.6.0/mptt/locale/fr/000077500000000000000000000000001220027744600165775ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/fr/LC_MESSAGES/000077500000000000000000000000001220027744600203645ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/fr/LC_MESSAGES/django.mo000066400000000000000000000070501220027744600221650ustar00rootroot00000000000000)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'django-mptt-0.6.0/mptt/locale/fr/LC_MESSAGES/django.po000066400000000000000000000114071220027744600221710ustar00rootroot00000000000000# 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 »" django-mptt-0.6.0/mptt/locale/nb/000077500000000000000000000000001220027744600165675ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/nb/LC_MESSAGES/000077500000000000000000000000001220027744600203545ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/nb/LC_MESSAGES/django.mo000066400000000000000000000075411220027744600221620ustar00rootroot00000000000000 +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'django-mptt-0.6.0/mptt/locale/nb/LC_MESSAGES/django.po000066400000000000000000000126151220027744600221630ustar00rootroot00000000000000# 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." django-mptt-0.6.0/mptt/locale/pl/000077500000000000000000000000001220027744600166035ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/pl/LC_MESSAGES/000077500000000000000000000000001220027744600203705ustar00rootroot00000000000000django-mptt-0.6.0/mptt/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000074001220027744600221700ustar00rootroot00000000000000)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'django-mptt-0.6.0/mptt/locale/pl/LC_MESSAGES/django.po000066400000000000000000000120421220027744600221710ustar00rootroot00000000000000# 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'" django-mptt-0.6.0/mptt/managers.py000066400000000000000000001232401220027744600171020ustar00rootroot00000000000000""" A custom manager for working with trees of objects. """ from __future__ import unicode_literals import contextlib from django.db import models, transaction, connections, router from django.db.models import F, Max, Q from django.utils.translation import ugettext as _ from mptt.exceptions import CantDisableUpdates, InvalidMove __all__ = ('TreeManager',) COUNT_SUBQUERY = """( SELECT COUNT(*) FROM %(rel_table)s WHERE %(mptt_fk)s = %(mptt_table)s.%(mptt_pk)s )""" CUMULATIVE_COUNT_SUBQUERY = """( SELECT COUNT(*) FROM %(rel_table)s WHERE %(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 ) )""" class TreeManager(models.Manager): """ A manager for working with trees of objects. """ def init_from_model(self, model): """ Sets things up. This would normally be done in contribute_to_class(), but Django calls that before we've created our extra tree fields on the model (which we need). So it's done here instead, after field setup. """ # Avoid calling "get_field_by_name()", which populates the related # models cache and can cause circular imports in complex projects. # Instead, find the tree_id field using "get_fields_with_model()". [tree_field] = [fld for fld in model._meta.get_fields_with_model() if fld[0].name == self.tree_id_attr] if tree_field[1]: # tree_model is the model that contains the tree fields. # this is usually just the same as model, but not for derived models. self.tree_model = tree_field[1] else: self.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_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. """ filters = [] assert self.model is queryset.model opts = queryset.model._mptt_meta if not queryset: return self.none() filters = None for node in queryset: lft, rght = node.lft, node.rght if include_self: lft -= 1 rght += 1 q = Q(**{ opts.tree_id_attr: getattr(node, opts.tree_id_attr), '%s__gt' % opts.left_attr: lft, '%s__lt' % opts.right_attr: rght, }) if filters is None: filters = q else: filters |= q return self.filter(filters) @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 (1 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.commit_on_success(): 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.commit_on_success(): 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 def _mptt_filter(self, qs=None, **filters): """ Like self.filter(), but translates name-agnostic filters for MPTT fields. """ if self._base_manager: return self._base_manager._mptt_filter(qs=qs, **filters) if qs is None: qs = self.get_query_set() return qs.filter(**self._translate_lookups(**filters)) def _mptt_update(self, qs=None, **items): """ Like self.update(), but translates name-agnostic MPTT fields. """ if self._base_manager: return self._base_manager._mptt_update(qs=qs, **items) if qs is None: qs = self.get_query_set() 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 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_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 % { '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_pk': qn(meta.pk.column), } return queryset.extra(select={count_attr: subquery}) def get_query_set(self): """ Returns a ``QuerySet`` which contains all tree items, ordered in such a way that that root nodes appear in tree id order and their subtrees appear in depth-first order. """ return super(TreeManager, self).get_query_set().order_by( self.tree_id_attr, self.left_attr) def insert_node(self, node, target, position='last-child', save=False, allow_existing_pk=False): """ 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 self._base_manager: return self._base_manager.insert_node(node, target, position=position, save=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']: 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) space_target, level, left, parent, right_shift = \ self._calculate_inter_tree_move_values(node, target, position) tree_id = getattr(parent, 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 def _move_node(self, node, target, position='last-child', save=True): if self._base_manager: return self._base_manager.move_node(node, target, position=position) 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) 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) transaction.commit_unless_managed() 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) def root_node(self, tree_id): """ Returns the root node of the tree with the given id. """ if self._base_manager: return self._base_manager.root_node(tree_id) return self._mptt_filter(tree_id=tree_id, parent=None).get() def root_nodes(self): """ Creates a ``QuerySet`` containing root nodes. """ if self._base_manager: return self._base_manager.root_nodes() return self._mptt_filter(parent=None) def rebuild(self): """ Rebuilds whole tree in database using `parent` link. """ if self._base_manager: return self._base_manager.rebuild() 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) def partial_rebuild(self, tree_id): if self._base_manager: return self._base_manager.partial_rebuild(tree_id) 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. """ qs = self.get_query_set() max_tree_id = list(qs.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.pk, 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 self._inter_tree_move_and_close_gap(node, level_change, left_right_change, new_tree_id, parent.pk) # 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.pk, parent.pk, 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.pk, parent.pk, 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 django-mptt-0.6.0/mptt/models.py000066400000000000000000001042121220027744600165660ustar00rootroot00000000000000from __future__ import unicode_literals from functools import reduce import operator import threading import warnings from django.db import models from django.db.models.base import ModelBase from django.db.models.query import Q from django.utils import six from django.utils.translation import ugettext as _ from mptt.fields import TreeForeignKey, TreeOneToOneField, TreeManyToManyField from mptt.managers import TreeManager 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 pre_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,)) field_names__add = field_names.add if self.order_insertion_by: for f in self.order_insertion_by: if f[0] == '-': f = f[1:] field_names__add(f) for field_name in field_names: 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.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 == {}: # skip ModelBase, on django < 1.5 it doesn't handle NewBase. super_new = super(ModelBase, meta).__new__ return super_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) for key in ('left_attr', 'right_attr', 'tree_id_attr', 'level_attr'): field_name = getattr(cls._mptt_meta, key) try: cls._meta.get_field(field_name) except models.FieldDoesNotExist: 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') if hasattr(manager, 'init_from_model'): manager.init_from_model(cls) # make sure we have a tree manager somewhere tree_manager = None for attr in sorted(dir(cls)): try: obj = getattr(cls, attr) except AttributeError: continue if isinstance(obj, TreeManager): tree_manager = obj # prefer any locally defined manager (i.e. keep going if not local) if obj.model is cls: 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') tree_manager.init_from_model(cls) # avoid using ManagerDescriptor, so instances can refer to self._tree_manager setattr(cls, '_tree_manager', tree_manager) return cls 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) 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 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. return self._tree_manager.filter(pk=self.pk) 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'), ) return qs.order_by(order_by) 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) 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 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) ) 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 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 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() 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): """ 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) 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 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) 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 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 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] same_order = old_parent_id == parent_id if same_order and len(self._mptt_cached_fields) > 1: get_raw_field_value = opts.get_raw_field_value for field_name, old_value in self._mptt_cached_fields.items(): if old_value != 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. update_cached_parent = ( getattr(self, opts.tree_id_attr) != getattr(parent, opts.tree_id_attr) or 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) 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) 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) else: opts.set_raw_field_value(self, opts.parent_attr, parent_id) 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) 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) 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) django-mptt-0.6.0/mptt/templates/000077500000000000000000000000001220027744600167275ustar00rootroot00000000000000django-mptt-0.6.0/mptt/templates/admin/000077500000000000000000000000001220027744600200175ustar00rootroot00000000000000django-mptt-0.6.0/mptt/templates/admin/grappelli_mptt_change_list.html000066400000000000000000000003701220027744600262700ustar00rootroot00000000000000{% extends "admin/change_list.html" %} {% load admin_list i18n mptt_admin %} {% block result_list %} {% mptt_result_list cl %} {% if action_form and actions_on_bottom and cl.full_result_count %}{% admin_actions %}{% endif %} {% endblock %}django-mptt-0.6.0/mptt/templates/admin/grappelli_mptt_change_list_results.html000066400000000000000000000021231220027744600300470ustar00rootroot00000000000000{% 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 %}django-mptt-0.6.0/mptt/templates/admin/mptt_change_list.html000066400000000000000000000005341220027744600242330ustar00rootroot00000000000000{% 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 %} django-mptt-0.6.0/mptt/templates/admin/mptt_change_list_results.html000066400000000000000000000013041220027744600260100ustar00rootroot00000000000000{% 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 %} django-mptt-0.6.0/mptt/templatetags/000077500000000000000000000000001220027744600174235ustar00rootroot00000000000000django-mptt-0.6.0/mptt/templatetags/__init__.py000066400000000000000000000000501220027744600215270ustar00rootroot00000000000000from __future__ import unicode_literals django-mptt-0.6.0/mptt/templatetags/mptt_admin.py000066400000000000000000000156771220027744600221510ustar00rootroot00000000000000from __future__ import unicode_literals import django from django.conf import settings from django.contrib.admin.util import lookup_field, display_for_field from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils.html import escape, conditional_escape from django.utils.safestring import mark_safe import collections try: from django.utils.encoding import smart_text, force_text except ImportError: from django.utils.encoding import smart_unicode as smart_text, force_unicode as force_text from django.template import Library from django.contrib.admin.templatetags.admin_list import _boolean_icon, result_headers if django.VERSION >= (1, 2, 3): from django.contrib.admin.templatetags.admin_list import result_hidden_fields else: result_hidden_fields = lambda cl: [] 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 ### # 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 = EMPTY_CHANGELIST_VALUE 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 = 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 = EMPTY_CHANGELIST_VALUE else: result_repr = escape(field_val) else: result_repr = display_for_field(value, f) 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-left:%spx"' % (5 + 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 = repr(force_text(value))[1:] ##### 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 ''), 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) django-mptt-0.6.0/mptt/templatetags/mptt_tags.py000066400000000000000000000307731220027744600220110ustar00rootroot00000000000000""" Template tags for working with lists of model instances which represent trees. """ from __future__ import unicode_literals from django import template from django.db.models import get_model from django.db.models.fields import FieldDoesNotExist try: from django.utils.encoding import force_text except ImportError: from django.utils.encoding import force_unicode as force_text from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from mptt.utils import tree_item_iterator, drilldown_tree_for_node 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 = 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 = 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 %} """ 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 tree.new_level %}
  • {% else %}
  • {% endif %} {{ genre.name }} {% for level in tree.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): """ Takes a list/queryset of model objects in MPTT left (depth-first) order, caches the children on each node, as well as the parent of each child node, allowing up and down traversal through the tree without the need for further queries. This makes it possible to have a recursively included template without worrying about database queries. 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. """ current_path = [] top_nodes = [] # If ``queryset`` is QuerySet-like, set ordering to depth-first if hasattr(queryset, 'order_by'): mptt_opts = queryset.model._mptt_meta tree_id_attr = mptt_opts.tree_id_attr left_attr = mptt_opts.left_attr queryset = queryset.order_by(tree_id_attr, left_attr) 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) # 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 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) django-mptt-0.6.0/mptt/utils.py000066400000000000000000000136711220027744600164530ustar00rootroot00000000000000""" 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 __all__ = ('previous_current_next', 'tree_item_iterator', 'drilldown_tree_for_node') 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) try: next(cur) next(nex) next(nex) except StopIteration: pass 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): """ 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) 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) django-mptt-0.6.0/requirements.txt000066400000000000000000000000201220027744600172210ustar00rootroot00000000000000Django >= 1.4.2 django-mptt-0.6.0/setup.py000077500000000000000000000030131220027744600154570ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import unicode_literals from mptt import VERSION requires=('Django>=1.4.2',) try: from setuptools import setup kwargs ={'install_requires': requires} except ImportError: from distutils.core import setup kwargs = {'requires': requires} # Dynamically calculate the version based on mptt.VERSION version_tuple = VERSION version = ".".join([str(v) for v in version_tuple]) # on py3, all these are text strings # on py2, they're all byte strings. # ... and that's how setuptools likes it. setup( name=str('django-mptt'), description=str('''Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances.'''), version=version, author=str('Craig de Stigter'), author_email=str('craig.ds@gmail.com'), url=str('http://github.com/django-mptt/django-mptt'), packages=[str('mptt'), str('mptt.templatetags')], package_data={str('mptt'): [str('templates/admin/*'), str('locale/*/*/*.*')]}, classifiers=[ str('Development Status :: 4 - Beta'), str('Environment :: Web Environment'), str('Framework :: Django'), str('Intended Audience :: Developers'), str('License :: OSI Approved :: BSD License'), str('Operating System :: OS Independent'), str('Programming Language :: Python'), str('Programming Language :: Python :: 2'), str('Programming Language :: Python :: 3'), str('Topic :: Utilities'), ], **kwargs ) django-mptt-0.6.0/tests/000077500000000000000000000000001220027744600151075ustar00rootroot00000000000000django-mptt-0.6.0/tests/__init__.py000066400000000000000000000000501220027744600172130ustar00rootroot00000000000000from __future__ import unicode_literals django-mptt-0.6.0/tests/myapp/000077500000000000000000000000001220027744600162355ustar00rootroot00000000000000django-mptt-0.6.0/tests/myapp/__init__.py000066400000000000000000000000501220027744600203410ustar00rootroot00000000000000from __future__ import unicode_literals django-mptt-0.6.0/tests/myapp/doctests.txt000066400000000000000000001256301220027744600206350ustar00rootroot00000000000000>>> from datetime import date >>> 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])) # 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(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']) # TreeManager Methods ######################################################### # check that tree manager is the explicitly defined tree manager for Person >>> Person._tree_manager == Person.my_tree_manager True # managers of non-abstract bases don't get inherited, so: >>> Student._tree_manager == Student.my_tree_manager 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) # django 1.4 and 1.5 have different msgs on this exception, so we can't use the normal doctest approach >>> try: ... Genre.objects.root_node(3) ... except Genre.DoesNotExist: ... pass ... else: ... raise >>> [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'] >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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() >>> 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.my_tree_manager.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) >>> jack.get_descendants() [, ] >>> jack.get_root() >>> jill = Person.objects.get(pk=3) >>> jill.get_root() >>> jess = Student.objects.get(pk=4) >>> jess.get_descendants() [, , , ] >>> jess.get_root() >>> jeff = Person.objects.get(pk=5) >>> jeff.get_descendants() [, ] >>> jeff.get_root() >>> jane = Student.objects.get(pk=6) >>> jane.get_root() >>> jane.get_ancestors() [, ] django-mptt-0.6.0/tests/myapp/fixtures/000077500000000000000000000000001220027744600201065ustar00rootroot00000000000000django-mptt-0.6.0/tests/myapp/fixtures/categories.json000066400000000000000000000040521220027744600231270ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.category", "fields": { "rght": 20, "name": "PC & Video Games", "parent": null, "level": 0, "lft": 1, "tree_id": 1 } }, { "pk": 2, "model": "myapp.category", "fields": { "rght": 7, "name": "Nintendo Wii", "parent": 1, "level": 1, "lft": 2, "tree_id": 1 } }, { "pk": 3, "model": "myapp.category", "fields": { "rght": 4, "name": "Games", "parent": 2, "level": 2, "lft": 3, "tree_id": 1 } }, { "pk": 4, "model": "myapp.category", "fields": { "rght": 6, "name": "Hardware & Accessories", "parent": 2, "level": 2, "lft": 5, "tree_id": 1 } }, { "pk": 5, "model": "myapp.category", "fields": { "rght": 13, "name": "Xbox 360", "parent": 1, "level": 1, "lft": 8, "tree_id": 1 } }, { "pk": 6, "model": "myapp.category", "fields": { "rght": 10, "name": "Games", "parent": 5, "level": 2, "lft": 9, "tree_id": 1 } }, { "pk": 7, "model": "myapp.category", "fields": { "rght": 12, "name": "Hardware & Accessories", "parent": 5, "level": 2, "lft": 11, "tree_id": 1 } }, { "pk": 8, "model": "myapp.category", "fields": { "rght": 19, "name": "PlayStation 3", "parent": 1, "level": 1, "lft": 14, "tree_id": 1 } }, { "pk": 9, "model": "myapp.category", "fields": { "rght": 16, "name": "Games", "parent": 8, "level": 2, "lft": 15, "tree_id": 1 } }, { "pk": 10, "model": "myapp.category", "fields": { "rght": 18, "name": "Hardware & Accessories", "parent": 8, "level": 2, "lft": 17, "tree_id": 1 } } ] django-mptt-0.6.0/tests/myapp/fixtures/genres.json000066400000000000000000000045011220027744600222640ustar00rootroot00000000000000[ { "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 } } ] django-mptt-0.6.0/tests/myapp/models.py000066400000000000000000000115271220027744600201000ustar00rootroot00000000000000from __future__ import unicode_literals from django.contrib.auth.models import Group from django.db import models from django.utils.encoding import python_2_unicode_compatible import mptt from mptt.models import MPTTModel, TreeForeignKey from mptt.managers import TreeManager class CustomTreeManager(TreeManager): pass class Category(MPTTModel): name = models.CharField(max_length=50) parent = models.ForeignKey('self', null=True, blank=True, related_name='children') @python_2_unicode_compatible def __str__(self): return self.name def delete(self): super(Category, self).delete() class Genre(MPTTModel): name = models.CharField(max_length=50, unique=True) parent = models.ForeignKey('self', null=True, blank=True, related_name='children') @python_2_unicode_compatible def __str__(self): return self.name class Insert(MPTTModel): parent = models.ForeignKey('self', null=True, blank=True, related_name='children') class MultiOrder(MPTTModel): name = models.CharField(max_length=50) size = models.PositiveIntegerField() date = models.DateField() parent = models.ForeignKey('self', null=True, blank=True, related_name='children') class MPTTMeta: order_insertion_by = ['name', 'size', '-date'] @python_2_unicode_compatible def __str__(self): return self.name class Node(MPTTModel): parent = models.ForeignKey('self', null=True, blank=True, related_name='children') class MPTTMeta: left_attr = 'does' right_attr = 'zis' level_attr = 'madness' tree_id_attr = 'work' class OrderedInsertion(MPTTModel): name = models.CharField(max_length=50) parent = models.ForeignKey('self', null=True, blank=True, related_name='children') class MPTTMeta: order_insertion_by = ['name'] @python_2_unicode_compatible def __str__(self): return self.name class Tree(MPTTModel): parent = models.ForeignKey('self', null=True, blank=True, related_name='children') class NewStyleMPTTMeta(MPTTModel): parent = models.ForeignKey('self', null=True, blank=True, related_name='children') class MPTTMeta(object): left_attr = 'testing' class Person(MPTTModel): name = models.CharField(max_length=50) parent = models.ForeignKey('self', null=True, blank=True, related_name='children') # just testing it's actually possible to override the tree manager objects = models.Manager() my_tree_manager = CustomTreeManager() @python_2_unicode_compatible def __str__(self): return self.name class Student(Person): type = models.CharField(max_length=50) class CustomPKName(MPTTModel): my_id = models.AutoField(db_column='my_custom_name', primary_key=True) name = models.CharField(max_length=50) parent = models.ForeignKey('self', null=True, blank=True, related_name='children', db_column="my_cusom_parent") @python_2_unicode_compatible def __str__(self): return self.name # for testing various types of inheritance: # 1. multi-table inheritance, with mptt fields on base class. class MultiTableInheritanceA1(MPTTModel): parent = models.ForeignKey('self', null=True, blank=True, related_name='children') 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 = models.ForeignKey('self', null=True, blank=True, related_name='children') # 3. abstract models class AbstractModel(MPTTModel): parent = models.ForeignKey('self', null=True, blank=True, related_name='children') 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): class Meta: proxy = True class DoubleProxyModel(SingleProxyModel): class Meta: proxy = True class AutoNowDateFieldModel(MPTTModel): parent = models.ForeignKey('self', null=True, blank=True, related_name='children') now = models.DateTimeField(auto_now_add=True) class MPTTMeta: order_insertion_by = ('now',) # test registering of remote model TreeForeignKey(Group, blank=True, null=True).contribute_to_class(Group, 'parent') mptt.register(Group, order_insertion_by=('name',)) django-mptt-0.6.0/tests/myapp/tests.py000066400000000000000000001177351220027744600177670ustar00rootroot00000000000000from __future__ import unicode_literals import os import re import sys import tempfile import django from django.contrib import admin from django.contrib.auth.models import Group from django.db.models import get_models from django.forms.models import modelform_factory from django.template import Template, Context from django.test import TestCase from django.utils.six import string_types, PY3, b try: import feincms except ImportError: feincms = False from mptt.exceptions import CantDisableUpdates, InvalidMove from mptt.forms import MPTTAdminForm from mptt.models import MPTTModel from mptt.templatetags.mptt_tags import cache_tree_children from myapp.models import Category, Genre, CustomPKName, SingleProxyModel, DoubleProxyModel, ConcreteModel, OrderedInsertion, AutoNowDateFieldModel extra_queries_per_update = 0 if django.VERSION < (1, 6): # before django 1.6, Model.save() did a select then an update/insert. # now, Model.save() does an update followed an insert if the update changed 0 rows. extra_queries_per_update = 1 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)) temp.flush() import doctest doctest.testfile( temp.name, module_relative=False, optionflags=doctest.IGNORE_EXCEPTION_DETAIL, encoding='utf-8', raise_on_error=True, ) sys.stdout = before content = dummy_stream.content if content: sys.stderr.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) # 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 if feincms: class FeinCMSModelAdminTestCase(TreeTestCase): """ Tests for FeinCMSModelAdmin. """ fixtures = ['categories.json'] def test_actions_column(self): """ The action column should have an "add" button inserted. """ from mptt.admin import FeinCMSModelAdmin model_admin = FeinCMSModelAdmin(Category, admin.site) category = Category.objects.get(id=1) self.assertTrue( '' in model_admin._actions_column(category)[0] ) 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 + extra_queries_per_update): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1 + extra_queries_per_update): # 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 + extra_queries_per_update): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1 + extra_queries_per_update): # 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 + extra_queries_per_update): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1 + extra_queries_per_update): # 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 + extra_queries_per_update): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1 + extra_queries_per_update): # 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(7): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(1): # 1 query here: 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 6 4 2 1 2 3 5 - 3 0 1 2 """) # remaining queries (3 through 7) 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(9 + extra_queries_per_update): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(1 + extra_queries_per_update): # 1 update query self.c.parent = self.b self.c.save() # query 2 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 + extra_queries_per_update): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2 + extra_queries_per_update): # 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 + extra_queries_per_update): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(3 + extra_queries_per_update): # 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 + extra_queries_per_update): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2 + extra_queries_per_update): # 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(11): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(1): # 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 + extra_queries_per_update): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(1 + extra_queries_per_update): # 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 + extra_queries_per_update): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(2 + extra_queries_per_update): # 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 + extra_queries_per_update): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(3 + extra_queries_per_update): # 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 + extra_queries_per_update): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(2 + extra_queries_per_update): # 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'] def test_all_managers_are_different(self): # all tree managers should be different. otherwise, possible infinite recursion. seen = {} for model in get_models(): if not issubclass(model, MPTTModel): continue tm = model._tree_manager if tm in seen: self.fail("Tree managers for %s and %s are the same manager" % (model.__name__, seen[tm].__name__)) seen[tm] = model def test_all_managers_have_correct_model(self): # all tree managers should have the correct model. for model in 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 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_get_queryset_descendants(self): def get_desc_names(qs, include_self=False): desc = Category.objects.get_queryset_descendants(qs, include_self=include_self) return list(desc.values_list('name', flat=True).order_by('name')) qs = Category.objects.filter(name='Nintendo Wii') self.assertEqual( get_desc_names(qs), ['Games', 'Hardware & Accessories'], ) self.assertEqual( get_desc_names(qs, include_self=True), ['Games', 'Hardware & Accessories', 'Nintendo Wii'], ) 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) class RecurseTreeTestCase(TreeTestCase): """ Tests for the ``recursetree`` template filter. """ fixtures = ['categories.json'] template = ( '{% 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), })) 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, })) self.assertEqual(html, ( '
  • PlayStation 3
      ' '
    • Games
    • Hardware & Accessories
' )) 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) CategoryForm(instance=c) django-mptt-0.6.0/tests/runtests.sh000077500000000000000000000006671220027744600173460ustar00rootroot00000000000000#!/bin/sh export PYTHONPATH="./" export DJANGO_SETTINGS_MODULE='settings' if [ `which django-admin.py` ] ; then export DJANGO_ADMIN=django-admin.py else export DJANGO_ADMIN=django-admin fi export args="$@" if [ -z "$args" ] ; then # avoid running the tests for django.contrib.* (they're in INSTALLED_APPS) export args=myapp fi $DJANGO_ADMIN test --traceback --settings=$DJANGO_SETTINGS_MODULE --pythonpath="../" "$args" django-mptt-0.6.0/tests/settings.py000066400000000000000000000006541220027744600173260ustar00rootroot00000000000000from __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.auth', 'django.contrib.contenttypes', 'mptt', 'myapp', ) # Required for Django 1.4+ STATIC_URL = '/static/' # Required for Django 1.5+ SECRET_KEY = 'abc123' django-mptt-0.6.0/tox.ini000066400000000000000000000017771220027744600152740ustar00rootroot00000000000000[tox] envlist= py26-14, py26-15, py26-master, py27-14, py27-15, py27-master, py32-15, py32-master, py33-15, py33-master [testenv] changedir = {toxinidir}/tests commands = ./runtests.sh {posargs} [testenv:py26-14] basepython = python2.6 deps = Django>=1.4,<1.5 [testenv:py26-15] basepython = python2.6 deps = Django>=1.5,<1.6 [testenv:py26-master] basepython = python2.6 deps = https://github.com/django/django/tarball/master [testenv:py27-14] basepython = python2.7 deps = Django>=1.4,<1.5 [testenv:py27-15] basepython = python2.7 deps = Django>=1.5,<1.6 [testenv:py27-master] basepython = python2.7 deps = https://github.com/django/django/tarball/master [testenv:py32-15] basepython = python3.2 deps = Django>=1.5,<1.6 [testenv:py32-master] basepython = python3.2 deps = https://github.com/django/django/tarball/master [testenv:py33-15] basepython = python3.3 deps = Django>=1.5,<1.6 [testenv:py33-master] basepython = python3.3 deps = https://github.com/django/django/tarball/master