pax_global_header00006660000000000000000000000064141121403400014500gustar00rootroot0000000000000052 comment=63c88c078458481e22be22c18391fade79259498 django-mptt-0.13.2/000077500000000000000000000000001411214034000140075ustar00rootroot00000000000000django-mptt-0.13.2/.github/000077500000000000000000000000001411214034000153475ustar00rootroot00000000000000django-mptt-0.13.2/.github/workflows/000077500000000000000000000000001411214034000174045ustar00rootroot00000000000000django-mptt-0.13.2/.github/workflows/tests.yml000066400000000000000000000022671411214034000213000ustar00rootroot00000000000000name: Tests on: push: branches: - main pull_request: jobs: tests: name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - 3.6 - 3.7 - 3.8 - 3.9 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip wheel setuptools tox - name: Run tox targets for ${{ matrix.python-version }} run: | ENV_PREFIX=$(tr -C -d "0-9" <<< "${{ matrix.python-version }}") TOXENV=$(tox --listenvs | grep "^py$ENV_PREFIX" | tr '\n' ',') python -m tox lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip tox - name: Run lint run: tox -e style django-mptt-0.13.2/.gitignore000066400000000000000000000001361411214034000157770ustar00rootroot00000000000000*.pyc *.egg-info build dist .tox MANIFEST tests/mydatabase *.egg .eggs venv/ .idea/ .coverage django-mptt-0.13.2/CHANGELOG.rst000066400000000000000000000302421411214034000160310ustar00rootroot00000000000000========== Change log ========== Next version ============ - Merged the ``docs/upgrade.rst`` file into the main ``CHANGELOG.rst``. - Fixed the Sphinx autodoc configuration to also work locally. Ensured that readthedocs is able to build the docs again. - Fixed a bug where ``DraggableMPTTAdmin`` assumed that the user model's primary key is called ``id``. - Ensured that we do not install the ``tests.myapp`` package. - Added dark mode support to the draggable model admin. 0.13 ==== - **MARKED THE PROJECT AS UNMAINTAINED, WHICH IT STILL IS** - Reformatted everything using black, isort etc. - Switched from Travis CI to GitHub actions. - Switched to a declarative setup. - Verified compatibility with Django up to 3.2 and Python up to 3.9. Dropped compatibility guarantees (ha!) with anything older than Django 2.2 and Python 3.6. - Renamed the main development branch to main. - Fixed a problem where our ``_get_user_field_names`` mistakenly returned abstract fields. - Added a workaround for the ``default_app_config`` warning. - Changed saving to always call ``get_ordered_insertion_target`` when using ordered insertion. - Made it possible to override the starting level when using the tree node choice field. 0.12 ==== - Add support for Django 3.1 - Drop support for Django 1.11. - Drop support for Python 3.5. - Fix an issue where the `rebuild()` method would not work correctly if you were using multiple databases. - Update spanish translations. 0.11 ==== - Add support for Django 3.0. - Add support for Python 3.8. - Add an admin log message when moving nodes when using the `DraggableMPTTAdmin` admin method. - Fix `_is_saved` returning `False` when `pk == 0`. - Add an `all_descendants` argument to `drilldown_tree_for_node`. - Add traditional Chinese localization. - properly log error user messages at the error level in the admin. 0.10 ==== - Drop support for Pythons 3.4 and Python 2. - Add support for Python 3.7. - Add support for Django 2.1 and 2.2. - Fix `get_cached_trees` to cleanly handle cases where nodes' parents were not included in the original queryset. - Add a `build_tree_nodes` method to the `TreeManager` Model manager to allow for efficient bulk inserting of a tree (as represented by a bulk dictionary). 0.9.1 ===== Support for Python 3.3 has been removed. Support for Django 2.1 has been added, support for Django<1.11 is removed. Support for deprecated South has been removed. Some updates have been made on the documentation such as: - Misc updates in the docs (formatting) - Added italian translation - Remove unnecessary `db_index=True` from doc examples - Include on_delete in all TreeForeignKey examples in docs - Use https:// URLs throughout docs where available - Document project as stable and ready for use in production - Add an example of add_related_count usage with the admin - Updates README.rst with svg badge - Update tutorial Bug fixes: - Fix django-grappelli rendering bug (#661) - Fixing MPTT models (use explicit db) Misc: - Update pypi.python.org URL to pypi.org - Remove redundant tox.ini options that respecify defaults - Remove unused argument from `_inter_tree_move_and_close_gap()` - Trim trailing white space throughout the project - Pass python_requires argument to setuptools - Added MpttConfig - Add test case to support ancestor coercion callbacks. - Extend tree_item_iterator with ancestor coercion callback. 0.9.0 ===== Now supports django 1.11 and 2.0. Removed tests for unsupported django versions (django 1.9, 1.10) 0.8.6 ===== Now supports django 1.10. After upgrading, you may come across this error when running migrations:: Unhandled exception in thread started by Traceback (most recent call last): #... File "venv/lib/python2.7/site-packages/django/db/models/manager.py", line 120, in contribute_to_class setattr(model, name, ManagerDescriptor(self)) AttributeError: can't set attribute To fix this, please replace ``._default_manager`` in your historic migrations with ``.objects``. For more detailed information see `#469`_, `#498`_ .. _`#469`: https://github.com/django-mptt/django-mptt/issues/469 .. _`#498`: https://github.com/django-mptt/django-mptt/issues/498 0.8.0 ===== Dropped support for old Django versions and Python 2.6 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unsupported versions of django (1.4, 1.5, 1.6, 1.7) are no longer supported, and Python 2.6 is no longer supported. These versions of python/django no longer receive security patches. You should upgrade to Python 2.7 and Django 1.8+. Django 1.9 support has been added. 0.7.0 ===== Dropped support for Django 1.5, Added support for 1.8 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django 1.5 support has been removed since django 1.5 is not supported upstream any longer. Django 1.8 support has been added. Deprecated: Calling ``recursetree``/``cache_tree_children`` with incorrectly-ordered querysets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Previously, when given a queryset argument, ``cache_tree_children`` called ``.order_by`` to ensure that the queryset was in the correct order. In 0.7, calling ``cache_tree_children`` with an incorrectly-ordered queryset will cause a deprecation warning. In 0.8, it will raise an error. This also applies to ``recursetree``, since it calls ``cache_tree_children``. This probably doesn't affect many usages, since the default ordering for mptt models will work fine. Minor: ``TreeManager.get_queryset`` no longer provided on Django < 1.6 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django renamed ``get_query_set`` to ``get_queryset`` in Django 1.6. For backward compatibility django-mptt had both methods available for 1.4-1.5 users. This has been removed. You should use ``get_query_set`` on Django 1.4-1.5, and ``get_queryset`` if you're on 1.6+. Removed FeinCMSModelAdmin ~~~~~~~~~~~~~~~~~~~~~~~~~ Deprecated in 0.6.0, this has now been removed. 0.6.0 ===== mptt now requires Python 2.6+, and supports Python 3.2+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ mptt 0.6 drops support for both Python 2.4 and 2.5. This was done to make it easier to support Python 3, as well as support the new context managers (delay_mptt_updates and disable_mptt_updates). If you absolutely can't upgrade your Python version, you'll need to stick to mptt 0.5.5 until you can. No more implicit ``empty_label=True`` on form fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Until 0.5, ``TreeNodeChoiceField`` and ``TreeNodeMultipleChoiceField`` implicitly set ``empty_label=True``. This was around since a long time ago, for unknown reasons. It has been removed in 0.6.0 as it caused occasional headaches for users. If you were relying on this behavior, you'll need to explicitly pass ``empty_label=True`` to any of those fields you use, otherwise you will start seeing new '--------' choices appearing in them. Deprecated FeinCMSModelAdmin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you were using ``mptt.admin.FeinCMSModelAdmin``, you should switch to using ``feincms.admin.tree_editor.TreeEditor`` instead, or you'll get a loud deprecation warning. 0.4.2 to 0.5.5 ============== ``TreeManager`` is now the default manager, ``YourModel.tree`` removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In 0.5, ``TreeManager`` now behaves just like a normal django manager. If you don't override anything, you'll now get a ``TreeManager`` by default (``.objects``.) Before 0.5, ``.tree`` was the default name for the ``TreeManager``. That's been removed, so we recommend updating your code to use ``.objects``. If you don't want to update ``.tree`` to ``.objects`` everywhere just yet, you should add an explicit ``TreeManager`` to your models:: objects = tree = TreeManager() ``save(raw=True)`` keyword argument removed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In earlier versions, MPTTModel.save() had a ``raw`` keyword argument. If True, the MPTT fields would not be updated during the save. This (undocumented) argument has now been removed. ``_meta`` attributes moved to ``_mptt_meta`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In 0.4, we deprecated all these attributes on model._meta. These have now been removed:: MyModel._meta.left_attr MyModel._meta.right_attr MyModel._meta.tree_id_attr MyModel._meta.level_attr MyModel._meta.tree_manager_attr MyModel._meta.parent_attr MyModel._meta.order_insertion_by If you're still using any of these, you'll need to update by simply renaming ``_meta`` to ``_mptt_meta``. Running the tests ~~~~~~~~~~~~~~~~~ Tests are now run with:: cd tests/ ./runtests.sh The previous method (``python setup.py test``) no longer works since we switched to plain distutils. 0.3 to 0.4.2 ============ Model changes ~~~~~~~~~~~~~ MPTT attributes on ``MyModel._meta`` deprecated, moved to ``MyModel._mptt_meta`` ---------------------------------------------------------------------------------- Most people won't need to worry about this, but if you're using any of the following, note that these are deprecated and will be removed in 0.5:: MyModel._meta.left_attr MyModel._meta.right_attr MyModel._meta.tree_id_attr MyModel._meta.level_attr MyModel._meta.tree_manager_attr MyModel._meta.parent_attr MyModel._meta.order_insertion_by They'll continue to work as previously for now, but you should upgrade your code if you can. Simply replace ``_meta`` with ``_mptt_meta``. Use model inheritance where possible ------------------------------------ The preferred way to do model registration in ``django-mptt`` 0.4 is via model inheritance. Suppose you start with this:: class Node(models.Model): ... mptt.register(Node, order_insertion_by=['name'], parent_attr='padre') First, Make your model a subclass of ``MPTTModel``, instead of ``models.Model``:: from mptt.models import MPTTModel class Node(MPTTModel): ... Then remove your call to ``mptt.register()``. If you were passing it keyword arguments, you should add them to an ``MPTTMeta`` inner class on the model:: class Node(MPTTModel): ... class MPTTMeta: order_insertion_by = ['name'] parent_attr = 'padre' If necessary you can still use ``mptt.register``. It was removed in 0.4.0 but restored in 0.4.2, since people reported use cases that didn't work without it.) For instance, if you need to register models where the code isn't under your control, you'll need to use ``mptt.register()``. Behind the scenes, ``mptt.register()`` in 0.4 will actually add MPTTModel to ``Node.__bases__``, thus achieving the same result as subclassing ``MPTTModel``. If you're already inheriting from something other than ``Model``, that means multiple inheritance. You're probably all upgraded at this point :) A couple more notes for more complex scenarios: More complicated scenarios ~~~~~~~~~~~~~~~~~~~~~~~~~~ What if I'm already inheriting from something? ---------------------------------------------- If your model is already a subclass of an abstract model, you should use multiple inheritance:: class Node(MPTTModel, ParentModel): ... You should always put MPTTModel as the first model base. This is because there's some complicated metaclass stuff going on behind the scenes, and if Django's model metaclass gets called before the MPTT one, strange things can happen. Isn't multiple inheritance evil? Well, maybe. However, the `Django model docs`_ don't forbid this, and as long as your other model doesn't have conflicting methods, it should be fine. .. note:: As always when dealing with multiple inheritance, approach with a bit of caution. Our brief testing says it works, but if you find that the Django internals are somehow breaking this approach for you, please `create an issue`_ with specifics. .. _`create an issue`: https://github.com/django-mptt/django-mptt/issues .. _`Django model docs`: https://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.13.2/INSTALL000066400000000000000000000007551411214034000150470ustar00rootroot00000000000000Thanks 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 3.5 or newer - Django 1.11 or newer You can obtain Python from https://www.python.org/ and Django from https://www.djangoproject.com/ django-mptt-0.13.2/LICENSE000066400000000000000000000020771411214034000150220ustar00rootroot00000000000000Django 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.13.2/MANIFEST.in000066400000000000000000000004421411214034000155450ustar00rootroot00000000000000include INSTALL include LICENSE include MANIFEST.in include NOTES include README.rst recursive-include docs *.rst *.py Makefile recursive-include mptt *.json recursive-include mptt/templates * recursive-include mptt/locale * recursive-include mptt/static * global-exclude __pycache__ *.pyc django-mptt-0.13.2/NOTES000066400000000000000000000452171411214034000146330ustar00rootroot00000000000000============================= 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.13.2/README.rst000066400000000000000000000074041411214034000155030ustar00rootroot00000000000000========================================== **This project is currently unmaintained** ========================================== Alternatives to django-mptt include: * `django-treebeard `_ includes a MPTT implementation (called nested set) * Maybe you do not need MPTT, especially when using newer databases. See `django-tree-queries `_ for an implementation using recursive Common Table Expressions (CTE). See the `announcement blog post `__. =========== 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.svg?branch=master :alt: Build Status :target: https://travis-ci.org/django-mptt/django-mptt Project home: https://github.com/django-mptt/django-mptt/ Documentation: https://django-mptt.readthedocs.io/ Discussion group: https://groups.google.com/forum/#!forum/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`: https://www.ibase.ru/files/articles/programming/dbmstrees/sqltrees.html .. _`Storing Hierarchical Data in a Database`: https://www.sitepoint.com/hierarchical-data-database/ .. _`Managing Hierarchical Data in MySQL`: http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/ What is ``django-mptt``? ======================== ``django-mptt`` is a reusable Django app which aims to make it easy for you to use MPTT with your own Django models. It takes care of the details of managing a database table as a tree structure and provides tools for working with trees of model instances. Requirements ------------ * Python 3.6+ * A supported version of Django (currently 2.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. * `Admin classes`_ for visualizing and modifying trees in Django's administration interface. .. _`Form fields`: https://django-mptt.readthedocs.io/en/latest/forms.html .. _`Utility functions`: https://django-mptt.readthedocs.io/en/latest/utilities.html .. _`Template tags and filters`: https://django-mptt.readthedocs.io/en/latest/templates.html .. _`Admin classes`: https://django-mptt.readthedocs.io/en/latest/admin.html django-mptt-0.13.2/create-release.sh000077500000000000000000000002661411214034000172330ustar00rootroot00000000000000#!/bin/bash -ex # Clean environment, to avoid https://github.com/django-mptt/django-mptt/issues/513 python3 ./setup.py clean rm -rf ./*.egg-info python3 setup.py sdist bdist_wheel django-mptt-0.13.2/docs/000077500000000000000000000000001411214034000147375ustar00rootroot00000000000000django-mptt-0.13.2/docs/Makefile000066400000000000000000000110111411214034000163710ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build/docs # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-mptt.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-mptt.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-mptt" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-mptt" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." django-mptt-0.13.2/docs/admin.rst000066400000000000000000000126611411214034000165670ustar00rootroot00000000000000============= Admin classes ============= ``mptt.admin.MPTTModelAdmin`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is a bare-bones tree admin. All it does is enforce ordering, and indent the nodes in the tree to make a pretty tree list view. .. image:: mpttmodeladmin-genres.png :align: center :width: 26.21cm :alt: MPTTModelAdmin screenshot Usage:: from django.contrib import admin from mptt.admin import MPTTModelAdmin from myproject.myapp.models import Node admin.site.register(Node, MPTTModelAdmin) You can change the indent pixels per level globally by putting this in your settings.py:: # default is 10 pixels MPTT_ADMIN_LEVEL_INDENT = 20 If you'd like to specify the pixel amount per Model, define an ``mptt_level_indent`` attribute in your MPTTModelAdmin:: from django.contrib import admin from mptt.admin import MPTTModelAdmin from myproject.myapp.models import Node class CustomMPTTModelAdmin(MPTTModelAdmin): # specify pixel amount for this ModelAdmin only: mptt_level_indent = 20 admin.site.register(Node, CustomMPTTModelAdmin) If you'd like to specify which field should be indented, add an ``mptt_indent_field`` to your MPTTModelAdmin:: # … class CustomMPTTModelAdmin(MPTTModelAdmin): mptt_indent_field = "some_node_field" # … ``mptt.admin.DraggableMPTTAdmin`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 0.8.1 .. image:: draggablempttadmin-genres.png :align: center :width: 26.39cm :alt: DraggableMPTTAdmin screenshot This is a tree admin based on FeinCMS_ offering drag-drop functionality for moving nodes:: from django.contrib import admin from mptt.admin import DraggableMPTTAdmin from myproject.myapp.models import Node admin.site.register( Node, DraggableMPTTAdmin, list_display=( 'tree_actions', 'indented_title', # ...more fields if you feel like it... ), list_display_links=( 'indented_title', ), ) .. note:: Supported browsers include all recent versions of Firefox, Chrome, Safari and Internet Explorer (9 or better). .. warning:: Does not work well with big trees (more than a few hundred nodes, or trees deeper than 10 levels). Patches implementing lazy-loading of deep trees are very much appreciated. It is recommended that ``tree_actions`` is the first value passed to ``list_display``; this also requires you to specify ``list_display_links`` because ``tree_actions`` cannot be used as the object link field. ``indented_title`` does nothing but return the indented self-description of nodes, ``20px`` per level (or the value of ``mptt_level_indent``, see below.) ``list_per_page`` is set to 2000 by default (which effectively disables pagination for most trees). You may set the attribute ``expand_tree_by_default = True`` in your DraggableMPTTAdmin to expand the tree on first page visit (default is False). After this the state of every node (expanded or collapsed) is saved to the browser storage. Replacing ``indented_title`` ---------------------------- If you want to replace the ``indented_title`` method with your own, we recommend using the following code:: from django.utils.html import format_html class MyDraggableMPTTAdmin(DraggableMPTTAdmin): list_display = ('tree_actions', 'something') list_display_links = ('something',) def something(self, instance): return format_html( '
{}
', instance._mpttfield('level') * self.mptt_level_indent, instance.name, # Or whatever you want to put here ) something.short_description = _('something nice') For changing the indentation per node, look below. Simply replacing ``indented_title`` is insufficient because the indentation also needs to be communicated to the JavaScript code. Overriding admin templates per app or model ------------------------------------------- ``DraggableMPTTAdmin`` uses the stock admin changelist template with some CSS and JavaScript on top, so simply follow the official guide for `overriding admin templates`_. Changing the indentation of nodes --------------------------------- Simply set ``mptt_level_indent`` to a different pixel value (defaults to ``20``):: # ... class MyDraggableMPTTAdmin(DraggableMPTTAdmin): mptt_level_indent = 50 # ... .. _overriding admin templates: https://docs.djangoproject.com/en/2.0/ref/contrib/admin/#overriding-admin-templates .. _FeinCMS: https://github.com/feincms/feincms/ ``mptt.admin.TreeRelatedFieldListFilter`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Admin filter class which filters models related to parent model with all it's descendants. .. image:: treerelatedfieldlistlilter-genres.png :align: center :width: 26.21cm :alt: MPTTModelAdmin screenshot Usage:: from mptt.admin import TreeRelatedFieldListFilter @admin.register(models.MyModel) class MyModelAdmin(admin.ModelAdmin): model = models.MyModel list_filter = ( ('my_related_model', TreeRelatedFieldListFilter), ) Changing the indentation of list filter nodes --------------------------------------------- Simply set ``mptt_level_indent`` to a different pixel value (defaults to ``10``):: # ... class MyTreeRelatedFieldListFilter(TreeRelatedFieldListFilter): mptt_level_indent = 20 # ... django-mptt-0.13.2/docs/autogenerated.rst000066400000000000000000000005141411214034000203200ustar00rootroot00000000000000Autogenerated 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`: https://github.com/django-mptt/django-mptt/ django-mptt-0.13.2/docs/changelog.rst000066400000000000000000000000361411214034000174170ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst django-mptt-0.13.2/docs/conf.py000066400000000000000000000211251411214034000162370ustar00rootroot00000000000000# 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("..")) from django import setup from django.conf import settings ####################################### settings.configure( DATABASES={ "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}, }, INSTALLED_APPS=( "django.contrib.auth", "django.contrib.admin", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sitemaps", "django.contrib.sites", "django.contrib.staticfiles", "mptt", ), STATIC_URL="/static/", SECRET_KEY="tests", ALLOWED_HOSTS=["*"], MIDDLEWARE=( "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.locale.LocaleMiddleware", ), USE_TZ=True, LANGUAGES=[("en", "English")], TEMPLATES=[ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, } ], ) setup() # -- 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", "sphinx_rtd_theme"] # 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 - 2020, Craig de Stigter, Jonathan Buchanan and others" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version_tuple = __import__("mptt").VERSION version = ".".join(str(v) for v in version_tuple) # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # 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.13.2/docs/draggablempttadmin-genres.png000066400000000000000000002420231411214034000225570ustar00rootroot00000000000000PNG  IHDR:4d* iCCPICC ProfileHWXS[RIB D@JM^wA@:I*vdQb(@֊!` *+bʛ$N9gsfPu9BIlh;9%M9LL$2m\u q: p]XRM-ĚH"YJ'Jl 82Ñ`㳋!^ 7ÃX y$)FDO`/@1EPeXmAbH B> {?qwcȏ:4+1D #yp!$@otɇɹrAxDNnDDejDs6d0Z`v0f nY7,\03sV瓳>A=Ú2"}[%3GKl!;c&Ǝa%'04[[6#9 $ "0O<]"dqF\ߕ7,wa]?G9T 8&z 7kp+)u#*P;C` sr `0Dx&êg\z* 2PU`ZM08΀\w/@/x!!t b8"D"H "Ef" YlA_C <ҁF"k4TGѨ;F$Th)]֠Fzh0a;EcX&fcXV5`-]_dX'L'\</x#~ ?{:A`C$ TBpp.;""ZL!fg7;}$IdC&E8BRi-iE@V!!T\B"$%w)j3'%£L,lPP(Tu՛O͢Σ6POSQߨxWUYWC4 5-6&-ݦt?z*^G?I@`2 cdTNV-VRݯzEGfQVvHZ:SA=Z=W}N5H53 3m1j370N0.1c|߄jnaҤդtLz;f3wLfgޛ['/0o2nmnQlQoqϒnkoYcy͊hnmvδbڸm6t"%U3-߶ȶ.ҮĮhѩ>;}6cJZ^;Z;r9ѝB85;rq;ott勫ĵ-mMwM<s<{|t,WNc,l؛[I#5>3m{oe}$`@YǃР`uBC!!.3B"– ׅu;kZD\ĺG֑Ȗq踱Ve%j+X68>f|3c1{4ne45Q5qbb]$YYStS)ͩ}'5eb,&Mt~GNLٟFHKJۙͩOrWs_x+y|o~%YwFes`;7*G(\'|)}vt쁜=ܴC" QTA޴L,_+l/@ &4j£%'"S>M4t鋦?+)e>;uy3e62;}v9sέG=r}IeI[JK>)2F 9-Zk9B}EU~vyK2-u]qqhٍk+++q%{eʷ:_\i5utlM浦k.s=/Z~oCF 7UlY֖-55U[[>ݖ/m^Yml:z;֣]w`۰ekO^W_~/b_~ ?w\|)wO&vjϝ 9sc>y M]/6^rtmmWܮ4{t8yj3¯]uF[7'ޔz~;;EwνGW~_~5[G*;0Gq>>~ҧU =w|~; t)S/-_Kɽ]$^/~f[緭}1}_ACGg?%}z?3/V_ZF|7;0 H8S K!Ψ@?aM!D3F P'6(NX4x!|x/ _A8Ʌw+pd t>m# $ pHYs%%IR$iTXtXML:com.adobe.xmp 1496 570 iDOT(5S@IDATxe! @ B 5z v\QE\UD(r˵ ^ @*'|gλsfvg9=[33{y."s2t^/%O@N)H`9 " " " " " " " " " " 5^4e :D@D@D@D@D@D@D@D@D@D@Dk," " " " " " " " " " " ]@@{ ДED@D@D@D@D@D@D@D@D@D@$H`v" " " " " " " " " " "P 5@Ss@D@D@D@D@D@D@D@D@D@D@j h"" " " " " " " " " " u@ $MYD@D@D@D@D@D@D@D@D@D@D@)H`9 " " " " " " " " " " 5^4e :D@D@D@D@D@D@D@D@D@D@Dk," " " " " " " " " " " ]@ W_ׯ_X_dIU;uv[7d7|p9{ 6lp/B+;Sׯ_ڰشi{饗|v)ܤML`Μ9n%-޽m\.]JkED@D@D@D@D@D@Dq՟~i7UXv&LP}ƍnѢEPٵkWճgOstgu_~y9wᇷڮ [+W .q{;/ܤMJA}knҥ%-ѣя~zZ?yiӦIX?ZB`s kSZش6xo`>`7z̴ϻ.UAj6ly֮]?|pdIw]7wܒn /^BE+" " " " " " "Pwu1cF V"P^{9 귻[o~ӟ`) ^Hh$w K`'>_,b@D@D@D@D@D@DI W[nz-YxO<]M`G\'$Ê+>N,óe Gq&.xS QO9唆uدJ裏f BlVq<0FupBkxLA|+N&sUO# :JRZ LۃKOvtFZ]7x{''V? n:/IC S!ΚU1kgv[{)@ (z wvۭ[7QކXr#jJ[mCmk1"g]aӯ|+K. XdCq3sGV[; Sc]kYQ`yPz%К4FV;9ލ3C3F"bG=OvC GQʨ~׺oUs;[moˆ{wDPl֎תyzvg<>}2oDq&3O˒.6lv[>lY%89rK1FwX.߇HV` >Vm; K mԩu)Q^uس?#8H;V!!1 /{~E`G+8VfrۨQB?yq/&dyCF<~X28gyu|^<"#?D 3n͚5咕CЇ>TV %7M[AXL+1y֮m:N;jK[G}W6o?iڄ ܿ˿ɳvR~_׿q)/qʣ4ko~-7n\Bc[KD@D@D@D@D@D@@?#cq^CjO G~*ٮjM߼*:V*=#p}pmFCX? y䑺 bcC;/0 1K)~YXho|Ç~(_kJ^uUK4W5Ĉe#JpV>FH l.[,0 ,x7*Zo}d43,kq7tSY qm\=FNأ?'M]v,.̜9l`{|\ ;ˉ!^0^SI;餓ܻLa:M4*/ɓ'-wy p I#}1 BōH;Ѓ ^zi/YG;$ K%U[1͟4<VJFx'ͻ|_WKF4^iPFc: CВ"kyjeWZ<`_bf C(R•fƜzJ۝{0o~}yKeѢE%^y=حKxOI G,M6͇qJNsx;Zjw,+ ᷅liVAD@D@D@D@D@D0'\^'Z޻wowYg:2W_+Z8ؙSMp4`'4֭Fv`caz['O[IKЃ16b5eKKmފE`ALWwB|@BN =IYx.3lhap{R}iQVD}ktp [ID# DyF# F8iVc}eYݔx)BDGK%aneJ~olcVj^?JO}SnwQ6Θ1#f`P62``fidzQiZz2>ٹox)iY't T7a֬YײڒU6 7Å&4cP~GLb|Q`ow\ɷ?8asZJi-Y_[~+K*Պ@S 3~ev#y+sBXf5--rt=zx9$Cİ8WfVw1܄J6#=zU/O'x’K!3cHGݴ8'xGhr c0u'Ƽ^k*?! 2 YsaVY||^ˉ7Cv 1*MK(pCψy1Z)qH8.08gr {Zw('ÒQ6&j;¬|3'?O2_:볟l,%Vդ |9NeC]y啩12"ț 1m&%6f&7Ņz2 xг4f.],:BgQ6M;.w'H6 Qs>b4l(}zI9lц j\|ũqڒUW/̟v>e\|ȗgZ{Y}yU.r~sfʵeqNFVZhn[&Ӈ=׹xx a< -AKX$|ce-+5+_r{gyfymZo{2N{LzʌdM&. -h3_c9'e m;X&hd !4AM5aD Fy9yme}$z8K ݖYuoyӖmiKZyl̴c Oiae}NG.yg1ܬXV~qN6h)" " " " " "5)y .TN`?0%&n'̓ M7a҅7ELHzjtdi+Wrގ&d B]'rRvqVqFY~#) NlF׈!ݯ_?$_\o ږNcYm Y҄dQ| gMr|5+]=SXrd /ek-^".Xu!gȡmy䄰Yߙ)+}> S.|NЖjҋlxP9ﵥ5Iq:OYdxn,d/2eJ\uRn5 +cmaӫ:nw2 alv&L!Dz2m1$fϞ'NtC "gԩuhMan#:_pD'p%4qy%γn+#+#hZE9&,WOV;)WF}YǤ"aVpdW)z=Y.yiZm<_%J1M7d2\&#%PW|MIPnZ8.B_WJڐ)|JU~r!+ }/* w"򟓿-Y^[~+[U " " " " " " MI^aN87bĈxŋ}P{!f6!h8I'B#pD03MS7a񨷸mN_: ؉'|ala﬛@6w0Xnh L^`V}QG=أe%P+n#ҜtI~6[>bYbmef]GCb|_d^^!DN՟J"idhG;ejR9&YA 'G 0ԁ}my F x?oUrR 笁Z'$$gؖrMس;&H@ՆfVU~ʊ_GTY{Vիm" " " " " " G6&cНdtmt026%TO(?#_kD& SSkEu؁8Syq̬,I@#R:4+z8QbTeDĭ%KL=ܒ tQΪQ=+lU5#pYwyn9p;Y$Nse K:uR_5Vy׿u7iYab5ַcy\d7봓7Z^9,/D$ _B |;;/pn"0?,K~o[U4zaONl2%΄5SB%MN+$ySNɥ?4J`/LYIc}'?D38Åm&0)K)gV@4;=ǸVgu_~w5ٳU[.ԑdX Ħ/)1a W(h YyvˊA>^@LoՎznvylh$DM4iR!qvx"80ޓ%+NۖYSIZǴH3tWc8ȗ}C;3}k_󣎶-\".0s熻K>QiI9VO +4 [4lذveˤ1jO}*PV#͜<{GUsY<{gF^[0ΧJ0N;SMnKPDp!7rIۈrm>!@l©Ò!M%i'P@>|43-L Q]. =;n#Z==O}!4Ú5k|C#Q?$m֬Y[ۼC>c0D̛ =\,e63m$DDO>9nU&E`OF9Q)D{]uUYYH '=#NB6KJ4ш?f_,`|YB$fi#&t<~ynkJGxdf<ڄm򋙖'm[#?:.!~" _p2<ޓ?OIRqLCA2FU54J\yu\c=ֿZtR՜&rNei`YVj e0xG668FZƉjgk[r4#-g|/Ǐ_2H>=H=+uk]D@D@D@D@D@D9 Ӌ=Dx)y p~:OAgӧ/;OqCEA 8 4<ՒarXү(u0Z2GK\,шQ*W̲ڗ&!!v!Wki,Ol}yh-:iO2 *"/Ojz^z% [KhkmK^L{cptmXrP1AFuAehiAooeX>4/zz7o/ղF@!䵅FCɲ(uڶÇ%!@xф{I3._N&oZ+7Q' `UciVa+)@VS'ibOg0KXϨ o Tsp3LsJҾ .=V@V_e=S+e[B T^͑?z%TՖyU%3p3` egM'>0L\/BߤY;1u((7˛|`zEjzo%jo@ a\e"P 6@ HAQ=ж.;," 5&#xZ 2֭[91e%A( ^+hwI˚1L BXzy2D<1 I4#39aqH b-i/v3g,E,oq(5atԾX:sCb9sX%K<}iaw9L^i:ky@^39JG!c@%ӵu߷JR`MR627n!}dwʔ)QG:,gM8>;[~+1 HS>_җZ> ЬbZ9424.F r! ܰh# GHBFT7Q]"lgKD>d3[# Mx?@,'or>4֭+؍" "Љ 0Q{D { " " " " " " ޞ9sh_r[~&D ceL\ڽ{w:#FKr-^@'"MԩSݓO>,YRg9ݑGjiSD@D@D@D@D@D@D@Tط#~l6n >;^ggxȐ!o߾[4u^D@D@D@D@D@D@D@҈@@PRHUC@{JJ#" " " " " " " " " "  @*" " " " " " " " " " yH`CIiD@D@D@D@D@D@D@D@D@D@D A@{VE@D@D@D@D@D@D@D@D@D@D 6 ӦMS҈@S0aB#MYD@D@D@D@D@D@D@D@D@Dh @FW^MOGk+" u!پu`okCBET$`+zWL" " l߭uG9O" " " " " " " ",Y 웊H}S¹[MOdKE agFa)h)a?0G {ZhF/zyeo+z qlN|Z~\_<[@R$TaMޑE.{Q8Jߥݒm*kq VjGLѢ(Gb4M`{񚠅49`qEX59>RX:@m/0fm"ԧ/&ӷcBQW;L:W۫R !o-ߗy(M{QRD@D@D@D@D@D@D@D ,\з7zDJvQeQro$ ZmT*\v) &, 3oK@H]7&k#!w{s3 (V#wݶ*7Kָًdcv=}z z+,uuKka;DMg}xzl՛n.vQ;V}\*#Ǎ<͖D-Z6Dm gH'oG_YʊQyMO>cĴ7,̈/̎+;u?2AfE23ƍ=:w@0xy3_䀘ޞU@^v?tݤZtZFΈ3pYmM&Qa&BeϝH.ً׸i,lb9}1K2D+#O/FoޯyoS^fx#<1XEv=^(<|!pO 9\b[3Ţ %5%k=YH%~];m['Lom io[vam]0x4]M`Wғ 9rӀIG;jtL +1"WgVn߬SN96l~EuE9dHEnb/0۶=iKo˻G;Is\[%B/,r "v˝t1lWGB{YdsDJm;.G}{OfP%횻t{(O 2K}n`N%YQaymj(,ţyxBߘ`^zrt5Gn'z~UD}ԩ#GuTp1@ @D@D@D@D (c{m/ȸwNv2)},a_| ׷kLK=s~vYe7\(D \Eh}3DfFxe~uN#޾٬ƶ[;ʟ[)VէvH4N8x]%F?Tc׮|Φ\zH;:ޑ*3<=ىɎ^Z'IT~[x㍾i+Ih~b!ab{Ox}QlBLrkp}s , Q8|#lni'u+7y6[@0*ݬū}H dHrf~&.t+PwAH8?a%X( WJ" ~G$Ed/klS:wbpℝs=-eH1#ͽQhoVm~G(XqmY-VuR(_ǥg3֭o}.=Y!̟?=[裏mV#E@:'w}>&A*V={%wt_m,5liM7<[f._t/oE=[PLƄ ,pݺusFL]+kV^^:th>t>v?(6yQ`-6 m1, 2~v>K {l$ZwrSQEsvG^vڮ;,o~ln,ӎci)]p~X Z/fS,:ZB`䁿}x C1%E~=$L@`-!}wP*2-op\ߍۥT80gya8)ږ ; yn7[1Ώv·q~ W4>c4䡖 >g~zg뭷vÆ g۟{9; Q-[23Gcǎ-b֬Y~+lfjNZq7-~_ܭYkѤI z뵊8#4svۓ̙z0Urƍo&lmV2M{O2͜91>}k϶ճ?nݺunwt'|r=VY" " " MH٢F}b}FD^Ց7;ءH\=nyC$:㙽z]>'B=@腅nd}V*zb{hDŽh"S6(磐+3 ⡠Lã T9Q"qngŻeQn" CE3+C`yEMvx#? &`]1إăfq9.w}(z竉^,+.+j(x4=7["o~9ɓ'{4K/_ o}kl^xbwavhe[E@::뮻w-!Q݀GQ=ph7{e5x'7 vr-FӦMsO>dAw۱l!CxEc&M(Nnp9xUov/\>}K+#l_V\8'xbGh(" " "v?H.JQ X5A6:´ E,yH\^ ;Dt% j-aS}HH>>ʁğ֚Q"G&*S-@IDAT0dK}!2c(0;#  xcDT[<ܗyöĺw\}8>ka0a#>||`N"=L}e@8^͒ \c/`&֋5py)9Gr~͈ZQ)Ӷ Q<ح7<CdzxХU`}wzϼzpU" [7x#T.%Bbwڵ=p&5.vyg뮻={mftzOWo}e#l?#&/uY 6n¦Zt61c49䐊o1$5jZZk*ݧZnV>;&سit.v?='Ea=c!:!f c(.qr ^t&~csb}BW|T0(b-qԙGfӢ/E"yTQX(\پW$w=5EM< Bͬk磳DKms{sǓsݒ(n<2ުAOkDGB{K;.{G!z޳[q6E6ݔҔ_(ғdsk `kfz6m5г^k~ҥ~4m&tsAl~+|C>ϸK/F'|%a O-?яʶCu 8w;x~s}w'N,4ѹVns9mm[Ո+ݧԻvL$כ$`P1ZTJ QwO]n㜗.Ƿxoظyy>yOU% ޫVw-Ȼ|U>_hR<.Fq-Y m3>؋bA#UQ[tV4d$FN:k*@,ߐXq.pc0˴p1 +yb]~N72'/,aﴗ:}Js/f2p`y˵×aŮx?{-6M}ݷBGԷpBl2Cq߾}cJ < E=qxG` @9đ]vߊ7a!\A(HlSꠍ+V8H/E{`B ļ ^쵆0P' ]s^Owqǹ~7{&$L_W-^߷vR?|xv)XaVb5}{_\wǸݏҸ.y{0`˗/W)~=p`-ubx@'%`.+:;tϝ"gMDxo3\xA0|iń F!`ܬeȭ{(6z蹽p[|^$:=ޒeERKO駞z:"{-V&j6%bJ]6&Π3hgǜze|ÎqdRV{^m-9L=3]~-Uy򦥡]\9e w}p4]wR`'?a8НtIlHM;>q.O!=>G661N .ʱ{BI+" " "Y 젇 \bI񦒄-+n{bQOK~Ӵَx؁Q< H`䉻~G0XSLq#s Vl}q1yn@=17rQ +? D#qi^/hg+vWoѤ pcvnjsΒ5Gx1!u o~-1-yaJyD.&V|.s~2ըxN;ǘmL`oP&F.q gbbq[Uwfxx<#,hg+ζPĹ;7' dMqwu#='s9﷌Vu쫧W#v@5~ӟ/}KY.1F-g晞AԄ\Yq;޽^#Kg?Z3b+[y m1blb52ib mu Ç?6]Z mSvr=E>pg{`9qm~/|ofoaY@}L1<?vMuXs^uO?OI;6s07n\HԍRK^uoCgt1à 'eO]xyfF}g{キmR{]0" ""@B-mxVcm?콊|EH)d%/~Br#vPh6)+QGUb==65-):T \S;0]sxϐgۮ Dz&23<= ㍎S`{8€1㸟1#,S}H};&ӎ ţ>߼= gaYߩ?" " "  FҴΎQXQBlVn9oՑ9oA^eXθ(,L( FQV^bha[}o7_4%x# j$~cs?׻{hѷ/}(+ܮww?(@؜,ƭoIe[x[G}]yeH*t] QVH_+JǥQJD<ǸfRFm\s ouDj#k/Ј7=4R?/JW<"8 ȃyj5714% nw+/0 >"[HzKbc`OѺ@%]a9jv~w˙E /0 /|;ӟtbR1h@xݎo@ ݹ瞛bR?2 H@eSiX4y%x.38`B R_e]Cr>s ~SEb澂k+c=?ӦMYGt\ ӳgO_DW=`@xb= \`8TzCǎIzV7㞂 1^[_ȳr+^" " " OXH6 _2_2K,&G&J~[Zm$ ޮvx4Q[XʃTU@[^)y܄)"}cU7|M#__/=K5R?6kD[˜dyW{- =md{V"`]ӲrʭV-Ti;KIc;iN<`_)&CџJoHX:-E@D@D@:g&Z\ʴ)j!+]I#94Ҷ>QzxDW&ث4+ הy`5qpΚ5g&f<xqn3<4may%σy>^NN'lO#>˃TU@[^o+}p]txN̠/V|+]}ʮsxlaڅ^+yl!&M&GJuO&]0e vJx-1;~iqݶֳp#_,yk_#?lm~W^.7m*,Mƌ¹).M7qbyq9&W[,Q[(R=[hz ܛo&ep9 ,N\Ig&3*I b$ˮz=vE7_x='㘕3<)M PN9CK %R%G"Оؓę8qo2Ĺ2B/LV!d|Ӽ"? ]f^a=Xv%qiu>ooHK_I?`vemO&&x>"[Nc`Zه'w5c XLs2ۼE1bĈB){[&`[`>|x<5jiW-E@D@D`&`]/ hTInvbB}aO[9q-9[T; GDRWHCS_&R|C*yryk=/Řw][d`䓟d"p!x>ԋi%7oxŝI*رc}L~>+ҷm|%[F0=|C)N~1Ј5?G?pwg~.sʄ0#}xM4Y*#,g~/iG>V江7 rknuJ&N^+]Ӳ˳M(ؼ,v둅v>|x.o{&bmD0-gvdOD}LJIdZh^v?=%B;mݮ̖[wmskX ?ެ >vYlEݾVDQ! ?xEjV]K$yfW$^*ֆXPE<ǻ8i"~+)>txVK{pr0&%'Hc?^f5w= |; ޵Ia3痝 Lf9e$%t@r%O`lPfKKFE &9e2g*'Zw%E `vь3?qd }IJ oAf8Y;`ߧxX=v~ORT3p(B[:a\8p3+wM4.mrNÃV2ò{N- oiy8zZ9ONm!i)'`Ivܹn:mx1 PT6yQ2b wQ V l-٢lY5^YVLvn^N#:?<~j ’ߘRi1xkOV ><.5-Nחx'# !\0ƍoqp y{3E!BCV_wqv[p㍊]w !ס?2SBQNL~<ƀys, FHO2E$ -E@D@Ds_XF𝎂:J@/n64*GFuū/u\JpDgӐx4Ϋ|~0pԩj^m w{ x!~xSOxR%K`'ၘrr o3wfrNx'Օw)=/)H#ӟ}K_J.~6u_m)N_1Oo,I؉ˎ<^zg>ύoC8E kxBC(22kVsm2ᘷ,PᏕG2387cyРAY#Dqނ3a:k2h^~lq+L}!fMz{s} 1!+`;y1l3 0," "  읬[&`ѝJ`W:+`:<'Xb!, ?x!PF\[|x~>~}Iya6Τ[}%b06$Gy#Gwqp̀ 1Xd_&" " "й H`WLS 앻]] <~'Kl>@L+)DG=.j<1XW oj\~r& "sjyπyIC[0&V'yk1 C9y'`sbi\~E`2!MNYh@%`q{@mmN^ j  m" FwW\q_%l Ujjr""m@W'=Mko6dLo~$1ʿűKv+A5(" " " " 5^6eD$w9vx*x{[D9$6޵6>Yqi-cD%͛Mx,gx?w xL{ZJ#" " " "~ZM^4  r$:x;.h"Vwf&`L(Y2oD죏>l1@hg$7QD@D@D@D@~Z--׹>>}=z3fLKWq" "Pއr!l ]3{ ۽͒ތGGmi :D@Tط#_~ϝ;ƀI;03_'"aLr衇I;f oEL{}" " " " OK`\G@ln7Dznnȑng@G"38n4iRŦ@D@D@D@D@Zi hB@rۣLwm&N ժ nԩ~N5I=0h3Gxt&sAD`K% }K= bx<ىެ^ BbE@6wnw_j y/^^{\7W" 웋(G%}" |t7SB v;찃 }3U+:a,Yϟ?ju!:@lGDhoۛRbd" "#l$oч;wCl_h_[T5lvСC~" " " " "LS`_j1cv[m㹠W^5}mn}}&MxV^nFi&^{cǶe<$wc@hQp8UP6mٳ݃>wM<9T_n>c&"tuYD@D@D@D@D@D@D@A7XeРAn͚5mKnd+{-]@& c?^D@D@D@D@D@D@D@6F v[l߶n=X׽{VɸV t@^{Pxt>ںee^D@D@D@D@D@D@D@j&HܦLֳgOwI'4VXNŴE`ǣ/o~M|43|,}\2j2ڵk… fbӟr/B 0 Yduӻ[|sO~Æ %?O!C"L6lo4  r$`)##GxL{18TpdZ&=製m'L[fӦMs϶!wq}G}{Krĉn&}FiXhjS[ !mi'HnSN9{/oBMCųܼنG{޼y>?ݺus,7,98`{xA7j([B9V;x㍱W<ƍ=$,AZڈ0`x>nƛ "`LT;gwގv,tN=^z=c>=o0X;vGX~ED`3jLE!N#Nq]zW.kku]EeN8!'NE=}24B4FxNڣGilEf%v7sƌ' pg`QW8}AYagϻz޳|~G PnrJ!O|`xz־ =__x[88$<+Ŏ^Gډ|Siw+?i$?*d"H`\Utpbns%lDPm'm(yqv A=ia쳶e+lCX-xfa[m[2 ?f"lKO:oyҖlng I`i"tvmX^;;M x=&(y,/q17Cq;N+V1a||Gy=,Dֶ<[۝=o}mСc`UzD ;<4|='* رcW1b?aH092؜$oN[D@D@D@D@D@D@D@:FJ5 18OnݺdEpf7|${{Joft%#M6SC{̙><  ֮Z֗Z0~|Ϟ=}XJm_8 m|/@ &Lj^9rde(cqkQD@D@D@D@D@DF3;u3hB<;¶`O :yomݜQ`ǃ݄0cAܑGi+zۄYqAч&ËڳbЇel}jX!.;,oD @ca mu;BauB !C_|EN^}5P$s#8o)Sxc=z}`G`b6'^?9-߿;C7y}ԩS.C';5^1[d˲}ezP0#^pcep -ZL_ $ . soo'X-EH`o#i@Sv"NU@&`׍؁w{15I%B{'S'Mz[%"wi{1cx;ҥKb}x7 w7,x'Ac=6$i]ړ6Ҷ 6l:n \ՉtJ|hooXƒP7匰0&Γ,4DtFhv}v2G߾}6 n<Bܳ2R=/RWV9Ƒ:V-Q7|>vqdzFv!F]ǭ:NIK` ol$ۅD{AsvvD@D@D@D@D@D@:%{N&XR@3ƣb muZ{k1It\^`'^̙3ݜ9sŋY<1B1mytv!^͞#{LC qC-=_K`]sMJ&"б th_wO< "lfzcL"a/Z2I#=hР<}H`ύ)V{aBVwlߜs|;3de?s~_gf;lFk &E@@\fƛ%k5PKϬC9$']H$7)ޮj[RN/1}+^va|ǟc<'?L)[iw]7w\)Ej=_K`o3J "A tXGvzrL ѵkW)\I\chz{'Y͘1 <ϻ_%fl33f$v&iӴo6iI:4;4IlNl6ib!s ##|@g<{K׳ѣw]K.W]{QQ>cƌ&#FBW,TyS =fHŐ~"ػwogSmt1G#77cO=멍9>Oo_@@@RI |MJ± @c \^XX 6~x?QizMWYQQCUV?\ӢU^Won >7ݔ"ZDWEj ߰sŇ b>tyTp'x"yXmڴ)Ҋe˖6vH    @k8YZ m*sUz=CGy$MHcJ1o>1ѧT*3gՂHUꪂPz6c ?t3[   @"Te[ N)=:&^zɦOnUD˗/BӅ ڃ>&SF$bg|$V>nܸj?3 tU]Zvm\~OWZ/ o)_@@@[ |M7%@4H=1/M6ͷId61-x ) RW,yk{M-^JKK#-bTݮ1ءC>ݻ^$ַewzN)e&A@@H@=S @R.`W^)S4Hd{cJ-1qBDT F7Zf{[_WSN|w=Q}ڶm7@@@>_+ 졙SU P2ax.J{|СCb ~:v,W@@@>_Xd>VDK}ԨQ1M˫ڠgώ8T!Hļ)` JuaF>UO| j[gUe+((q@@@@. 9ի-jօ2M>C֬Y =SN  `M=savTa}xW^{-cy<>c֭[%"   @!rS`}&m?]2ުULsТE ?Ae @ \>rȘ̙Ӡf͊8<ٳgL˳Pj$bX_T_z #Ϗ9>YfZ/]ԯuںv   @F "`W]rvvun:u^BF'NRѣ5ofh0m=sa${8_7t^XXh*HcTO /ɀ3gZvng P' G ̝;A+E ?F$bK_R>vjZN:Y]vw'MfmڴCŋU^TKyCǎTՖ-[ԚVgee`|E C3`'`d?\Id?l'N5`J%%%O6Ʀ{   T'|NvV1/Rs]weUy˖-_j+Z#0O<ٖ,Yَ_2dH]f߿UpOT@:x*/QڢE̙3U}z]5=Ѿ/xm۶>VHkন6_qDQ޷ox   @ "Fa*XU<@\i)V _\}0Ǡ}(WE* 0=~_GǦZAFx?ZO˗/Yo`曶o߾`5k[4+}:_!4ǥJvC?]vZW?ԩHRE0!i_R&bѕXF{,ZLΛ~TK??ƤoTm;v7ym'Or,,   U9}3ox5ʇk۷o7V|0 4m 8І ;w[pWhHTaW0Tzj4ދޏgҤI>߰aر/z e0eʔXt ր] WϞ=g~~_/鼵_ykZ>Wוs肄/\cW}샯@ÇNrq#,ˮ`R}b-ײ:F0o3W)   @ :`I@z8r>eAj]&L={߮^X~;zCf_^^nuR]E {u79 쪶.2F78f)u#z_Hۀ]Wբcf]@IDAT!u]Aӏڵ_9~\(|#!`E+uaRg.8@@@>_7t+HWet0*S{yT([G   c(++9sG˫GY]F!u8`omHٔ ;/׃TCzPvݰU=ʣG͆ wp\ע+`3z"^Z5uԝ @@@ }ש+#?u꭮#,ײ t/@:XGnکS'~MA ֩.ȯir+] oyuی^6xWp>{~?"Ńu:@oF"F^mH@Ӿo$ɞyK#   P@:UvMFUWaܹsܹsk5S[𭕂z*VuO蛕rBKu:1BzWKUGvmcʕvA=TZP 9k yTH]? Nرcp >[G   `,K.5iS\¯v=W'Z^^{۶mi&X-`&MB{moԩ]<^C;bX. )޾}mܸo#77ׇ999>VU.(hTMF5)W*;,Uӱ oCmbn6v9XF'O}UzAmvn'jʸퟃmd )|#!`Wg!  d@T=6-vuE^zol"};XV_f+V[iӦ/oͮ[. )Vjy_ۨn5-voɑ ڧ]Snڎ~*͇~T={W.d(\OE/@: \{4?W`2GyKH@@@RW |̀]UK/ ꪲ%޽cƍa݀SB`(|6)z]ܣG!zk}>Hk;[9=m/^ѾmΝآ,WWK,1 FЏ]TG_ U=T/y*L_ln^UV6m4W$rn@@@@H샀=W~i֫]mfj.))1m gZeee]ډ UԫuMwu:_mvtj Ѷ+оѾxT `Ox@@@@4Ȥ=uMq\gPawڮs9{Vo:VD@@@ "|΄- P{ 0|#!`U,5cRc8 @@@>_sEEEEC=)gz G%@t=NF^ykdpv   tG]F7d u `V5HثIᗘ @@@ m 6))@HlՙFgw   )|&`@ j[$FB^RǼޜpD   '|&`O@ 1q:HlՙFgw   )|&`@ j[$FB^RǼޜpD   '|&`O@ 1q:HlՙFgw   )|&`@ j[$FB^RǼޜpD   '|&`O@ 1q:HlՙFgw   )|&`@ j[$FB^RǼޜpD   '|&`O@ 1q:HlՙFgw   )|&`@ j[$FB^RwnJ9n.kz!  @=CH[I)W9.u)nrZi {ؒp}_a.]Wf=~ ߲gJѡ]ci&^H.W~0߮\vP9-럾ǚ5M1%]"  )|&`@ m:`?uض]vgYYY$߶˗/[YYMlАֱsA|#!`,%ϼ)|js;=:kTׂ'Njkhş䌤    @ f8PH@\߸nlnݺu͛[f/(dWqŋv+w!bd{δ>v*C|je7k~?rlgS!̮pg[kSOIj_'   i!|&`O @6`߹m->fVZY-,;;7 Gu%ZΟ?o;w6pȈ`ZHkeJ73o5:~v߽Cl}vW9    PO5{=Y ^ mUZ.G>`W޴ BvEEEv)?izL|#It_tmpc4d{lBL@ Lx]|%;+Zs#}ܛϼ̮&ȩs/XMꕷk66Ed WeH{K~iy?x6>j{XvV3k%?ޒo[W]gb׃Z5~NZȯyv8}mRM^nkp,|E@@97|s p@`&7sÖ0ןzWT^QQaŶr*zUY$QCAlㆵ`۽k{lݳlBܥ_XbB _F|yYi9++-Y^ֺuUnN;j3o-7}j&n7lN_Y3G[{FG?_m//^uԃ1ms6DnަGo7}#  d@=癳C6`_46c 05b見 ,iiIvCEvj[r^ҽGO7a0 nj[Hعdu\ŧ^;x`8qJϝZ_mzZik;wX}W"HF;G~}-7mF/;O?2䟫=̗7.T\v-?^\ߋwUӪ` "T z-k   +|&`9@v ؗ.M>ZliYYYe^|.\` .f<Z7DseF[0U/];WUU8l6lhIWwº lyY;~m[nr;~;y򄫶/UFsjk:uz[m[OVХql^&1_ϼCf.`U7#YWg1'og._jϞ-{F,>xc2=eg٩r۸in1Ho3@@@nB5M82x XM6ͷId61-;=I"vU>tּ^}e۰~>[`t3m16l񮊽`븐}{l7lúcV;vosb颀PuE.hw?YPе 4F. aDR?{}-J[9yΟz3\̂лI/}tl;p/w0.PޞEv߶L~zm#|.Z.|Ln9^}m   @D |M!di/_2eJK,SB$zrM썥Kluv;ڰhqWe#F;bni]!F뱮5d|#U2\z[z 6e6jAўQy S3-Ek"a};mU=1K-aVnR]acHN6sh1UT^/pU^l>]ֽss    5;@f `ͼU]W;'۾]7mygNfcOyv%Q! VgxVuU+lWE25k+Uej];&[})m,m?u0g8,r^61 D̺}͞8оRP8 7Ƶ]s t"s1_b{O]?^g    |&`ܬ졙o]7UtV7qɭ^+9SlW-_⧶UkpУ6nDk>W+q>nݜo(kW ߿X61e]ǧ_گ~U7s}\{'e?W  UBnutϳgSOus,u^.]֮GǠ^S:(7j أ߫m@@@>_߼8snvП շlIv-WhwR`ڵ.hWp0[79?gk׾鏲p=& dWǏגmvzwǵUu<:lߋ/Xɮy~y]Dqx;GƻJvg ٓ5绀}}E[;nڸ*w7 ?@-Eweyn]X[;ғ=xO_7m7:]-@@@@V=f̘(r7fdAm"{+/.>m] m6vֳwg]7U䣟{o^ٵ͛7@{Ȑ~;|X_ݍ^;nyz[d߹V1{-/W\Qwӳ.׹or m݅\E*5L_)lQݰ5Y#{IY}G ժ@[%`A6sOUt7==x__uC￰_yYk>WNvrdany@@@@0#`)h \5mڷ\%wMqoU[oEmc'KvkW Zyy.h.*>"{m{mB[z_pb hϗؾN:y͍ۑt .Lt7_͵ݻv;wls[$׃}6y w9Ȁ]\P;M6=溟Hp/%`ٵ'%wڷ [б=Mx*:mb صnU"6sޠAݬid9     p3 fvUQŹ7.7W۶;'Oao5Ug]lDž\=e..`W%͛m2Ǝk۶mW{v߿ ڦ ,eK󮩮$S}Ee[Ƨ۶nbW*҇ nf9-r|e ֑]>֯*mr#Ȁ]|MwK'7 Ⱥzմ~]gXIi h;\Yedec߹imZ{XvvLmwW\r7@ P#<@@@@CZlݺ߼t =[l$WZEWMe4wsUB,]-ZTE^ *r! |չZe(4iK_[hvnή[ vnƺon@{oWߧ_ݶyNj]#r\\>{7S*/̸T1q]m`IZGg빾v[?gFVpt/}tJ$ ` ښ?U^>_@@@@t 7t~Q[uWmW߷]7mګ4n릠ji0>ւ Zh.T!ѻO__^&iuim޴~{w^Sʹ;'Ou-hƚ(߱c-^*wSaM*  ԾfAwy}wۙf᷎OpU]AW>1-v ؿwwjkO=nحux/ىwCvS}xcA=/j7~ZU މ# WwLzly`[F/$k1@@@@6`_<7nnݺ-PjK.Yii^cfMUy.Am:!x>\ 6YvQF{_A/U0 P7> 1vk;Wzr9+hYMYn\+ׯ ׶E[me]\DD±ӥno =ͬUlk=_o? l@@@@ 6`߲a |޴i|AիWM%/ٱcࡣ6l.{y}";gyBFe]>dWMFsF72UeF gELpѺMy״lB@@@@F mؕe6`S-oȮpʕ++vf얞bCP=nr.ɩZ=Mֻo?sx6}&tU]8ūl[l=vLosU79UtiL X@@@@n +\=[Swn|]2ԶD9rN.CULKlcٵOYfQo2}=6bToeeʯ~S۸~_UCV-yy}v9٨Z. njᮍK]zǦݍU_[U/gl_ɮMTu3CY 5]}tWף] 5iw쓧ΰ[ [53o:V    )8ϕۑC{]lW]cW5{]w̙3vֽi&V*ŧ}mfQ`ֳwkӦ//+U+?lQFG> ?a Νټyq>dpµlPź.,uwkv/]P%**`:c7fϞ]x<s7GvtB1tC-ff ا NF}-Y~@@@@@ 5:`*O<~ҳ.нV-+wm`:ur=~}ZU͟-)Ç|{] ]>W o?>]W[n3Vx^|׶aSvѺu_>e NK-U쥥|-P^GcymwI,;E@@@@H=٪jRQQa TABsV^ܪk3nDkpFn]V. ?B_zV61{]EB :Vӧwg\U}{|."hb@_W>mĈʅ ϼ%/    {ȠVZÜp}be7mygNfc}ΝV1 =TEYkUys~.\+'{ZT^s]z˖-]6fxUOcݧν}~DC@@@@ `sԞ?{˦ %>޽g/_~]Slح# [톬u=UW~ek֎6e"@0h>\˚SmFa{kWўyK@@@@H{ j}shZW^ϋݍBk(HW[2}*\ \*/+]ukmǎvB梫X Z4wjSе 4F. zk' >9oq+#    @ 9u jVf[l^79ݵsm^쏲}~ 8t6lІh;~m[nݻvZсv +//K/ųOssS֣Wo?` z tlw=8'}M伅c@@@@yDCEj/+١(i&LrDׇ}k#z:bw{}W wAUVVʹhmsֳWݻMPjrXr     9qeZ\T-aN?f7縊(d3e#FZ.eL&M 5o@@@@@ O8:$xv    @ 9q&iu-I@@@@  `s2 jL[-    $@d ՙ$[@@@@2H=$0I3oIg    d{IP'`Vgޒn@@@@ 8'6N$μ% "    AqN&AmIZyKf?Xf#    ޽{FƌƚF<[Xf_=E֍U=V)C@@@@R.`7D0. 4GlyK8@@@@H]p<\;aHcS6xbǼ%Ƒ     p3 9q&iu-I@@@@  `s2 jL[-    $@d ՙ$[@@@@2H=$0I3oIg    d{IP'`Vgޒn@@@@ 8'6N$μ% "    AqN&AmIZyKXQH+dׯK.Yee?vi]v#F5SZO:ykGյ% ;G'\>tY[w@_ {ܓ=3@@@@Hۀ}wz=|piӦ^.Vz^3f̈IZ /ؖ-[ 6~a 5o?w}٥U6 2k=5`?rv<+ufF[lPva[||+wn|.ml[=MWl?Yl*Uޭc_CUYt✝+路7`M]dXm{vM`۹vX]9|v:cGNECˎծ *os^g|{t*oW     qi71U0)//7Uǿ}iju _z-[:x?={ڤIqƙUϨ@G5o쩭괣ߚo]_ƤS,{Y^iTGխ;nhWo/m6: ?loV5XC{Gۤ[GބaO7&>@@@@[ m_|ѦOn-[˗… pB{{6jΝa^yo߾W`T?x`{セUL6mv1p,yK'|-Η?6Z" F?ϵ1=SVl{#{ݿb?Yr"jL-Xwo:l1{ϯϟ|hz+?y漭yܲ][O0nGZWYvrhr[6?6oVs0mpWk&ǿ>g^k-B8}K   d@/M6ͷId61-|0YNdPyUEOs)2F0;v1cSUQ7D[0KGW7mķ WmDzme~4v٪7@uփ5.A^S%<~z\=K7:Qtaɯ=     @ mަL %KiԪ 6ŋmݺuv;{?mZ^l6uT9r3&y۞f w7˨KZ7;=[Va-[dor7 =w&` v-2sM.V7+ص̈zkί-`W?H =֫4tcJkѼ}u } Y@@@TE*ؓkeeevq۽{>o<۹s?ʁ̙3Mo]t͹vr!ZhY/MGʼk1XٴdumC 6Wy_7ʏ^Z]C=t8(W@4XNwZy9mњzWyqN[uA}Y߸4X^_]=tz< db,;fp } -ݐ3z朿Yrtn:樫NhִDs72(+{wmVG ӻwn}]WXv-EX{֑@5E8`W˜sYCL3ww    d{hb:`x8ߺu-^VXa[lUӦM#F*?޴ﶡCZV|J7ڢE f~M:/v1dM;31`ש=]f_rUuաz]).\y_HUתí^+صbkܖ-j!wn2WU[؈}nhjaݰ;?mϾ=ߊUؐ~WA[̿ZJJk~?'Dj]p.27ܸ ݶyI短Ҫ~h{S {y-[;uۺEzk 걮>++nW'O.\Q?c JF-c42t7L 5GOW +;1#]hӢCp୍)hVX{F `zlܐ~1U?~zD2ZK_bNU_~꺋~w~ XoέV&\أux   d{h>:`?z\Aۼys1}m9R?uaSSEzSeW}*ݧOnwy'bJ{YePW_{'KPKi,TykjtR;BV65]XKVjچ*Ϲ_z~e\k[?Rݷ4sysS_vemT8`|E@@@HM uЮ]lMՇիb\7 U2"&Y.8G^@@@@@ 6`߳g60`o _rŷXٵkׯ_LsYD( e     @ϟ-[zv'Wua]׏9o(:l0kժULOP*ի};<5kZ.]|QO~[v?czQKǏ̙ceeemθqlԨQ^fIDATe3ol@@@@@&Hۀ]s+dWs(.CU Ϝ9kUǼvi۷om޼_9rڶm+/[̞{9s'I&By]8{Ά QӡCkѢ1d.Xy@N @@@@H]JvL?uU]p^vǎ}OX+׃}7U8^RRbEEE=MAAHP\U G??>`W‪&GrLw@@@@@ vc?ՆY2V***|`=++ˇzM˗/Eĉ}}NN_Wm+pkh] y}˼    7{3ȠVAZèݻ}yΝ;Q8fΜi޿߯]b3&y۞Y@@@@2E=ΙLdPv74S_x[8[0ʦW^6zh:uo{Ϟ=c!kQ'r2 A@@@@c~D>`_jKQoy^cƌ|ƏWkHո@@@@@ `szԪE̹slƍ+n߾caMN^1biӆ1D[=v*     qNbZ__l6FC`&M{=i=/V`5@@@@Hc8'/Ao[;v֮]kرAlVXXheL&M' M7l~Ó;    .mv^Ѡ0==]bbb$22ڍBv ukll?uuur6!eԙAdǠ„?8nWj[iK3|i?#}~{͸@@@@tۀ(-qI޽%66Vm~pï!כ2re9xMMqA'_q>cbD8uBP)w^$I}i@@@@ 6`ߺi _ fv #""M[vɆsdEA9wTP믘28/# :I tԸ5X' SLpzk7/*\g{׷w=<}8+Wq]t~3>݆^NQ͵I3Mg(`8q˔sjCI%φ    @Wڷ_3gHjjݰ$̍u&{MMʖ-[em{qofoN@@@@\ `A-:ݭ]ۯsraS}]RVcq`RS}5F$ `w|\]W&\א=p{e>Yw˦z?߳zyXz    tvQ̀I*"ill$99EbMЮyUe]99假&c^Rk2)9W,2d0雜,>O@kHeI/CBvYWz{ds[X40`35xo7FzܛZbe)2=[$s%BWUeC@@@@  Fg^Ǐɾ}{lP>qd1RκZ*fMoـ]KhH>{ }RSٰx#F__gۼrd%ߚ[` 5/6hzi45[5T&yUؤK^匔O,|@@@@.!@3 345ַIBB,Z|h ztTTUU{۷4["3%1)Q/Mv-!:m9721{ 2ٚi*l{J]䑻ˤу'V_ >׸mg˽qrǬqK)әxp ǻO76yg입U7W}ugdW̆    >#{]]q~`^ٸ~lWhgϽudM$:<W\ɺȡ9;N/X"'H||oS{΄/ܳ[6{,{̙7ߜwLMv1d?R4/햍Nxh)֖CrLX`z=ya2̆/)}JYtUh-_q?~cr@@@@  BGewnxMW**%.>N-뢵Ե/ZF̩"{vS>lv-7sy[˽{,ddLJjSV1`R'>' fnEN5lJOuu!O=SǛ{aOΒQCR5/o oZ ڤ#_` O l    tjGge fZ;3[]/ՅNLmM Ĩ({tn#F3_(3gͥTU'@z`j|3 _ZWQ-tJm]'gK\L VTךZh7'7.ZztùfM~ s3eiX9A@@@@-no۸RrsK>}BV^*++e۶;giPcҖ=(N9-)ꢍVK[O=:*"֘+H-}]T#    6`/ܵU24GD8 횚Db9ydM԰%DLPrR[ƭS:D#     m~icNJXѺ 5\oll-r!I!̶,riԖq    t;n\,ǏŸ>Z-0׀]믟9sF_,o2 6 jϝ+=;%6y?=IHHEo)92` SB#Z*[w?#;m_= 7O)}l%yoKuuL͹Yr)^oe3n܎C     mcuTΜ:*%%%cmtV]/++˦tQ-JiE9y۷~)0qd1RڙUUu&y?99|Nf̜- K*}eF0aOjj? 9zډf"    @:`GәϟR]Yaf׷Gl>}or?7`p3=ioP+"[f4INNΪ2<_Cۀ=,5///3+^ 2`Q;L'A.ZSS#fĘQQQ64}emfKΘ%)ggky:y0q #@@@@;KCjPaJL]cGe횕rAcɂKM2rhk &qk[˜    "@p$C^|YN:)w뤠}9uhuݴ>Ќᒝ=Ư/Y&Сһwp :|԰<V0<     -@4U'2&`߱}xuٵs^ Azj2y4Yvr6} 1{BBBco     eP%b*/][.rz~)+-LIMEN/]&YY')1P[;@@@@vV>=}_fͲm&9Utr ɝ9L\3{}i0־^p    tgvի$Lɹb)ؕ/yޒÇ^).ٓsd4[2W^g^q뙊<5    =[wTP[[[+{ O"^fNȒ,[&fKllÞ;jz*O    @ `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0rQoYYoJJJwO,x~@@@@ =zdڴinٜaǎrv'\{R    - ti҃v~`wb<}2R@@@@+84 vljM{[h     =Y:tr%xE@@@@ &AC@.g\Y@@@@H`:tr%xE@@@@ &AC@.g\Y@@@@H`:tr%xE@@@@ &AC@.g\Y@@@@H`:tr%xE@@@@ &AC@.g\Y@@@@H`:tr%xE@@@@ &AC@.g\Y@@@@H`:tr%xE@@@@ &AC@.g\aJI"#IdOa @@@@}^YY)ǎSNɅ '8'Cȑ#O>guz4MCo76_r׻U'/l3*$>+<{|: ]}]off_~b3o@@@@hnΝ;A%11QWիWUUUR^^n2eAmߍO q\'U`Bum򅻧?{Z5bSnI D"#ؽ(A@@@@nw}Wbbb$==ݾFFFv]uO]]9sF[n z&{8& q\ _mዏ_|66T?=;TΨfv|*ArN@@@@ 6`/((gʸqw+6XQӐ]륶V._,KvvV_1m`8[K?<4K7zMw+?Y%啵~CkڗY.B7l|@@@@pAyyyvalzDĵm ռɆZ/j8:*_Dx嗥%++K nnjRBMeg,ckmwԸfGk-`=TxK_s]Z O$wZ3SQ&ʒ#%}*kRKހ]O./WL><$k=;|L.Ҋ+xz>2m|$E{N{)x    @ _z%1c.TlޒdRKKKe֭}S:~hЯeŊrl2ɱu5v~sZ>ZfKD{ݯ:PlRR}`֝~8Lq1˯.Qyݒuwj.`r/?{%ߖ=ҽ='|ndYa?juW2}`c~|@@@@pADgk3CLl uv'>nVm&6m?'O}Ȑٳg۟\z=ŋϫ?ǎsIEEɮ}ʠAdȑ2j(ӯ_N獞s<湯{Yf3{޳.}2iY|o\c+cmt=k'Ud7=ϴ?r}֐'X'`R@@@@%n+h"544ȕ+WdrwuPھ 0o ogkgtm~wR1:S|Xvmg?~\JJJZ*F7 ʈ#DgO4Iƌcv"͵ՙB9n #Mrohɽi -Mvw-GLyݧ[`bL̷{G._ ^agW]7({''^%u sfr 2qTm\1 .ؒ1- ӿ6mퟖYjwĦu #Gƍm+gΜjuu,lsʭjw'?%fkfѺ3uu)3c\,}͝2L>-=k˾{O@@@@. @3¡ZFl-AϺT6o,O?9ԙ>̚5cj^.|/_ޖچh-a͛' |-kf=뢠:C]{hy 3"?BIwݷ{0 G7䷏ݏ    ]@g:3`׺%W4NKKkpaM'ۯո>}<#vS 5`Lm`%Ym=tGk[ ׵_Zww-~ixյ8]{~kv+Z]Qy؞7m{c6(Iyv||Cx߀}[sO0%\ERݴvR7W}zo-,h; fT%bY/^%XcN>rH[g]R1?~?lv- !}EEm|ĉ>aVVݹs^Z^}U0ZTNЩ,L|@8:_45ju]h"2e]5NĞV4OAd/)|e3-z! u@"tSo'/-!fnZǮ5i5="   %@#Y6[kt ov[F,q ll3g[gk|y뭷l(ܵF:+=^˳?Z]_0a 333m +ϟ%m-O3`馛:C_"۷Ot1VxbzR?>A.rGLVvگ6[r *3㼶цtҿ* Hoo?[W|~ WwB~ ݙL     K>kYqڵkwߕB;k}…-:\v UV-rd`kX΄/((5k{gee-" ,id?t萭!aׅJ~ZEgkx uƻn!~A:^KSNb;v~Iʮ?!~ð:3#U-zk3 [i>w0[J,*:n|\=EL:ZY3: ]kϟ!3ֈ_tSD>XtUf>QfeدY򗟘f@@@@:CGv-{nxkv u6+Z\&stӰ\^˯hv-Ѣ< AgϞmo-ҖMCޖٿۖv4Ç:s^y40י3>KKͼ+v}>۷+c;c\p]TkǴ \o~$[tARn9oo=&ϼu}w=.r0_04ꧦ|ns|l={V6l`gtOȫ lo}AG u-Mm=ϳШ?:3^WݴֹtZsmS:gm?5HיK.e%h4p0_ Xr {=O~@h ]Lw > Wpٴ_Jk,=}b=se1={Jwbܚ)wO^W#^mSfYd7MϦ}o ,*&!;e|)ϼA@@@@mo۰Wlk P5 vm-淿-Amw ؏9bѣG~Yl BZc]g3F֔P+l)zv_nAk]KDGEJo3<5)ޯzC5462:{>!.JL};Րd%@8owۆ3    ntۀ]<ր="Ådgk֖љ_t.)2k,[/^Cvejt6Z^u yfo"))S"&.A@@@@,mv*Z!Z\Kh=q3wEN5`ENZ2c ;_.֭[EIX27ZjF9ՀEN@@@@6`יӅ6MOOmeK4`̙36 ʲ3,y-m6[FK999/tO?-R=%Z\bZEk2.iii& "8֌?y^zQLJs41ywZ]C[FF uV]խ=V_i@@@@@ ؕSgF i ~\:-*X˚h3uv[{Z-rE9v옝_ L`wIP_@]u 54}:;_ mD.2|kЮ>ɦZ^ebtFixɮ3un:+_3s]DM sε׵[qsϴ    ]Kx2ՠ\Kh]u Fʕ+ƍKX4ȖѰ;ԛ΢6 uR~;^d/tӐ߷.jKnn77@@@@@=v j܍ZO}ڵv'l>|  غph[kD~]LUKM}O9bcxY-no7@@@@@v j,:[^[]7 ҵVGEgk`Q50ZE/޽{̙3XYR01:].7t]r2:;wty(QG@@@@á ePa.ZPP o]tvS뮏?.rzwHvv]<#JxXXׅdlݻrq)))<:k}2b̔I&뺀s5{_#    @ `w8֡juS-â7mdtVn:[}'77ז_qiELyGk [{]3׵ֆ9r5"`'l    #@p,CZ<_b1˖-5lɘ\,TgkѮ3F5֓u-3ۻqjG@@@@@wTP͵T/,YYYr=0:[MkݮR:ڷPg5    $u匛K4    @ ;LZ.]θO    $u匛K4    @ ;LZ.]θO    $u匛K4    @ ;LZ.]θO    $u匛K4    @ ;LZ.]θO    $u匛K4    @ ;LZ.]θO    $u匛K4    @ ;LZ.]θO    @ GխxlSRRU{zg#    \3wڴinٜ;Q`؃<@@@@hI#3sZk_^r+y.?:ռּ {@@@@@ xc]f;{Ǚ `oW"    5v ]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7iI.T\+5!ё- q1m>nW͇#礶N+}O} iAFGɔy<@@@>`E\i)/ 5m2(m 6Rz6qu7rym>9Tt􎏖)0{T^G[%M_M"#zxp?pB0n U}C;L`z9?үoo`Dg`}ymQ:.c~Ǧ,]>o_Wzo'`cC@@@6`יoIOO?:7 5\׭ə3gxSg=2v7wLzr5u@CIc$91^.UY3+J?S߀AU$ȷߣ'/S?1@AdCwj{\iljd^]Dޜ @@@@-nW 2n8ݻJtt o{5dןzSZV._,i2nBV_ j[7wM|¾[arߒBr OTȖEtX;[:_h)`ײ9?R*zb̎`+KomCu_O? ="   @WMe2l0kqmAĶ:MsdEAOG)?)Sh9!K>nɚmPԬ@G[ rԄJDKƠ~lxzTp\Ne֤ k.VwJ:k>:*ҔI2fX?55uP~άXʘkx}%Xk=rpuGZ~哙>0IDATIz?m-z]sLyL |Rp,.( 2t`n>\G<ހ])5u 2d`_D"29^\&jl-ϠD  ؽ|A@@@. mo,3gΐTaIYL)---[ʂ%xZ%犥`WzK:`36S/]'Ai`LazIDG}d i=[JRޖJwZY4hvxI䑻ouKk co>4~ٰ>|6gGqk&So΃g=\xu'h-`p+B>-g,/{><[{{^}s_kO;س˾q@@@\(/[&:ZdꖡjOImufٶe*:i2tXΜ-3fΒi2d谐={PF'zˆCE?ؽsn,ۦ?<̢6=0<=AڧU͝; 7/TEg=|ρu_xMϾ֤Yߞſ|ai~L̹jSkd9_^%Wj-=.fw&g?1U-+Jp7w!   %myʢE$>>ޔc OCC\rEV^-=Cj.Iaa\aMr~)3uK1ǎoٲx2ʖ>II!{8LN 帅 I>ُ>g0ן%z?&0@bo>8Y.x߻dam;{2eO#u .ҹeI3ydh Z_k~sd)qS.U˖"9DF IWk| QK>mf$ʭn˖ڲ/7`יrɳ-7^O#N_q ,y-sE&ZEd)s׭7I]}-cZ_4,3^23R:Wo}>6@@@@mqpB[&YfY<1ePO:);oo.vҋl.گL2Mݱ\MafgYAu7MyӢnр.?~[@`L)xV{' +Xfj/35[|ݶ>گ=!o3~\?kbA|-?|g?N= (.ߍަ|Ct )gS9Yt1s{@@@ no^'?~֭Y?(Z~셻w뤠}9u\TaWf 2g|ɚ4Y]w rr<Y$ɉqm ookf|]/lax߂ރ>o|ݶ3z],tfQw?a[PEs5/,KW?Xd6ׄ-,_/=|rF3NJ.ixdпC^?30ꓦ^˘w VǛ=@@@@ `-:ݭ]ۯsraS}]Rd3N,\jO$!11>c[`BsΒWz}];DL`vҺoʸvAM^}`Y=uG͢{u4,180zg<;m낦k1X2+/+m[7F v^fOɑ{{@rg̒T;{_>hjM]gsm\\ iKo [Zߞ^/g_6pL oo4= PZ[7mOms˒16Y<[W3q̾ SOS~7#zH=Z|;9cC@@@:KG3&(/ӧQJ5AUvgzR^Lz.rا{I ˤ\DFFʐ!äorDDD<oE:޲%ښ'&n'PF==V"Q2:7o\yz_]Sg1?R(hpɐI]6/Zo5^t|o=єlERп*H˺h_#f os_^-_?!.Z>0K2kGKaƞ   tng^Ǐɾ}{lP>qd1RκZ*fMoـ]KhH>{ }RSٰx#@Io}`OMΛx{qiSGٰ.z:W?=[EY4 lg.Jý^-3M<Ƶ~7 ط=Wb&|pu羾 ~h7s}|ӷD{][?ۻwL' 68{/     bv ϙ{ v$!!Q-MSW=:*Z*[-sPrk2ޖ*sў+ 3+y]:rǁebIcLg .V\iꢜ(뵁nquZ{) wzB::*_/Ι^M6!8S&S flxR;[xRh݁}ޓkZ-[k75-fn 6c`q y1oYfO-{l&A@@@B$@{]]q~`^ٸ~lWhgϽudM$:<W\ɺȡ9;N/X"'H|lv _glxg Y2=3o9&7>cyKW wg]Р}Dz]By9U*f󄨁p`~4e_v._.#d1|MY.r!k"~J?*S)BƏ`0hfm8[&+6;VKo[k{e$_9oo}])1fn~weF#wlBK拖oiG\ln-Oޒ@@@@ v؋ʖlvfcGm111'11.^ j ښQQQ<-!9Gϫ yFegϛPfΚKGgvݎ%Y̲>ĘffiON%`}|ιΌ޾v6.%˪;|]i 7 ;O|˔ٰ,wُN)cۏ&օ=]9W{L\3S-Q-O[ ϵǣ-G vK)ܦm~5$m Z){@:}ޙz\/?1S LMM_\jм堼_?_#Sǥ{N@@@Tno۸RrsK>}BV^*++e۶;giPі=(N9-)6XN\:W17DJ>2(%Q4M뎟7i"{EDHb|i?zO=Fs^Ӧnj[U̗cD+{}r[|<}/!/Vej'-vuCk =[DYD:x@@@ 6`/ܵU24GD\[MMM{qqyDdرpY80,Pؾ]F3UK:tH"ceH-A-E)'e:C4    N5W.#{e@?gzO&o˦55`gΜe£khOP{\));IBB,Z|dOɑXo=̷yLILck/9g{[dj͒3-W&fOz+Vn!@@@@6`ױT^*gNfq[]geYZwL.KQ-JiE9y۷~)0qd1RڙUUu&y?99|Nf̜- K*}eF0aOjj? 9zډf"    @:`GәϟR]Yaf׷Gl>}or?7`p3=ioP+"[f4INNΪ2<_Cۀ=,5///3+^ 2`Q;L'A.ZSS#fĘQQQ64}emfKΘ%)ggky:y0q #@@@@;KCjPaJL]cGe횕rAcɂKM2rhk &qk[˜    "@p$C^|YN:)w뤠}9uhuݴ>Ќᒝ=Ư/Y&Сһwp :|԰<V0<     -@4U'2&`߱}xuٵs^ Azj2y4Yvr6} 1{BBBco     eP%b*/][.rz~)+-LIMEN/]&YY')1P[;@@@@vV>=}_fͲm&9Utr ɝ9L\3{}i0־^p    tgvի$Lɹb)ؕ/yޒÇ^).ٓsd4[2W^g^q뙊<5    =[wTP[[[+{ O"^fNȒ,[&fKllÞ;jz*O    @ `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0 `w8]qs f@@@@#vIPХ7i@@@@0rQoYYoJJJwO,x~@@@@ =zdڴinٜaǎrv'\{R    - $~@@@@@Z `oC     $vjZzP#     JOsvj;H(q     - $~@@@@@Z `oC     $@ޒ @@@@@hE!     В{K2G@@@@@Vp8     @K-ɰ@@@@@V[     - $~@@@@@Z `oC     $@ޒ @@@@@hE!     В{K2G@@@@@Vp8     @K-ɰ@@@@@V[     - $~@@@@@Z `oC     $@ޒ @@@@@hE!     В{K2G@@@@@Vp8     @K-ɰ@@@@@V[     - $~@@@@@Z `oC     $@ޒ @@@@@hE!     В{K2G@@@@@Vp8     @K-tRTT$񒕕%zF     @1իWСCm.瞓wyG?DFFv9@@@@@ zL^YY)7q/oxW=?AVZ%//]u    oNC*8?efefQcZSe EHo V(}](^"ڠ("|QDڢaQfeн̙'gߝyܳ~;uoii˖-ivkc}l]WT=3{?0G @ @@}qUM @ @ wEU|,rX6lؐ֬Y?<8lɃnݺqCc7]Ӑ!C /|^VZ6oޜ~ 5oWޯ_~>}lN&@ @ @@ ;̗.]~RϚ5+;7(mҤIiɅu1GI+Wl駟N;Vcū^ywÆ K{N/n~tM~X~};wn5,Q_lS  @ @ }wO]vY۷o*ۃ͡⋼yw*rק0}[oͽceױhѢT3fL˫ćyuQIؘVX孕 EA… ӶmrٳgW{Ǻ >?+7|n~w9,Xz|_-իW;o7eʔt'q;3mݺ5ySN}O=u| @ @ @@=x\ңDX}7~uy]wUay/{/·`oo3x tWM6kcĵ=3o~}裏|~cr!nȁ}ĉ鬳Ϊl/ @ @}>3_n}q%`:&GW_>gΜ]Ϧw}O=h)v֯ږ>G{k*: >*cիWn7lO&~=^z& @ @uW^zI/4nܸWm|>Ϝ9 cJXqǥ /ug]vUZlE衇rGh+e˖VW>֏1 @ @Ի@ׯO7xc:BY*ayTv=`ݻw:3r{>Wk\ziر @ @kأ7s7oXZMxa|PmwKccckZ/_OZfhףN?}'W- @ @w-Ze{X[n| o9@,Ϙ1#_iv˗fJc{.'Ο;O[-h:z  @ @ @ MmO"u]pݺu;HlGx{w>tEL/c9 '?i٩)~￟~yȐ!9ӧO/eqG`Vhz4jԨ'w|xiv*3 @ @ &`7o^?~ ~HK,Nɓ'I&U{}ӦMѾ%^4/ݺuk^7a„4mڴ%DQ_Kˀ=]pazǪ?}]S^Ҝ9sqm]w] uM:5tIc@ @ @zҧ۷o:sӘ1cZ~8*ͣ]KTϜ93 8}sO?~|:ŇV^~7_sIO%;wnZlY/c/ hOqqV_fMa8ϔ)Sĉ  @ @ PuGU6l-aZ|)ٻk_rUzѾ&'>D[~)Wp9ooEK܏~yYvm5v亚g @ @ԃ@0# @ @N@^;kg"@ @ @$P}ر=׭ @ @ @@OX`AquwW ػh_ @ @@c9V< @ @ @ .`4  @ @ @TSw @ @ eUkg @ @ Pcƌm; @ @EJӃ]޵jo @ @@ѣGΝ @ @tA࣏>{]ޅiW @ @@QF杈 @ @tV?λ]Gi? @ @@#G @ @tJO>]ީh' @ @@>NG @ @:.駟JS.`C @ @^t#j @ @ @/{]'hs @ @GuT@8) @ @%K楩`wٖ @ @KtG]K @ @vX>ۖ]Ά @ @ Ѝ é  @ @ @ ,]4oX #8bǮ_jG;+gرEQ(EQ(EQ(EQ(E(Dze&`?ËV5En<ő(P<ő(P<ő(P<ő(P<ő(PmK˗ȫJ>x28;u˙'u<ő(P<ő(P<ő(P<ő(P<ő(PuXdaÊFF @ @ @MMMJS.`/,qI @ @ Jta" @ @ @eXreT:lF @ @ .  @ @ P6UVK*MlS @ @ @@[`#@ @ @ .`/!@ @ @ EL{; @ @ P6nz @ @ @@-vR9 @ @I4  @ @L@^3j'"@ @ @$ `IOӽ @ @ @@5v" @ @I4  @ @L@^3j'"@ @ @$ `IOӽ @ @ @@5v" @ @I-OIENDB`django-mptt-0.13.2/docs/forms.rst000066400000000000000000000134031411214034000166200ustar00rootroot00000000000000================================== Working with trees in Django forms ================================== .. contents:: :depth: 3 Fields ====== The following custom form fields are provided in the ``mptt.forms`` package. ``TreeNodeChoiceField`` ----------------------- This is the default formfield used by ``TreeForeignKey`` A subclass of `ModelChoiceField`_ which represents the tree level of each node when generating option labels. For example, where a form which used a ``ModelChoiceField``:: category = ModelChoiceField(queryset=Category.objects.all()) ...would result in a select with the following options:: --------- Root 1 Child 1.1 Child 1.1.1 Root 2 Child 2.1 Child 2.1.1 Using a ``TreeNodeChoiceField`` instead:: category = TreeNodeChoiceField(queryset=Category.objects.all()) ...would result in a select with the following options:: Root 1 --- Child 1.1 ------ Child 1.1.1 Root 2 --- Child 2.1 ------ Child 2.1.1 The text used to indicate a tree level can by customised by providing a ``level_indicator`` argument:: category = TreeNodeChoiceField(queryset=Category.objects.all(), level_indicator='+--') ...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 The starting level can be set so querysets not including the root object can still be displayed in a convenient way. Use the ``start_level`` argument to set the starting point for levels:: obj = Category.objects.get(pk=1) category = TreeNodeChoiceField(queryset=obj.get_descendants(), start_level=obj.level) ...which for this example would result in a select with the following options:: --- Child 1.1.1 .. _`ModelChoiceField`: https://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`: https://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.13.2/docs/treerelatedfieldlistlilter-genres.png000066400000000000000000001532131411214034000243470ustar00rootroot00000000000000PNG  IHDRD4OsRGBgAMA a pHYs+ IDATx^ `?/dsldɝ@HIH$ VXZm__{o}ַ=ֶZTrۄB9 !$$ l9A~av}ocn<$"""""""""fDDDDDDDDD.4#""""""""r ͈\0hFDDDDDDDDA3""""""""" `ЌfDDDDDDDDD.]ȯ.nTVVJK\t: ((HLLƇA3"""""""h Yaa!jkk)D##&&Rm(4#""""""1WSS#G0XFn3͈hLر¼y䱁]fA6Oj<0~w2q""UxA?Ly\m۶KX£Ά43!:j0YY :t!E#9)ɩHKA)͈h4#""""1`yf6Oiի :hV ECƵH<2".iq" 6gmC?kV<DˣĠ30KXL g<u;cjLdB]Q~MDDDDDDD 3R"^y43l3oB*L6OpDGGKI|-NǏKק;͚~ {h45]9"`Ҫ{o[Jk=kK7|k?p4AAA:u*f͚%%8m$w}z4k:kWQ3G? x煡[3S'""""""Cee(V"E7FKK_bR;db_{']i3:0?RR>r< P~h;cb;0ǡ}y((BU]= ($$$#g"IFȀh*9]ybϡ=F#9)ًa~ڄ!u,`n+GC/*EUzaj!:>I٘5+ vǍv=H~ Gy) ^U8N!V&"u0PrhKQ^]DE E*Ǭh ,gׅ*¡rΉ&Ql% c>lݞr{ꐐyXfJ訵/a<:X) ~~(>yykʫP'Wo4]p#DIŜ3gQw|ڇR;p0*_va{Us.qՊEȜ \$>S)|գxO!P)?V8 HayȌ3kSqK+JJ=&4V$Xkϵx ڤK%R'K%>zcTwKhٳgKCQ]]<( GN< ~JD!jоx1W6ೊ+J4wc~h;fb=w`qdm%غ~=6{dOfrl%lJoaw|xcSJ/n;ƚ\#x%XGX聎 ?Wߴ\+_h1ĆW=(ؔ\ ` iCdBc> 3 ]6w3~$^>;^M[>?"vUw#p2olrЬ %c{xq2nw&"""ݍW^yEykJh_ xuGoAG42ZCXLt=)@~Vי]P饫 O!M^Ch풧x׀8O̜hg U>ڳwB\g ~h6:kP7UI﷈h_蕧yd|4歜 5صmϹᝀ leYUe9H XMXy o\g]zTlEDv x^,8АEnuSxU{74U=0˖zplk}4TFo}Or/ʅq糷yT PsbϮ^@p +%\+y{xT2գHKh7GN :^쇨^|St^wD#*7ie˱'񏽵ąmj3gc#-f5ZqlW5nza3/,Kd}97ii96CX?WzRm ]KYNOA_[1eZ4|ez[qX r"&}.\ 9R>2S'BoAs] orHXgNzJЭXg*}Ur1M(* E5cTt彼K]e  rUs& X k"..ގYGGTRL\,ţ6ueR{磪,OKC|P^˅bFφZ~ˀW+#ƒxφ^BѴo=6Nc[%Tg.J}] 6`[k:,*w µQy8R c"'̨ 46=vyeA 8>r;€/C8G!ӟy ͯߵy٧iN,gI'"""a3-^4L7NwRMLTVBH*ܲlbC0@4\ycCx,i5L0@X=6Դt-X}4$L$Ear2rlD*7qRXͿkQ 5>QYu#E N[Ord#UqYltsZ%I*NZ5bJyy'?9Ȳq0v}>g**AEFhM_mxPZB@Ek[nƵ+afz4PWjC߅Mg aH]KK߽qRXS&3o\+\⺥3vP_sЧЇ8^|%!+g:22").µkyR4rI1yR:*j)NvXe3bda9eɑC,ӆzl3-_J\l!̙ | ·>%̨~u31+; ) vnCbIN_Nb#mD]1.4bY>ג}'oQHϳplBǛP:@b%ͼ :L^TU iHsL8ۻyZoCz&/|:fbg\E˖bey˄3-5Q+E+|>`Zɬ1ڏ=/KVjtw[V%̈́Z5fQT6V|m-:,NT5[WAE0-L9hI /\QNӑȯ7|LB@ >kqժlŇu9݄{iG"GI3/rIr҄o'{ Qiäig%ͼt3F5X^[>GpC&">D,AIX1 oUIrhK}b.=| xUhL)X< YS­6a{>u߾; N?B50Cz+tK}!>t1N7Qx|+K⍲f8w:;բC#VLKKǬ< /ц={fb1FPpp/>!*E]?t4/>~si& $0xtPЧO1ok@aA&zUU;3?uq A`Hc$Dn6ë;qGw\9ӑ951Y]E\k+ ӑ.,3Zk\su9eJjL`nĮv%u3~M=a!]XD'$#}z./]f!I7Mw9mus1=S<6ƾ"JȂr[:JvZK]E z=\ti1cQ&kyQepȒ6n#wY9.˰,Ìҽen]0̠9Y> ػLe0`,kEH3jŽ?GݝWL-xLa21=GenYu89 6hEOYl2W#dN]=9 aaB&D".1!U<&Fz~~.HĿ῿738T趭=ՠYQ׽X_=t+r#ϠH$ #{ͷ!urBDDDDuqLw+!}QǑh꾀&{U@Jl^L;iMVTPmu">=Q @A3p\9>Cqq ,]h:TLfģ.SB3ih™3&e&؟T_&oooJU$ŪI6)kó^xmyyy./, z{Q^ӦMǬc10o$}+A0՗"o xG?[KඦXM}Eam_Q8ܓ(~v ^ZɹwT-+׹pwYhI O^N?.myCuW]X;{~^"̺!. HNH[} G]Q1,GGP} }]td\n{JMJ.߇mєM IP+蟼DŽm#YEXaH"ӑb"YSJ0[Okڭ1! N^ u<_+ѠuԠTO("FZKrY@_VzLn5 +v*,'(;Si˴%ν J`YP]֣^Pt=>Q>¶U" :uؿZJ'ԮWfdy =S5D6eԚ{To)3SZH*ԟq|(嫗d`j*$)G=g*Eb>haGXzFy'{/;GIUa5Kw~^~bd8|,S;LzhgA}6׃>:iowP;R^RjI9l)-+&q[6H,p/'RsSxa{߇a߮g~'n3$T?/qm) yӓZ@MxhrDSiQAj\})o~~g^P,(w 䝐A^Q?}lMG ،7nG.yCWEn#)wT`wg2M*[ ~߿76mî((*Eii**K{As \ #pCJܕJX}Fg)iB<"""">1ȊwC0g9`ZdhJ* ^AEL޳oo; *$F篾'd HyjpA3wtVz}kW`^v*=ZnCCQ b2`pmg3äְoܯg;v> ~A[HXjaң0vmx Oٳ[Qҧ`QTAE`A]q0F3!Miؙg&w;];W?O} }{F#L .h?*;jnډg$^宴0+mM2߅R?ןĤ-'& \.9ڄ,xt-jփnB_Q MEZ&EK: V`疍x?݌ QjꏴDG{{i]>b2ݸ]3Q?># RjI:)cf#-+&q[6CzR! l χ= a(r(ӱ{f˰6;:YRl7wz Y w/ڄ?%Nf#4>(/Ը7OԻo=Fl?Ĩ|RìH# HTsX{}oK?w`鏔H9ӃVdz.\^!H G[`M鏄PE01`Rj&gg"RE<|y֚(ڒbkLk"Ʀ"R~ K*CN!bo'OġCT8O\f{BzfL4| -vԆ:/vpxC;YrFl T«[ݟ@1_)w%.yluPX Vyxß͑mAp[ӢhJ?h""""`bP`2 DZ;=ЗVڿ&g2+P Xl6&E+@5VU:/A9˰0]lc+)Ƀ y!4}V,99l>OR- h_J) \y)'8Lk.L5X,fRc"3)S8>ՏnnnF]]jjllt*9&-+&q[!\z]rJs,p%,x0Lz$Gxh7Et+y~qg+׉%??O<=p7ݼff#)}(M8w0ݕZӗBQӵj45GN{V連E#sۋhDSHLYPpInFr{l[|cLxn%0A*vcT㣉’7am 7NJ6#Az(hfڊe#Uٯ8Ro. x5*#CxvDlP4$>'ȯ]ի6Rƶڑ7B3W'K+z ARyn*3 kxx2fWyݞCP0&K˞TSϓZ2z͆ #ͩj+-⟜Lݞ#J6ľL &E$^HTZEMz(G4J?{/;z{ ]h)?qit;ۆªV{fc*?Վ/7 ¶>vBS{S̤KfU>|C|&6;&eE{S w [6H8>ǧL2HԮWKQQ /=#W0~֔Lt.!X/vQa=}5dzC +@G==~@ IqSL7e ldžhҰ(y8;2s}y4lijcLus 0cł2#V;f=z|6i {^/er-Q7_Y*p'&u;Ņz: ojjtJ<eTn@-xپ-;C%"cB=زu:B7nA^iWκ?i&nmx)AgfLNՕ[BluFZ 8St}3Cu9jp N6ᕷ>YwD-WMv)H]v[:t{MJBBAJ؅&7̹({L*ħ&`JLcw_+܂0E-IJ{AQ]y5%jCu ChpnmuQxCK9+gakƣ6UgxyEuhJO(vm~ v6X".};=9Cw^j^δ 06!4Ha>*Ԏ;pʌ`o+,$⛙h:SCo{X0ι[o>:J9+1?Rp;v|2.MO[Yn&r=٘_@ߺ7^09DB̉`>+/:[]7M9&-r&, }aJH)5Tqx<մHCBl't 'M4!eL\5;\1+ Rܬ 5ǎ4k,">>^:;;q9tttHõd6]ȯ2WϞ5BU4H)<4K6zgxH-$Cc4¨ףޠP2ڰW?ūv"qsg絺xDB6Ыd)Gm .ц =ƏHSym |OjƌwK Fp uU09ڙ?C12ǞL~P3Gf]u!0?}J3~X7|cxzװJ*#=#, kK(4ri '݇?<290i>gUT3nF]*$bOA"3 ~ڵe>sO:TU.gż{]1[6} 6UoWO~榺HD7z7.M}3GܯJDDDDCyfctW#M~0V}"q2,IJԎaah̙3QQ֞ qK^Zs6f܁Tf̺!pSMSbңEEV$LZS1٤FEiJKK+P]0#"""ѐ+KZO-杆IXsMZQDRobULkqH(h歍w;`fލG~OƚG3yb`yG/xІx;z,0BfaGrDap* '})!z}>+Qã>v5k?2U3eikù/=a>aE(r&<g߇f0Ϣ}W 3=Vw #҄l0?W`xޗGWIll,2331>ÆvDU},]?moڑ V F{{;No]3Źwrank 6. i9Kq;qMi*CkJ鋯_tkPI4ŧ#g޵{p[N<]?/5ۡGMWQHr>,ENE1(.Z}Vy :az-5u"Rތ&NTiN/z:/{YmCCM%֬4QX{]WcJXi 8:]Z8'm쒷!)cn%WՂ"´cpr⍰XjϗQȗeKV蝧aDeSk~iϓ9K#'1E[`"Z(Tϯ& 3#{jpMwGZKhqKWc~?S~1<$ZWi3-Oœ7;Q.c sn͈AE gOGp5}Qp%5v)dS;$ X-ꫯyԦ:3MçPR7nC[Y u%#9a~0 bc=x{#Dx>bV1%6TWN^w!"X') !nKD ̌rTY:> U'o|2&aA|| 0(#>4s+Pw>>INJF^\zlj;MI|$ϓyRUU7D,}''c%TlKl1ljB&1iHMtK Coܷ-H^9d6AZϵNw$\J'+LNYBtqnP1SiA3"4sC¬Dv~N{M*{!| bfD@S>vV3+cF<6.1pm6v @XlŊD %{2hBB&U?]yn{](PARd.?y:&#>;iIH{$@}WT3yٷo?. ccЌh,كf>BDp͆Nx*JlLl/h^2k_GgѥA?Ā]^}3`FDDD#0V^믿^ j4/:lLĒfDc|9 Уd$̻y-.NftwA~i;2]***gb1q___I$-fDc܄" :=&(F/:993s%.*UrboȺx$$e`YLUODDDDDfDDDDDDDDD.ئ ͈\0hFDDDDDDDDA3""""""""" `ЌfDDDDDDDDD.]ȯ8rfDDDDDDDDD.<*i+ 諭\VtJC"Mhw4dЌlAiHDt 3WKCf'ODDDDDDզ륡4=-6 tl/Lwley-g'Og>dtC&Og>dtC&OWcPA)SHC""""""jkjjeʠY|HǙdI3""""""4;x>l5/ȯ8tæ<_bߎY>G1oc>!XS25!XS-XҌ`+ivxش SllK"Cmu u}+Ҏ̇mh[ߊo|ַb>l[cBO'A4iHDDDDDD_m T(?fڋ8?l^:^9/7N~uLq%=b^!1V̇4o1acPASJC""""""j-bLzxشr̰r+;ʶmuܾ>Bż ñp-b>"zkGAtiHDDDDDD_muuuҰD4szF ^~0il =.m@aRX}|@0L u&`l&thJsuE'[gM/C)x!ھ}uh. lxaee>l̇m^0fӦMDDDDDDV[k-aqR cؔ^~3YI=كf!X bElj Plk>-I(,lDk;÷6]ڦqY\&0a{a0G^ W*l&LDDDDDDaH5HCRx_9n Zh6}'l-?$Fn(o"C H#Ǻ|GDd+V,Mgg'qY;y%CN|Úiuy8-+MYd>D%"㬟,iFDDDDDDvE-* |bʒf=*i@Ѷu`{8鹀^qT6,myaU4:Y|[I3x##+ʒfpKUlVt5{|<{b>u|2>ny#fGA iH_N7o_ իWDDDUVScm˧pi{t=}?GuTЃu(4ʣl :#ʠ|RdfUf^dG@'hB]C;Jf]!6=3PqJD h:U]Jqb4̔ȯn_u9եKccoaS+suAZ$hYg3>v af4GЃŵr,dĠYì?{0 8t=r6^44 oyCM簏A3pv#ڋ0f[,77W ^e~l~fHt/KЬ]l?f@q;bIk\f[;h H@ m5V4\v,M*]hk=63_^6R#1?kl=hF `o%mmu8y|^d6̇3$ &h_n PUϴU㈉?P)lc>TӽX4sAO A0p "h6-A58n0Yјh,:Tȝ ،ΉFC *Nԡݺ'i(Av4l^9d5ۄϋms'O''0R/|Hɾg^X=c034dL""_qhk<4ǒNm18V?JZA|DžXl EI&ܦ!>3AQʫgW\ 1='+TXWĵiZkiiG' [tXRbgkaE!uN\7h;̇l$ = /{O2hFDCatMPMUU4>HDDDdeQe?*+#㼴X27:iLԃj|!Uo6`}'wAAkf;3ǍĬQ*q`۞Wb"hvp *m.>h 5h6}"+Jȃn)~l=Pmߎ}|C þ4`> Je>}Kyވ˛F-XKQy쓘|ب坉98_ϗa_y9q!Yup4S4e]}#Y0˳%f{_%ךsFJZ;´'w}]ʃl:. u=wizP7ܥnoKCPs. u{C]]|{6͈LNo>!>b_OCqɇ GdblS$TȇlYK><;mZu0QJi=/Rb><0<0=;wb88v ;0!Z1yʈC;ٮw(![3O,F=J8o?-:UvoD ͞j/z?܏8Lp h~xC8eIBFW ]lzA׵iW`{1I p|Es5]k8Gj/iDImg{hm0yy7<|Gxdb@3j--f?L-3#dB0C}H] C2ٱ" 0B#+ml%@ɅS>zPmV6a~ /׺|g}v5AY, 1Y 9qDq&l{<$w:m: ظ0sB,) (ܳ6^{RpROs>>>P'|0ʵm7㆛u߸!v`VYwdS'V|\{ nX˯ W-LG/-"gbMo݉u݆uw~ X=qbo{ :TֽI<"ycTnXt9%냥rJm$`߼nnm7ZKWe{P_QD -睗Xf0LuCy}el$|[b>᜾̜i8n4#WޔS@B0gאw(&v7{!n72[2F6n( {& ^fi̙30<Ѽ?`_:aSc‰U o~ zKX?}'HaPɛv^qW`!»CLjܰ4`8i [nìPyh"-G=0H 3e5n__b x om9DD`Y;Ѵr}ll ) A8+2yI5!Pf>̇@z*(hvWHÑ~ Y;G7iu%k~ZZQv(Fs[Kxf@F^79UXz'nLŶ\t|J=ZFtHex Ysz8y||:,:yºbuaW`bnzwߕ }5[7ggl[nEaaaY)^X4??D< b!,U }?e?IJ(yp#_qaTI"!sxxM~Ұ}4K->q V܉ V/nu nÚADҽ\י{#>9f V{<"|:}\W%hSo #= Q{WdG]$d̝DlY|֛.I9:{ϔnkcI:Zک8Ȝ1SՃl#^CPZ݀ni D]"=R;G)AH̘;Aߗv_wV8Bm1Ê2Cʘy(c>lH~#i8rϾ@}.P_h}PTֆ.C/MDJQKk1>޴ εO.4n6ףFiP =mkj"-! BĉqGE'a´LqňnDD#njf49S@$(`h8Q$]Ռ{ Qo",<]0vt$1%ϩ^{m8xLKCv@g/Q:᜘7ֆ=*i6c i8R,{0yM;BbFe9;,7`K=MO,\(.=j*zlD뱳 +ª>%yxkqq튒f]زT _,ǍKtcA>Vܒc[ޒ1F,ϲ}V |-z,!/׿!K}uyRl(D{׾5i [_⊟s_ʬ?+V`~tZF9~|d؁_d $l߾]eَ.GW܊b>_^==2ٷގ)ԅoaky-Y^ oGp\uR\VjAO,t y{Sp]0AciiEgcnx_Js{%4cI䘱t Y-oc0R|Y9]f"sͭh-ĻKIUuJx/_-`c0Cn\,?[(>׊%τ'ٷv4#Qbض ><ẞv 6=O|X1V1Vs>2Cj--F.pL3)>td&}|uLA λY'}v Fm=[ Gx))$yi#R X`y>X:7BZ f<:3>Y"^OZu-=>^J!G*W}{,[;τy %bHS#W''3Oa㛥0)uZ}^>E%/z`QM8*'1w,(uxY>b9=GPڢ'm/ӆ:OĀ"Ped6 wrqI;'$Zxg!)`YXx{`ʢ$ƞ|T٧Z8/>tȌq[>K 0G;5GYK˧J=6/DAU X+K D`4 B}ɜ8INt9qrR땓m}a5$)$/o[_̃웱'-XNߚab9CX)c>< HG,uC63yMK@T9bqh<(Ez'i <9 (u5W2 T+8Qȃ@w-j;m"cX^Jޡ7e^pZUcb˼HD7w\$;J3cZAD4+I6RfRX빵mU)惈i ؖ1xtpB0|l|TӨKęwu~Y,тWhp7QE\պH[!8rX9-hp],^@4ſy /:6|1NT)v1j^|E>j=0ڎc5sڶ^?e:#ec&7C5/CDBZFnu[jFhBD4&[aW(%azI^N#kOܖze؞u-1]u,{a|X|اKb>ӥmp>lƸYX7n 1Jobur)׵N/̓<[:]95`2󅑾F1 U׉⛷"6gO~仾ֹ̻e~Xd˼[Ϳ8˶]/ wFHŖl\U."bXu:rm*s| [lMrm7.IA@)8j{ V-FHxV{n[?qctdff1-TxyEb)jPDJɫ9od_Iex|Nib}mɫ M[w&&&%v_^njW_֖|8'91ihےfe(˯!|nk8d9Ar˿vY#xza: 'E~>@1U#?Fi&'%Et%Q+Q6VDj77a8߰L})hEdkm}#oho"$*GE`$tRVTݙLhhB<)ZĒYW>#ZfP  ۾$L%+nm|]l>bw-vZqIs0m9 ^%RgmCrNF;?S酠oOYN|7ѧ THXRC*z{dfyR-zǍ&i`[ {kz6̇50d|X aM6ƣ3GE mF_<.*>ވ% tx𛼰Ĭzlڇ73׮c=]f~G'7~cv&|r7)Rj܍rwyGL˃'͡dzPMyy4 Y.q7m̮`ֺa'6 ki{)Eeq䧳pk\Ѯ]a0x]:W5ƵrVذogd} :,Ls]5oo86buwl7߷)L3Nf79(Gi0i)Lݪm^&܊o7lg apJM^Y|;VѰw H6{ /Kp6{Cu.a<ͯ~`Z&"NDtjiicW̢2G( 23n}/b<VH~5cDžCJ]jUBϼhRs/f_í,Mx-Y~`GᣏJfDDC,Y6D_iRi, >=no|Tg9S}!6=u,f¶& ym?w^{b= ydyrMfY;Jjpd0FsAL\I7=||{}Ā͘\Pdf?]RRʂk]6S!]N,ף4D!wv ;7cwZɱ.V w ژ |dwr黄Ʀ(,QlWlqnm߉ĎRƘ4lo~¶׷LKeYZO@ >ILhm3'd!q4xsK^²1Պ0 r#gmI3QWnXzO"#eңVo\ d%͈.?bi3pKSSo~ 1cr|GCiUU}3p/ʔ6m~oy7ކg>|aNwboOQؽ{4ْf" Z ?Ļy [ AoA{C58U\.n?}2iZbt¿&k6SqMK"%I5`&:{4of,[Pub/6=$wCKWfDDDDDDtAٳ8+ lV2MFޱX4BYPy1+3j92J%-ɋ4KQ|7G=ߧ7CQ 1 sZZP JFq] ,iFDDDDDDX񴔞ģ݅% Q`K ޱKPٴY17p2αݥ2cľ4AHS2hBSrη׮YR#\2s.f-t륹Odyv L `Q3 bЌ.W#FbՂR8f.I_Σr7F2_"PN'4ND=1`ˉA3""""""Eާݬ嘦X|٣RF=APs%JN ӆ5&6D]?[ira"w`SXk#OSX;I<4#"""""KKK)j] /iU2d,U,2SX)K[aӔ>/VTh/Ԟ+mlג%8o#f4bVs dX_ ȿe(%z\ VJCOtwwܹs(((@QQQ"6?n8hZxyy๖kݰ0*""""""r%>NޓJeaa{Ϙ"sx2lÉ,>kDZ.e(Ǚ4*gI3F)S`ܹXpᐓ q[6mވ2Iou0!c4$EX3@7bW݋[mVq ȅփ+~34Z퍍;bKAx}EhЬB()56׶mWa ]yzXe6y)]9 cݏsfzi3 Kzy&TN\<0}LKc#o?-%RPnx՘0Lk\,.ѡ#h̏>E*%6R߿_ ]{T zMwagPTUQU%|8d`՘/O%"""""jꙣ46+(E`EMg.ۀ'~DQ1 +zK#Q"bf$v nSh¡ ?nvU8DC=*W$O%"""""h+([IXehhWinGMʀ b!0#Jђf7nϗ#e߾}p͚5SC)ivRyLy(#!ހQ_?C6;=fDDDDDD_r_fW7ay~X;܆On2'@dq.W!W.f#4{wyH˓z4ffx)û?ÖbW~1* @.Х1,/"άHj+&UczYWM RF)4f 0#R mp4wwy.-]hac[xc$f:d EFvN|Z`$G 5"""""|YJ[I3z{$6/٨s|&bOǷCN#oAd~'Џw Y$&IYQlϳKXP:>R%$f[$ Xg\h)cLLLLLLLLLLLL$""(hfk[l4޳d6ߤ Ɣ|1[+s1%JKiR)Vd#H 0@g B㴎Z{Ό f&?&qzo#NTt (qS\*0j,33(IDAy<a#xw'{p]~߹VI_{LN~_9*AăD K4|DyD|OO}"$z6J43KCF sLvىS'Mi0q,qDz$G;fHft з`-\Ex4,~|{TJӬ:J%b湔4kƑO%2,JYԊh"*}!~{-R*?JXE,iFDDDDD=G]l%]觥#GHӧKÁlڴ ===;w ///|TTUUIÄixy0O?ŋcq| /wAC"""""ٞå!妬LzT,22RDpꂿ*_`` RRR䩞RI/ǑH-Bғj+f'\8iW-tL"""""]FM3[.1",FTVVJ'N( Id‘6{qx6a6Ȼ〭ٵ\,ViDDDDDDDD%gfffJÁ̙36FFZbb%lΝ;' W_-8\D͈s1Ӏv,+oX>elځPZ} q, z或4äIvȆJ\WV)nKܦ/W wcqV5A<{ 1̈PsbL[uR;3DDJeddHÁtwwMY\FJC""""""9jKp|QxYX}uT[ScI3"#R!!!pٴ̈fFE8r#B||U "ϣ3#"/*ŪbO+b/Mⶆ萆bϛDDDDDD40shYbülNV_.<υN=Y ~KKA\EOL-BDQ=h`ݚGtj4|"DKR{.*JM0A~ٰEI}!2>WHBpӕ0tw2,iFDc%͈"聾 WqN ~ 3Νl$ͣ3¤!]Fs^z#q sv)G^n=ȨIDDDDDDS])z~  KA'k؍&]\4KJ! lZ*ahd4$"""""ٞFY'>7doَbP-( +W#:%͈h̰-YJ3yIRL^vIDfDDDDDD4zRVveh[kUM!.dDթVa-"A3""""""}=8Ue^OI@JSJgԌ.͈hq*n1򉘂hi|'뺭fDDDDDD4ʺQwfT 3|"'vЋs0Y)IDDDDDDv{&ѥg ͈\0hFDDDDDDDDb;ۍ6}=ΞE38H]4ƍ{Nv@DDDDD48.w0*/|چ0 UU~Bض91ђfURK,%N'zTTTH̬F$s<ÒfDDDDDDÒfDt4ѠY]Rt)`6٩S3S=39X-l6#Oxhų0-Ke}E6Aq4a?Â>9 !jOK((=*:Ѯn)^ r'}Eہ&""" ͈r7*AH.\(O>q{J]Jꙡ ƳhE֊?_22'{ܱNy\krq@/lF=Px͘F#X뚈hlQ---p )))+"QiӬ4n8iDg^y_0 .1 f$tpQhGe`7u4X(]jz/8_5`&. @\ANzqa~kW&"""""Ѡ頙l 8m<;[fඥaim1Zy tȯl({w a),o_e8ƒʳ8}^KKs^T8b %ů]]_p[3"lXڗk""""""}˿yH˓V}Mzj3{Pg.<~z{t'v/!1ĥ(MqZ3xG萞Uّvڮ'ކV[ǂ/qH!=X3F1h(3&46~^c]R@ÌܩXl^vk<׬I=^ 6^HJ<0O 5r[PZoH#0@ηAwL4\9 D\0E.Pm9XZ[\$9bxKY vO ֑x(ۻQl?X4 7$)ѪFl|{²体epl+ hp5=gDCQq<gс A8N/su+^AZw/}X5>J+5bRj4"|Y8bazF!(kF7Y Uk8y~,= UU8f fabdyWLbJ XOKL1c$+S'sZ}W代E4턵LT4~Ƕvn{8V~,1cr lzZ)?AAX 18pZ(\uE>?:g ]Q{;KHOD@3Kڼyl۵`gQk+01,Ζ~ʘUKҰpj&:NooEB/:[Px c ©/p,\@pL ?:rMlp$A^r]]>lQ{V$WGQ9Zi,'F߅?oGTyH}ET|wec(dMĤ38"GW&fJ.x'fw$3v'"?)QZo"-_ԡImw .{S>k25yF $27Ubt\D<# aN[5X𵅸~~>EGbr9ߩ g-X0[Yѳy0ʅXquj5%͈r7*%;1e$d);wƯ/iq0tiGMdpi+)xj 5,(.PQaͼ0G M0LU2$EJzPRnK% rD~>?©@V,-t 2f6ee1X- !d9A8R<~:\,`۠i0F&mKsRJELjX)HE fw#v.I%Y؟@#_T2sוcǩ䖷ʉ6WWK6ã\þhTi7!¼¹3ux}A<L`э#A+nh[ǓVbϙN4w K<^aZ ϥ-8VX/67AAxazv:n~~Z< p S Ө=WƩ4i;2b崞]/  YW/j‹^{h<&_9?>ރ,Nzi\@8&^nbq>v3àZ Z׍{?W}u91I,mر^ôUh'O U)_VX^3r=fc_}i5}u15)saMw#čRwW૮_{fbp)1a 0 <Ơ[na]:?(1elH(iɩL^aRI@_.UԂ ]fE'*:jpXp|np%q|(ts 2w³]&]\z} T%Wő~ܥj+ GqTTEz1Qk.r@> \?ʠKqL5-٨rNs]uMDDDDc7KWMi{ݪ_Vcw=(?+פA|Zc.]ѧ ͔ڪۻ |WlᅠH:RlCDŧo;pZIxkFx4T37 }XZۇqԖQPsn<\"\߃ w@R3UN/?IAwHC޶UvK- `eo=&4-6|]uMDDDDc& rU C#hfnnk?; .~(.Z-]81yݗP'/'2|a0ݰcxAe5q}xc9n|u2O&q+5ox;œkR0Eϔa>?|(ީ~^T/-0w6-GlxFQy=pX}lPW]b9X9R\gE:.)Ki*Toħ%n"1HTAsrv^x(^jX#A t>--]!O˹/r!w\^HIxN jYqdz+&SNw4/'Nųo݂ 뚈Ǝx/&/Â0%.aԂ||ܬ8XZ_m0"H2W(mOaxZe$~ tH#0,͉IXcKXa}+ GexE =.]8_߬/Ffaݿv B9 O7ջm?RVOʍ7ЂS;w˱  _uGtx=._Q[[+ &HÁpTWWKiSDOh?ʁN;'pp uI܂+pm7~ BQFtH!^k)qOn\ZziE WeMg)9o1gvtox:\/&Emq-ތ|V=+U@1PI5 FfX/h[S(<ƖOOC!!~COv@؋:ooYQ) &4?L B̹ 7V]u I .0nG[h]Mػ=[vڃdnBq_2P`Q>h9NZ(,ǧJq(ʏ"WX"Ŷ5@ۅp hw{d̏u aG|g%b$[P./=+0nȯCP._[#GI-//$Nƽ'ah̝ X⃳%ͨoiR,26Ѭz $ugq2;WOT 94߈g,OMĕ¶fᚬN5|pe-dY{dpx0iQ6YřѸx7`_ejI.=|8$.=90-Q'pk|[PW3Sdnw,bbҧ⧷Q=-KgD J߀Nc9EwKI麩N8c٤^/% r:ΊڌMݑscc8WxJ 'a -x N)18DKKxĿE͞vjҊF tMr%pL=w/xL 'k{#y@,:TN=TWN:nzͥڦ QMzT?) kSG؈O?-(lb,ӱU Ξ{2m5īRE$ၕc&Ux*XeolX sS[+^:%vl 2g#)Ӌ5v٨jX*V{]uMDDDDҍ*ƲJh^%?NG5S1CxnBZ1H|{Uy$d1eƩj7T0|su;f{ۄŒa=howaaeuNoDf^ʆGbdmÔ7gbL u GPp02;W}_x?Q (\}ݕxP yuIHslTW{s8bo7-̊B;ko_5 KN8NK؍‡xLS.%]H#YWP+nOc\B> ~Ddh% ܱ*:c}ftO8UGm~U?z#7%`&g{Ω4}?H{P9 l9Ԫĥbk$,*+: `J?Uz(gU _ -㬉UoЂcy ŕnuֶMn] .}9rD&͐ RSSWD(++C`P,Nֆ i$ /[ͧ?N%-)c< l?; SF7GDDDDD`{UOHJJ_`D "֌_IX8-ܹt@ll-8&rgdLtۯM迆/X|̗a˛u$J'67#< rq߶ gMSB c y=<#ߜ2LFCTC7Qonn*1`VYiw4uN.η"^|(4;W7W8+037Î]{Ű`:EF#C""""Ҕ9w̐ݖW}fjqKO\?J) nW-E)ʆR JQmliAm9u6bOաq+3HstpEth,0D'uӃR>|romZ>(>XE(wbi=k8v(^(4u2f56:%(sHU: ^c\͏l58&Ģk0-ǣp޳RL.7-OCtaأhsTCR0W|0wZ>Y|FKʈx#:>S3iL\788XVM7,r/kVJu%zl|?}3ϩwMD Z7,g qZ*ꆽB^c1f9}&<9!""""Kg4=y|a+4#6n߃#15zC#[HRyG$hUl)zi4ڍY}9?IXV|Rf=D_#4jqaQH˜K;oِYҶ lSom^7PDiWwAH\΍".>k^+{m$"""""R _ZTS+8nGMi'2{[UOs0#Xn*Jt\{q|\fT YPwj NMBa(9E'fC+|<>4vAp/'*R؟Z쯽|D#M&= )G y{T; """""~X#@ y!..:#GEq;,a]j8Owe(+>l@8卸Jt[TFz<טp2%aL{ _V+\`!99g&fDDDDDDWfT pW،.–ceNӈ{P0OG(p HWʼnu=1R1&R-Ǫ䑪2 RJ6bH统<:E@s)v0Ez`WJ݀BVMՌTJM90U]] UKDDDDDD8`uM)vUh=~5*)0*E+3,+ '|s6Ԣ#P٘< ^H]\q'a,H3hi!G\o`& AQJزj34Xa*U+W"C:cDmpcM꾝k"""+3;gމ-c/=مUa4iu{vzD[.Q|nؘ9ڸ+,JX cFh?gQ{ҸQ`eC3jFgc+0y9`nS?;:Pu;0 ׏SxiA\)v6+XYƉ3'O}Pg4Zo{;!y}MDDDtsAtmZ * ]ie RUcө~p 5REôffp:Ӷr-1c_s 8=n_E"*Zٜ>xՃx7C779}S<6tEA.Dm("| F'{]7o)7k"""9@x"zԛ&@st. uA6=/>~)>n-IހmM8‰Z'K~٣ /6btyȿ>;_;EsDso]?ݠx_ dKHh9~]W\kMDzxĈpj\u1c.,,d]fNuV\t۟â1cq:T)cz>jqLC [ۃ>,AZENn ,j:pꍽx마xy|UG hl۔cR[fT((Jؖ GtYAy`=Nso=s/+iJO> '>xߝ5xzg-zׅ߅B<É&s Um.AΕq |W/?yC ~U#5sWUyiI CMhGEW+P#Y"z2mds?;WB+0'j۫=X0_.g's i!5MbMKNQs0݆#6)DkzGe[8W܌+u{1^Y!J3a!;?~ ǎ5"=rtL3ӈ.Ɤ₱3Z:F[`c)b4o?c)0TH)*F^|ԣ;$cfDSL@ 3ce\\ș3g fpz^/.]~1L:22RZO,fkvp$cu0t) pkb$ńA-س`*O&wr 6 ͍mHBl)F12LYx arxrMa>% k_2ީES.\i'pDDDDs{Th,* `]@5hl>s- kp &hX 篴AJ,U}fkk~v=5CeM: >#a#/nGSTdž &}9L -ILs??~xD G5D gq4Z;qA#C3i<WlC6N?hlGsK#ǛI&oF9S˵C{[[cY;jz/˺Ўo4bSc=xr[.yQj Ú8(p懮_{'~^MqggaSj(:+j}#&W%5ImgSpBæ mLC"~ʟ;OyʏkX)n=ߜqd"sw?"3 "beIކ&.nCTD(ä眃zmx2lw;vL<>XG#'u☉? 缚;6WhxԊqfr:-L(Yg7ƹB_q{޷~bGe *t^(b b)|_эM`mA6tj(ð,0- .%Ek`偲dܸEIVn`h#(l,/ʁ6PY،AL:l,Y8 N z1tlB"$s Efq\FDT =.b%{i]PP KYYi=YW=S@ͮ^1^`G!gB"Χ_N܉O?|q\Gynv{z4}%vtC&If"ҰL5f+_<)F0 yĎos }O 꺱 m~^2-(twW>EHȣS?m8'\_E+w\?[*:\YkKtw!^:o}.moej6ECrzgA6^_]6ŀt+CHj:|A`=z{xjݓ`cpo ޏ짮/\0{sg.%+JS/{&ͺDD=sZ+nrdnZے zwOl?vF+.vGL^LcUIxp;xxW9y`}h9qƕU1^3~Π!N'$ C$z_7s*F2iI2Xš"0v{G0 ֧xu>˂I0Rr'6yfl :'+#gGacϤaw+ <-;Ro 5L5k|dwx}DEe_K8oXG |&14 H%K7?OnL=YZ$:{}C0[*8^AZd֛6|dyCānhQ~IX{{f]DG~/CTZq/\ aPA<˿U>5|oU|Wh0"ӿJlȆ=R鞡QAL͡!O W&' "T" "~Pxl& 9}@'ks`dBR2('T ^96'Wﺯ.#d~8ybp5[hɊCgY!|~t&}ߎT}Dr_]9f'@") bV,)<)&˯K^$` }]! >a<slwmZ@8e&l8~ʀI.:1Q$܏i^Xϙ6DQIڗ{*w<ثMe:RR cgT_@׸KQc0^)9^cns=c홥@߉,W{Z@w%XE>sƛe&8%KRӾ&]x\VzN]s7}ADDDDDDsC3o`iϕ( 4.skưH8NQ5\ʼ^&>mQ4W;ݢ i^Ǹ؅-a eGWwM7uۻqc3P D| vO\5Q4Hj$ 9Yvg="/o Vsɐ*}?B لv=̋:)mqbg|_e>So?OMx#DDDDDD4'Ȟtͥ''ripBi=]ڤuvv,$###UXdfw㼀z=ӅN|VՆ>oΫ`*qMOò u g.">TUaaH\|+/+p[_\v?:]5s 27Ɇvڀ[š+f$! sS]fmi8ԃ3MQ`Dsy A;Zzܯ@n'JهN_p_U& \Bo 7V.N̥X҅vj$SOT!NaVᝳ(y=rTRWHGO+vU{,% Ћ#mY n/`*:f{];-(kA5_=8ߕxي hWa}hZQTm‘I%ѣoǾ# (=Ӊҿ5Cx`#'ƽan24M;pn(hů? :/ iX;NNpܪshtLL Ω{_-Q'""y~^7}ƲͨjFԜJ_ To! v ]7+#>EK5͏÷6ʎοVc_E*9Wۀ5`4#|yNx]Ykaibn]kF1wi}֛zrEgb\@w95UĭxyKk0hw_;>4|sqvo+On:ou(^kҔ3VFDDDDDSЌ Dbp600 g6< n.$$D2L4B3"""""Ќ n$ ͈ ajoD}]~" (Tjhțϋ֮V4a2A :fe#5&37-̓1+C3k+N~&WP \8vD]P7 gg)=bpe  9`GSBI^ !C3""""""cօf8 b.aI~.ćC[PW[ =k7!&+rva$d,AjB = ഢ1k2 ?ȑj# c]'4 KDDDDDD4 3W`LDPh3Qn3$BqQbbzf"Y!_΁:3Jt[TFz<טb eP *E9Bt\XM5 rQq9:apj^[qrVMcݸ{fW*Ө1=\l26oX PZoG#d ۴N8ո{}زj Fc2M26oiT O%MBXݓbWO LJcRi bFъxM9jlLdn2;C8fA:(ʀֿӄ,r!!19(#9ZjWJ{5]XfDDDDDDtSrX5aY`tmbEsQR&"\E4~?FTr] AxL8[A1Xj@4(rqg?K?FEV gh8= pW،[V^~p{gzGV݇Rŕu{vR &A]wX_"TY`wA p_asvŻ: ?E n\1iE(78Pv♘Mg"If5t2L:22RZyG=8PkX1/px1AhХ AuTM|g0lFB 5))tX;z'0K@B$20#4ΞIDDDDDD R7E7OrP|rI{Cf //2K;0ap5 M t^dfDSuSfb̈nN dKqv?A+MV߈q&dS.Զ213jD44#"""""Y* Ʌˡ &[~ohydQyHC*{=J xsZhkEEVve}p݊Yp=h>Y)0; fD榞`ڊ2 2 A6%X^X.f?TqJa_Zpq01 3,vaoyVm.4 i2PZovB* !2'VX6*#6,mCjTKP2ӈ1͈ KĊ [ I:5amXlQi[yo`&'c(NB*vs7  F A%ecyQP/1y백d94P:- u MsP}yOb=" hV`fDDӉfDDDDDDDDD`hFDDDDDDDD䇡fDDDDDDDDD~ahFDDDDDDDD䇡fDDDDDDDDD~ahFDDDDDDDD䇡fDDDDDDDDD~ahFDDDDDDDD䇡[. cTTTH|iMDDDDDD7&ihL45fcAra*5:'&"1& 2 G=a5߻ɡo0ֺ=]i<WhG ވF=#m&(TjhDt55+͈qT1?P^BZl6s7:NVX{yV>;2 mTARAnڬb]=Vш4ž ""̻iEWk#Zjd-KDa`-0;+V^i6Њ{:,Œpԓ9`loA]m5Tݘ}f V5 AQ_`&!&9+|3Q(bS3qfoe3e V/F`&Bu  fDӆfDDDDDD4b*͜0؍MvErwh8Tn 5!^=*5&xΪo]P$ ]{S&̃P5{kQc{dנ~FT vpYI8:M 9@y\J W 6>nXњ0ҍ]1 {^SZIk""""""L&i:STalhÅa:j` !üy -\R ~q*"ǝ41;LT^s@JJ*Gc&-8?EbYf RR38.M]Ћ4ązz4aB?|[WEX$%Z:o1|pң!S`qrJe><݃KA]+s3La' -0uHEt9}z1͋rR§8> :?=N'^UkW#oI)H]qL0[0R9wڊ .90% 3ӄvMUV.$wCavRӅN:|8qƺ¥H"Y8vf6=zM'!Uz3><YiFDDDDDDFh"J`-,Ǒ{_?<~!;d AfZ (^ eȎÎnk tߔ%c eR[ D'4Ð#~APHLU– -F;`4 a@q|?,1yz@X~gb|rP"3 /uQlL%byAH,\]w-z8Uz "ytNtUW,l)RP#(E[,F] ͈蚑Ep}IRGt^=jŞv LdDHP\_z:$ pdӺA6w!BD\P42bWN)t:)(rk6tD]-4X8D1,햱mG˜W_qƐ됬f=KU eЈmkyDC3""""""d&"#oހ_ފ-kD l(oD˦7 Ȅ%%M 2`*)j|9ntM6܁C_Xݧ"Gz^x&lݺյܿ c)2}E@4hpnjx:R6;%/ҋݓ%]? ͈ GN*H=1FOWXHHIGzxw$2F)7/~8|%#)iS d9!9蒊@A(5ЌnaQa׻wb'"aK]]&!/2Kf,\(eOwgף\c`Fu4s%1984J?؂53=gͰ4-F+͔k!/w%k4 rA|{kKxŗxtB?5:)%""""""""6[h&_0^'ň.%G!wۣ 4蚺A'p@sGF !Iwt*;{qF4& >l+!lh:.vVlkhYt_ FJF32O@W'rT,ɹX(Y茅Dxry^0 S8_ǎ9wwZoĀjp ܱi8EDDDDDt=3:OLi+Ni'>Fp*l̏/)a/~qʅHK˜(1S94vP:e4spx ?jDwE_Ep؁Z|QlY˱4+89ExB.O y g~?{sC+-!0R4;SG?s({| 1NFZ"D(!2X4ĉhZL&iz# =!4v1z=4"{Ƭ43ďe{ / ȁz85=? _J(sI$DwΏܾ@p>J{-gxD'2|ŇԓT=/-ԅWS@ױъ1Gώ {`Hu M{r4sQpq"EJ3"""""4#`&+10If")Y]0(f}>(=/l#f"˿( ÅUzOW DXu=Џ fC<,r)!|5>A!7el0&H``FDDDDDDD4nL.˟C,OX }K5@8@:kDDDDDDDD4Mn$poQQQ!txwͮBX.M%׭ǣx a;=}Fa[87rm:ߟN ,W y3ξT 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`: https://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', on_delete=models.CASCADE, 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 and apply the migrations to create the table in the database:: python manage.py makemigrations python manage.py migrate Create some data ---------------- Fire up a django shell:: python manage.py shell Now create some data to test:: from myapp.models import Genre rock = Genre.objects.create(name="Rock") blues = Genre.objects.create(name="Blues") Genre.objects.create(name="Hard Rock", parent=rock) Genre.objects.create(name="Pop Rock", parent=rock) Make a view ----------- This one's pretty simple for now. Add this lightweight view to your ``views.py``:: def show_genres(request): return render(request, "genres.html", {'genres': Genre.objects.all()}) And add a URL for it in ``urls.py``:: (r'^genres/$', 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 genres %}
  • {{ 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/ .. _order_insertion_by_gotcha: ``order_insertion_by`` gotcha ----------------------------- In the example above, we used ``order_insertion_by`` option, which makes ``django-mptt`` order items in the tree automatically, using ``name`` key. What does this mean, technically? Well, in case you add items in an unordered manner, ``django-mptt`` will update the database, so they will be ordered in the tree. So why this is exactly a gotcha? Well, it is not. As long as you don't keep instances with references to old data. But chances are you do keep them and you don't even know about this. In case you do, you will need to reload your items from the database, or else you will be left with strange bugs, looking like data inconsistency. The sole reason behind that is we can't actually tell Django to reload every single instance of ``django.db.models.Model`` based on the table row. You will need to reload manually, by calling `Model.refresh_from_db()`_. For example, using that model from the previous code snippet: .. highlightlang:: python .. python:: >>> root = Genre.objects.create(name="") # # Bear in mind, that we're going to add children in an unordered # manner: # >>> b = OrderedInsertion.objects.create(name="b", parent=root) >>> a = OrderedInsertion.objects.create(name="a", parent=root) # # At this point, the tree will be reorganized in the database # and unless you will refresh the 'b' instance, it will be left # containing old data, which in turn will lead to bugs like: # >>> a in a.get_ancestors(include_self=True) True >>> b in b.get_ancestors(include_self=True) False # # What? That's wrong! Let's reload # >>> b.refresh_from_db() >>> b in b.get_ancestors(include_self=True) True # Everything's back to normal. .. _`Model.refresh_from_db()`: https://docs.djangoproject.com/en/dev/ref/models/instances/#refreshing-objects-from-database django-mptt-0.13.2/docs/utilities.rst000066400000000000000000000072131411214034000175070ustar00rootroot00000000000000================================ 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 or all descendants, all in tree order. Optional arguments may be given to specify details of a relationship between the given node's class and another model class, for the purpose of adding related item counts to the node's children. Required arguments ~~~~~~~~~~~~~~~~~~ ``node`` A model instance which represents a node in a tree. Optional arguments ~~~~~~~~~~~~~~~~~~ ``rel_cls`` A model class which has a relationship to the node's class. ``rel_field`` The name of the field in ``rel_cls`` which holds the relationship to the node's class. ``count_attr`` The name of an attribute which should be added to each child of the node in the drilldown tree (if any), containing a count of how many instances of ``rel_cls`` are related to it through ``rel_field``. ``cumulative`` If ``True``, the count will be for items related to the child node *and* all of its descendants. Defaults to ``False``. ``get_cached_trees()`` ----------------------------- Takes a list/queryset of model objects in MPTT left (depth-first) order and caches the children and parent on each node. This allows up and down traversal through the tree without the need for further queries. Use cases include using a recursively included template or arbitrarily traversing trees. Returns a list of top-level nodes. If a single tree was provided in its entirety, the list will of course consist of just the tree's root node. Aliases to this function are also available: ``mptt.templatetags.mptt_tag.cache_tree_children`` Use for recursive rendering in templates. ``mptt.querysets.TreeQuerySet.get_cached_trees`` Useful for chaining with queries; e.g., `Node.objects.filter(**kwargs).get_cached_trees()` Required arguments ~~~~~~~~~~~~~~~~~~ ``queryset`` An iterable that consists of all nodes which are to be cached. django-mptt-0.13.2/mptt/000077500000000000000000000000001411214034000147735ustar00rootroot00000000000000django-mptt-0.13.2/mptt/__init__.py000066400000000000000000000011771411214034000171120ustar00rootroot00000000000000import django __version__ = "0.13.2" VERSION = tuple(__version__.split(".")) if django.VERSION < (3, 2): # pragma: no cover default_app_config = "mptt.apps.MpttConfig" def register(*args, **kwargs): """ Registers a model class as an MPTTModel, adding MPTT fields and adding MPTTModel to __bases__. This is equivalent to just subclassing MPTTModel, but works for an already-created model. """ from mptt.models import MPTTModelBase return MPTTModelBase.register(*args, **kwargs) class AlreadyRegistered(Exception): "Deprecated - don't use this anymore. It's never thrown, you don't need to catch it" django-mptt-0.13.2/mptt/admin.py000066400000000000000000000366301411214034000164450ustar00rootroot00000000000000import json from django import forms, http from django.conf import settings from django.contrib import messages from django.contrib.admin import RelatedFieldListFilter from django.contrib.admin.actions import delete_selected from django.contrib.admin.models import CHANGE, LogEntry from django.contrib.admin.options import ( IncorrectLookupParameters, ModelAdmin, get_content_type_for_model, ) from django.contrib.admin.utils import get_model_from_relation from django.core.exceptions import ValidationError from django.core.serializers.json import DjangoJSONEncoder from django.db import IntegrityError, transaction from django.db.models.fields.related import ForeignObjectRel, ManyToManyField from django.utils.encoding import force_str, smart_str from django.utils.html import format_html, mark_safe from django.utils.translation import get_language_bidi, gettext as _, gettext_lazy from js_asset import JS from mptt.exceptions import InvalidMove from mptt.forms import MPTTAdminForm, TreeNodeChoiceField from mptt.models import MPTTModel, TreeForeignKey __all__ = ("MPTTModelAdmin", "MPTTAdminForm", "DraggableMPTTAdmin") IS_GRAPPELLI_INSTALLED = "grappelli" in settings.INSTALLED_APPS class MPTTModelAdmin(ModelAdmin): """ A basic admin class that displays tree items according to their position in the tree. No extra editing functionality beyond what Django admin normally offers. """ if IS_GRAPPELLI_INSTALLED: change_list_template = "admin/grappelli_mptt_change_list.html" else: change_list_template = "admin/mptt_change_list.html" form = MPTTAdminForm def formfield_for_foreignkey(self, db_field, request, **kwargs): if ( issubclass(db_field.remote_field.model, MPTTModel) and not isinstance(db_field, TreeForeignKey) and db_field.name not in self.raw_id_fields ): db = kwargs.get("using") limit_choices_to = db_field.get_limit_choices_to() defaults = dict( form_class=TreeNodeChoiceField, queryset=db_field.remote_field.model._default_manager.using( db ).complex_filter(limit_choices_to), required=False, ) defaults.update(kwargs) kwargs = defaults return super().formfield_for_foreignkey(db_field, request, **kwargs) def get_ordering(self, request): """ Changes the default ordering for changelists to tree-order. """ mptt_opts = self.model._mptt_meta return self.ordering or (mptt_opts.tree_id_attr, mptt_opts.left_attr) def delete_selected_tree(self, modeladmin, request, queryset): """ Deletes multiple instances and makes sure the MPTT fields get recalculated properly. (Because merely doing a bulk delete doesn't trigger the post_delete hooks.) """ # If this is True, the confirmation page has been displayed if request.POST.get("post"): n = 0 with queryset.model._tree_manager.delay_mptt_updates(): for obj in queryset: if self.has_delete_permission(request, obj): obj_display = force_str(obj) self.log_deletion(request, obj, obj_display) obj.delete() n += 1 self.message_user( request, _("Successfully deleted %(count)d items.") % {"count": n} ) # Return None to display the change list page again return None else: # (ab)using the built-in action to display the confirmation page return delete_selected(self, request, queryset) def get_actions(self, request): actions = super().get_actions(request) if actions is not None and "delete_selected" in actions: actions["delete_selected"] = ( self.delete_selected_tree, "delete_selected", _("Delete selected %(verbose_name_plural)s"), ) return actions class DraggableMPTTAdmin(MPTTModelAdmin): """ The ``DraggableMPTTAdmin`` modifies the standard Django administration change list to a drag-drop enabled interface. """ change_list_template = None # Back to default list_per_page = 2000 # This will take a really long time to load. list_display = ("tree_actions", "indented_title") # Sane defaults. list_display_links = ("indented_title",) # Sane defaults. mptt_level_indent = 20 expand_tree_by_default = False def tree_actions(self, item): try: url = item.get_absolute_url() except Exception: # Nevermind. url = "" return format_html( '
' '
', item.pk, item._mpttfield("level"), url, ) tree_actions.short_description = "" def indented_title(self, item): """ Generate a short title for an object, indent it depending on the object's depth in the hierarchy. """ return format_html( '
{}
', item._mpttfield("level") * self.mptt_level_indent, item, ) indented_title.short_description = gettext_lazy("title") def changelist_view(self, request, *args, **kwargs): if request.POST.get("cmd") == "move_node": return self._move_node(request) response = super().changelist_view(request, *args, **kwargs) try: response.context_data["media"] = response.context_data[ "media" ] + forms.Media( css={ "all": ["mptt/draggable-admin.css"], }, js=[ "admin/js/vendor/jquery/jquery.js", "admin/js/jquery.init.js", JS( "mptt/draggable-admin.js", { "id": "draggable-admin-context", "data-context": json.dumps( self._tree_context(request), cls=DjangoJSONEncoder ), }, ), ], ) except (AttributeError, KeyError): # Not meant for us if there is no context_data attribute (no # TemplateResponse) or no media in the context. pass return response def get_data_before_update(self, request, cut_item, pasted_on): mptt_opts = self.model._mptt_meta mptt_attr_fields = ( "parent_attr", "left_attr", "right_attr", "tree_id_attr", "level_attr", ) mptt_fields = [getattr(mptt_opts, attr) for attr in mptt_attr_fields] return {k: getattr(cut_item, k) for k in mptt_fields} def get_move_node_change_message( self, request, cut_item, pasted_on, data_before_update ): changed_fields = [ k for k, v in data_before_update.items() if v != getattr(cut_item, k) ] return [{"changed": {"fields": changed_fields}}] @transaction.atomic def _move_node(self, request): position = request.POST.get("position") if position not in ("last-child", "left", "right"): self.message_user( request, _("Did not understand moving instruction."), level=messages.ERROR, ) return http.HttpResponse("FAIL, unknown instruction.") queryset = self.get_queryset(request) try: cut_item = queryset.get(pk=request.POST.get("cut_item")) pasted_on = queryset.get(pk=request.POST.get("pasted_on")) except (self.model.DoesNotExist, TypeError, ValueError): self.message_user( request, _("Objects have disappeared, try again."), level=messages.ERROR ) return http.HttpResponse("FAIL, invalid objects.") if not self.has_change_permission(request, cut_item): self.message_user(request, _("No permission"), level=messages.ERROR) return http.HttpResponse("FAIL, no permission.") data_before_update = self.get_data_before_update(request, cut_item, pasted_on) try: self.model._tree_manager.move_node(cut_item, pasted_on, position) except InvalidMove as e: self.message_user(request, "%s" % e, level=messages.ERROR) return http.HttpResponse("FAIL, invalid move.") except IntegrityError as e: self.message_user( request, _("Database error: %s") % e, level=messages.ERROR ) raise change_message = self.get_move_node_change_message( request, cut_item, pasted_on, data_before_update ) LogEntry.objects.log_action( user_id=request.user.pk, content_type_id=get_content_type_for_model(cut_item).pk, object_id=cut_item.pk, object_repr=str(cut_item), action_flag=CHANGE, change_message=change_message, ) self.message_user(request, _("%s has been successfully moved.") % cut_item) return http.HttpResponse("OK, moved.") def _tree_context(self, request): opts = self.model._meta return { "storageName": "tree_%s_%s_collapsed" % (opts.app_label, opts.model_name), "treeStructure": self._build_tree_structure(self.get_queryset(request)), "levelIndent": self.mptt_level_indent, "messages": { "before": _("move node before node"), "child": _("move node to child position"), "after": _("move node after node"), "collapseTree": _("Collapse tree"), "expandTree": _("Expand tree"), }, "expandTreeByDefault": self.expand_tree_by_default, } def _build_tree_structure(self, queryset): """ Build an in-memory representation of the item tree, trying to keep database accesses down to a minimum. The returned dictionary looks like this (as json dump): {"6": [7, 8, 10] "7": [12], ... } Leaves are not included in the dictionary. """ all_nodes = {} mptt_opts = self.model._mptt_meta items = queryset.values_list( "pk", "%s_id" % mptt_opts.parent_attr, ) for p_id, parent_id in items: all_nodes.setdefault( str(parent_id) if parent_id else 0, [], ).append(p_id) return all_nodes class TreeRelatedFieldListFilter(RelatedFieldListFilter): """ Admin filter class which filters models related to parent model with all its descendants. Usage: from mptt.admin import TreeRelatedFieldListFilter @admin.register(models.MyModel) class MyModelAdmin(admin.ModelAdmin): model = models.MyModel list_filter = ( ('my_related_model', TreeRelatedFieldListFilter), ) """ template = "admin/mptt_filter.html" mptt_level_indent = 10 def __init__(self, field, request, params, model, model_admin, field_path): self.other_model = get_model_from_relation(field) if field.remote_field is not None and hasattr( field.remote_field, "get_related_field" ): self.rel_name = field.remote_field.get_related_field().name else: self.rel_name = self.other_model._meta.pk.name self.changed_lookup_kwarg = "%s__%s__inhierarchy" % (field_path, self.rel_name) super().__init__(field, request, params, model, model_admin, field_path) self.lookup_val = request.GET.get(self.changed_lookup_kwarg) def expected_parameters(self): return [self.changed_lookup_kwarg, self.lookup_kwarg_isnull] # Ripped from contrib.admin.filters,FieldListFilter Django 1.8 to deal with # lookup name 'inhierarchy' def queryset(self, request, queryset): try: # #### MPTT ADDITION START if self.lookup_val: other_model = self.other_model.objects.get(pk=self.lookup_val) other_models = other_model.get_descendants(True) del self.used_parameters[self.changed_lookup_kwarg] self.used_parameters.update( {"%s__%s__in" % (self.field_path, self.rel_name): other_models} ) # #### MPTT ADDITION END return queryset.filter(**self.used_parameters) except ValidationError as e: raise IncorrectLookupParameters(e) # Adding padding_style to each choice tuple def field_choices(self, field, request, model_admin): mptt_level_indent = getattr( model_admin, "mptt_level_indent", self.mptt_level_indent ) language_bidi = get_language_bidi() initial_choices = field.get_choices(include_blank=False) pks = [pk for pk, val in initial_choices] models = field.related_model._default_manager.filter(pk__in=pks) levels_dict = { model.pk: getattr(model, model._mptt_meta.level_attr) for model in models } choices = [] for pk, val in initial_choices: padding_style = ' style="padding-%s:%spx"' % ( "right" if language_bidi else "left", mptt_level_indent * levels_dict[pk], ) choices.append((pk, val, mark_safe(padding_style))) return choices # Ripped from contrib.admin.filters,RelatedFieldListFilter Django 1.8 to # yield padding_style def choices(self, cl): # #### MPTT ADDITION START EMPTY_CHANGELIST_VALUE = self.empty_value_display # #### MPTT ADDITION END yield { "selected": self.lookup_val is None and not self.lookup_val_isnull, "query_string": cl.get_query_string( {}, [self.changed_lookup_kwarg, self.lookup_kwarg_isnull] ), "display": _("All"), } for pk_val, val, padding_style in self.lookup_choices: yield { "selected": self.lookup_val == smart_str(pk_val), "query_string": cl.get_query_string( { self.changed_lookup_kwarg: pk_val, }, [self.lookup_kwarg_isnull], ), "display": val, # #### MPTT ADDITION START "padding_style": padding_style, # #### MPTT ADDITION END } if ( isinstance(self.field, ForeignObjectRel) and (self.field.field.null or isinstance(self.field.field, ManyToManyField)) or self.field.remote_field is not None and (self.field.null or isinstance(self.field, ManyToManyField)) ): yield { "selected": bool(self.lookup_val_isnull), "query_string": cl.get_query_string( { self.lookup_kwarg_isnull: "True", }, [self.changed_lookup_kwarg], ), "display": EMPTY_CHANGELIST_VALUE, } django-mptt-0.13.2/mptt/apps.py000066400000000000000000000002471411214034000163130ustar00rootroot00000000000000from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ class MpttConfig(AppConfig): name = "mptt" verbose_name = _("mptt") django-mptt-0.13.2/mptt/compat.py000066400000000000000000000006351411214034000166340ustar00rootroot00000000000000def cached_field_value(instance, attr): try: # In Django 2.0, use the new field cache API field = instance._meta.get_field(attr) if field.is_cached(instance): return field.get_cached_value(instance) except AttributeError: cache_attr = "_%s_cache" % attr if hasattr(instance, cache_attr): return getattr(instance, cache_attr) return None django-mptt-0.13.2/mptt/exceptions.py000066400000000000000000000006111411214034000175240ustar00rootroot00000000000000""" MPTT exceptions. """ 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.13.2/mptt/fields.py000066400000000000000000000021141411214034000166110ustar00rootroot00000000000000""" Model fields for working with trees. """ from django.db import models from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField __all__ = ("TreeForeignKey", "TreeOneToOneField", "TreeManyToManyField") class TreeForeignKey(models.ForeignKey): """ Extends the foreign key, but uses mptt's ``TreeNodeChoiceField`` as the default form field. This is useful if you are creating models that need automatically generated ModelForms to use the correct widgets. """ def formfield(self, **kwargs): """ Use MPTT's ``TreeNodeChoiceField`` """ kwargs.setdefault("form_class", TreeNodeChoiceField) return super().formfield(**kwargs) class TreeOneToOneField(models.OneToOneField): def formfield(self, **kwargs): kwargs.setdefault("form_class", TreeNodeChoiceField) return super().formfield(**kwargs) class TreeManyToManyField(models.ManyToManyField): def formfield(self, **kwargs): kwargs.setdefault("form_class", TreeNodeMultipleChoiceField) return super().formfield(**kwargs) django-mptt-0.13.2/mptt/forms.py000066400000000000000000000161001411214034000164710ustar00rootroot00000000000000""" Form components for working with trees. """ from django import forms from django.forms.forms import NON_FIELD_ERRORS from django.utils.encoding import smart_str from django.utils.html import conditional_escape, mark_safe from django.utils.translation import gettext_lazy as _ from mptt.exceptions import InvalidMove from mptt.settings import DEFAULT_LEVEL_INDICATOR __all__ = ( "TreeNodeChoiceField", "TreeNodeMultipleChoiceField", "TreeNodePositionField", "MoveNodeForm", ) # Fields ###################################################################### class TreeNodeChoiceFieldMixin: def __init__(self, queryset, *args, **kwargs): self.level_indicator = kwargs.pop("level_indicator", DEFAULT_LEVEL_INDICATOR) self.start_level = kwargs.pop("start_level", 0) # 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().__init__(queryset, *args, **kwargs) def _get_relative_level(self, obj): level = getattr(obj, obj._mptt_meta.level_attr) return level - self.start_level def _get_level_indicator(self, obj): level = self._get_relative_level(obj) 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_str(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().__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().__init__(*args, **kwargs) opts = node._mptt_meta if valid_targets is None: valid_targets = node._tree_manager.exclude( **{ opts.tree_id_attr: getattr(node, opts.tree_id_attr), opts.left_attr + "__gte": getattr(node, opts.left_attr), opts.right_attr + "__lte": getattr(node, opts.right_attr), } ) self.fields["target"].queryset = valid_targets self.fields["target"].widget.attrs["size"] = target_select_size if level_indicator: self.fields["target"].level_indicator = level_indicator if position_choices: self.fields["position"].choices = position_choices def save(self): """ Attempts to move the node using the selected target and position. If an invalid move is attempted, the related error message will be added to the form's non-field errors and the error will be re-raised. Callers should attempt to catch ``InvalidNode`` to redisplay the form with the error, should it occur. """ try: self.node.move_to( self.cleaned_data["target"], self.cleaned_data["position"] ) return self.node except InvalidMove as e: self.errors[NON_FIELD_ERRORS] = self.error_class(e) raise class MPTTAdminForm(forms.ModelForm): """ A form which validates that the chosen parent for a node isn't one of its descendants. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance and self.instance.pk: instance = self.instance opts = self._meta.model._mptt_meta parent_field = self.fields.get(opts.parent_attr) if parent_field: parent_qs = parent_field.queryset parent_qs = parent_qs.exclude( pk__in=instance.get_descendants(include_self=True).values_list( "pk", flat=True ) ) parent_field.queryset = parent_qs def clean(self): cleaned_data = super().clean() opts = self._meta.model._mptt_meta parent = cleaned_data.get(opts.parent_attr) if self.instance and parent: if parent.is_descendant_of(self.instance, include_self=True): if opts.parent_attr not in self._errors: self._errors[opts.parent_attr] = self.error_class() self._errors[opts.parent_attr].append(_("Invalid parent")) del self.cleaned_data[opts.parent_attr] return cleaned_data django-mptt-0.13.2/mptt/locale/000077500000000000000000000000001411214034000162325ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/da/000077500000000000000000000000001411214034000166165ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/da/LC_MESSAGES/000077500000000000000000000000001411214034000204035ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/da/LC_MESSAGES/django.mo000066400000000000000000000057051411214034000222110ustar00rootroot000000000000006 @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.13.2/mptt/locale/da/LC_MESSAGES/django.po000066400000000000000000000100301411214034000221770ustar00rootroot00000000000000# Danish translation for django-mptt # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the django-mptt package. # Michael Lind Mortensen , 2009. # scootergrisen, 2017. # msgid "" msgstr "" "Project-Id-Version: django-mptt\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2009-09-11 10:38+0200\n" "PO-Revision-Date: 2017-01-06 00:00+0000\n" "Last-Translator: scootergrisen\n" "Language-Team: Danish\n" "Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: __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.13.2/mptt/locale/de/000077500000000000000000000000001411214034000166225ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/de/LC_MESSAGES/000077500000000000000000000000001411214034000204075ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/de/LC_MESSAGES/django.mo000066400000000000000000000106421411214034000222110ustar00rootroot00000000000000)d;6!9A){;+ "24 gu'&      $$E j%x@:6FQLEF+Cr0&. U [   % :C ~ C 9 G Md  - C ) 9 -M {       /7Ay@<8&I_MDI<H"! :5+p# " % ) &'! $( By %(filter_title)s %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: django-mptt Report-Msgid-Bugs-To: POT-Creation-Date: 2016-08-24 12:25+0200 PO-Revision-Date: 2016-08-24 12:32+0200 Language: de MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last-Translator: Henning Hraban Ramm Language-Team: fiëé visuëlle X-Generator: Poedit 1.8.7 X-Poedit-SourceCharset: UTF-8 Nach %(filter_title)s %s wurde erfolgreich verschoben.%s-Tag benötigt ein Queryset%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.AlleEine ungültige Position wurde angegeben: %s.Kann ein Element, welches schon gespeichert wurde, nicht einfügen.Alles zuklappenDatenbankfehler: %sAusgewählte %(verbose_name_plural)s löschenUnbekannter Verschiebe-Befehl.Alles aufklappenErstes UnterelementUngültiges Eltern-ElementLetztes UnterelementLinker NachbarKeine BerechtigungKnoten %s ist nicht in Tiefe-zuerst-ReihenfolgeObjekte sind verschwunden, bitte noch einmal versuchen.Rechter Nachbar%(count)d Einträge gelöscht.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' seinKnoten nach den Knoten verschiebenKnoten vor den Knoten verschiebenKnoten als Unterknoten einfügenregister() erwartet als Argument eine Django-Modell-KlasseZweites Argument für %s-Tag muss 'as' seinTiteldjango-mptt-0.13.2/mptt/locale/de/LC_MESSAGES/django.po000066400000000000000000000135541411214034000222210ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: django-mptt\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-08-24 12:25+0200\n" "PO-Revision-Date: 2016-08-24 12:32+0200\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Last-Translator: Henning Hraban Ramm \n" "Language-Team: fiëé visuëlle \n" "X-Generator: Poedit 1.8.7\n" "X-Poedit-SourceCharset: UTF-8\n" #: admin.py:87 #, python-format msgid "Successfully deleted %(count)d items." msgstr "%(count)d Einträge gelöscht." #: admin.py:100 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Ausgewählte %(verbose_name_plural)s löschen" #: admin.py:182 msgid "title" msgstr "Titel" #: admin.py:212 msgid "Did not understand moving instruction." msgstr "Unbekannter Verschiebe-Befehl." #: admin.py:220 msgid "Objects have disappeared, try again." msgstr "Objekte sind verschwunden, bitte noch einmal versuchen." #: admin.py:224 msgid "No permission" msgstr "Keine Berechtigung" #: admin.py:233 #, python-format msgid "Database error: %s" msgstr "Datenbankfehler: %s" #: admin.py:238 #, python-format msgid "%s has been successfully moved." msgstr "%s wurde erfolgreich verschoben." #: admin.py:249 msgid "move node before node" msgstr "Knoten vor den Knoten verschieben" #: admin.py:250 msgid "move node to child position" msgstr "Knoten als Unterknoten einfügen" #: admin.py:251 msgid "move node after node" msgstr "Knoten nach den Knoten verschieben" #: admin.py:252 msgid "Collapse tree" msgstr "Alles zuklappen" #: admin.py:253 msgid "Expand tree" msgstr "Alles aufklappen" #: admin.py:364 msgid "All" msgstr "Alle" #: forms.py:63 msgid "First child" msgstr "Erstes Unterelement" #: forms.py:64 msgid "Last child" msgstr "Letztes Unterelement" #: forms.py:65 msgid "Left sibling" msgstr "Linker Nachbar" #: forms.py:66 msgid "Right sibling" msgstr "Rechter Nachbar" #: forms.py:184 msgid "Invalid parent" msgstr "Ungültiges Eltern-Element" #: managers.py:521 msgid "Cannot insert a node which has already been saved." msgstr "Kann ein Element, welches schon gespeichert wurde, nicht einfügen." #: managers.py:739 managers.py:912 managers.py:948 managers.py:1114 #, python-format msgid "An invalid position was given: %s." msgstr "Eine ungültige Position wurde angegeben: %s." #: managers.py:898 managers.py:1094 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:1073 managers.py:1199 msgid "A node may not be made a child of itself." msgstr "Ein Element kann nicht Unterelement von sich selbst sein." #: managers.py:1075 managers.py:1201 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:1096 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." #: models.py:292 msgid "register() expects a Django model class argument" msgstr "register() erwartet als Argument eine Django-Modell-Klasse" #: templates/admin/mptt_filter.html:3 #, python-format msgid " By %(filter_title)s " msgstr " Nach %(filter_title)s " #: templatetags/mptt_tags.py:31 #, 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:55 #, 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:62 #, 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:89 #, python-format msgid "%s tag requires three arguments" msgstr "%s Tag benötigt drei Argumente" #: templatetags/mptt_tags.py:91 templatetags/mptt_tags.py:146 #, 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:143 #, 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:150 #, 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:154 #, 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:160 #, 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:164 #, 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:168 #, 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" #: templatetags/mptt_tags.py:287 #, python-format msgid "%s tag requires a queryset" msgstr "%s-Tag benötigt ein Queryset" #: utils.py:240 #, python-format msgid "Node %s not in depth-first order" msgstr "Knoten %s ist nicht in Tiefe-zuerst-Reihenfolge" #~ msgid "The model %s has already been registered." #~ msgstr "%s wurde schon registriert." django-mptt-0.13.2/mptt/locale/es/000077500000000000000000000000001411214034000166415ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/es/LC_MESSAGES/000077500000000000000000000000001411214034000204265ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/es/LC_MESSAGES/django.mo000066400000000000000000000103531411214034000222270ustar00rootroot00000000000000)d;C.9N);+"2A t'&     # 1$R w@:6F8LEFCY0& A !G i  ? " I# $m L '  ' 55 k { . +     * B 5R 2  K A=_INH6NE1"KnAs-" &! $ )%' #( By %(filter_title)s %s has been successfully moved.%s tag requires a queryset%s tag requires either three, four, seven, eight, or nine 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.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingdrilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionmpttregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: django-mptt master Report-Msgid-Bugs-To: PO-Revision-Date: 2020-04-03 18:55:20+0200 Last-Translator: Guillermo Rodríguez Language: es MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); Por %(filter_title)s %s ha sido movido correctamente.el tag %s requiere un querysetel tag %s requiere tres, cuatro, siete, ocho o nueve argumentosEl tag %s requiere tres argumentosUn nono no puede estar formado por hijos de ninguno de sus descendientes.Un nodo no puede ser su propio hijo.Un nodo no puede estar formado por hermanos de ninguno de sus descendientes.Un nodo no puede ser su propio hermano.TodosSe ha dado una posición inválida: %s.No se puede insertar un nodo que ya ha sido guardado.Contraer árbolError de base de datos: %sEliminar %(verbose_name_plural)s seleccionadosNo entendí esa instrucción de movimiento.Expandir árbolPrimer hijoPadre inválidoÚltimo hijoHermano de la izquierdaNo hay permisosEl nodo %s no está en el primer orden de profundidadLos objetos han desaparecido, inténtalo de nuevo.Hermano de la derechaSe ha dado un campo del modelo inválido al tag drilldown_tree_for_node: %sSe ha dado un modelo inválido al tag drilldown_tree_for_node: %sSe ha dado un modelo inválido al tag full_tree_for_model: %ssi se dan ocho argumentos al tag %s, el quinto argumento debe ser 'count'si se dan ocho argumentos al tag %s, el cuarto argumento debe ser 'cumulative'si se dan ocho argumentos al tag %s, el séptimo argumento debe ser 'in'si se dan siete argumentos al tag %s, el cuatro argumento tiene que ser 'with'si se da siete argumentos al tag %s, el sexto argumento debe ser 'in'mover nodo después que nodomover nodo antes que nodomover nodo a la posición del hijompttregister() espera como urgumento una clase de un modelo de Djangoel segundo argumento del tag %s debe ser 'as'títulodjango-mptt-0.13.2/mptt/locale/es/LC_MESSAGES/django.po000066400000000000000000000137251411214034000222400ustar00rootroot00000000000000# SPANISH TRANSLATION # This file is distributed under the same license as the PACKAGE django-mptt. # Guillermo Rodríguez 2020. # msgid "" msgstr "" "Project-Id-Version: django-mptt master\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-04-03 16:54+0000\n" "PO-Revision-Date: 2020-04-03 18:55:20+0200\n" "Last-Translator: Guillermo Rodríguez\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: mptt/admin.py:86 #, fuzzy, python-format #| msgid "Successfully deleted %s items." msgid "Successfully deleted %(count)d items." msgstr "%(count)d registros eliminados correctamente." #: mptt/admin.py:99 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Eliminar %(verbose_name_plural)s seleccionados" #: mptt/admin.py:142 msgid "title" msgstr "título" #: mptt/admin.py:188 msgid "Did not understand moving instruction." msgstr "No entendí esa instrucción de movimiento." #: mptt/admin.py:196 msgid "Objects have disappeared, try again." msgstr "Los objetos han desaparecido, inténtalo de nuevo." #: mptt/admin.py:200 msgid "No permission" msgstr "No hay permisos" #: mptt/admin.py:211 #, python-format msgid "Database error: %s" msgstr "Error de base de datos: %s" #: mptt/admin.py:227 #, python-format msgid "%s has been successfully moved." msgstr "%s ha sido movido correctamente." #: mptt/admin.py:238 msgid "move node before node" msgstr "mover nodo antes que nodo" #: mptt/admin.py:239 msgid "move node to child position" msgstr "mover nodo a la posición del hijo" #: mptt/admin.py:240 msgid "move node after node" msgstr "mover nodo después que nodo" #: mptt/admin.py:241 msgid "Collapse tree" msgstr "Contraer árbol" #: mptt/admin.py:242 msgid "Expand tree" msgstr "Expandir árbol" #: mptt/admin.py:350 msgid "All" msgstr "Todos" #: mptt/apps.py:7 msgid "mptt" msgstr "mptt" #: mptt/forms.py:62 msgid "First child" msgstr "Primer hijo" #: mptt/forms.py:63 msgid "Last child" msgstr "Último hijo" #: mptt/forms.py:64 msgid "Left sibling" msgstr "Hermano de la izquierda" #: mptt/forms.py:65 msgid "Right sibling" msgstr "Hermano de la derecha" #: mptt/forms.py:183 msgid "Invalid parent" msgstr "Padre inválido" #: mptt/managers.py:521 msgid "Cannot insert a node which has already been saved." msgstr "No se puede insertar un nodo que ya ha sido guardado." #: mptt/managers.py:811 mptt/managers.py:969 mptt/managers.py:1005 #: mptt/managers.py:1169 #, python-format msgid "An invalid position was given: %s." msgstr "Se ha dado una posición inválida: %s." #: mptt/managers.py:955 mptt/managers.py:1149 msgid "A node may not be made a sibling of itself." msgstr "Un nodo no puede ser su propio hermano." #: mptt/managers.py:1128 mptt/managers.py:1246 msgid "A node may not be made a child of itself." msgstr "Un nodo no puede ser su propio hijo." #: mptt/managers.py:1130 mptt/managers.py:1248 msgid "A node may not be made a child of any of its descendants." msgstr "Un nono no puede estar formado por hijos de ninguno de sus descendientes." #: mptt/managers.py:1151 msgid "A node may not be made a sibling of any of its descendants." msgstr "Un nodo no puede estar formado por hermanos de ninguno de sus descendientes." #: mptt/models.py:299 msgid "register() expects a Django model class argument" msgstr "register() espera como urgumento una clase de un modelo de Django" #: mptt/templates/admin/mptt_filter.html:3 #, python-format msgid " By %(filter_title)s " msgstr " Por %(filter_title)s " #: mptt/templatetags/mptt_tags.py:29 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "Se ha dado un modelo inválido al tag full_tree_for_model: %s" #: mptt/templatetags/mptt_tags.py:54 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "Se ha dado un modelo inválido al tag drilldown_tree_for_node: %s" #: mptt/templatetags/mptt_tags.py:61 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "Se ha dado un campo del modelo inválido al tag drilldown_tree_for_node: %s" #: mptt/templatetags/mptt_tags.py:88 #, python-format msgid "%s tag requires three arguments" msgstr "El tag %s requiere tres argumentos" #: mptt/templatetags/mptt_tags.py:90 mptt/templatetags/mptt_tags.py:146 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "el segundo argumento del tag %s debe ser 'as'" #: mptt/templatetags/mptt_tags.py:143 #, python-format msgid "%s tag requires either three, four, seven, eight, or nine arguments" msgstr "el tag %s requiere tres, cuatro, siete, ocho o nueve argumentos" #: mptt/templatetags/mptt_tags.py:158 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "si se dan siete argumentos al tag %s, el cuatro argumento tiene que ser 'with'" #: mptt/templatetags/mptt_tags.py:162 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "si se da siete argumentos al tag %s, el sexto argumento debe ser 'in'" #: mptt/templatetags/mptt_tags.py:168 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "si se dan ocho argumentos al tag %s, el cuarto argumento debe ser 'cumulative'" #: mptt/templatetags/mptt_tags.py:172 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "si se dan ocho argumentos al tag %s, el quinto argumento debe ser 'count'" #: mptt/templatetags/mptt_tags.py:176 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "si se dan ocho argumentos al tag %s, el séptimo argumento debe ser 'in'" #: mptt/templatetags/mptt_tags.py:295 #, python-format msgid "%s tag requires a queryset" msgstr "el tag %s requiere un queryset" #: mptt/utils.py:252 #, python-format msgid "Node %s not in depth-first order" msgstr "El nodo %s no está en el primer orden de profundidad" django-mptt-0.13.2/mptt/locale/es_AR/000077500000000000000000000000001411214034000172235ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/es_AR/LC_MESSAGES/000077500000000000000000000000001411214034000210105ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/es_AR/LC_MESSAGES/django.mo000066400000000000000000000056711411214034000226200ustar00rootroot000000000000006 @9`);+",2O    )@:6ZFLE%FkC&p0!9'<C*5    % #5 >Y 5 1 M RN L M J< 1     %s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.First childLast childLeft siblingRight siblingThe model %s has already been registered.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2014-08-29 12:31+0200 PO-Revision-Date: 2015-10-11 21:39-0300 Last-Translator: Gonzalo Bustos Language-Team: Spanish (Argentina) Language: es_AR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Generator: Poedit 1.6.10 El tag %s requiere tres, siete u ocho argumentosEl tag %s require tres argumentosUn nodo no puede ser hijo de alguno de sus descendientes.Un nodo no puede ser hijo de sí mismo.Un nodo no puede ser hermano de alguno de sus descendientes.Un nodo no puede ser hermano de sí mismo.Posición inválida: %s.No se puede insertar un nodo que ya ha sido guardado.Primer hijoÚltimo hijoHermano izquierdoHermano derechoEl modelo %s ya ha sido registrado.Campo de modelo inválido para tag drilldown_tree_for_node: %sModelo inválido para tag drilldown_tree_for_node: %sModelo inválido para tag full_tree_for_model: %sSi se proporcionan ocho argumentos, el quinto para el tag %s debe ser 'count'Si se proporcionan ocho argumentos, el cuarto para el tag %s debe ser 'cumulative'Si se proporcionan ocho argumentos, el séptimo para el tag %s debe ser 'in'Si se proporcionan siete argumentos, el cuarto para el tag %s debe ser 'with'Si se proporcionan siete argumentos, el sexto para el tag %s debe ser 'in'El segundo argumento para el tag %s debe ser 'as'django-mptt-0.13.2/mptt/locale/es_AR/LC_MESSAGES/django.po000066400000000000000000000076361411214034000226260ustar00rootroot00000000000000# SPANISH (ARGENTINA) TRANSLATION # This file is distributed under the same license as the PACKAGE django-mptt. # Gonzalo Bustos, 2015. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-08-29 12:31+0200\n" "PO-Revision-Date: 2015-10-11 21:39-0300\n" "Last-Translator: Gonzalo Bustos\n" "Language-Team: Spanish (Argentina)\n" "Language: es_AR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.6.10\n" #: __init__.py:34 #, python-format msgid "The model %s has already been registered." msgstr "El modelo %s ya ha sido registrado." #: forms.py:41 msgid "First child" msgstr "Primer hijo" #: forms.py:42 msgid "Last child" msgstr "Último hijo" #: forms.py:43 msgid "Left sibling" msgstr "Hermano izquierdo" #: forms.py:44 msgid "Right sibling" msgstr "Hermano derecho" #: managers.py:121 msgid "Cannot insert a node which has already been saved." msgstr "No se puede insertar un nodo que ya ha sido guardado." #: managers.py:306 managers.py:480 managers.py:516 managers.py:673 #, python-format msgid "An invalid position was given: %s." msgstr "Posición inválida: %s." #: managers.py:466 managers.py:653 msgid "A node may not be made a sibling of itself." msgstr "Un nodo no puede ser hermano de sí mismo." #: managers.py:632 managers.py:753 msgid "A node may not be made a child of itself." msgstr "Un nodo no puede ser hijo de sí mismo." #: managers.py:634 managers.py:755 msgid "A node may not be made a child of any of its descendants." msgstr "Un nodo no puede ser hijo de alguno de sus descendientes." #: managers.py:655 msgid "A node may not be made a sibling of any of its descendants." msgstr "Un nodo no puede ser hermano de alguno de sus descendientes." #: templatetags/mptt_tags.py:23 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "Modelo inválido para tag full_tree_for_model: %s" #: templatetags/mptt_tags.py:44 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "Modelo inválido para tag drilldown_tree_for_node: %s" #: templatetags/mptt_tags.py:48 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "Campo de modelo inválido para tag drilldown_tree_for_node: %s" #: templatetags/mptt_tags.py:72 #, python-format msgid "%s tag requires three arguments" msgstr "El tag %s require tres argumentos" #: templatetags/mptt_tags.py:74 templatetags/mptt_tags.py:125 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "El segundo argumento para el tag %s debe ser 'as'" #: templatetags/mptt_tags.py:123 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "El tag %s requiere tres, siete u ocho argumentos" #: templatetags/mptt_tags.py:128 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "Si se proporcionan siete argumentos, el cuarto para el tag %s debe ser 'with'" #: templatetags/mptt_tags.py:130 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "" "Si se proporcionan siete argumentos, el sexto para el tag %s debe ser 'in'" #: templatetags/mptt_tags.py:134 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "Si se proporcionan ocho argumentos, el cuarto para el tag %s debe ser " "'cumulative'" #: templatetags/mptt_tags.py:136 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "Si se proporcionan ocho argumentos, el quinto para el tag %s debe ser 'count'" #: templatetags/mptt_tags.py:138 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "Si se proporcionan ocho argumentos, el séptimo para el tag %s debe ser 'in'" django-mptt-0.13.2/mptt/locale/fi/000077500000000000000000000000001411214034000166305ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/fi/LC_MESSAGES/000077500000000000000000000000001411214034000204155ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/fi/LC_MESSAGES/django.mo000066400000000000000000000104031411214034000222120ustar00rootroot00000000000000'T5`a69)-;W+"2 '':&b      $ %*@P:6FLJEFC$h}0&   @ !! 3C &w 7 *   0' X d &x       -" *P { * F >:DY_Z9ZWGe3/&# %" '$   ! %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'Project-Id-Version: Django-mptt POT-Creation-Date: 2016-11-24 02:16+0200 PO-Revision-Date: 2016-11-24 02:47+0200 Last-Translator: Taija Mertanen Language-Team: Taija Mertanen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Generator: Poedit 1.5.4 X-Poedit-KeywordsList: _;gettext;gettext_noop X-Poedit-Basepath: . Language: fi %s on onnistuneesti liikutettu.%s tägi vaatii querysetin%s tägi vaatii joko kolme, seitsemän tai kahdeksan argumenttia%s tägi vaatii kolme argumenttiaSolmusta ei voi tehdä oman jälkeläisensä lasta.Solmusta ei voi tehdä itsensä lasta.Solmusta ei voi tehdä oman jälkeläisensä sisarusta.Solmusta ei voi tehdä itsensä sisarusta.KaikkiInvalidi sijainti annettu: %s.Ei voida lisätä solmua joka on jo tallennettu.Piilota puuTietokantavirhe: %sPoista valitut %(verbose_name_plural)sSiirto-ohjetta ei ymmärrettyLaajenna puuEnsimmäinen lapsiInvalidi vanhempiViimeinen lapsiVasemmanpuoleinen sisarusEi lupaaSolmu %s ei ole syvyys-ensin järjestyksessäObjektit ovat kadonneet, yritä uudelleen.Oikeanpuoleinen sisarusOnnistuneesti poistettiin %(count)d asiaa.drilldown_tree_for_node -tägille annettiin invalidi malli kenttä: %sdrilldown_tree_for_node -tägille annettiin invalidi malli: %sfull_tree_for_model -tägille annettiin invalidi malli: %sjos kahdeksan argumenttia annetaan, viidennen argumentin %s tägille pitää olla 'count'jos kahdeksan argumenttia annetaan, neljännen argumentin %s tägille pitää olla 'cumulative'jos kahdeksan argumenttia annetaan, seitsemännen argumentin %s tägille pitää olla 'in'jos seitsemän argumenttia annetaan, neljännen argumentin %s tägille pitää olla 'with'jos seitsemän argumenttia annetaan, kuudennen argumentin %s tägille pitää olla 'in'liikuta solmu solmun jälkeenliikuta solmu solmun edelleliikuta solmu lapsisijaintiinregister() odottaa Djangon model-luokan argumenttiatoinen argumentti %s tägille pitää olla 'as'django-mptt-0.13.2/mptt/locale/fi/LC_MESSAGES/django.po000066400000000000000000000133411411214034000222210ustar00rootroot00000000000000# django-mptt Finnish translation (suomeksi). # This file is distributed under the same license as the django-mptt package. # Taija Mertanen , 2016. # msgid "" msgstr "" "Project-Id-Version: Django-mptt\n" "POT-Creation-Date: 2016-11-24 02:16+0200\n" "PO-Revision-Date: 2016-11-24 02:47+0200\n" "Last-Translator: Taija Mertanen \n" "Language-Team: Taija Mertanen \n" "Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.5.4\n" #: mptt/forms.py:63 msgid "First child" msgstr "Ensimmäinen lapsi" #: mptt/forms.py:64 msgid "Last child" msgstr "Viimeinen lapsi" #: mptt/forms.py:65 msgid "Left sibling" msgstr "Vasemmanpuoleinen sisarus" #: mptt/forms.py:66 msgid "Right sibling" msgstr "Oikeanpuoleinen sisarus" #: mptt/forms.py:184 msgid "Invalid parent" msgstr "Invalidi vanhempi" #: mptt/models.py:292 msgid "register() expects a Django model class argument" msgstr "register() odottaa Djangon model-luokan argumenttia" #: mptt/admin.py:87 #, python-format msgid "Successfully deleted %(count)d items." msgstr "Onnistuneesti poistettiin %(count)d asiaa." #: mptt/admin.py:100 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Poista valitut %(verbose_name_plural)s" #: mptt/admin.py:212 msgid "Did not understand moving instruction." msgstr "Siirto-ohjetta ei ymmärretty" #: mptt/admin.py:220 msgid "Objects have disappeared, try again." msgstr "Objektit ovat kadonneet, yritä uudelleen." #: mptt/admin.py:224 msgid "No permission" msgstr "Ei lupaa" #: mptt/admin.py:233 #, python-format msgid "Database error: %s" msgstr "Tietokantavirhe: %s" #: mptt/admin.py:238 #, python-format msgid "%s has been successfully moved." msgstr "%s on onnistuneesti liikutettu." #: mptt/admin.py:249 msgid "move node before node" msgstr "liikuta solmu solmun edelle" #: mptt/admin.py:250 msgid "move node to child position" msgstr "liikuta solmu lapsisijaintiin" #: mptt/admin.py:251 msgid "move node after node" msgstr "liikuta solmu solmun jälkeen" #: mptt/admin.py:252 msgid "Collapse tree" msgstr "Piilota puu" #: mptt/admin.py:253 msgid "Expand tree" msgstr "Laajenna puu" #: mptt/admin.py:364 msgid "All" msgstr "Kaikki" #: mptt/utils.py:240 #, python-format msgid "Node %s not in depth-first order" msgstr "Solmu %s ei ole syvyys-ensin järjestyksessä" #: mptt/managers.py:521 msgid "Cannot insert a node which has already been saved." msgstr "Ei voida lisätä solmua joka on jo tallennettu." #: mptt/managers.py:739 mptt/managers.py:912 mptt/managers.py:948 #: mptt/managers.py:1114 #, python-format msgid "An invalid position was given: %s." msgstr "Invalidi sijainti annettu: %s." #: mptt/managers.py:898 mptt/managers.py:1094 msgid "A node may not be made a sibling of itself." msgstr "Solmusta ei voi tehdä itsensä sisarusta." #: mptt/managers.py:1073 mptt/managers.py:1199 msgid "A node may not be made a child of itself." msgstr "Solmusta ei voi tehdä itsensä lasta." #: mptt/managers.py:1075 mptt/managers.py:1201 msgid "A node may not be made a child of any of its descendants." msgstr "Solmusta ei voi tehdä oman jälkeläisensä lasta." #: mptt/managers.py:1096 msgid "A node may not be made a sibling of any of its descendants." msgstr "Solmusta ei voi tehdä oman jälkeläisensä sisarusta." #: mptt/templatetags/mptt_tags.py:31 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model -tägille annettiin invalidi malli: %s" #: mptt/templatetags/mptt_tags.py:55 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldown_tree_for_node -tägille annettiin invalidi malli: %s" #: mptt/templatetags/mptt_tags.py:62 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldown_tree_for_node -tägille annettiin invalidi malli kenttä: %s" #: mptt/templatetags/mptt_tags.py:89 #, python-format msgid "%s tag requires three arguments" msgstr "%s tägi vaatii kolme argumenttia" #: mptt/templatetags/mptt_tags.py:91 mptt/templatetags/mptt_tags.py:146 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "toinen argumentti %s tägille pitää olla 'as'" #: mptt/templatetags/mptt_tags.py:143 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s tägi vaatii joko kolme, seitsemän tai kahdeksan argumenttia" #: mptt/templatetags/mptt_tags.py:150 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "jos seitsemän argumenttia annetaan, neljännen argumentin %s tägille pitää " "olla 'with'" #: mptt/templatetags/mptt_tags.py:154 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "" "jos seitsemän argumenttia annetaan, kuudennen argumentin %s tägille pitää " "olla 'in'" #: mptt/templatetags/mptt_tags.py:160 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "jos kahdeksan argumenttia annetaan, neljännen argumentin %s tägille pitää " "olla 'cumulative'" #: mptt/templatetags/mptt_tags.py:164 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "jos kahdeksan argumenttia annetaan, viidennen argumentin %s tägille pitää " "olla 'count'" #: mptt/templatetags/mptt_tags.py:168 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "jos kahdeksan argumenttia annetaan, seitsemännen argumentin %s tägille pitää " "olla 'in'" #: mptt/templatetags/mptt_tags.py:287 #, python-format msgid "%s tag requires a queryset" msgstr "%s tägi vaatii querysetin" django-mptt-0.13.2/mptt/locale/fr/000077500000000000000000000000001411214034000166415ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/fr/LC_MESSAGES/000077500000000000000000000000001411214034000204265ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/fr/LC_MESSAGES/django.mo000066400000000000000000000110311411214034000222210ustar00rootroot00000000000000)d;6!9A){;+ "24 gu'&      $$E j%x@:6FQLEF+Cr0&. U K[  " ) 5 'A ;i ) < + 8 .= >l  5 5 Q i x    8 ' 5.NdEAc;h_bh^"*!M'o@:# " % ) &'! $( By %(filter_title)s %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: django-mptt master Report-Msgid-Bugs-To: POT-Creation-Date: 2016-09-21 13:25-0500 PO-Revision-Date: 2016-09-21 20:28+0200 Last-Translator: Claude Paroz Language-Team: French Language: fr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Par %(filter_title)s %s a été déplacé avec succès.la balise %s requiert un « queryset »la balise %s requiert trois, sept ou huit paramètresla balise %s requiert trois paramètresUn nœud ne peut être l'enfant d'aucun de ses descendants.Un nœud ne peut être son propre enfant.Un nœud ne peut être voisin avec aucun de ses descendants.Un nœud ne peut être voisin de lui-même.ToutUne position non valide a été fournie : %s.Impossible d'insérer un nœud qui a déjà été enregistré.Condenser l'arborescenceErreur de base de données : %sSupprimer les %(verbose_name_plural)s sélectionnéesL'instruction de déplacement n'a pas été comprise.Étendre l'arborescencePremier enfantParent non valideDernier enfantVoisin de gauchePas de permissionLe nœud %s n'est pas dans l'ordre de profondeur d'abordLes objets ont disparu, essayez encore.Voisin de droite%(count)d éléments on été supprimés avec succèsla balise drilldown_tree_for_node a reçu un champ de modèle non valide : %sla balise drilldown_tree_for_node a reçu un modèle non valide : %sla balise full_tree_for_model a reçu un modèle non valide : %ssi huit paramètres sont fournis, le cinquième paramètre de la balise %s doit être « count »si huit paramètres sont fournis, le quatrième paramètre de la balise %s doit être « cumulative »si huit paramètres sont fournis, le septième paramètre de la balise %s doit être « in »si sept paramètres sont fournis, le quatrième paramètre de la balise %s doit être « with »si sept paramètres sont fournis, le sixième paramètre de la balise %s doit être « in »déplacer le nœud après ce nœuddéplacer le nœud avant ce nœuddéplacer le nœud à une sous-positionregister() s'attend à un paramètre de classe de modèle Djangole second paramètre de la balise %s doit être « as »titredjango-mptt-0.13.2/mptt/locale/fr/LC_MESSAGES/django.po000066400000000000000000000141721411214034000222350ustar00rootroot00000000000000# Copyright (C) 2011-2016 Listed translators # This file is distributed under the same license as the django-mptt package. # Maxime Haineault, 2011 # Claude Paroz, 2016 # msgid "" msgstr "" "Project-Id-Version: django-mptt master\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-09-21 13:25-0500\n" "PO-Revision-Date: 2016-09-21 20:28+0200\n" "Last-Translator: Claude Paroz \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: mptt/admin.py:87 #, python-format msgid "Successfully deleted %(count)d items." msgstr "%(count)d éléments on été supprimés avec succès" #: mptt/admin.py:100 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Supprimer les %(verbose_name_plural)s sélectionnées" #: mptt/admin.py:182 msgid "title" msgstr "titre" #: mptt/admin.py:212 msgid "Did not understand moving instruction." msgstr "L'instruction de déplacement n'a pas été comprise." #: mptt/admin.py:220 msgid "Objects have disappeared, try again." msgstr "Les objets ont disparu, essayez encore." #: mptt/admin.py:224 msgid "No permission" msgstr "Pas de permission" #: mptt/admin.py:233 #, python-format msgid "Database error: %s" msgstr "Erreur de base de données : %s" #: mptt/admin.py:238 #, python-format msgid "%s has been successfully moved." msgstr "%s a été déplacé avec succès." #: mptt/admin.py:249 msgid "move node before node" msgstr "déplacer le nœud avant ce nœud" #: mptt/admin.py:250 msgid "move node to child position" msgstr "déplacer le nœud à une sous-position" #: mptt/admin.py:251 msgid "move node after node" msgstr "déplacer le nœud après ce nœud" #: mptt/admin.py:252 msgid "Collapse tree" msgstr "Condenser l'arborescence" #: mptt/admin.py:253 msgid "Expand tree" msgstr "Étendre l'arborescence" #: mptt/admin.py:364 msgid "All" msgstr "Tout" #: mptt/forms.py:63 msgid "First child" msgstr "Premier enfant" #: mptt/forms.py:64 msgid "Last child" msgstr "Dernier enfant" #: mptt/forms.py:65 msgid "Left sibling" msgstr "Voisin de gauche" #: mptt/forms.py:66 msgid "Right sibling" msgstr "Voisin de droite" #: mptt/forms.py:184 msgid "Invalid parent" msgstr "Parent non valide" #: mptt/managers.py:521 msgid "Cannot insert a node which has already been saved." msgstr "Impossible d'insérer un nœud qui a déjà été enregistré." #: mptt/managers.py:739 mptt/managers.py:912 mptt/managers.py:948 #: mptt/managers.py:1114 #, python-format msgid "An invalid position was given: %s." msgstr "Une position non valide a été fournie : %s." #: mptt/managers.py:898 mptt/managers.py:1094 msgid "A node may not be made a sibling of itself." msgstr "Un nœud ne peut être voisin de lui-même." #: mptt/managers.py:1073 mptt/managers.py:1199 msgid "A node may not be made a child of itself." msgstr "Un nœud ne peut être son propre enfant." #: mptt/managers.py:1075 mptt/managers.py:1201 msgid "A node may not be made a child of any of its descendants." msgstr "Un nœud ne peut être l'enfant d'aucun de ses descendants." #: mptt/managers.py:1096 msgid "A node may not be made a sibling of any of its descendants." msgstr "Un nœud ne peut être voisin avec aucun de ses descendants." #: mptt/models.py:292 msgid "register() expects a Django model class argument" msgstr "register() s'attend à un paramètre de classe de modèle Django" #: mptt/templates/admin/mptt_filter.html:3 #, python-format msgid " By %(filter_title)s " msgstr " Par %(filter_title)s " #: mptt/templatetags/mptt_tags.py:31 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "la balise full_tree_for_model a reçu un modèle non valide : %s" #: mptt/templatetags/mptt_tags.py:55 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "la balise drilldown_tree_for_node a reçu un modèle non valide : %s" #: mptt/templatetags/mptt_tags.py:62 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "" "la balise drilldown_tree_for_node a reçu un champ de modèle non valide : %s" #: mptt/templatetags/mptt_tags.py:89 #, python-format msgid "%s tag requires three arguments" msgstr "la balise %s requiert trois paramètres" #: mptt/templatetags/mptt_tags.py:91 mptt/templatetags/mptt_tags.py:146 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "le second paramètre de la balise %s doit être « as »" #: mptt/templatetags/mptt_tags.py:143 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "la balise %s requiert trois, sept ou huit paramètres" #: mptt/templatetags/mptt_tags.py:150 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "si sept paramètres sont fournis, le quatrième paramètre de la balise %s doit " "être « with »" #: mptt/templatetags/mptt_tags.py:154 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "" "si sept paramètres sont fournis, le sixième paramètre de la balise %s doit " "être « in »" #: mptt/templatetags/mptt_tags.py:160 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "si huit paramètres sont fournis, le quatrième paramètre de la balise %s doit " "être « cumulative »" #: mptt/templatetags/mptt_tags.py:164 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "si huit paramètres sont fournis, le cinquième paramètre de la balise %s doit " "être « count »" #: mptt/templatetags/mptt_tags.py:168 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "si huit paramètres sont fournis, le septième paramètre de la balise %s doit " "être « in »" #: mptt/templatetags/mptt_tags.py:287 #, python-format msgid "%s tag requires a queryset" msgstr "la balise %s requiert un « queryset »" #: mptt/utils.py:240 #, python-format msgid "Node %s not in depth-first order" msgstr "Le nœud %s n'est pas dans l'ordre de profondeur d'abord" django-mptt-0.13.2/mptt/locale/hu/000077500000000000000000000000001411214034000166465ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/hu/LC_MESSAGES/000077500000000000000000000000001411214034000204335ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/hu/LC_MESSAGES/django.mo000066400000000000000000000106331411214034000222350ustar00rootroot00000000000000)d;6!9A){;+ "24 gu'&      $$E j%x@:6FQLEF+Cr0&. U e[    7 I 8j * 9 + 4 9 =V   / 5 . < R b v % >  #D'>lEX]JTVST$"#G9\# " % ) &'! $( By %(filter_title)s %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2016-11-21 15:26+0100 PO-Revision-Date: 2016-11-21 15:26+0100 Last-Translator: Istvan Farkas Language-Team: Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); %(filter_title)s alapján %s sikeresen átmozgatva.a %s tagnek queryset kella %s tag vagy három, hét, vagy nyolc paramétert vára %s tag három paramétert várEgy elem nem lehet a saját leszármazottainak gyermeke.Egy elem nem lehet a saját maga gyermeke.Egy elem nem lehet a saját leszármazottainak testvére.Egy elem nem lehet a saját maga testvére.MindÉrvénytelen pozíció: %s.Nem lehet beilleszteni olyan elemt, amely már el van mentve.Fa összecsukásaAdatbázis hiba: %sKiválasztott %(verbose_name_plural)s törléseNem lehetett értelmezni a mozgatási instrukciókat.Fa kinyitásaElső gyermekÉrvénytelen szülőUtolsó gyermekBal oldali testvérNincs jogosultságA(z) %s elem mélység sorrendben vanObjektumok eltűntek menet közben, kérjük próbálja újra.Jobb oldali testvér%(count)d elem sikeresen törölve.a drilldown_tree_for_node tag érvénytelen modell mezőt kapott: %sa drilldown_tree_for_node tag érvénytelen modellt kapott: %sa full_tree_for_model tag érvénytelen modell paramétert kapott: %sha nyolc paraméter van megadva, a %s tag ötödik paramétere 'count' kell, hogy legyenha nyolc paraméter van megadva, a %s tag negyedik paramétere 'cumulative' kell, hogy legyenha nyolc paraméter van megadva, a %s tag hetedik paramétere 'in' kell, hogy legyenha hét paraméter van megadva, a %s tag negyedik paramétere 'with' kell, hogy legyenha hét paraméter van megadva, a %s tag hatodik paramétere 'in' kell, hogy legyenelem mozgatása a másik elem mögéelem mozgatása a másik elem eléelem mozgatása gyermek pozícióbaA register() eljárás egy Django modell osztályt vár paraméterkéntA %s tag második paramétere 'as' szó kell, hogy legyencímdjango-mptt-0.13.2/mptt/locale/hu/LC_MESSAGES/django.po000066400000000000000000000132471411214034000222440ustar00rootroot00000000000000msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-11-21 15:26+0100\n" "PO-Revision-Date: 2016-11-21 15:26+0100\n" "Last-Translator: Istvan Farkas \n" "Language-Team: \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: .\admin.py:88 #, python-format msgid "Successfully deleted %(count)d items." msgstr "%(count)d elem sikeresen törölve." #: .\admin.py:101 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Kiválasztott %(verbose_name_plural)s törlése" #: .\admin.py:183 msgid "title" msgstr "cím" #: .\admin.py:213 msgid "Did not understand moving instruction." msgstr "Nem lehetett értelmezni a mozgatási instrukciókat." #: .\admin.py:221 msgid "Objects have disappeared, try again." msgstr "Objektumok eltűntek menet közben, kérjük próbálja újra." #: .\admin.py:225 msgid "No permission" msgstr "Nincs jogosultság" #: .\admin.py:234 #, python-format msgid "Database error: %s" msgstr "Adatbázis hiba: %s" #: .\admin.py:239 #, python-format msgid "%s has been successfully moved." msgstr "%s sikeresen átmozgatva." #: .\admin.py:250 msgid "move node before node" msgstr "elem mozgatása a másik elem elé" #: .\admin.py:251 msgid "move node to child position" msgstr "elem mozgatása gyermek pozícióba" #: .\admin.py:252 msgid "move node after node" msgstr "elem mozgatása a másik elem mögé" #: .\admin.py:253 msgid "Collapse tree" msgstr "Fa összecsukása" #: .\admin.py:254 msgid "Expand tree" msgstr "Fa kinyitása" #: .\admin.py:365 msgid "All" msgstr "Mind" #: .\forms.py:63 msgid "First child" msgstr "Első gyermek" #: .\forms.py:64 msgid "Last child" msgstr "Utolsó gyermek" #: .\forms.py:65 msgid "Left sibling" msgstr "Bal oldali testvér" #: .\forms.py:66 msgid "Right sibling" msgstr "Jobb oldali testvér" #: .\forms.py:184 msgid "Invalid parent" msgstr "Érvénytelen szülő" #: .\managers.py:522 msgid "Cannot insert a node which has already been saved." msgstr "Nem lehet beilleszteni olyan elemt, amely már el van mentve." #: .\managers.py:740 .\managers.py:913 .\managers.py:949 .\managers.py:1115 #, python-format msgid "An invalid position was given: %s." msgstr "Érvénytelen pozíció: %s." #: .\managers.py:899 .\managers.py:1095 msgid "A node may not be made a sibling of itself." msgstr "Egy elem nem lehet a saját maga testvére." #: .\managers.py:1074 .\managers.py:1200 msgid "A node may not be made a child of itself." msgstr "Egy elem nem lehet a saját maga gyermeke." #: .\managers.py:1076 .\managers.py:1202 msgid "A node may not be made a child of any of its descendants." msgstr "Egy elem nem lehet a saját leszármazottainak gyermeke." #: .\managers.py:1097 msgid "A node may not be made a sibling of any of its descendants." msgstr "Egy elem nem lehet a saját leszármazottainak testvére." #: .\models.py:293 msgid "register() expects a Django model class argument" msgstr "A register() eljárás egy Django modell osztályt vár paraméterként" #: .\templates\admin\mptt_filter.html:3 #, python-format msgid " By %(filter_title)s " msgstr " %(filter_title)s alapján " #: .\templatetags\mptt_tags.py:31 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "a full_tree_for_model tag érvénytelen modell paramétert kapott: %s" #: .\templatetags\mptt_tags.py:55 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "a drilldown_tree_for_node tag érvénytelen modellt kapott: %s" #: .\templatetags\mptt_tags.py:62 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "a drilldown_tree_for_node tag érvénytelen modell mezőt kapott: %s" #: .\templatetags\mptt_tags.py:89 #, python-format msgid "%s tag requires three arguments" msgstr "a %s tag három paramétert vár" #: .\templatetags\mptt_tags.py:91 .\templatetags\mptt_tags.py:146 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "A %s tag második paramétere 'as' szó kell, hogy legyen" #: .\templatetags\mptt_tags.py:143 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "a %s tag vagy három, hét, vagy nyolc paramétert vár" #: .\templatetags\mptt_tags.py:150 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "ha hét paraméter van megadva, a %s tag negyedik paramétere 'with' kell, hogy legyen" #: .\templatetags\mptt_tags.py:154 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "ha hét paraméter van megadva, a %s tag hatodik paramétere 'in' kell, hogy legyen" #: .\templatetags\mptt_tags.py:160 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "ha nyolc paraméter van megadva, a %s tag negyedik paramétere 'cumulative' kell, hogy legyen" #: .\templatetags\mptt_tags.py:164 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "ha nyolc paraméter van megadva, a %s tag ötödik paramétere 'count' kell, hogy legyen" #: .\templatetags\mptt_tags.py:168 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "ha nyolc paraméter van megadva, a %s tag hetedik paramétere 'in' kell, hogy legyen" #: .\templatetags\mptt_tags.py:287 #, python-format msgid "%s tag requires a queryset" msgstr "a %s tagnek queryset kell" #: .\utils.py:240 #, python-format msgid "Node %s not in depth-first order" msgstr "A(z) %s elem mélység sorrendben van" django-mptt-0.13.2/mptt/locale/it/000077500000000000000000000000001411214034000166465ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/it/LC_MESSAGES/000077500000000000000000000000001411214034000204335ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/it/LC_MESSAGES/django.mo000066400000000000000000000101021411214034000222240ustar00rootroot00000000000000)d;6!9A){;+ "24 gu'&      $$E j%x@:6FQLEF+Cr0&. U g[  "  . K ;l , = 1 E 0K 7|      $ 1 B P b 'v -  ) J DQ @ Q V)OQN"q!2<;# " % ) &'! $( By %(filter_title)s %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleLanguage: it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per %(filter_title)s %s è stato spostato con successo.il tag %s richiede un querysetil tag %s richiede tre, sette o otto argomentiil tag %s richiede tre argomentiUn nodo non può essere figlio di uno dei suoi discendenti.Un nodo non può essere figlio di se stesso.Un nodo non può essere fratello di uno dei suoi discendenti.Un nodo non può essere il fratello di se stesso.TuttoE' stata fornita una posizione non valida : %s.Non è possibile inserire un nodo che è stato salvato.Comprimi l'alberoErrore database : %sElimina %(verbose_name_plural)sSpostamento non validoEspandi l'alberoPrimo figlioPadre non validoUltimo figlioFratello sinistroNon hai il permessoIl nodo %s non è in ordine depth-firstGli oggetti sono spariti, riprova per favore.Fratello destro%(count)d elementi eliminati con successoil tag drilldown_tree_for_node ha ricevuto un model field non valido : %sil tag drilldown_tree_for_node ha ricevuto un model non valido : %sil tag full_tree_for_model ha ricevuto un model non valido : %sse sono forniti otto argomenti, il quinto argomento del tag %s dev'essere 'count'se sono forniti otto argomenti, il quarto argomento del tag %s dev'essere 'cumulative'se sono forniti otto argomenti, il settimo argomento del tag %s dev'essere 'in'se sono forniti sette argomenti, il quarto argomento del tag %s dev'essere 'with'se sono forniti sette argomenti, il sesto argomento del tag %s dev'essere 'in'muovi il nodo dopo il nodomuovi il nodo prima del nodomuovi il nodo in posizione figlioregister() richiede un Django model come argomentoil secondo argomento nel tag %s dev'essere passato come 'as'titolodjango-mptt-0.13.2/mptt/locale/it/LC_MESSAGES/django.po000066400000000000000000000131761411214034000222450ustar00rootroot00000000000000# Translation of django-mptt into Italian. # This file is distributed under the same license as the django-mptt package. # Andrea Rabbaglietti , 2018. msgid "" msgstr "" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: mptt/admin.py:87 #, python-format msgid "Successfully deleted %(count)d items." msgstr "%(count)d elementi eliminati con successo" #: mptt/admin.py:100 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Elimina %(verbose_name_plural)s" #: mptt/admin.py:182 msgid "title" msgstr "titolo" #: mptt/admin.py:212 msgid "Did not understand moving instruction." msgstr "Spostamento non valido" #: mptt/admin.py:220 msgid "Objects have disappeared, try again." msgstr "Gli oggetti sono spariti, riprova per favore." #: mptt/admin.py:224 msgid "No permission" msgstr "Non hai il permesso" #: mptt/admin.py:233 #, python-format msgid "Database error: %s" msgstr "Errore database : %s" #: mptt/admin.py:238 #, python-format msgid "%s has been successfully moved." msgstr "%s è stato spostato con successo." #: mptt/admin.py:249 msgid "move node before node" msgstr "muovi il nodo prima del nodo" #: mptt/admin.py:250 msgid "move node to child position" msgstr "muovi il nodo in posizione figlio" #: mptt/admin.py:251 msgid "move node after node" msgstr "muovi il nodo dopo il nodo" #: mptt/admin.py:252 msgid "Collapse tree" msgstr "Comprimi l'albero" #: mptt/admin.py:253 msgid "Expand tree" msgstr "Espandi l'albero" #: mptt/admin.py:364 msgid "All" msgstr "Tutto" #: mptt/forms.py:63 msgid "First child" msgstr "Primo figlio" #: mptt/forms.py:64 msgid "Last child" msgstr "Ultimo figlio" #: mptt/forms.py:65 msgid "Left sibling" msgstr "Fratello sinistro" #: mptt/forms.py:66 msgid "Right sibling" msgstr "Fratello destro" #: mptt/forms.py:184 msgid "Invalid parent" msgstr "Padre non valido" #: mptt/managers.py:521 msgid "Cannot insert a node which has already been saved." msgstr "Non è possibile inserire un nodo che è stato salvato." #: mptt/managers.py:739 mptt/managers.py:912 mptt/managers.py:948 #: mptt/managers.py:1114 #, python-format msgid "An invalid position was given: %s." msgstr "E' stata fornita una posizione non valida : %s." #: mptt/managers.py:898 mptt/managers.py:1094 msgid "A node may not be made a sibling of itself." msgstr "Un nodo non può essere il fratello di se stesso." #: mptt/managers.py:1073 mptt/managers.py:1199 msgid "A node may not be made a child of itself." msgstr "Un nodo non può essere figlio di se stesso." #: mptt/managers.py:1075 mptt/managers.py:1201 msgid "A node may not be made a child of any of its descendants." msgstr "Un nodo non può essere figlio di uno dei suoi discendenti." #: mptt/managers.py:1096 msgid "A node may not be made a sibling of any of its descendants." msgstr "Un nodo non può essere fratello di uno dei suoi discendenti." #: mptt/models.py:292 msgid "register() expects a Django model class argument" msgstr "register() richiede un Django model come argomento" #: mptt/templates/admin/mptt_filter.html:3 #, python-format msgid " By %(filter_title)s " msgstr " Per %(filter_title)s " #: mptt/templatetags/mptt_tags.py:31 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "il tag full_tree_for_model ha ricevuto un model non valido : %s" #: mptt/templatetags/mptt_tags.py:55 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "il tag drilldown_tree_for_node ha ricevuto un model non valido : %s" #: mptt/templatetags/mptt_tags.py:62 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "il tag drilldown_tree_for_node ha ricevuto un model field non valido : %s" #: mptt/templatetags/mptt_tags.py:89 #, python-format msgid "%s tag requires three arguments" msgstr "il tag %s richiede tre argomenti" #: mptt/templatetags/mptt_tags.py:91 mptt/templatetags/mptt_tags.py:146 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "il secondo argomento nel tag %s dev'essere passato come 'as'" #: mptt/templatetags/mptt_tags.py:143 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "il tag %s richiede tre, sette o otto argomenti" #: mptt/templatetags/mptt_tags.py:150 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "se sono forniti sette argomenti, il quarto argomento del tag %s dev'essere 'with'" #: mptt/templatetags/mptt_tags.py:154 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "se sono forniti sette argomenti, il sesto argomento del tag %s dev'essere 'in'" #: mptt/templatetags/mptt_tags.py:160 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "se sono forniti otto argomenti, il quarto argomento del tag %s dev'essere 'cumulative'" #: mptt/templatetags/mptt_tags.py:164 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "se sono forniti otto argomenti, il quinto argomento del tag %s dev'essere 'count'" #: mptt/templatetags/mptt_tags.py:168 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "se sono forniti otto argomenti, il settimo argomento del tag %s dev'essere 'in'" #: mptt/templatetags/mptt_tags.py:287 #, python-format msgid "%s tag requires a queryset" msgstr "il tag %s richiede un queryset" #: mptt/utils.py:240 #, python-format msgid "Node %s not in depth-first order" msgstr "Il nodo %s non è in ordine depth-first" django-mptt-0.13.2/mptt/locale/mn/000077500000000000000000000000001411214034000166445ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/mn/LC_MESSAGES/000077500000000000000000000000001411214034000204315ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/mn/LC_MESSAGES/django.mo000066400000000000000000000070311411214034000222310ustar00rootroot000000000000006 @9`);+",2O    )@:6ZFLE%FkC&uX6U#EyZJ e U     ;4 bp b ^6 o r lx l jR [     %s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.First childLast childLeft siblingRight siblingThe model %s has already been registered.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: Mongolian translatioin Report-Msgid-Bugs-To: POT-Creation-Date: 2009-09-23 14:44+0200 PO-Revision-Date: 2014-03-04 23:32+0800 Last-Translator: Bayarkhuu Bataa Language-Team: Bayarkhuu Bataa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-Generator: Poedit 1.5.7 %s нь гурав, долоо эсвэл найман аргумент шаардана%s нь гурван аргумент шаарданаЗангилаа өөрийнхөө үр садын хүү байж болохгүй.Зангилаа өөрийнхөө хүү байж болохгүй.Зангилаа өөрийнхөө үр садын ах дүү байж болохгүй.Зангилаа өөрийнхөө ах дүү байж болохгүй.Буруу байрлал: %s.Өмнө нь орсон зангилааг дахиж оруулж болохгүй.Эхний хүүСүүлийн хүүЗүүн ах дүүсБаруун ах дүүс%s модел аль хэдийнэ бүртгэгдсэн.drilldown_tree_for_node нь буруу моделд тодорхойлогдсон байна: %sdrilldown_tree_for_node нь буруу моделд тодорхойлогдсон байна: %sfull_tree_for_model нь буруу моделд тодорхойлогдсон байна: %s%s руу найман аргумент дамжуулсан бол 5 дахь нь 'count' байх ёстой%s руу найман аргумент дамжуулсан бол 4 дэх нь 'cumulative' байх ёстой%s руу найман аргумент дамжуулсан бол 7 дахь нь 'in' байх ёстой%s руу долоон аргумент дамжуулсан бол 4 дэх нь 'with' байх ёстой%s руу долоон аргумент дамжуулсан бол 6 дэх нь 'in' байх ёстой%s руу дамжуулах хоёрдох аргумент нь 'as байх ёстой'django-mptt-0.13.2/mptt/locale/mn/LC_MESSAGES/django.po000066400000000000000000000110311411214034000222270ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: Mongolian translation\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2009-09-23 14:44+0200\n" "PO-Revision-Date: 2014-03-04 23:32+0800\n" "Last-Translator: Bayarkhuu Bataa \n" "Language-Team: Bayarkhuu Bataa \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 1.5.7\n" #: __init__.py:34 #, python-format msgid "The model %s has already been registered." msgstr "%s модел аль хэдийнэ бүртгэгдсэн." #: forms.py:41 msgid "First child" msgstr "Эхний хүү" #: forms.py:42 msgid "Last child" msgstr "Сүүлийн хүү" #: forms.py:43 msgid "Left sibling" msgstr "Зүүн ах дүүс" #: forms.py:44 msgid "Right sibling" msgstr "Баруун ах дүүс" #: managers.py:121 msgid "Cannot insert a node which has already been saved." msgstr "Өмнө нь орсон зангилааг дахиж оруулж болохгүй." #: managers.py:306 managers.py:480 managers.py:516 managers.py:673 #, python-format msgid "An invalid position was given: %s." msgstr "Буруу байрлал: %s." #: managers.py:466 managers.py:653 msgid "A node may not be made a sibling of itself." msgstr "Зангилаа өөрийнхөө ах дүү байж болохгүй." #: managers.py:632 managers.py:753 msgid "A node may not be made a child of itself." msgstr "Зангилаа өөрийнхөө хүү байж болохгүй." #: managers.py:634 managers.py:755 msgid "A node may not be made a child of any of its descendants." msgstr "Зангилаа өөрийнхөө үр садын хүү байж болохгүй." #: managers.py:655 msgid "A node may not be made a sibling of any of its descendants." msgstr "Зангилаа өөрийнхөө үр садын ах дүү байж болохгүй." #: templatetags/mptt_tags.py:23 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model нь буруу моделд тодорхойлогдсон байна: %s" #: templatetags/mptt_tags.py:44 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldown_tree_for_node нь буруу моделд тодорхойлогдсон байна: %s" #: templatetags/mptt_tags.py:48 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldown_tree_for_node нь буруу моделд тодорхойлогдсон байна: %s" #: templatetags/mptt_tags.py:72 #, python-format msgid "%s tag requires three arguments" msgstr "%s нь гурван аргумент шаардана" #: templatetags/mptt_tags.py:74 templatetags/mptt_tags.py:125 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "%s руу дамжуулах хоёрдох аргумент нь 'as байх ёстой'" #: templatetags/mptt_tags.py:123 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s нь гурав, долоо эсвэл найман аргумент шаардана" #: templatetags/mptt_tags.py:128 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "%s руу долоон аргумент дамжуулсан бол 4 дэх нь 'with' байх ёстой" #: templatetags/mptt_tags.py:130 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "%s руу долоон аргумент дамжуулсан бол 6 дэх нь 'in' байх ёстой" #: templatetags/mptt_tags.py:134 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "%s руу найман аргумент дамжуулсан бол 4 дэх нь 'cumulative' байх ёстой" #: templatetags/mptt_tags.py:136 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "%s руу найман аргумент дамжуулсан бол 5 дахь нь 'count' байх ёстой" #: templatetags/mptt_tags.py:138 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "%s руу найман аргумент дамжуулсан бол 7 дахь нь 'in' байх ёстой" django-mptt-0.13.2/mptt/locale/nb/000077500000000000000000000000001411214034000166315ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/nb/LC_MESSAGES/000077500000000000000000000000001411214034000204165ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/nb/LC_MESSAGES/django.mo000066400000000000000000000075411411214034000222240ustar00rootroot00000000000000 +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.13.2/mptt/locale/nb/LC_MESSAGES/django.po000066400000000000000000000126151411214034000222250ustar00rootroot00000000000000# 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.13.2/mptt/locale/pl/000077500000000000000000000000001411214034000166455ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/pl/LC_MESSAGES/000077500000000000000000000000001411214034000204325ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000074001411214034000222320ustar00rootroot00000000000000)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.13.2/mptt/locale/pl/LC_MESSAGES/django.po000066400000000000000000000120421411214034000222330ustar00rootroot00000000000000# 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.13.2/mptt/locale/pt_BR/000077500000000000000000000000001411214034000172405ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/pt_BR/LC_MESSAGES/000077500000000000000000000000001411214034000210255ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/pt_BR/LC_MESSAGES/django.mo000066400000000000000000000056131411214034000226310ustar00rootroot000000000000006 @9`);+",2O    )@:6ZFLE%FkC&C.a :';(Py3  7$ 7\ 3 P U No O L /[     %s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.First childLast childLeft siblingRight siblingThe model %s has already been registered.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'second argument to %s tag must be 'as'Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2014-08-19 16:30-300 PO-Revision-Date: 2014-08-19 16:30-300 Last-Translator: Luis Fagundes Language-Team: LANGUAGE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tag %s requer três, sete ou oito argumentosA tag %s requer três argumentosUm nó não pode ser filho de nenhum de seus descendentes.Um nó não pode ser filho de si mesmo.Um nó não pode ser irmão de nenhum de seus descendentes.Um nó não pode ser irmão de si mesmo.Posição inválida: %s.Não é possível inserir um nó que já foi salvo.Primeiro filhoÚltimo filhoIrmão à esquerdaIrmão à direitaO modelo %s foi registrado.drilldown_tree_for_node recebeu um modelo inválido: %sdrilldown_tree_for_node recebeu um modelo inválido: %sfull_tree_for_model recebeu um modelo inválido: %sse oito argumentos são dados, o quinto argumento para a tag %s deve ser 'count'se oito argumentos são dados, o quarto argumento para a tag %s deve ser 'cumulative'se oito argumentos são dados, o sétimo argumento para a tag %s deve ser 'in'se sete argumentos são dados, o quarto argumento para a tag %s deve ser 'with'se sete argumentos são dados, o sexto argumento para a tag %s deve ser 'in'o segundo argumento para a tag %s deve ser 'as'django-mptt-0.13.2/mptt/locale/pt_BR/LC_MESSAGES/django.po000066400000000000000000000076241411214034000226400ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-08-19 16:30-300\n" "PO-Revision-Date: 2014-08-19 16:30-300\n" "Last-Translator: Luis Fagundes \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: __init__.py:34 #, python-format msgid "The model %s has already been registered." msgstr "O modelo %s foi registrado." #: forms.py:41 msgid "First child" msgstr "Primeiro filho" #: forms.py:42 msgid "Last child" msgstr "Último filho" #: forms.py:43 msgid "Left sibling" msgstr "Irmão à esquerda" #: forms.py:44 msgid "Right sibling" msgstr "Irmão à direita" #: managers.py:121 msgid "Cannot insert a node which has already been saved." msgstr "Não é possível inserir um nó que já foi salvo." #: managers.py:306 managers.py:480 managers.py:516 managers.py:673 #, python-format msgid "An invalid position was given: %s." msgstr "Posição inválida: %s." #: managers.py:466 managers.py:653 msgid "A node may not be made a sibling of itself." msgstr "Um nó não pode ser irmão de si mesmo." #: managers.py:632 managers.py:753 msgid "A node may not be made a child of itself." msgstr "Um nó não pode ser filho de si mesmo." #: managers.py:634 managers.py:755 msgid "A node may not be made a child of any of its descendants." msgstr "Um nó não pode ser filho de nenhum de seus descendentes." #: managers.py:655 msgid "A node may not be made a sibling of any of its descendants." msgstr "Um nó não pode ser irmão de nenhum de seus descendentes." #: templatetags/mptt_tags.py:23 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model recebeu um modelo inválido: %s" #: templatetags/mptt_tags.py:44 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldown_tree_for_node recebeu um modelo inválido: %s" #: templatetags/mptt_tags.py:48 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldown_tree_for_node recebeu um modelo inválido: %s" #: templatetags/mptt_tags.py:72 #, python-format msgid "%s tag requires three arguments" msgstr "A tag %s requer três argumentos" #: templatetags/mptt_tags.py:74 templatetags/mptt_tags.py:125 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "o segundo argumento para a tag %s deve ser 'as'" #: templatetags/mptt_tags.py:123 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "A tag %s requer três, sete ou oito argumentos" #: templatetags/mptt_tags.py:128 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "se sete argumentos são dados, o quarto argumento para a tag %s deve ser 'with'" #: templatetags/mptt_tags.py:130 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "se sete argumentos são dados, o sexto argumento para a tag %s deve ser 'in'" #: templatetags/mptt_tags.py:134 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "se oito argumentos são dados, o quarto argumento para a tag %s deve ser 'cumulative'" #: templatetags/mptt_tags.py:136 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "se oito argumentos são dados, o quinto argumento para a tag %s deve ser 'count'" #: templatetags/mptt_tags.py:138 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "se oito argumentos são dados, o sétimo argumento para a tag %s deve ser 'in'" django-mptt-0.13.2/mptt/locale/ru/000077500000000000000000000000001411214034000166605ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/ru/LC_MESSAGES/000077500000000000000000000000001411214034000204455ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/ru/LC_MESSAGES/django.mo000066400000000000000000000122361411214034000222500ustar00rootroot00000000000000'T5`a69)-;W+"2 #'6&^      $ %&@L:6FLFEFC dy0&  , % K +U [ O [- O 6 d u & 9 K !@b2~!N@L3[R3N}S~]g3F-zHTDF%"  '$! &#   %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.An invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: django-mptt Report-Msgid-Bugs-To: POT-Creation-Date: 2016-03-02 12:10+0300 PO-Revision-Date: 2013-08-28 19:49+0400 Last-Translator: Rafael Kamashev Language-Team: Language: ru_RU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); %s был успешно перемещен.для %s требуется queryset%s требует три, семь или восемь аргументов%s требует три аргументаЭлемент не может быть потомком своего наследника.Элемент не может быть потомком самому себе.Элемент не может быть дочерним своему наследнику.Элемент не может быть дочерним самому себе.Была дана неверная позиция: %s.Невозможно добавить элемент, который уже был сохранён.Свернуть деревоОшибка базы данных: %sУдалить выбранное %(verbose_name_plural)sИзучите инструкцию по перемещению узлов.Развернуть деревоПервый потомокНеверный родительский узелПоследний потомокЛевый братНет доступаУзел %s имеет уровень меньше первого уровняОбъекты пропали, повторите еще раз.Правый братУспешно удалено %(count)d узлов.для drilldown_tree_for_node было дано неверное поле модели: %sдля drilldown_tree_for_node была дана неверная модель: %sдля full_tree_for_model была дана неверная модель: %sесли дано восемь аргументов, то пятый аргумент для %s должен быть 'count'если дано восемь аргументов, то четвёртый аргумент для %s должен быть 'cumulative'если дано восемь аргументов, то седьмой аргумент для %s должен быть 'in'если дано семь аргументов, то четвёртый аргумент для %s должен быть 'with'если дано семь аргументов, то шестой для %s должен быть 'in'переместить узел после узлапереместить узел до узлапереместить узел в подчиненную позициюregister() ожидает модель Django в качестве аргументавторым аргуметом для %s должен быть 'as'заголовокdjango-mptt-0.13.2/mptt/locale/ru/LC_MESSAGES/django.po000066400000000000000000000160011411214034000222450ustar00rootroot00000000000000# django-mptt in russian # This file is distributed under the same license as the django-mptt package. # Translators: # Sergey Vishnikin , 2013. # Rafael Kamashev , 2016. msgid "" msgstr "" "Project-Id-Version: django-mptt\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-03-02 12:10+0300\n" "PO-Revision-Date: 2013-08-28 19:49+0400\n" "Last-Translator: Rafael Kamashev \n" "Language-Team: \n" "Language: ru_RU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: admin.py:80 #, python-format #| msgid "Successfully deleted %s items." msgid "Successfully deleted %(count)d items." msgstr "Успешно удалено %(count)d узлов." #: admin.py:93 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Удалить выбранное %(verbose_name_plural)s" #: admin.py:175 msgid "title" msgstr "заголовок" #: admin.py:205 msgid "Did not understand moving instruction." msgstr "Изучите инструкцию по перемещению узлов." #: admin.py:213 msgid "Objects have disappeared, try again." msgstr "Объекты пропали, повторите еще раз." #: admin.py:217 msgid "No permission" msgstr "Нет доступа" #: admin.py:226 #, python-format #| msgid "Database error" msgid "Database error: %s" msgstr "Ошибка базы данных: %s" #: admin.py:231 #, python-format msgid "%s has been successfully moved." msgstr "%s был успешно перемещен." #: admin.py:242 msgid "move node before node" msgstr "переместить узел до узла" #: admin.py:243 msgid "move node to child position" msgstr "переместить узел в подчиненную позицию" #: admin.py:244 msgid "move node after node" msgstr "переместить узел после узла" #: admin.py:245 msgid "Collapse tree" msgstr "Свернуть дерево" #: admin.py:246 msgid "Expand tree" msgstr "Развернуть дерево" #: forms.py:63 msgid "First child" msgstr "Первый потомок" #: forms.py:64 msgid "Last child" msgstr "Последний потомок" #: forms.py:65 msgid "Left sibling" msgstr "Левый брат" #: forms.py:66 msgid "Right sibling" msgstr "Правый брат" #: forms.py:184 msgid "Invalid parent" msgstr "Неверный родительский узел" #: managers.py:514 msgid "Cannot insert a node which has already been saved." msgstr "Невозможно добавить элемент, который уже был сохранён." #: managers.py:743 managers.py:916 managers.py:952 managers.py:1116 #, python-format msgid "An invalid position was given: %s." msgstr "Была дана неверная позиция: %s." #: managers.py:902 managers.py:1096 msgid "A node may not be made a sibling of itself." msgstr "Элемент не может быть дочерним самому себе." #: managers.py:1075 managers.py:1200 msgid "A node may not be made a child of itself." msgstr "Элемент не может быть потомком самому себе." #: managers.py:1077 managers.py:1202 msgid "A node may not be made a child of any of its descendants." msgstr "Элемент не может быть потомком своего наследника." #: managers.py:1098 msgid "A node may not be made a sibling of any of its descendants." msgstr "Элемент не может быть дочерним своему наследнику." #: models.py:291 msgid "register() expects a Django model class argument" msgstr "register() ожидает модель Django в качестве аргумента" #: templatetags/mptt_tags.py:31 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "для full_tree_for_model была дана неверная модель: %s" #: templatetags/mptt_tags.py:55 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "для drilldown_tree_for_node была дана неверная модель: %s" #: templatetags/mptt_tags.py:62 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "для drilldown_tree_for_node было дано неверное поле модели: %s" #: templatetags/mptt_tags.py:89 #, python-format msgid "%s tag requires three arguments" msgstr "%s требует три аргумента" #: templatetags/mptt_tags.py:91 templatetags/mptt_tags.py:146 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "вторым аргуметом для %s должен быть 'as'" #: templatetags/mptt_tags.py:143 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s требует три, семь или восемь аргументов" #: templatetags/mptt_tags.py:150 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "если дано семь аргументов, то четвёртый аргумент для %s должен быть 'with'" #: templatetags/mptt_tags.py:154 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "если дано семь аргументов, то шестой для %s должен быть 'in'" #: templatetags/mptt_tags.py:160 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "если дано восемь аргументов, то четвёртый аргумент для %s должен быть " "'cumulative'" #: templatetags/mptt_tags.py:164 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "если дано восемь аргументов, то пятый аргумент для %s должен быть 'count'" #: templatetags/mptt_tags.py:168 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "если дано восемь аргументов, то седьмой аргумент для %s должен быть 'in'" #: templatetags/mptt_tags.py:287 #, python-format msgid "%s tag requires a queryset" msgstr "для %s требуется queryset" #: utils.py:240 #, python-format msgid "Node %s not in depth-first order" msgstr "Узел %s имеет уровень меньше первого уровня" #~ msgid "%(count)s %(name)s was changed successfully." #~ msgid_plural "%(count)s %(name)s were changed successfully." #~ msgstr[0] "%(count)s %(name)s успешно изменен." #~ msgstr[1] "%(count)s %(name)s успешно изменены." #~ msgstr[2] "%(count)s %(name)s успешно изменено." #~ msgid "Add child" #~ msgstr "Добавить потомка" #~ msgid "View on site" #~ msgstr "Перейти на сайт" django-mptt-0.13.2/mptt/locale/uk/000077500000000000000000000000001411214034000166515ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/uk/LC_MESSAGES/000077500000000000000000000000001411214034000204365ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/uk/LC_MESSAGES/django.mo000066400000000000000000000126571411214034000222500ustar00rootroot00000000000000)d;6!9A){;+ "24 gu'&      $$E j%x@:6FQLEF+Cr0&. U [ E +] 4 N 8 gF X c Tk  3 R N&l7C!1%MsD<==S^UQF%G/[5@KMN# " % ) &'! $( By %(filter_title)s %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: django-mptt Report-Msgid-Bugs-To: https://github.com/django-mptt/django-mptt/issues POT-Creation-Date: 2017-03-12 12:45+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Illia Volochii Language-Team: Ukrainian Language: uk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); За %(filter_title)s %s - успішно переміщений.тег %s вимагає набору запитівтег %s вимагає три, сім або вісім аргументівтег %s вимагає трьох аргументівВузол не можна зробити нащадком будь-якого його нащадка.Вузол не можна зробити нащадком для самого себе.Вузол не можна зробити братом будь-якого його нащадка.Вузол не можна зробити братом для самого себе.ВсіДано неправильну позицію: %s.Не можна вставити вузол, який вже збережений.Згорнути деревоПомилка бази даних: %sВидалити вибрані %(verbose_name_plural)sНезрозуміла інструкція переміщення.Розгорнути деревоПерший нащадокНеправильний предокОстанній нащадокЛівий братНемає правВузол %s не є у порядку типу "в глибину"Обʼєкти зникли. Спробуйте ще раз.Правий братУспішно видалено %(count)d елементів.тег drilldown_tree_for_node отримав неправильне поле моделі: %sтег drilldown_tree_for_node отримав неправильну модель: %sтег full_tree_for_model отримав неправильну модель: %sякщо дано вісім аргументів, тоді пʼятий аргумент для тегу %s повинен бути "count"якщо дано вісім аргументів, тоді четвертий аргумент для тегу %s повинен бути "cumulative"якщо дано вісім аргументів, тоді сьомий аргумент для тегу %s повинен бути "in"якщо дано сім аргументів, тоді четвертий аргумент для тегу %s повинен бути "with"якщо дано сім аргументів, тоді шостий аргумент для тегу %s повинен бути "in"помістити вузол за вузломпомістити вузол перед вузломпомістити вузол на позицію нащадкаregister() очікує клас Django-моделі, як аргументдругий аргумент для тегу %s повинен бути "as"заголовокdjango-mptt-0.13.2/mptt/locale/uk/LC_MESSAGES/django.po000066400000000000000000000154671411214034000222550ustar00rootroot00000000000000# Translation of django-mptt into Ukrainian. # This file is distributed under the same license as the django-mptt package. # Illia Volochii , 2017. msgid "" msgstr "" "Project-Id-Version: django-mptt\n" "Report-Msgid-Bugs-To: https://github.com/django-mptt/django-mptt/issues\n" "POT-Creation-Date: 2017-03-12 12:45+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Illia Volochii \n" "Language-Team: Ukrainian\n" "Language: uk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" #: admin.py:89 #, python-format msgid "Successfully deleted %(count)d items." msgstr "Успішно видалено %(count)d елементів." #: admin.py:102 #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "Видалити вибрані %(verbose_name_plural)s" #: admin.py:184 msgid "title" msgstr "заголовок" #: admin.py:216 msgid "Did not understand moving instruction." msgstr "Незрозуміла інструкція переміщення." #: admin.py:224 msgid "Objects have disappeared, try again." msgstr "Обʼєкти зникли. Спробуйте ще раз." #: admin.py:228 msgid "No permission" msgstr "Немає прав" #: admin.py:237 #, python-format msgid "Database error: %s" msgstr "Помилка бази даних: %s" #: admin.py:242 #, python-format msgid "%s has been successfully moved." msgstr "%s - успішно переміщений." #: admin.py:253 msgid "move node before node" msgstr "помістити вузол перед вузлом" #: admin.py:254 msgid "move node to child position" msgstr "помістити вузол на позицію нащадка" #: admin.py:255 msgid "move node after node" msgstr "помістити вузол за вузлом" #: admin.py:256 msgid "Collapse tree" msgstr "Згорнути дерево" #: admin.py:257 msgid "Expand tree" msgstr "Розгорнути дерево" #: admin.py:368 msgid "All" msgstr "Всі" #: forms.py:63 msgid "First child" msgstr "Перший нащадок" #: forms.py:64 msgid "Last child" msgstr "Останній нащадок" #: forms.py:65 msgid "Left sibling" msgstr "Лівий брат" #: forms.py:66 msgid "Right sibling" msgstr "Правий брат" #: forms.py:184 msgid "Invalid parent" msgstr "Неправильний предок" #: managers.py:522 msgid "Cannot insert a node which has already been saved." msgstr "Не можна вставити вузол, який вже збережений." #: managers.py:741 managers.py:904 managers.py:940 managers.py:1106 #, python-format msgid "An invalid position was given: %s." msgstr "Дано неправильну позицію: %s." #: managers.py:890 managers.py:1086 msgid "A node may not be made a sibling of itself." msgstr "Вузол не можна зробити братом для самого себе." #: managers.py:1065 managers.py:1183 msgid "A node may not be made a child of itself." msgstr "Вузол не можна зробити нащадком для самого себе." #: managers.py:1067 managers.py:1185 msgid "A node may not be made a child of any of its descendants." msgstr "Вузол не можна зробити нащадком будь-якого його нащадка." #: managers.py:1088 msgid "A node may not be made a sibling of any of its descendants." msgstr "Вузол не можна зробити братом будь-якого його нащадка." #: models.py:301 msgid "register() expects a Django model class argument" msgstr "register() очікує клас Django-моделі, як аргумент" #: utils.py:247 #, python-format msgid "Node %s not in depth-first order" msgstr "Вузол %s не є у порядку типу \"в глибину\"" #: templates/admin/mptt_filter.html:3 #, python-format msgid " By %(filter_title)s " msgstr " За %(filter_title)s " #: templatetags/mptt_tags.py:31 #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "тег full_tree_for_model отримав неправильну модель: %s" #: templatetags/mptt_tags.py:55 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "тег drilldown_tree_for_node отримав неправильну модель: %s" #: templatetags/mptt_tags.py:62 #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "тег drilldown_tree_for_node отримав неправильне поле моделі: %s" #: templatetags/mptt_tags.py:89 #, python-format msgid "%s tag requires three arguments" msgstr "тег %s вимагає трьох аргументів" #: templatetags/mptt_tags.py:91 templatetags/mptt_tags.py:146 #, python-format msgid "second argument to %s tag must be 'as'" msgstr "другий аргумент для тегу %s повинен бути \"as\"" #: templatetags/mptt_tags.py:143 #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "тег %s вимагає три, сім або вісім аргументів" #: templatetags/mptt_tags.py:150 #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "" "якщо дано сім аргументів, тоді четвертий аргумент для тегу %s повинен бути " "\"with\"" #: templatetags/mptt_tags.py:154 #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "" "якщо дано сім аргументів, тоді шостий аргумент для тегу %s повинен бути " "\"in\"" #: templatetags/mptt_tags.py:160 #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "" "якщо дано вісім аргументів, тоді четвертий аргумент для тегу %s повинен бути " "\"cumulative\"" #: templatetags/mptt_tags.py:164 #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "" "якщо дано вісім аргументів, тоді пʼятий аргумент для тегу %s повинен бути " "\"count\"" #: templatetags/mptt_tags.py:168 #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "" "якщо дано вісім аргументів, тоді сьомий аргумент для тегу %s повинен бути " "\"in\"" #: templatetags/mptt_tags.py:287 #, python-format msgid "%s tag requires a queryset" msgstr "тег %s вимагає набору запитів" django-mptt-0.13.2/mptt/locale/zh_Hans/000077500000000000000000000000001411214034000176245ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/zh_Hans/LC_MESSAGES/000077500000000000000000000000001411214034000214115ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/zh_Hans/LC_MESSAGES/django.mo000066400000000000000000000076271411214034000232240ustar00rootroot00000000000000)d;6!9A){;+ "24 gu'&      $$E j%x@:6FQLEF+Cr0&. U E[    !  3# 0W 6 0    * 1 &E l      % !  . =I F B 38E0~20  '27%j# " % ) &'! $( By %(filter_title)s %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: 0.8.6 Report-Msgid-Bugs-To: POT-Creation-Date: 2016-11-08 18:24+0800 PO-Revision-Date: 2019-05-17 08:58+0800 Last-Translator: Language-Team: zh-Hant Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=1; plural=0; %(filter_title)s 已成功移动 %s%s 标签需要一个queryset%s 标签需要3或7或8個参数%s 标签需要三個参数节点无法指定自己成为子节点的子节点节点无法指定自己成为自己的子节点节点无法指定自己成为子节点的同层节点节点无法指定自己成为自己同层节点全部无效的位置: %s无法加入已存在的节点收起数据库错误: %s刪除已选: %(verbose_name_plural)s 无法执行移动展开第一子节点无效的父节点最后子节点同层左节点没有权限节点 %s 不在深度优先次序中对象已消失,请再试一次同层右节点成功刪除 %(count)d 项drilldoen_tree_for_node 标签被指定到无效的字段: %sdrilldoen_tree_for_node 标签被指定到无效的数据库模型: %sfull_tree_for_model 标签被指定到无效的数据库模型: %s%s 标签传入8个参数时,第5个需为'count'%s 标签传入8个参数时,第4个需为'cumulative'%s 标签传入8个参数时,第7个需为'in'%s 标签传入7個参数时,第4个需为'with'%s 标签传入7个参数时,第6个需为'in'往后移往前移移至下一层register() 需要一个Django模型类別的参数%s 标签的第二个参数需为'as'标题django-mptt-0.13.2/mptt/locale/zh_Hans/LC_MESSAGES/django.po000066400000000000000000000104021411214034000232100ustar00rootroot00000000000000# Traditional Chinese translation # Copyright (C) 2016 # This file is distributed under the same license as the mptt package. # Translators: # myyang 2016. # msgid "" msgstr "" "Project-Id-Version: 0.8.6\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-11-08 18:24+0800\n" "PO-Revision-Date: 2019-05-17 08:58+0800\n" "Last-Translator: \n" "Language-Team: zh-Hant \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #, python-format msgid "Successfully deleted %(count)d items." msgstr "成功刪除 %(count)d 项" #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "刪除已选: %(verbose_name_plural)s " msgid "title" msgstr "标题" msgid "Did not understand moving instruction." msgstr "无法执行移动" msgid "Objects have disappeared, try again." msgstr "对象已消失,请再试一次" msgid "No permission" msgstr "没有权限" #, python-format msgid "Database error: %s" msgstr "数据库错误: %s" #, python-format msgid "%s has been successfully moved." msgstr "已成功移动 %s" msgid "move node before node" msgstr "往前移" msgid "move node to child position" msgstr "移至下一层" msgid "move node after node" msgstr "往后移" msgid "Collapse tree" msgstr "收起" msgid "Expand tree" msgstr "展开" msgid "All" msgstr "全部" msgid "First child" msgstr "第一子节点" msgid "Last child" msgstr "最后子节点" msgid "Left sibling" msgstr "同层左节点" msgid "Right sibling" msgstr "同层右节点" msgid "Invalid parent" msgstr "无效的父节点" msgid "Cannot insert a node which has already been saved." msgstr "无法加入已存在的节点" #, python-format msgid "An invalid position was given: %s." msgstr "无效的位置: %s" msgid "A node may not be made a sibling of itself." msgstr "节点无法指定自己成为自己同层节点" msgid "A node may not be made a child of itself." msgstr "节点无法指定自己成为自己的子节点" msgid "A node may not be made a child of any of its descendants." msgstr "节点无法指定自己成为子节点的子节点" msgid "A node may not be made a sibling of any of its descendants." msgstr "节点无法指定自己成为子节点的同层节点" msgid "register() expects a Django model class argument" msgstr "register() 需要一个Django模型类別的参数" #, python-format msgid " By %(filter_title)s " msgstr " %(filter_title)s " #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model 标签被指定到无效的数据库模型: %s" #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldoen_tree_for_node 标签被指定到无效的数据库模型: %s" #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldoen_tree_for_node 标签被指定到无效的字段: %s" #, python-format msgid "%s tag requires three arguments" msgstr "%s 标签需要三個参数" #, python-format msgid "second argument to %s tag must be 'as'" msgstr "%s 标签的第二个参数需为'as'" #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s 标签需要3或7或8個参数" #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "%s 标签传入7個参数时,第4个需为'with'" #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "%s 标签传入7个参数时,第6个需为'in'" #, python-format msgid "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "%s 标签传入8个参数时,第4个需为'cumulative'" #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "%s 标签传入8个参数时,第5个需为'count'" #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "%s 标签传入8个参数时,第7个需为'in'" #, python-format msgid "%s tag requires a queryset" msgstr "%s 标签需要一个queryset" #, python-format msgid "Node %s not in depth-first order" msgstr "节点 %s 不在深度优先次序中" django-mptt-0.13.2/mptt/locale/zh_Hant/000077500000000000000000000000001411214034000176255ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/zh_Hant/LC_MESSAGES/000077500000000000000000000000001411214034000214125ustar00rootroot00000000000000django-mptt-0.13.2/mptt/locale/zh_Hant/LC_MESSAGES/django.mo000066400000000000000000000076761411214034000232310ustar00rootroot00000000000000)d;6!9A){;+ "24 gu'&      $$E j%x@:6FQLEF+Cr0&. U _[    !  38 0l 6 0    ? O &c       % !# E U =p F B 388l020  : DN2^%# " % ) &'! $( By %(filter_title)s %s has been successfully moved.%s tag requires a queryset%s tag requires either three, seven or eight arguments%s tag requires three argumentsA node may not be made a child of any of its descendants.A node may not be made a child of itself.A node may not be made a sibling of any of its descendants.A node may not be made a sibling of itself.AllAn invalid position was given: %s.Cannot insert a node which has already been saved.Collapse treeDatabase error: %sDelete selected %(verbose_name_plural)sDid not understand moving instruction.Expand treeFirst childInvalid parentLast childLeft siblingNo permissionNode %s not in depth-first orderObjects have disappeared, try again.Right siblingSuccessfully deleted %(count)d items.drilldown_tree_for_node tag was given an invalid model field: %sdrilldown_tree_for_node tag was given an invalid model: %sfull_tree_for_model tag was given an invalid model: %sif eight arguments are given, fifth argument to %s tag must be 'count'if eight arguments are given, fourth argument to %s tag must be 'cumulative'if eight arguments are given, seventh argument to %s tag must be 'in'if seven arguments are given, fourth argument to %s tag must be 'with'if seven arguments are given, sixth argument to %s tag must be 'in'move node after nodemove node before nodemove node to child positionregister() expects a Django model class argumentsecond argument to %s tag must be 'as'titleProject-Id-Version: 0.8.6 Report-Msgid-Bugs-To: POT-Creation-Date: 2016-11-08 18:24+0800 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: myyang Language-Team: zh-Hant Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=1; plural=0; %(filter_title)s 已成功移動 %s%s 標籤需要查詢集%s 標籤需要3或7或8個參數%s 標籤需要三個參數節點無法指定自己成為子節點的子節點節點無法指定自己成為自己的子節點節點無法指定自己成為子節點的同層節點節點無法指定自己成為自己同層節點全部無效的位置: %s無法加入已存在的節點收起關聯樹資料庫錯誤: %s刪除已選: %(verbose_name_plural)s 無法執行移動展開關聯樹第一子節點無效的父節點最後子節點同層左節點沒有權限節點 %s 不在深度優先次序中物件已消失,請再試一次同層右節點成功刪除 %(count)d 項drilldoen_tree_for_node 標籤被指定到無效的欄位: %sdrilldoen_tree_for_node 標籤被指定到無效的資料庫模型: %sfull_tree_for_model 標籤被指定到無效的資料庫模型: %s%s 標籤傳入8個參數時,第5個需為'count'%s 標籤傳入8個參數時,第4個需為'cumulative'%s 標籤傳入8個參數時,第7個需為'in'%s 標籤傳入7個參數時,第4個需為'with'%s 標籤傳入7個參數時,第6個需為'in'往後移往前移移至下一層register() 需要一個Django模型類別的參數%s 標籤的第二個參數需為'as'標題django-mptt-0.13.2/mptt/locale/zh_Hant/LC_MESSAGES/django.po000066400000000000000000000104641411214034000232210ustar00rootroot00000000000000# Traditional Chinese translation # Copyright (C) 2016 # This file is distributed under the same license as the mptt package. # Translators: # myyang 2016. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.8.6\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-11-08 18:24+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: myyang \n" "Language-Team: zh-Hant \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #, python-format msgid "Successfully deleted %(count)d items." msgstr "成功刪除 %(count)d 項" #, python-format msgid "Delete selected %(verbose_name_plural)s" msgstr "刪除已選: %(verbose_name_plural)s " msgid "title" msgstr "標題" msgid "Did not understand moving instruction." msgstr "無法執行移動" msgid "Objects have disappeared, try again." msgstr "物件已消失,請再試一次" msgid "No permission" msgstr "沒有權限" #, python-format msgid "Database error: %s" msgstr "資料庫錯誤: %s" #, python-format msgid "%s has been successfully moved." msgstr "已成功移動 %s" msgid "move node before node" msgstr "往前移" msgid "move node to child position" msgstr "移至下一層" msgid "move node after node" msgstr "往後移" msgid "Collapse tree" msgstr "收起關聯樹" msgid "Expand tree" msgstr "展開關聯樹" msgid "All" msgstr "全部" msgid "First child" msgstr "第一子節點" msgid "Last child" msgstr "最後子節點" msgid "Left sibling" msgstr "同層左節點" msgid "Right sibling" msgstr "同層右節點" msgid "Invalid parent" msgstr "無效的父節點" msgid "Cannot insert a node which has already been saved." msgstr "無法加入已存在的節點" #, python-format msgid "An invalid position was given: %s." msgstr "無效的位置: %s" msgid "A node may not be made a sibling of itself." msgstr "節點無法指定自己成為自己同層節點" msgid "A node may not be made a child of itself." msgstr "節點無法指定自己成為自己的子節點" msgid "A node may not be made a child of any of its descendants." msgstr "節點無法指定自己成為子節點的子節點" msgid "A node may not be made a sibling of any of its descendants." msgstr "節點無法指定自己成為子節點的同層節點" msgid "register() expects a Django model class argument" msgstr "register() 需要一個Django模型類別的參數" #, python-format msgid " By %(filter_title)s " msgstr " %(filter_title)s " #, python-format msgid "full_tree_for_model tag was given an invalid model: %s" msgstr "full_tree_for_model 標籤被指定到無效的資料庫模型: %s" #, python-format msgid "drilldown_tree_for_node tag was given an invalid model: %s" msgstr "drilldoen_tree_for_node 標籤被指定到無效的資料庫模型: %s" #, python-format msgid "drilldown_tree_for_node tag was given an invalid model field: %s" msgstr "drilldoen_tree_for_node 標籤被指定到無效的欄位: %s" #, python-format msgid "%s tag requires three arguments" msgstr "%s 標籤需要三個參數" #, python-format msgid "second argument to %s tag must be 'as'" msgstr "%s 標籤的第二個參數需為'as'" #, python-format msgid "%s tag requires either three, seven or eight arguments" msgstr "%s 標籤需要3或7或8個參數" #, python-format msgid "if seven arguments are given, fourth argument to %s tag must be 'with'" msgstr "%s 標籤傳入7個參數時,第4個需為'with'" #, python-format msgid "if seven arguments are given, sixth argument to %s tag must be 'in'" msgstr "%s 標籤傳入7個參數時,第6個需為'in'" #, python-format msgid "" "if eight arguments are given, fourth argument to %s tag must be 'cumulative'" msgstr "%s 標籤傳入8個參數時,第4個需為'cumulative'" #, python-format msgid "if eight arguments are given, fifth argument to %s tag must be 'count'" msgstr "%s 標籤傳入8個參數時,第5個需為'count'" #, python-format msgid "if eight arguments are given, seventh argument to %s tag must be 'in'" msgstr "%s 標籤傳入8個參數時,第7個需為'in'" #, python-format msgid "%s tag requires a queryset" msgstr "%s 標籤需要查詢集" #, python-format msgid "Node %s not in depth-first order" msgstr "節點 %s 不在深度優先次序中" django-mptt-0.13.2/mptt/managers.py000066400000000000000000001376661411214034000171650ustar00rootroot00000000000000""" A custom manager for working with trees of objects. """ import contextlib import functools from itertools import groupby from django.db import connections, models, router from django.db.models import ( F, IntegerField, ManyToManyField, Max, OuterRef, Q, Subquery, ) from django.utils.translation import gettext as _ from mptt.compat import cached_field_value from mptt.exceptions import CantDisableUpdates, InvalidMove from mptt.querysets import TreeQuerySet from mptt.signals import node_moved from mptt.utils import _get_tree_model __all__ = ("TreeManager",) class SQCount(Subquery): template = "(SELECT count(*) FROM (%(subquery)s) _count)" output_field = IntegerField() def delegate_manager(method): """ Delegate method calls to base manager, if exists. """ @functools.wraps(method) def wrapped(self, *args, **kwargs): if self._base_manager: return getattr(self._base_manager, method.__name__)(*args, **kwargs) return method(self, *args, **kwargs) return wrapped class TreeManager(models.Manager.from_queryset(TreeQuerySet)): """ A manager for working with trees of objects. """ def contribute_to_class(self, model, name): super().contribute_to_class(model, name) if not model._meta.abstract: self.tree_model = _get_tree_model(model) self._base_manager = None if self.tree_model and self.tree_model is not model: # _base_manager is the treemanager on tree_model self._base_manager = self.tree_model._tree_manager def get_queryset(self, *args, **kwargs): """ Ensures that this manager always returns nodes in tree order. """ return ( super() .get_queryset(*args, **kwargs) .order_by(self.tree_id_attr, self.left_attr) ) def _get_queryset_relatives(self, queryset, direction, include_self): """ Returns a queryset containing either the descendants ``direction == desc`` or the ancestors ``direction == asc`` of a given queryset. This function is not meant to be called directly, although there is no harm in doing so. Instead, it should be used via ``get_queryset_descendants()`` and/or ``get_queryset_ancestors()``. This function works by grouping contiguous siblings and using them to create a range that selects all nodes between the range, instead of querying for each node individually. Three variables are required when querying for ancestors or descendants: tree_id_attr, left_attr, right_attr. If we weren't using ranges and our queryset contained 100 results, the resulting SQL query would contain 300 variables. However, when using ranges, if the same queryset contained 10 sets of contiguous siblings, then the resulting SQL query should only contain 30 variables. The attributes used to create the range are completely dependent upon whether you are ascending or descending the tree. * Ascending (ancestor nodes): select all nodes whose right_attr is greater than (or equal to, if include_self = True) the smallest right_attr within the set of contiguous siblings, and whose left_attr is less than (or equal to) the largest left_attr within the set of contiguous siblings. * Descending (descendant nodes): select all nodes whose left_attr is greater than (or equal to, if include_self = True) the smallest left_attr within the set of contiguous siblings, and whose right_attr is less than (or equal to) the largest right_attr within the set of contiguous siblings. The result is the more contiguous siblings in the original queryset, the fewer SQL variables will be required to execute the query. """ assert self.model is queryset.model opts = queryset.model._mptt_meta filters = Q() e = "e" if include_self else "" max_op = "lt" + e min_op = "gt" + e if direction == "asc": max_attr = opts.left_attr min_attr = opts.right_attr elif direction == "desc": max_attr = opts.right_attr min_attr = opts.left_attr tree_key = opts.tree_id_attr min_key = "%s__%s" % (min_attr, min_op) max_key = "%s__%s" % (max_attr, max_op) q = queryset.order_by(opts.tree_id_attr, opts.parent_attr, opts.left_attr).only( opts.tree_id_attr, opts.left_attr, opts.right_attr, min_attr, max_attr, opts.parent_attr, # These fields are used by MPTTModel.update_mptt_cached_fields() *[f.lstrip("-") for f in opts.order_insertion_by] ) if not q: return self.none() for group in groupby( q, key=lambda n: ( getattr(n, opts.tree_id_attr), getattr(n, opts.parent_attr + "_id"), ), ): next_lft = None for node in list(group[1]): tree, lft, rght, min_val, max_val = ( getattr(node, opts.tree_id_attr), getattr(node, opts.left_attr), getattr(node, opts.right_attr), getattr(node, min_attr), getattr(node, max_attr), ) if next_lft is None: next_lft = rght + 1 min_max = {"min": min_val, "max": max_val} elif lft == next_lft: if min_val < min_max["min"]: min_max["min"] = min_val if max_val > min_max["max"]: min_max["max"] = max_val next_lft = rght + 1 elif lft != next_lft: filters |= Q( **{ tree_key: tree, min_key: min_max["min"], max_key: min_max["max"], } ) min_max = {"min": min_val, "max": max_val} next_lft = rght + 1 filters |= Q( **{ tree_key: tree, min_key: min_max["min"], max_key: min_max["max"], } ) return self.filter(filters) def get_queryset_descendants(self, queryset, include_self=False): """ Returns a queryset containing the descendants of all nodes in the given queryset. If ``include_self=True``, nodes in ``queryset`` will also be included in the result. """ return self._get_queryset_relatives(queryset, "desc", include_self) def get_queryset_ancestors(self, queryset, include_self=False): """ Returns a queryset containing the ancestors of all nodes in the given queryset. If ``include_self=True``, nodes in ``queryset`` will also be included in the result. """ return self._get_queryset_relatives(queryset, "asc", include_self) @contextlib.contextmanager def disable_mptt_updates(self): """ Context manager. Disables mptt updates. NOTE that this context manager causes inconsistencies! MPTT model methods are not guaranteed to return the correct results. When to use this method: If used correctly, this method can be used to speed up bulk updates. This doesn't do anything clever. It *will* mess up your tree. You should follow this method with a call to ``TreeManager.rebuild()`` to ensure your tree stays sane, and you should wrap both calls in a transaction. This is best for updates that span a large part of the table. If you are doing localised changes (one tree, or a few trees) consider using ``delay_mptt_updates``. If you are making only minor changes to your tree, just let the updates happen. Transactions: This doesn't enforce any transactional behavior. You should wrap this in a transaction to ensure database consistency. If updates are already disabled on the model, this is a noop. Usage:: with transaction.atomic(): with MyNode.objects.disable_mptt_updates(): ## bulk updates. MyNode.objects.rebuild() """ # Error cases: if self.model._meta.abstract: # an abstract model. Design decision needed - do we disable # updates for all concrete models that derive from this model? I # vote no - that's a bit implicit and it's a weird use-case # anyway. Open to further discussion :) raise CantDisableUpdates( "You can't disable/delay mptt updates on %s," " it's an abstract model" % self.model.__name__ ) elif self.model._meta.proxy: # a proxy model. disabling updates would implicitly affect other # models using the db table. Caller should call this on the # manager for the concrete model instead, to make the behavior # explicit. raise CantDisableUpdates( "You can't disable/delay mptt updates on %s, it's a proxy" " model. Call the concrete model instead." % self.model.__name__ ) elif self.tree_model is not self.model: # a multiple-inheritance child of an MPTTModel. Disabling # updates may affect instances of other models in the tree. raise CantDisableUpdates( "You can't disable/delay mptt updates on %s, it doesn't" " contain the mptt fields." % self.model.__name__ ) if not self.model._mptt_updates_enabled: # already disabled, noop. yield else: self.model._set_mptt_updates_enabled(False) try: yield finally: self.model._set_mptt_updates_enabled(True) @contextlib.contextmanager def delay_mptt_updates(self): """ Context manager. Delays mptt updates until the end of a block of bulk processing. NOTE that this context manager causes inconsistencies! MPTT model methods are not guaranteed to return the correct results until the end of the context block. When to use this method: If used correctly, this method can be used to speed up bulk updates. This is best for updates in a localised area of the db table, especially if all the updates happen in a single tree and the rest of the forest is left untouched. No subsequent rebuild is necessary. ``delay_mptt_updates`` does a partial rebuild of the modified trees (not the whole table). If used indiscriminately, this can actually be much slower than just letting the updates occur when they're required. The worst case occurs when every tree in the table is modified just once. That results in a full rebuild of the table, which can be *very* slow. If your updates will modify most of the trees in the table (not a small number of trees), you should consider using ``TreeManager.disable_mptt_updates``, as it does much fewer queries. Transactions: This doesn't enforce any transactional behavior. You should wrap this in a transaction to ensure database consistency. Exceptions: If an exception occurs before the processing of the block, delayed updates will not be applied. Usage:: with transaction.atomic(): with MyNode.objects.delay_mptt_updates(): ## bulk updates. """ with self.disable_mptt_updates(): if self.model._mptt_is_tracking: # already tracking, noop. yield else: self.model._mptt_start_tracking() try: yield except Exception: # stop tracking, but discard results self.model._mptt_stop_tracking() raise results = self.model._mptt_stop_tracking() partial_rebuild = self.partial_rebuild for tree_id in results: partial_rebuild(tree_id) @property def parent_attr(self): return self.model._mptt_meta.parent_attr @property def left_attr(self): return self.model._mptt_meta.left_attr @property def right_attr(self): return self.model._mptt_meta.right_attr @property def tree_id_attr(self): return self.model._mptt_meta.tree_id_attr @property def level_attr(self): return self.model._mptt_meta.level_attr def _translate_lookups(self, **lookups): new_lookups = {} join_parts = "__".join for k, v in lookups.items(): parts = k.split("__") new_parts = [] new_parts__append = new_parts.append for part in parts: new_parts__append(getattr(self, part + "_attr", part)) new_lookups[join_parts(new_parts)] = v return new_lookups @delegate_manager def _mptt_filter(self, qs=None, **filters): """ Like ``self.filter()``, but translates name-agnostic filters for MPTT fields. """ if qs is None: qs = self return qs.filter(**self._translate_lookups(**filters)) @delegate_manager def _mptt_update(self, qs=None, **items): """ Like ``self.update()``, but translates name-agnostic MPTT fields. """ if qs is None: qs = self return qs.update(**self._translate_lookups(**items)) def _get_connection(self, **hints): return connections[router.db_for_write(self.model, **hints)] def add_related_count( self, queryset, rel_model, rel_field, count_attr, cumulative=False, extra_filters={}, ): """ 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. ``extra_filters`` Dict with aditional parameters filtering the related queryset. """ if cumulative: subquery_filters = { rel_field + "__tree_id": OuterRef(self.tree_id_attr), rel_field + "__lft__gte": OuterRef(self.left_attr), rel_field + "__lft__lte": OuterRef(self.right_attr), } else: current_rel_model = rel_model for rel_field_part in rel_field.split("__"): current_mptt_field = current_rel_model._meta.get_field(rel_field_part) current_rel_model = current_mptt_field.related_model mptt_field = current_mptt_field if isinstance(mptt_field, ManyToManyField): field_name = "pk" else: field_name = mptt_field.remote_field.field_name subquery_filters = { rel_field: OuterRef(field_name), } subquery = rel_model.objects.filter(**subquery_filters, **extra_filters).values( "pk" ) return queryset.annotate(**{count_attr: SQCount(subquery)}) @delegate_manager def insert_node( self, node, target, position="last-child", save=False, allow_existing_pk=False, refresh_target=True, ): """ Sets up the tree state for ``node`` (which has not yet been inserted into in the database) so it will be positioned relative to a given ``target`` node as specified by ``position`` (when appropriate) it is inserted, with any necessary space already having been made for it. A ``target`` of ``None`` indicates that ``node`` should be the last root node. If ``save`` is ``True``, ``node``'s ``save()`` method will be called before it is returned. NOTE: This is a low-level method; it does NOT respect ``MPTTMeta.order_insertion_by``. In most cases you should just set the node's parent and let mptt call this during save. """ if node.pk and not allow_existing_pk and self.filter(pk=node.pk).exists(): raise ValueError(_("Cannot insert a node which has already been saved.")) if target is None: tree_id = self._get_next_tree_id() setattr(node, self.left_attr, 1) setattr(node, self.right_attr, 2) setattr(node, self.level_attr, 0) setattr(node, self.tree_id_attr, tree_id) setattr(node, self.parent_attr, None) elif target.is_root_node() and position in ["left", "right"]: if refresh_target: # Ensure mptt values on target are not stale. target._mptt_refresh() target_tree_id = getattr(target, self.tree_id_attr) if position == "left": tree_id = target_tree_id space_target = target_tree_id - 1 else: tree_id = target_tree_id + 1 space_target = target_tree_id self._create_tree_space(space_target) setattr(node, self.left_attr, 1) setattr(node, self.right_attr, 2) setattr(node, self.level_attr, 0) setattr(node, self.tree_id_attr, tree_id) setattr(node, self.parent_attr, None) else: setattr(node, self.left_attr, 0) setattr(node, self.level_attr, 0) if refresh_target: # Ensure mptt values on target are not stale. target._mptt_refresh() ( space_target, level, left, parent, right_shift, ) = self._calculate_inter_tree_move_values(node, target, position) tree_id = getattr(target, self.tree_id_attr) self._create_space(2, space_target, tree_id) setattr(node, self.left_attr, -left) setattr(node, self.right_attr, -left + 1) setattr(node, self.level_attr, -level) setattr(node, self.tree_id_attr, tree_id) setattr(node, self.parent_attr, parent) if parent: self._post_insert_update_cached_parent_right(parent, right_shift) if save: node.save() return node @delegate_manager def _move_node( self, node, target, position="last-child", save=True, refresh_target=True ): if self.tree_model._mptt_is_tracking: # delegate to insert_node and clean up the gaps later. return self.insert_node( node, target, position=position, save=save, allow_existing_pk=True, refresh_target=refresh_target, ) else: if target is None: if node.is_child_node(): self._make_child_root_node(node) elif target.is_root_node() and position in ("left", "right"): self._make_sibling_of_root_node(node, target, position) else: if node.is_root_node(): self._move_root_node(node, target, position) else: self._move_child_node(node, target, position) def move_node(self, node, target, position="last-child"): """ Moves ``node`` relative to a given ``target`` node as specified by ``position`` (when appropriate), by examining both nodes and calling the appropriate method to perform the move. A ``target`` of ``None`` indicates that ``node`` should be turned into a root node. Valid values for ``position`` are ``'first-child'``, ``'last-child'``, ``'left'`` or ``'right'``. ``node`` will be modified to reflect its new tree state in the database. This method explicitly checks for ``node`` being made a sibling of a root node, as this is a special case due to our use of tree ids to order root nodes. NOTE: This is a low-level method; it does NOT respect ``MPTTMeta.order_insertion_by``. In most cases you should just move the node yourself by setting node.parent. """ self._move_node(node, target, position=position) node.save() node_moved.send( sender=node.__class__, instance=node, target=target, position=position ) @delegate_manager def root_node(self, tree_id): """ Returns the root node of the tree with the given id. """ return self._mptt_filter(tree_id=tree_id, parent=None).get() @delegate_manager def root_nodes(self): """ Creates a ``QuerySet`` containing root nodes. """ return self._mptt_filter(parent=None) @delegate_manager def rebuild(self): """ Rebuilds all trees in the database table using `parent` link. """ opts = self.model._mptt_meta qs = self._mptt_filter(parent=None) if opts.order_insertion_by: qs = qs.order_by(*opts.order_insertion_by) pks = qs.values_list("pk", flat=True) rebuild_helper = self._rebuild_helper idx = 0 for pk in pks: idx += 1 rebuild_helper(pk, 1, idx) rebuild.alters_data = True @delegate_manager def partial_rebuild(self, tree_id): """ Partially rebuilds a tree i.e. It rebuilds only the tree with given ``tree_id`` in database table using ``parent`` link. """ opts = self.model._mptt_meta qs = self._mptt_filter(parent=None, tree_id=tree_id) if opts.order_insertion_by: qs = qs.order_by(*opts.order_insertion_by) pks = qs.values_list("pk", flat=True) if not pks: return if len(pks) > 1: raise RuntimeError( "More than one root node with tree_id %d. That's invalid," " do a full rebuild." % tree_id ) self._rebuild_helper(pks[0], 1, tree_id) @delegate_manager def build_tree_nodes(self, data, target=None, position="last-child"): """ Load a tree from a nested dictionary for bulk insert, returning an array of records. Use to efficiently insert many nodes within a tree without an expensive `rebuild`. :: records = MyModel.objects.build_tree_nodes({ 'id': 7, 'name': 'parent', 'children': [ { 'id': 8, 'parent_id': 7, 'name': 'child', 'children': [ { 'id': 9, 'parent_id': 8, 'name': 'grandchild', } ] } ] }) MyModel.objects.bulk_create(records) """ opts = self.model._mptt_meta if target: tree_id = target.tree_id if position in ("left", "right"): level = getattr(target, opts.level_attr) if position == "left": cursor = getattr(target, opts.left_attr) else: cursor = getattr(target, opts.right_attr) + 1 else: level = getattr(target, opts.level_attr) + 1 if position == "first-child": cursor = getattr(target, opts.left_attr) + 1 else: cursor = getattr(target, opts.right_attr) else: tree_id = self._get_next_tree_id() cursor = 1 level = 0 stack = [] def treeify(data, cursor=1, level=0): data = dict(data) children = data.pop("children", []) node = self.model(**data) stack.append(node) setattr(node, opts.tree_id_attr, tree_id) setattr(node, opts.level_attr, level) setattr(node, opts.left_attr, cursor) for child in children: cursor = treeify(child, cursor=cursor + 1, level=level + 1) cursor += 1 setattr(node, opts.right_attr, cursor) return cursor treeify(data, cursor=cursor, level=level) if target: self._create_space(2 * len(stack), cursor - 1, tree_id) return stack 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.db_manager(self.db).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 ) parent = cached_field_value(instance, self.parent_attr) if parent: if not seen: seen = set() seen.add(instance) if parent in seen: # detect infinite recursion and throw an error raise InvalidMove self._post_insert_update_cached_parent_right(parent, right_shift, seen=seen) def _calculate_inter_tree_move_values(self, node, target, position): """ Calculates values required when moving ``node`` relative to ``target`` as specified by ``position``. """ left = getattr(node, self.left_attr) level = getattr(node, self.level_attr) target_left = getattr(target, self.left_attr) target_right = getattr(target, self.right_attr) target_level = getattr(target, self.level_attr) if position == "last-child" or position == "first-child": if position == "last-child": space_target = target_right - 1 else: space_target = target_left level_change = level - target_level - 1 parent = target elif position == "left" or position == "right": if position == "left": space_target = target_left - 1 else: space_target = target_right level_change = level - target_level parent = getattr(target, self.parent_attr) else: raise ValueError(_("An invalid position was given: %s.") % position) left_right_change = left - space_target - 1 right_shift = 0 if parent: right_shift = 2 * (node.get_descendant_count() + 1) return space_target, level_change, left_right_change, parent, right_shift def _close_gap(self, size, target, tree_id): """ Closes a gap of a certain ``size`` after the given ``target`` point in the tree identified by ``tree_id``. """ self._manage_space(-size, target, tree_id) def _create_space(self, size, target, tree_id): """ Creates a space of a certain ``size`` after the given ``target`` point in the tree identified by ``tree_id``. """ self._manage_space(size, target, tree_id) def _create_tree_space(self, target_tree_id, num_trees=1): """ Creates space for a new tree by incrementing all tree ids greater than ``target_tree_id``. """ qs = self._mptt_filter(tree_id__gt=target_tree_id) self._mptt_update(qs, tree_id=F(self.tree_id_attr) + num_trees) self.tree_model._mptt_track_tree_insertions(target_tree_id + 1, num_trees) def _get_next_tree_id(self): """ Determines the next largest unused tree id for the tree managed by this manager. """ max_tree_id = list(self.aggregate(Max(self.tree_id_attr)).values())[0] max_tree_id = max_tree_id or 0 return max_tree_id + 1 def _inter_tree_move_and_close_gap( self, node, level_change, left_right_change, new_tree_id ): """ 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. """ 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 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), } 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, getattr(node, self.tree_id_attr), ] 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 ) # 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 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), "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, 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 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), } cursor = connection.cursor() cursor.execute( move_tree_query, [ level_change, left_right_change, left_right_change, new_tree_id, 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.13.2/mptt/models.py000066400000000000000000001265311411214034000166400ustar00rootroot00000000000000import operator import threading from functools import reduce, wraps from django.db import models from django.db.models.base import ModelBase from django.db.models.query import Q from django.db.models.query_utils import DeferredAttribute from django.utils.translation import gettext as _ from mptt.compat import cached_field_value from mptt.fields import TreeForeignKey, TreeManyToManyField, TreeOneToOneField from mptt.managers import TreeManager from mptt.signals import node_moved from mptt.utils import _get_tree_model __all__ = ( "TreeForeignKey", "TreeOneToOneField", "TreeManyToManyField", "TreeManager", "MPTTOptions", "MPTTModelBase", "MPTTModel", ) class _classproperty: 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().__init__( members.get("__get__"), members.get("__set__"), members.get("__delete__"), members.get("__doc__"), ) classproperty = classpropertytype("classproperty") class MPTTOptions: """ 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, str): self.order_insertion_by = [self.order_insertion_by] elif isinstance(self.order_insertion_by, tuple): self.order_insertion_by = list(self.order_insertion_by) elif self.order_insertion_by is None: self.order_insertion_by = [] def __iter__(self): return ((k, v) for k, v in self.__dict__.items() if k[0] != "_") # Helper methods for accessing tree attributes on models. def get_raw_field_value(self, instance, field_name): """ Gets the value of the given fieldname for the instance. This is not the same as getattr(). This function will return IDs for foreignkeys etc, rather than doing a database query. """ field = instance._meta.get_field(field_name) return field.value_from_object(instance) def set_raw_field_value(self, instance, field_name, value): """ Sets the value of the given fieldname for the instance. This is not the same as setattr(). This function requires an ID for a foreignkey (etc) rather than an instance. """ field = instance._meta.get_field(field_name) setattr(instance, field.attname, value) def update_mptt_cached_fields(self, instance): """ Caches (in an instance._mptt_cached_fields dict) the original values of: - parent pk - fields specified in order_insertion_by These are used in save() to determine if the relevant fields have changed, so that the MPTT fields need to be updated. """ instance._mptt_cached_fields = {} field_names = {self.parent_attr} if self.order_insertion_by: for f in self.order_insertion_by: if f[0] == "-": f = f[1:] field_names.add(f) deferred_fields = instance.get_deferred_fields() for field_name in field_names: if deferred_fields: field = instance._meta.get_field(field_name) if ( field.attname in deferred_fields and field.attname not in instance.__dict__ ): # deferred attribute (i.e. via .only() or .defer()) # It'd be silly to cache this (that'd do a database query) # Instead, we mark it as a deferred attribute here, then # assume it hasn't changed during save(), unless it's no # longer deferred. instance._mptt_cached_fields[field_name] = DeferredAttribute continue instance._mptt_cached_fields[field_name] = self.get_raw_field_value( instance, field_name ) def insertion_target_filters(self, instance, order_insertion_by): """ Creates a filter which matches suitable right siblings for ``node``, where insertion should maintain ordering according to the list of fields in ``order_insertion_by``. For example, given an ``order_insertion_by`` of ``['field1', 'field2', 'field3']``, the resulting filter should correspond to the following SQL:: field1 > %s OR (field1 = %s AND field2 > %s) OR (field1 = %s AND field2 = %s AND field3 > %s) """ fields = [] filters = [] fields__append = fields.append filters__append = filters.append and_ = operator.and_ or_ = operator.or_ for field_name in order_insertion_by: if field_name[0] == "-": field_name = field_name[1:] filter_suffix = "__lt" else: filter_suffix = "__gt" value = getattr(instance, field_name) if value is None: # node isn't saved yet. get the insertion value from pre_save. field = instance._meta.get_field(field_name) value = field.pre_save(instance, True) if value is None: # we have to use __isnull instead of __lt or __gt becase __lt = Null is invalid # depending on order, we need to find the first node where code is null or not null value = filter_suffix == "__lt" filter_suffix = "__isnull" 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 self.order_insertion_by and ( parent is None or parent.get_descendant_count() > 0 ): opts = node._mptt_meta order_by = opts.order_insertion_by[:] filters = self.insertion_target_filters(node, order_by) if parent: filters = filters & Q(**{opts.parent_attr: parent}) # Fall back on tree ordering if multiple child nodes have # the same values. order_by.append(opts.left_attr) else: filters = filters & Q(**{opts.parent_attr: None}) # Fall back on tree id ordering if multiple root nodes have # the same values. order_by.append(opts.tree_id_attr) queryset = ( node.__class__._tree_manager.db_manager(node._state.db) .filter(filters) .order_by(*order_by) ) if node.pk: queryset = queryset.exclude(pk=node.pk) try: right_sibling = queryset[:1][0] except IndexError: # No suitable right sibling could be found pass return right_sibling class MPTTModelBase(ModelBase): """ Metaclass for MPTT models """ def __new__(meta, class_name, bases, class_dict): """ Create subclasses of MPTTModel. This: - adds the MPTT fields to the class - adds a TreeManager to the model """ if class_name == "NewBase" and class_dict == {}: return super().__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().__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) is_cls_tree_model = _get_tree_model(cls) is cls if is_cls_tree_model: # HACK: _meta.get_field() doesn't work before AppCache.ready in Django>=1.8 # ( see https://code.djangoproject.com/ticket/24231 ) # So the only way to get existing fields is using local_fields on all superclasses. existing_field_names = set() for base in cls.mro(): if hasattr(base, "_meta"): existing_field_names.update( [f.name for f in base._meta.local_fields] ) mptt_meta = cls._mptt_meta indexed_attrs = (mptt_meta.tree_id_attr,) field_names = ( mptt_meta.left_attr, mptt_meta.right_attr, mptt_meta.tree_id_attr, mptt_meta.level_attr, ) for field_name in field_names: if field_name not in existing_field_names: field = models.PositiveIntegerField( db_index=field_name in indexed_attrs, editable=False ) field.contribute_to_class(cls, field_name) # Add an index_together on tree_id_attr and left_attr, as these are very # commonly queried (pretty much all reads). index_together = (cls._mptt_meta.tree_id_attr, cls._mptt_meta.left_attr) if index_together not in cls._meta.index_together: cls._meta.index_together += (index_together,) # Add a tree manager, if there isn't one already if not abstract: # make sure we have a tree manager somewhere tree_manager = None # Use the default manager defined on the class if any if cls._default_manager and isinstance( cls._default_manager, TreeManager ): tree_manager = cls._default_manager else: for cls_manager in cls._meta.managers: if isinstance(cls_manager, TreeManager): # prefer any locally defined manager (i.e. keep going if not local) if cls_manager.model is cls: tree_manager = cls_manager break if is_cls_tree_model: idx_together = ( cls._mptt_meta.tree_id_attr, cls._mptt_meta.left_attr, ) if idx_together not in cls._meta.index_together: cls._meta.index_together += (idx_together,) if tree_manager and tree_manager.model is not cls: tree_manager = tree_manager._copy_to_model(cls) elif tree_manager is None: tree_manager = TreeManager() tree_manager.contribute_to_class(cls, "_tree_manager") # avoid using ManagerDescriptor, so instances can refer to self._tree_manager setattr(cls, "_tree_manager", tree_manager) return cls def raise_if_unsaved(func): @wraps(func) def _fn(self, *args, **kwargs): if self._state.adding: raise ValueError( "Cannot call %(function)s on unsaved %(class)s instances" % {"function": func.__name__, "class": self.__class__.__name__} ) return func(self, *args, **kwargs) return _fn class MPTTModel(models.Model, metaclass=MPTTModelBase): """ Base class for tree models. """ class Meta: abstract = True objects = TreeManager() def __init__(self, *args, **kwargs): super().__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 = {(t + num_inserted if t >= tree_id else t) for t in changes} cls._threadlocal.mptt_delayed_tree_changes = new_changes @raise_if_unsaved def get_ancestors(self, ascending=False, include_self=False): """ Creates a ``QuerySet`` containing the ancestors of this model instance. This defaults to being in descending order (root ancestor first, immediate parent last); passing ``True`` for the ``ascending`` argument will reverse the ordering (immediate parent first, root ancestor last). If ``include_self`` is ``True``, the ``QuerySet`` will also include this model instance. """ opts = self._mptt_meta if self.is_root_node(): if not include_self: return self._tree_manager.none() else: # Filter on pk for efficiency. qs = self._tree_manager.filter(pk=self.pk) else: order_by = opts.left_attr if ascending: order_by = "-" + order_by left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) if not include_self: left -= 1 right += 1 qs = self._tree_manager._mptt_filter( left__lte=left, right__gte=right, tree_id=self._mpttfield("tree_id"), ) qs = qs.order_by(order_by) if hasattr(self, "_mptt_use_cached_ancestors"): # Called during or after a `recursetree` tag. # There should be cached parents up to level 0. # So we can use them to avoid doing a query at all. ancestors = [] p = self if not include_self: p = getattr(p, opts.parent_attr) while p is not None: ancestors.append(p) p = getattr(p, opts.parent_attr) ancestors.reverse() qs._result_cache = ancestors return qs @raise_if_unsaved def get_family(self): """ Returns a ``QuerySet`` containing the ancestors, the model itself and the descendants, in tree order. """ opts = self._mptt_meta left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) ancestors = Q( **{ "%s__lte" % opts.left_attr: left, "%s__gte" % opts.right_attr: right, opts.tree_id_attr: self._mpttfield("tree_id"), } ) descendants = Q( **{ "%s__gte" % opts.left_attr: left, "%s__lte" % opts.left_attr: right, opts.tree_id_attr: self._mpttfield("tree_id"), } ) return self._tree_manager.filter(ancestors | descendants) @raise_if_unsaved def get_children(self): """ Returns a ``QuerySet`` containing the immediate children of this model instance, in tree order. The benefit of using this method over the reverse relation provided by the ORM to the instance's children is that a database query can be avoided in the case where the instance is a leaf node (it has no children). If called from a template where the tree has been walked by the ``cache_tree_children`` filter, no database query is required. """ if hasattr(self, "_cached_children"): qs = self._tree_manager.filter(pk__in=[n.pk for n in self._cached_children]) qs._result_cache = self._cached_children return qs else: if self.is_leaf_node(): return self._tree_manager.none() return self._tree_manager._mptt_filter(parent=self) @raise_if_unsaved def get_descendants(self, include_self=False): """ Creates a ``QuerySet`` containing descendants of this model instance, in tree order. If ``include_self`` is ``True``, the ``QuerySet`` will also include this model instance. """ if self.is_leaf_node(): if not include_self: return self._tree_manager.none() else: return self._tree_manager.filter(pk=self.pk) opts = self._mptt_meta left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) if not include_self: left += 1 right -= 1 return self._tree_manager._mptt_filter( tree_id=self._mpttfield("tree_id"), left__gte=left, left__lte=right ) def get_descendant_count(self): """ Returns the number of descendants this model instance has. """ if self._mpttfield("right") is None: # node not saved yet return 0 else: return (self._mpttfield("right") - self._mpttfield("left") - 1) // 2 @raise_if_unsaved def get_leafnodes(self, include_self=False): """ Creates a ``QuerySet`` containing leafnodes of this model instance, in tree order. If ``include_self`` is ``True``, the ``QuerySet`` will also include this model instance (if it is a leaf node) """ descendants = self.get_descendants(include_self=include_self) return self._tree_manager._mptt_filter( descendants, left=(models.F(self._mptt_meta.right_attr) - 1) ) @raise_if_unsaved def get_next_sibling(self, *filter_args, **filter_kwargs): """ Returns this model instance's next sibling in the tree, or ``None`` if it doesn't have a next sibling. """ qs = self._tree_manager.filter(*filter_args, **filter_kwargs) if self.is_root_node(): qs = self._tree_manager._mptt_filter( qs, parent=None, tree_id__gt=self._mpttfield("tree_id"), ) else: qs = self._tree_manager._mptt_filter( qs, parent__pk=getattr(self, self._mptt_meta.parent_attr + "_id"), left__gt=self._mpttfield("right"), ) siblings = qs[:1] return siblings and siblings[0] or None @raise_if_unsaved def get_previous_sibling(self, *filter_args, **filter_kwargs): """ Returns this model instance's previous sibling in the tree, or ``None`` if it doesn't have a previous sibling. """ opts = self._mptt_meta qs = self._tree_manager.filter(*filter_args, **filter_kwargs) if self.is_root_node(): qs = self._tree_manager._mptt_filter( qs, parent=None, tree_id__lt=self._mpttfield("tree_id"), ) qs = qs.order_by("-" + opts.tree_id_attr) else: qs = self._tree_manager._mptt_filter( qs, parent__pk=getattr(self, opts.parent_attr + "_id"), right__lt=self._mpttfield("left"), ) qs = qs.order_by("-" + opts.right_attr) siblings = qs[:1] return siblings and siblings[0] or None @raise_if_unsaved def get_root(self): """ Returns the root node of this model instance's tree. """ if self.is_root_node() and type(self) == self._tree_manager.tree_model: return self return self._tree_manager._mptt_filter( tree_id=self._mpttfield("tree_id"), parent=None, ).get() @raise_if_unsaved def get_siblings(self, include_self=False): """ Creates a ``QuerySet`` containing siblings of this model instance. Root nodes are considered to be siblings of other root nodes. If ``include_self`` is ``True``, the ``QuerySet`` will also include this model instance. """ if self.is_root_node(): queryset = self._tree_manager._mptt_filter(parent=None) else: parent_id = getattr(self, self._mptt_meta.parent_attr + "_id") queryset = self._tree_manager._mptt_filter(parent__pk=parent_id) if not include_self: queryset = queryset.exclude(pk=self.pk) return queryset def get_level(self): """ Returns the level of this node (distance from root) """ return getattr(self, self._mptt_meta.level_attr) def insert_at( self, target, position="first-child", save=False, allow_existing_pk=False, refresh_target=True, ): """ Convenience method for calling ``TreeManager.insert_node`` with this model instance. """ self._tree_manager.insert_node( self, target, position, save, allow_existing_pk=allow_existing_pk, refresh_target=refresh_target, ) def is_child_node(self): """ Returns ``True`` if this model instance is a child node, ``False`` otherwise. """ return not self.is_root_node() def is_leaf_node(self): """ Returns ``True`` if this model instance is a leaf node (it has no children), ``False`` otherwise. """ return not self.get_descendant_count() def is_root_node(self): """ Returns ``True`` if this model instance is a root node, ``False`` otherwise. """ return getattr(self, self._mptt_meta.parent_attr + "_id") is None @raise_if_unsaved def is_descendant_of(self, other, include_self=False): """ Returns ``True`` if this model is a descendant of the given node, ``False`` otherwise. If include_self is True, also returns True if the two nodes are the same node. """ opts = self._mptt_meta if include_self and other.pk == self.pk: return True if getattr(self, opts.tree_id_attr) != getattr(other, opts.tree_id_attr): return False else: left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) return left > getattr(other, opts.left_attr) and right < getattr( other, opts.right_attr ) @raise_if_unsaved def is_ancestor_of(self, other, include_self=False): """ Returns ``True`` if this model is an ancestor of the given node, ``False`` otherwise. If include_self is True, also returns True if the two nodes are the same node. """ if include_self and other.pk == self.pk: return True return other.is_descendant_of(self) def move_to(self, target, position="first-child"): """ Convenience method for calling ``TreeManager.move_node`` with this model instance. NOTE: This is a low-level method; it does NOT respect ``MPTTMeta.order_insertion_by``. In most cases you should just move the node yourself by setting node.parent. """ self._tree_manager.move_node(self, target, position) def _is_saved(self, using=None): if self.pk is None or self._mpttfield("tree_id") is None: return False opts = self._meta if opts.pk.remote_field is None: return True else: if not hasattr(self, "_mptt_saved"): manager = self.__class__._base_manager manager = manager.using(using) self._mptt_saved = manager.filter(pk=self.pk).exists() return self._mptt_saved def _get_user_field_names(self): """Returns the list of user defined (i.e. non-mptt internal) field names.""" from django.db.models.fields import AutoField field_names = [] internal_fields = ( self._mptt_meta.left_attr, self._mptt_meta.right_attr, self._mptt_meta.tree_id_attr, self._mptt_meta.level_attr, ) for field in self._meta.concrete_fields: if ( (field.name not in internal_fields) and (not isinstance(field, AutoField)) and (not field.primary_key) ): # noqa field_names.append(field.name) return field_names def save(self, *args, **kwargs): """ If this is a new node, sets tree fields up before it is inserted into the database, making room in the tree structure as necessary, 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 = cached_field_value(self, opts.parent_attr) # 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().save(*args, **kwargs) parent_id = opts.get_raw_field_value(self, opts.parent_attr) # determine whether this instance is already in the db force_update = kwargs.get("force_update", False) force_insert = kwargs.get("force_insert", False) collapse_old_tree = None deferred_fields = self.get_deferred_fields() if force_update or ( not force_insert and self._is_saved(using=kwargs.get("using")) ): # it already exists, so do a move old_parent_id = self._mptt_cached_fields[opts.parent_attr] if old_parent_id is DeferredAttribute: same_order = True else: same_order = old_parent_id == parent_id if same_order and len(self._mptt_cached_fields) > 1: for field_name, old_value in self._mptt_cached_fields.items(): if ( old_value is DeferredAttribute and field_name not in deferred_fields ): same_order = False break if old_value != opts.get_raw_field_value(self, field_name): same_order = False break if not do_updates and not same_order: same_order = True self.__class__._mptt_track_tree_modified(self._mpttfield("tree_id")) elif (not do_updates) and not same_order and old_parent_id is None: # the old tree no longer exists, so we need to collapse it. collapse_old_tree = self._mpttfield("tree_id") parent = getattr(self, opts.parent_attr) tree_id = parent._mpttfield("tree_id") left = parent._mpttfield("left") + 1 self.__class__._mptt_track_tree_modified(tree_id) setattr(self, opts.tree_id_attr, tree_id) setattr(self, opts.left_attr, left) setattr(self, opts.right_attr, left + 1) setattr(self, opts.level_attr, parent._mpttfield("level") + 1) same_order = True if not same_order: parent = getattr(self, opts.parent_attr) opts.set_raw_field_value(self, opts.parent_attr, old_parent_id) try: right_sibling = opts.get_ordered_insertion_target(self, parent) if parent_id is not None: # If we aren't already a descendant of the new parent, # we need to update the parent.rght so things like # get_children and get_descendant_count work correctly. # # parent might be None if parent_id was assigned # directly -- then we certainly do not have to update # the cached parent. update_cached_parent = parent and ( getattr(self, opts.tree_id_attr) != getattr(parent, opts.tree_id_attr) or getattr(self, opts.left_attr) # noqa < getattr(parent, opts.left_attr) or getattr(self, opts.right_attr) > getattr(parent, opts.right_attr) ) if right_sibling: self._tree_manager._move_node( self, right_sibling, "left", save=False, refresh_target=False, ) else: # Default movement if parent_id is None: root_nodes = self._tree_manager.root_nodes() try: rightmost_sibling = root_nodes.exclude( pk=self.pk ).order_by("-" + opts.tree_id_attr)[0] self._tree_manager._move_node( self, rightmost_sibling, "right", save=False, refresh_target=False, ) except IndexError: pass else: self._tree_manager._move_node( self, parent, "last-child", save=False ) if parent_id is not None and update_cached_parent: # Update rght of cached parent right_shift = 2 * (self.get_descendant_count() + 1) self._tree_manager._post_insert_update_cached_parent_right( parent, right_shift ) finally: # Make sure the new parent is always # restored on the way out in case of errors. opts.set_raw_field_value(self, opts.parent_attr, parent_id) # If there were no exceptions raised then send a moved signal node_moved.send( sender=self.__class__, instance=self, target=getattr(self, opts.parent_attr), ) else: opts.set_raw_field_value(self, opts.parent_attr, parent_id) if not track_updates: # When not using delayed/disabled updates, # populate update_fields with user defined model fields. # This helps preserve tree integrity when saving model on top # of a modified tree. if len(args) > 3: if not args[3]: args = list(args) args[3] = self._get_user_field_names() args = tuple(args) else: if not kwargs.get("update_fields", None): kwargs["update_fields"] = self._get_user_field_names() else: # new node, do an insert if getattr(self, opts.left_attr) and getattr(self, opts.right_attr): # This node has already been set up for insertion. pass else: parent = getattr(self, opts.parent_attr) right_sibling = None # if we're inside delay_mptt_updates, don't do queries to find # sibling position. instead, do default insertion. correct # positions will be found during partial rebuild later. # *unless* this is a root node. (as update tracking doesn't # handle re-ordering of trees.) if do_updates or parent is None: if opts.order_insertion_by: right_sibling = opts.get_ordered_insertion_target(self, parent) if right_sibling: self.insert_at( right_sibling, "left", allow_existing_pk=True, refresh_target=False, ) if parent: # since we didn't insert into parent, we have to update parent.rght # here instead of in TreeManager.insert_node() right_shift = 2 * (self.get_descendant_count() + 1) self._tree_manager._post_insert_update_cached_parent_right( parent, right_shift ) else: # Default insertion self.insert_at( parent, position="last-child", allow_existing_pk=True ) try: super().save(*args, **kwargs) finally: if collapse_old_tree is not None: self._tree_manager._create_tree_space(collapse_old_tree, -1) self._mptt_saved = True opts.update_mptt_cached_fields(self) save.alters_data = True def delete(self, *args, **kwargs): """Calling ``delete`` on a node will delete it as well as its full subtree, as opposed to reattaching all the subnodes to its parent node. There are no argument specific to a MPTT model, all the arguments will be passed directly to the django's ``Model.delete``. ``delete`` will not return anything.""" try: # We have to make sure we use database's mptt values, since they # could have changed between the moment the instance was retrieved and # the moment it is deleted. # This happens for example if you delete several nodes at once from a queryset. fields_to_refresh = [ self._mptt_meta.right_attr, self._mptt_meta.left_attr, self._mptt_meta.tree_id_attr, ] self.refresh_from_db(fields=fields_to_refresh) except self.__class__.DoesNotExist: # In case the object was already deleted, we don't want to throw an exception pass 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 = cached_field_value(self, self._mptt_meta.parent_attr) if parent: right_shift = -self.get_descendant_count() - 2 self._tree_manager._post_insert_update_cached_parent_right( parent, right_shift ) return super().delete(*args, **kwargs) delete.alters_data = True def _mptt_refresh(self): if not self.pk: return manager = type(self)._tree_manager opts = self._mptt_meta values = ( manager.using(self._state.db) .filter(pk=self.pk) .values( opts.left_attr, opts.right_attr, opts.level_attr, opts.tree_id_attr, )[0] ) for k, v in values.items(): setattr(self, k, v) django-mptt-0.13.2/mptt/querysets.py000066400000000000000000000021551411214034000174140ustar00rootroot00000000000000from django.db import models from mptt import utils class TreeQuerySet(models.query.QuerySet): def as_manager(cls): # Address the circular dependency between `Queryset` and `Manager`. from mptt.managers import TreeManager manager = TreeManager.from_queryset(cls)() manager._built_with_as_manager = True return manager as_manager.queryset_only = True as_manager = classmethod(as_manager) def get_descendants(self, *args, **kwargs): """ Alias to `mptt.managers.TreeManager.get_queryset_descendants`. """ return self.model._tree_manager.get_queryset_descendants(self, *args, **kwargs) get_descendants.queryset_only = True def get_ancestors(self, *args, **kwargs): """ Alias to `mptt.managers.TreeManager.get_queryset_ancestors`. """ return self.model._tree_manager.get_queryset_ancestors(self, *args, **kwargs) get_ancestors.queryset_only = True def get_cached_trees(self): """ Alias to `mptt.utils.get_cached_trees`. """ return utils.get_cached_trees(self) django-mptt-0.13.2/mptt/settings.py000066400000000000000000000002541411214034000172060ustar00rootroot00000000000000from django.conf import settings """Default level indicator. By default is `'---'`.""" DEFAULT_LEVEL_INDICATOR = getattr(settings, "MPTT_DEFAULT_LEVEL_INDICATOR", "---") django-mptt-0.13.2/mptt/signals.py000066400000000000000000000004541411214034000170100ustar00rootroot00000000000000from django.db.models.signals import ModelSignal # Behaves like Djangos normal pre-/post_save signals signals with the # added arguments ``target`` and ``position`` that matches those of # ``move_to``. # If the signal is called from ``save`` it'll not be pass position. node_moved = ModelSignal() django-mptt-0.13.2/mptt/static/000077500000000000000000000000001411214034000162625ustar00rootroot00000000000000django-mptt-0.13.2/mptt/static/mptt/000077500000000000000000000000001411214034000172465ustar00rootroot00000000000000django-mptt-0.13.2/mptt/static/mptt/arrow-move-black.png000066400000000000000000000007111411214034000231230ustar00rootroot00000000000000PNG  IHDR{gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs I8tIME  +9XIDAT(ϝ PЇ9x#7;t*(>+ 細3Q(`qwL+$TL)Jbr'[Q/IzI>V9;x=\\=DB,~al<ŸO38]b%tEXtdate:create2015-12-16T13:43:57+01:00%%tEXtdate:modify2015-12-16T13:43:57+01:00˙tEXtSoftwarewww.inkscape.org<IENDB`django-mptt-0.13.2/mptt/static/mptt/arrow-move-white.png000066400000000000000000000013441411214034000231720ustar00rootroot00000000000000PNG  IHDR/<gAMA a cHRMz&u0`:pQ< pHYs ycYiTXtXML:com.adobe.xmp 1 L'YIDAT8 @] K)7;ŻUhޭ*`?{8q#nKZQe K#eD8WyTGx)K08U?E B@9wD/lI3OYS1Ox1ReZ ᗁvp=U 8&k Y 38P+'MyżoC|o5l|e͊+pb~|F?=64QIENDB`django-mptt-0.13.2/mptt/static/mptt/disclosure-down-black.png000066400000000000000000000007601411214034000241520ustar00rootroot00000000000000PNG  IHDR{gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs I8tIME  +9XIDAT(ύ!s 3my#*f2UTT\1`-}_GP]ɛW/y5Vj񝵕~ֳ@\5I'{Gyp'2XM15TmCiJ T!YJ\Տ6{JoaO4y/ W#%tEXtdate:create2015-12-16T13:43:57+01:00%%tEXtdate:modify2015-12-16T13:43:57+01:00˙tEXtSoftwarewww.inkscape.org<IENDB`django-mptt-0.13.2/mptt/static/mptt/disclosure-down-white.png000066400000000000000000000013611411214034000242140ustar00rootroot00000000000000PNG  IHDR/<gAMA a cHRMz&u0`:pQ< pHYs ycYiTXtXML:com.adobe.xmp 1 L'YIDAT8=A悎^^Ј_P+tFPNCMf9"&y2s3w,*Hj&;:wK\hQ0ݞ<'fuXf eB,)GM 8 |VO3iybqJ0-Xi& ,Ai(%ke!H54)A M6#E mXJ-@5cgR/%+h}HR6}mU?ozAC,گ~whU6IENDB`django-mptt-0.13.2/mptt/static/mptt/disclosure-right-black.png000066400000000000000000000007201411214034000243140ustar00rootroot00000000000000PNG  IHDR{gAMA a cHRMz&u0`:pQ<bKGD̿ pHYs I8tIME  +9XIDAT(ϕ1o5ozF Ε\ :JBRmdQаzLɼĻZ8_K41>*R}Bao#0W/#p5DNQ=]v aO0lu0pP*c +rUqhj%tEXtdate:create2015-12-16T13:43:57+01:00%%tEXtdate:modify2015-12-16T13:43:57+01:00˙tEXtSoftwarewww.inkscape.org<IENDB`django-mptt-0.13.2/mptt/static/mptt/disclosure-right-white.png000066400000000000000000000013071411214034000243620ustar00rootroot00000000000000PNG  IHDR/<gAMA a cHRMz&u0`:pQ< pHYs ycYiTXtXML:com.adobe.xmp 1 L'YIDAT80)fp4~C  8<!AQS 3|\mKܭ߿5fza fFLS!,a -wrX`5eAnS؀ aGC4i)j|2DDSސb"e!NDMԕD}/5weRCSbG ݉sH_>SQu5%-IENDB`django-mptt-0.13.2/mptt/static/mptt/draggable-admin.css000066400000000000000000000025161411214034000227620ustar00rootroot00000000000000.field-tree_actions { width: 50px; padding: 2px; } .field-tree_actions > div { display: inline-block; vertical-align: middle; background-repeat: no-repeat; width: 18px; height: 18px; margin: 7px 2px 0 0; } .tree-node { cursor: pointer; } .tree-node.children { background-image: url(disclosure-down.png); } .tree-node.closed { background-image: url(disclosure-right.png); } .drag-handle { background-image: url(arrow-move.png); cursor: move; } /* focus */ #result_list tbody tr:focus { background-color: #ffffcc !important; outline: 0px; } @media (prefers-color-scheme: dark) { .tree-node.children { background-image: url(disclosure-down-white.png); } .tree-node.closed { background-image: url(disclosure-right-white.png); } .drag-handle { background-image: url(arrow-move-white.png); } #result_list tbody tr:focus { background-color: #002f33 !important; outline: 0px; } } #drag-line { position: absolute; height: 3px; font-size: 0px; background-color: #417690; } #drag-line:before { content: ' '; display: block; position: absolute; height: 10px; width: 10px; background: #417690; margin: -3px 0 0 0; border-radius: 9px; } #drag-line span { display: block; position: absolute; left: 15px; bottom: 0; width: 300px; height: 18px; color: #417690; font-size: 12px; } django-mptt-0.13.2/mptt/static/mptt/draggable-admin.js000066400000000000000000000345321411214034000226110ustar00rootroot00000000000000/* global django */ // IE<9 lacks Array.prototype.indexOf if (!Array.prototype.indexOf) { Array.prototype.indexOf = function(needle) { for (var i=0, l=this.length; i= 0) DraggableMPTTAdmin.collapsedNodes.splice(idx, 1); } function markNodeAsCollapsed(id) { if(isExpandedNode(id)) DraggableMPTTAdmin.collapsedNodes.push(id); } function treeNode(pk) { return $('.tree-node[data-pk="' + pk + '"]'); } // toggle children function doToggle(id, show) { var children = DraggableMPTTAdmin.treeStructure[id] || []; for (var i=0; i').appendTo('body'); } $('#ghost').html(cloned).css({ 'opacity': .8, 'position': 'absolute', 'top': event.pageY, 'left': event.pageX - 30, 'width': 600 }); // check on edge of screen if(event.pageY+100 > $(window).height()+$(window).scrollTop()) { $('html,body').stop().animate({scrollTop: $(window).scrollTop()+250 }, 500); } else if(event.pageY-50 < $(window).scrollTop()) { $('html,body').stop().animate({scrollTop: $(window).scrollTop()-250 }, 500); } // check if drag-line element already exists, else append if($('#drag-line').length < 1) { $('body').append('
'); } // loop trough all rows $('tr', originalRow.parent()).each(function(index, el) { var element = $(el), top = element.offset().top, next; // check if mouse is over a row if (event.pageY >= top && event.pageY < top + rowHeight) { var targetRow = null, targetLoc = null, elementLevel = rowLevel(element); if (event.pageY >= top && event.pageY < top + rowHeight / 3) { targetRow = element; targetLoc = BEFORE; } else if (event.pageY >= top + rowHeight / 3 && event.pageY < top + rowHeight * 2 / 3) { next = element.next(); // there's no point in allowing adding children when there are some already // better move the items to the correct place right away if (!next.length || rowLevel(next) <= elementLevel) { targetRow = element; targetLoc = CHILD; } } else if (event.pageY >= top + rowHeight * 2 / 3 && event.pageY < top + rowHeight) { next = element.next(); if (!next.length || rowLevel(next) <= elementLevel) { targetRow = element; targetLoc = AFTER; } } if(targetRow) { // Positioning relative to cell containing the link var offset = targetRow.find('th').offset(); var left = offset.left + rowLevel(targetRow) * CHILD_PAD + (targetLoc == CHILD ? CHILD_PAD : 0) + 5; // Center of the circle aligns with start of link text (cell padding!) $('#drag-line').css({ 'width': resultListWidth - left, 'left': left, 'top': offset.top + (targetLoc == BEFORE ? 0 : rowHeight) }).find('span').text(DraggableMPTTAdmin.messages[targetLoc] || ''); // Store the found row and options moveTo.hovering = element; moveTo.relativeTo = targetRow; moveTo.side = targetLoc; return true; } } }); }); $('body').keydown(function(event) { if (event.which == '27') { $('#drag-line').remove(); $('#ghost').remove(); $('body').removeClass('dragging').enableSelection().unbind('mousemove').unbind('mouseup'); event.preventDefault(); } }); $('body').bind('mouseup', function() { if(moveTo.relativeTo) { var cutItem = originalRow.find('.tree-node').data('pk'); var pastedOn = moveTo.relativeTo.find('.tree-node').data('pk'); // get out early if items are the same if(cutItem != pastedOn) { var isParent = ( rowLevel(moveTo.relativeTo.next()) > rowLevel(moveTo.relativeTo)); var position = ''; // determine position if(moveTo.side == CHILD && !isParent) { position = 'last-child'; } else if (moveTo.side == BEFORE) { position = 'left'; } else { position = 'right'; } $.ajax({ complete: function() { window.location.reload(); }, data: { cmd: 'move_node', position: position, cut_item: cutItem, pasted_on: pastedOn }, headers: { 'X-CSRFToken': $('input[type=hidden][name=csrfmiddlewaretoken]').val() }, method: 'POST' }); } else { $('#drag-line').remove(); $('#ghost').remove(); } $('body').removeClass('dragging').enableSelection().unbind('mousemove').unbind('mouseup'); } }); }); return this; }); /* Every time the user expands or collapses a part of the tree, we remember the current state of the tree so we can restore it on a reload. */ function storeCollapsedNodes(nodes) { window.localStorage && window.localStorage.setItem( DraggableMPTTAdmin.storageName, JSON.stringify(nodes) ); } function retrieveCollapsedNodes() { try { return JSON.parse(window.localStorage.getItem( DraggableMPTTAdmin.storageName )); } catch(e) { return null; } } function expandOrCollapseNode(item) { var show = true; if (!item.hasClass('children')) return; var itemId = item.data('pk'); if (!isExpandedNode(itemId)) { item.removeClass('closed'); markNodeAsExpanded(itemId); } else { item.addClass('closed'); show = false; markNodeAsCollapsed(itemId); } storeCollapsedNodes(DraggableMPTTAdmin.collapsedNodes); doToggle(itemId, show); } function collapseTree() { var rlist = $("#result_list"); rlist.hide(); $('tbody tr', rlist).each(function(i, el) { var marker = $('.tree-node', el); if (marker.hasClass('children')) { var itemId = marker.data('pk'); doToggle(itemId, false); marker.addClass('closed'); markNodeAsCollapsed(itemId); } }); storeCollapsedNodes(DraggableMPTTAdmin.collapsedNodes); rlist.show(); return false; } function expandTree() { var rlist = $("#result_list"); rlist.hide(); $('tbody tr', rlist).each(function(i, el) { var marker = $('.tree-node', el); if (marker.hasClass('children')) { var itemId = $('.tree-node', el).data('pk'); doToggle(itemId, true); marker.removeClass('closed'); markNodeAsExpanded(itemId); } }); storeCollapsedNodes([]); rlist.show(); return false; } var changelistTab = function(elem, event, direction) { event.preventDefault(); elem = $(elem); var ne = (direction > 0) ? elem.nextAll(':visible:first') : elem.prevAll(':visible:first'); if(ne) { elem.attr('tabindex', -1); ne.attr('tabindex', '0'); ne.focus(); } }; function keyboardNavigationHandler(event) { // On form element? Ignore. if (/textarea|select|input/i.test(event.target.nodeName)) return; // console.log('keydown', this, event.keyCode); switch (event.keyCode) { case 40: // down changelistTab(this, event, 1); break; case 38: // up changelistTab(this, event, -1); break; case 37: // left case 39: // right expandOrCollapseNode($(this).find('.tree-node')); break; case 13: // return document.location = $('a', this).attr('href'); break; default: break; } } function addObjectTool(title, handler) { var $a = $('
'); $a.click(handler); $a.text(title); $a.prependTo('.object-tools').wrap('
  • '); } // Some old browsers do not support JSON.parse (the only thing we require) var jsonParse = JSON.parse || function jsonParse(sJSON) { return eval('(' + sJSON + ')'); }; DraggableMPTTAdmin = jsonParse( document.getElementById('draggable-admin-context').getAttribute('data-context')); addObjectTool(DraggableMPTTAdmin.messages.collapseTree, collapseTree); addObjectTool(DraggableMPTTAdmin.messages.expandTree, expandTree); // fire! var rlist = $("#result_list"), rlist_tbody = rlist.find('tbody'); if ($('tbody tr', rlist).length > 1) { rlist_tbody.feinTree(); rlist.find('.tree-node').on('click', function(event) { event.preventDefault(); event.stopPropagation(); expandOrCollapseNode($(this)); }); /* Enable focussing, put focus on first result, add handler for keyboard navigation */ $('tr', rlist).attr('tabindex', -1); $('tbody tr:first', rlist).attr('tabindex', 0).focus(); $('tr', rlist).keydown(keyboardNavigationHandler); DraggableMPTTAdmin.collapsedNodes = []; var storedNodes = retrieveCollapsedNodes(); if (storedNodes) { for(var i=0; i{# DIV for HTML validation #} {% for item in result_hidden_fields %}{{ item }}{% endfor %} {% endif %} {% if results %}
    {% for header in result_headers %} {% endfor %} {% for result in results %} {% if result.form.non_field_errors %} {% endif %} {% for item in result %}{{ item }}{% endfor %} {% endfor %}
    {{ header.text|capfirst }}
    {{ result.form.non_field_errors }}
    {% endif %} django-mptt-0.13.2/mptt/templates/admin/mptt_change_list.html000066400000000000000000000005361411214034000242770ustar00rootroot00000000000000{% extends "admin/change_list.html" %} {% load admin_list i18n mptt_admin %} {% block result_list %} {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %} {% mptt_result_list cl %} {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} {% endblock %} django-mptt-0.13.2/mptt/templates/admin/mptt_change_list_results.html000066400000000000000000000014651411214034000260620ustar00rootroot00000000000000{% 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.13.2/mptt/templates/admin/mptt_filter.html000066400000000000000000000005351411214034000233030ustar00rootroot00000000000000{% load i18n %} {% load mptt_admin %}

    {% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}

    django-mptt-0.13.2/mptt/templatetags/000077500000000000000000000000001411214034000174655ustar00rootroot00000000000000django-mptt-0.13.2/mptt/templatetags/__init__.py000066400000000000000000000000001411214034000215640ustar00rootroot00000000000000django-mptt-0.13.2/mptt/templatetags/mptt_admin.py000066400000000000000000000215501411214034000221760ustar00rootroot00000000000000import datetime import warnings from django.conf import settings from django.contrib.admin.templatetags.admin_list import ( result_headers, result_hidden_fields, ) from django.contrib.admin.templatetags.admin_urls import add_preserved_filters from django.contrib.admin.utils import ( display_for_field, display_for_value, lookup_field, ) from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist from django.db import models from django.template import Library from django.urls import NoReverseMatch try: from django.utils.deprecation import RemovedInDjango20Warning except ImportError: RemovedInDjango20Warning = RuntimeWarning from django.contrib.admin.templatetags.admin_list import _coerce_field_name from django.utils.encoding import force_str from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import get_language_bidi register = Library() MPTT_ADMIN_LEVEL_INDENT = getattr(settings, "MPTT_ADMIN_LEVEL_INDENT", 10) IS_GRAPPELLI_INSTALLED = True if "grappelli" in settings.INSTALLED_APPS else False ### # Ripped from contrib.admin's (1.10) 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. """ def link_in_col(is_first, field_name, cl): if cl.list_display_links is None: return False if is_first and not cl.list_display_links: return True return field_name in cl.list_display_links 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 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 # figure out how much to indent mptt_level_indent = getattr( cl.model_admin, "mptt_level_indent", MPTT_ADMIN_LEVEL_INDENT ) # #### MPTT ADDITION END for field_index, field_name in enumerate(cl.list_display): # #### MPTT SUBSTITUTION START empty_value_display = cl.model_admin.get_empty_value_display() # #### MPTT SUBSTITUTION END row_classes = ["field-%s" % _coerce_field_name(field_name, field_index)] try: f, attr, value = lookup_field(field_name, result, cl.model_admin) except ObjectDoesNotExist: result_repr = empty_value_display else: empty_value_display = getattr( attr, "empty_value_display", empty_value_display ) if f is None or f.auto_created: if field_name == "action_checkbox": row_classes = ["action-checkbox"] allow_tags = getattr(attr, "allow_tags", False) boolean = getattr(attr, "boolean", False) # #### MPTT SUBSTITUTION START result_repr = display_for_value(value, empty_value_display, boolean) # #### MPTT SUBSTITUTION END if allow_tags: warnings.warn( "Deprecated allow_tags attribute used on field {}. " "Use django.utils.safestring.format_html(), " "format_html_join(), or mark_safe() instead.".format( field_name ), RemovedInDjango20Warning, ) result_repr = mark_safe(result_repr) if isinstance(value, (datetime.date, datetime.time)): row_classes.append("nowrap") else: # #### MPTT SUBSTITUTION START is_many_to_one = isinstance(f.remote_field, models.ManyToOneRel) if is_many_to_one: # #### MPTT SUBSTITUTION END field_val = getattr(result, f.name) if field_val is None: result_repr = empty_value_display else: result_repr = field_val else: # #### MPTT SUBSTITUTION START result_repr = display_for_field(value, f, empty_value_display) # #### MPTT SUBSTITUTION END if isinstance( f, (models.DateField, models.TimeField, models.ForeignKey) ): row_classes.append("nowrap") if force_str(result_repr) == "": result_repr = mark_safe(" ") row_class = mark_safe(' class="%s"' % " ".join(row_classes)) # #### MPTT ADDITION START if field_name == mptt_indent_field: level = getattr(result, result._mptt_meta.level_attr) padding_attr = mark_safe( ' style="padding-%s:%spx"' % ( "right" if get_language_bidi() else "left", 8 + mptt_level_indent * level, ) ) else: padding_attr = "" # #### MPTT ADDITION END # If list_display_links not defined, add the link tag to the first field if link_in_col(first, field_name, cl): table_tag = "th" if first else "td" first = False # Display link to the result's change_view if the url exists, else # display just the result's representation. try: url = cl.url_for_result(result) except NoReverseMatch: link_or_text = result_repr else: url = add_preserved_filters( {"preserved_filters": cl.preserved_filters, "opts": cl.opts}, url, ) # 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) if cl.is_popup: opener = format_html(' data-popup-opener="{}"', value) else: opener = "" link_or_text = format_html( '{}', url, opener, result_repr ) # #### MPTT SUBSTITUTION START yield format_html( "<{}{}{}>{}", table_tag, row_class, padding_attr, link_or_text, 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_str(bf.errors) + force_str(bf)) # #### MPTT SUBSTITUTION START yield format_html("{}", row_class, padding_attr, result_repr) # #### MPTT SUBSTITUTION END if form and not form[cl.model._meta.pk.name].is_hidden: yield format_html("{}", force_str(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.13.2/mptt/templatetags/mptt_tags.py000066400000000000000000000255171411214034000220530ustar00rootroot00000000000000""" Template tags for working with lists of model instances which represent trees. """ from django import template from django.apps import apps from django.core.exceptions import FieldDoesNotExist from django.utils.encoding import force_str from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ from mptt.utils import drilldown_tree_for_node, get_cached_trees, tree_item_iterator register = template.Library() # ## ITERATIVE TAGS class FullTreeForModelNode(template.Node): def __init__(self, model, context_var): self.model = model self.context_var = context_var def render(self, context): cls = apps.get_model(*self.model.split(".")) if cls is None: raise template.TemplateSyntaxError( _("full_tree_for_model tag was given an invalid model: %s") % self.model ) context[self.context_var] = cls._tree_manager.all() return "" class DrilldownTreeForNodeNode(template.Node): def __init__( self, node, context_var, foreign_key=None, count_attr=None, cumulative=False, all_descendants=False, ): self.node = template.Variable(node) self.context_var = context_var self.foreign_key = foreign_key self.count_attr = count_attr self.cumulative = cumulative self.all_descendants = all_descendants def render(self, context): # Let any VariableDoesNotExist raised bubble up args = [self.node.resolve(context)] if self.foreign_key is not None: app_label, model_name, fk_attr = self.foreign_key.split(".") cls = apps.get_model(app_label, model_name) if cls is None: raise template.TemplateSyntaxError( _("drilldown_tree_for_node tag was given an invalid model: %s") % ".".join([app_label, model_name]) ) try: cls._meta.get_field(fk_attr) except FieldDoesNotExist: raise template.TemplateSyntaxError( _( "drilldown_tree_for_node tag was given an invalid model field: %s" ) % fk_attr ) args.extend([cls, fk_attr, self.count_attr, self.cumulative]) context[self.context_var] = drilldown_tree_for_node( *args, all_descendants=self.all_descendants ) 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 or all descendants. 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] all_descendants %} {% drilldown_tree_for_node [node] as [varname] count [foreign_key] in [count_attr] %} {% drilldown_tree_for_node [node] as [varname] cumulative count [foreign_key] in [count_attr] %} The foreign key is specified in ``[appname].[modelname].[fieldname]`` format, where ``fieldname`` is the name of a field in the specified model which relates it to the given node's model. When this form is used, a ``count_attr`` attribute on each child of the given node in the drilldown tree will contain a count of the number of items associated with it through the given foreign key. If cumulative is also specified, this count will be for items related to the child node and all of its descendants. Examples:: {% drilldown_tree_for_node genre as drilldown %} {% drilldown_tree_for_node genre as drilldown count tests.Game.genre in game_count %} {% drilldown_tree_for_node genre as drilldown cumulative count tests.Game.genre in game_count %} """ # noqa bits = token.contents.split() len_bits = len(bits) if len_bits not in (4, 5, 8, 9, 10): raise template.TemplateSyntaxError( _("%s tag requires either three, four, seven, eight, or nine arguments") % bits[0] ) if bits[2] != "as": raise template.TemplateSyntaxError( _("second argument to %s tag must be 'as'") % bits[0] ) all_descendants = False if len_bits > 4: if bits[4] == "all_descendants": len_bits -= 1 bits.pop(4) all_descendants = True 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], all_descendants=all_descendants ) 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, all_descendants=all_descendants, ) else: return DrilldownTreeForNodeNode( bits[1], bits[3], all_descendants=all_descendants ) @register.filter def tree_info(items, features=None): """ Given a list of tree items, produces doubles of a tree item and a ``dict`` containing information about the tree structure around the item, with the following contents: new_level ``True`` if the current item is the start of a new level in the tree, ``False`` otherwise. closed_levels A list of levels which end after the current item. This will be an empty list if the next item is at the same level as the current item. Using this filter with unpacking in a ``{% for %}`` tag, you should have enough information about the tree structure to create a hierarchical representation of the tree. Example:: {% for genre,structure in genres|tree_info %} {% if structure.new_level %}
    • {% else %}
    • {% endif %} {{ genre.name }} {% for level in structure.closed_levels %}
    {% endfor %} {% endfor %} """ kwargs = {} if features: feature_names = features.split(",") if "ancestors" in feature_names: kwargs["ancestors"] = True return tree_item_iterator(items, **kwargs) @register.filter def tree_path(items, separator=" :: "): """ Creates a tree path represented by a list of ``items`` by joining the items with a ``separator``. Each path item will be coerced to unicode, so a list of model instances may be given if required. Example:: {{ some_list|tree_path }} {{ some_node.get_ancestors|tree_path:" > " }} """ return separator.join(force_str(i) for i in items) # ## RECURSIVE TAGS @register.filter def cache_tree_children(queryset): """ Alias to `mptt.utils.get_cached_trees`. """ return get_cached_trees(queryset) class RecurseTreeNode(template.Node): def __init__(self, template_nodes, queryset_var): self.template_nodes = template_nodes self.queryset_var = queryset_var def _render_node(self, context, node): bits = [] context.push() for child in node.get_children(): bits.append(self._render_node(context, child)) context["node"] = node context["children"] = mark_safe("".join(bits)) rendered = self.template_nodes.render(context) context.pop() return rendered def render(self, context): queryset = self.queryset_var.resolve(context) roots = cache_tree_children(queryset) bits = [self._render_node(context, node) for node in roots] return "".join(bits) @register.tag def recursetree(parser, token): """ Iterates over the nodes in the tree, and renders the contained block for each node. This tag will recursively render children into the template variable {{ children }}. Only one database query is required (children are cached for the whole tree) Usage:
      {% recursetree nodes %}
    • {{ node.name }} {% if not node.is_leaf_node %}
        {{ children }}
      {% endif %}
    • {% endrecursetree %}
    """ bits = token.contents.split() if len(bits) != 2: raise template.TemplateSyntaxError(_("%s tag requires a queryset") % bits[0]) queryset_var = template.Variable(bits[1]) template_nodes = parser.parse(("endrecursetree",)) parser.delete_first_token() return RecurseTreeNode(template_nodes, queryset_var) django-mptt-0.13.2/mptt/utils.py000066400000000000000000000247351411214034000165200ustar00rootroot00000000000000""" Utilities for working with lists of model instances which represent trees. """ import copy import csv import itertools import sys from django.utils.translation import gettext as _ __all__ = ( "previous_current_next", "tree_item_iterator", "drilldown_tree_for_node", "get_cached_trees", ) def previous_current_next(items): """ From http://www.wordaligned.org/articles/zippy-triples-served-with-python Creates an iterator which returns (previous, current, next) triples, with ``None`` filling in when there is no previous or next available. """ extend = itertools.chain([None], items, [None]) prev, cur, nex = itertools.tee(extend, 3) # Advancing an iterator twice when we know there are two items (the # two Nones at the start and at the end) will never fail except if # `items` is some funny StopIteration-raising generator. There's no point # in swallowing this exception. next(cur) next(nex) next(nex) return zip(prev, cur, nex) def tree_item_iterator(items, ancestors=False, callback=str): """ 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 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 -> ['Books'] Dystopian Futures -> ['Books', 'Sci-fi'] You can overload the default representation by providing an optional ``callback`` function which takes a single argument and performs coersion as required. """ 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(callback(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, all_descendants=False, ): """ Creates a drilldown tree for the given node. A drilldown tree consists of a node's ancestors, itself and its immediate children or all descendants, 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. ``all_descendants`` If ``True``, return all descendants, not just immediate children. """ if all_descendants: children = node.get_descendants() else: children = node.get_children() if rel_cls and rel_field and count_attr: children = node._tree_manager.add_related_count( children, rel_cls, rel_field, count_attr, cumulative ) return itertools.chain(node.get_ancestors(), [node], children) def print_debug_info(qs, file=None): """ Given an mptt queryset, prints some debug information to stdout. Use this when things go wrong. Please include the output from this method when filing bug issues. """ opts = qs.model._mptt_meta writer = csv.writer(sys.stdout if file is None else file) header = ( "pk", opts.level_attr, "%s_id" % opts.parent_attr, opts.tree_id_attr, opts.left_attr, opts.right_attr, "pretty", ) writer.writerow(header) for n in qs.order_by("tree_id", "lft"): level = getattr(n, opts.level_attr) row = [] for field in header[:-1]: row.append(getattr(n, field)) row_text = "%s%s" % ("- " * level, str(n)) row.append(row_text) writer.writerow(row) def _get_tree_model(model_class): # Find the model that contains the tree fields. # This is a weird way of going about it, but Django doesn't let us access # the fields list to detect where the tree fields actually are, # because the app cache hasn't been loaded yet. # So, it *should* be the *last* concrete MPTTModel subclass in the mro(). bases = list(model_class.mro()) while bases: b = bases.pop() # NOTE can't use `issubclass(b, MPTTModel)` here because we can't # import MPTTModel yet! So hasattr(b, '_mptt_meta') will have to do. if hasattr(b, "_mptt_meta") and not (b._meta.abstract or b._meta.proxy): return b return None def get_cached_trees(queryset): """ Takes a list/queryset of model objects in MPTT left (depth-first) order and caches the children and parent on each node. This allows up and down traversal through the tree without the need for further queries. Use cases include using a recursively included template or arbitrarily traversing trees. NOTE: nodes _must_ be passed in the correct (depth-first) order. If they aren't, a ValueError will be raised. Returns a list of top-level nodes. If a single tree was provided in its entirety, the list will of course consist of just the tree's root node. For filtered querysets, if no ancestors for a node are included in the queryset, it will appear in the returned list as a top-level node. Aliases to this function are also available: ``mptt.templatetags.mptt_tag.cache_tree_children`` Use for recursive rendering in templates. ``mptt.querysets.TreeQuerySet.get_cached_trees`` Useful for chaining with queries; e.g., `Node.objects.filter(**kwargs).get_cached_trees()` """ current_path = [] top_nodes = [] if queryset: # Get the model's parent-attribute name parent_attr = queryset[0]._mptt_meta.parent_attr root_level = None is_filtered = hasattr(queryset, "query") and queryset.query.has_filters() for obj in queryset: # Get the current mptt node level node_level = obj.get_level() if root_level is None or (is_filtered and node_level < root_level): # First iteration, so set the root level to the top node level root_level = node_level elif node_level < root_level: # ``queryset`` was a list or other iterable (unable to order), # and was provided in an order other than depth-first raise ValueError( _("Node %s not in depth-first order") % (type(queryset),) ) # Set up the attribute on the node that will store cached children, # which is used by ``MPTTModel.get_children`` obj._cached_children = [] # Remove nodes not in the current branch while len(current_path) > node_level - root_level: current_path.pop(-1) if node_level == root_level: # Add the root to the list of top nodes, which will be returned top_nodes.append(obj) else: # Cache the parent on the current node, and attach the current # node to the parent's list of children _parent = current_path[-1] setattr(obj, parent_attr, _parent) _parent._cached_children.append(obj) if root_level == 0: # get_ancestors() can use .parent.parent.parent... setattr(obj, "_mptt_use_cached_ancestors", True) # Add the current node to end of the current path - the last node # in the current path is the parent for the next iteration, unless # the next iteration is higher up the tree (a new branch), in which # case the paths below it (e.g., this one) will be removed from the # current path during the next iteration current_path.append(obj) return top_nodes django-mptt-0.13.2/requirements.txt000066400000000000000000000000361411214034000172720ustar00rootroot00000000000000Django >= 2.2 django-js-asset django-mptt-0.13.2/setup.cfg000066400000000000000000000030701411214034000156300ustar00rootroot00000000000000[metadata] name = django-mptt version = 0.13.2 description = Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances. long_description = file: README.rst long_description_content_type = text/x-rst author = Craig de Stigter author_email = craig.ds@gmail.com url = https://github.com/django-mptt/django-mptt/ license = MIT-License license_file = LICENSE platforms = OS Independent classifiers = Development Status :: 5 - Production/Stable Environment :: Web Environment Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 Framework :: Django :: 3.2 Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] packages = find: install_requires = django-js-asset python_requires = >=3.6 include_package_data = True zip_safe = False [options.extras_require] tests = coverage mock-django [options.packages.find] exclude = tests tests.* [flake8] exclude = venv,.tox,build,docs ignore = E501,F841,W503 max-line-length = 88 # max-complexity = 10 [isort] profile = black combine_as_imports = True lines_after_imports = 2 [coverage:run] branch = True include = *mptt* omit = *migrations* *tests* *.tox* django-mptt-0.13.2/setup.py000077500000000000000000000000761411214034000155270ustar00rootroot00000000000000#!/usr/bin/env python3 from setuptools import setup setup() django-mptt-0.13.2/tests/000077500000000000000000000000001411214034000151515ustar00rootroot00000000000000django-mptt-0.13.2/tests/.coveragerc000066400000000000000000000006711411214034000172760ustar00rootroot00000000000000[run] include = */mptt/* [report] exclude_lines = pragma: no cover # Don't complain about missing debug-only code: def __unicode__ def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: omit = */tests/* show_missing = True django-mptt-0.13.2/tests/.gitignore000066400000000000000000000000221411214034000171330ustar00rootroot00000000000000.coverage htmlcov django-mptt-0.13.2/tests/__init__.py000066400000000000000000000000001411214034000172500ustar00rootroot00000000000000django-mptt-0.13.2/tests/manage.py000066400000000000000000000003631411214034000167550ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-mptt-0.13.2/tests/myapp/000077500000000000000000000000001411214034000162775ustar00rootroot00000000000000django-mptt-0.13.2/tests/myapp/__init__.py000066400000000000000000000000001411214034000203760ustar00rootroot00000000000000django-mptt-0.13.2/tests/myapp/admin.py000066400000000000000000000004251411214034000177420ustar00rootroot00000000000000from django.contrib import admin from myapp.models import Category, Person from mptt.admin import DraggableMPTTAdmin, MPTTModelAdmin class CategoryAdmin(MPTTModelAdmin): pass admin.site.register(Category, CategoryAdmin) admin.site.register(Person, DraggableMPTTAdmin) django-mptt-0.13.2/tests/myapp/doctests.txt000066400000000000000000001207141411214034000206750ustar00rootroot00000000000000>>> from datetime import date >>> from django.db import connection >>> from mptt.exceptions import InvalidMove >>> from myapp.models import Genre, Insert, MultiOrder, Node, OrderedInsertion, Tree, Person, Student >>> def print_tree_details(nodes): ... opts = nodes[0]._mptt_meta ... print('\n'.join(['%s %s %s %s %s %s' % \ ... (n.pk, getattr(n, '%s_id' % opts.parent_attr) or '-', ... getattr(n, opts.tree_id_attr), getattr(n, opts.level_attr), ... getattr(n, opts.left_attr), getattr(n, opts.right_attr)) \ ... for n in nodes])) >>> def reset_sequence(model): ... # This fixes test failures. For a strange reason, this snippet is ... # only necessary with PY3. Also, do not fail if the table does not ... # exist at all for some reason. ... try: ... connection.cursor().execute( ... 'UPDATE sqlite_sequence SET seq=0 WHERE name=%s', ... [model._meta.db_table]) ... except: ... pass # Creation #################################################################### # creation with a given ID >>> action = Genre.objects.create(pk=1, name='Action') >>> platformer = Genre.objects.create(name='Platformer', parent=action) >>> platformer_2d = Genre.objects.create(name='2D Platformer', parent=platformer) >>> platformer = Genre.objects.get(pk=platformer.pk) >>> platformer_3d = Genre.objects.create(name='3D Platformer', parent=platformer) >>> platformer = Genre.objects.get(pk=platformer.pk) >>> platformer_4d = Genre.objects.create(name='4D Platformer', parent=platformer) >>> rpg = Genre.objects.create(name='Role-playing Game') >>> arpg = Genre.objects.create(name='Action RPG', parent=rpg) >>> rpg = Genre.objects.get(pk=rpg.pk) >>> trpg = Genre.objects.create(name='Tactical RPG', parent=rpg) >>> print_tree_details(Genre.objects.all()) 1 - 1 0 1 10 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 6 - 2 0 1 6 7 6 2 1 2 3 8 6 2 1 4 5 # Utilities ################################################################### >>> from mptt.utils import previous_current_next, tree_item_iterator, drilldown_tree_for_node >>> for p,c,n in previous_current_next(Genre.objects.all()): ... print((p,c,n)) (None, , ) (, , ) (, , ) (, , ) (, , ) (, , ) (, , ) (, , None) >>> for i,s in tree_item_iterator(Genre.objects.all()): ... print((i, s['new_level'], list(s['closed_levels']))) (, True, []) (, True, []) (, True, []) (, False, []) (, False, [2, 1]) (, False, []) (, True, []) (, False, [1, 0]) >>> for i,s in tree_item_iterator(rpg.get_descendants()): ... print((i, s['new_level'], list(s['closed_levels']))) (, True, []) (, False, [1]) >>> for i,s in tree_item_iterator(trpg.get_descendants()): ... print((i, s['new_level'], list(s['closed_levels']))) >>> for i,s in tree_item_iterator(Genre.objects.all(), ancestors=True): ... print((i, s['new_level'], s['ancestors'], list(s['closed_levels']))) (, True, [], []) (, True, ['Action'], []) (, True, ['Action', 'Platformer'], []) (, False, ['Action', 'Platformer'], []) (, False, ['Action', 'Platformer'], [2, 1]) (, False, [], []) (, True, ['Role-playing Game'], []) (, False, ['Role-playing Game'], [1, 0]) >>> for i,s in tree_item_iterator(Genre.objects.all(), ancestors=True, callback=lambda x: x): ... print((i, s['new_level'], s['ancestors'], list(s['closed_levels']))) (, True, [], []) (, True, [], []) (, True, [, ], []) (, False, [, ], []) (, False, [, ], [2, 1]) (, False, [], []) (, True, [], []) (, False, [], [1, 0]) >>> action = Genre.objects.get(pk=action.pk) >>> [item.name for item in drilldown_tree_for_node(action)] ['Action', 'Platformer'] >>> platformer = Genre.objects.get(pk=platformer.pk) >>> [item.name for item in drilldown_tree_for_node(platformer)] ['Action', 'Platformer', '2D Platformer', '3D Platformer', '4D Platformer'] >>> platformer_3d = Genre.objects.get(pk=platformer_3d.pk) >>> [item.name for item in drilldown_tree_for_node(platformer_3d)] ['Action', 'Platformer', '3D Platformer'] # TreeManager Methods ######################################################### >>> Genre.objects.root_node(action.tree_id) >>> Genre.objects.root_node(rpg.tree_id) >>> Genre.objects.root_node(3) Traceback (most recent call last): ... DoesNotExist: Genre matching query does not exist. >>> [g.name for g in Genre.objects.root_nodes()] ['Action', '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()] ['Platformer'] >>> [g.name for g in action.get_descendants()] ['Platformer', '2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in action.get_descendants(include_self=True)] ['Action', 'Platformer', '2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in action.get_leafnodes()] ['2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in action.get_leafnodes(include_self=False)] ['2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in action.get_leafnodes(include_self=True)] ['2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in platformer.get_family()] ['Action', 'Platformer', '2D Platformer', '3D Platformer', '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()] ['Role-playing Game'] >>> [g.name for g in action.get_siblings(include_self=True)] ['Action', '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()] ['Action'] >>> [g.name for g in platformer.get_ancestors(ascending=True)] ['Action'] >>> [g.name for g in platformer.get_children()] ['2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in platformer.get_descendants()] ['2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in platformer.get_descendants(include_self=True)] ['Platformer', '2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in platformer.get_leafnodes()] ['2D Platformer', '3D Platformer', '4D Platformer'] >>> [g.name for g in platformer.get_leafnodes(include_self=True)] ['2D Platformer', '3D Platformer', '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)] ['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()] ['Action', 'Platformer'] >>> [g.name for g in platformer_3d.get_ancestors(ascending=True)] ['Platformer', '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)] ['3D Platformer'] >>> [g.name for g in platformer_3d.get_leafnodes()] [] >>> [g.name for g in platformer_3d.get_leafnodes(include_self=True)] ['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()] ['2D Platformer', '4D Platformer'] >>> [g.name for g in platformer_3d.get_siblings(include_self=True)] ['2D Platformer', '3D Platformer', '4D Platformer'] >>> platformer_3d.is_root_node() False >>> platformer_3d.is_child_node() True >>> platformer_3d.is_leaf_node() True >>> action.is_descendant_of(platformer_3d) False >>> action.is_descendant_of(platformer_3d, include_self=True) False >>> action.is_ancestor_of(platformer_3d) True >>> action.is_ancestor_of(platformer_3d, include_self=True) True >>> platformer_3d.is_descendant_of(platformer_3d) False >>> platformer_3d.is_descendant_of(platformer_3d, include_self=True) True >>> platformer_3d.is_ancestor_of(platformer_3d) False >>> platformer_3d.is_ancestor_of(platformer_3d, include_self=True) True # The move_to method will be used in other tests to verify that it calls the # TreeManager correctly. ####################### # Intra-Tree Movement # ####################### >>> root = Node.objects.create() >>> c_1 = Node.objects.create(parent=root) >>> c_1_1 = Node.objects.create(parent=c_1) >>> c_1 = Node.objects.get(pk=c_1.pk) >>> c_1_2 = Node.objects.create(parent=c_1) >>> root = Node.objects.get(pk=root.pk) >>> c_2 = Node.objects.create(parent=root) >>> c_2_1 = Node.objects.create(parent=c_2) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_2_2 = Node.objects.create(parent=c_2) >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Validate exceptions are raised appropriately >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(root, root, position='first-child') Traceback (most recent call last): ... InvalidMove: A node may not be made a child of itself. >>> c_1 = Node.objects.get(pk=c_1.pk) >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> Node.objects.move_node(c_1, c_1_1, position='last-child') Traceback (most recent call last): ... InvalidMove: A node may not be made a child of any of its descendants. >>> Node.objects.move_node(root, root, position='right') Traceback (most recent call last): ... InvalidMove: A node may not be made a sibling of itself. >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1, c_1_1, position='left') Traceback (most recent call last): ... InvalidMove: A node may not be made a sibling of any of its descendants. >>> Node.objects.move_node(c_1, c_2, position='cheese') Traceback (most recent call last): ... ValueError: An invalid position was given: cheese. # Move up the tree using first-child >>> c_2_2 = Node.objects.get(pk=c_2_2.pk) >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_2_2, c_1, 'first-child') >>> print_tree_details([c_2_2]) 7 2 1 2 3 4 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 9 7 2 1 2 3 4 3 2 1 2 5 6 4 2 1 2 7 8 5 1 1 1 10 13 6 5 1 2 11 12 # Undo the move using right >>> c_2_1 = Node.objects.get(pk=c_2_1.pk) >>> c_2_2.move_to(c_2_1, 'right') >>> print_tree_details([c_2_2]) 7 5 1 2 11 12 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Move up the tree with descendants using first-child >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_2, c_1, 'first-child') >>> print_tree_details([c_2]) 5 2 1 2 3 8 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 13 5 2 1 2 3 8 6 5 1 3 4 5 7 5 1 3 6 7 3 2 1 2 9 10 4 2 1 2 11 12 # Undo the move using right >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_2, c_1, 'right') >>> print_tree_details([c_2]) 5 1 1 1 8 13 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 COVERAGE | U1 | U> | D1 | D> ------------+----+----+----+---- first-child | Y | Y | | last-child | | | | left | | | | right | | | Y | Y # Move down the tree using first-child >>> c_1_2 = Node.objects.get(pk=c_1_2.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1_2, c_2, 'first-child') >>> print_tree_details([c_1_2]) 4 5 1 2 7 8 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 5 3 2 1 2 3 4 5 1 1 1 6 13 4 5 1 2 7 8 6 5 1 2 9 10 7 5 1 2 11 12 # Undo the move using last-child >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_1_2, c_1, 'last-child') >>> print_tree_details([c_1_2]) 4 2 1 2 5 6 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Move down the tree with descendants using first-child >>> c_1 = Node.objects.get(pk=c_1.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1, c_2, 'first-child') >>> print_tree_details([c_1]) 2 5 1 2 3 8 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 5 1 1 1 2 13 2 5 1 2 3 8 3 2 1 3 4 5 4 2 1 3 6 7 6 5 1 2 9 10 7 5 1 2 11 12 # Undo the move using left >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1, c_2, 'left') >>> print_tree_details([c_1]) 2 1 1 1 2 7 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 COVERAGE | U1 | U> | D1 | D> ------------+----+----+----+---- first-child | Y | Y | Y | Y last-child | Y | | | left | | Y | | right | | | Y | Y # Move up the tree using right >>> c_2_2 = Node.objects.get(pk=c_2_2.pk) >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> Node.objects.move_node(c_2_2, c_1_1, 'right') >>> print_tree_details([c_2_2]) 7 2 1 2 5 6 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 9 3 2 1 2 3 4 7 2 1 2 5 6 4 2 1 2 7 8 5 1 1 1 10 13 6 5 1 2 11 12 # Undo the move using last-child >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_2_2, c_2, 'last-child') >>> print_tree_details([c_2_2]) 7 5 1 2 11 12 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Move up the tree with descendants using right >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> Node.objects.move_node(c_2, c_1_1, 'right') >>> print_tree_details([c_2]) 5 2 1 2 5 10 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 13 3 2 1 2 3 4 5 2 1 2 5 10 6 5 1 3 6 7 7 5 1 3 8 9 4 2 1 2 11 12 # Undo the move using last-child >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(c_2, root, 'last-child') >>> print_tree_details([c_2]) 5 1 1 1 8 13 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 COVERAGE | U1 | U> | D1 | D> ------------+----+----+----+---- first-child | Y | Y | Y | Y last-child | Y | | Y | Y left | | Y | | right | Y | Y | Y | Y # Move down the tree with descendants using left >>> c_1 = Node.objects.get(pk=c_1.pk) >>> c_2_2 = Node.objects.get(pk=c_2_2.pk) >>> Node.objects.move_node(c_1, c_2_2, 'left') >>> print_tree_details([c_1]) 2 5 1 2 5 10 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 5 1 1 1 2 13 6 5 1 2 3 4 2 5 1 2 5 10 3 2 1 3 6 7 4 2 1 3 8 9 7 5 1 2 11 12 # Undo the move using first-child >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(c_1, root, 'first-child') >>> print_tree_details([c_1]) 2 1 1 1 2 7 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 # Move down the tree using left >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> c_2_2 = Node.objects.get(pk=c_2_2.pk) >>> Node.objects.move_node(c_1_1, c_2_2, 'left') >>> print_tree_details([c_1_1]) 3 5 1 2 9 10 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 5 4 2 1 2 3 4 5 1 1 1 6 13 6 5 1 2 7 8 3 5 1 2 9 10 7 5 1 2 11 12 # Undo the move using left >>> c_1_2 = Node.objects.get(pk=c_1_2.pk) >>> Node.objects.move_node(c_1_1, c_1_2, 'left') >>> print_tree_details([c_1_1]) 3 2 1 2 3 4 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 COVERAGE | U1 | U> | D1 | D> ------------+----+----+----+---- first-child | Y | Y | Y | Y last-child | Y | Y | Y | Y left | Y | Y | Y | Y right | Y | Y | Y | Y I guess we're covered :) ####################### # Inter-Tree Movement # ####################### >>> new_root = Node.objects.create() >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 8 - 2 0 1 2 # Moving child nodes between trees ############################################ # Move using default (last-child) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_2.move_to(new_root) >>> print_tree_details([c_2]) 5 8 2 1 2 7 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 8 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 8 - 2 0 1 8 5 8 2 1 2 7 6 5 2 2 3 4 7 5 2 2 5 6 # Move using left >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1_1, c_2, position='left') >>> print_tree_details([c_1_1]) 3 8 2 1 2 3 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 4 2 1 2 3 4 8 - 2 0 1 10 3 8 2 1 2 3 5 8 2 1 4 9 6 5 2 2 5 6 7 5 2 2 7 8 # Move using first-child >>> c_1_2 = Node.objects.get(pk=c_1_2.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> Node.objects.move_node(c_1_2, c_2, position='first-child') >>> print_tree_details([c_1_2]) 4 5 2 2 5 6 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 8 - 2 0 1 12 3 8 2 1 2 3 5 8 2 1 4 11 4 5 2 2 5 6 6 5 2 2 7 8 7 5 2 2 9 10 # Move using right >>> c_2 = Node.objects.get(pk=c_2.pk) >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(c_2, c_1, position='right') >>> print_tree_details([c_2]) 5 1 1 1 4 11 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 12 2 1 1 1 2 3 5 1 1 1 4 11 4 5 1 2 5 6 6 5 1 2 7 8 7 5 1 2 9 10 8 - 2 0 1 4 3 8 2 1 2 3 # Move using last-child >>> c_1_1 = Node.objects.get(pk=c_1_1.pk) >>> Node.objects.move_node(c_1_1, c_2, position='last-child') >>> print_tree_details([c_1_1]) 3 5 1 2 11 12 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 14 2 1 1 1 2 3 5 1 1 1 4 13 4 5 1 2 5 6 6 5 1 2 7 8 7 5 1 2 9 10 3 5 1 2 11 12 8 - 2 0 1 2 # Moving a root node into another tree as a child node ######################## # Validate exceptions are raised appropriately >>> Node.objects.move_node(root, c_1, position='first-child') Traceback (most recent call last): ... InvalidMove: A node may not be made a child of any of its descendants. >>> Node.objects.move_node(new_root, c_1, position='cheese') Traceback (most recent call last): ... ValueError: An invalid position was given: cheese. >>> new_root = Node.objects.get(pk=new_root.pk) >>> c_2 = Node.objects.get(pk=c_2.pk) >>> new_root.move_to(c_2, position='first-child') >>> print_tree_details([new_root]) 8 5 1 2 5 6 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 16 2 1 1 1 2 3 5 1 1 1 4 15 8 5 1 2 5 6 4 5 1 2 7 8 6 5 1 2 9 10 7 5 1 2 11 12 3 5 1 2 13 14 >>> new_root = Node.objects.create() >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(new_root, root, position='last-child') >>> print_tree_details([new_root]) 9 1 1 1 16 17 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 18 2 1 1 1 2 3 5 1 1 1 4 15 8 5 1 2 5 6 4 5 1 2 7 8 6 5 1 2 9 10 7 5 1 2 11 12 3 5 1 2 13 14 9 1 1 1 16 17 >>> new_root = Node.objects.create() >>> c_2_1 = Node.objects.get(pk=c_2_1.pk) >>> Node.objects.move_node(new_root, c_2_1, position='left') >>> print_tree_details([new_root]) 10 5 1 2 9 10 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 20 2 1 1 1 2 3 5 1 1 1 4 17 8 5 1 2 5 6 4 5 1 2 7 8 10 5 1 2 9 10 6 5 1 2 11 12 7 5 1 2 13 14 3 5 1 2 15 16 9 1 1 1 18 19 >>> new_root = Node.objects.create() >>> c_1 = Node.objects.get(pk=c_1.pk) >>> Node.objects.move_node(new_root, c_1, position='right') >>> print_tree_details([new_root]) 11 1 1 1 4 5 >>> print_tree_details(Node.objects.all()) 1 - 1 0 1 22 2 1 1 1 2 3 11 1 1 1 4 5 5 1 1 1 6 19 8 5 1 2 7 8 4 5 1 2 9 10 10 5 1 2 11 12 6 5 1 2 13 14 7 5 1 2 15 16 3 5 1 2 17 18 9 1 1 1 20 21 # Making nodes siblings of root nodes ######################################### # Validate exceptions are raised appropriately >>> root = Node.objects.get(pk=root.pk) >>> Node.objects.move_node(root, root, position='left') Traceback (most recent call last): ... InvalidMove: A node may not be made a sibling of itself. >>> Node.objects.move_node(root, root, position='right') Traceback (most recent call last): ... InvalidMove: A node may not be made a sibling of itself. >>> r1 = Tree.objects.create() >>> c1_1 = Tree.objects.create(parent=r1) >>> c1_1_1 = Tree.objects.create(parent=c1_1) >>> r2 = Tree.objects.create() >>> c2_1 = Tree.objects.create(parent=r2) >>> c2_1_1 = Tree.objects.create(parent=c2_1) >>> r3 = Tree.objects.create() >>> c3_1 = Tree.objects.create(parent=r3) >>> c3_1_1 = Tree.objects.create(parent=c3_1) >>> print_tree_details(Tree.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 7 - 3 0 1 6 8 7 3 1 2 5 9 8 3 2 3 4 # Target < root node, left sibling >>> r1 = Tree.objects.get(pk=r1.pk) >>> r2 = Tree.objects.get(pk=r2.pk) >>> r2.move_to(r1, 'left') >>> print_tree_details([r2]) 4 - 1 0 1 6 >>> print_tree_details(Tree.objects.all()) 4 - 1 0 1 6 5 4 1 1 2 5 6 5 1 2 3 4 1 - 2 0 1 6 2 1 2 1 2 5 3 2 2 2 3 4 7 - 3 0 1 6 8 7 3 1 2 5 9 8 3 2 3 4 # Target > root node, left sibling >>> r3 = Tree.objects.get(pk=r3.pk) >>> r2.move_to(r3, 'left') >>> print_tree_details([r2]) 4 - 2 0 1 6 >>> print_tree_details(Tree.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 7 - 3 0 1 6 8 7 3 1 2 5 9 8 3 2 3 4 # Target < root node, right sibling >>> r1 = Tree.objects.get(pk=r1.pk) >>> r3 = Tree.objects.get(pk=r3.pk) >>> r3.move_to(r1, 'right') >>> print_tree_details([r3]) 7 - 2 0 1 6 >>> print_tree_details(Tree.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 7 - 2 0 1 6 8 7 2 1 2 5 9 8 2 2 3 4 4 - 3 0 1 6 5 4 3 1 2 5 6 5 3 2 3 4 # Target > root node, right sibling >>> r1 = Tree.objects.get(pk=r1.pk) >>> r2 = Tree.objects.get(pk=r2.pk) >>> r1.move_to(r2, 'right') >>> print_tree_details([r1]) 1 - 3 0 1 6 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 6 8 7 1 1 2 5 9 8 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 1 - 3 0 1 6 2 1 3 1 2 5 3 2 3 2 3 4 # No-op, root left sibling >>> r2 = Tree.objects.get(pk=r2.pk) >>> r2.move_to(r1, 'left') >>> print_tree_details([r2]) 4 - 2 0 1 6 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 6 8 7 1 1 2 5 9 8 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 1 - 3 0 1 6 2 1 3 1 2 5 3 2 3 2 3 4 # No-op, root right sibling >>> r1.move_to(r2, 'right') >>> print_tree_details([r1]) 1 - 3 0 1 6 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 6 8 7 1 1 2 5 9 8 1 2 3 4 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 1 - 3 0 1 6 2 1 3 1 2 5 3 2 3 2 3 4 # Child node, left sibling >>> c3_1 = Tree.objects.get(pk=c3_1.pk) >>> c3_1.move_to(r1, 'left') >>> print_tree_details([c3_1]) 8 - 3 0 1 4 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 2 4 - 2 0 1 6 5 4 2 1 2 5 6 5 2 2 3 4 8 - 3 0 1 4 9 8 3 1 2 3 1 - 4 0 1 6 2 1 4 1 2 5 3 2 4 2 3 4 # Child node, right sibling >>> r3 = Tree.objects.get(pk=r3.pk) >>> c1_1 = Tree.objects.get(pk=c1_1.pk) >>> c1_1.move_to(r3, 'right') >>> print_tree_details([c1_1]) 2 - 2 0 1 4 >>> print_tree_details(Tree.objects.all()) 7 - 1 0 1 2 2 - 2 0 1 4 3 2 2 1 2 3 4 - 3 0 1 6 5 4 3 1 2 5 6 5 3 2 3 4 8 - 4 0 1 4 9 8 4 1 2 3 1 - 5 0 1 2 # Insertion of positioned nodes ############################################### >>> r1 = Insert.objects.create() >>> r2 = Insert.objects.create() >>> r3 = Insert.objects.create() >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 2 3 - 3 0 1 2 >>> r2 = Insert.objects.get(pk=r2.pk) >>> c1 = Insert() >>> c1 = Insert.objects.insert_node(c1, r2, save=True) >>> print_tree_details([c1]) 4 2 2 1 2 3 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 4 4 2 2 1 2 3 3 - 3 0 1 2 >>> c1.insert_at(r2) Traceback (most recent call last): ... ValueError: Cannot insert a node which has already been saved. # First child >>> r2 = Insert.objects.get(pk=r2.pk) >>> c2 = Insert() >>> c2 = Insert.objects.insert_node(c2, r2, position='first-child', save=True) >>> print_tree_details([c2]) 5 2 2 1 2 3 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 6 5 2 2 1 2 3 4 2 2 1 4 5 3 - 3 0 1 2 # Left >>> c1 = Insert.objects.get(pk=c1.pk) >>> c3 = Insert() >>> c3 = Insert.objects.insert_node(c3, c1, position='left', save=True) >>> print_tree_details([c3]) 6 2 2 1 4 5 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 8 5 2 2 1 2 3 6 2 2 1 4 5 4 2 2 1 6 7 3 - 3 0 1 2 # Right >>> c4 = Insert() >>> c4 = Insert.objects.insert_node(c4, c3, position='right', save=True) >>> print_tree_details([c4]) 7 2 2 1 6 7 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 10 5 2 2 1 2 3 6 2 2 1 4 5 7 2 2 1 6 7 4 2 2 1 8 9 3 - 3 0 1 2 # Last child >>> r2 = Insert.objects.get(pk=r2.pk) >>> c5 = Insert() >>> c5 = Insert.objects.insert_node(c5, r2, position='last-child', save=True) >>> print_tree_details([c5]) 8 2 2 1 10 11 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 12 5 2 2 1 2 3 6 2 2 1 4 5 7 2 2 1 6 7 4 2 2 1 8 9 8 2 2 1 10 11 3 - 3 0 1 2 # Left sibling of root >>> r2 = Insert.objects.get(pk=r2.pk) >>> r4 = Insert() >>> r4 = Insert.objects.insert_node(r4, r2, position='left', save=True) >>> print_tree_details([r4]) 9 - 2 0 1 2 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 9 - 2 0 1 2 2 - 3 0 1 12 5 2 3 1 2 3 6 2 3 1 4 5 7 2 3 1 6 7 4 2 3 1 8 9 8 2 3 1 10 11 3 - 4 0 1 2 # Right sibling of root >>> r2 = Insert.objects.get(pk=r2.pk) >>> r5 = Insert() >>> r5 = Insert.objects.insert_node(r5, r2, position='right', save=True) >>> print_tree_details([r5]) 10 - 4 0 1 2 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 9 - 2 0 1 2 2 - 3 0 1 12 5 2 3 1 2 3 6 2 3 1 4 5 7 2 3 1 6 7 4 2 3 1 8 9 8 2 3 1 10 11 10 - 4 0 1 2 3 - 5 0 1 2 # Last root >>> r6 = Insert() >>> r6 = Insert.objects.insert_node(r6, None, save=True) >>> print_tree_details([r6]) 11 - 6 0 1 2 >>> print_tree_details(Insert.objects.all()) 1 - 1 0 1 2 9 - 2 0 1 2 2 - 3 0 1 12 5 2 3 1 2 3 6 2 3 1 4 5 7 2 3 1 6 7 4 2 3 1 8 9 8 2 3 1 10 11 10 - 4 0 1 2 3 - 5 0 1 2 11 - 6 0 1 2 # order_insertion_by with single criterion #################################### >>> r1 = OrderedInsertion.objects.create(name='games') # Root ordering >>> r2 = OrderedInsertion.objects.create(name='food') >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 # Same name - insert after >>> r3 = OrderedInsertion.objects.create(name='food') >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 2 1 - 3 0 1 2 >>> c1 = OrderedInsertion.objects.create(name='zoo', parent=r3) >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 4 4 3 2 1 2 3 1 - 3 0 1 2 >>> r3 = OrderedInsertion.objects.get(pk=r3.pk) >>> c2 = OrderedInsertion.objects.create(name='monkey', parent=r3) >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 6 5 3 2 1 2 3 4 3 2 1 4 5 1 - 3 0 1 2 >>> r3 = OrderedInsertion.objects.get(pk=r3.pk) >>> c3 = OrderedInsertion.objects.create(name='animal', parent=r3) >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 8 6 3 2 1 2 3 5 3 2 1 4 5 4 3 2 1 6 7 1 - 3 0 1 2 # order_insertion_by reparenting with single criterion ######################## # Root -> child >>> r1 = OrderedInsertion.objects.get(pk=r1.pk) >>> r3 = OrderedInsertion.objects.get(pk=r3.pk) >>> r1.parent = r3 >>> r1.save() >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 3 - 2 0 1 10 6 3 2 1 2 3 1 3 2 1 4 5 5 3 2 1 6 7 4 3 2 1 8 9 # Child -> root >>> c3 = OrderedInsertion.objects.get(pk=c3.pk) >>> c3.parent = None >>> c3.save() >>> print_tree_details(OrderedInsertion.objects.all()) 6 - 1 0 1 2 2 - 2 0 1 2 3 - 3 0 1 8 1 3 3 1 2 3 5 3 3 1 4 5 4 3 3 1 6 7 # Child -> child >>> c1 = OrderedInsertion.objects.get(pk=c1.pk) >>> c1.parent = c3 >>> c1.save() >>> print_tree_details(OrderedInsertion.objects.all()) 6 - 1 0 1 4 4 6 1 1 2 3 2 - 2 0 1 2 3 - 3 0 1 6 1 3 3 1 2 3 5 3 3 1 4 5 >>> c3 = OrderedInsertion.objects.get(pk=c3.pk) >>> c2 = OrderedInsertion.objects.get(pk=c2.pk) >>> c2.parent = c3 >>> c2.save() >>> print_tree_details(OrderedInsertion.objects.all()) 6 - 1 0 1 6 5 6 1 1 2 3 4 6 1 1 4 5 2 - 2 0 1 2 3 - 3 0 1 4 1 3 3 1 2 3 # reordering after save, original insertion in order ########################## # sibling root nodes, inserted B,C, moved C-->A >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> c = OrderedInsertion.objects.create(name='c') >>> c.name = 'a' >>> c.save() >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 # sibling non-root nodes, inserted B,C, moved C-->A >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> c = OrderedInsertion.objects.create(name='c', parent=root) >>> c.name = 'a' >>> c.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 # sibling root nodes, inserted A,B, moved A-->C >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> a = OrderedInsertion.objects.create(name='a') >>> b = OrderedInsertion.objects.create(name='b') >>> a = OrderedInsertion.objects.get(name='a') >>> a.name = 'c' >>> a.save() >>> print_tree_details(OrderedInsertion.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 # sibling non-root nodes, inserted A,B, moved A-->C >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> a = OrderedInsertion.objects.create(name='a', parent=root) >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> a = OrderedInsertion.objects.get(name='a') >>> a.name = 'c' >>> a.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 # reordering after save, original insertion in reverse ######################## # sibling root nodes, inserted C,B, moved C-->A >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> c = OrderedInsertion.objects.create(name='c') >>> b = OrderedInsertion.objects.create(name='b') >>> c = OrderedInsertion.objects.get(name='c') >>> c.name = 'a' >>> c.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 2 # sibling non-root nodes, inserted C,B, moved C-->A >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> c = OrderedInsertion.objects.create(name='c', parent=root) >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> c = OrderedInsertion.objects.get(name='c') >>> c.name = 'a' >>> c.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 # sibling root nodes, inserted B,A, moved A-->C >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> a = OrderedInsertion.objects.create(name='a') >>> a.name = 'c' >>> a.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 2 2 - 2 0 1 2 # sibling non-root nodes, inserted B,A, moved A-->C >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> a = OrderedInsertion.objects.create(name='a', parent=root) >>> a.name = 'c' >>> a.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 # reordering after save, when only a single instance at level exists ########## # root node with no siblings no children, inserted B, moved B-->A (forward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'a' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 2 # root node with no siblings with children, inserted B,child, moved B-->A (forward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> child = OrderedInsertion.objects.create(name='child', parent=b) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'a' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 # parented node with no siblings no children, inserted root,B, moved B-->A (forward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'a' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 # parented node with no siblings with children, inserted root,B,C, moved B-->A (forward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> c = OrderedInsertion.objects.create(name='c', parent=b) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'a' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 # root node with no siblings no children, inserted B, moved B-->C (backward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'c' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 2 # root node with no siblings with children, inserted B,child, moved B-->C (backward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> b = OrderedInsertion.objects.create(name='b') >>> child = OrderedInsertion.objects.create(name='child', parent=b) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'c' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 # parented node with no siblings no children, inserted root,B, moved B-->C (backward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'c' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 4 2 1 1 1 2 3 # parented node with no siblings with children, inserted root,B,D, moved B-->C (backward) >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> b = OrderedInsertion.objects.create(name='b', parent=root) >>> d = OrderedInsertion.objects.create(name='d', parent=b) >>> b = OrderedInsertion.objects.get(name='b') >>> b.name = 'c' >>> b.save() >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 >>> _ = OrderedInsertion.objects.all().delete() >>> reset_sequence(OrderedInsertion) >>> root = OrderedInsertion.objects.create(name='root') >>> r1 = OrderedInsertion.objects.create(name='r1', parent=root) >>> r2 = OrderedInsertion.objects.create(name='r2', parent=root) >>> print_tree_details(OrderedInsertion.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 # Insertion of positioned nodes, multiple ordering criteria ################### >>> r1 = MultiOrder.objects.create(name='fff', size=20, date=date(2008, 1, 1)) # Root nodes - ordering by subsequent fields >>> r2 = MultiOrder.objects.create(name='fff', size=10, date=date(2009, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 >>> r3 = MultiOrder.objects.create(name='fff', size=20, date=date(2007, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 3 - 3 0 1 2 >>> r4 = MultiOrder.objects.create(name='fff', size=20, date=date(2008, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 4 - 3 0 1 2 3 - 4 0 1 2 >>> r5 = MultiOrder.objects.create(name='fff', size=20, date=date(2007, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 2 - 1 0 1 2 1 - 2 0 1 2 4 - 3 0 1 2 3 - 4 0 1 2 5 - 5 0 1 2 >>> r6 = MultiOrder.objects.create(name='aaa', size=999, date=date(2010, 1, 1)) >>> print_tree_details(MultiOrder.objects.all()) 6 - 1 0 1 2 2 - 2 0 1 2 1 - 3 0 1 2 4 - 4 0 1 2 3 - 5 0 1 2 5 - 6 0 1 2 # Child nodes >>> r1 = MultiOrder.objects.get(pk=r1.pk) >>> c1 = MultiOrder.objects.create(parent=r1, name='hhh', size=10, date=date(2009, 1, 1)) >>> print_tree_details(MultiOrder.objects.filter(tree_id=r1.tree_id)) 1 - 3 0 1 4 7 1 3 1 2 3 >>> r1 = MultiOrder.objects.get(pk=r1.pk) >>> c2 = MultiOrder.objects.create(parent=r1, name='hhh', size=20, date=date(2008, 1, 1)) >>> print_tree_details(MultiOrder.objects.filter(tree_id=r1.tree_id)) 1 - 3 0 1 6 7 1 3 1 2 3 8 1 3 1 4 5 >>> r1 = MultiOrder.objects.get(pk=r1.pk) >>> c3 = MultiOrder.objects.create(parent=r1, name='hhh', size=15, date=date(2008, 1, 1)) >>> print_tree_details(MultiOrder.objects.filter(tree_id=r1.tree_id)) 1 - 3 0 1 8 7 1 3 1 2 3 9 1 3 1 4 5 8 1 3 1 6 7 >>> r1 = MultiOrder.objects.get(pk=r1.pk) >>> c4 = MultiOrder.objects.create(parent=r1, name='hhh', size=15, date=date(2008, 1, 1)) >>> print_tree_details(MultiOrder.objects.filter(tree_id=r1.tree_id)) 1 - 3 0 1 10 7 1 3 1 2 3 9 1 3 1 4 5 10 1 3 1 6 7 8 1 3 1 8 9 # Multi-table Inheritance ##################################################### (pk, parent_id or '-', tree_id, level, left, right_attr) # Create test model instances >>> jack = Person.objects.create(name='Jack') >>> jill = Student.objects.create(name='Jill', parent=jack) >>> jim = Person.objects.create(name='Jim', parent=jack) >>> jess = Student.objects.create(name='Jess') >>> jeff = Person.objects.create(name='Jeff', parent=jess) >>> jane = Student.objects.create(name='Jane', parent=jeff) >>> joe = Person.objects.create(name='Joe', parent=jane) >>> julie = Student.objects.create(name='Julie', parent=jess) >>> print_tree_details(Person.objects.all()) 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 10 5 4 2 1 2 7 6 5 2 2 3 6 7 6 2 3 4 5 8 4 2 1 8 9 >>> print_tree_details(Student.objects.all()) 2 1 1 1 2 3 4 - 2 0 1 10 6 5 2 2 3 6 8 4 2 1 8 9 >>> jack = Person.objects.get(pk=1) >>> list(jack.get_descendants()) [, ] >>> jack.get_root() >>> jill = Person.objects.get(pk=3) >>> jill.get_root() >>> jess = Student.objects.get(pk=4) >>> list(jess.get_descendants()) [, , , ] >>> jess.get_root() >>> jeff = Person.objects.get(pk=5) >>> list(jeff.get_descendants()) [, ] >>> jeff.get_root() >>> jane = Student.objects.get(pk=6) >>> jane.get_root() >>> list(jane.get_ancestors()) [, ] django-mptt-0.13.2/tests/myapp/fixtures/000077500000000000000000000000001411214034000201505ustar00rootroot00000000000000django-mptt-0.13.2/tests/myapp/fixtures/categories.json000066400000000000000000000052521411214034000231740ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.category", "fields": { "rght": 20, "name": "PC & Video Games", "category_uuid": "6263ac21-f08b-4b44-9462-0489c56e0d3d", "parent": null, "level": 0, "lft": 1, "tree_id": 1 } }, { "pk": 2, "model": "myapp.category", "fields": { "rght": 7, "name": "Nintendo Wii", "category_uuid": "c6125cec-ef08-4095-9a07-fd5c6db50cc1", "parent": 1, "level": 1, "lft": 2, "tree_id": 1 } }, { "pk": 3, "model": "myapp.category", "fields": { "rght": 4, "name": "Games", "category_uuid": "85da870c-837a-48a5-a3e8-ab651cfdd799", "parent": 2, "level": 2, "lft": 3, "tree_id": 1 } }, { "pk": 4, "model": "myapp.category", "fields": { "rght": 6, "name": "Hardware & Accessories", "category_uuid": "840d1ccb-9ecc-48b0-bf7c-5b0f08334d3d", "parent": 2, "level": 2, "lft": 5, "tree_id": 1 } }, { "pk": 5, "model": "myapp.category", "fields": { "rght": 13, "name": "Xbox 360", "category_uuid": "b6299e26-d5e9-4dc2-9e2a-3f8033c8dfe5", "parent": 1, "level": 1, "lft": 8, "tree_id": 1 } }, { "pk": 6, "model": "myapp.category", "fields": { "rght": 10, "name": "Games", "category_uuid": "8171b0ec-f6e6-46dc-aabc-46a6399d9290", "parent": 5, "level": 2, "lft": 9, "tree_id": 1 } }, { "pk": 7, "model": "myapp.category", "fields": { "rght": 12, "name": "Hardware & Accessories", "category_uuid": "a64720cb-af69-440a-9abc-5a8f095974e4", "parent": 5, "level": 2, "lft": 11, "tree_id": 1 } }, { "pk": 8, "model": "myapp.category", "fields": { "rght": 19, "name": "PlayStation 3", "category_uuid": "01cac717-454e-48bd-8180-8a0089613bbd", "parent": 1, "level": 1, "lft": 14, "tree_id": 1 } }, { "pk": 9, "model": "myapp.category", "fields": { "rght": 16, "name": "Games", "category_uuid": "74f9c250-6e75-4152-bbc9-f7a89969c82b", "parent": 8, "level": 2, "lft": 15, "tree_id": 1 } }, { "pk": 10, "model": "myapp.category", "fields": { "rght": 18, "name": "Hardware & Accessories", "category_uuid": "a4918038-4d03-48ca-8cc3-1c28e177bed5", "parent": 8, "level": 2, "lft": 17, "tree_id": 1 } } ] django-mptt-0.13.2/tests/myapp/fixtures/genres.json000066400000000000000000000043521411214034000223320ustar00rootroot00000000000000[ { "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.13.2/tests/myapp/fixtures/items.json000066400000000000000000000012071411214034000221640ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.item", "fields": { "name": "FIFA 15", "category_fk": "b6299e26-d5e9-4dc2-9e2a-3f8033c8dfe5", "category_pk": 5 } }, { "pk": 2, "model": "myapp.item", "fields": { "name": "Halo: Reach", "category_fk": "b6299e26-d5e9-4dc2-9e2a-3f8033c8dfe5", "category_pk": 5 } }, { "pk": 3, "model": "myapp.item", "fields": { "name": "toplevel item", "category_fk": "6263ac21-f08b-4b44-9462-0489c56e0d3d", "category_pk": 1 } } ] django-mptt-0.13.2/tests/myapp/fixtures/persons.json000066400000000000000000000003031411214034000225300ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.person", "fields": { "rght": 16, "name": "John Doe", "parent": null, "level": 0, "lft": 1, "tree_id": 1 } } ] django-mptt-0.13.2/tests/myapp/fixtures/subitems.json000066400000000000000000000002671411214034000227030ustar00rootroot00000000000000[ { "pk": 1, "model": "myapp.subitem", "fields": { "item": 1 } }, { "pk": 2, "model": "myapp.subitem", "fields": { "item": 3 } } ] django-mptt-0.13.2/tests/myapp/models.py000066400000000000000000000231501411214034000201350ustar00rootroot00000000000000from uuid import uuid4 from django.db import models from django.db.models import Field from django.db.models.query import QuerySet import mptt from mptt.fields import TreeForeignKey, TreeManyToManyField, TreeOneToOneField from mptt.managers import TreeManager from mptt.models import MPTTModel class CustomTreeQueryset(QuerySet): def custom_method(self): pass class CustomTreeManager(TreeManager): def get_queryset(self): return CustomTreeQueryset(model=self.model, using=self._db) class Category(MPTTModel): name = models.CharField(max_length=50) visible = models.BooleanField(default=True) parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) category_uuid = models.CharField(max_length=50, unique=True, null=True) def __str__(self): return self.name def delete(self): super().delete() delete.alters_data = True class Item(models.Model): name = models.CharField(max_length=100) category_fk = models.ForeignKey( "Category", to_field="category_uuid", null=True, related_name="items_by_fk", on_delete=models.CASCADE, ) category_pk = models.ForeignKey( "Category", null=True, related_name="items_by_pk", on_delete=models.CASCADE ) def __str__(self): return self.name class SubItem(models.Model): item = models.ForeignKey(Item, null=True, on_delete=models.CASCADE) class Genre(MPTTModel): name = models.CharField(max_length=50, unique=True) parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) def __str__(self): return self.name class Game(models.Model): genre = TreeForeignKey(Genre, on_delete=models.CASCADE) genres_m2m = models.ManyToManyField(Genre, related_name="games_m2m") name = models.CharField(max_length=50) def __str__(self): return self.name class Insert(MPTTModel): parent = models.ForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class MultiOrder(MPTTModel): name = models.CharField(max_length=50) size = models.PositiveIntegerField() date = models.DateField() parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class MPTTMeta: order_insertion_by = ["name", "size", "-date"] def __str__(self): return self.name class Node(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) # To check that you can set level_attr etc to an existing field. level = models.IntegerField() class MPTTMeta: left_attr = "does" right_attr = "zis" level_attr = "level" tree_id_attr = "work" class UUIDNode(MPTTModel): parent = models.ForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) uuid = models.UUIDField(primary_key=True, default=uuid4) name = models.CharField(max_length=50) def __str__(self): return self.name class OrderedInsertion(MPTTModel): name = models.CharField(max_length=50) parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class MPTTMeta: order_insertion_by = ["name"] def __str__(self): return self.name class Tree(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class NewStyleMPTTMeta(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class MPTTMeta: left_attr = "testing" class Person(MPTTModel): name = models.CharField(max_length=50) parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) # just testing it's actually possible to override the tree manager objects = CustomTreeManager() 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 = TreeForeignKey( "self", null=True, blank=True, related_name="children", db_column="my_cusom_parent", on_delete=models.CASCADE, ) def __str__(self): return self.name class ReferencingModel(models.Model): fk = TreeForeignKey(Category, related_name="+", on_delete=models.CASCADE) one = TreeOneToOneField(Category, related_name="+", on_delete=models.CASCADE) m2m = TreeManyToManyField(Category, related_name="+") # for testing various types of inheritance: # 1. multi-table inheritance, with mptt fields on base class. class MultiTableInheritanceA1(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class MultiTableInheritanceA2(MultiTableInheritanceA1): name = models.CharField(max_length=50) # 2. multi-table inheritance, with mptt fields on child class. class MultiTableInheritanceB1(MPTTModel): name = models.CharField(max_length=50) class MultiTableInheritanceB2(MultiTableInheritanceB1): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) # 3. abstract models class AbstractModel(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) ghosts = models.CharField(max_length=50) class Meta: abstract = True class ConcreteModel(AbstractModel): name = models.CharField(max_length=50) class AbstractConcreteAbstract(ConcreteModel): # abstract --> concrete --> abstract class Meta: abstract = True class ConcreteAbstractConcreteAbstract(ConcreteModel): # concrete --> abstract --> concrete --> abstract pass class ConcreteConcrete(ConcreteModel): # another subclass (concrete this time) of the root concrete model pass # 4. proxy models class SingleProxyModel(ConcreteModel): objects = CustomTreeManager() class Meta: proxy = True class DoubleProxyModel(SingleProxyModel): class Meta: proxy = True # 5. swappable models class SwappableModel(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class Meta: swappable = "MPTT_SWAPPABLE_MODEL" class SwappedInModel(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) name = models.CharField(max_length=50) # Default manager class MultipleManager(TreeManager): def get_queryset(self): return super().get_queryset().exclude(published=False) class MultipleManagerModel(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) published = models.BooleanField() objects = TreeManager() foo_objects = MultipleManager() class AutoNowDateFieldModel(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) now = models.DateTimeField(auto_now_add=True) class MPTTMeta: order_insertion_by = ("now",) # test registering of remote model class Group(models.Model): name = models.CharField(max_length=100) TreeForeignKey( Group, blank=True, null=True, on_delete=models.CASCADE ).contribute_to_class(Group, "parent") mptt.register(Group, order_insertion_by=("name",)) class Book(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) name = models.CharField(max_length=50) fk = TreeForeignKey( Category, null=True, blank=True, related_name="books_fk", on_delete=models.CASCADE, ) m2m = TreeManyToManyField(Category, blank=True, related_name="books_m2m") class UniqueTogetherModel(MPTTModel): class Meta: unique_together = ( ( "parent", "code", ), ) parent = TreeForeignKey("self", null=True, on_delete=models.CASCADE) code = models.CharField(max_length=10) class NullableOrderedInsertionModel(MPTTModel): name = models.CharField(max_length=50, null=True) parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class MPTTMeta: order_insertion_by = ["name"] def __str__(self): return self.name class NullableDescOrderedInsertionModel(MPTTModel): name = models.CharField(max_length=50, null=True) parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) class MPTTMeta: order_insertion_by = ["-name"] def __str__(self): return self.name class FakeNotConcreteField(Field): """Returning None as column results in the field being not concrete""" def get_attname_column(self): return self.name, None class NotConcreteFieldModel(MPTTModel): parent = TreeForeignKey( "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE ) not_concrete_field = FakeNotConcreteField() django-mptt-0.13.2/tests/myapp/test_forms.py000066400000000000000000000207011411214034000210360ustar00rootroot00000000000000from django.forms.models import modelform_factory from myapp.models import Category, Genre, ReferencingModel from myapp.tests import TreeTestCase from mptt.forms import ( MoveNodeForm, MPTTAdminForm, TreeNodeChoiceField, TreeNodeMultipleChoiceField, ) class TestForms(TreeTestCase): fixtures = ["categories.json", "genres.json"] def test_adminform_instantiation(self): # https://github.com/django-mptt/django-mptt/issues/264 c = Category.objects.get(name="Nintendo Wii") CategoryForm = modelform_factory( Category, form=MPTTAdminForm, fields=("name", "parent"), ) self.assertTrue(CategoryForm(instance=c)) # Test that the parent field is properly limited. (queryset) form = CategoryForm( { "name": c.name, "parent": c.children.all()[0].pk, }, instance=c, ) self.assertFalse(form.is_valid()) self.assertIn("Select a valid choice", "%s" % form.errors) # Test that even though we remove the field queryset limit, # validation still fails. form = CategoryForm( { "name": c.name, "parent": c.children.all()[0].pk, }, instance=c, ) form.fields["parent"].queryset = Category.objects.all() self.assertFalse(form.is_valid()) self.assertIn("Invalid parent", "%s" % form.errors) def test_field_types(self): ReferencingModelForm = modelform_factory(ReferencingModel, exclude=("id",)) form = ReferencingModelForm() # Also check whether we have the correct form field type self.assertTrue(isinstance(form.fields["fk"], TreeNodeChoiceField)) self.assertTrue(isinstance(form.fields["one"], TreeNodeChoiceField)) self.assertTrue(isinstance(form.fields["m2m"], TreeNodeMultipleChoiceField)) def test_movenodeform_save(self): c = Category.objects.get(pk=2) form = MoveNodeForm( c, { "target": "5", "position": "first-child", }, ) self.assertTrue(form.is_valid()) form.save() self.assertTreeEqual( Category.objects.all(), """ 1 - 1 0 1 20 5 1 1 1 2 13 2 5 1 2 3 8 3 2 1 3 4 5 4 2 1 3 6 7 6 5 1 2 9 10 7 5 1 2 11 12 8 1 1 1 14 19 9 8 1 2 15 16 10 8 1 2 17 18 """, ) def test_movenodeform(self): self.maxDiff = 2000 form = MoveNodeForm(Genre.objects.get(pk=7)) expected = ( '' '" '' '" ) self.assertHTMLEqual(str(form), expected) form = MoveNodeForm( Genre.objects.get(pk=7), level_indicator="+--", target_select_size=5 ) self.assertIn('size="5"', str(form["target"])) self.assertInHTML( '', str(form["target"]) ) form = MoveNodeForm( Genre.objects.get(pk=7), position_choices=(("left", "left"),) ) self.assertHTMLEqual( str(form["position"]), ( '" ), ) def test_treenodechoicefield(self): field = TreeNodeChoiceField(queryset=Genre.objects.all()) self.assertHTMLEqual( field.widget.render("test", None), '", ) field = TreeNodeChoiceField( queryset=Genre.objects.all(), empty_label="None of the below" ) self.assertInHTML( '', field.widget.render("test", None), ) def test_treenodechoicefield_level_indicator(self): field = TreeNodeChoiceField(queryset=Genre.objects.all(), level_indicator="+--") self.assertHTMLEqual( field.widget.render("test", None), '", ) def test_treenodechoicefield_relative_level(self): top = Genre.objects.get(pk=2) field = TreeNodeChoiceField(queryset=top.get_descendants()) self.assertHTMLEqual( field.widget.render("test", None), '", ) field = TreeNodeChoiceField( queryset=top.get_descendants(include_self=True), start_level=top.level, ) self.assertHTMLEqual( field.widget.render("test", None), '", ) field = TreeNodeChoiceField( queryset=top.get_descendants(), start_level=top.level + 1, ) self.assertHTMLEqual( field.widget.render("test", None), '", ) django-mptt-0.13.2/tests/myapp/tests.py000066400000000000000000003057631411214034000200310ustar00rootroot00000000000000import io import os import re import sys import unittest from django.apps import apps from django.contrib.admin import ModelAdmin, site from django.contrib.admin.views.main import ChangeList from django.contrib.auth.models import Group, User from django.db.models import Q from django.db.models.query_utils import DeferredAttribute from django.template import Context, Template, TemplateSyntaxError from django.test import RequestFactory, TestCase from mptt.admin import TreeRelatedFieldListFilter from mptt.querysets import TreeQuerySet try: from mock_django import mock_signal_receiver except ImportError: mock_signal_receiver = None from myapp.models import ( AutoNowDateFieldModel, Book, Category, ConcreteModel, CustomPKName, CustomTreeManager, CustomTreeQueryset, DoubleProxyModel, Genre, Item, MultipleManagerModel, Node, NotConcreteFieldModel, NullableDescOrderedInsertionModel, NullableOrderedInsertionModel, OrderedInsertion, Person, SingleProxyModel, Student, SubItem, UniqueTogetherModel, UUIDNode, ) from mptt.exceptions import CantDisableUpdates, InvalidMove from mptt.managers import TreeManager from mptt.models import MPTTModel from mptt.signals import node_moved from mptt.templatetags.mptt_tags import cache_tree_children from mptt.utils import print_debug_info 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, str): tree1 = get_tree_details(tree1) tree1 = tree_details(tree1) if not isinstance(tree2, str): 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): import doctest 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 doctest.testfile( os.path.join(os.path.dirname(__file__), "doctests.txt"), module_relative=False, optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS, encoding="utf-8", ) sys.stdout = before content = dummy_stream.content if content: before.write(content + "\n") self.fail() # genres.json defines the following tree structure # # 1 - 1 0 1 16 action # 2 1 1 1 2 9 +-- platformer # 3 2 1 2 3 4 | |-- platformer_2d # 4 2 1 2 5 6 | |-- platformer_3d # 5 2 1 2 7 8 | +-- platformer_4d # 6 1 1 1 10 15 +-- shmup # 7 6 1 2 11 12 |-- shmup_vertical # 8 6 1 2 13 14 +-- shmup_horizontal # 9 - 2 0 1 6 rpg # 10 9 2 1 2 3 |-- arpg # 11 9 2 1 4 5 +-- trpg class ReparentingTestCase(TreeTestCase): """ Test that trees are in the appropriate state after reparenting and that reparented items have the correct tree attributes defined, should they be required for use after a save. """ fixtures = ["genres.json"] def test_new_root_from_subtree(self): shmup = Genre.objects.get(id=6) shmup.parent = None shmup.save() self.assertTreeEqual([shmup], "6 - 3 0 1 6") self.assertTreeEqual( Genre.objects.all(), """ 1 - 1 0 1 10 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 9 - 2 0 1 6 10 9 2 1 2 3 11 9 2 1 4 5 6 - 3 0 1 6 7 6 3 1 2 3 8 6 3 1 4 5 """, ) def test_new_root_from_leaf_with_siblings(self): platformer_2d = Genre.objects.get(id=3) platformer_2d.parent = None platformer_2d.save() self.assertTreeEqual([platformer_2d], "3 - 3 0 1 2") self.assertTreeEqual( Genre.objects.all(), """ 1 - 1 0 1 14 2 1 1 1 2 7 4 2 1 2 3 4 5 2 1 2 5 6 6 1 1 1 8 13 7 6 1 2 9 10 8 6 1 2 11 12 9 - 2 0 1 6 10 9 2 1 2 3 11 9 2 1 4 5 3 - 3 0 1 2 """, ) def test_new_child_from_root(self): action = Genre.objects.get(id=1) rpg = Genre.objects.get(id=9) action.parent = rpg action.save() self.assertTreeEqual([action], "1 9 2 1 6 21") self.assertTreeEqual([rpg], "9 - 2 0 1 22") self.assertTreeEqual( Genre.objects.all(), """ 9 - 2 0 1 22 10 9 2 1 2 3 11 9 2 1 4 5 1 9 2 1 6 21 2 1 2 2 7 14 3 2 2 3 8 9 4 2 2 3 10 11 5 2 2 3 12 13 6 1 2 2 15 20 7 6 2 3 16 17 8 6 2 3 18 19 """, ) def test_move_leaf_to_other_tree(self): shmup_horizontal = Genre.objects.get(id=8) rpg = Genre.objects.get(id=9) shmup_horizontal.parent = rpg shmup_horizontal.save() self.assertTreeEqual([shmup_horizontal], "8 9 2 1 6 7") self.assertTreeEqual([rpg], "9 - 2 0 1 8") self.assertTreeEqual( Genre.objects.all(), """ 1 - 1 0 1 14 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 6 1 1 1 10 13 7 6 1 2 11 12 9 - 2 0 1 8 10 9 2 1 2 3 11 9 2 1 4 5 8 9 2 1 6 7 """, ) def test_move_subtree_to_other_tree(self): shmup = Genre.objects.get(id=6) trpg = Genre.objects.get(id=11) shmup.parent = trpg shmup.save() self.assertTreeEqual([shmup], "6 11 2 2 5 10") self.assertTreeEqual([trpg], "11 9 2 1 4 11") self.assertTreeEqual( Genre.objects.all(), """ 1 - 1 0 1 10 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 9 - 2 0 1 12 10 9 2 1 2 3 11 9 2 1 4 11 6 11 2 2 5 10 7 6 2 3 6 7 8 6 2 3 8 9 """, ) def test_move_child_up_level(self): shmup_horizontal = Genre.objects.get(id=8) action = Genre.objects.get(id=1) shmup_horizontal.parent = action shmup_horizontal.save() self.assertTreeEqual([shmup_horizontal], "8 1 1 1 14 15") self.assertTreeEqual([action], "1 - 1 0 1 16") self.assertTreeEqual( Genre.objects.all(), """ 1 - 1 0 1 16 2 1 1 1 2 9 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 6 1 1 1 10 13 7 6 1 2 11 12 8 1 1 1 14 15 9 - 2 0 1 6 10 9 2 1 2 3 11 9 2 1 4 5 """, ) def test_move_subtree_down_level(self): shmup = Genre.objects.get(id=6) platformer = Genre.objects.get(id=2) shmup.parent = platformer shmup.save() self.assertTreeEqual([shmup], "6 2 1 2 9 14") self.assertTreeEqual([platformer], "2 1 1 1 2 15") self.assertTreeEqual( Genre.objects.all(), """ 1 - 1 0 1 16 2 1 1 1 2 15 3 2 1 2 3 4 4 2 1 2 5 6 5 2 1 2 7 8 6 2 1 2 9 14 7 6 1 3 10 11 8 6 1 3 12 13 9 - 2 0 1 6 10 9 2 1 2 3 11 9 2 1 4 5 """, ) def test_move_to(self): rpg = Genre.objects.get(pk=9) action = Genre.objects.get(pk=1) rpg.move_to(action) rpg.save() self.assertEqual(rpg.parent, action) def test_invalid_moves(self): # A node may not be made a child of itself action = Genre.objects.get(id=1) action.parent = action platformer = Genre.objects.get(id=2) platformer.parent = platformer self.assertRaises(InvalidMove, action.save) self.assertRaises(InvalidMove, platformer.save) # A node may not be made a child of any of its descendants platformer_4d = Genre.objects.get(id=5) action.parent = platformer_4d platformer.parent = platformer_4d self.assertRaises(InvalidMove, action.save) self.assertRaises(InvalidMove, platformer.save) # New parent is still set when an error occurs self.assertEqual(action.parent, platformer_4d) self.assertEqual(platformer.parent, platformer_4d) class ConcurrencyTestCase(TreeTestCase): """ Test that tree structure remains intact when saving nodes (without setting new parent) after tree structure has been changed. """ def setUp(self): fruit = ConcreteModel.objects.create(name="Fruit") vegie = ConcreteModel.objects.create(name="Vegie") ConcreteModel.objects.create(name="Apple", parent=fruit) ConcreteModel.objects.create(name="Pear", parent=fruit) ConcreteModel.objects.create(name="Tomato", parent=vegie) ConcreteModel.objects.create(name="Carrot", parent=vegie) # sanity check self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 3 1 1 1 2 3 4 1 1 1 4 5 2 - 2 0 1 6 5 2 2 1 2 3 6 2 2 1 4 5 """, ) def _modify_tree(self): fruit = ConcreteModel.objects.get(name="Fruit") vegie = ConcreteModel.objects.get(name="Vegie") vegie.move_to(fruit) def _assert_modified_tree_state(self): carrot = ConcreteModel.objects.get(id=6) self.assertTreeEqual([carrot], "6 2 1 2 5 6") self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 12 2 1 1 1 2 7 5 2 1 2 3 4 6 2 1 2 5 6 3 1 1 1 8 9 4 1 1 1 10 11 """, ) def test_node_save_after_tree_restructuring(self): carrot = ConcreteModel.objects.get(id=6) self._modify_tree() carrot.name = "Purple carrot" carrot.save() self._assert_modified_tree_state() def test_node_save_after_tree_restructuring_with_update_fields(self): """ Test that model is saved properly when passing update_fields """ carrot = ConcreteModel.objects.get(id=6) self._modify_tree() # update with kwargs carrot.name = "Won't change" carrot.ghosts = "Will get updated" carrot.save(update_fields=["ghosts"]) self._assert_modified_tree_state() updated_carrot = ConcreteModel.objects.get(id=6) self.assertEqual(updated_carrot.ghosts, carrot.ghosts) self.assertNotEqual(updated_carrot.name, carrot.name) # update with positional arguments carrot.name = "Will change" carrot.ghosts = "Will not be updated" carrot.save(False, False, None, ["name"]) updated_carrot = ConcreteModel.objects.get(id=6) self.assertNotEqual(updated_carrot.ghosts, carrot.ghosts) self.assertEqual(updated_carrot.name, carrot.name) def test_update_fields_positional(self): """ Test that update_fields works as a positional argument Test for https://github.com/django-mptt/django-mptt/issues/384 """ carrot = ConcreteModel.objects.get(id=6) # Why would you do it this way? Meh. carrot.save(False, False, None, None) # categories.json defines the following tree structure: # # 1 - 1 0 1 20 games # 2 1 1 1 2 7 +-- wii # 3 2 1 2 3 4 | |-- wii_games # 4 2 1 2 5 6 | +-- wii_hardware # 5 1 1 1 8 13 +-- xbox360 # 6 5 1 2 9 10 | |-- xbox360_games # 7 5 1 2 11 12 | +-- xbox360_hardware # 8 1 1 1 14 19 +-- ps3 # 9 8 1 2 15 16 |-- ps3_games # 10 8 1 2 17 18 +-- ps3_hardware class DeletionTestCase(TreeTestCase): """ Tests that the tree structure is maintained appropriately in various deletion scenarios. """ fixtures = ["categories.json"] def test_delete_root_node(self): # Add a few other roots to verify that they aren't affected Category(name="Preceding root").insert_at( Category.objects.get(id=1), "left", save=True ) Category(name="Following root").insert_at( Category.objects.get(id=1), "right", save=True ) self.assertTreeEqual( Category.objects.all(), """ 11 - 1 0 1 2 1 - 2 0 1 20 2 1 2 1 2 7 3 2 2 2 3 4 4 2 2 2 5 6 5 1 2 1 8 13 6 5 2 2 9 10 7 5 2 2 11 12 8 1 2 1 14 19 9 8 2 2 15 16 10 8 2 2 17 18 12 - 3 0 1 2 """, ) Category.objects.get(id=1).delete() self.assertTreeEqual( Category.objects.all(), """ 11 - 1 0 1 2 12 - 3 0 1 2 """, ) def test_delete_last_node_with_siblings(self): Category.objects.get(id=9).delete() self.assertTreeEqual( Category.objects.all(), """ 1 - 1 0 1 18 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 8 1 1 1 14 17 10 8 1 2 15 16 """, ) def test_delete_last_node_with_descendants(self): Category.objects.get(id=8).delete() self.assertTreeEqual( Category.objects.all(), """ 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 13 6 5 1 2 9 10 7 5 1 2 11 12 """, ) def test_delete_node_with_siblings(self): child = Category.objects.get(id=6) parent = child.parent self.assertEqual(parent.get_descendant_count(), 2) child.delete() self.assertTreeEqual( Category.objects.all(), """ 1 - 1 0 1 18 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 11 7 5 1 2 9 10 8 1 1 1 12 17 9 8 1 2 13 14 10 8 1 2 15 16 """, ) self.assertEqual(parent.get_descendant_count(), 1) parent = Category.objects.get(pk=parent.pk) self.assertEqual(parent.get_descendant_count(), 1) def test_delete_node_with_descendants_and_siblings(self): """ Regression test for Issue 23 - we used to use pre_delete, which resulted in tree cleanup being performed for every node being deleted, rather than just the node on which ``delete()`` was called. """ Category.objects.get(id=5).delete() self.assertTreeEqual( Category.objects.all(), """ 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 8 1 1 1 8 13 9 8 1 2 9 10 10 8 1 2 11 12 """, ) def test_delete_multiple_nodes(self): """Regression test for Issue 576.""" queryset = Category.objects.filter(id__in=[6, 7]) for category in queryset: category.delete() self.assertTreeEqual( Category.objects.all(), """ 1 - 1 0 1 16 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 5 1 1 1 8 9 8 1 1 1 10 15 9 8 1 2 11 12 10 8 1 2 13 14""", ) class IntraTreeMovementTestCase(TreeTestCase): pass class InterTreeMovementTestCase(TreeTestCase): pass class PositionedInsertionTestCase(TreeTestCase): pass class CustomPKNameTestCase(TreeTestCase): def setUp(self): manager = CustomPKName.objects c1 = manager.create(name="c1") manager.create(name="c11", parent=c1) manager.create(name="c12", parent=c1) c2 = manager.create(name="c2") manager.create(name="c21", parent=c2) manager.create(name="c22", parent=c2) manager.create(name="c3") def test_get_next_sibling(self): root = CustomPKName.objects.get(name="c12") sib = root.get_next_sibling() self.assertTrue(sib is None) class DisabledUpdatesTestCase(TreeTestCase): def setUp(self): self.a = ConcreteModel.objects.create(name="a") self.b = ConcreteModel.objects.create(name="b", parent=self.a) self.c = ConcreteModel.objects.create(name="c", parent=self.a) self.d = ConcreteModel.objects.create(name="d") # state is now: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 """, ) def test_single_proxy(self): self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(SingleProxyModel._mptt_updates_enabled) self.assertRaises( CantDisableUpdates, SingleProxyModel.objects.disable_mptt_updates().__enter__, ) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(SingleProxyModel._mptt_updates_enabled) with ConcreteModel.objects.disable_mptt_updates(): self.assertFalse(ConcreteModel._mptt_updates_enabled) self.assertFalse(SingleProxyModel._mptt_updates_enabled) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(SingleProxyModel._mptt_updates_enabled) def test_double_proxy(self): self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(DoubleProxyModel._mptt_updates_enabled) self.assertRaises( CantDisableUpdates, DoubleProxyModel.objects.disable_mptt_updates().__enter__, ) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(DoubleProxyModel._mptt_updates_enabled) with ConcreteModel.objects.disable_mptt_updates(): self.assertFalse(ConcreteModel._mptt_updates_enabled) self.assertFalse(DoubleProxyModel._mptt_updates_enabled) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(DoubleProxyModel._mptt_updates_enabled) def test_insert_child(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): # 1 query here: with self.assertNumQueries(1): ConcreteModel.objects.create(name="e", parent=self.d) # 2nd query here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 4 2 1 2 3 """, ) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 4 2 1 2 3 """, ) def test_insert_root(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 1 query here: ConcreteModel.objects.create(name="e") # 2nd query here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 5 - 0 0 1 2 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 """, ) self.assertTreeEqual( ConcreteModel.objects.all(), """ 5 - 0 0 1 2 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 """, ) def test_move_node_same_tree(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 2 queries here: # (django does a query to determine if the row is in the db yet) self.c.parent = self.b self.c.save() # 3rd query here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 2 1 1 4 5 4 - 2 0 1 2 """, ) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 2 1 1 4 5 4 - 2 0 1 2 """, ) def test_move_node_different_tree(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 1 update query self.c.parent = self.d self.c.save() # query 2 here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 4 1 1 4 5 4 - 2 0 1 2 """, ) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 4 1 1 4 5 4 - 2 0 1 2 """, ) def test_move_node_to_root(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 1 update query self.c.parent = None self.c.save() # query 2 here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 - 1 1 4 5 4 - 2 0 1 2 """, ) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 - 1 1 4 5 4 - 2 0 1 2 """, ) def test_move_root_to_child(self): with self.assertNumQueries(2): with ConcreteModel.objects.disable_mptt_updates(): with self.assertNumQueries(1): # 1 update query self.d.parent = self.c self.d.save() # query 2 here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 2 0 1 2 """, ) # yes, this is wrong. that's what disable_mptt_updates() does :/ self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 2 0 1 2 """, ) class DelayedUpdatesTestCase(TreeTestCase): def setUp(self): self.a = ConcreteModel.objects.create(name="a") self.b = ConcreteModel.objects.create(name="b", parent=self.a) self.c = ConcreteModel.objects.create(name="c", parent=self.a) self.d = ConcreteModel.objects.create(name="d") self.z = ConcreteModel.objects.create(name="z") # state is now: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 """, ) def test_proxy(self): self.assertFalse(ConcreteModel._mptt_is_tracking) self.assertFalse(SingleProxyModel._mptt_is_tracking) self.assertRaises( CantDisableUpdates, SingleProxyModel.objects.delay_mptt_updates().__enter__ ) self.assertFalse(ConcreteModel._mptt_is_tracking) self.assertFalse(SingleProxyModel._mptt_is_tracking) with ConcreteModel.objects.delay_mptt_updates(): self.assertTrue(ConcreteModel._mptt_is_tracking) self.assertTrue(SingleProxyModel._mptt_is_tracking) self.assertFalse(ConcreteModel._mptt_is_tracking) self.assertFalse(SingleProxyModel._mptt_is_tracking) def test_double_context_manager(self): with ConcreteModel.objects.delay_mptt_updates(): self.assertTrue(ConcreteModel._mptt_is_tracking) with ConcreteModel.objects.delay_mptt_updates(): self.assertTrue(ConcreteModel._mptt_is_tracking) self.assertTrue(ConcreteModel._mptt_is_tracking) self.assertFalse(ConcreteModel._mptt_is_tracking) def test_insert_child(self): with self.assertNumQueries(8): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 1 query for target stale check, # 1 query to save node. ConcreteModel.objects.create(name="e", parent=self.d) # 3rd query here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 6 4 2 1 2 3 5 - 3 0 1 2 """, ) # remaining queries (4 through 8) are the partial rebuild process. self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 4 6 4 2 1 2 3 5 - 3 0 1 2 """, ) def test_insert_root(self): with self.assertNumQueries(3): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries required here: # (one to get the correct tree_id, then one to insert) ConcreteModel.objects.create(name="e") # 3rd query here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 6 - 4 0 1 2 """, ) # no partial rebuild necessary, as no trees were modified # (newly created tree is already okay) self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 6 - 4 0 1 2 """, ) def test_move_node_same_tree(self): with self.assertNumQueries(10): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 1 query to ensure target fields aren't stale # 1 update query self.c.parent = self.b self.c.save() # query 3 here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 2 1 2 3 4 4 - 2 0 1 2 5 - 3 0 1 2 """, ) # the remaining 7 queries are the partial rebuild. self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 5 3 2 1 2 3 4 4 - 2 0 1 2 5 - 3 0 1 2 """, ) def test_move_node_different_tree(self): with self.assertNumQueries(12): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries here: # 1. update the node # 2. collapse old tree since it is now empty. self.d.parent = self.c self.d.save() # query 3 here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 1 2 5 6 5 - 2 0 1 2 """, ) # the other 9 queries are the partial rebuild self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 3 1 1 1 4 7 4 3 1 2 5 6 5 - 2 0 1 2 """, ) def test_move_node_to_root(self): with self.assertNumQueries(4): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(3): # 3 queries here! # 1. find the next tree_id to move to # 2. update the tree_id on all nodes to the right of that # 3. update tree fields on self.c self.c.parent = None self.c.save() # 4th query here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 4 - 2 0 1 2 5 - 3 0 1 2 3 - 4 0 1 2 """, ) self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 4 - 2 0 1 2 5 - 3 0 1 2 3 - 4 0 1 2 """, ) def test_move_root_to_child(self): with self.assertNumQueries(12): with ConcreteModel.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries here: # 1. update the node # 2. collapse old tree since it is now empty. self.d.parent = self.c self.d.save() # query 3 here: self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 1 2 5 6 5 - 2 0 1 2 """, ) # the remaining 9 queries are the partial rebuild. self.assertTreeEqual( ConcreteModel.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 3 1 1 1 4 7 4 3 1 2 5 6 5 - 2 0 1 2 """, ) class OrderedInsertionSortingTestCase(TestCase): def test_insert_unordered_stuff(self): root = OrderedInsertion.objects.create(name="") # "b" gets inserted first, b = OrderedInsertion.objects.create(name="b", parent=root) # "a" gets inserted later, a = OrderedInsertion.objects.create(name="a", parent=root) # ... but specifying OrderedInsertion.MPTTMeta.order_insertion_by # tells django-mptt to order added items by the name. So basically # instance "a", added later, will get the first place in the # tree. So what's exactly seems to be the problem? # # The problem is, item "b" will not get refreshed in any # way. We need to reload it manually or else there will be problems # like the one demonstrated below: self.assertIn(a, a.get_ancestors(include_self=True)) # This will raise an AssertionError, unless we reload the item from # the database. As long as we won't come up with a sensible way # of reloading all Django instances pointing to a given row in the # database... # self.assertIn(b, b.get_ancestors(include_self=True))) self.assertRaises( AssertionError, self.assertIn, b, b.get_ancestors(include_self=True) ) # ... we need to reload it properly ourselves: b.refresh_from_db() self.assertIn(b, b.get_ancestors(include_self=True)) class OrderedInsertionDelayedUpdatesTestCase(TreeTestCase): def setUp(self): self.c = OrderedInsertion.objects.create(name="c") self.d = OrderedInsertion.objects.create(name="d", parent=self.c) self.e = OrderedInsertion.objects.create(name="e", parent=self.c) self.f = OrderedInsertion.objects.create(name="f") self.z = OrderedInsertion.objects.create(name="z") # state is now: self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 """, ) def test_insert_child(self): with self.assertNumQueries(12): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 1 query here: OrderedInsertion.objects.create(name="dd", parent=self.c) # 2nd query here: self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 6 1 1 1 6 7 4 - 2 0 1 2 5 - 3 0 1 2 """, ) # remaining 9 queries are the partial rebuild process. self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 6 1 1 1 4 5 3 1 1 1 6 7 4 - 2 0 1 2 5 - 3 0 1 2 """, ) def test_insert_root(self): with self.assertNumQueries(4): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(3): # 3 queries required here: # 1. get correct tree_id (delay_mptt_updates doesn't handle # root-level ordering when using ordered insertion) # 2. increment tree_id of all following trees # 3. insert the object OrderedInsertion.objects.create(name="ee") # 4th query here: self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 6 - 2 0 1 2 4 - 3 0 1 2 5 - 4 0 1 2 """, ) # no partial rebuild is required self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 6 - 2 0 1 2 4 - 3 0 1 2 5 - 4 0 1 2 """, ) def test_move_node_same_tree(self): with self.assertNumQueries(9): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(1): # 1 update query self.e.name = "before d" self.e.save() # query 2 here: self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 """, ) # the remaining 7 queries are the partial rebuild. self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 4 - 2 0 1 2 5 - 3 0 1 2 """, ) def test_move_node_different_tree(self): with self.assertNumQueries(12): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries here: # 1. update the node # 2. collapse old tree since it is now empty. self.f.parent = self.c self.f.name = "dd" self.f.save() # query 3 here: self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 4 1 1 1 2 3 3 1 1 1 4 5 5 - 2 0 1 2 """, ) # the remaining 9 queries are the partial rebuild self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 4 1 1 1 4 5 3 1 1 1 6 7 5 - 2 0 1 2 """, ) def test_move_node_to_root(self): with self.assertNumQueries(4): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(3): # 3 queries here! # 1. find the next tree_id to move to # 2. update the tree_id on all nodes to the right of that # 3. update tree fields on self.c self.e.parent = None self.e.save() # query 4 here: self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 - 2 0 1 2 4 - 3 0 1 2 5 - 4 0 1 2 """, ) self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 - 2 0 1 2 4 - 3 0 1 2 5 - 4 0 1 2 """, ) def test_move_root_to_child(self): with self.assertNumQueries(12): with OrderedInsertion.objects.delay_mptt_updates(): with self.assertNumQueries(2): # 2 queries here: # 1. update the node # 2. collapse old tree since it is now empty. self.f.parent = self.e self.f.save() # query 3 here: self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 4 3 1 2 5 6 5 - 2 0 1 2 """, ) # the remaining 9 queries are the partial rebuild. self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 3 3 1 1 1 4 7 4 3 1 2 5 6 5 - 2 0 1 2 """, ) class ManagerTests(TreeTestCase): fixtures = ["categories.json", "genres.json", "persons.json"] def test_all_managers_are_different(self): # all tree managers should be different. otherwise, possible infinite recursion. seen = {} for model in apps.get_models(): if not issubclass(model, MPTTModel): continue tm = model._tree_manager if id(tm) in seen: self.fail( "Tree managers for %s and %s are the same manager" % (model.__name__, seen[id(tm)].__name__) ) seen[id(tm)] = model def test_manager_multi_table_inheritance(self): self.assertIs(Student._tree_manager.model, Student) self.assertIs(Student._tree_manager.tree_model, Person) self.assertIs(Person._tree_manager.model, Person) self.assertIs(Person._tree_manager.tree_model, Person) def test_all_managers_have_correct_model(self): # all tree managers should have the correct model. for model in apps.get_models(): if not issubclass(model, MPTTModel): continue self.assertEqual(model._tree_manager.model, model) def test_base_manager_infinite_recursion(self): # repeatedly calling _base_manager should eventually return None for model in apps.get_models(): if not issubclass(model, MPTTModel): continue manager = model._tree_manager for i in range(20): manager = manager._base_manager if manager is None: break else: self.fail( "Detected infinite recursion in %s._tree_manager._base_manager" % model ) def test_proxy_custom_manager(self): self.assertIsInstance(SingleProxyModel._tree_manager, CustomTreeManager) self.assertIsInstance(SingleProxyModel._tree_manager._base_manager, TreeManager) self.assertIsInstance(SingleProxyModel.objects, CustomTreeManager) self.assertIsInstance(SingleProxyModel.objects._base_manager, TreeManager) def test_get_queryset_descendants(self): def get_desc_names(qs, include_self=False): desc = qs.model.objects.get_queryset_descendants( qs, include_self=include_self ) return list(desc.values_list("name", flat=True).order_by("name")) qs = Category.objects.filter(Q(name="Nintendo Wii") | Q(name="PlayStation 3")) self.assertEqual( get_desc_names(qs), ["Games", "Games", "Hardware & Accessories", "Hardware & Accessories"], ) self.assertEqual( get_desc_names(qs, include_self=True), [ "Games", "Games", "Hardware & Accessories", "Hardware & Accessories", "Nintendo Wii", "PlayStation 3", ], ) qs = Genre.objects.filter(parent=None) self.assertEqual( get_desc_names(qs), [ "2D Platformer", "3D Platformer", "4D Platformer", "Action RPG", "Horizontal Scrolling Shootemup", "Platformer", "Shootemup", "Tactical RPG", "Vertical Scrolling Shootemup", ], ) self.assertEqual( get_desc_names(qs, include_self=True), [ "2D Platformer", "3D Platformer", "4D Platformer", "Action", "Action RPG", "Horizontal Scrolling Shootemup", "Platformer", "Role-playing Game", "Shootemup", "Tactical RPG", "Vertical Scrolling Shootemup", ], ) def _get_anc_names(self, qs, include_self=False): anc = qs.model.objects.get_queryset_ancestors(qs, include_self=include_self) return list(anc.values_list("name", flat=True).order_by("name")) def test_get_queryset_ancestors(self): qs = Category.objects.filter(Q(name="Nintendo Wii") | Q(name="PlayStation 3")) self.assertEqual(self._get_anc_names(qs), ["PC & Video Games"]) self.assertEqual( self._get_anc_names(qs, include_self=True), ["Nintendo Wii", "PC & Video Games", "PlayStation 3"], ) qs = Genre.objects.filter(parent=None) self.assertEqual(self._get_anc_names(qs), []) self.assertEqual( self._get_anc_names(qs, include_self=True), ["Action", "Role-playing Game"] ) def test_get_queryset_ancestors_regression_379(self): # https://github.com/django-mptt/django-mptt/issues/379 qs = Genre.objects.all() self.assertEqual( self._get_anc_names(qs, include_self=True), list(Genre.objects.values_list("name", flat=True).order_by("name")), ) def test_custom_querysets(self): """ Test that a custom manager also provides custom querysets. """ self.assertTrue(isinstance(Person.objects.all(), CustomTreeQueryset)) self.assertTrue( isinstance(Person.objects.all()[0].get_children(), CustomTreeQueryset) ) self.assertTrue(hasattr(Person.objects.none(), "custom_method")) # Check that empty querysets get custom methods self.assertTrue( hasattr(Person.objects.all()[0].get_children().none(), "custom_method") ) self.assertEqual(type(Person.objects.all()), type(Person.objects.root_nodes())) def test_manager_from_custom_queryset(self): """ Test that a manager created from a custom queryset works. Regression test for #378. """ TreeManager.from_queryset(CustomTreeQueryset)().contribute_to_class( Genre, "my_manager" ) self.assertIsInstance(Genre.my_manager.get_queryset(), CustomTreeQueryset) def test_num_queries_on_get_queryset_descendants(self): """ Test the number of queries to access descendants is not O(n). At the moment it is O(1)+1. Ideally we should aim for O(1). """ with self.assertNumQueries(2): qs = Category.objects.get_queryset_descendants( Category.objects.all(), include_self=True ) self.assertEqual(len(qs), 10) def test_default_manager_with_multiple_managers(self): """ Test that a model with multiple managers defined always uses the default manager as the tree manager. """ self.assertEqual(type(MultipleManagerModel._tree_manager), TreeManager) class CacheTreeChildrenTestCase(TreeTestCase): """ Tests for the ``cache_tree_children`` template filter. """ fixtures = ["categories.json"] def test_cache_tree_children_caches_parents(self): """ Ensures that each node's parent is cached by ``cache_tree_children``. """ # Ensure only 1 query is used during this test with self.assertNumQueries(1): roots = cache_tree_children(Category.objects.all()) games = roots[0] wii = games.get_children()[0] wii_games = wii.get_children()[0] # Ensure that ``wii`` is cached as ``parent`` on ``wii_games``, and # likewise for ``games`` being ``parent`` on the attached ``wii`` self.assertEqual(wii, wii_games.parent) self.assertEqual(games, wii_games.parent.parent) def test_cache_tree_children_with_invalid_ordering(self): """ Ensures that ``cache_tree_children`` fails with a ``ValueError`` when passed a list which is not in tree order. """ with self.assertNumQueries(1): with self.assertRaises(ValueError): cache_tree_children(list(Category.objects.order_by("-id"))) # Passing a list with correct ordering should work, though. with self.assertNumQueries(1): cache_tree_children(list(Category.objects.all())) # The exact ordering tuple doesn't matter, long as the nodes end up in depth-first order. cache_tree_children(Category.objects.order_by("tree_id", "lft", "name")) cache_tree_children(Category.objects.filter(tree_id=1).order_by("lft")) class RecurseTreeTestCase(TreeTestCase): """ Tests for the ``recursetree`` template filter. """ fixtures = ["categories.json"] template = re.sub( r"(?m)^[\s]+", "", """ {% load mptt_tags %}
      {% recursetree nodes %}
    • {{ node.name }} {% if not node.is_leaf_node %}
        {{ children }}
      {% endif %}
    • {% endrecursetree %}
    """, ) def test_leaf_html(self): html = ( Template(self.template) .render( Context( { "nodes": Category.objects.filter(pk=10), } ) ) .replace("\n", "") ) self.assertEqual(html, "
    • Hardware & Accessories
    ") def test_nonleaf_html(self): qs = Category.objects.get(pk=8).get_descendants(include_self=True) html = ( Template(self.template) .render( Context( { "nodes": qs, } ) ) .replace("\n", "") ) self.assertEqual( html, ( '
    • PlayStation 3
        ' "
      • Games
      • Hardware & Accessories
    " ), ) def test_parsing_fail(self): self.assertRaises( TemplateSyntaxError, Template, "{% load mptt_tags %}{% recursetree %}{% endrecursetree %}", ) def test_cached_ancestors(self): template = Template( """ {% load mptt_tags %} {% recursetree nodes %} {{ node.get_ancestors|join:" > " }} {{ node.name }} {% if not node.is_leaf_node %} {{ children }} {% endif %} {% endrecursetree %} """ ) with self.assertNumQueries(1): qs = Category.objects.all() template.render(Context({"nodes": qs})) class TreeInfoTestCase(TreeTestCase): fixtures = ["genres.json"] template = re.sub( r"(?m)^[\s]+", "", """ {% load mptt_tags %} {% for node, structure in nodes|tree_info %} {% if structure.new_level %}
    • {% else %}
    • {% endif %} {{ node.pk }} {% for level in structure.closed_levels %}
    {% endfor %} {% endfor %}""", ) template_with_ancestors = re.sub( r"(?m)^[\s]+", "", """ {% load mptt_tags %} {% for node, structure in nodes|tree_info:"ancestors" %} {% if structure.new_level %}
    • {% else %}
    • {% endif %} {{ node.pk }} {% for ancestor in structure.ancestors %} {% if forloop.first %}A:{% endif %} {{ ancestor }}{% if not forloop.last %},{% endif %} {% endfor %} {% for level in structure.closed_levels %}
    {% endfor %} {% endfor %}""", ) def test_tree_info_html(self): html = ( Template(self.template) .render( Context( { "nodes": Genre.objects.all(), } ) ) .replace("\n", "") ) self.assertEqual( html, "
    • 1
      • 2
        • 3
        • 4
        • 5
      • " "
      • 6
        • 7
        • 8
    • 9
        " "
      • 10
      • 11
    ", ) html = ( Template(self.template) .render( Context( { "nodes": Genre.objects.filter( **{ "%s__gte" % Genre._mptt_meta.level_attr: 1, "%s__lte" % Genre._mptt_meta.level_attr: 2, } ), } ) ) .replace("\n", "") ) self.assertEqual( html, "
    • 2
      • 3
      • 4
      • 5
    • 6
        " "
      • 7
      • 8
    • 10
    • 11
    ", ) html = ( Template(self.template_with_ancestors) .render( Context( { "nodes": Genre.objects.filter( **{ "%s__gte" % Genre._mptt_meta.level_attr: 1, "%s__lte" % Genre._mptt_meta.level_attr: 2, } ), } ) ) .replace("\n", "") ) self.assertEqual( html, "
    • 2
      • 3A:Platformer
      • 4A:Platformer
      • " "
      • 5A:Platformer
    • 6
      • 7A:Shootemup
      • " "
      • 8A:Shootemup
    • 10
    • 11
    ", ) class FullTreeTestCase(TreeTestCase): fixtures = ["genres.json"] template = re.sub( r"(?m)^[\s]+", "", """ {% load mptt_tags %} {% full_tree_for_model myapp.Genre as tree %} {% for node in tree %}{{ node.pk }},{% endfor %} """, ) def test_full_tree_html(self): html = Template(self.template).render(Context({})).replace("\n", "") self.assertEqual(html, "1,2,3,4,5,6,7,8,9,10,11,") class DrilldownTreeTestCase(TreeTestCase): fixtures = ["genres.json"] template = re.sub( r"(?m)^[\s]+", "", """ {% load mptt_tags %} {% drilldown_tree_for_node node as tree count myapp.Game.genre in game_count %} {% for n in tree %} {% if n == node %}[{% endif %} {{ n.pk }}:{{ n.game_count }} {% if n == node %}]{% endif %}{% if not forloop.last %},{% endif %} {% endfor %} """, ) def render_for_node(self, pk, cumulative=False, m2m=False, all_descendants=False): template = self.template if all_descendants: template = template.replace( " count myapp.Game.genre in game_count ", " all_descendants " ) if cumulative: template = template.replace(" count ", " cumulative count ") if m2m: template = template.replace("Game.genre", "Game.genres_m2m") return ( Template(template) .render( Context( { "node": Genre.objects.get(pk=pk), } ) ) .replace("\n", "") ) def test_drilldown_html(self): for idx, genre in enumerate(Genre.objects.all()): for i in range(idx): game = genre.game_set.create(name="Game %s" % i) genre.games_m2m.add(game) self.assertEqual(self.render_for_node(1), "[1:],2:1,6:5") self.assertEqual(self.render_for_node(2), "1:,[2:],3:2,4:3,5:4") self.assertEqual(self.render_for_node(1, cumulative=True), "[1:],2:10,6:18") self.assertEqual( self.render_for_node(2, cumulative=True), "1:,[2:],3:2,4:3,5:4" ) self.assertEqual(self.render_for_node(1, m2m=True), "[1:],2:1,6:5") self.assertEqual(self.render_for_node(2, m2m=True), "1:,[2:],3:2,4:3,5:4") self.assertEqual( self.render_for_node(1, cumulative=True, m2m=True), "[1:],2:10,6:18" ) self.assertEqual( self.render_for_node(2, cumulative=True, m2m=True), "1:,[2:],3:2,4:3,5:4" ) self.assertEqual( self.render_for_node(1, all_descendants=True), "[1:],2:,3:,4:,5:,6:,7:,8:" ) self.assertEqual( self.render_for_node(2, all_descendants=True), "1:,[2:],3:,4:,5:" ) 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 TestAltersData(TreeTestCase): def test_alters_data(self): node = Node() output = Template("{{ node.save }}").render( Context( { "node": node, } ) ) self.assertEqual(output, "") self.assertEqual(node.pk, None) node.save() self.assertNotEqual(node.pk, None) output = Template("{{ node.delete }}").render( Context( { "node": node, } ) ) self.assertEqual(node, Node.objects.get(pk=node.pk)) class TestDebugInfo(TreeTestCase): fixtures = ["categories.json"] def test_debug_info(self): with io.StringIO() as out: print_debug_info(Category.objects.all(), file=out) output = out.getvalue() self.assertIn("1,0,,1,1,20", output) def test_debug_info_with_non_ascii_representations(self): Category.objects.create(name="El niño") with io.StringIO() as out: print_debug_info(Category.objects.all(), file=out) output = out.getvalue() self.assertIn("El niño", output) class AdminBatch(TreeTestCase): fixtures = ["categories.json"] def test_changelist(self): user = User.objects.create_superuser("admin", "test@example.com", "p") self.client.login(username=user.username, password="p") response = self.client.get("/admin/myapp/category/") self.assertContains(response, 'name="_selected_action"', 10) mptt_opts = Category._mptt_meta self.assertSequenceEqual( response.context["cl"].result_list.query.order_by[:2], [mptt_opts.tree_id_attr, mptt_opts.left_attr], ) data = { "action": "delete_selected", "_selected_action": ["5", "8", "9"], } response = self.client.post("/admin/myapp/category/", data) self.assertRegex(response.rendered_content, r'value="Yes, I(\'|’)m sure"') data["post"] = "yes" response = self.client.post("/admin/myapp/category/", data) self.assertRedirects(response, "/admin/myapp/category/") self.assertEqual(Category.objects.count(), 4) # Batch deletion has not clobbered MPTT values, because our method # delete_selected_tree has been used. self.assertTreeEqual( Category.objects.all(), """ 1 - 1 0 1 8 2 1 1 1 2 7 3 2 1 2 3 4 4 2 1 2 5 6 """, ) class TestUnsaved(TreeTestCase): def test_unsaved(self): for method in [ "get_ancestors", "get_family", "get_children", "get_descendants", "get_leafnodes", "get_next_sibling", "get_previous_sibling", "get_root", "get_siblings", ]: self.assertRaisesRegex( ValueError, "Cannot call %s on unsaved Genre instances" % method, getattr(Genre(), method), ) class QuerySetTests(TreeTestCase): fixtures = ["categories.json"] def test_get_ancestors(self): self.assertEqual( [ c.pk for c in Category.objects.get(name="Nintendo Wii").get_ancestors( include_self=False ) ], [ c.pk for c in Category.objects.filter(name="Nintendo Wii").get_ancestors( include_self=False ) ], ) self.assertEqual( [ c.pk for c in Category.objects.get(name="Nintendo Wii").get_ancestors( include_self=True ) ], [ c.pk for c in Category.objects.filter(name="Nintendo Wii").get_ancestors( include_self=True ) ], ) def test_get_descendants(self): self.assertEqual( [ c.pk for c in Category.objects.get(name="Nintendo Wii").get_descendants( include_self=False ) ], [ c.pk for c in Category.objects.filter(name="Nintendo Wii").get_descendants( include_self=False ) ], ) self.assertEqual( [ c.pk for c in Category.objects.get(name="Nintendo Wii").get_descendants( include_self=True ) ], [ c.pk for c in Category.objects.filter(name="Nintendo Wii").get_descendants( include_self=True ) ], ) def test_as_manager(self): self.assertTrue(issubclass(TreeQuerySet.as_manager().__class__, TreeManager)) class TreeManagerTestCase(TreeTestCase): fixtures = ["categories.json", "items.json", "subitems.json"] def test_add_related_count_with_fk_to_natural_key(self): # Regression test for #284 queryset = Category.objects.filter(name="Xbox 360").order_by("id") # Test using FK that doesn't point to a primary key for c in Category.objects.add_related_count( queryset, Item, "category_fk", "item_count", cumulative=False ): self.assertEqual(c.item_count, c.items_by_pk.count()) # Also works when using the FK that *does* point to a primary key for c in Category.objects.add_related_count( queryset, Item, "category_pk", "item_count", cumulative=False ): self.assertEqual(c.item_count, c.items_by_pk.count()) def test_add_related_count_multistep(self): queryset = Category.objects.filter(name="Xbox 360").order_by("id") topqueryset = Category.objects.filter(name="PC & Video Games").order_by("id") # Test using FK that doesn't point to a primary key for c in Category.objects.add_related_count( queryset, SubItem, "item__category_fk", "subitem_count", cumulative=False ): self.assertEqual(c.subitem_count, 1) for topc in Category.objects.add_related_count( topqueryset, SubItem, "item__category_fk", "subitem_count", cumulative=False ): self.assertEqual(topc.subitem_count, 1) # Also works when using the FK that *does* point to a primary key for c in Category.objects.add_related_count( queryset, SubItem, "item__category_pk", "subitem_count", cumulative=False ): self.assertEqual(c.subitem_count, 1) for topc in Category.objects.add_related_count( topqueryset, SubItem, "item__category_pk", "subitem_count", cumulative=False ): self.assertEqual(topc.subitem_count, 1) # Test using FK that doesn't point to a primary key, cumulative for c in Category.objects.add_related_count( queryset, SubItem, "item__category_fk", "subitem_count", cumulative=True ): self.assertEqual(c.subitem_count, 1) for topc in Category.objects.add_related_count( topqueryset, SubItem, "item__category_fk", "subitem_count", cumulative=True ): self.assertEqual(topc.subitem_count, 2) # Also works when using the FK that *does* point to a primary key, cumulative for c in Category.objects.add_related_count( queryset, SubItem, "item__category_pk", "subitem_count", cumulative=True ): self.assertEqual(c.subitem_count, 1) for topc in Category.objects.add_related_count( topqueryset, SubItem, "item__category_pk", "subitem_count", cumulative=True ): self.assertEqual(topc.subitem_count, 2) def test_add_related_count_with_extra_filters(self): """Test that filtering by extra_filters works""" queryset = Category.objects.all() # Test using FK that doesn't point to a primary key for c in Category.objects.add_related_count( queryset, Item, "category_fk", "item_count", cumulative=False, extra_filters={"name": "Halo: Reach"}, ): if c.pk == 5: self.assertEqual(c.item_count, 1) else: self.assertEqual(c.item_count, 0) # Also works when using the FK that *does* point to a primary key for c in Category.objects.add_related_count( queryset, Item, "category_pk", "item_count", cumulative=False, extra_filters={"name": "Halo: Reach"}, ): if c.pk == 5: self.assertEqual(c.item_count, 1) else: self.assertEqual(c.item_count, 0) # Test using FK that doesn't point to a primary key for c in Category.objects.add_related_count( queryset, Item, "category_fk", "item_count", cumulative=True, extra_filters={"name": "Halo: Reach"}, ): if c.pk in (5, 1): self.assertEqual(c.item_count, 1) else: self.assertEqual(c.item_count, 0) # Also works when using the FK that *does* point to a primary key for c in Category.objects.add_related_count( queryset, Item, "category_pk", "item_count", cumulative=True, extra_filters={"name": "Halo: Reach"}, ): if c.pk in (5, 1): self.assertEqual(c.item_count, 1) else: self.assertEqual(c.item_count, 0) class TestOrderedInsertionBFS(TreeTestCase): def test_insert_ordered_DFS_backwards_root_nodes(self): rock = OrderedInsertion.objects.create(name="Rock") OrderedInsertion.objects.create(name="Led Zeppelin", parent=rock) OrderedInsertion.objects.create(name="Classical") self.assertTreeEqual( OrderedInsertion.objects.all(), """ 3 - 1 0 1 2 1 - 2 0 1 4 2 1 2 1 2 3 """, ) def test_insert_ordered_BFS_backwards_root_nodes(self): rock = OrderedInsertion.objects.create(name="Rock") self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 2 """, ) OrderedInsertion.objects.create(name="Classical") self.assertTreeEqual( OrderedInsertion.objects.all(), """ 2 - 1 0 1 2 1 - 2 0 1 2 """, ) # This tends to fail if it uses `rock.tree_id`, which is 1, although # in the database Rock's tree_id has been updated to 2. OrderedInsertion.objects.create(name="Led Zeppelin", parent=rock) self.assertTreeEqual( OrderedInsertion.objects.all(), """ 2 - 1 0 1 2 1 - 2 0 1 4 3 1 2 1 2 3 """, ) def test_insert_ordered_DFS_backwards_nonroot_nodes(self): music = OrderedInsertion.objects.create(name="music") rock = OrderedInsertion.objects.create(name="Rock", parent=music) OrderedInsertion.objects.create(name="Led Zeppelin", parent=rock) OrderedInsertion.objects.create(name="Classical", parent=music) self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 4 1 1 1 2 3 2 1 1 1 4 7 3 2 1 2 5 6 """, ) def test_insert_ordered_BFS_backwards_nonroot_nodes(self): music = OrderedInsertion.objects.create(name="music") rock = OrderedInsertion.objects.create(name="Rock", parent=music) OrderedInsertion.objects.create(name="Classical", parent=music) self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 """, ) OrderedInsertion.objects.create(name="Led Zeppelin", parent=rock) self.assertTreeEqual( OrderedInsertion.objects.all(), """ 1 - 1 0 1 8 3 1 1 1 2 3 2 1 1 1 4 7 4 2 1 2 5 6 """, ) class CacheChildrenTestCase(TreeTestCase): """ Tests that the queryset function `get_cached_trees` results in a minimum number of database queries. """ fixtures = ["genres.json"] def test_genre_iter(self): """ Test a query with two root nodes. """ with self.assertNumQueries(1): root_nodes = Genre.objects.all().get_cached_trees() # `get_cached_trees` should only return the root nodes self.assertEqual(len(root_nodes), 2) # Getting the children of each node should not result in db hits. with self.assertNumQueries(0): for genre in root_nodes: self.assertIsInstance(genre, Genre) for child in genre.get_children(): self.assertIsInstance(child, Genre) for child2 in child.get_children(): self.assertIsInstance(child2, Genre) def test_hide_nodes(self): """ Test that caching a tree with missing nodes works """ root = Category.objects.create(name="Root", visible=False) child = Category.objects.create(name="Child", parent=root) root2 = Category.objects.create(name="Root2") list(Category.objects.all().get_cached_trees()) == [root, child, root2] list(Category.objects.filter(visible=True).get_cached_trees()) == [child, root2] @unittest.skipUnless( mock_signal_receiver, "Signals tests require mock_django installed" ) class Signals(TestCase): fixtures = ["categories.json"] def setUp(self): self.signal = node_moved self.wii = Category.objects.get(pk=2) self.ps3 = Category.objects.get(pk=8) def test_signal_should_not_be_sent_when_parent_hasnt_changed(self): with mock_signal_receiver(self.signal, sender=Category) as receiver: self.wii.name = "Woo" self.wii.save() self.assertEqual(receiver.call_count, 0) def test_signal_should_not_be_sent_when_model_created(self): with mock_signal_receiver(self.signal, sender=Category) as receiver: Category.objects.create(name="Descriptive name") self.assertEqual(receiver.call_count, 0) def test_move_by_using_move_to_should_send_signal(self): with mock_signal_receiver(self.signal, sender=Category) as receiver: self.wii.move_to(self.ps3) receiver.assert_called_once_with( instance=self.wii, signal=self.signal, target=self.ps3, sender=Category, position="first-child", ) def test_move_by_changing_parent_should_send_signal(self): """position is not set when sent from save(). I assume it would be the default(first-child) but didn't feel comfortable setting it. """ with mock_signal_receiver(self.signal, sender=Category) as receiver: self.wii.parent = self.ps3 self.wii.save() receiver.assert_called_once_with( instance=self.wii, signal=self.signal, target=self.ps3, sender=Category ) class DeferredAttributeTests(TreeTestCase): """ Regression tests for #176 and #424 """ def setUp(self): OrderedInsertion.objects.create(name="a") def test_deferred_order_insertion_by(self): qs = OrderedInsertion.objects.defer("name") with self.assertNumQueries(1): nodes = list(qs) with self.assertNumQueries(0): self.assertTreeEqual( nodes, """ 1 - 1 0 1 2 """, ) def test_deferred_cached_field_undeferred(self): obj = OrderedInsertion.objects.defer("name").get() self.assertEqual(obj._mptt_cached_fields["name"], DeferredAttribute) with self.assertNumQueries(1): obj.name with self.assertNumQueries(3): # does a node move, since the order_insertion_by field changed obj.save() self.assertEqual(obj._mptt_cached_fields["name"], "a") def test_deferred_cached_field_change(self): obj = OrderedInsertion.objects.defer("name").get() self.assertEqual(obj._mptt_cached_fields["name"], DeferredAttribute) with self.assertNumQueries(0): obj.name = "b" with self.assertNumQueries(3): # does a node move, since the order_insertion_by field changed obj.save() self.assertEqual(obj._mptt_cached_fields["name"], "b") class DraggableMPTTAdminTestCase(TreeTestCase): def setUp(self): self.user = User.objects.create_superuser("admin", "test@example.com", "p") self.client.login(username=self.user.username, password="p") def test_changelist(self): p1 = Person.objects.create(name="Franz") p2 = Person.objects.create(name="Fritz") p3 = Person.objects.create(name="Hans") self.assertNotEqual(p1._mpttfield("tree_id"), p2._mpttfield("tree_id")) response = self.client.get("/admin/myapp/person/") self.assertContains(response, 'class="drag-handle"', 3) self.assertContains(response, 'style="text-indent:0px"', 3) self.assertContains( response, 'src="/static/mptt/draggable-admin.js" data-context="{"', ) self.assertContains(response, '}" id="draggable-admin-context">') response = self.client.post( "/admin/myapp/person/", { "cmd": "move_node", "cut_item": p1.pk, "pasted_on": p2.pk, "position": "last-child", }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 200) p1.refresh_from_db() p2.refresh_from_db() self.assertEqual(p1.parent, p2) self.assertTreeEqual( Person.objects.all(), """ 2 - 2 0 1 4 1 2 2 1 2 3 3 - 3 0 1 2 """, ) response = self.client.get("/admin/myapp/person/") self.assertContains(response, 'style="text-indent:0px"', 2) self.assertContains(response, 'style="text-indent:20px"', 1) response = self.client.post( "/admin/myapp/person/", { "cmd": "move_node", "cut_item": p3.pk, "pasted_on": p1.pk, "position": "left", }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 200) self.assertTreeEqual( Person.objects.all(), """ 2 - 2 0 1 6 3 2 2 1 2 3 1 2 2 1 4 5 """, ) response = self.client.post( "/admin/myapp/person/", { "action": "delete_selected", "_selected_action": [1], }, ) self.assertContains(response, "Are you sure?") response = self.client.post( "/admin/myapp/person/", { "action": "delete_selected", "_selected_action": [1], "post": "yes", }, ) self.assertRedirects(response, "/admin/myapp/person/") self.assertTreeEqual( Person.objects.all(), """ 2 - 2 0 1 4 3 2 2 1 2 3 """, ) class BookAdmin(ModelAdmin): list_filter = ( ("fk", TreeRelatedFieldListFilter), ("m2m", TreeRelatedFieldListFilter), ) ordering = ("id",) class CategoryAdmin(ModelAdmin): list_filter = ( ("books_fk", TreeRelatedFieldListFilter), ("books_m2m", TreeRelatedFieldListFilter), ) ordering = ("id",) class ListFiltersTests(TestCase): def setUp(self): self.user = User.objects.create_superuser("admin", "test@example.com", "p") self.request_factory = RequestFactory() self.parent_category = Category.objects.create(name="Parent category") self.child_category1 = Category.objects.create( name="Child category1", parent=self.parent_category ) self.child_category2 = Category.objects.create( name="Child category2", parent=self.parent_category ) self.simple_category = Category.objects.create(name="Simple category") self.book1 = Book.objects.create(name="book1", fk=self.child_category1) self.book2 = Book.objects.create( name="book2", fk=self.parent_category, parent=self.book1 ) self.book3 = Book.objects.create( name="book3", fk=self.simple_category, parent=self.book1 ) self.book4 = Book.objects.create(name="book4") self.book1.m2m.add(self.child_category1) self.book2.m2m.add(self.parent_category) self.book3.m2m.add(self.simple_category) def get_request(self, path, params=None): req = self.request_factory.get(path, params) req.user = self.user return req def get_changelist(self, request, model, modeladmin): args = [ request, model, modeladmin.list_display, modeladmin.list_display_links, modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields, modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_max_show_all, modeladmin.list_editable, modeladmin, ] if hasattr(modeladmin, "sortable_by"): # New in Django 2.1 args.append(modeladmin.sortable_by) if hasattr(modeladmin, "search_help_text"): # New in Django 4.0 args.append(modeladmin.search_help_text) return ChangeList(*args) def test_treerelatedfieldlistfilter_foreignkey(self): modeladmin = BookAdmin(Book, site) request = self.get_request("/") changelist = self.get_changelist(request, Book, modeladmin) # Make sure that all categories are present in the referencing model's list filter filterspec = changelist.get_filters(request)[0][0] expected = [ ( self.parent_category.pk, self.parent_category.name, ' style="padding-left:0px"', ), ( self.child_category1.pk, self.child_category1.name, ' style="padding-left:10px"', ), ( self.child_category2.pk, self.child_category2.name, ' style="padding-left:10px"', ), ( self.simple_category.pk, self.simple_category.name, ' style="padding-left:0px"', ), ] self.assertEqual(sorted(filterspec.lookup_choices), sorted(expected)) request = self.get_request("/", {"fk__isnull": "True"}) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.book4]) # Make sure the last choice is None and is selected filterspec = changelist.get_filters(request)[0][0] choices = list(filterspec.choices(changelist)) self.assertEqual(choices[-1]["selected"], True) self.assertEqual(choices[-1]["query_string"], "?fk__isnull=True") # Make sure child's categories books included request = self.get_request( "/", {"fk__id__inhierarchy": self.parent_category.pk} ) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual((list(queryset)), [self.book1, self.book2]) # Make sure filter for child category works as expected request = self.get_request( "/", {"fk__id__inhierarchy": self.child_category1.pk} ) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual((list(queryset)), [self.book1]) # Make sure filter for empty category works as expected request = self.get_request( "/", {"fk__id__inhierarchy": self.child_category2.pk} ) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual(queryset.count(), 0) # Make sure filter for simple category with no hierarchy works as expected request = self.get_request( "/", {"fk__id__inhierarchy": self.simple_category.pk} ) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual((list(queryset)), [self.book3]) def test_treerelatedfieldlistfilter_manytomany(self): modeladmin = BookAdmin(Book, site) request = self.get_request("/") changelist = self.get_changelist(request, Book, modeladmin) # Make sure that all categories are present in the referencing model's list filter filterspec = changelist.get_filters(request)[0][1] expected = [ ( self.parent_category.pk, self.parent_category.name, ' style="padding-left:0px"', ), ( self.child_category1.pk, self.child_category1.name, ' style="padding-left:10px"', ), ( self.child_category2.pk, self.child_category2.name, ' style="padding-left:10px"', ), ( self.simple_category.pk, self.simple_category.name, ' style="padding-left:0px"', ), ] self.assertEqual(sorted(filterspec.lookup_choices), sorted(expected)) request = self.get_request("/", {"m2m__isnull": "True"}) changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.book4]) # Make sure the last choice is None and is selected filterspec = changelist.get_filters(request)[0][1] choices = list(filterspec.choices(changelist)) self.assertEqual(choices[-1]["selected"], True) self.assertEqual(choices[-1]["query_string"], "?m2m__isnull=True") # Make sure child's categories books included request = self.get_request( "/", {"m2m__id__inhierarchy": self.parent_category.pk} ) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual((list(queryset)), [self.book1, self.book2]) # Make sure filter for child category works as expected request = self.get_request( "/", {"m2m__id__inhierarchy": self.child_category1.pk} ) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual((list(queryset)), [self.book1]) # Make sure filter for empty category works as expected request = self.get_request( "/", {"fk__id__inhierarchy": self.child_category2.pk} ) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual(queryset.count(), 0) # Make sure filter for simple category with no hierarchy works as expected request = self.get_request( "/", {"m2m__id__inhierarchy": self.simple_category.pk} ) changelist = self.get_changelist(request, Book, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual((list(queryset)), [self.book3]) def test_treerelatedfieldlistfilter_reverse_relationships(self): modeladmin = CategoryAdmin(Category, site) # FK relationship ----- request = self.get_request("/", {"books_fk__isnull": "True"}) changelist = self.get_changelist(request, Category, modeladmin) # Make sure the correct queryset is returned queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.child_category2]) # Make sure the last choice is None and is selected filterspec = changelist.get_filters(request)[0][0] choices = list(filterspec.choices(changelist)) self.assertEqual(choices[-1]["selected"], True) self.assertEqual(choices[-1]["query_string"], "?books_fk__isnull=True") # Make sure child's books categories included request = self.get_request("/", {"books_fk__id__inhierarchy": self.book1.pk}) changelist = self.get_changelist(request, Category, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual( (list(queryset)), [self.parent_category, self.child_category1, self.simple_category], ) # Make sure filter for child book works as expected request = self.get_request("/", {"books_fk__id__inhierarchy": self.book2.pk}) changelist = self.get_changelist(request, Category, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual((list(queryset)), [self.parent_category]) # Make sure filter for book with no category works as expected request = self.get_request("/", {"books_fk__id__inhierarchy": self.book4.pk}) changelist = self.get_changelist(request, Category, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual(queryset.count(), 0) # M2M relationship ----- request = self.get_request("/", {"books_m2m__isnull": "True"}) changelist = self.get_changelist(request, Category, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.child_category2]) # Make sure the last choice is None and is selected filterspec = changelist.get_filters(request)[0][1] choices = list(filterspec.choices(changelist)) self.assertEqual(choices[-1]["selected"], True) self.assertEqual(choices[-1]["query_string"], "?books_m2m__isnull=True") # Make sure child's books categories included request = self.get_request("/", {"books_m2m__id__inhierarchy": self.book1.pk}) changelist = self.get_changelist(request, Category, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual( (list(queryset)), [self.parent_category, self.child_category1, self.simple_category], ) # Make sure filter for child book works as expected request = self.get_request("/", {"books_m2m__id__inhierarchy": self.book2.pk}) changelist = self.get_changelist(request, Category, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual((list(queryset)), [self.parent_category]) # Make sure filter for book with no category works as expected request = self.get_request("/", {"books_m2m__id__inhierarchy": self.book4.pk}) changelist = self.get_changelist(request, Category, modeladmin) queryset = changelist.get_queryset(request) self.assertEqual(queryset.count(), 0) class UUIDPrimaryKey(TreeTestCase): def test_save_uuid_model(self): n1 = UUIDNode.objects.create(name="node") n2 = UUIDNode.objects.create(name="sub_node", parent=n1) self.assertEqual(n1.name, "node") self.assertEqual(n1.tree_id, n2.tree_id) self.assertEqual(n2.parent, n1) def test_move_uuid_node(self): n1 = UUIDNode.objects.create(name="n1") n2 = UUIDNode.objects.create(name="n2", parent=n1) n3 = UUIDNode.objects.create(name="n3", parent=n1) self.assertEqual(list(n1.get_children()), [n2, n3]) n3.move_to(n2, "left") self.assertEqual(list(n1.get_children()), [n3, n2]) def test_move_root_node(self): root1 = UUIDNode.objects.create(name="n1") child = UUIDNode.objects.create(name="n2", parent=root1) root2 = UUIDNode.objects.create(name="n3") self.assertEqual(list(root1.get_children()), [child]) root2.move_to(child, "left") self.assertEqual(list(root1.get_children()), [root2, child]) def test_move_child_node(self): root1 = UUIDNode.objects.create(name="n1") child1 = UUIDNode.objects.create(name="n2", parent=root1) root2 = UUIDNode.objects.create(name="n3") child2 = UUIDNode.objects.create(name="n4", parent=root2) self.assertEqual(list(root1.get_children()), [child1]) child2.move_to(child1, "left") self.assertEqual(list(root1.get_children()), [child2, child1]) class DirectParentAssignment(TreeTestCase): def test_assignment(self): """Regression test for #428""" n1 = Node.objects.create() n2 = Node.objects.create() n1.parent_id = n2.id n1.save() class MovingNodeWithUniqueConstraint(TreeTestCase): def test_unique_together_move_to_same_parent_change_code(self): """Regression test for #466 1""" UniqueTogetherModel.objects.all().delete() a = UniqueTogetherModel.objects.create(code="a", parent=None) b = UniqueTogetherModel.objects.create(code="b", parent=None) a1 = UniqueTogetherModel.objects.create(code="1", parent=a) b1 = UniqueTogetherModel.objects.create(code="1", parent=b) b1.parent, b1.code = a, "2" # b1 -> a2 b1.save() self.assertTreeEqual( UniqueTogetherModel.objects.all(), """ 1 - 1 0 1 6 3 1 1 1 2 3 4 1 1 1 4 5 2 - 2 0 1 2 """, ) def test_unique_together_move_to_same_code_change_parent(self): """Regression test for #466 1""" UniqueTogetherModel.objects.all().delete() a = UniqueTogetherModel.objects.create(code="a", parent=None) b = UniqueTogetherModel.objects.create(code="b", parent=None) a1 = UniqueTogetherModel.objects.create(code="1", parent=a) a2 = UniqueTogetherModel.objects.create(code="2", parent=a) a2.parent, a2.code = b, "1" # a2 -> b1 a2.save() self.assertTreeEqual( UniqueTogetherModel.objects.all(), """ 1 - 1 0 1 4 3 1 1 1 2 3 2 - 2 0 1 4 4 2 2 1 2 3 """, ) class NullableOrderedInsertion(TreeTestCase): def test_nullable_ordered_insertion(self): genreA = NullableOrderedInsertionModel.objects.create(name="A", parent=None) genreA1 = NullableOrderedInsertionModel.objects.create(name="A1", parent=genreA) genreAnone = NullableOrderedInsertionModel.objects.create( name=None, parent=genreA ) self.assertTreeEqual( NullableOrderedInsertionModel.objects.all(), """ 1 - 1 0 1 6 3 1 1 1 2 3 2 1 1 1 4 5 """, ) def test_nullable_ordered_insertion_desc(self): genreA = NullableDescOrderedInsertionModel.objects.create(name="A", parent=None) genreA1 = NullableDescOrderedInsertionModel.objects.create( name="A1", parent=genreA ) genreAnone = NullableDescOrderedInsertionModel.objects.create( name=None, parent=genreA ) self.assertTreeEqual( NullableDescOrderedInsertionModel.objects.all(), """ 1 - 1 0 1 6 2 1 1 1 2 3 3 1 1 1 4 5 """, ) class ModelMetaIndexes(TreeTestCase): def test_no_index_set(self): class SomeModel(MPTTModel): class Meta: app_label = "myapp" tree_id_attr = getattr(SomeModel._mptt_meta, "tree_id_attr") self.assertTrue(SomeModel._meta.get_field(tree_id_attr).db_index) for key in ("right_attr", "left_attr", "level_attr"): field_name = getattr(SomeModel._mptt_meta, key) self.assertFalse(SomeModel._meta.get_field(field_name).db_index) def test_index_together(self): already_idx = [["tree_id", "lft"], ("tree_id", "lft")] no_idx = [tuple(), list()] some_idx = [["tree_id"], ("tree_id",), [["tree_id"]], (("tree_id",),)] for idx, case in enumerate(already_idx + no_idx + some_idx): class Meta: index_together = case app_label = "myapp" # Use type() here and in test_index_together_different_attr over # an explicit class X(MPTTModel):, as this throws a warning that # re-registering models with the same name (which is what an explicit # class does) could cause errors. Kind of... weird, but surprisingly # effective. SomeModel = type( str("model_{}".format(idx)), (MPTTModel,), { "Meta": Meta, "__module__": __name__, }, ) self.assertIn(("tree_id", "lft"), SomeModel._meta.index_together) def test_index_together_different_attr(self): already_idx = [["abc", "def"], ("abc", "def")] no_idx = [tuple(), list()] some_idx = [["abc"], ("abc",), [["abc"]], (("abc",),)] for idx, case in enumerate(already_idx + no_idx + some_idx): class MPTTMeta: tree_id_attr = "abc" left_attr = "def" class Meta: index_together = case app_label = "myapp" SomeModel = type( str("model__different_attr_{}".format(idx)), (MPTTModel,), {"MPTTMeta": MPTTMeta, "Meta": Meta, "__module__": str(__name__)}, ) self.assertIn(("abc", "def"), SomeModel._meta.index_together) class BulkLoadTests(TestCase): fixtures = ["categories.json"] def setUp(self): self.games = { "id": 11, "name": "Role-playing", "children": [ { "id": 12, "parent_id": 11, "name": "Single-player", }, { "id": 13, "parent_id": 11, "name": "Multi-player", }, ], } def test_bulk_root(self): data = { "id": 11, "name": "Enterprise Software", "children": [ { "id": 12, "parent_id": 11, "name": "Databases", }, { "id": 13, "parent_id": 11, "name": "Timekeeping", }, ], } records = Category.objects.build_tree_nodes(data) self.assertEqual(len(records), 3) self.assertEqual((records[0].lft, records[0].rght), (1, 6)) self.assertEqual((records[1].lft, records[1].rght), (2, 3)) self.assertEqual((records[2].lft, records[2].rght), (4, 5)) def test_bulk_last_child(self): games = Category.objects.get(id=3) records = Category.objects.build_tree_nodes(self.games, target=games) self.assertEqual(len(records), 3) for record in records: self.assertEqual(record.tree_id, games.tree_id) self.assertEqual((records[0].lft, records[0].rght), (4, 9)) self.assertEqual((records[1].lft, records[1].rght), (5, 6)) self.assertEqual((records[2].lft, records[2].rght), (7, 8)) games.refresh_from_db() self.assertEqual((games.lft, games.rght), (3, 10)) def test_bulk_left(self): games = Category.objects.get(id=3) records = Category.objects.build_tree_nodes( self.games, target=games, position="left" ) self.assertEqual(len(records), 3) for record in records: self.assertEqual(record.tree_id, games.tree_id) self.assertEqual((records[0].lft, records[0].rght), (3, 8)) self.assertEqual((records[1].lft, records[1].rght), (4, 5)) self.assertEqual((records[2].lft, records[2].rght), (6, 7)) games.refresh_from_db() self.assertEqual((games.lft, games.rght), (9, 10)) class ModelMetaTests(TestCase): def test_get_user_field_names_with_not_concrete_fields(self): """Make sure _get_user_field_names only returns concrete fields""" instance = NotConcreteFieldModel() field_names = instance._get_user_field_names() self.assertEqual(field_names, ["parent"]) django-mptt-0.13.2/tests/myapp/urls.py000066400000000000000000000001601411214034000176330ustar00rootroot00000000000000from django.contrib import admin from django.urls import path urlpatterns = [path("admin/", admin.site.urls)] django-mptt-0.13.2/tests/requirements.txt000066400000000000000000000000631411214034000204340ustar00rootroot00000000000000mock-django Django >= 2.2 coverage django-js-asset django-mptt-0.13.2/tests/runtests.sh000077500000000000000000000005371411214034000174040ustar00rootroot00000000000000#!/bin/sh set -e export PYTHONPATH="./" export DJANGO_SETTINGS_MODULE='settings' if [ `which coverage` ] ; then export COVERAGE='coverage run' else export COVERAGE='python' fi $COVERAGE -m django test --traceback --settings=$DJANGO_SETTINGS_MODULE --verbosity 2 --pythonpath="../" "$@" if [ `which coverage` ] ; then coverage report fi django-mptt-0.13.2/tests/settings.py000066400000000000000000000025251411214034000173670ustar00rootroot00000000000000import os DIRNAME = os.path.dirname(__file__) DEBUG = True DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "mydatabase"}} DEFAULT_AUTO_FIELD = "django.db.models.AutoField" INSTALLED_APPS = ( "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.messages", "django.contrib.sessions", "django.contrib.staticfiles", "mptt", "myapp", ) STATIC_URL = "/static/" SECRET_KEY = "abc123" MIDDLEWARE = [ "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ] TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] ROOT_URLCONF = "myapp.urls" # Swappable model testing MPTT_SWAPPABLE_MODEL = "myapp.SwappedInModel" django-mptt-0.13.2/tox.ini000066400000000000000000000014011411214034000153160ustar00rootroot00000000000000[tox] envlist = py{36,37,38,39}-dj{22,30,31,32} py{38,39}-dj{main} style [testenv] usedevelop = true extras = tests commands = python -Wd {envbindir}/coverage run tests/manage.py test -v2 --keepdb {posargs:myapp} coverage report -m deps = dj22: Django>=2.2,<3.0 dj30: Django>=3.0,<3.1 dj31: Django>=3.1,<3.2 dj32: Django>=3.2,<4.0 djmain: https://github.com/django/django/archive/main.tar.gz [testenv:style] deps = black flake8 isort changedir = {toxinidir} commands = isort setup.py mptt tests black . flake8 . skip_install = true [testenv:docs] deps = Sphinx sphinx-rtd-theme Django django-js-asset changedir = docs commands = make html skip_install = true whitelist_externals = make