diff -Nru python-django-mptt-0.11.0/CHANGELOG.rst python-django-mptt-0.13.2/CHANGELOG.rst --- python-django-mptt-0.11.0/CHANGELOG.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/CHANGELOG.rst 2021-08-27 10:39:28.000000000 +0000 @@ -0,0 +1,340 @@ +========== +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. diff -Nru python-django-mptt-0.11.0/create-release.sh python-django-mptt-0.13.2/create-release.sh --- python-django-mptt-0.11.0/create-release.sh 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/create-release.sh 2021-08-27 10:39:28.000000000 +0000 @@ -1,7 +1,7 @@ #!/bin/bash -ex # Clean environment, to avoid https://github.com/django-mptt/django-mptt/issues/513 -python ./setup.py clean +python3 ./setup.py clean rm -rf ./*.egg-info -python setup.py sdist bdist_wheel register upload +python3 setup.py sdist bdist_wheel diff -Nru python-django-mptt-0.11.0/debian/changelog python-django-mptt-0.13.2/debian/changelog --- python-django-mptt-0.11.0/debian/changelog 2020-05-23 12:22:44.000000000 +0000 +++ python-django-mptt-0.13.2/debian/changelog 2021-08-30 23:12:02.000000000 +0000 @@ -1,3 +1,53 @@ +python-django-mptt (0.13.2-2) unstable; urgency=medium + + * Team upload. + * python-django-mptt-doc: add Breaks: and Replaces: on older versions or + python3-django-mptt, as some files were moved from the later to the former + in the last upload (fixing a bug, but introducing another one when + upgrading). + + -- Antonio Terceiro Mon, 30 Aug 2021 20:12:02 -0300 + +python-django-mptt (0.13.2-1) unstable; urgency=medium + + * Team upload + + [ Debian Janitor ] + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + + [ Ondřej Nový ] + * d/control: Update Maintainer field with new Debian Python Team + contact address. + * d/control: Update Vcs-* fields with new Debian Python Team Salsa + layout. + + [ Debian Janitor ] + * Apply multi-arch hints. + + python-django-mptt-doc: Add Multi-Arch: foreign. + + [ Carsten Schoenert ] + * [7d78df5] d/gbp.conf: Add some more defaults + * [9d74c21] New upstream version 0.13.2 + * [3d89083] Add patches from patch queue branch + Added patches: + models-Adjust-msg-Title-underline-to-short.patch + tutorial-Remove-unknown-sphinx-directive-python.patch + * [ae5becf] d/control: Add new B-D on python3-sphinx-rtd-theme + * [b9e6089] python-django-mptt-doc: Use dh_sphinxdoc for build + * [a11009c] d/control: Order binary packages alphabetical + * [ae61a4a] d/control: Remove Breaks for obsolete binary package + The binary package python-django-mptt is now only existent in + oldoldoldstable (stretch). + * [3ef201b] d/control: Compressing the (Build-)Depends fields + * [f2cdad6] d/control: Increase to Django version >=2 while built + * [ebdb1da] d/control: Update Standards-Version to 4.6.0 + No further changes needed. + * [b3aad95] d/watch: Update to version 4 + * [f144016] d/control: Adding entry Rules-Requires-Root: no + + -- Carsten Schoenert Sat, 28 Aug 2021 09:47:35 +0200 + python-django-mptt (0.11.0-1) unstable; urgency=medium * Team upload diff -Nru python-django-mptt-0.11.0/debian/control python-django-mptt-0.13.2/debian/control --- python-django-mptt-0.11.0/debian/control 2020-05-23 12:22:44.000000000 +0000 +++ python-django-mptt-0.13.2/debian/control 2021-08-30 23:12:02.000000000 +0000 @@ -1,50 +1,57 @@ Source: python-django-mptt Section: python Priority: optional -Maintainer: Debian Python Modules Team +Maintainer: Debian Python Team Uploaders: Brian May -Build-Depends: debhelper-compat (= 13), - dh-python, - python3-all, - python3-django (>= 1.6), - python3-django-js-asset, - python3-setuptools, - python3-sphinx -Standards-Version: 4.5.0 +Build-Depends: + debhelper-compat (= 13), + dh-python, + python3-all, + python3-django (>= 2), + python3-django-js-asset, + python3-setuptools, + python3-sphinx, + python3-sphinx-rtd-theme, +Rules-Requires-Root: no +Standards-Version: 4.6.0 Homepage: https://github.com/django-mptt/django-mptt -Vcs-Git: https://salsa.debian.org/python-team/modules/python-django-mptt.git -Vcs-Browser: https://salsa.debian.org/python-team/modules/python-django-mptt +Vcs-Git: https://salsa.debian.org/python-team/packages/python-django-mptt.git +Vcs-Browser: https://salsa.debian.org/python-team/packages/python-django-mptt -Package: python3-django-mptt +Package: python-django-mptt-doc +Section: doc Architecture: all -Suggests: python-django-mptt-doc -Depends: libjs-jquery, - libjs-underscore, - python3-django, - python3-django-js-asset, - ${misc:Depends}, - ${python3:Depends} -Provides: ${python3:Provides} -Description: Modified Preorder Tree Traversal Django application +Depends: + ${misc:Depends}, + ${sphinxdoc:Depends}, +Breaks: python3-django-mptt (<< 0.13.2-1~) +Replaces: python3-django-mptt (<< 0.13.2-1~) +Multi-Arch: foreign +Description: Modified Preorder Tree Traversal Django application (documentation) Django MPTT is a reusable/standalone Django application which aims to make it easy for you to use Modified Preorder Tree Traversal with your own Django models in your own applications. . 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. + . + This package contains the documentation. -Package: python-django-mptt-doc -Section: doc +Package: python3-django-mptt Architecture: all -Depends: ${misc:Depends} -Breaks: python-django-mptt (<< 0.6.1-1~) -Replaces: python-django-mptt (<< 0.6.1-1~) -Description: Modified Preorder Tree Traversal Django application (documentation) +Suggests: python-django-mptt-doc +Depends: + libjs-jquery, + libjs-underscore, + python3-django, + python3-django-js-asset, + ${misc:Depends}, + ${python3:Depends} +Provides: ${python3:Provides} +Description: Modified Preorder Tree Traversal Django application Django MPTT is a reusable/standalone Django application which aims to make it easy for you to use Modified Preorder Tree Traversal with your own Django models in your own applications. . 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. - . - This package contains the documentation. diff -Nru python-django-mptt-0.11.0/debian/gbp.conf python-django-mptt-0.13.2/debian/gbp.conf --- python-django-mptt-0.11.0/debian/gbp.conf 2020-05-23 12:22:44.000000000 +0000 +++ python-django-mptt-0.13.2/debian/gbp.conf 2021-08-30 23:12:02.000000000 +0000 @@ -1,2 +1,16 @@ +# Configuration file for git-buildpackage and friends + [DEFAULT] +# use pristine-tar: +pristine-tar = True +# generate gz compressed orig tarball +compression = gz +debian-branch=debian/master +upstream-branch = upstream + +[pq] +patch-numbers = False + +[dch] +id-length = 7 debian-branch=debian/master diff -Nru python-django-mptt-0.11.0/debian/patches/models-Adjust-msg-Title-underline-to-short.patch python-django-mptt-0.13.2/debian/patches/models-Adjust-msg-Title-underline-to-short.patch --- python-django-mptt-0.11.0/debian/patches/models-Adjust-msg-Title-underline-to-short.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/debian/patches/models-Adjust-msg-Title-underline-to-short.patch 2021-08-30 23:12:02.000000000 +0000 @@ -0,0 +1,21 @@ +From: Carsten Schoenert +Date: Sat, 28 Aug 2021 08:42:34 +0200 +Subject: models: Adjust msg Title underline to short + +--- + docs/models.rst | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/docs/models.rst b/docs/models.rst +index f2ffb51..cf5d7df 100644 +--- a/docs/models.rst ++++ b/docs/models.rst +@@ -381,7 +381,7 @@ It is recommended to rebuild the tree inside a ``transaction.atomic()`` block + for safety and better performance. + + ``add_related_count(queryset, rel_cls, rel_field, count_attr, cumulative=False, extra_filters={})`` +-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ++~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Adds a related item count to a given ``QuerySet`` using its + `extra method`_, for a model which has a relation to this manager's diff -Nru python-django-mptt-0.11.0/debian/patches/series python-django-mptt-0.13.2/debian/patches/series --- python-django-mptt-0.11.0/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/debian/patches/series 2021-08-30 23:12:02.000000000 +0000 @@ -0,0 +1,2 @@ +models-Adjust-msg-Title-underline-to-short.patch +tutorial-Remove-unknown-sphinx-directive-python.patch diff -Nru python-django-mptt-0.11.0/debian/patches/tutorial-Remove-unknown-sphinx-directive-python.patch python-django-mptt-0.13.2/debian/patches/tutorial-Remove-unknown-sphinx-directive-python.patch --- python-django-mptt-0.11.0/debian/patches/tutorial-Remove-unknown-sphinx-directive-python.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/debian/patches/tutorial-Remove-unknown-sphinx-directive-python.patch 2021-08-30 23:12:02.000000000 +0000 @@ -0,0 +1,26 @@ +From: Carsten Schoenert +Date: Sat, 28 Aug 2021 08:54:25 +0200 +Subject: tutorial: Remove unknown sphinx directive "python" + +The build is complaining about some unknown type: + +/build/python-django-mptt-0.13.2/docs/tutorial.rst:173: WARNING: Unknown directive type "python". + +.. python:: +--- + docs/tutorial.rst | 2 -- + 1 file changed, 2 deletions(-) + +diff --git a/docs/tutorial.rst b/docs/tutorial.rst +index 327ee75..755487a 100644 +--- a/docs/tutorial.rst ++++ b/docs/tutorial.rst +@@ -170,8 +170,6 @@ 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 diff -Nru python-django-mptt-0.11.0/debian/python-django-mptt-doc.docs python-django-mptt-0.13.2/debian/python-django-mptt-doc.docs --- python-django-mptt-0.11.0/debian/python-django-mptt-doc.docs 2020-05-23 12:22:44.000000000 +0000 +++ python-django-mptt-0.13.2/debian/python-django-mptt-doc.docs 2021-08-30 23:12:02.000000000 +0000 @@ -1,3 +1,2 @@ NOTES README.rst -build/docs/html diff -Nru python-django-mptt-0.11.0/debian/rules python-django-mptt-0.13.2/debian/rules --- python-django-mptt-0.11.0/debian/rules 2020-05-23 12:22:44.000000000 +0000 +++ python-django-mptt-0.13.2/debian/rules 2021-08-30 23:12:02.000000000 +0000 @@ -3,11 +3,13 @@ export PYBUILD_NAME=django-mptt %: - dh $@ --with python3 --buildsystem=pybuild + dh $@ --with python3,sphinxdoc --buildsystem=pybuild -override_dh_auto_build: - dh_auto_build - make -C docs html +override_dh_sphinxdoc: +ifeq (,$(findstring nodoc, $(DEB_BUILD_OPTIONS))) + PYTHONPATH=. python3 -m sphinx -b html -N docs $(CURDIR)/debian/python-django-mptt-doc/usr/share/doc/python-django-mptt-doc/html + dh_sphinxdoc +endif override_dh_auto_test: PYTHONPATH=.:tests \ diff -Nru python-django-mptt-0.11.0/debian/upstream/metadata python-django-mptt-0.13.2/debian/upstream/metadata --- python-django-mptt-0.11.0/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/debian/upstream/metadata 2021-08-30 23:12:02.000000000 +0000 @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/django-mptt/django-mptt/issues +Bug-Submit: https://github.com/django-mptt/django-mptt/issues/new +Repository: https://github.com/django-mptt/django-mptt.git +Repository-Browse: https://github.com/django-mptt/django-mptt diff -Nru python-django-mptt-0.11.0/debian/watch python-django-mptt-0.13.2/debian/watch --- python-django-mptt-0.11.0/debian/watch 2020-05-23 12:22:44.000000000 +0000 +++ python-django-mptt-0.13.2/debian/watch 2021-08-30 23:12:02.000000000 +0000 @@ -1,3 +1,3 @@ -version=3 +version=4 opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ https://github.com/django-mptt/django-mptt/releases .*/(.*).tar.gz diff -Nru python-django-mptt-0.11.0/docs/changelog.rst python-django-mptt-0.13.2/docs/changelog.rst --- python-django-mptt-0.11.0/docs/changelog.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/docs/changelog.rst 2021-08-27 10:39:28.000000000 +0000 @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff -Nru python-django-mptt-0.11.0/docs/conf.py python-django-mptt-0.13.2/docs/conf.py --- python-django-mptt-0.11.0/docs/conf.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/docs/conf.py 2021-08-27 10:39:28.000000000 +0000 @@ -14,40 +14,87 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) -os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' +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' +# 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'] +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' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'django-mptt' -copyright = '2007 - 2017, Craig de Stigter, Jonathan Buchanan and others' +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_tuple = __import__("mptt").VERSION version = ".".join(str(v) for v in version_tuple) # The full version, including alpha/beta/rc tags. @@ -55,68 +102,68 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# 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 +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# 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 +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +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 = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# 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 +# 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 +# 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, @@ -125,86 +172,91 @@ # 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' +# 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 +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = 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 = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-mpttdoc' +htmlhelp_basename = "django-mpttdoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# 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'), + ( + "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 +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -212,6 +264,5 @@ # 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) + ("index", "django-mptt", "django-mptt Documentation", ["Craig de Stigter"], 1) ] diff -Nru python-django-mptt-0.11.0/docs/forms.rst python-django-mptt-0.13.2/docs/forms.rst --- python-django-mptt-0.11.0/docs/forms.rst 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/docs/forms.rst 2021-08-27 10:39:28.000000000 +0000 @@ -51,7 +51,7 @@ ``level_indicator`` argument:: category = TreeNodeChoiceField(queryset=Category.objects.all(), - level_indicator=u'+--') + level_indicator='+--') ...which for this example would result in a select with the following options:: @@ -63,6 +63,17 @@ +-- 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`` @@ -186,4 +197,4 @@ 'category_tree': Category.objects.all(), }) -.. _`move_to method`: models.html#move-to-target-position-first-child \ No newline at end of file +.. _`move_to method`: models.html#move-to-target-position-first-child diff -Nru python-django-mptt-0.11.0/docs/index.rst python-django-mptt-0.13.2/docs/index.rst --- python-django-mptt-0.11.0/docs/index.rst 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/docs/index.rst 2021-08-27 10:39:28.000000000 +0000 @@ -23,7 +23,7 @@ forms templates utilities - upgrade + changelog technical_details .. toctree:: diff -Nru python-django-mptt-0.11.0/docs/models.rst python-django-mptt-0.13.2/docs/models.rst --- python-django-mptt-0.11.0/docs/models.rst 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/docs/models.rst 2021-08-27 10:39:28.000000000 +0000 @@ -380,7 +380,7 @@ It is recommended to rebuild the tree inside a ``transaction.atomic()`` block for safety and better performance. -``add_related_count(queryset, rel_cls, rel_field, count_attr, cumulative=False)`` +``add_related_count(queryset, rel_cls, rel_field, count_attr, cumulative=False, extra_filters={})`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Adds a related item count to a given ``QuerySet`` using its @@ -402,6 +402,9 @@ 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. + Example usage in the admin -------------------------- @@ -439,7 +442,7 @@ def related_products_count(self, instance): return instance.products_count - related_product_count.short_description = 'Related products (for this specific category)' + related_products_count.short_description = 'Related products (for this specific category)' def related_products_cumulative_count(self, instance): return instance.products_cumulative_count @@ -458,7 +461,7 @@ 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) when it -is inserted, with any neccessary space already having been made for 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. diff -Nru python-django-mptt-0.11.0/docs/requirements.txt python-django-mptt-0.13.2/docs/requirements.txt --- python-django-mptt-0.11.0/docs/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/docs/requirements.txt 2021-08-27 10:39:28.000000000 +0000 @@ -0,0 +1,2 @@ +Django +django-js-asset diff -Nru python-django-mptt-0.11.0/docs/templates.rst python-django-mptt-0.13.2/docs/templates.rst --- python-django-mptt-0.11.0/docs/templates.rst 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/docs/templates.rst 2021-08-27 10:39:28.000000000 +0000 @@ -170,8 +170,8 @@ on the right:: Books -> [] - Sci-fi -> [u'Books'] - Dystopian Futures -> [u'Books', u'Sci-fi'] + Sci-fi -> ['Books'] + Dystopian Futures -> ['Books', 'Sci-fi'] Using this filter with unpacking in a ``{% for %}`` tag, you should have enough information about the tree structure to create a hierarchical diff -Nru python-django-mptt-0.11.0/docs/upgrade.rst python-django-mptt-0.13.2/docs/upgrade.rst --- python-django-mptt-0.11.0/docs/upgrade.rst 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/docs/upgrade.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,288 +0,0 @@ -============= -Upgrade notes -============= - - -UNRELEASED -========== - -End of life Python 3.4 is no longer supported. - -Support for Python 2.7, which will reach its end-of-life on January 1st 2020, -has been removed. - -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. diff -Nru python-django-mptt-0.11.0/.github/workflows/tests.yml python-django-mptt-0.13.2/.github/workflows/tests.yml --- python-django-mptt-0.11.0/.github/workflows/tests.yml 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/.github/workflows/tests.yml 2021-08-27 10:39:28.000000000 +0000 @@ -0,0 +1,49 @@ +name: 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 diff -Nru python-django-mptt-0.11.0/.gitignore python-django-mptt-0.13.2/.gitignore --- python-django-mptt-0.11.0/.gitignore 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/.gitignore 2021-08-27 10:39:28.000000000 +0000 @@ -7,3 +7,6 @@ tests/mydatabase *.egg .eggs +venv/ +.idea/ +.coverage diff -Nru python-django-mptt-0.11.0/mptt/admin.py python-django-mptt-0.13.2/mptt/admin.py --- python-django-mptt-0.11.0/mptt/admin.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/admin.py 2021-08-27 10:39:28.000000000 +0000 @@ -2,29 +2,32 @@ from django import forms, http from django.conf import settings -from django.contrib.admin.actions import delete_selected -from django.contrib.admin.options import ModelAdmin, IncorrectLookupParameters, get_content_type_for_model -from django.contrib.admin.models import LogEntry, CHANGE from django.contrib import messages -from django.db import IntegrityError, transaction -from django.utils.encoding import force_str, smart_str -from django.utils.html import format_html, mark_safe -from django.utils.translation import gettext as _, gettext_lazy 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.utils.translation import get_language_bidi +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 + +__all__ = ("MPTTModelAdmin", "MPTTAdminForm", "DraggableMPTTAdmin") +IS_GRAPPELLI_INSTALLED = "grappelli" in settings.INSTALLED_APPS class MPTTModelAdmin(ModelAdmin): @@ -35,28 +38,31 @@ """ if IS_GRAPPELLI_INSTALLED: - change_list_template = 'admin/grappelli_mptt_change_list.html' + change_list_template = "admin/grappelli_mptt_change_list.html" else: - change_list_template = 'admin/mptt_change_list.html' + 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') + 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) + db + ).complex_filter(limit_choices_to), + required=False, + ) defaults.update(kwargs) kwargs = defaults - return super(MPTTModelAdmin, self).formfield_for_foreignkey( - db_field, request, **kwargs) + return super().formfield_for_foreignkey(db_field, request, **kwargs) def get_ordering(self, request): """ @@ -72,7 +78,7 @@ trigger the post_delete hooks.) """ # If this is True, the confirmation page has been displayed - if request.POST.get('post'): + if request.POST.get("post"): n = 0 with queryset.model._tree_manager.delay_mptt_updates(): for obj in queryset: @@ -82,8 +88,8 @@ obj.delete() n += 1 self.message_user( - request, - _('Successfully deleted %(count)d items.') % {'count': n}) + request, _("Successfully deleted %(count)d items.") % {"count": n} + ) # Return None to display the change list page again return None else: @@ -91,12 +97,13 @@ return delete_selected(self, request, queryset) def get_actions(self, request): - actions = super(MPTTModelAdmin, self).get_actions(request) - if actions is not None and 'delete_selected' in actions: - actions['delete_selected'] = ( + 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')) + "delete_selected", + _("Delete selected %(verbose_name_plural)s"), + ) return actions @@ -108,8 +115,8 @@ 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. + list_display = ("tree_actions", "indented_title") # Sane defaults. + list_display_links = ("indented_title",) # Sane defaults. mptt_level_indent = 20 expand_tree_by_default = False @@ -117,17 +124,18 @@ try: url = item.get_absolute_url() except Exception: # Nevermind. - url = '' + url = "" return format_html( '
' '
', item.pk, - item._mpttfield('level'), + item._mpttfield("level"), url, ) - tree_actions.short_description = '' + + tree_actions.short_description = "" def indented_title(self, item): """ @@ -136,32 +144,37 @@ """ return format_html( '
{}
', - item._mpttfield('level') * self.mptt_level_indent, + item._mpttfield("level") * self.mptt_level_indent, item, ) - indented_title.short_description = gettext_lazy('title') + + indented_title.short_description = gettext_lazy("title") def changelist_view(self, request, *args, **kwargs): - if request.is_ajax() and request.POST.get('cmd') == 'move_node': + if request.POST.get("cmd") == "move_node": return self._move_node(request) - response = super(DraggableMPTTAdmin, self).changelist_view( - request, *args, **kwargs) + response = super().changelist_view(request, *args, **kwargs) try: - response.context_data['media'] = response.context_data['media'] + forms.Media( + response.context_data["media"] = response.context_data[ + "media" + ] + forms.Media( css={ - 'all': ['mptt/draggable-admin.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 - ), - }), + "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): @@ -173,75 +186,93 @@ 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_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}}] + 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.') + 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')) + 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.') + 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.') + 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.') + 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) + 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) + change_message = self.get_move_node_change_message( + request, cut_item, pasted_on, data_before_update + ) LogEntry.objects.log_action( - user_id=request.user.id, + 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 + change_message=change_message, ) - self.message_user( - request, - _('%s has been successfully moved.') % cut_item) - return http.HttpResponse('OK, moved.') + 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'), + "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, + "expandTreeByDefault": self.expand_tree_by_default, } def _build_tree_structure(self, queryset): @@ -261,8 +292,8 @@ mptt_opts = self.model._mptt_meta items = queryset.values_list( - 'pk', - '%s_id' % mptt_opts.parent_attr, + "pk", + "%s_id" % mptt_opts.parent_attr, ) for p_id, parent_id in items: all_nodes.setdefault( @@ -274,7 +305,8 @@ class TreeRelatedFieldListFilter(RelatedFieldListFilter): """ - Admin filter class which filters models related to parent model with all it's descendants. + Admin filter class which filters models related to parent model with all + its descendants. Usage: @@ -288,18 +320,20 @@ ('my_related_model', TreeRelatedFieldListFilter), ) """ - template = 'admin/mptt_filter.html' + + 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'): + 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(TreeRelatedFieldListFilter, self).__init__(field, request, params, - model, model_admin, field_path) + 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): @@ -315,7 +349,7 @@ 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} + {"%s__%s__in" % (self.field_path, self.rel_name): other_models} ) # #### MPTT ADDITION END return queryset.filter(**self.used_parameters) @@ -324,17 +358,22 @@ # 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) + 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} + 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]) + "right" if language_bidi else "left", + mptt_level_indent * levels_dict[pk], + ) choices.append((pk, val, mark_safe(padding_style))) return choices @@ -345,29 +384,39 @@ 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'), + "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, + "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, + "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))): + 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, + "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, } diff -Nru python-django-mptt-0.11.0/mptt/compat.py python-django-mptt-0.13.2/mptt/compat.py --- python-django-mptt-0.11.0/mptt/compat.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/compat.py 2021-08-27 10:39:28.000000000 +0000 @@ -5,7 +5,7 @@ if field.is_cached(instance): return field.get_cached_value(instance) except AttributeError: - cache_attr = '_%s_cache' % attr + cache_attr = "_%s_cache" % attr if hasattr(instance, cache_attr): return getattr(instance, cache_attr) return None diff -Nru python-django-mptt-0.11.0/mptt/exceptions.py python-django-mptt-0.13.2/mptt/exceptions.py --- python-django-mptt-0.11.0/mptt/exceptions.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/exceptions.py 2021-08-27 10:39:28.000000000 +0000 @@ -9,6 +9,7 @@ For example, attempting to make a node a child of itself. """ + pass @@ -17,4 +18,5 @@ User tried to disable updates on a model that doesn't support it (abstract, proxy or a multiple-inheritance subclass of an MPTTModel) """ + pass diff -Nru python-django-mptt-0.11.0/mptt/fields.py python-django-mptt-0.13.2/mptt/fields.py --- python-django-mptt-0.11.0/mptt/fields.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/fields.py 2021-08-27 10:39:28.000000000 +0000 @@ -2,11 +2,11 @@ Model fields for working with trees. """ from django.db import models -from django.conf import settings + from mptt.forms import TreeNodeChoiceField, TreeNodeMultipleChoiceField -__all__ = ('TreeForeignKey', 'TreeOneToOneField', 'TreeManyToManyField') +__all__ = ("TreeForeignKey", "TreeOneToOneField", "TreeManyToManyField") class TreeForeignKey(models.ForeignKey): @@ -22,17 +22,17 @@ """ Use MPTT's ``TreeNodeChoiceField`` """ - kwargs.setdefault('form_class', TreeNodeChoiceField) - return super(TreeForeignKey, self).formfield(**kwargs) + kwargs.setdefault("form_class", TreeNodeChoiceField) + return super().formfield(**kwargs) class TreeOneToOneField(models.OneToOneField): def formfield(self, **kwargs): - kwargs.setdefault('form_class', TreeNodeChoiceField) - return super(TreeOneToOneField, self).formfield(**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(TreeManyToManyField, self).formfield(**kwargs) + kwargs.setdefault("form_class", TreeNodeMultipleChoiceField) + return super().formfield(**kwargs) diff -Nru python-django-mptt-0.11.0/mptt/forms.py python-django-mptt-0.13.2/mptt/forms.py --- python-django-mptt-0.11.0/mptt/forms.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/forms.py 2021-08-27 10:39:28.000000000 +0000 @@ -12,8 +12,10 @@ __all__ = ( - 'TreeNodeChoiceField', 'TreeNodeMultipleChoiceField', - 'TreeNodePositionField', 'MoveNodeForm', + "TreeNodeChoiceField", + "TreeNodeMultipleChoiceField", + "TreeNodePositionField", + "MoveNodeForm", ) # Fields ###################################################################### @@ -21,17 +23,22 @@ class TreeNodeChoiceFieldMixin: def __init__(self, queryset, *args, **kwargs): - self.level_indicator = kwargs.pop('level_indicator', DEFAULT_LEVEL_INDICATOR) + 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'): + if hasattr(queryset, "model"): mptt_opts = queryset.model._mptt_meta queryset = queryset.order_by(mptt_opts.tree_id_attr, mptt_opts.left_attr) - super(TreeNodeChoiceFieldMixin, self).__init__(queryset, *args, **kwargs) + super().__init__(queryset, *args, **kwargs) - def _get_level_indicator(self, obj): + 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): @@ -40,45 +47,50 @@ generating option labels. """ level_indicator = self._get_level_indicator(obj) - return mark_safe(level_indicator + ' ' + conditional_escape(smart_str(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): +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' + + 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')), + (FIRST_CHILD, _("First child")), + (LAST_CHILD, _("Last child")), + (LEFT, _("Left sibling")), + (RIGHT, _("Right sibling")), ) def __init__(self, *args, **kwargs): - if 'choices' not in kwargs: - kwargs['choices'] = self.DEFAULT_CHOICES - super(TreeNodePositionField, self).__init__(*args, **kwargs) + 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() @@ -113,24 +125,26 @@ in the target options. """ self.node = node - valid_targets = kwargs.pop('valid_targets', None) - target_select_size = kwargs.pop('target_select_size', 10) - position_choices = kwargs.pop('position_choices', None) - level_indicator = kwargs.pop('level_indicator', None) - super(MoveNodeForm, self).__init__(*args, **kwargs) + 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 + 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 + self.fields["target"].level_indicator = level_indicator if position_choices: - self.fields['position'].choices = position_choices + self.fields["position"].choices = position_choices def save(self): """ @@ -143,8 +157,9 @@ redisplay the form with the error, should it occur. """ try: - self.node.move_to(self.cleaned_data['target'], - self.cleaned_data['position']) + 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) @@ -158,7 +173,7 @@ """ def __init__(self, *args, **kwargs): - super(MPTTAdminForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.instance and self.instance.pk: instance = self.instance opts = self._meta.model._mptt_meta @@ -166,20 +181,20 @@ 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) + pk__in=instance.get_descendants(include_self=True).values_list( + "pk", flat=True + ) ) parent_field.queryset = parent_qs def clean(self): - cleaned_data = super(MPTTAdminForm, self).clean() + 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')) + self._errors[opts.parent_attr].append(_("Invalid parent")) del self.cleaned_data[opts.parent_attr] return cleaned_data diff -Nru python-django-mptt-0.11.0/mptt/__init__.py python-django-mptt-0.13.2/mptt/__init__.py --- python-django-mptt-0.11.0/mptt/__init__.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/__init__.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,7 +1,11 @@ -__version__ = "0.11.0" +import django + + +__version__ = "0.13.2" VERSION = tuple(__version__.split(".")) -default_app_config = "mptt.apps.MpttConfig" +if django.VERSION < (3, 2): # pragma: no cover + default_app_config = "mptt.apps.MpttConfig" def register(*args, **kwargs): @@ -10,6 +14,7 @@ This is equivalent to just subclassing MPTTModel, but works for an already-created model. """ from mptt.models import MPTTModelBase + return MPTTModelBase.register(*args, **kwargs) Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/locale/es/LC_MESSAGES/django.mo and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/locale/es/LC_MESSAGES/django.mo differ diff -Nru python-django-mptt-0.11.0/mptt/locale/es/LC_MESSAGES/django.po python-django-mptt-0.13.2/mptt/locale/es/LC_MESSAGES/django.po --- python-django-mptt-0.11.0/mptt/locale/es/LC_MESSAGES/django.po 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/locale/es/LC_MESSAGES/django.po 2021-08-27 10:39:28.000000000 +0000 @@ -1,151 +1,202 @@ # SPANISH TRANSLATION # This file is distributed under the same license as the PACKAGE django-mptt. -# Borja Fernandez 2014. +# Guillermo Rodríguez 2020. # -#, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: django-mptt master\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-08-29 12:31+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"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" -#: admin.py:105 admin.py:107 -msgid "Add child" -msgstr "Añadir hijo" +#: mptt/admin.py:86 +#, fuzzy, python-format +#| msgid "Successfully deleted %s items." +msgid "Successfully deleted %(count)d items." +msgstr "%(count)d registros eliminados correctamente." -#: admin.py:113 admin.py:115 -msgid "View on site" -msgstr "Ver en el sitio" +#: 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" -#: admin.py:127 +#: mptt/admin.py:211 #, python-format -msgid "Successfully deleted %s items." -msgstr "%s registros eliminados correctamente" +msgid "Database error: %s" +msgstr "Error de base de datos: %s" -#: admin.py:132 +#: mptt/admin.py:227 #, python-format -msgid "Elimina seleccionado %(verbose_name_plural)s" -msgstr "" +msgid "%s has been successfully moved." +msgstr "%s ha sido movido correctamente." -#: forms.py:62 -msgid "Primer hijo" -msgstr "" +#: mptt/admin.py:238 +msgid "move node before node" +msgstr "mover nodo antes que nodo" -#: forms.py:63 -msgid "Ultimo hijo" -msgstr "" +#: mptt/admin.py:239 +msgid "move node to child position" +msgstr "mover nodo a la posición del hijo" -#: forms.py:64 -msgid "Hermano izquierda" -msgstr "" +#: mptt/admin.py:240 +msgid "move node after node" +msgstr "mover nodo después que nodo" -#: forms.py:65 -msgid "Hermano derecha" -msgstr "" +#: mptt/admin.py:241 +msgid "Collapse tree" +msgstr "Contraer árbol" -#: forms.py:183 -msgid "Padre no válido" -msgstr "" +#: mptt/admin.py:242 +msgid "Expand tree" +msgstr "Expandir árbol" -#: managers.py:413 -msgid "No se puede insertar un nodo que ya ha sido salvado." -msgstr "" +#: 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" -#: managers.py:625 managers.py:798 managers.py:834 managers.py:998 +#: 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 "Se ha proporcionado una posición no válida: %s." -msgstr "" +msgid "An invalid position was given: %s." +msgstr "Se ha dado una posición inválida: %s." -#: managers.py:784 managers.py:978 -msgid "Un nodo no se puede hacer hermano de si mismo." -msgstr "" +#: 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." -#: managers.py:957 managers.py:1082 -msgid "Un nodo no puede ser hijo de si mismo" -msgstr "" +#: 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." -#: managers.py:959 managers.py:1084 -msgid "Un nodo no puede ser hijo de alguno de sus descendientes." -msgstr "" +#: 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." -#: managers.py:980 -msgid "Un nodo no puede ser hermano de uno de sus descendientes" -msgstr "" +#: 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." -#: models.py:271 -msgid "register() espera un Django model class argument" -msgstr "" +#: mptt/models.py:299 +msgid "register() expects a Django model class argument" +msgstr "register() espera como urgumento una clase de un modelo de Django" -#: templatetags/mptt_tags.py:32 +#: 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 "" +msgstr "Se ha dado un modelo inválido al tag full_tree_for_model: %s" -#: templatetags/mptt_tags.py:56 +#: mptt/templatetags/mptt_tags.py:54 #, python-format -msgid "drilldown_tree_for_node tag modelo inválido: %s" -msgstr "" +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" -#: templatetags/mptt_tags.py:63 +#: mptt/templatetags/mptt_tags.py:61 #, python-format -msgid "drilldown_tree_for_node tag campo del model inválido: %s" -msgstr "" +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" -#: templatetags/mptt_tags.py:90 +#: mptt/templatetags/mptt_tags.py:88 #, python-format -msgid "%s tag requiere tres argumentos" -msgstr "" +msgid "%s tag requires three arguments" +msgstr "El tag %s requiere tres argumentos" -#: templatetags/mptt_tags.py:92 templatetags/mptt_tags.py:147 +#: mptt/templatetags/mptt_tags.py:90 mptt/templatetags/mptt_tags.py:146 #, python-format -msgid "segundo argumento para %s tag debe ser 'as'" -msgstr "" +msgid "second argument to %s tag must be 'as'" +msgstr "el segundo argumento del tag %s debe ser 'as'" -#: templatetags/mptt_tags.py:144 +#: mptt/templatetags/mptt_tags.py:143 #, python-format -msgid "%s tag requiere tres, siete o ocho argumentos" -msgstr "" +msgid "%s tag requires either three, four, seven, eight, or nine arguments" +msgstr "el tag %s requiere tres, cuatro, siete, ocho o nueve argumentos" -#: templatetags/mptt_tags.py:151 +#: mptt/templatetags/mptt_tags.py:158 #, python-format -msgid "Si se proporcionan 7 argumentos, el cuarto para %s debe ser 'with'" -msgstr "" +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'" -#: templatetags/mptt_tags.py:154 +#: mptt/templatetags/mptt_tags.py:162 #, python-format -msgid "Si se proporcionan siete argumentos, el sexto argumento para %s tag debe ser 'in'" -msgstr "" +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'" -#: templatetags/mptt_tags.py:159 +#: mptt/templatetags/mptt_tags.py:168 #, python-format msgid "" -"Si se proporcionan ocho argumentos, el cuarto argumento para %s tag debe ser 'cumulative'" -msgstr "" +"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'" -#: templatetags/mptt_tags.py:162 +#: mptt/templatetags/mptt_tags.py:172 #, python-format -msgid "Si se proporcionan ocho argumentos, el quinto argumento para %s tag debe ser 'count'" -msgstr "" +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'" -#: templatetags/mptt_tags.py:165 +#: mptt/templatetags/mptt_tags.py:176 #, python-format -msgid "si se proporcionan ocho argumentos, el septimo argumento para %s tag debe ser 'in'" -msgstr "" +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'" -#: templatetags/mptt_tags.py:268 +#: mptt/templatetags/mptt_tags.py:295 #, python-format -msgid "El nodo %s no es el primero en orden de profundidad" -msgstr "" +msgid "%s tag requires a queryset" +msgstr "el tag %s requiere un queryset" -#: templatetags/mptt_tags.py:345 +#: mptt/utils.py:252 #, python-format -msgid "%s tag requiere un queryset" -msgstr "" +msgid "Node %s not in depth-first order" +msgstr "El nodo %s no está en el primer orden de profundidad" diff -Nru python-django-mptt-0.11.0/mptt/managers.py python-django-mptt-0.13.2/mptt/managers.py --- python-django-mptt-0.11.0/mptt/managers.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/managers.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,74 +1,48 @@ """ A custom manager for working with trees of objects. """ -import functools import contextlib +import functools from itertools import groupby -from django.db import models, connections, router -from django.db.models import F, ManyToManyField, Max, Q +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.utils import _get_tree_model from mptt.signals import node_moved +from mptt.utils import _get_tree_model -__all__ = ('TreeManager',) +__all__ = ("TreeManager",) -COUNT_SUBQUERY = """( - SELECT COUNT(*) - FROM %(rel_table)s - WHERE %(mptt_fk)s = %(mptt_table)s.%(mptt_rel_to)s -)""" - -CUMULATIVE_COUNT_SUBQUERY = """( - SELECT COUNT(*) - FROM %(rel_table)s - WHERE %(mptt_fk)s IN - ( - SELECT m2.%(mptt_rel_to)s - FROM %(mptt_table)s m2 - WHERE m2.%(tree_id)s = %(mptt_table)s.%(tree_id)s - AND m2.%(left)s BETWEEN %(mptt_table)s.%(left)s - AND %(mptt_table)s.%(right)s - ) -)""" - -COUNT_SUBQUERY_M2M = """( - SELECT COUNT(*) - FROM %(rel_table)s j - INNER JOIN %(rel_m2m_table)s k ON j.%(rel_pk)s = k.%(rel_m2m_column)s - WHERE k.%(mptt_fk)s = %(mptt_table)s.%(mptt_pk)s -)""" - -CUMULATIVE_COUNT_SUBQUERY_M2M = """( - SELECT COUNT(*) - FROM %(rel_table)s j - INNER JOIN %(rel_m2m_table)s k ON j.%(rel_pk)s = k.%(rel_m2m_column)s - WHERE k.%(mptt_fk)s IN - ( - SELECT m2.%(mptt_pk)s - FROM %(mptt_table)s m2 - WHERE m2.%(tree_id)s = %(mptt_table)s.%(tree_id)s - AND m2.%(left)s BETWEEN %(mptt_table)s.%(left)s - AND %(mptt_table)s.%(right)s - ) -)""" +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 @@ -79,7 +53,7 @@ """ def contribute_to_class(self, model, name): - super(TreeManager, self).contribute_to_class(model, name) + super().contribute_to_class(model, name) if not model._meta.abstract: self.tree_model = _get_tree_model(model) @@ -93,10 +67,10 @@ """ Ensures that this manager always returns nodes in tree order. """ - return super(TreeManager, self).get_queryset( - *args, **kwargs - ).order_by( - self.tree_id_attr, self.left_attr + return ( + super() + .get_queryset(*args, **kwargs) + .order_by(self.tree_id_attr, self.left_attr) ) def _get_queryset_relatives(self, queryset, direction, include_self): @@ -142,19 +116,19 @@ filters = Q() - e = 'e' if include_self else '' - max_op = 'lt' + e - min_op = 'gt' + e - if direction == 'asc': + 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': + 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) + 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, @@ -164,47 +138,54 @@ max_attr, opts.parent_attr, # These fields are used by MPTTModel.update_mptt_cached_fields() - *[f.lstrip('-') for f in opts.order_insertion_by] + *[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'), - )): + 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)) + 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} + 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 + 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} + 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'], - }) + filters |= Q( + **{ + tree_key: tree, + min_key: min_max["min"], + max_key: min_max["max"], + } + ) return self.filter(filters) @@ -216,7 +197,7 @@ If ``include_self=True``, nodes in ``queryset`` will also be included in the result. """ - return self._get_queryset_relatives(queryset, 'desc', include_self) + return self._get_queryset_relatives(queryset, "desc", include_self) def get_queryset_ancestors(self, queryset, include_self=False): """ @@ -226,7 +207,7 @@ If ``include_self=True``, nodes in ``queryset`` will also be included in the result. """ - return self._get_queryset_relatives(queryset, 'asc', include_self) + return self._get_queryset_relatives(queryset, "asc", include_self) @contextlib.contextmanager def disable_mptt_updates(self): @@ -282,16 +263,14 @@ # 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__ + " 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__ + " contain the mptt fields." % self.model.__name__ ) if not self.model._mptt_updates_enabled: @@ -388,13 +367,13 @@ def _translate_lookups(self, **lookups): new_lookups = {} - join_parts = '__'.join + join_parts = "__".join for k, v in lookups.items(): - parts = k.split('__') + parts = k.split("__") new_parts = [] new_parts__append = new_parts.append for part in parts: - new_parts__append(getattr(self, part + '_attr', part)) + new_parts__append(getattr(self, part + "_attr", part)) new_lookups[join_parts(new_parts)] = v return new_lookups @@ -420,8 +399,15 @@ 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): + 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 @@ -445,65 +431,51 @@ ``cumulative`` If ``True``, the count will be for each item and all of its descendants, otherwise it will be for each item itself. - """ - connection = self._get_connection() - qn = connection.ops.quote_name - - meta = self.model._meta - mptt_field = rel_model._meta.get_field(rel_field) - if isinstance(mptt_field, ManyToManyField): - if cumulative: - subquery = CUMULATIVE_COUNT_SUBQUERY_M2M % { - 'rel_table': qn(rel_model._meta.db_table), - 'rel_pk': qn(rel_model._meta.pk.column), - 'rel_m2m_table': qn(mptt_field.m2m_db_table()), - 'rel_m2m_column': qn(mptt_field.m2m_column_name()), - 'mptt_fk': qn(mptt_field.m2m_reverse_name()), - 'mptt_table': qn(self.tree_model._meta.db_table), - 'mptt_pk': qn(meta.pk.column), - 'tree_id': qn(meta.get_field(self.tree_id_attr).column), - 'left': qn(meta.get_field(self.left_attr).column), - 'right': qn(meta.get_field(self.right_attr).column), - } - else: - subquery = COUNT_SUBQUERY_M2M % { - 'rel_table': qn(rel_model._meta.db_table), - 'rel_pk': qn(rel_model._meta.pk.column), - 'rel_m2m_table': qn(mptt_field.m2m_db_table()), - 'rel_m2m_column': qn(mptt_field.m2m_column_name()), - 'mptt_fk': qn(mptt_field.m2m_reverse_name()), - 'mptt_table': qn(self.tree_model._meta.db_table), - 'mptt_pk': qn(meta.pk.column), - } + ``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: - if cumulative: - subquery = CUMULATIVE_COUNT_SUBQUERY % { - 'rel_table': qn(rel_model._meta.db_table), - 'mptt_fk': qn(rel_model._meta.get_field(rel_field).column), - 'mptt_table': qn(self.tree_model._meta.db_table), - 'mptt_rel_to': qn(mptt_field.remote_field.field_name), - 'tree_id': qn(meta.get_field(self.tree_id_attr).column), - 'left': qn(meta.get_field(self.left_attr).column), - 'right': qn(meta.get_field(self.right_attr).column), - } + 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: - subquery = COUNT_SUBQUERY % { - 'rel_table': qn(rel_model._meta.db_table), - 'mptt_fk': qn(rel_model._meta.get_field(rel_field).column), - 'mptt_table': qn(self.tree_model._meta.db_table), - 'mptt_rel_to': qn(mptt_field.remote_field.field_name), - } - return queryset.extra(select={count_attr: subquery}) + 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): + def insert_node( + self, + node, + target, + position="last-child", + save=False, + allow_existing_pk=False, + refresh_target=True, + ): """ Sets up the tree state for ``node`` (which has not yet been inserted into in the database) so it will be positioned relative to a given ``target`` node as specified by ``position`` (when - appropriate) it is inserted, with any neccessary space already + appropriate) it is inserted, with any necessary space already having been made for it. A ``target`` of ``None`` indicates that ``node`` should be @@ -518,7 +490,7 @@ """ 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.')) + raise ValueError(_("Cannot insert a node which has already been saved.")) if target is None: tree_id = self._get_next_tree_id() @@ -527,13 +499,13 @@ 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']: + 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': + if position == "left": tree_id = target_tree_id space_target = target_tree_id - 1 else: @@ -554,8 +526,13 @@ # 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) + ( + 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) @@ -574,16 +551,24 @@ return node @delegate_manager - def _move_node(self, node, target, position='last-child', save=True, refresh_target=True): + 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) + 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'): + 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(): @@ -591,7 +576,7 @@ else: self._move_child_node(node, target, position) - def move_node(self, node, target, position='last-child'): + 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 @@ -616,8 +601,9 @@ """ self._move_node(node, target, position=position) node.save() - node_moved.send(sender=node.__class__, instance=node, - target=target, position=position) + node_moved.send( + sender=node.__class__, instance=node, target=target, position=position + ) @delegate_manager def root_node(self, tree_id): @@ -643,13 +629,14 @@ 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) + 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 @@ -663,18 +650,19 @@ 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) + 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) + " 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'): + 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 @@ -706,15 +694,15 @@ opts = self.model._mptt_meta if target: tree_id = target.tree_id - if position in ('left', 'right'): + if position in ("left", "right"): level = getattr(target, opts.level_attr) - if position == 'left': + 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': + if position == "first-child": cursor = getattr(target, opts.left_attr) + 1 else: cursor = getattr(target, opts.right_attr) @@ -727,7 +715,7 @@ def treeify(data, cursor=1, level=0): data = dict(data) - children = data.pop('children', []) + children = data.pop("children", []) node = self.model(**data) stack.append(node) setattr(node, opts.tree_id_attr, tree_id) @@ -753,25 +741,21 @@ 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) + child_ids = qs.values_list("pk", flat=True) rebuild_helper = self._rebuild_helper for child_id in child_ids: right = rebuild_helper(child_id, right, tree_id, level + 1) - qs = self.model._default_manager.filter(pk=pk) - self._mptt_update( - qs, - left=left, - right=right, - level=level, - tree_id=tree_id - ) + 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) + 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: @@ -793,22 +777,22 @@ 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': + 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': + 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) + raise ValueError(_("An invalid position was given: %s.") % position) left_right_change = left - space_target - 1 @@ -851,8 +835,8 @@ return max_tree_id + 1 def _inter_tree_move_and_close_gap( - self, node, level_change, - left_right_change, new_tree_id): + 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 @@ -885,11 +869,11 @@ 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), + "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) @@ -897,13 +881,23 @@ 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) + 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() @@ -952,21 +946,21 @@ a special case which involves shuffling tree ids around. """ if node == target: - raise InvalidMove(_('A node may not be made a sibling of itself.')) + 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': + if position == "left": space_target = target_tree_id - 1 new_tree_id = target_tree_id - elif position == 'right': + 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) + raise ValueError(_("An invalid position was given: %s.") % position) self._create_tree_space(space_target) if tree_id > space_target: @@ -977,7 +971,7 @@ setattr(node, self.tree_id_attr, tree_id + 1) self._make_child_root_node(node, new_tree_id) else: - if position == 'left': + if position == "left": if target_tree_id > tree_id: left_sibling = target.get_previous_sibling() if node == left_sibling: @@ -989,7 +983,7 @@ new_tree_id = target_tree_id lower_bound, upper_bound = new_tree_id, tree_id shift = 1 - elif position == 'right': + elif position == "right": if target_tree_id > tree_id: new_tree_id = target_tree_id lower_bound, upper_bound = tree_id, target_tree_id @@ -1002,7 +996,7 @@ lower_bound, upper_bound = new_tree_id, tree_id shift = 1 else: - raise ValueError(_('An invalid position was given: %s.') % position) + raise ValueError(_("An invalid position was given: %s.") % position) connection = self._get_connection(instance=node) qn = connection.ops.quote_name @@ -1014,13 +1008,15 @@ 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), + "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]) + 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): @@ -1048,14 +1044,15 @@ 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), + "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]) + cursor.execute( + space_query, [target, size, target, size, tree_id, target, target] + ) def _move_child_node(self, node, target, position): """ @@ -1085,8 +1082,13 @@ 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) + ( + 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 @@ -1094,7 +1096,8 @@ 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) + node, level_change, left_right_change, new_tree_id + ) # Update the node to be consistent with the updated # tree in the database. @@ -1123,12 +1126,14 @@ 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" or position == "first-child": if node == target: - raise InvalidMove(_('A node may not be made a child of itself.')) + 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': + 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 @@ -1144,12 +1149,14 @@ new_right = target_left + width level_change = level - target_level - 1 parent = target - elif position == 'left' or position == 'right': + elif position == "left" or position == "right": if node == target: - raise InvalidMove(_('A node may not be made a sibling of itself.')) + 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': + 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 @@ -1166,7 +1173,7 @@ level_change = level - target_level parent = getattr(target, self.parent_attr) else: - raise ValueError(_('An invalid position was given: %s.') % position) + raise ValueError(_("An invalid position was given: %s.") % position) left_boundary = min(left, new_left) right_boundary = max(right, new_right) @@ -1202,21 +1209,35 @@ 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), + "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]) + 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. @@ -1243,12 +1264,19 @@ width = right - left + 1 if node == target: - raise InvalidMove(_('A node may not be made a child of itself.')) + 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.')) + 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) + ( + 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) @@ -1266,18 +1294,26 @@ %(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), + "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]) + 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. diff -Nru python-django-mptt-0.11.0/mptt/models.py python-django-mptt-0.13.2/mptt/models.py --- python-django-mptt-0.11.0/mptt/models.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/models.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,24 +1,28 @@ -from functools import reduce, wraps import 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, TreeOneToOneField, TreeManyToManyField +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', + "TreeForeignKey", + "TreeOneToOneField", + "TreeManyToManyField", + "TreeManager", + "MPTTOptions", + "MPTTModelBase", + "MPTTModel", ) @@ -38,15 +42,15 @@ class classpropertytype(property): def __init__(self, name, bases=(), members={}): - return super(classpropertytype, self).__init__( - members.get('__get__'), - members.get('__set__'), - members.get('__delete__'), - members.get('__doc__') + return super().__init__( + members.get("__get__"), + members.get("__set__"), + members.get("__delete__"), + members.get("__doc__"), ) -classproperty = classpropertytype('classproperty') +classproperty = classpropertytype("classproperty") class MPTTOptions: @@ -60,11 +64,11 @@ """ order_insertion_by = [] - left_attr = 'lft' - right_attr = 'rght' - tree_id_attr = 'tree_id' - level_attr = 'level' - parent_attr = 'parent' + 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 @@ -74,13 +78,14 @@ opts = [] opts.extend(list(kwargs.items())) - if 'tree_manager_attr' in [opt[0] for opt in opts]: + 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.") + " a TreeManager as a normal manager on your model instead." + ) for key, value in opts: - if key[:2] == '__': + if key[:2] == "__": continue setattr(self, key, value) @@ -93,7 +98,7 @@ self.order_insertion_by = [] def __iter__(self): - return ((k, v) for k, v in self.__dict__.items() if k[0] != '_') + 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): @@ -125,18 +130,20 @@ so that the MPTT fields need to be updated. """ instance._mptt_cached_fields = {} - field_names = set((self.parent_attr,)) + field_names = {self.parent_attr} if self.order_insertion_by: for f in self.order_insertion_by: - if f[0] == '-': + 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__: + 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 @@ -145,7 +152,8 @@ instance._mptt_cached_fields[field_name] = DeferredAttribute continue instance._mptt_cached_fields[field_name] = self.get_raw_field_value( - instance, field_name) + instance, field_name + ) def insertion_target_filters(self, instance, order_insertion_by): """ @@ -169,11 +177,11 @@ and_ = operator.and_ or_ = operator.or_ for field_name in order_insertion_by: - if field_name[0] == '-': + if field_name[0] == "-": field_name = field_name[1:] - filter_suffix = '__lt' + filter_suffix = "__lt" else: - filter_suffix = '__gt' + filter_suffix = "__gt" value = getattr(instance, field_name) if value is None: # node isn't saved yet. get the insertion value from pre_save. @@ -183,8 +191,8 @@ 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' + value = filter_suffix == "__lt" + filter_suffix = "__isnull" q = Q(**{field_name + filter_suffix: value}) @@ -204,7 +212,9 @@ right_sibling = None # Optimisation - if the parent doesn't have descendants, # the node will always be its last child. - if parent is None or parent.get_descendant_count() > 0: + 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) @@ -218,8 +228,11 @@ # 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) + 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: @@ -241,16 +254,17 @@ - adds the MPTT fields to the class - adds a TreeManager to the model """ - if class_name == 'NewBase' and class_dict == {}: - return super(MPTTModelBase, meta).__new__(meta, class_name, bases, class_dict) + 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) + MPTTMeta = class_dict.pop("MPTTMeta", None) if not MPTTMeta: + class MPTTMeta: pass @@ -258,15 +272,15 @@ # extend MPTTMeta from base classes for base in bases: - if hasattr(base, '_mptt_meta'): + if hasattr(base, "_mptt_meta"): for name, value in base._mptt_meta: - if name == 'tree_manager_attr': + if name == "tree_manager_attr": continue if name not in initial_options: setattr(MPTTMeta, name, value) - class_dict['_mptt_meta'] = MPTTOptions(MPTTMeta) - super_new = super(MPTTModelBase, meta).__new__ + class_dict["_mptt_meta"] = MPTTOptions(MPTTMeta) + super_new = super().__new__ cls = super_new(meta, class_name, bases, class_dict) cls = meta.register(cls) @@ -277,8 +291,10 @@ 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): + 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: @@ -298,10 +314,10 @@ if not issubclass(cls, models.Model): raise ValueError(_("register() expects a Django model class argument")) - if not hasattr(cls, '_mptt_meta'): + if not hasattr(cls, "_mptt_meta"): cls._mptt_meta = MPTTOptions(**kwargs) - abstract = getattr(cls._meta, 'abstract', False) + abstract = getattr(cls._meta, "abstract", False) try: MPTTModel @@ -333,16 +349,25 @@ # 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]) + 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) + 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 = 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 @@ -356,8 +381,9 @@ # make sure we have a tree manager somewhere tree_manager = None # Use the default manager defined on the class if any - _meta = cls._meta - if cls._default_manager and isinstance(cls._default_manager, TreeManager): + if cls._default_manager and isinstance( + cls._default_manager, TreeManager + ): tree_manager = cls._default_manager else: for cls_manager in cls._meta.managers: @@ -368,7 +394,10 @@ break if is_cls_tree_model: - idx_together = (cls._mptt_meta.tree_id_attr, cls._mptt_meta.left_attr) + 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,) @@ -377,22 +406,23 @@ tree_manager = tree_manager._copy_to_model(cls) elif tree_manager is None: tree_manager = TreeManager() - tree_manager.contribute_to_class(cls, '_tree_manager') + tree_manager.contribute_to_class(cls, "_tree_manager") # avoid using ManagerDescriptor, so instances can refer to self._tree_manager - setattr(cls, '_tree_manager', tree_manager) + setattr(cls, "_tree_manager", tree_manager) return cls def raise_if_unsaved(func): @wraps(func) def _fn(self, *args, **kwargs): - if not self.pk: + if self._state.adding: raise ValueError( - 'Cannot call %(function)s on unsaved %(class)s instances' - % {'function': func.__name__, 'class': self.__class__.__name__} + "Cannot call %(function)s on unsaved %(class)s instances" + % {"function": func.__name__, "class": self.__class__.__name__} ) return func(self, *args, **kwargs) + return _fn @@ -407,47 +437,52 @@ objects = TreeManager() def __init__(self, *args, **kwargs): - super(MPTTModel, self).__init__(*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') + 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) + 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." + 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'): + 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 ( + 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 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 @@ -470,8 +505,7 @@ if num_inserted < 0: deleted = range(tree_id + num_inserted, -num_inserted) changes.difference_update(deleted) - new_changes = set( - (t + num_inserted if t >= tree_id else t) for t in changes) + 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 @@ -488,6 +522,7 @@ 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() @@ -495,11 +530,9 @@ # Filter on pk for efficiency. qs = self._tree_manager.filter(pk=self.pk) else: - opts = self._mptt_meta - order_by = opts.left_attr if ascending: - order_by = '-' + order_by + order_by = "-" + order_by left = getattr(self, opts.left_attr) right = getattr(self, opts.right_attr) @@ -511,12 +544,12 @@ qs = self._tree_manager._mptt_filter( left__lte=left, right__gte=right, - tree_id=self._mpttfield('tree_id'), + tree_id=self._mpttfield("tree_id"), ) qs = qs.order_by(order_by) - if hasattr(self, '_mptt_use_cached_ancestors'): + 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. @@ -545,17 +578,21 @@ 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'), - }) + 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) @@ -573,7 +610,7 @@ 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'): + 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 @@ -607,20 +644,18 @@ right -= 1 return self._tree_manager._mptt_filter( - tree_id=self._mpttfield('tree_id'), - left__gte=left, - left__lte=right + 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: + if self._mpttfield("right") is None: # node not saved yet return 0 else: - return (self._mpttfield('right') - self._mpttfield('left') - 1) // 2 + return (self._mpttfield("right") - self._mpttfield("left") - 1) // 2 @raise_if_unsaved def get_leafnodes(self, include_self=False): @@ -634,8 +669,7 @@ descendants = self.get_descendants(include_self=include_self) return self._tree_manager._mptt_filter( - descendants, - left=(models.F(self._mptt_meta.right_attr) - 1) + descendants, left=(models.F(self._mptt_meta.right_attr) - 1) ) @raise_if_unsaved @@ -649,13 +683,13 @@ qs = self._tree_manager._mptt_filter( qs, parent=None, - tree_id__gt=self._mpttfield('tree_id'), + 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'), + parent__pk=getattr(self, self._mptt_meta.parent_attr + "_id"), + left__gt=self._mpttfield("right"), ) siblings = qs[:1] @@ -673,16 +707,16 @@ qs = self._tree_manager._mptt_filter( qs, parent=None, - tree_id__lt=self._mpttfield('tree_id'), + tree_id__lt=self._mpttfield("tree_id"), ) - qs = qs.order_by('-' + opts.tree_id_attr) + 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'), + parent__pk=getattr(self, opts.parent_attr + "_id"), + right__lt=self._mpttfield("left"), ) - qs = qs.order_by('-' + opts.right_attr) + qs = qs.order_by("-" + opts.right_attr) siblings = qs[:1] return siblings and siblings[0] or None @@ -696,7 +730,7 @@ return self return self._tree_manager._mptt_filter( - tree_id=self._mpttfield('tree_id'), + tree_id=self._mpttfield("tree_id"), parent=None, ).get() @@ -713,7 +747,7 @@ if self.is_root_node(): queryset = self._tree_manager._mptt_filter(parent=None) else: - parent_id = getattr(self, self._mptt_meta.parent_attr + '_id') + 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) @@ -725,15 +759,26 @@ """ 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): + 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) + self, + target, + position, + save, + allow_existing_pk=allow_existing_pk, + refresh_target=refresh_target, + ) def is_child_node(self): """ @@ -754,7 +799,7 @@ Returns ``True`` if this model instance is a root node, ``False`` otherwise. """ - return getattr(self, self._mptt_meta.parent_attr + '_id') is None + return getattr(self, self._mptt_meta.parent_attr + "_id") is None @raise_if_unsaved def is_descendant_of(self, other, include_self=False): @@ -774,9 +819,9 @@ 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)) + 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): @@ -789,7 +834,7 @@ return True return other.is_descendant_of(self) - def move_to(self, target, position='first-child'): + def move_to(self, target, position="first-child"): """ Convenience method for calling ``TreeManager.move_node`` with this model instance. @@ -800,35 +845,42 @@ 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: + 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'): + 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. """ + """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.fields: - if (field.name not in internal_fields) and (not isinstance(field, AutoField)) and (not field.primary_key): # noqa + 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 neccessary, + 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 @@ -851,33 +903,37 @@ 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: + 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 + 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) + setattr(self, opts.level_attr, parent._mpttfield("level") + 1) + setattr(self, opts.tree_id_attr, parent._mpttfield("tree_id")) + self._tree_manager._post_insert_update_cached_parent_right( + parent, 2 + ) else: setattr(self, opts.left_attr, 1) setattr(self, opts.right_attr, 2) setattr(self, opts.level_attr, 0) setattr(self, opts.tree_id_attr, 0) - return super(MPTTModel, self).save(*args, **kwargs) + 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) + 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'))): + 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: @@ -887,7 +943,10 @@ 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: + 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): @@ -895,27 +954,25 @@ break if not do_updates and not same_order: same_order = True - self.__class__._mptt_track_tree_modified(self._mpttfield('tree_id')) + 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') + collapse_old_tree = self._mpttfield("tree_id") parent = getattr(self, opts.parent_attr) - tree_id = parent._mpttfield('tree_id') - left = parent._mpttfield('left') + 1 + 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) + 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 = None - if opts.order_insertion_by: - right_sibling = opts.get_ordered_insertion_target(self, parent) + 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, @@ -926,43 +983,61 @@ # directly -- then we certainly do not have to update # the cached parent. update_cached_parent = parent and ( - getattr(self, opts.tree_id_attr) != getattr(parent, opts.tree_id_attr) or # noqa - getattr(self, opts.left_attr) < getattr(parent, opts.left_attr) or - getattr(self, opts.right_attr) > getattr(parent, opts.right_attr)) + 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) + 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] + pk=self.pk + ).order_by("-" + opts.tree_id_attr)[0] self._tree_manager._move_node( - self, rightmost_sibling, 'right', save=False, - refresh_target=False) + self, + rightmost_sibling, + "right", + save=False, + refresh_target=False, + ) except IndexError: pass else: self._tree_manager._move_node( - self, parent, 'last-child', save=False) + 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) + 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)) + 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: @@ -981,7 +1056,7 @@ else: # new node, do an insert - if (getattr(self, opts.left_attr) and getattr(self, opts.right_attr)): + if getattr(self, opts.left_attr) and getattr(self, opts.right_attr): # This node has already been set up for insertion. pass else: @@ -998,26 +1073,34 @@ 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) + 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) + parent, right_shift + ) else: # Default insertion - self.insert_at(parent, position='last-child', allow_existing_pk=True) + self.insert_at( + parent, position="last-child", allow_existing_pk=True + ) try: - super(MPTTModel, self).save(*args, **kwargs) + 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): @@ -1027,30 +1110,34 @@ 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. """ + ``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,] + 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 as e: + 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') + 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) + self._tree_manager._post_insert_update_cached_parent_right( + parent, right_shift + ) + + return super().delete(*args, **kwargs) - return super(MPTTModel, self).delete(*args, **kwargs) delete.alters_data = True def _mptt_refresh(self): @@ -1058,11 +1145,15 @@ 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] + 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) diff -Nru python-django-mptt-0.11.0/mptt/querysets.py python-django-mptt-0.13.2/mptt/querysets.py --- python-django-mptt-0.11.0/mptt/querysets.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/querysets.py 2021-08-27 10:39:28.000000000 +0000 @@ -4,11 +4,23 @@ 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): @@ -16,6 +28,7 @@ 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): diff -Nru python-django-mptt-0.11.0/mptt/settings.py python-django-mptt-0.13.2/mptt/settings.py --- python-django-mptt-0.11.0/mptt/settings.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/settings.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,5 +1,5 @@ from django.conf import settings + """Default level indicator. By default is `'---'`.""" -DEFAULT_LEVEL_INDICATOR = getattr(settings, 'MPTT_DEFAULT_LEVEL_INDICATOR', - '---') +DEFAULT_LEVEL_INDICATOR = getattr(settings, "MPTT_DEFAULT_LEVEL_INDICATOR", "---") diff -Nru python-django-mptt-0.11.0/mptt/signals.py python-django-mptt-0.13.2/mptt/signals.py --- python-django-mptt-0.11.0/mptt/signals.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/signals.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,12 +1,9 @@ from 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(providing_args=[ - 'instance', - 'target', - 'position' -]) +node_moved = ModelSignal() Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/arrow-move-black.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/arrow-move-black.png differ Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/arrow-move.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/arrow-move.png differ Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/arrow-move-white.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/arrow-move-white.png differ Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/disclosure-down-black.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/disclosure-down-black.png differ Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/disclosure-down.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/disclosure-down.png differ Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/disclosure-down-white.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/disclosure-down-white.png differ Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/disclosure-right-black.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/disclosure-right-black.png differ Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/disclosure-right.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/disclosure-right.png differ Binary files /tmp/tmp7ixvk0tk/ZrjL8drqzA/python-django-mptt-0.11.0/mptt/static/mptt/disclosure-right-white.png and /tmp/tmp7ixvk0tk/dQjBkAWtzr/python-django-mptt-0.13.2/mptt/static/mptt/disclosure-right-white.png differ diff -Nru python-django-mptt-0.11.0/mptt/static/mptt/draggable-admin.css python-django-mptt-0.13.2/mptt/static/mptt/draggable-admin.css --- python-django-mptt-0.11.0/mptt/static/mptt/draggable-admin.css 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/static/mptt/draggable-admin.css 2021-08-27 10:39:28.000000000 +0000 @@ -22,6 +22,15 @@ #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; diff -Nru python-django-mptt-0.11.0/mptt/templatetags/mptt_admin.py python-django-mptt-0.13.2/mptt/templatetags/mptt_admin.py --- python-django-mptt-0.11.0/mptt/templatetags/mptt_admin.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/templatetags/mptt_admin.py 2021-08-27 10:39:28.000000000 +0000 @@ -3,30 +3,37 @@ from django.conf import settings from django.contrib.admin.templatetags.admin_list import ( - result_hidden_fields, result_headers) + 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) + 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 -from django.contrib.admin.templatetags.admin_list import _coerce_field_name 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 +MPTT_ADMIN_LEVEL_INDENT = getattr(settings, "MPTT_ADMIN_LEVEL_INDENT", 10) +IS_GRAPPELLI_INSTALLED = True if "grappelli" in settings.INSTALLED_APPS else False ### @@ -49,14 +56,13 @@ # #### MPTT ADDITION START # figure out which field to indent - mptt_indent_field = getattr(cl.model_admin, 'mptt_indent_field', None) + 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'): + if mptt_indent_field is None and field_name != "action_checkbox": mptt_indent_field = field_name else: # first model field, use this one @@ -64,25 +70,29 @@ break # figure out how much to indent - mptt_level_indent = getattr(cl.model_admin, 'mptt_level_indent', MPTT_ADMIN_LEVEL_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)] + 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) + 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) + 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 @@ -90,12 +100,14 @@ 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 + "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') + row_classes.append("nowrap") else: # #### MPTT SUBSTITUTION START is_many_to_one = isinstance(f.remote_field, models.ManyToOneRel) @@ -110,25 +122,31 @@ # #### 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)) + 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)) + padding_attr = mark_safe( + ' style="padding-%s:%spx"' + % ( + "right" if get_language_bidi() else "left", + 8 + mptt_level_indent * level, + ) + ) else: - padding_attr = '' + 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' + table_tag = "th" if first else "td" first = False # Display link to the result's change_view if the url exists, else @@ -139,7 +157,8 @@ link_or_text = result_repr else: url = add_preserved_filters( - {'preserved_filters': cl.preserved_filters, 'opts': cl.opts}, url, + {"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. @@ -151,35 +170,40 @@ if cl.is_popup: opener = format_html(' data-popup-opener="{}"', value) else: - opener = '' + opener = "" link_or_text = format_html( - '{}', - url, - opener, - result_repr) + '{}', url, opener, result_repr + ) # #### MPTT SUBSTITUTION START - yield format_html('<{}{}{}>{}', - table_tag, - row_class, - padding_attr, - link_or_text, - table_tag) + 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)): + 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) + 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])) + yield format_html("{}", force_str(form[cl.model._meta.pk.name])) def mptt_results(cl): @@ -195,17 +219,21 @@ """ 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))} + 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) + "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) + mptt_result_list = register.inclusion_tag("admin/mptt_change_list_results.html")( + mptt_result_list + ) diff -Nru python-django-mptt-0.11.0/mptt/templatetags/mptt_tags.py python-django-mptt-0.13.2/mptt/templatetags/mptt_tags.py --- python-django-mptt-0.11.0/mptt/templatetags/mptt_tags.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/templatetags/mptt_tags.py 2021-08-27 10:39:28.000000000 +0000 @@ -9,32 +9,40 @@ 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) +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('.')) + 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 + _("full_tree_for_model tag was given an invalid model: %s") % self.model ) context[self.context_var] = cls._tree_manager.all() - return '' + return "" class DrilldownTreeForNodeNode(template.Node): - def __init__(self, node, context_var, foreign_key=None, count_attr=None, - cumulative=False, all_descendants=False): + 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 @@ -47,23 +55,28 @@ args = [self.node.resolve(context)] if self.foreign_key is not None: - app_label, model_name, fk_attr = self.foreign_key.split('.') + 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]) + _("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 + _( + "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 '' + context[self.context_var] = drilldown_tree_for_node( + *args, all_descendants=self.all_descendants + ) + return "" @register.tag @@ -85,13 +98,17 @@ """ 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]) + 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') +@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 @@ -140,44 +157,71 @@ 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': + _("%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]) + _("second argument to %s tag must be 'as'") % bits[0] + ) all_descendants = False if len_bits > 4: - if bits[4] == 'all_descendants': + if bits[4] == "all_descendants": len_bits -= 1 bits.pop(4) all_descendants = True if len_bits == 8: - if bits[4] != 'count': + 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': + _( + "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) + % bits[0] + ) + return DrilldownTreeForNodeNode( + bits[1], bits[3], bits[5], bits[7], all_descendants=all_descendants + ) elif len_bits == 9: - if bits[4] != 'cumulative': + 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': + _( + "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': + _( + "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) + _( + "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) + return DrilldownTreeForNodeNode( + bits[1], bits[3], all_descendants=all_descendants + ) @register.filter @@ -211,14 +255,14 @@ """ kwargs = {} if features: - feature_names = features.split(',') - if 'ancestors' in feature_names: - kwargs['ancestors'] = True + 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=' :: '): +def tree_path(items, separator=" :: "): """ Creates a tree path represented by a list of ``items`` by joining the items with a ``separator``. @@ -237,6 +281,7 @@ # ## RECURSIVE TAGS + @register.filter def cache_tree_children(queryset): """ @@ -256,8 +301,8 @@ context.push() for child in node.get_children(): bits.append(self._render_node(context, child)) - context['node'] = node - context['children'] = mark_safe(''.join(bits)) + context["node"] = node + context["children"] = mark_safe("".join(bits)) rendered = self.template_nodes.render(context) context.pop() return rendered @@ -266,7 +311,7 @@ queryset = self.queryset_var.resolve(context) roots = cache_tree_children(queryset) bits = [self._render_node(context, node) for node in roots] - return ''.join(bits) + return "".join(bits) @register.tag @@ -292,11 +337,11 @@ """ bits = token.contents.split() if len(bits) != 2: - raise template.TemplateSyntaxError(_('%s tag requires a queryset') % bits[0]) + raise template.TemplateSyntaxError(_("%s tag requires a queryset") % bits[0]) queryset_var = template.Variable(bits[1]) - template_nodes = parser.parse(('endrecursetree',)) + template_nodes = parser.parse(("endrecursetree",)) parser.delete_first_token() return RecurseTreeNode(template_nodes, queryset_var) diff -Nru python-django-mptt-0.11.0/mptt/utils.py python-django-mptt-0.13.2/mptt/utils.py --- python-django-mptt-0.11.0/mptt/utils.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/mptt/utils.py 2021-08-27 10:39:28.000000000 +0000 @@ -9,8 +9,13 @@ from django.utils.translation import gettext as _ -__all__ = ('previous_current_next', 'tree_item_iterator', - 'drilldown_tree_for_node', 'get_cached_trees',) + +__all__ = ( + "previous_current_next", + "tree_item_iterator", + "drilldown_tree_for_node", + "get_cached_trees", +) def previous_current_next(items): @@ -62,8 +67,8 @@ are given on the right:: Books -> [] - Sci-fi -> [u'Books'] - Dystopian Futures -> [u'Books', u'Sci-fi'] + 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 @@ -79,37 +84,35 @@ current_level = getattr(current, opts.level_attr) if previous: - structure['new_level'] = (getattr(previous, - opts.level_attr) < current_level) + 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 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)) + if structure["new_level"]: + structure["ancestors"].append(callback(previous)) else: - structure['new_level'] = True + structure["new_level"] = True if ancestors: # Set up the ancestors list on the first item - structure['ancestors'] = [] + structure["ancestors"] = [] first_item_level = current_level if next_: - structure['closed_levels'] = list(range( - current_level, - getattr(next_, opts.level_attr), - -1)) + 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)) + 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 @@ -117,8 +120,14 @@ 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): +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 @@ -153,7 +162,8 @@ 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) + children, rel_cls, rel_field, count_attr, cumulative + ) return itertools.chain(node.get_ancestors(), [node], children) @@ -166,22 +176,22 @@ opts = qs.model._mptt_meta writer = csv.writer(sys.stdout if file is None else file) header = ( - 'pk', + "pk", opts.level_attr, - '%s_id' % opts.parent_attr, + "%s_id" % opts.parent_attr, opts.tree_id_attr, opts.left_attr, opts.right_attr, - 'pretty', + "pretty", ) writer.writerow(header) - for n in qs.order_by('tree_id', 'lft'): + 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_text = "%s%s" % ("- " * level, str(n)) row.append(row_text) writer.writerow(row) @@ -195,9 +205,9 @@ 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): + # 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 @@ -236,7 +246,7 @@ # 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()) + 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() @@ -249,7 +259,7 @@ # ``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),) + _("Node %s not in depth-first order") % (type(queryset),) ) # Set up the attribute on the node that will store cached children, @@ -272,7 +282,7 @@ if root_level == 0: # get_ancestors() can use .parent.parent.parent... - setattr(obj, '_mptt_use_cached_ancestors', True) + 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 diff -Nru python-django-mptt-0.11.0/README.rst python-django-mptt-0.13.2/README.rst --- python-django-mptt-0.11.0/README.rst 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/README.rst 2021-08-27 10:39:28.000000000 +0000 @@ -1,3 +1,17 @@ +========================================== +**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 =========== @@ -48,8 +62,8 @@ Requirements ------------ -* Python 3.5+ -* A supported version of Django (currently 1.11+) +* Python 3.6+ +* A supported version of Django (currently 2.2+) Feature overview ---------------- diff -Nru python-django-mptt-0.11.0/requirements.txt python-django-mptt-0.13.2/requirements.txt --- python-django-mptt-0.11.0/requirements.txt 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/requirements.txt 2021-08-27 10:39:28.000000000 +0000 @@ -1,2 +1,2 @@ -Django >= 1.11 +Django >= 2.2 django-js-asset diff -Nru python-django-mptt-0.11.0/setup.cfg python-django-mptt-0.13.2/setup.cfg --- python-django-mptt-0.11.0/setup.cfg 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/setup.cfg 2021-08-27 10:39:28.000000000 +0000 @@ -1,17 +1,67 @@ -[bumpversion] -current_version = 0.11.0 -commit = True -tag = True -tag_name = {new_version} -files = setup.py mptt/__init__.py +[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 -[flake8] -exclude = venv,.tox,docs/conf.py -max-line-length = 99 +[options] +packages = find: +install_requires = + django-js-asset +python_requires = >=3.6 +include_package_data = True +zip_safe = False -[bdist_wheel] -universal = 1 +[options.extras_require] +tests = + coverage + mock-django -[metadata] -license_file = LICENSE +[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* diff -Nru python-django-mptt-0.11.0/setup.py python-django-mptt-0.13.2/setup.py --- python-django-mptt-0.11.0/setup.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/setup.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,46 +1,5 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 +from setuptools import setup -from setuptools import find_packages, setup - -setup( - name='django-mptt', - description=( - 'Utilities for implementing Modified Preorder Tree Traversal ' - 'with your Django Models and working with trees of Model instances.' - ), - version='0.11.0', - author='Craig de Stigter', - author_email='craig.ds@gmail.com', - url='https://github.com/django-mptt/django-mptt', - license='MIT License', - packages=find_packages(exclude=['tests', 'tests.*']), - include_package_data=True, - install_requires=[ - 'Django>=1.11', - 'django-js-asset', - ], - python_requires=">=3.5", - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - 'Topic :: Utilities', - ], -) +setup() diff -Nru python-django-mptt-0.11.0/tests/manage.py python-django-mptt-0.13.2/tests/manage.py --- python-django-mptt-0.11.0/tests/manage.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/manage.py 2021-08-27 10:39:28.000000000 +0000 @@ -2,9 +2,10 @@ 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) \ No newline at end of file + execute_from_command_line(sys.argv) diff -Nru python-django-mptt-0.11.0/tests/myapp/admin.py python-django-mptt-0.13.2/tests/myapp/admin.py --- python-django-mptt-0.11.0/tests/myapp/admin.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/myapp/admin.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,9 +1,8 @@ from django.contrib import admin - -from mptt.admin import MPTTModelAdmin, DraggableMPTTAdmin - from myapp.models import Category, Person +from mptt.admin import DraggableMPTTAdmin, MPTTModelAdmin + class CategoryAdmin(MPTTModelAdmin): pass diff -Nru python-django-mptt-0.11.0/tests/myapp/fixtures/items.json python-django-mptt-0.13.2/tests/myapp/fixtures/items.json --- python-django-mptt-0.11.0/tests/myapp/fixtures/items.json 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/myapp/fixtures/items.json 2021-08-27 10:39:28.000000000 +0000 @@ -16,5 +16,14 @@ "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 + } } ] diff -Nru python-django-mptt-0.11.0/tests/myapp/fixtures/subitems.json python-django-mptt-0.13.2/tests/myapp/fixtures/subitems.json --- python-django-mptt-0.11.0/tests/myapp/fixtures/subitems.json 1970-01-01 00:00:00.000000000 +0000 +++ python-django-mptt-0.13.2/tests/myapp/fixtures/subitems.json 2021-08-27 10:39:28.000000000 +0000 @@ -0,0 +1,16 @@ +[ + { + "pk": 1, + "model": "myapp.subitem", + "fields": { + "item": 1 + } + }, + { + "pk": 2, + "model": "myapp.subitem", + "fields": { + "item": 3 + } + } +] diff -Nru python-django-mptt-0.11.0/tests/myapp/models.py python-django-mptt-0.13.2/tests/myapp/models.py --- python-django-mptt-0.11.0/tests/myapp/models.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/myapp/models.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,21 +1,21 @@ -from django.db import models from 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, TreeOneToOneField, TreeManyToManyField -from mptt.models import MPTTModel +from mptt.fields import TreeForeignKey, TreeManyToManyField, TreeOneToOneField from mptt.managers import TreeManager -from django.db.models.query import QuerySet +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) @@ -24,15 +24,16 @@ 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) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) category_uuid = models.CharField(max_length=50, unique=True, null=True) def __str__(self): return self.name def delete(self): - super(Category, self).delete() + super().delete() + delete.alters_data = True @@ -40,21 +41,29 @@ 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", + 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) + "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) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) def __str__(self): return self.name @@ -62,7 +71,7 @@ class Game(models.Model): genre = TreeForeignKey(Genre, on_delete=models.CASCADE) - genres_m2m = models.ManyToManyField(Genre, related_name='games_m2m') + genres_m2m = models.ManyToManyField(Genre, related_name="games_m2m") name = models.CharField(max_length=50) def __str__(self): @@ -71,8 +80,8 @@ class Insert(MPTTModel): parent = models.ForeignKey( - 'self', null=True, blank=True, related_name='children', - on_delete=models.CASCADE) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) class MultiOrder(MPTTModel): @@ -80,11 +89,11 @@ size = models.PositiveIntegerField() date = models.DateField() parent = TreeForeignKey( - 'self', null=True, blank=True, related_name='children', - on_delete=models.CASCADE) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) class MPTTMeta: - order_insertion_by = ['name', 'size', '-date'] + order_insertion_by = ["name", "size", "-date"] def __str__(self): return self.name @@ -92,22 +101,22 @@ class Node(MPTTModel): parent = TreeForeignKey( - 'self', null=True, blank=True, related_name='children', - on_delete=models.CASCADE) + "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' + 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) + "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) @@ -118,11 +127,11 @@ class OrderedInsertion(MPTTModel): name = models.CharField(max_length=50) parent = TreeForeignKey( - 'self', null=True, blank=True, related_name='children', - on_delete=models.CASCADE) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) class MPTTMeta: - order_insertion_by = ['name'] + order_insertion_by = ["name"] def __str__(self): return self.name @@ -130,24 +139,24 @@ class Tree(MPTTModel): parent = TreeForeignKey( - 'self', null=True, blank=True, related_name='children', - on_delete=models.CASCADE) + "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) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) class MPTTMeta: - left_attr = 'testing' + 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) + "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() @@ -161,31 +170,36 @@ class CustomPKName(MPTTModel): - my_id = models.AutoField(db_column='my_custom_name', primary_key=True) + 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) + "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='+') + 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) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) class MultiTableInheritanceA2(MultiTableInheritanceA1): @@ -194,22 +208,24 @@ # 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) + "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) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) ghosts = models.CharField(max_length=50) class Meta: @@ -239,6 +255,7 @@ # 4. proxy models + class SingleProxyModel(ConcreteModel): objects = CustomTreeManager() @@ -247,39 +264,39 @@ 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) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) class Meta: - swappable = 'MPTT_SWAPPABLE_MODEL' + swappable = "MPTT_SWAPPABLE_MODEL" class SwappedInModel(MPTTModel): parent = TreeForeignKey( - 'self', null=True, blank=True, related_name='children', - on_delete=models.CASCADE) + "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(MultipleManager, self).get_queryset().exclude(published=False) + return super().get_queryset().exclude(published=False) class MultipleManagerModel(MPTTModel): parent = TreeForeignKey( - 'self', null=True, blank=True, related_name='children', - on_delete=models.CASCADE) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) published = models.BooleanField() objects = TreeManager() @@ -288,12 +305,12 @@ class AutoNowDateFieldModel(MPTTModel): parent = TreeForeignKey( - 'self', null=True, blank=True, related_name='children', - on_delete=models.CASCADE) + "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',) + order_insertion_by = ("now",) # test registering of remote model @@ -303,37 +320,47 @@ TreeForeignKey( Group, blank=True, null=True, on_delete=models.CASCADE -).contribute_to_class(Group, 'parent') -mptt.register(Group, order_insertion_by=('name',)) +).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) + "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') + 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) + 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) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) class MPTTMeta: - order_insertion_by = ['name'] + order_insertion_by = ["name"] def __str__(self): return self.name @@ -342,11 +369,25 @@ 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) + "self", null=True, blank=True, related_name="children", on_delete=models.CASCADE + ) class MPTTMeta: - order_insertion_by = ['-name'] + 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() diff -Nru python-django-mptt-0.11.0/tests/myapp/test_forms.py python-django-mptt-0.13.2/tests/myapp/test_forms.py --- python-django-mptt-0.11.0/tests/myapp/test_forms.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/myapp/test_forms.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,76 +1,77 @@ from django.forms.models import modelform_factory - -from mptt.forms import ( - MPTTAdminForm, TreeNodeChoiceField, TreeNodeMultipleChoiceField, - MoveNodeForm) - 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'] + 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') + c = Category.objects.get(name="Nintendo Wii") CategoryForm = modelform_factory( Category, form=MPTTAdminForm, - fields=('name', 'parent'), + 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) + 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) + 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() + 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) + self.assertIn("Invalid parent", "%s" % form.errors) def test_field_types(self): - ReferencingModelForm = modelform_factory( - ReferencingModel, - exclude=('id',)) + 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)) + 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', - }) + form = MoveNodeForm( + c, + { + "target": "5", + "position": "first-child", + }, + ) self.assertTrue(form.is_valid()) form.save() - self.assertTreeEqual(Category.objects.all(), ''' + self.assertTreeEqual( + Category.objects.all(), + """ 1 - 1 0 1 20 5 1 1 1 2 13 2 5 1 2 3 8 @@ -81,7 +82,8 @@ 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 @@ -100,28 +102,34 @@ '' '' '' - '' + "" '' '' + "" ) 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'])) + 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']) + '', str(form["target"]) + ) + form = MoveNodeForm( + Genre.objects.get(pk=7), position_choices=(("left", "left"),) + ) + self.assertHTMLEqual( + str(form["position"]), + ( + '" + ), ) - 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()) @@ -140,16 +148,18 @@ '' '' '' - '' + "", + ) + field = TreeNodeChoiceField( + queryset=Genre.objects.all(), empty_label="None of the below" ) - field = TreeNodeChoiceField(queryset=Genre.objects.all(), empty_label='None of the below') self.assertInHTML( '', - field.widget.render("test", None) + field.widget.render("test", None), ) def test_treenodechoicefield_level_indicator(self): - field = TreeNodeChoiceField(queryset=Genre.objects.all(), level_indicator='+--') + 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), + '", ) diff -Nru python-django-mptt-0.11.0/tests/myapp/tests.py python-django-mptt-0.13.2/tests/myapp/tests.py --- python-django-mptt-0.11.0/tests/myapp/tests.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/myapp/tests.py 2021-08-27 10:39:28.000000000 +0000 @@ -2,37 +2,58 @@ import os import re import sys -import tempfile 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.apps import apps -from django.template import Template, TemplateSyntaxError, Context +from django.template import Context, Template, TemplateSyntaxError from django.test import RequestFactory, TestCase -from django.contrib.admin.views.main import ChangeList -from django.contrib.admin import ModelAdmin, site + 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.models import MPTTModel 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 -from myapp.models import ( - Category, Item, Genre, CustomPKName, SingleProxyModel, DoubleProxyModel, - ConcreteModel, OrderedInsertion, AutoNowDateFieldModel, Person, - CustomTreeQueryset, Node, CustomTreeManager, Book, UUIDNode, Student, - MultipleManagerModel, UniqueTogetherModel, NullableOrderedInsertionModel, NullableDescOrderedInsertionModel) - def get_tree_details(nodes): """ @@ -40,18 +61,27 @@ 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')) + 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]) + 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) +leading_whitespace_re = re.compile(r"^\s+", re.MULTILINE) def tree_details(text): @@ -61,11 +91,10 @@ readable format (says who?), to be compared with the result of using the ``get_tree_details`` function. """ - return leading_whitespace_re.sub('', text.rstrip()) + 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) @@ -77,13 +106,12 @@ class DocTestTestCase(TreeTestCase): - def test_run_doctest(self): import doctest class DummyStream: content = "" - encoding = 'utf8' + encoding = "utf8" def write(self, text): self.content += text @@ -96,17 +124,18 @@ sys.stdout = dummy_stream doctest.testfile( - os.path.join(os.path.dirname(__file__), 'doctests.txt'), + os.path.join(os.path.dirname(__file__), "doctests.txt"), module_relative=False, optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS, - encoding='utf-8', + encoding="utf-8", ) sys.stdout = before content = dummy_stream.content if content: - before.write(content + '\n') + before.write(content + "\n") self.fail() + # genres.json defines the following tree structure # # 1 - 1 0 1 16 action @@ -129,14 +158,17 @@ that reparented items have the correct tree attributes defined, should they be required for use after a save. """ - fixtures = ['genres.json'] + + 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(), """ + 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 @@ -148,14 +180,17 @@ 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(), """ + 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 @@ -167,16 +202,19 @@ 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(), """ + 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 @@ -188,16 +226,19 @@ 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(), """ + 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 @@ -209,16 +250,19 @@ 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(), """ + 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 @@ -230,16 +274,19 @@ 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(), """ + 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 @@ -251,16 +298,19 @@ 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(), """ + 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 @@ -272,7 +322,8 @@ 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) @@ -318,14 +369,17 @@ ConcreteModel.objects.create(name="Carrot", parent=vegie) # sanity check - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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") @@ -334,15 +388,18 @@ 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(), """ + 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) @@ -416,15 +473,20 @@ Tests that the tree structure is maintained appropriately in various deletion scenarios. """ - fixtures = ['categories.json'] + + 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(), """ + 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 @@ -437,18 +499,23 @@ 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(), """ + 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(), """ + self.assertTreeEqual( + Category.objects.all(), + """ 1 - 1 0 1 18 2 1 1 1 2 7 3 2 1 2 3 4 @@ -458,11 +525,14 @@ 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(), """ + self.assertTreeEqual( + Category.objects.all(), + """ 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 @@ -470,14 +540,17 @@ 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(), """ + self.assertTreeEqual( + Category.objects.all(), + """ 1 - 1 0 1 18 2 1 1 1 2 7 3 2 1 2 3 4 @@ -487,7 +560,8 @@ 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) @@ -500,7 +574,9 @@ called. """ Category.objects.get(id=5).delete() - self.assertTreeEqual(Category.objects.all(), """ + self.assertTreeEqual( + Category.objects.all(), + """ 1 - 1 0 1 14 2 1 1 1 2 7 3 2 1 2 3 4 @@ -508,15 +584,18 @@ 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]) + queryset = Category.objects.filter(id__in=[6, 7]) for category in queryset: - category .delete() + category.delete() - self.assertTreeEqual(Category.objects.all(), """ + self.assertTreeEqual( + Category.objects.all(), + """ 1 - 1 0 1 16 2 1 1 1 2 7 3 2 1 2 3 4 @@ -524,7 +603,8 @@ 5 1 1 1 8 9 8 1 1 1 10 15 9 8 1 2 11 12 - 10 8 1 2 13 14""") + 10 8 1 2 13 14""", + ) class IntraTreeMovementTestCase(TreeTestCase): @@ -540,7 +620,6 @@ class CustomPKNameTestCase(TreeTestCase): - def setUp(self): manager = CustomPKName.objects c1 = manager.create(name="c1") @@ -560,19 +639,21 @@ 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(), """ + 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) @@ -580,7 +661,8 @@ self.assertRaises( CantDisableUpdates, - SingleProxyModel.objects.disable_mptt_updates().__enter__) + SingleProxyModel.objects.disable_mptt_updates().__enter__, + ) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(SingleProxyModel._mptt_updates_enabled) @@ -598,7 +680,8 @@ self.assertRaises( CantDisableUpdates, - DoubleProxyModel.objects.disable_mptt_updates().__enter__) + DoubleProxyModel.objects.disable_mptt_updates().__enter__, + ) self.assertTrue(ConcreteModel._mptt_updates_enabled) self.assertTrue(DoubleProxyModel._mptt_updates_enabled) @@ -617,22 +700,28 @@ with self.assertNumQueries(1): ConcreteModel.objects.create(name="e", parent=self.d) # 2nd query here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -641,20 +730,26 @@ # 1 query here: ConcreteModel.objects.create(name="e") # 2nd query here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + """, + ) + 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): @@ -665,20 +760,26 @@ self.c.parent = self.b self.c.save() # 3rd query here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -688,20 +789,26 @@ self.c.parent = self.d self.c.save() # query 2 here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -711,20 +818,26 @@ self.c.parent = None self.c.save() # query 2 here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -734,24 +847,29 @@ self.d.parent = self.c self.d.save() # query 2 here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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) @@ -759,21 +877,24 @@ self.d = ConcreteModel.objects.create(name="d") self.z = ConcreteModel.objects.create(name="z") # state is now: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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__) + CantDisableUpdates, SingleProxyModel.objects.delay_mptt_updates().__enter__ + ) self.assertFalse(ConcreteModel._mptt_is_tracking) self.assertFalse(SingleProxyModel._mptt_is_tracking) @@ -801,24 +922,30 @@ # 1 query to save node. ConcreteModel.objects.create(name="e", parent=self.d) # 3rd query here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -828,24 +955,30 @@ # (one to get the correct tree_id, then one to insert) ConcreteModel.objects.create(name="e") # 3rd query here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -856,22 +989,28 @@ self.c.parent = self.b self.c.save() # query 3 here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -883,22 +1022,28 @@ self.d.parent = self.c self.d.save() # query 3 here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -911,21 +1056,27 @@ self.c.parent = None self.c.save() # 4th query here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -937,22 +1088,28 @@ self.d.parent = self.c self.d.save() # query 3 here: - self.assertTreeEqual(ConcreteModel.objects.all(), """ + 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(), """ + 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): @@ -980,7 +1137,9 @@ # 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)) + self.assertRaises( + AssertionError, self.assertIn, b, b.get_ancestors(include_self=True) + ) # ... we need to reload it properly ourselves: b.refresh_from_db() @@ -995,13 +1154,16 @@ self.f = OrderedInsertion.objects.create(name="f") self.z = OrderedInsertion.objects.create(name="z") # state is now: - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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): @@ -1010,24 +1172,30 @@ # 1 query here: OrderedInsertion.objects.create(name="dd", parent=self.c) # 2nd query here: - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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(), """ + 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): @@ -1040,48 +1208,60 @@ # 3. insert the object OrderedInsertion.objects.create(name="ee") # 4th query here: - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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(), """ + 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.name = "before d" self.e.save() # query 2 here: - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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(), """ + 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): @@ -1091,25 +1271,31 @@ # 1. update the node # 2. collapse old tree since it is now empty. self.f.parent = self.c - self.f.name = 'dd' + self.f.name = "dd" self.f.save() # query 3 here: - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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(), """ + 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): @@ -1122,21 +1308,27 @@ self.e.parent = None self.e.save() # query 4 here: - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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(), """ + 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): @@ -1148,28 +1340,32 @@ self.f.parent = self.e self.f.save() # query 3 here: - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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(), """ + 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'] + fixtures = ["categories.json", "genres.json", "persons.json"] def test_all_managers_are_different(self): # all tree managers should be different. otherwise, possible infinite recursion. @@ -1181,7 +1377,8 @@ if id(tm) in seen: self.fail( "Tree managers for %s and %s are the same manager" - % (model.__name__, seen[id(tm)].__name__)) + % (model.__name__, seen[id(tm)].__name__) + ) seen[id(tm)] = model def test_manager_multi_table_inheritance(self): @@ -1209,7 +1406,10 @@ if manager is None: break else: - self.fail("Detected infinite recursion in %s._tree_manager._base_manager" % model) + self.fail( + "Detected infinite recursion in %s._tree_manager._base_manager" + % model + ) def test_proxy_custom_manager(self): self.assertIsInstance(SingleProxyModel._tree_manager, CustomTreeManager) @@ -1221,70 +1421,90 @@ 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, 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')) + qs = Category.objects.filter(Q(name="Nintendo Wii") | Q(name="PlayStation 3")) self.assertEqual( get_desc_names(qs), - ['Games', 'Games', - 'Hardware & Accessories', 'Hardware & Accessories'], + ["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'] + [ + "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'] + [ + "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'] + [ + "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')) + 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')) + 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), ["PC & Video Games"]) self.assertEqual( self._get_anc_names(qs, include_self=True), - ['Nintendo Wii', 'PC & Video Games', 'PlayStation 3'] + ["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']) + 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'))) + list(Genre.objects.values_list("name", flat=True).order_by("name")), + ) def test_custom_querysets(self): """ @@ -1292,23 +1512,26 @@ """ 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')) + 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()) + 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') + TreeManager.from_queryset(CustomTreeQueryset)().contribute_to_class( + Genre, "my_manager" + ) self.assertIsInstance(Genre.my_manager.get_queryset(), CustomTreeQueryset) @@ -1321,7 +1544,8 @@ """ with self.assertNumQueries(2): qs = Category.objects.get_queryset_descendants( - Category.objects.all(), include_self=True) + Category.objects.all(), include_self=True + ) self.assertEqual(len(qs), 10) def test_default_manager_with_multiple_managers(self): @@ -1337,7 +1561,8 @@ """ Tests for the ``cache_tree_children`` template filter. """ - fixtures = ['categories.json'] + + fixtures = ["categories.json"] def test_cache_tree_children_caches_parents(self): """ @@ -1362,15 +1587,15 @@ with self.assertNumQueries(1): with self.assertRaises(ValueError): - cache_tree_children(list(Category.objects.order_by('-id'))) + 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')) + 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): @@ -1378,8 +1603,12 @@ """ Tests for the ``recursetree`` template filter. """ - fixtures = ['categories.json'] - template = re.sub(r'(?m)^[\s]+', '', ''' + + fixtures = ["categories.json"] + template = re.sub( + r"(?m)^[\s]+", + "", + """ {% load mptt_tags %}
    {% recursetree nodes %} @@ -1393,32 +1622,54 @@ {% 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
') + 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
' - )) + 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 %}') + "{% load mptt_tags %}{% recursetree %}{% endrecursetree %}", + ) def test_cached_ancestors(self): - template = Template(''' + template = Template( + """ {% load mptt_tags %} {% recursetree nodes %} {{ node.get_ancestors|join:" > " }} {{ node.name }} @@ -1426,23 +1677,31 @@ {{ children }} {% endif %} {% endrecursetree %} - ''') + """ + ) with self.assertNumQueries(1): qs = Category.objects.all() - template.render(Context({'nodes': qs})) + template.render(Context({"nodes": qs})) class TreeInfoTestCase(TreeTestCase): - fixtures = ['genres.json'] - template = re.sub(r'(?m)^[\s]+', '', ''' + 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 %}''') + {% endfor %}""", + ) - template_with_ancestors = re.sub(r'(?m)^[\s]+', '', ''' + 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 %} @@ -1452,63 +1711,100 @@ {{ ancestor }}{% if not forloop.last %},{% endif %} {% endfor %} {% for level in structure.closed_levels %}
{% endfor %} - {% endfor %}''') + {% endfor %}""", + ) def test_tree_info_html(self): - html = Template(self.template).render(Context({ - 'nodes': Genre.objects.all(), - })).replace('\n', '') + 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', '') + "
  • 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
') + "
  • 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', '') + 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
') + "
  • 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]+', '', ''' + 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,') + 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]+', '', ''' + 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 %} @@ -1516,61 +1812,62 @@ {{ 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 ') + template = template.replace( + " count myapp.Game.genre in game_count ", " all_descendants " + ) if cumulative: - template = template.replace(' count ', ' cumulative count ') + template = template.replace(" count ", " cumulative count ") if m2m: - template = template.replace('Game.genre', 'Game.genres_m2m') + template = template.replace("Game.genre", "Game.genres_m2m") - return Template(template).render(Context({ - 'node': Genre.objects.get(pk=pk), - })).replace('\n', '') + 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) + 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), "[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(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.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, 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.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.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.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:') + self.render_for_node(2, all_descendants=True), "1:,[2:],3:,4:,5:" + ) class TestAutoNowDateFieldModel(TreeTestCase): @@ -1582,178 +1879,315 @@ class RegisteredRemoteModel(TreeTestCase): - def test_save_registered_model(self): - g1 = Group.objects.create(name='group 1') + 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, '') + 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, - })) + output = Template("{{ node.delete }}").render( + Context( + { + "node": node, + } + ) + ) self.assertEqual(node, Node.objects.get(pk=node.pk)) class TestDebugInfo(TreeTestCase): - fixtures = ['categories.json'] + 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) + self.assertIn("1,0,,1,1,20", output) def test_debug_info_with_non_ascii_representations(self): - Category.objects.create(name='El niño') + 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) + self.assertIn("El niño", output) class AdminBatch(TreeTestCase): - fixtures = ['categories.json'] + fixtures = ["categories.json"] def test_changelist(self): - user = User.objects.create_superuser('admin', 'test@example.com', 'p') + user = User.objects.create_superuser("admin", "test@example.com", "p") - self.client.login(username=user.username, password='p') + self.client.login(username=user.username, password="p") - response = self.client.get('/admin/myapp/category/') - self.assertContains( - response, - 'name="_selected_action"', - 10) + 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]) + 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'], + "action": "delete_selected", + "_selected_action": ["5", "8", "9"], } - response = self.client.post('/admin/myapp/category/', data) + 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) + data["post"] = "yes" + response = self.client.post("/admin/myapp/category/", data) - self.assertRedirects( - response, - '/admin/myapp/category/') + 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(), ''' + 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', + "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)) + "Cannot call %s on unsaved Genre instances" % method, + getattr(Genre(), method), + ) class QuerySetTests(TreeTestCase): - fixtures = ['categories.json'] + 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.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)], + 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.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)], + 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.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)], + 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.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)], + 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'] + 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') + 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): + 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): + 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") -class TestOrderedInsertionBFS(TreeTestCase): + # 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") @@ -1761,70 +2195,91 @@ OrderedInsertion.objects.create(name="Classical") - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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(), """ + self.assertTreeEqual( + OrderedInsertion.objects.all(), + """ 1 - 1 0 1 2 - """) + """, + ) OrderedInsertion.objects.create(name="Classical") - self.assertTreeEqual(OrderedInsertion.objects.all(), """ + 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(), """ + 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') + 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(), """ + 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') + 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(), """ + 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(), """ + 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): @@ -1833,7 +2288,8 @@ Tests that the queryset function `get_cached_trees` results in a minimum number of database queries. """ - fixtures = ['genres.json'] + + fixtures = ["genres.json"] def test_genre_iter(self): """ @@ -1858,17 +2314,19 @@ """ 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') + 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") +@unittest.skipUnless( + mock_signal_receiver, "Signals tests require mock_django installed" +) class Signals(TestCase): - fixtures = ['categories.json'] + fixtures = ["categories.json"] def setUp(self): self.signal = node_moved @@ -1877,14 +2335,14 @@ 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.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') + Category.objects.create(name="Descriptive name") self.assertEqual(receiver.call_count, 0) @@ -1897,23 +2355,20 @@ signal=self.signal, target=self.ps3, sender=Category, - position='first-child' + position="first-child", ) def test_move_by_changing_parent_should_send_signal(self): - '''position is not set when sent from save(). I assume it + """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 + instance=self.wii, signal=self.signal, target=self.ps3, sender=Category ) @@ -1927,17 +2382,20 @@ OrderedInsertion.objects.create(name="a") def test_deferred_order_insertion_by(self): - qs = OrderedInsertion.objects.defer('name') + qs = OrderedInsertion.objects.defer("name") with self.assertNumQueries(1): nodes = list(qs) with self.assertNumQueries(0): - self.assertTreeEqual(nodes, ''' + 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) + obj = OrderedInsertion.objects.defer("name").get() + self.assertEqual(obj._mptt_cached_fields["name"], DeferredAttribute) with self.assertNumQueries(1): obj.name @@ -1945,56 +2403,52 @@ # does a node move, since the order_insertion_by field changed obj.save() - self.assertEqual(obj._mptt_cached_fields['name'], 'a') + 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) + obj = OrderedInsertion.objects.defer("name").get() + self.assertEqual(obj._mptt_cached_fields["name"], DeferredAttribute) with self.assertNumQueries(0): - obj.name = 'b' + 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') + 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') + 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') + 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')) + self.assertNotEqual(p1._mpttfield("tree_id"), p2._mpttfield("tree_id")) - response = self.client.get('/admin/myapp/person/') + response = self.client.get("/admin/myapp/person/") self.assertContains(response, 'class="drag-handle"', 3) self.assertContains(response, 'style="text-indent:0px"', 3) self.assertContains( response, - 'javascript" src="/static/mptt/draggable-admin.js"' - ' data-context="{"') - self.assertContains( - response, - '}" id="draggable-admin-context">') + 'src="/static/mptt/draggable-admin.js" data-context="{"', + ) + self.assertContains(response, '}" id="draggable-admin-context">') response = self.client.post( - '/admin/myapp/person/', + "/admin/myapp/person/", { - 'cmd': 'move_node', - 'cut_item': p1.pk, - 'pasted_on': p2.pk, - 'position': 'last-child', + "cmd": "move_node", + "cut_item": p1.pk, + "pasted_on": p2.pk, + "position": "last-child", }, - HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 200) @@ -2003,86 +2457,106 @@ self.assertEqual(p1.parent, p2) - self.assertTreeEqual(Person.objects.all(), """ + 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/') + 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/', + "/admin/myapp/person/", { - 'cmd': 'move_node', - 'cut_item': p3.pk, - 'pasted_on': p1.pk, - 'position': 'left', + "cmd": "move_node", + "cut_item": p3.pk, + "pasted_on": p1.pk, + "position": "left", }, - HTTP_X_REQUESTED_WITH='XMLHttpRequest', + HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 200) - self.assertTreeEqual(Person.objects.all(), """ + 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', - }) + 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.assertRedirects(response, "/admin/myapp/person/") - self.assertTreeEqual(Person.objects.all(), """ + 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), + ("fk", TreeRelatedFieldListFilter), + ("m2m", TreeRelatedFieldListFilter), ) - ordering = ('id',) + ordering = ("id",) class CategoryAdmin(ModelAdmin): list_filter = ( - ('books_fk', TreeRelatedFieldListFilter), - ('books_m2m', TreeRelatedFieldListFilter), + ("books_fk", TreeRelatedFieldListFilter), + ("books_m2m", TreeRelatedFieldListFilter), ) - ordering = ('id',) + ordering = ("id",) class ListFiltersTests(TestCase): - def setUp(self): - self.user = User.objects.create_superuser('admin', 'test@example.com', 'p') + 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.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) @@ -2095,34 +2569,60 @@ 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, + 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'): + 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('/') + 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.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'}) + 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]) @@ -2130,29 +2630,37 @@ # 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') + 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}) + 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}) + 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}) + 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}) + 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]) @@ -2160,21 +2668,36 @@ def test_treerelatedfieldlistfilter_manytomany(self): modeladmin = BookAdmin(Book, site) - request = self.get_request('/') + 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.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'}) + request = self.get_request("/", {"m2m__isnull": "True"}) changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned @@ -2184,29 +2707,37 @@ # 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') + 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}) + 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}) + 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}) + 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}) + 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]) @@ -2215,7 +2746,7 @@ modeladmin = CategoryAdmin(Category, site) # FK relationship ----- - request = self.get_request('/', {'books_fk__isnull': 'True'}) + request = self.get_request("/", {"books_fk__isnull": "True"}) changelist = self.get_changelist(request, Category, modeladmin) # Make sure the correct queryset is returned @@ -2225,30 +2756,32 @@ # 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') + 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}) + 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]) + 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}) + 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}) + 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'}) + 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]) @@ -2256,66 +2789,67 @@ # 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') + 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}) + 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]) + 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}) + 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}) + 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') + 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) + 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') + 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') + 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') + 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) + 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') + child2.move_to(child1, "left") self.assertEqual(list(root1.get_children()), [child2, child1]) @@ -2335,88 +2869,107 @@ 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 + 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(), """ + 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 + 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(), """ + 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) + 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(), """ + 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) + 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(), """ + 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' + app_label = "myapp" - tree_id_attr = getattr(SomeModel._mptt_meta, 'tree_id_attr') + 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'): + 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')] + already_idx = [["tree_id", "lft"], ("tree_id", "lft")] no_idx = [tuple(), list()] - some_idx = [['tree_id'], ('tree_id',), [['tree_id']], (('tree_id',),)] + 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' + 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 @@ -2424,72 +2977,77 @@ # class does) could cause errors. Kind of... weird, but surprisingly # effective. - SomeModel = type(str('model_{0}'.format(idx)), (MPTTModel,), { - 'Meta': Meta, - '__module__': __name__, - }) + SomeModel = type( + str("model_{}".format(idx)), + (MPTTModel,), + { + "Meta": Meta, + "__module__": __name__, + }, + ) - self.assertIn(('tree_id', 'lft'), SomeModel._meta.index_together) + self.assertIn(("tree_id", "lft"), SomeModel._meta.index_together) def test_index_together_different_attr(self): - already_idx = [['abc', 'def'], ('abc', 'def')] + already_idx = [["abc", "def"], ("abc", "def")] no_idx = [tuple(), list()] - some_idx = [['abc'], ('abc',), [['abc']], (('abc',),)] + 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' + tree_id_attr = "abc" + left_attr = "def" class Meta: index_together = case - app_label = 'myapp' + app_label = "myapp" - SomeModel = type(str('model__different_attr_{0}'.format(idx)), (MPTTModel,), { - 'MPTTMeta': MPTTMeta, - 'Meta': Meta, - '__module__': str(__name__) - }) + SomeModel = type( + str("model__different_attr_{}".format(idx)), + (MPTTModel,), + {"MPTTMeta": MPTTMeta, "Meta": Meta, "__module__": str(__name__)}, + ) - self.assertIn(('abc', 'def'), SomeModel._meta.index_together) + self.assertIn(("abc", "def"), SomeModel._meta.index_together) class BulkLoadTests(TestCase): - fixtures = ['categories.json'] + fixtures = ["categories.json"] def setUp(self): self.games = { - 'id': 11, - 'name': 'Role-playing', - 'children': [ + "id": 11, + "name": "Role-playing", + "children": [ { - 'id': 12, - 'parent_id': 11, - 'name': 'Single-player', + "id": 12, + "parent_id": 11, + "name": "Single-player", }, { - 'id': 13, - 'parent_id': 11, - 'name': 'Multi-player', + "id": 13, + "parent_id": 11, + "name": "Multi-player", }, ], } def test_bulk_root(self): data = { - 'id': 11, - 'name': 'Enterprise Software', - 'children': [ + "id": 11, + "name": "Enterprise Software", + "children": [ { - 'id': 12, - 'parent_id': 11, - 'name': 'Databases', + "id": 12, + "parent_id": 11, + "name": "Databases", }, { - 'id': 13, - 'parent_id': 11, - 'name': 'Timekeeping', + "id": 13, + "parent_id": 11, + "name": "Timekeeping", }, ], } @@ -2513,7 +3071,9 @@ def test_bulk_left(self): games = Category.objects.get(id=3) - records = Category.objects.build_tree_nodes(self.games, target=games, position='left') + 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) @@ -2522,3 +3082,11 @@ 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"]) diff -Nru python-django-mptt-0.11.0/tests/myapp/urls.py python-django-mptt-0.13.2/tests/myapp/urls.py --- python-django-mptt-0.11.0/tests/myapp/urls.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/myapp/urls.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,6 +1,5 @@ -import django -from django.conf.urls import include, url - from django.contrib import admin +from django.urls import path + -urlpatterns = [url(r'^admin/', admin.site.urls)] +urlpatterns = [path("admin/", admin.site.urls)] diff -Nru python-django-mptt-0.11.0/tests/requirements.txt python-django-mptt-0.13.2/tests/requirements.txt --- python-django-mptt-0.11.0/tests/requirements.txt 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/requirements.txt 2021-08-27 10:39:28.000000000 +0000 @@ -1,4 +1,4 @@ mock-django -Django >= 1.11 +Django >= 2.2 coverage django-js-asset diff -Nru python-django-mptt-0.11.0/tests/settings.py python-django-mptt-0.13.2/tests/settings.py --- python-django-mptt-0.11.0/tests/settings.py 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tests/settings.py 2021-08-27 10:39:28.000000000 +0000 @@ -1,55 +1,50 @@ import os -import django DIRNAME = os.path.dirname(__file__) DEBUG = True -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'mydatabase' - } -} +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', + "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' +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', + "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', + "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' +ROOT_URLCONF = "myapp.urls" # Swappable model testing -MPTT_SWAPPABLE_MODEL = 'myapp.SwappedInModel' +MPTT_SWAPPABLE_MODEL = "myapp.SwappedInModel" diff -Nru python-django-mptt-0.11.0/tox.ini python-django-mptt-0.13.2/tox.ini --- python-django-mptt-0.11.0/tox.ini 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/tox.ini 2021-08-27 10:39:28.000000000 +0000 @@ -1,18 +1,41 @@ [tox] envlist = - {py35,py36,py37,pypy}-{dj111} - {py35,py36,py37}-{dj20} - {py35,py36,py37}-{dj21} - {py35,py36,py37,py38}-{dj22} - {py36,py37,py38}-{dj30} + py{36,37,38,39}-dj{22,30,31,32} + py{38,39}-dj{main} + style [testenv] -changedir = {toxinidir}/tests -commands = ./runtests.sh {posargs} +usedevelop = true +extras = tests +commands = + python -Wd {envbindir}/coverage run tests/manage.py test -v2 --keepdb {posargs:myapp} + coverage report -m deps = - mock_django>=0.6.7 - dj111: Django>=1.11.17,<2.0 - dj20: Django>=2.0,<2.1 - dj21: Django>=2.1,<2.2 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 diff -Nru python-django-mptt-0.11.0/.travis.yml python-django-mptt-0.13.2/.travis.yml --- python-django-mptt-0.11.0/.travis.yml 2020-01-18 13:59:47.000000000 +0000 +++ python-django-mptt-0.13.2/.travis.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,41 +0,0 @@ -dist: xenial -language: python -cache: pip -git: - depth: 1 - -python: - - "3.8" - - "3.7" - - "3.6" - - "3.5" - -env: - - DJANGO="Django==1.11.*" - - DJANGO="Django==2.0.*" - - DJANGO="Django==2.1.*" - - DJANGO="Django==2.2.*" - - DJANGO="Django==3.0.*" - - DJANGO="https://github.com/django/django/archive/master.tar.gz" - -matrix: - exclude: - - env: DJANGO="Django==1.11.*" - python: "3.8" - - env: DJANGO="Django==2.0.*" - python: "3.8" - - env: DJANGO="Django==2.1.*" - python: "3.8" - - env: DJANGO="Django==3.0.*" - python: "3.5" - - env: DJANGO="https://github.com/django/django/archive/master.tar.gz" - python: "3.5" - allow_failures: - - env: DJANGO="https://github.com/django/django/archive/master.tar.gz" - -before_install: pip install --upgrade pip - -# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors -install: pip install $DJANGO mock-django==0.6.9 django-js-asset -# command to run tests, e.g. python setup.py test -script: cd tests/ && ./runtests.sh