pax_global_header00006660000000000000000000000064144051062660014516gustar00rootroot0000000000000052 comment=7b9c89f75acc8216e45931e29ca7c1e360802ff7 django-ordered-model-3.7.4/000077500000000000000000000000001440510626600155135ustar00rootroot00000000000000django-ordered-model-3.7.4/.github/000077500000000000000000000000001440510626600170535ustar00rootroot00000000000000django-ordered-model-3.7.4/.github/workflows/000077500000000000000000000000001440510626600211105ustar00rootroot00000000000000django-ordered-model-3.7.4/.github/workflows/distribute.yml000066400000000000000000000022101440510626600240040ustar00rootroot00000000000000name: Packaging on: release: types: [published] jobs: build: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip setuptools setuptools_scm twine wheel - name: Create packages run: python setup.py sdist bdist_wheel - name: Run twine check run: twine check dist/* - uses: actions/upload-artifact@v2 with: name: django-ordered-model-dist path: dist - name: Run twine upload (prerelease to test pypi) env: TWINE_PASSWORD: ${{ secrets.TWINE_TEST_PASSWORD }} if: ${{ env.TWINE_PASSWORD != null && github.event.release.prerelease }} run: twine upload --username __token__ --non-interactive -r testpypi dist/* - name: Run twine upload (release) env: TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} if: ${{ env.TWINE_PASSWORD != null && !github.event.release.prerelease }} run: twine upload --username __token__ --non-interactive -r pypi dist/* django-ordered-model-3.7.4/.github/workflows/lint.yml000066400000000000000000000003351440510626600226020ustar00rootroot00000000000000name: Lint on: pull_request: push: branches: [master] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - uses: psf/black@stable django-ordered-model-3.7.4/.github/workflows/test.yml000066400000000000000000000020021440510626600226040ustar00rootroot00000000000000name: Test and coverage on: push: branches: [master] pull_request: jobs: build: runs-on: ubuntu-20.04 strategy: matrix: python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10'] steps: - uses: actions/checkout@v1 - 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 pip install tox tox-gh-actions - name: Test with tox run: tox # - name: Upload coverage.xml # if: ${{ matrix.platform == 'ubuntu-latest' && matrix.python-version == '3.9' }} # uses: actions/upload-artifact@v2 # with: # name: django-ordered-model-coverage # path: coverage.xml # if-no-files-found: error # - name: Upload coverage.xml to codecov # if: ${{ matrix.platform == 'ubuntu-latest' && matrix.python-version == '3.9' }} # uses: codecov/codecov-action@v1 django-ordered-model-3.7.4/.gitignore000066400000000000000000000041001440510626600174760ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # Ignore static content for our test app tests/migrations tests/staticfiles django-ordered-model-3.7.4/CHANGES.md000066400000000000000000000154061440510626600171130ustar00rootroot00000000000000Change log ========== Unreleased ---------- 3.7.4 - 2023-03-17 ---------- - Relax Check for `OrderedModelManager` to a `Warning`, if the manager returns the correct queryset (#290) 3.7.3 - 2023-03-15 ---------- - Restrict signal handler 'senders' to subclasses of `OrderedModelBase` to avoid query count regression due to `Collector.can_fast_delete` logic in `models/deletion.py` (#288) - Fix `reorder_model` management command re-ordering with multiple `order_with_respect_to` values 3.7.2 - 2023-03-14 ---------- - Fix a performance regression (unnecessary queries) in the WRT change detection (#286) - Add a Check that `order_with_respect_to` specifies only ForeignKey fields - Add a Check that our subclasses of ModelManager and QuerySet are used (#286) 3.7.1 - 2023-03-06 ---------- - Fix for `model.save()` falsely detecting WRT change from admin create since 3.7 - Cascaded deletes of `OrderedModel` instances now handled using signals (#182) 3.7 - 2023-03-03 ---------- - Use bulk update method in `reorder_model` management command for performance (#273) - Add tox builder for python 3.10, use upstream DRF with upstream django - Emit a system Check failure if a subclass of `OrderedModelBase` fails to specify `Meta.ordering` - Updating the value of fields within `order_with_respect_to` now adjusts ordering accordingly (#198) 3.6 - 2022-05-30 ---------- - Add `serializers.OrderedModelSerializer` to allow Django Rest Framework to re-order models (#251 #264) - Add tox builder for Django 4.0, drop building against 2.0 and 2.1 due to DRF compatibility. 3.5 - 2022-01-12 ---------------- - Django 4.0 compatibility: Fix ChangeList constructor for Admin (#256) - Remove usage of `assertEquals` in tests (#255) - Add admin screenshots to README (#245) - Fix reorder command for custom order field models (#257) 3.4.3 - 2021-04-20 ------------------ - Fix packaging, setup.py was missing management command package path 3.4.2 - 2021-04-20 ------------------ - Fix `OrderedTabularInline` for models with custom primary key field (#233) - Add management command `reorder_model` that can re-order most models with a broken ordering (#240) - Fix handling of keyword arguments passed to `bulk_create` by Django 3 (#235) - Fix inline admin support for Proxy Models by adding parent model to url name (#242) - Migrated to GitHub Actions workflow (#241) 3.4.1 - 2020-05-11 ------------------ - Fix regression in admin OrderedInlineMixin after refactor in 3.4.0 3.4.0 - 2020-05-11 ------------------ - Fix `bulk_create` not returning - Fix `OrderedModelQuerySet` returning parent class instances for polymorphic cases - Support django 3.0 - Drop support python 3.4 3.3.0 - 2019-07-10 ------------------ - `bulk_create` now works with `order_with_respect_to` - more internal refactoring moved most methods to `OrderedModelQuerySet` 3.2.0 - 2019-07-10 ------------------ - Internal refactoring now using `Manager` - probably will break some code - provide `bulk_create` 3.1.1 - 2018-11-13 ------------------ - Fix arrow-top and arrow-bottom not found 3.1.0 - 2018-11-10 ------------------ - Remove depricated `allow_tags` - Add `previous` and `next` methods - Add `top` and `bottom` buttons in admin - Delete duplicated code from InlineModelAdminMixin - Add Simplified Chinese translations - Use `import_string` instead of `__import__` - Make order field's `verbose_name` translatable(NOTE: this will cause creation of new migration file, which will not affect db state) 3.0.0 - 2018-09-21 ------------------ - Drop support for python 2.x - Drop support for django 1.x - Fix AdminInline for django > 2.1 - Do not install tests - delete deprecated methods `move`, `move_up`, `move_down` and `_move` 2.1.0 - 2018-08-16 ------------------ - Add support for Django 2.1 - Support `order_with_respect_to` on related fields - Add Tabular and Stacked inline 2.0.0 - 2018-06-07 ------------------ - Drop support for Django < 1.11 1.5.0 - 2018-06-07 ------------------ - Add support for Django 2.0 - Fix problem where swap took a queryset instead of a model instance 1.4.3 - 2017-08-29 ------------------ - Fix a problem with links in the admin when using multiple threads. 1.4.2 - 2017-08-18 ------------------ - Use Django's version of `six` - Fix various deprecations - Fix missing up/down links with custom primary key 1.4.1 - 2017-04-16 ------------------ ### Fixed - `pip install` not working due to missing `requirements.txt` 1.4.0 - 2017-04-14 ------------------ ### Added - Support for ordering using a specified base class when using Multi-table inheritance - Suport for Python 3.6, Django 1.10 and 1.11. ### Fixed - The move up/down links in OrderedTabularInline - Passing args to `filter()` which broke django-polymorphic. 1.3.0 – 2016-10-08 ------------------ - Add `extra_update` argument to various methods. - Fix bug in `order_with_respect_to` when using string in Python 3. 1.2.1 – 2016-07-12 ------------------ - Various bug fixes in admin - Add support for URL namespaces other than "admin" 1.2.0 – 2016-07-08 ------------------ - Remove support for Django <1.8 and Python 2.6 - Support for multiple order_with_respect_to fields - Remove usage of deprecated django.conf.urls.patterns 1.1.0 – 2016-01-15 ------------------ - Add support for many-to-many models. - Add Italian translations. 1.0.0 – 2015-11-24 ------------------ 1.0, because why not. Seems to be working alright for everyone. Some little things in this release: - Add support for custom order field by inheriting from `OrderedModelBase` and setting `order_field_name`. - Add support for Python 3. - Drop support for Django 1.4. 0.4.2 – 2015-06-02 ------------------ - Fix admin buttons not working with custom primary keys. - Fix admin using deprecated `get_query_set` method. 0.4.1 – 2015-04-06 ------------------ - Add support for Django 1.7 and 1.8. - Fix deprecation warning about module\_name. - Add French translations. 0.4.0 – 2014-07-31 ------------------ - Models can now be moved to any position, not just up and down. `move_up()` and `move_down()` are replaced by `up()` and `down()`. See the readme for the full set of new methods. - Add `order_with_respect_to` option so models can be ordered based on another field. - The admin ordering controls are now rendered using templates. - Ordering now always starts from 0 and has no gaps. Previously, gaps in the ordering numbers could appear when models were deleted, etc. - Fix bug where objects always get the order of "0". - Models with custom primary keys can now be used as ordered models. 0.3.0 – 2013-10-25 ------------------ - Support for Django 1.4, 1.5 and 1.6. - Fix list_filter being deselected when moving in admin - Improve performance of ordering by adding index and using Max aggregate 0.2.0 – 2012-11-14 ------------------ - First release django-ordered-model-3.7.4/LICENSE000066400000000000000000000026741440510626600165310ustar00rootroot00000000000000Copyright (c) 2009, Ben Firshman All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. django-ordered-model-3.7.4/MANIFEST.in000066400000000000000000000001371440510626600172520ustar00rootroot00000000000000include MANIFEST.in *.md LICENSE *.sh requirements.txt recursive-include ordered_model *.json django-ordered-model-3.7.4/README.md000066400000000000000000000355741440510626600170100ustar00rootroot00000000000000django-ordered-model ==================== [![Build Status](https://secure.travis-ci.org/bfirsh/django-ordered-model.png?branch=master)](https://travis-ci.org/bfirsh/django-ordered-model) [![PyPI version](https://badge.fury.io/py/django-ordered-model.svg)](https://badge.fury.io/py/django-ordered-model) [![codecov](https://codecov.io/gh/bfirsh/django-ordered-model/branch/master/graph/badge.svg)](https://codecov.io/gh/bfirsh/django-ordered-model) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) django-ordered-model allows models to be ordered and provides a simple admin interface for reordering them. Based on https://djangosnippets.org/snippets/998/ and https://djangosnippets.org/snippets/259/ See our [compatability notes](#compatibility-with-django-and-python) for the appropriate version to use with older Django and Python releases. Installation ------------ Please install using Pip: ```bash $ pip install django-ordered-model ``` Or if you have checked out the repository: ```bash $ python setup.py install ``` Or to use the latest development code from our master branch: ```bash $ pip uninstall django-ordered-model $ pip install git+git://github.com/django-ordered-model/django-ordered-model.git ``` Usage ----- Add `ordered_model` to your `SETTINGS.INSTALLED_APPS`. Inherit your model from `OrderedModel` to make it ordered: ```python from django.db import models from ordered_model.models import OrderedModel class Item(OrderedModel): name = models.CharField(max_length=100) ``` Then run the usual `$ ./manage.py makemigrations` and `$ ./manage.py migrate` to update your database schema. Model instances now have a set of methods to move them relative to each other. To demonstrate those methods we create two instances of `Item`: ```python foo = Item.objects.create(name="Foo") bar = Item.objects.create(name="Bar") ``` ### Swap positions ```python foo.swap(bar) ``` This swaps the position of two objects. ### Move position up on position ```python foo.up() foo.down() ``` Moving an object up or down just makes it swap its position with the neighbouring object directly above of below depending on the direction. ### Move to arbitrary position ```python foo.to(12) bar.to(13) ``` Move the object to an arbitrary position in the stack. This essentially sets the order value to the specified integer. Objects between the original and the new position get their order value increased or decreased according to the direction of the move. ### Move object above or below reference ```python foo.above(bar) foo.below(bar) ``` Move the object directly above or below the reference object, increasing or decreasing the order value for all objects between the two, depending on the direction of the move. ### Move to top of stack ```python foo.top() ``` This sets the order value to the lowest value found in the stack and increases the order value of all objects that were above the moved object by one. ### Move to bottom of stack ```python foo.bottom() ``` This sets the order value to the highest value found in the stack and decreases the order value of all objects that were below the moved object by one. ### Updating fields that would be updated during save() For performance reasons, the `delete()`, `to()`, `below()`, `above()`, `top()`, and `bottom()` methods use Django's `update()` method to change the order of other objects that are shifted as a result of one of these calls. If the model has fields that are typically updated in a customized save() method, or through other app level functionality such as `DateTimeField(auto_now=True)`, you can add additional fields to be passed through to `update()`. This will only impact objects where their order is being shifted as a result of an operation on the target object, not the target object itself. ```python foo.to(12, extra_update={'modified': now()}) ``` ### Get the previous or next objects ```python foo.previous() foo.next() ``` The `previous()` and `next()` methods return the neighbouring objects directly above or below within the ordered stack. ## Subset Ordering In some cases, ordering objects is required only on a subset of objects. For example, an application that manages contact lists for users, in a many-to-one/many relationship, would like to allow each user to order their contacts regardless of how other users choose their order. This option is supported via the `order_with_respect_to` parameter. A simple example might look like so: ```python class Contact(OrderedModel): user = models.ForeignKey(User, on_delete=models.CASCADE) phone = models.CharField() order_with_respect_to = 'user' ``` If objects are ordered with respect to more than one field, `order_with_respect_to` supports tuples to define multiple fields: ```python class Model(OrderedModel) # ... order_with_respect_to = ('field_a', 'field_b') ``` In a many-to-many relationship you need to use a separate through model which is derived from the OrderedModel. For example, an application which manages pizzas with toppings. A simple example might look like so: ```python class Topping(models.Model): name = models.CharField(max_length=100) class Pizza(models.Model): name = models.CharField(max_length=100) toppings = models.ManyToManyField(Topping, through='PizzaToppingsThroughModel') class PizzaToppingsThroughModel(OrderedModel): pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE) topping = models.ForeignKey(Topping, on_delete=models.CASCADE) order_with_respect_to = 'pizza' class Meta: ordering = ('pizza', 'order') ``` You can also specify `order_with_respect_to` to a field on a related model. An example use-case can be made with the following models: ```python class ItemGroup(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) general_info = models.CharField(max_length=100) class GroupedItem(OrderedModel): group = models.ForeignKey(ItemGroup, on_delete=models.CASCADE) specific_info = models.CharField(max_length=100) order_with_respect_to = 'group__user' ``` Here items are put into groups that have some general information used by its items, but the ordering of the items is independent of the group the item is in. In all cases `order_with_respect_to` must specify a `ForeignKey` field on the model, or a Django Check `E002`, `E005` or `E006` error will be raised with further help. When you want ordering on the baseclass instead of subclasses in an ordered list of objects of various classes, specify the full module path of the base class: ```python class BaseQuestion(OrderedModel): order_class_path = __module__ + '.BaseQuestion' question = models.TextField(max_length=100) class Meta: ordering = ('order',) class MultipleChoiceQuestion(BaseQuestion): good_answer = models.TextField(max_length=100) wrong_answer1 = models.TextField(max_length=100) wrong_answer2 = models.TextField(max_length=100) wrong_answer3 = models.TextField(max_length=100) class OpenQuestion(BaseQuestion): answer = models.TextField(max_length=100) ``` Custom Manager and QuerySet ----------------- When your model your extends `OrderedModel`, it inherits a custom `ModelManager` instance which in turn provides additional operations on the resulting `QuerySet`. For example if `Item` is an `OrderedModel` subclass, the queryset `Item.objects.all()` has functions: * `above_instance(object)`, * `below_instance(object)`, * `get_min_order()`, * `get_max_order()`, * `above(index)`, * `below(index)` If your `Model` uses a custom `ModelManager` (such as `ItemManager` below) please have it extend `OrderedModelManager`, or else Django Check `E003` will be raised. If your `ModelManager` returns a custom `QuerySet` (such as `ItemQuerySet` below) please have it extend `OrderedModelQuerySet`, or Django Check `E004` will be raised. ```python from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet class ItemQuerySet(OrderedModelQuerySet): pass class ItemManager(OrderedModelManager): def get_queryset(self): return ItemQuerySet(self.model, using=self._db) class Item(OrderedModel): objects = ItemManager() ``` If another Django plugin requires you to use specific `Model`, `QuerySet` or `ModelManager` classes, you might need to construct intermediate classes using multiple inheritance, [see an example in issue 270](https://github.com/django-ordered-model/django-ordered-model/issues/270). Custom ordering field --------------------- Extending `OrderedModel` creates a `models.PositiveIntegerField` field called `order` and the appropriate migrations. It customises the default `class Meta` to then order returned querysets by this field. If you wish to use an existing model field to store the ordering, subclass `OrderedModelBase` instead and set the attribute `order_field_name` to match your field name and the `ordering` attribute on `Meta`: ```python class MyModel(OrderedModelBase): ... sort_order = models.PositiveIntegerField(editable=False, db_index=True) order_field_name = "sort_order" class Meta: ordering = ("sort_order",) ``` Setting `order_field_name` is specific for this library to know which field to change when ordering actions are taken. The `Meta` `ordering` line is existing Django functionality to use a field for sorting. See `tests/models.py` object `CustomOrderFieldModel` for an example. Admin integration ----------------- To add arrows in the admin change list page to do reordering, you can use the `OrderedModelAdmin` and the `move_up_down_links` field: ```python from django.contrib import admin from ordered_model.admin import OrderedModelAdmin from models import Item class ItemAdmin(OrderedModelAdmin): list_display = ('name', 'move_up_down_links') admin.site.register(Item, ItemAdmin) ``` ![ItemAdmin screenshot](./static/items.png) For a many-to-many relationship you need one of the following inlines. `OrderedTabularInline` or `OrderedStackedInline` just like the django admin. For the `OrderedTabularInline` it will look like this: ```python from django.contrib import admin from ordered_model.admin import OrderedTabularInline, OrderedInlineModelAdminMixin from models import Pizza, PizzaToppingsThroughModel class PizzaToppingsTabularInline(OrderedTabularInline): model = PizzaToppingsThroughModel fields = ('topping', 'order', 'move_up_down_links',) readonly_fields = ('order', 'move_up_down_links',) ordering = ('order',) extra = 1 class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): model = Pizza list_display = ('name', ) inlines = (PizzaToppingsTabularInline, ) admin.site.register(Pizza, PizzaAdmin) ``` ![PizzaAdmin screenshot](./static/pizza.png) For the `OrderedStackedInline` it will look like this: ```python from django.contrib import admin from ordered_model.admin import OrderedStackedInline, OrderedInlineModelAdminMixin from models import Pizza, PizzaToppingsThroughModel class PizzaToppingsStackedInline(OrderedStackedInline): model = PizzaToppingsThroughModel fields = ('topping', 'move_up_down_links',) readonly_fields = ('move_up_down_links',) ordering = ('order',) extra = 1 class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): list_display = ('name', ) inlines = (PizzaToppingsStackedInline, ) admin.site.register(Pizza, PizzaAdmin) ``` ![PizzaAdmin screenshot](./static/pizza-stacked.png) **Note:** `OrderedModelAdmin` requires the inline subclasses of `OrderedTabularInline` and `OrderedStackedInline` to be listed on `inlines` so that we register appropriate URL routes. If you are using Django 3.0 feature `get_inlines()` or `get_inline_instances()` to return the list of inlines dynamically, consider it a filter and still add them to `inlines` or you might encounter a “No Reverse Match” error when accessing model change view. Re-ordering models ------------------ In certain cases the models will end up in a not properly ordered state. This can be caused by bypassing the 'delete' / 'save' methods, or when a user changes a foreign key of a object which is part of the 'order_with_respect_to' fields. You can use the following command to re-order one or more models. $ ./manage.py reorder_model . \ [. ... ] The arguments are as follows: - ``: Name of the application for the model. - ``: Name of the model that's an OrderedModel. Django Rest Framework --------------------- To support updating ordering fields by Django Rest Framework, we include a serializer `OrderedModelSerializer` that intercepts writes to the ordering field, and calls `OrderedModel.to()` method to effect a re-ordering: from rest_framework import routers, serializers, viewsets from ordered_model.serializers import OrderedModelSerializer from tests.models import CustomItem class ItemSerializer(serializers.HyperlinkedModelSerializer, OrderedModelSerializer): class Meta: model = CustomItem fields = ['pkid', 'name', 'modified', 'order'] class ItemViewSet(viewsets.ModelViewSet): queryset = CustomItem.objects.all() serializer_class = ItemSerializer router = routers.DefaultRouter() router.register(r'items', ItemViewSet) Note that you need to include the 'order' field (or your custom field name) in the `Serializer`'s `fields` list, either explicitly or using `__all__`. See [ordered_model/serializers.py](ordered_model/serializers.py) for the implementation. Test suite ---------- To run the tests against your current environment, use: ```bash $ pip install djangorestframework $ django-admin test --pythonpath=. --settings=tests.settings ``` Otherwise please install `tox` and run the tests for a specific environment with `-e` or all environments: ```bash $ tox -e py36-django30 $ tox ``` Compatibility with Django and Python ----------------------------------------- |django-ordered-model version | Django version | Python version | DRF (optional) |-----------------------------|---------------------|-------------------|---------------- | **3.6.x** | **3.x**, **4.x** | **3.5** and above | 3.12 and above | **3.5.x** | **3.x**, **4.x** | **3.5** and above | - | **3.4.x** | **2.x**, **3.x** | **3.5** and above | - | **3.3.x** | **2.x** | **3.4** and above | - | **3.2.x** | **2.x** | **3.4** and above | - | **3.1.x** | **2.x** | **3.4** and above | - | **3.0.x** | **2.x** | **3.4** and above | - | **2.1.x** | **1.x** | **2.7** to 3.6 | - | **2.0.x** | **1.x** | **2.7** to 3.6 | - Maintainers ----------- * [Ben Firshman](https://github.com/bfirsh) * [Chris Shucksmith](https://github.com/shuckc) * [Sardorbek Imomaliev](https://github.com/imomaliev) django-ordered-model-3.7.4/ordered_model/000077500000000000000000000000001440510626600203175ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/__init__.py000066400000000000000000000000751440510626600224320ustar00rootroot00000000000000default_app_config = "ordered_model.apps.OrderedModelConfig" django-ordered-model-3.7.4/ordered_model/admin.py000066400000000000000000000210721440510626600217630ustar00rootroot00000000000000from functools import update_wrapper from django.http import HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404 from django.urls import reverse from django.utils.encoding import escape_uri_path, iri_to_uri from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from django.contrib import admin from django.contrib.admin.utils import unquote from django.contrib.admin.options import csrf_protect_m from django.contrib.admin.views.main import ChangeList from django import VERSION class BaseOrderedModelAdmin: """ Functionality common to both OrderedModelAdmin and OrderedInlineMixin. """ request_query_string = "" def _get_model_info(self): return {"app": self.model._meta.app_label, "model": self.model._meta.model_name} def _get_changelist(self, request): list_display = self.get_list_display(request) list_display_links = self.get_list_display_links(request, list_display) args = ( request, self.model, list_display, list_display_links, self.list_filter, self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self.list_max_show_all, self.list_editable, self, ) if VERSION >= (2, 1): args = args + (self.sortable_by,) if VERSION >= (4, 0): args = args + (self.search_help_text,) return ChangeList(*args) @csrf_protect_m def changelist_view(self, request, extra_context=None): cl = self._get_changelist(request) self.request_query_string = cl.get_query_string() return super().changelist_view(request, extra_context) class OrderedModelAdmin(BaseOrderedModelAdmin, admin.ModelAdmin): def get_urls(self): from django.urls import path def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view) model_info = self._get_model_info() return [ path( "/move-/", wrap(self.move_view), name="{app}_{model}_change_order".format(**model_info), ) ] + super().get_urls() def move_view(self, request, object_id, direction): obj = get_object_or_404(self.model, pk=unquote(object_id)) if direction not in ("up", "down", "top", "bottom"): raise Http404 getattr(obj, direction)() # guts from request.get_full_path(), calculating ../../ and restoring GET arguments mangled = "/".join(escape_uri_path(request.path).split("/")[0:-3]) redir_path = "%s%s%s" % ( mangled, "/" if not mangled.endswith("/") else "", ("?" + iri_to_uri(request.META.get("QUERY_STRING", ""))) if request.META.get("QUERY_STRING", "") else "", ) return HttpResponseRedirect(redir_path) def move_up_down_links(self, obj): model_info = self._get_model_info() return render_to_string( "ordered_model/admin/order_controls.html", { "app_label": model_info["app"], "model_name": model_info["model"], "module_name": model_info["model"], # for backwards compatibility "object_id": obj.pk, "urls": { "up": reverse( "{admin_name}:{app}_{model}_change_order".format( admin_name=self.admin_site.name, **model_info ), args=[obj.pk, "up"], ), "down": reverse( "{admin_name}:{app}_{model}_change_order".format( admin_name=self.admin_site.name, **model_info ), args=[obj.pk, "down"], ), "top": reverse( "{admin_name}:{app}_{model}_change_order".format( admin_name=self.admin_site.name, **model_info ), args=[obj.pk, "top"], ), "bottom": reverse( "{admin_name}:{app}_{model}_change_order".format( admin_name=self.admin_site.name, **model_info ), args=[obj.pk, "bottom"], ), }, "query_string": self.request_query_string, }, ) move_up_down_links.short_description = _("Move") class OrderedInlineModelAdminMixin: """ ModelAdminMixin for classes that contain OrderedInilines """ def get_urls(self): urls = super().get_urls() for inline in self.inlines: if issubclass(inline, OrderedInlineMixin): urls = inline(self.model, self.admin_site).get_urls() + urls return urls class OrderedInlineMixin(BaseOrderedModelAdmin): def _get_model_info(self): return dict( **super()._get_model_info(), parent_model=self.parent_model._meta.model_name, ) def get_urls(self): from django.urls import path def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) wrapper.model_admin = self return update_wrapper(wrapper, view) model_info = self._get_model_info() return [ path( "/{model}//move-/".format( **model_info ), wrap(self.move_view), name="{app}_{parent_model}_{model}_change_order_inline".format( **model_info ), ) ] def move_view(self, request, admin_id, object_id, direction): obj = get_object_or_404(self.model, pk=unquote(object_id)) if direction not in ("up", "down", "top", "bottom"): raise Http404 getattr(obj, direction)() # guts from request.get_full_path(), calculating ../../ and restoring GET arguments mangled = "/".join(escape_uri_path(request.path).split("/")[0:-4] + ["change"]) redir_path = "%s%s%s" % ( mangled, "/" if not mangled.endswith("/") else "", ("?" + iri_to_uri(request.META.get("QUERY_STRING", ""))) if request.META.get("QUERY_STRING", "") else "", ) return HttpResponseRedirect(redir_path) def move_up_down_links(self, obj): if not obj.pk: return "" # Find the fields which refer to the parent model of this inline, and # use one of them if they aren't None. fields = [] for value in obj._get_related_objects(): # Note 'a class is considered a subclass of itself' pydocs if issubclass(self.parent_model, type(value)): if value is not None and value.pk is not None: fields.append(str(value.pk)) order_obj_name = fields[0] if len(fields) > 0 else None model_info = self._get_model_info() if not order_obj_name: return "" name = "{admin_name}:{app}_{parent_model}_{model}_change_order_inline".format( admin_name=self.admin_site.name, **model_info ) return render_to_string( "ordered_model/admin/order_controls.html", { "app_label": model_info["app"], "model_name": model_info["model"], "module_name": model_info["model"], # backwards compat "object_id": obj.pk, "urls": { "up": reverse(name, args=[order_obj_name, obj.pk, "up"]), "down": reverse(name, args=[order_obj_name, obj.pk, "down"]), "top": reverse(name, args=[order_obj_name, obj.pk, "top"]), "bottom": reverse(name, args=[order_obj_name, obj.pk, "bottom"]), }, "query_string": self.request_query_string, }, ) move_up_down_links.short_description = _("Move") class OrderedTabularInline(OrderedInlineMixin, admin.TabularInline): pass class OrderedStackedInline(OrderedInlineMixin, admin.StackedInline): pass django-ordered-model-3.7.4/ordered_model/apps.py000066400000000000000000000007421440510626600216370ustar00rootroot00000000000000from django.apps import AppConfig, apps from django.db.models.signals import post_delete class OrderedModelConfig(AppConfig): name = "ordered_model" label = "ordered_model" def ready(self): from .models import OrderedModelBase for cls in apps.get_models(): if issubclass(cls, OrderedModelBase): post_delete.connect( cls._on_ordered_model_delete, sender=cls, dispatch_uid=cls.__name__ ) django-ordered-model-3.7.4/ordered_model/locale/000077500000000000000000000000001440510626600215565ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/de/000077500000000000000000000000001440510626600221465ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/de/LC_MESSAGES/000077500000000000000000000000001440510626600237335ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/de/LC_MESSAGES/django.mo000066400000000000000000000007111440510626600255310ustar00rootroot00000000000000,<PQjVMoveProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2012-06-29 12:49+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1) Bewegendjango-ordered-model-3.7.4/ordered_model/locale/de/LC_MESSAGES/django.po000066400000000000000000000012401440510626600255320ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-06-29 12:49+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #: admin.py:50 msgid "Move" msgstr "Bewegen" django-ordered-model-3.7.4/ordered_model/locale/fr/000077500000000000000000000000001440510626600221655ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/fr/LC_MESSAGES/000077500000000000000000000000001440510626600237525ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/fr/LC_MESSAGES/django.po000066400000000000000000000013371440510626600255600ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-07-13 08:16+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2)\n" #: admin.py:76 msgid "Move" msgstr "Déplacer" django-ordered-model-3.7.4/ordered_model/locale/it/000077500000000000000000000000001440510626600221725ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/it/LC_MESSAGES/000077500000000000000000000000001440510626600237575ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/it/LC_MESSAGES/django.mo000066400000000000000000000021001440510626600255470ustar00rootroot00000000000000DlHhd@kKodkMoveThe method move() is deprecated and will be removed in the next release.The method move_down() is deprecated and will be removed in the next release. Please use down() instead!The method move_up() is deprecated and will be removed in the next release. Please use up() instead!Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2014-08-27 18:04+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); SpostaIl metodo move() è stato deprecato e sarà rimosso nella prossima release.Il metodo move_down() è stato deprecato e sarà rimosso nella prossima release.Al suo posto utilizzare down()!Il metodo move_up() è stato deprecato e sarà rimosso nella prossima release.Al suo posto utilizzare up()!django-ordered-model-3.7.4/ordered_model/locale/it/LC_MESSAGES/django.po000066400000000000000000000012401440510626600255560ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-08-27 18:04+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:76 msgid "Move" msgstr "Sposta" django-ordered-model-3.7.4/ordered_model/locale/pl/000077500000000000000000000000001440510626600221715ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/pl/LC_MESSAGES/000077500000000000000000000000001440510626600237565ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000010071440510626600255530ustar00rootroot00000000000000,<PQV MoveProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2012-07-13 08:16+0200 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2) Kolejnośćdjango-ordered-model-3.7.4/ordered_model/locale/pl/LC_MESSAGES/django.po000066400000000000000000000013411440510626600255570ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-07-13 08:16+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2)\n" #: admin.py:52 msgid "Move" msgstr "Kolejność" django-ordered-model-3.7.4/ordered_model/locale/zh_Hans/000077500000000000000000000000001440510626600231505ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/zh_Hans/LC_MESSAGES/000077500000000000000000000000001440510626600247355ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/locale/zh_Hans/LC_MESSAGES/django.mo000066400000000000000000000007371440510626600265430ustar00rootroot000000000000004L`afdlMoveorderProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2018-10-25 13:38+0800 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=1; plural=0; 移动序号django-ordered-model-3.7.4/ordered_model/locale/zh_Hans/LC_MESSAGES/django.po000066400000000000000000000014001440510626600265320ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-10-25 13:38+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: ordered_model/admin.py:111 ordered_model/admin.py:198 msgid "Move" msgstr "移动" #: ordered_model/models.py:216 msgid "order" msgstr "序号" django-ordered-model-3.7.4/ordered_model/management/000077500000000000000000000000001440510626600224335ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/management/__init__.py000066400000000000000000000000001440510626600245320ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/management/commands/000077500000000000000000000000001440510626600242345ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/management/commands/__init__.py000066400000000000000000000000001440510626600263330ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/management/commands/reorder_model.py000066400000000000000000000060101440510626600274250ustar00rootroot00000000000000from django.apps import apps from django.core.management import BaseCommand, CommandError from django.db import transaction from ordered_model.models import OrderedModelBase class Command(BaseCommand): help = "Re-do the ordering of a certain Model" def add_arguments(self, parser): parser.add_argument("model_name", type=str, nargs="*") def handle(self, *args, **options): """ Sometimes django-ordered-models ordering goes wrong, for various reasons, try re-ordering to a working state. """ self.verbosity = options["verbosity"] orderedmodels = [ m._meta.label for m in apps.get_models() if issubclass(m, OrderedModelBase) ] candidates = "\n {}".format("\n ".join(orderedmodels)) if not options["model_name"]: return self.stdout.write("No model specified, try: {}".format(candidates)) for model_name in options["model_name"]: if model_name not in orderedmodels: self.stdout.write( "Model '{}' is not an ordered model, try: {}".format( model_name, candidates ) ) break model = apps.get_model(model_name) if not issubclass(model, OrderedModelBase): raise CommandError( "{} does not inherit from OrderedModel or OrderedModelBase".format( str(model) ) ) self.reorder(model) def reorder(self, model): owrt = model.get_order_with_respect_to() if owrt: rel_kwargs = dict([("{}__isnull".format(k), False) for k in owrt]) relation_to_list = ( model.objects.order_by(*owrt) .values_list(*owrt) .filter(**rel_kwargs) .distinct() ) for relation_to in relation_to_list: kwargs = dict([(k, v) for k, v in zip(owrt, relation_to)]) # print('re-ordering: {}'.format(kwargs)) self.reorder_queryset(model.objects.filter(**kwargs)) else: self.reorder_queryset(model.objects.all()) @transaction.atomic def reorder_queryset(self, queryset): model = queryset.model order_field_name = model.order_field_name bulk_update_list = [] for order, obj in enumerate(queryset): if getattr(obj, order_field_name) != order: if self.verbosity: self.stdout.write( "changing order of {} ({}) from {} to {}".format( model._meta.label, obj.pk, getattr(obj, order_field_name), order, ) ) setattr(obj, order_field_name, order) bulk_update_list.append(obj) model.objects.bulk_update(bulk_update_list, [order_field_name]) django-ordered-model-3.7.4/ordered_model/models.py000066400000000000000000000405711440510626600221630ustar00rootroot00000000000000from functools import partial, reduce from django.core import checks from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist from django.db import models from django.db.models import Max, Min, F from django.db.models.fields.related import ForeignKey from django.db.models.constants import LOOKUP_SEP from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ def get_lookup_value(obj, field): try: return reduce(lambda i, f: getattr(i, f), field.split(LOOKUP_SEP), obj) except ObjectDoesNotExist: return None class OrderedModelQuerySet(models.QuerySet): def _get_order_field_name(self): return self.model.order_field_name def _get_order_field_lookup(self, lookup): order_field_name = self._get_order_field_name() return LOOKUP_SEP.join([order_field_name, lookup]) def get_max_order(self): order_field_name = self._get_order_field_name() return self.aggregate(Max(order_field_name)).get( self._get_order_field_lookup("max") ) def get_min_order(self): order_field_name = self._get_order_field_name() return self.aggregate(Min(order_field_name)).get( self._get_order_field_lookup("min") ) def get_next_order(self): order = self.get_max_order() return order + 1 if order is not None else 0 def above(self, order, inclusive=False): """Filter items above order.""" lookup = "gte" if inclusive else "gt" return self.filter(**{self._get_order_field_lookup(lookup): order}) def above_instance(self, ref, inclusive=False): """Filter items above ref's order.""" order_field_name = self._get_order_field_name() order = getattr(ref, order_field_name) return self.above(order, inclusive=inclusive) def below(self, order, inclusive=False): """Filter items below order.""" lookup = "lte" if inclusive else "lt" return self.filter(**{self._get_order_field_lookup(lookup): order}) def below_instance(self, ref, inclusive=False): """Filter items below ref's order.""" order_field_name = self._get_order_field_name() order = getattr(ref, order_field_name) return self.below(order, inclusive=inclusive) def decrease_order(self, **extra_kwargs): """Decrease `order_field_name` value by 1.""" order_field_name = self._get_order_field_name() update_kwargs = {order_field_name: F(order_field_name) - 1} if extra_kwargs: update_kwargs.update(extra_kwargs) return self.update(**update_kwargs) def increase_order(self, **extra_kwargs): """Increase `order_field_name` value by 1.""" order_field_name = self._get_order_field_name() update_kwargs = {order_field_name: F(order_field_name) + 1} if extra_kwargs: update_kwargs.update(extra_kwargs) return self.update(**update_kwargs) def bulk_create(self, objs, *args, **kwargs): order_field_name = self._get_order_field_name() order_with_respect_to = self.model.get_order_with_respect_to() objs = list(objs) order_with_respect_to_mapping = {} for obj in objs: key = frozenset(obj._wrt_map().items()) if key in order_with_respect_to_mapping: order_with_respect_to_mapping[key] += 1 else: order_with_respect_to_mapping[key] = self.filter( **obj._wrt_map() ).get_next_order() setattr(obj, order_field_name, order_with_respect_to_mapping[key]) return super().bulk_create(objs, *args, **kwargs) class OrderedModelManager(models.Manager.from_queryset(OrderedModelQuerySet)): pass class OrderedModelBase(models.Model): """ An abstract model that allows objects to be ordered relative to each other. Usage (See ``OrderedModel``): - create a model subclassing ``OrderedModelBase`` - add an indexed ``PositiveIntegerField`` to the model - set ``order_field_name`` to the name of that field - use the same field name in ``Meta.ordering`` [optional] - set ``order_with_respect_to`` to limit order to a subset - specify ``order_class_path`` in case of polymorphic classes """ objects = OrderedModelManager() order_field_name = None order_with_respect_to = None order_class_path = None class Meta: abstract = True def __init__(self, *args, **kwargs): super(OrderedModelBase, self).__init__(*args, **kwargs) self._original_wrt_map = self._wrt_map() def _wrt_map(self): d = {} for order_wrt_name in self.get_order_with_respect_to(): # we know order_wrt_name is a ForeignKey, so use a cheaper _id lookup field_path = order_wrt_name + "_id" d[order_wrt_name] = get_lookup_value(self, field_path) return d def _get_related_objects(self): # slow path, for use in the admin which requires the objects # expected to generate extra queries return [ get_lookup_value(self, name) for name in self.get_order_with_respect_to() ] @classmethod def _on_ordered_model_delete(cls, sender=None, instance=None, **kwargs): """ This signal handler makes sure that when an OrderedModelBase is deleted via cascade database deletes, or queryset delete that the models keep order. """ if getattr(instance, "_was_deleted_via_delete_method", False): return extra_update = kwargs.get("extra_update", None) # Copy of upshuffle logic from OrderedModelBase.delete qs = instance.get_ordering_queryset() extra_update = {} if extra_update is None else extra_update qs.above_instance(instance).decrease_order(**extra_update) setattr(instance, "_was_deleted_via_delete_method", True) @classmethod def get_order_with_respect_to(cls): if type(cls.order_with_respect_to) is tuple: return cls.order_with_respect_to elif type(cls.order_with_respect_to) is str: return (cls.order_with_respect_to,) elif cls.order_with_respect_to is None: return tuple() else: raise ValueError("Invalid value for model.order_with_respect_to") def _validate_ordering_reference(self, ref): if self._wrt_map() != ref._wrt_map(): raise ValueError( "{0!r} can only be swapped with instances of {1!r} with equal {2!s} fields.".format( self, self._meta.default_manager.model, " and ".join( ["'{}'".format(o) for o in self.get_order_with_respect_to()] ), ) ) def get_ordering_queryset(self, qs=None, wrt=None): if qs is None: if self.order_class_path: model = import_string(self.order_class_path) qs = model._meta.default_manager.all() else: qs = self._meta.default_manager.all() if wrt: return qs.filter(**wrt) return qs.filter(**self._wrt_map()) def previous(self): """ Get previous element in this object's ordered stack. """ return self.get_ordering_queryset().below_instance(self).last() def next(self): """ Get next element in this object's ordered stack. """ return self.get_ordering_queryset().above_instance(self).first() def save(self, *args, **kwargs): order_field_name = self.order_field_name wrt_changed = self._wrt_map() != self._original_wrt_map if wrt_changed and getattr(self, order_field_name) is not None: # do delete-like upshuffle using original_wrt values! qs = self.get_ordering_queryset(wrt=self._original_wrt_map) qs.above_instance(self).decrease_order() if getattr(self, order_field_name) is None or wrt_changed: order = self.get_ordering_queryset().get_next_order() setattr(self, order_field_name, order) super().save(*args, **kwargs) self._original_wrt_map = self._wrt_map() def delete(self, *args, extra_update=None, **kwargs): # Flag re-ordering performed so that post_delete signal # does not duplicate the re-ordering. See signals.py self._was_deleted_via_delete_method = True qs = self.get_ordering_queryset() extra_update = {} if extra_update is None else extra_update qs.above_instance(self).decrease_order(**extra_update) super().delete(*args, **kwargs) def swap(self, replacement): """ Swap the position of this object with a replacement object. """ self._validate_ordering_reference(replacement) order_field_name = self.order_field_name order, replacement_order = ( getattr(self, order_field_name), getattr(replacement, order_field_name), ) setattr(self, order_field_name, replacement_order) setattr(replacement, order_field_name, order) self.save() replacement.save() def up(self): """ Move this object up one position. """ previous = self.previous() if previous: self.swap(previous) def down(self): """ Move this object down one position. """ _next = self.next() if _next: self.swap(_next) def to(self, order, extra_update=None): """ Move object to a certain position, updating all affected objects to move accordingly up or down. """ if not isinstance(order, int): raise TypeError( "Order value must be set using an 'int', not using a '{0}'.".format( type(order).__name__ ) ) order_field_name = self.order_field_name if order is None or getattr(self, order_field_name) == order: # object is already at desired position return qs = self.get_ordering_queryset() extra_update = {} if extra_update is None else extra_update if getattr(self, order_field_name) > order: qs.below_instance(self).above(order, inclusive=True).increase_order( **extra_update ) else: qs.above_instance(self).below(order, inclusive=True).decrease_order( **extra_update ) setattr(self, order_field_name, order) self.save() def above(self, ref, extra_update=None): """ Move this object above the referenced object. """ self._validate_ordering_reference(ref) order_field_name = self.order_field_name if getattr(self, order_field_name) == getattr(ref, order_field_name): return if getattr(self, order_field_name) > getattr(ref, order_field_name): o = getattr(ref, order_field_name) else: o = self.get_ordering_queryset().below_instance(ref).get_max_order() or 0 self.to(o, extra_update=extra_update) def below(self, ref, extra_update=None): """ Move this object below the referenced object. """ self._validate_ordering_reference(ref) order_field_name = self.order_field_name if getattr(self, order_field_name) == getattr(ref, order_field_name): return if getattr(self, order_field_name) > getattr(ref, order_field_name): o = self.get_ordering_queryset().above_instance(ref).get_min_order() or 0 else: o = getattr(ref, order_field_name) self.to(o, extra_update=extra_update) def top(self, extra_update=None): """ Move this object to the top of the ordered stack. """ o = self.get_ordering_queryset().get_min_order() self.to(o, extra_update=extra_update) def bottom(self, extra_update=None): """ Move this object to the bottom of the ordered stack. """ o = self.get_ordering_queryset().get_max_order() self.to(o, extra_update=extra_update) @classmethod def check(cls, **kwargs): errors = super().check(**kwargs) ordering = getattr(cls._meta, "ordering", None) if ordering is None or len(ordering) < 1: errors.append( checks.Error( "OrderedModelBase subclass needs Meta.ordering specified.", hint="If you have overwritten Meta, try inheriting with Meta(OrderedModel.Meta).", obj=str(cls.__qualname__), id="ordered_model.E001", ) ) owrt = getattr(cls, "order_with_respect_to") if not (type(owrt) is tuple or type(owrt) is str or owrt is None): errors.append( checks.Error( "OrderedModelBase subclass order_with_respect_to value invalid. Expected tuple, str or None.", obj=str(cls.__qualname__), id="ordered_model.E002", ) ) if not issubclass(cls.objects.__class__, OrderedModelManager): # Not using our Manager. This is an Error if the queryset is also wrong, or # a Warning if our own QuerySet is returned. if issubclass(cls.objects.none().__class__, OrderedModelQuerySet): errors.append( checks.Warning( "OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager. This is not ideal but will work.", obj=str(cls.__qualname__), id="ordered_model.W003", ) ) else: errors.append( checks.Error( "OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager.", obj=str(cls.__qualname__), id="ordered_model.E003", ) ) elif not issubclass(cls.objects.none().__class__, OrderedModelQuerySet): errors.append( checks.Error( "OrderedModelBase subclass ModelManager did not return a QuerySet inheriting from OrderedModelQuerySet.", obj=str(cls.__qualname__), id="ordered_model.E004", ) ) # each field may be an FK, or recursively an FK ref to an FK try: for wrt_field in cls.get_order_with_respect_to(): mc = cls for p in wrt_field.split(LOOKUP_SEP): try: f = mc._meta.get_field(p) if not isinstance(f, ForeignKey): errors.append( checks.Error( "OrderedModel order_with_respect_to specifies field '{0}' (within '{1}') which is not a ForeignKey. This is unsupported.".format( p, wrt_field ), obj=str(cls.__qualname__), id="ordered_model.E005", ) ) break mc = f.remote_field.model except FieldDoesNotExist: errors.append( checks.Error( "OrderedModel order_with_respect_to specifies field '{0}' (within '{1}') which does not exist.".format( p, wrt_field ), obj=str(cls.__qualname__), id="ordered_model.E006", ) ) except ValueError: # already handled by type checks for E002 pass return errors class OrderedModel(OrderedModelBase): """ An abstract model that allows objects to be ordered relative to each other. Provides an ``order`` field. """ order = models.PositiveIntegerField(_("order"), editable=False, db_index=True) order_field_name = "order" class Meta: abstract = True ordering = ("order",) django-ordered-model-3.7.4/ordered_model/serializers.py000066400000000000000000000065211440510626600232310ustar00rootroot00000000000000from rest_framework import serializers, fields class OrderedModelSerializer(serializers.ModelSerializer): """ A ModelSerializer to provide a serializer that can be update and create objects in a specific order. Typically a `models.PositiveIntegerField` field called `order` is used to store the order of the Model objects. This field can be customized by setting the `order_field_name` attribute on the Model class. This serializer will move the object to the correct order if the ordering field is passed in the validated data. """ def get_order_field(self): """ Return the field name for the ordering field. If inheriting from `OrderedModelBase`, the `order_field_name` attribute must be set on the Model class. If inheriting from `OrderedModel`, the `order_field_name` attribute is not required, as the `OrderedModel` has the `order_field_name` attribute defaulting to 'order'. Returns: str: The field name for the ordering field. Raises: AttributeError: If the `order_field_name` attribute is not set, either on the Model class or on the serializer's Meta class. """ ModelClass = self.Meta.model # pylint: disable=no-member,invalid-name order_field_name = getattr(ModelClass, "order_field_name") if not order_field_name: raise AttributeError( "The `order_field_name` attribute must be set to use the " "OrderedModelSerializer. Either inherit from OrderedModel " "(to use the default `order` field) or inherit from " "`OrderedModelBase` and set the `order_field_name` attribute " "on the " + ModelClass.__name__ + " Model class." ) return order_field_name def get_fields(self): # make sure that DRF considers the ordering field writable order_field = self.get_order_field() d = super().get_fields() for name, field in d.items(): if name == order_field: if field.read_only: d[name] = fields.IntegerField() return d def update(self, instance, validated_data): """ Update the instance. If the `order_field_name` attribute is passed in the validated data, the instance will be moved to the specified order. Returns: Model: The updated instance. """ order = None order_field = self.get_order_field() if order_field in validated_data: order = validated_data.pop(order_field) instance = super().update(instance, validated_data) if order is not None: instance.to(order) return instance def create(self, validated_data): """ Create a new instance. If the `order_field_name` attribute is passed in the validated data, the instance will be created at the specified order. Returns: Model: The created instance. """ order = None order_field = self.get_order_field() if order_field in validated_data: order = validated_data.pop(order_field) instance = super().create(validated_data) if order is not None: instance.to(order) return instance django-ordered-model-3.7.4/ordered_model/static/000077500000000000000000000000001440510626600216065ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/static/ordered_model/000077500000000000000000000000001440510626600244125ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/static/ordered_model/arrow-bottom.gif000066400000000000000000000001201440510626600275260ustar00rootroot00000000000000GIF89a Ȳɱ虙!, XJ5Xyaj\ ;django-ordered-model-3.7.4/ordered_model/static/ordered_model/arrow-down.gif000066400000000000000000000001201440510626600271710ustar00rootroot00000000000000GIF89a Ȳɱ虙!, XJ5Xyaj\bg ;django-ordered-model-3.7.4/ordered_model/static/ordered_model/arrow-top.gif000066400000000000000000000015111440510626600270310ustar00rootroot00000000000000GIF89a Ȳɱ虙!, & (ЀA 4X 0 Dh|H A;django-ordered-model-3.7.4/ordered_model/static/ordered_model/arrow-up.gif000066400000000000000000000015061440510626600266570ustar00rootroot00000000000000GIF89a Ȳɱ虙!, # 4@` 0  ​"ܸ@@;django-ordered-model-3.7.4/ordered_model/templates/000077500000000000000000000000001440510626600223155ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/templates/ordered_model/000077500000000000000000000000001440510626600251215ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/templates/ordered_model/admin/000077500000000000000000000000001440510626600262115ustar00rootroot00000000000000django-ordered-model-3.7.4/ordered_model/templates/ordered_model/admin/order_controls.html000066400000000000000000000006741440510626600321440ustar00rootroot00000000000000{% load static %} django-ordered-model-3.7.4/requirements.txt000066400000000000000000000000071440510626600207740ustar00rootroot00000000000000Django django-ordered-model-3.7.4/script/000077500000000000000000000000001440510626600170175ustar00rootroot00000000000000django-ordered-model-3.7.4/script/release000077500000000000000000000001131440510626600203600ustar00rootroot00000000000000#!/bin/bash set -e git tag $1 git push --tags python setup.py sdist upload django-ordered-model-3.7.4/script/take_screenshots.sh000077500000000000000000000027411440510626600227260ustar00rootroot00000000000000# requires ubuntu # sudo apt install cutycapt xvfb set -x # https://stackoverflow.com/questions/24390488/django-admin-without-authentication # https://askubuntu.com/questions/75058/how-can-i-take-a-full-page-screenshot-of-a-webpage-from-the-command-line # delete test DB if it exists rm -f testdb rm -Rf tests/staticfiles mkdir -p tests/migrations tests/staticfiles touch tests/migrations/__init__.py mkdir -p static killall django-admin function djangoadmin() { django-admin $1 --pythonpath=. --settings=tests.settings --skip-checks $2 } djangoadmin "makemigrations" djangoadmin "migrate" # requires Django > 3.0 DJANGO_SUPERUSER_PASSWORD=password DJANGO_SUPERUSER_EMAIL="x@test.com" DJANGO_SUPERUSER_USERNAME=admin \ djangoadmin "createsuperuser" "--no-input" djangoadmin "collectstatic" # to refresh sample data, use runserver then this export command # django-admin dumpdata --pythonpath=. --settings=tests.settings tests --output tests/fixtures/screenshot-sample-data.json --indent 4 djangoadmin "loaddata" "screenshot-sample-data" django-admin runserver --pythonpath=. --settings=tests.settings_autoauth 7000 & sleep 2 function capture() { xvfb-run --server-args="-screen 0, 1024x768x24" cutycapt --url=http://localhost:7000/$1 --out=static/$2 } capture "admin/tests/item/" "items.png" capture "admin/tests/pizza/1/change/" "pizza.png" capture "admin/tests/pizzaproxy/1/change/" "pizza-stacked.png" sleep 1 killall django-admin rm -Rf tests/migrations rm -Rf tests/staticfiles django-ordered-model-3.7.4/setup.py000066400000000000000000000031761440510626600172340ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup with open("requirements.txt") as f: requires = f.read().splitlines() with open("README.md", encoding="utf-8") as f: long_description = f.read() setup( name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", version="3.7.4", description="Allows Django models to be ordered and provides a simple admin interface for reordering them.", author="Ben Firshman", author_email="ben@firshman.co.uk", url="http://github.com/django-ordered-model/django-ordered-model", packages=[ "ordered_model", "ordered_model.management", "ordered_model.management.commands", ], requires=requires, classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", ], zip_safe=False, package_data={ "ordered_model": [ "static/ordered_model/arrow-up.gif", "static/ordered_model/arrow-down.gif", "static/ordered_model/arrow-top.gif", "static/ordered_model/arrow-bottom.gif", "locale/de/LC_MESSAGES/django.po", "locale/de/LC_MESSAGES/django.mo", "locale/pl/LC_MESSAGES/django.po", "locale/pl/LC_MESSAGES/django.mo", "templates/ordered_model/admin/order_controls.html", ] }, ) django-ordered-model-3.7.4/static/000077500000000000000000000000001440510626600170025ustar00rootroot00000000000000django-ordered-model-3.7.4/static/items.png000066400000000000000000001000271440510626600206310ustar00rootroot00000000000000PNG  IHDR Xvp pHYsaa?i IDATxw^q5E(`7`7h~c|-Q!hT, (JiG?q~{?[X~>>gƇOÌr""""""ޮp(((((((((((((((((xvDD=ʔr&#UerOE&lͭ$h/oekFZObd5&+|r 4#lb?7LaxT5uFvXLnǏ㖩$a`gv /nᗳjo69>%PNj+ޮ:sVr.Y@|h=FRYN_Ώ\GerODr.Lép \71A}jhٚĿ7RhaE\4k̬;ƻtBy~uݹ?K?s- kSI* rO$&U1`Po7 +;:/GtPDJ|^CJ@m~\3!c04w:h |v7dt u8/ϟ~=mpZpau.bB|mTxQj]Ӟ2J}9 ćs2cc*vlM5綳lk?r"^֏o3']"xT cq4ڍWojwgdwnp0f [?N Rʿ7Ű?FDUstؐB;/|Wǂ6;1?V+[[Mz~zv&wOOBFLN̐Z8'{:x2_K_֌ ~j^ 嶳Hϟ $%'=1 *iUZɭb8N'x2J88oP)wq*w73oi7羌egv & -ᑋSyh Rr\eMF{F:U^A4؍L_ίgqAIDt.X"r+31xe[4LX0Ig/5ؚDyEizߜ oF"~ P"?r+]}LImZxwW_t@ss\=(`_;*2O8ZCGp=\Gd@C@u:fᶼʒwQVk- #@n_J -ĆԷZsK؜fŇ"0[dZ:U٣ž]X""*u3蠱1Q \:Q3 ]Qn̈́ hmvp~'X-u6#emݛOeb2:UlTt14_KNdtRsxؐzr+4ݞ p9 q- `r`o4Zߢ+ױ-;9Zd՞eQZӼvw[N- 5CJ Mx|hPhg[3050֢fķk#"r:P3SRciЈ40QXe0`19x=:|vJjk_``o/G(Gxm[4}KY'ݕ:/xv NMh0g/c2OΡpVӥi m8 *7L!| njsꆎCșDDDh auxdݺpYYGgǭ50y)KPMP_[ir 3~&n>Ğ܀6˴ i|lf ޷f3%V i-V2_9kl=IșFc@D䌶`D!Ӄ[a8ZvdZҊ%jMąuZ6?kAmQ˱n[' ǀmo[27~sm0H鄽y$fpDM=0#Emew)eHd2:Vbl%"? "r:g`Fd5b M؃N`.Qj]\H@Z33nb&Z\>&O_j3fȓ'{{9rl,^G|)537' >]7)[fZnk20 ~ok,D5`:iOtP)i5#ZW65sH/Vip*YDL""=щbrgibpZ~vN&?_>@AU}V s89~6#kv8={hGUsl" q2j??cxj<7^#kxCdyֳi(44#k1)GRTmq[2&07`;\6&F<=mH ( D4``x-M#֕)̛_;+;kKF&7=z" /:|VBFUR xwW_4cQ\71'Ӆƀi1\1&F* [3$;|=8OΡ#֦~3(=3QXeW 喩Y}.8΢j w/O <0?*+RX;W-_ߟœrQM-卯8T!)jkFsL7Cx]GIG|$#ҮE8h ~&L\ʝf~m~m[DDL.X"pшBnC^7Œ㙮OrT]ce=xCfW:AzQSd`)^&^1}0 l>P3+Өol{Ƒ[Row] rԭ#Iq,c4kw׷O4&vUtUu,c4Iqirma4"˦Esǂײ MO 5ŊxzvyLMo^^ZƓ_hnlX7ZMw,}OMC# Oyu-///?BVnv?l&q w5p:,=?ͨ0{~x fn#µܒ v;pRUkw[n2p4Mky| ^Z_nYzUlMMgLB|Yv-^&n7ɉ 0p +@u]C׿/s-N'{3r 7LezFk(~vѹ5oەɉ \q8}}fMdQ,Ϳ%25iN53&k5s'_Ovq9̯CIe NTd[F2 Kx))ooo뒸Y{:##1LWV8_澆Gq@Yu LYy/]_~*LF#v>a~7Rh#_A,""r؛^ohp2aH0 y{QSogx|`,ޘY5f~dVLJ~2<GT7>&/ 䑷1 PRPf22mxΓݺ9#xr|Z1:<Ot7=Fq^&w.L}?+U<3 )79|Hjv۲=\!X56Wmy t='^7= ?n۫49>XpL[׿-}y d&7ΞU]FaYħ;?xEٗGx?.9dnozQ ǓGXCQX^ŜqIƅī{n,x9_0ab6?Gs>o=%?r >: Չ挋`N׉:,:k4w=ʫk4?/k} C]ğ]G/i!?]:Ǒ"׹>r"Vm "C]DN5v F$.ƨ>l:Pʁ*xu]&f.ܗh979tWK`B#yCkd`>$.j6qVedV|z f.x9%G2 ʯ\DŽZH:bor2qHӆ>^{p&F r->j8}@sIWTc2 J/xSZU@UI/ֱ‰|-ra՟cort:և;a+7! 5lp㜩mo;>[s{dN{3(:µʈhX & s-7 y[( v{j((b@o}f]J*c0,.@sRPV |}Fi_O6m>xyL59wuꬎs'$;(Z|u n؆j_G akj:fp8O>ݪ"r ٓ^@2 ش~`+Gj\eI}+/v?*ԛ"b6RPvNE`+ kwrwCN'̟ԗgqIuJoo̡o9#tGo``;M/Ĭ1}8_ÌQ}H]U"~?)i=ڝY8eoǐ~Y[4ڛޯO9IJP& gKj:~>GM}{3Zx~͗dž]l>N햗Xf{??r"ni>xLUWX^ŋ~qLF^d[T3w|gW΋w_]W?Ōom]F9#Ձ4!"8Ȑ ʪp8<|=$>̆Tv9u{s@fa)qV$28y! 6<71~ai';>"B\7,>Zj[6u: $ȽV1}[R& MVDԥ"r sI!ZMl9Xʮd^c9U@frv959Jh-J)c` 3s&ݭqT6]\ǖ2G +E]{m1ng eR"l_i)/3w^sL|1֏?9 bxsB|9f(ծ';S_ajR.8OV_ֱֽŏisGž6Ðe Hc))yӰܺ[UNsr}1Z?YKPq8ǀtBP;>> )3.Wt3F E3(=r_-d9$Fr g549Ocİ R\Y'\ Y^8!u~Z餎Fѵy8k}rW:;|E.X"$0aH0[Rh9(jd>ݯ%"&mSϟ:;&'!ǻCZM)(?LF3R_Mdo{.Nj)!ZX),>?ۧ1{Z-&*"2؛^ԟ=T0>#u1+yo؛|Q5F fujX.#*4ؾZI49|[/qczt;>5nkDX֨?`0#CYv<{X^{BSQ\Y͕s-o~];yھ+j? 7>*{okzLxCb"Yv39㓀I&'&PU[ͻ3oäM7 CؒNNIU 3FaOnI -nweW%χ 'Cc:?`PQSz_$DÈF;//ScDԤ"r )h`0c&y@C݇=Ǿ]YRĔPE7?,FysݬXJq:a|hVm|oI2,.YcnR1}t8f/#{{=!-9# gH~wPʫmnoiMF ˨dX\va2u=.*f/#A~vY ͚LQyy=Naڈ5?lMU2_R[_[8ws57 SQx m~/oqܳ0  ] & gc'=aʼnZm{cx[V‚p8۷s'f׮:^K5 Օs͘ _k,[yLܕeF25z({o[u/̜HdH %ϛVu**cch6e!$%yV릏 g6[:YWm'<-?o+6nH9 lnΜIˉ IDAT{s@/pvPDz`bgTRylCqxfUX.t>ܕ듰ٛ"]Wx4Ϭ;(h~~ n0~5hWҧqײWmSytBEm~cv]0 ȯm;a?r6>V>ܲ}:Z?#oۡ n;mLіmc1^P/~k/ w_-{ٛ\kb}}[fkj:OiwO6b2aojbŦ:FiU-{C45u<\cHff<˱x(΃| 3{JƊn`w_:%KSz*E'eB./ 'DjK)|W@D#$R^ckՒ#"""?,%"ٗEDDAEDDDDc"" 1nΞIMҝBhX#>U5GGG׻;jʫhca<Q,)SvDDDDDB- """""1 """""1 """""1 """""1 """""1 """""1 """""1 """""1 """""1 """""1 """""1^y6-ÈPnw/լs}|Qp`.ϛvޮj9%3Fv66ڿ,^fxYoD1*&OIDDDD =pRTU*ݖ"ysk9C I>9,C\9a !4cT`6?2Q1a4؛ؑYĺ98c %f2 F'p̑l:χ3-m=>>AWkbD0B9TPN^y g@ !VjdkePDFR!ą`2(goN 6u9Ӓ(b醽 qra~޼AlG >ܝ.7Mqyn>|-^\9aU66EbT3~Gǚ?*  l~V/8@~9ÿ>G7o c-"} p,"*؏ѡ;MӁh`WԲ/`?+cbéQU@ti74X>/bB "/Q1l=VIDDDDLdVR?f$F-3 U0_(f/#L䠰 @NX$)"ȯ#%rXLxM 5;k~Hcb[:{*adA>ߛ* v/&7mM駽'+eH`k)ﰎ7; Éy4ڈ %:؏lpI#ٓSBNE]#%X-jNiMs]dg_i bHKnY &k}zIeӭRAMae$*pѮFCqސ~UQVЭsX-bB9Wk_W;/qlWԒUZݭ:șHM@j7ϵjr8yfhopg1w;tvyeLIfIEDDDD~8NT鄰@e}c-d4kzj>Do$o;:h`X|,&vdlO/dH-tZd_yyѪ񳚩۩iQhDC@7)!%zEDDD4v٥49\:v>$E0sh?8!+Jkٗ[?>]YEWԲXãØ68 [j*jzt R0 h3z/9CG]DDDDD̡""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""cp:ޮ0DDDDDD#.\؋n̞= """rJ;|pHKK/t-:t(ϧѣGINNv}p$%%ЀdΝl۶O6 g}Ƽy󈍍d2KUUk׮%//r׶eee :E/ug?}[nyoW>|^j9rÇw -f33gO>9#tرc=.,, ooo@233yw1 s9,X{ϵ~lݺzl/裏fܹKu>>>l6{6l=O?Եlȑ]>]9v3RTTѣGUVBmm-QQQ̝; &tz,V\ɑ#Gp8 8 V+WrA\7W1VZG}D'T+|A͛ĉK//ٳK]w'|Bnn.V#Gr%*裏rʕ+l63j(*ٸq#6lN\hrnUUU^]vQUUEXXSLaΜ9]=QMM ˗/g׮]455UW]Ett[L>cҨ%<Z/ssuaٲe O<s+裏2}t.rf37ߤ3f`,Xq/ǀYm۶qWGEE}=<>>>{EI~~>o&/2r 7Bzz::&sYέG}CrwDFFGaɒ%~e+׷ECCO?4SNel6>z)~aؠEWnn.o,Zȭlܸ xbx7Yl?O]劋yǘ:u*\s fR[[osoHnoc06l*1!VǠul[ou^~=G@1tPK\\]SNeر[V.";w'ҫq뭷q[oŴiӸ ߟ &p-bŊvoβe˸[:u*r2k,/_v &`cĉ|,_[݌]ƍ{HLLח(n&[ݜƒ%K4h <믿;wN||<5K.7~o&qqq,^H:t({/;СC̟?ӧBDD\s F={8pE1c ftM_՚bǎ,YQFH||<7pvrqʕ 6+pmڴVbcc1{nxJJJݮ[UU)))mfܹ|n뛐QFbbb(..v-KZNZ*,L6eXV~N'vsm߄"""ΫmFAXXz+/ˋ!CYh4٠Exx8Cb߯_?bbb8|pÖ-[Xv럆ȉ˷l~zr+h 2{lfϞMii)۶m#99%KKVVmݦtӹ{\Dmm-f[1 }=?.JOO';;>zc41nEZx{{t:ill7VZDEE~Ȑ!86auyJ]]۲">SRSS[<+-++6˞ܛ"""r\`` eee].oZ߿?׿\a?}5̘16Ü9s'ڵk .^^^Ύ;\Ljmyg{S"(44sryOop7S]]MDDD!$W]]ͤI;ܶW\+#Fd2yFz___N'ml}]cǎSO1}t,YBXX& M8qM{ͽ)"""ߟ.OLL$##íѣ\pqqq~ixWZ嶏)S0nܸN]]]믿Vf߾}L>o}߿z1l6mĉ… Yt)7x#>>>TWWj芖ٶ:ۻW@ny~|||hw}YYF݇ۜ,X;gK*:,mM9nzJJJx3<4Ϫ-ݕn湹nSBsχGy}OvU!!!ZR Ǝj1hrb6U}{uG#sN"--e˖5\*}Yrrrhll$;;{͛7{ 75kXbeeesA}WyfV^Myy9tR-Zֽ(::ףEcTVV+yxg/M6t;9r^{BIKK'$::YNԝUӧOgܹrx\]NU ,ȑ#;PYYyZ<{SDDDEFF2~ޮk„ DFFjzuz@@oINNO?%//FILL䷿-}q `<3TUUԩS;wnӗ >_^?Ol6?ۗXb> 6 ƍdzg׿n̛7^x￟cr-Ѭ[z ;ݾ|}jBCCYr%=SNeٝN!םUf`0|rJKK 笳gqh 㗿%1L 2nKmMq7sLJJJ4w,!!Ox!"t:;뮻zuș+V(|GXhQzC"6ǖڵ ٬Ar"""#W\X߁sW"ҮK/q%0ff…wFAAj 馄Oc>N"ҁ V^Çl׏sx\ii)G!==JմgՊ?$$$0h BCC{ZmRޝWUO搙! S@@P" C؟SmڪjCZjPA WA 30O LWm IDATg9 d'<<>{>9=k4!0 i LC`@4!0 i`3z6SVV*++U__߅xrwwWhh;"@:A{.Ҟ0\WW (88vQoXleLa{]]JKK!CB@nVZZzQ_z%+%b0?%,0(???, 0~Czoo:TRAAApfhiuСnؑm`bs4U@ԙ 1r\ e\ i皂.}=9 _~_u;m۶ٶ׮]{+ vqH/[wg+Wgվ}Lon ah֭WBBB=zT۷om3F7xcWtA:_ hKpZ߮'jƌ?N % ,mu Os=isbmذA4xV|zݯ{'T^^zJMMmW?ow[tn~[nkƶZ|RSSI&'?bccz׿֫͛7+33S>$iՊW]] ٳgSz5g}7ֵ^[oUzgCo߾2s}%%%N[/u;ޏ?v=x`eeeWCCxKg|G9sl2]VrK) @έ?~ݧΔC)::[@[u{ټy&MAF $-ZH/Z5|+))I}1nݪx=#o7ѣG3(33S_4n8h 6VB:3|H]fbccNF$EEE8` YYY_޽[7oY#]A$諯:ѣGxbOS}}>cW_m.-]cVWW_ 9s_~|Bhܹy;[ZJa(::Z~'h߾}rppЭުF}W5sVJM_ʬ[NrrroO:CNґ#Gl0&MҖ-[$INRJJO.WWWjܹql٢믿^rvv]wݥ,eee~rqqQdd+I9r$OYF~!IVӫZ{_^^*++5tGDD覛njUmm~_+22R+VSO=7|S۷ooQ-m=*++jի***Zill;CYfСCGWgVxxYCjZ6zh-[^xAwq6m$IZtƍg}VӧOג%KZ˳M#GڰajjjӒ%KoY#F̙3YɓZ~~V3uU^^'|RRӅ$͘1C2 C{oBJKK`vtt%}}as>|.\(I UPPm}-ܢ_ܟ}Ymw}zm~饗ֶ={l-^mY摐ٷ=C[~U\\,IaZl|MIMS└ rppА!C4c };vysvg 瓕%www'N>meff*##C? [-3h {Ӱa4otXB;$I<򈼽y#GjΜ9-^۳gO~k46m:+]m ÇU]]A/&&F[lF/B*++oÇKUZZ.&Lk*//OO~]_{'-{zz*,,Laaa mPv^Zl{{{~ Km//˙SS?*00PڱcY嚏B544O:g{G* IRJJآLPPjjjtIMS,;sy;w^x=3z"|HҲe˔1co޽{%I݁䬻QEEE)%%6ʴ~z)**J 2 Czt)۫u#""zar @w-[hg>b}G*((нޫKꩧbԩS5aIõf=zW[cܸqoJEDDN:X= ?xm9ϟw}W5j*++/YV :Tսޫcǎ)..N>i# 4g}Ztӟ~{{{-X@K,Quuz緻h"_^a;$͚5K~m wݢ^hhf̘_] ĉ5qDegg$;Ei޼yuppW__~Y)t/c=ztwwRy{{u?seddW^i.b gs￾f-[LիF6M2''Go222ԫW/}ݶ)ҹŹBa:q=Zi6ez35p_lu߹Ō\|Hk|л_\ ej7|vuT՞)X+.LmQ;IͿ7br'7nZe}]t^z6ѫWNg\555<Kfm*==]R3u9"uԫT,ժZUUU+Ҿmx/Rbbbwwiiꔕʋ~fWsttBCC4v@4!0 i LC`@4!0Cw5]Mnib vG$9;;wKL`@4!0 iAmvԈ#G:r3]u7.F ] X4HO~[W]u^I_|k꥗^$iڵ/4{l7E]gggO$)11Qruu+b;ޙx3cy'I6lІ TQQpq }%I_~֬YzHoY;QN;wW_׮]rYmm٣IRRRRǘ={\\\$5-Rrϻm۶O?ՕW^kÆ ~Z111Zvk {wȐ!?~x~~~&veY233QF*..nOeddDEEiȐ!2d9=Յ>6nܨ`͝;W&LнޫjڵKaaaמ={$I%%%VLL,$99tPav4iuֳڵK9sl!͆t(55U88=q=s+\.??_}mJ3/&&F***QFo۶9qJ:Sک_~;vVXhVU>>ׯ$IѺk%ɶzҴsNkPrrm;vjUtt6oެ,=C׏?K>,i+%I $(((HIIIĉmӯ 0t&`unf566^۵k#Gу>(;;!jj:q<<<!zӕK>&O<}ںu/^,WWё#GT__b$>'TSScڹxtЄ }vƏz[N))) ӼyZqԨQZr|}}Ϻnzzm܆X M"m*eM2]ǎׄ :--oooǷǏ|5"t!0 i LC`@4$jvK$t`0 i LC`@4!0CwwBTVV֮:^^^3fL@GRVV0ENY)|#9:g`R{OnSTE~aiz]?;iD5 LsI\*3p)G/8jf,2IO۠jM6@|,)Z4^nߣS?}9j5$Lܯz oJ:rROĮԏ/{{;mw\-3 D'h͚c9:[`_n r$N>O*Ja~^w(W:<C$Mתkh**{kwQs'[)E!~+W /Xh] Wqei>:2]}皝Zm50WGE+Gٔ~T}z-n>ݶlIӐP9;j<ŮOHϟoBd]FοY#wy.- .< \yyy pӧOoQt=H'ۙH7eJ+7[4G#m垾: -iך*h@: m:]خ }+CzirqrPDJ>Yruv$ 7~+PEM8w]مzu0Pi?̺A-_%E\z ok 0 Y,M Pjjjе Hn$iLD$)]{`v2 $ϣ$Tmvoztںv#L o\b_NI>Y,9[ʚm5~t,DvvG4nP$@vAҵpBEGG+//Oqqqm !m`[PX,PjFNh/ԴBOpr$Y[ae{$?O7p`o3NQOQ&#j`)//,X@ еjl5)ܒ5*a$)#d$o$)p$Z J`%{;UTU/Xյ yQǶXjh*X$) }}\h7 CAAA #}*rss&[@a V]7|ܕ%Sp(K J%IBtU }1YGsUT]u|_xN-W^Ͼݣy+ ThDs%o bX|ud]EGb,XCZ m44>>ow=O]4!0 i LC`@4q讆i-Z 0h$ggni)XLC`@4!0 i L4FE=Q=>k̘1(ܿ-oW/G?.F>nTeU;ѫžVWNa} VTSW/7mj/HlNyrVL͟#^}.@kz|$//bQT$)Bkvޔ>lEI>vz6JޞӪ8Y&9ڷ\ʫk5ףbwI^FIRM]**׶'4yh.o"iߍ m]0$-\Ka5ih?5XJ>u㨨vj*":9*_N+Br4G6X~<\uݰ;y,f*gݤ`_sD}USԯnMmù:[ _Y C;fi`Po-nQ6H_rxΫu$XeUtӤ$Ivve U֩$)B'*x,SE˶QdM|(x$Z?Ӈz[W*=+_LRq;3TV]BWNVВiZrP7\)72I_-7gG}u>ژDuBr:]$I rWni:i~:[eE6Gsm Ej|Uҍ1:|F-K48_rmqwUumt-=62Tc#C%5<4^Fm8F9VgUA9)K7A7D$msT15e@.IHW(X~dt9J9k+p$};t?QZZ*oo1}[@#SmC~h{骨0U7S*XZFGs:-y)]gjm?4j>dn[/veѪ΃Yz~zm8FpΥQw]=\xE?54j柗(hNr1C̝`Z}_nN)[56"TH{:S rwq\/'}1Ek+H& 9J:rR#kv<\5oPufho~[)((HabH Pjjj5yIDATе dҳeW1rsv,zgNeWQl#Q ק;uLg_z{ WVQӭi c#CY$ zs巶s8UߠFw=i}]¼\$5''UШFݸJ+uzk%֨j&{;tXEGG+//Oqqqm !m3|[PX,Pj ju_zVVҪSz}ŶNmA OwkцdWi5\n{Oԫf##uo7S_v.Aף|Y6$}#CtNkD~~$)88!uCEPPӈD{Z< gȐ4~P cSSޫ$^ V 񓿧=)iMDe0$.<ݷ|=/mk1z*vFt2Uj@`o]7|Bz{6- 9ӣڶ );;WȻFjDx*k|:$M8c8id6`3G1wG4 CAAASiiiԑBEnnrss&[@a5=+TґEӍADPo%_ܪP_/*p[WW y)KGZ1u{]vbUӼIMwٔ߸-z6OoOUԪBz{hZLN5^ gt(r:A5z|7zSn/vo?^+BuT%U4,ޘJOJ+J8%G]SYYz_hԀ>Xمɹ:[6DAAwVoܦ2`+#iWչt_#y#tucdggo?F thN9'zh^Vԓ3&kJtN++#+sv]$I-/R];=ޗ{"O4^nߣS?{)\* m]\.I hq'l iz8銰?Yg?M|Ģ:_~>khʤ))׫o#IY,zvV*KU:QPj+k;Y`_YV%PeMuC5BO}Jvv^*MT^IV&WmC~{ǵZvX|\}' 5zu#ݥW(>F 죩##;tεp_`1}[@#Qum$eU]=5kbm-Izb$EiL-Zr@#X~ kݬF\)GsdXtӶ}ǕWZA鲷w?ס"IR\;YmAE+uqUԶ: rA7/XGدxd2Q3)FE8?VTrK*oZ V-ޔ"צ?[$IuF Ok;Ӹp-߶[ڡم! ok 0 Y,MS Pjjjе mQRӅ$Uie~51ZsiQ?B%IbEѼb8Y>~>^t4KFF^<ȱ~mI6K+JX,7ȶms"׶.{;;:YTF9iD`籑ay0S^[C5~P& ÐN1H $.hq$I>n .\h)..M!u,X` **rrrZnNqjSkp+m'fLo%UGs$5043 Cdu 5k}+#BuUW}M:6D^lB}=׫F|SjfOꔃڜ~T݇73Oxx\)[pLA"?id?88!uCEPPӗi -kquF}|?jS$Th\T Way}IO0FU)CIs k  Oj5dRew7X~i\TB|<`[^{N0 Cild];t3+_n^3M>>ow Ð^Z^%U5",PKTVuJc#HeiѺD+& ok{& p 0 `@4!0 i LC`8tWyyy4p v-apmmmw4 @s,!0 i LC`@4q\HZZUS#F@5f̘vIHH~Thߖ۶-:[*9Wl=zxf*g끩W*ngʪkuUTj9۩Vjd+C}e>͞8B'FjGSvDu opG Ð$7QNnxA/}Q[$vMT-ژc%puuh䑲XOt$[3ڴÚ9nVŮOִJWZSw\ J'eVŮOVD+kaQ֔@\A+7;*zf?"^.-ʎ Sd)҂wtӨ(M؈}VNI+Uni:i~:[eE{rvt묒U)`g;\bccϻP~n'jlD>ۑu(Xc#B9nG/-\K_j !5ZvHoZC+hu!9"wWgtTbgjцd-ޜ{ЁB+ք!}5yXU)]z,WO-^9-њ5a,m#Ig,'.x3F{.Z'((HAAA2 CӀ Pjjjе dҳmۣ?:>n$oQzVj -?5#gj(,SPd<\%IWF:Sj;a~^z7jofI>{MrsvҭW>|,zgNeWֿ3 n6$ԏhUll6֙?-X,[iN{z<\wso$;YSu >] Ӝyz϶n-ޜe[0ڸhmg/_lUCUc>D~~ӗuSiiiՙ?rssm/&[@"tPoGU*ixbbV4Yޞ`N*OIfK3#@7]߭#qqt$2G:3 dH?(\cgb4_sԑ m -0"?T O*ng_9 {V(CYn.NqTV$R^Jʗnunj6}"WnP4yIu*KVAy $}|PyJ:pHZWѵТ`.E}dVcnRt$[յuG;fٞ_SFbƽt0HWh clkBԧrMxSR(J}|=tA1)LЮC'u Pŕ5*2TLIDn })Uye2.\f:r1-#zKm3oooǷΥ vCְ֦@U"vwո%zr;>QseECt{]# 2]0 i LC`@4!0 ijvK0nie)XLC`@4!0 i LC`@4!0%@k׮]{OTQQ/K*|*,,<1co@VXaVCwweM2EGi͛h-_\_Z ,e˖H мy;vh۷{GYYYZn4uTM6MO?T%%% ѬYԷo_ RuIXV-ZHwu/Xk?.I_+??_of̘^{M!!!zmurrr4|p˚8q}]W^s}***Rmm.\[ _]v%@VZ޽{k>Ǝ;4x` 6L曵~UWWK#ggg;Vahڴirrr|^׏c=cuZ駟kz衇l٩$eVcoQ\\t뭷\z|Yv*++VRt,0Co[p>޺+}mOUU\]]jjÆ z7kق )Xs?[z뭷4sL 4HoyÇ$)//OVUcǎUJJ#ժǏk2 ]),,ԋ/cǎN2 CULL^x4tP=ϴpBy{{nkwp۷nTEE_Bvv=>bw:4!0 i LC`@4!0 i LC`8tWyyy4Nئr0. Hb @4!0 i LC`@ۯcAֳU6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@6l`# F@MiB?>[IENDB`django-ordered-model-3.7.4/static/pizza-stacked.png000066400000000000000000002657421440510626600223010ustar00rootroot00000000000000PNG  IHDR ?&R pHYsaa?i IDATxwT,,,E(=51FM1|cLԨ FcTT@Pؐ&-e [(Yq}ޙ=<1̸Q?""""""a` W+ "yLm[Y_Ҩk7k2Y7)=ExM[vz'omK]);.fܓZ/bEDEDDؒο7f`6Ɉw1kX]߿?Mq~?,+L>lw2N轟a6OSruj9zy h}f}YDNDD^#NSuK5lg?# ϦQͯ[OkNbMXgp(%rӤR{{{n"-^@/3_3YW,7$OfDz16/խ6ݕ¢`_^%쬎hidSEϬΦe 9NۦS0 8ȏf`T>whJBkW1%H7m6VI ͙{Z 9I ~J# >+Vvf>'f>ܟҺBJn3*cd]IS۹uj9%CCБXɬaf7ryzt}w[*b>LBBdjQb}׏r(i ܡBH^ڐK2{ғkWq:$MqDYCs ypPT˚pόbf~P\#S7@P,kvZHu,;ϯd4GVrih5_/ʺxE񧕹Lm&.PJ6l[m4[X7~8~V)xtU.-8&V򓷇10Gy#8xxy.k̴vY} 6d0ﴚ`95:%;SivZng.6~d(;chvQß?ef&t a_]N Jyn]]㹏G݊O|4V흁{C̀8Q g "Ɉwa1ypPvDthpXxcK:[+☙pL>R^r;NW 哢D;MԷYg<682i=iTDPﰲh{%2{ĉADT""_z^6Eۻ?[0H N}+Z"I 4~ ̒ݎ.ʖHSr[xq}VN6`Q?_ϮFvBG QIVmN _벿>;;S9&=uU>6ial۸뵑4[= glfNS]>dSyxkY-e4:*~`Y9AN,ë㓢c>ᆦ::!)!de.N>|H ke9͜,ޙʯ n3`5xM'tK|x ޟڷL a(dt-_Pŋ3yl- W޹ŇeG(" g nbڐ&n\A²H= }yEdw,vUG͓+Hc(__b#z{;t|(ȗZnOIcϽ :7 'Nw!>CQ9:M$E{ݟ݉g5D[2_3m=1/ ?E 0j@N-gJn3?[8۰c5>Yv('ۏD)y//KGײ8 IigcY|]'J{AΣ-"%392lpى𳽇a }^IjL纶Ia~^S04 {Hr;z G㱯.a=lzl%"_ "u&5u[FH+=Ψx \2۾A!-l(3`qռ;Y199ӫCV_ES Ft~aOe7Lඩݶ>CwLjtaa%3ޅy?.mEc=dk1^4Tȗ|~&Vhv{v)ߝVGwr LR~z~)1m' h㶩v!M?<'>Ĩm}N .lf#=6EdWմ;8LiyԵYC>3*H鱝\1KFמuۛ[ә7&VIǐv~vAU-LeM$FyBsԕ֖c2b&tɅ=쬎](idLA}.669:(H7_S gTU{怈U㪹j\5#5v+J|mn\AjL'5Vֶ4g~NpDʏmSxx|U ;Yf|cb%WHCdlMuUW Iؼ쯏2S#)\)j]I<_gTrǴozP^q ]k87 v|-$.20 qynJa+ϮӋ0#\)㶩e<|n Z˯ %- B{>.JdbN |m&n|qL~a}&\}z?uȎ~p_Wa\=/g?f}iy`0nC^q/ eo{ )!à~nJn3nuZHDDz@D+!%!Õ 7֝=7h]m|i|%\2oN3RO\;љv,&?%9Q[DDNDDDDDDF"""""6 """""6 """""6 """""6 """""6 """""6 """""6 """""6 """""6 """""6 """""6 """""6 """""6 """""6LUggq384M'^OcjA]6cSiszE{Ы{(,ovy|Fqtxnl2r9.]"::̿w< !޼!\81iuuKXs,6'}DQ8;l-jE;xα e{%,@vj$/,+bZ&w^ǭȁvs,Y1FXFLk=/<yϳo'ۯ9|뢳oa 8\|]ym}j~ty!O+f2(- {~n: Vg|WϘYX&c"m|{k%嵼oSr^OlTJJ;:e&  19$E褣My}?V,""r^l_h48,(#8:<ʉeC ,+)V~s;Uj';5G3<77h^_6_6^=œg5(LsofOi)|aN>'-lq8klj0gƐysMǏj^3lb#m6rg-{x}=QX^m[q%s|=fpmΎ9n簫yv!6t)1붇3h\>o=Rp_|t$.1]1T6tt ߾,#C<η.:ZuLHLYkgr#/rw?-I|t$ x}>nl\z`O~B k0 |)< &9]~"vTÓw]ފZJzݷ=rӓp1*'C3Slg߹M?‰#UݿBm_[DP:i,T{uqʈ\vXyuڙZoog Ƀ7\o_^gOFy}SWYևnk(H,SCMQ 0:7[8}h<#ű?cL*w5@Ykx|a.siDXMc| '_sZ27c8ZMfMezlNVfWEky$XCh '=K sưD~޴9]|iM36>c@ d`$[0EEi#)&Oc##h;p=G&;yRg|K!Al/n/XGMlcXOv1x@J>*gKeNL,?mtWZ(۱ƺb6/`oE-}FWDԥSȶVFPZd,HfÞflr^;} s& )~FRu7WWij1Rt`6hX _+aΤ [)#2 H1{Bm *n!@uc.YR9P`ƘTVms{nu)wR⻗fKQKelnMÒ;]ֽWϡxzb[{{v%11?$DG2!K=xf'9ͅ]sK_l wc> q9ldtNaXZldfCj^c2>2 ;7m.P|1YC犰ZR|(۝.Y}d4ri<9v*bہ bIOɎ+wbֽR}o[q%r2(md",cÞRb_)\!5d%' }U70(-}#sb)mѽ/G8B{/205[.md㪓@DN!2 (ټ\<1*4H_YU~\Ü>RAz8N*Ꝝ>$f0xv4Z;)wg "5F]˱=IYK `Flx)ϵӏ^˚]$DG2c{?~[+5(3qéoi >>ފZnzy 3Fqi7n:1M?r眖;z C6<]s@LϦaC[;xrчwy!soFst$Ea1n+|9 <,\0[0{|/gX3 {͠zܷ@ evww.>*u v8~\@Fb#mԷ:aHFC}MLg߫nRG8sd16`:t;zEMCDN![[(Ȏe6rhw2cl*>+D L6wJòbhszp{'e3mP0 ,^W͜IxR:UlH3SF$g_Ma0yX~hF 17(jb:"l+n!})w\uv zh~ϹcY_u~1H!D|Dnt>m/[';q'tcqtn7%9PoTZB,77ᄑ'}7b9m]VlC}kW3>6gS׸W/.榇盏__>0%aӸx9n8'E&kr_^&!:Is0CV|lh!7="'-)d"rnzy׈0!)v_"jb <:V738=S;spb6^AiIT`5>?M=S)@l {56m>_Y)I 43#aV6mx]5w^bd4sh{Cw62rP,ƥaRc,f#xm/>{FQB ώ37yJk2mvbY؃ KiNȵG[z}ϲadT5gi(t{42,+FZڏ>Dd$MY~^p&F'&@TrsÁ+ras,肕!/^;/=kJ;O/P+> i`_=s.l6b"mUG`Nֵ"ن=%;m-^o\t_^hY#:2ʞ߳ͲcxKBJA5Mvڜ.SBՂH7o:7?ϖr^jYGcXxB\81wWn*15c06+ Yima1?<b=ѮmS۞_o?gx~?;KC?Ǐx3d`H~}F|,Z%U}YRMk~O \05dN$Fȷ jUמ] ^ fE! w=!{#7 yUGew5yX&<^/oʺnhmv8ysHvUʪx;Wb5hhu OzÌ_ Ɉˇr[*rLtz|M.µ/ "r(HXLDݭ'GDDDZ4KDh."""_ ."""""a00%!d/2AhSI/HڜnїT"}qSMs  QQQQQQQQQQQQQQ1O2 """""_QQQQQQQQQQ1ɿ?k [XYX}V@Fg&[fG=u{xg{iktFg%qݤkGMvNcq1 K'+1DŞf]cf58dvY sNh)yX ~ү䋬?1e;)i5fgbU-X=e6#xT49Qш?oBbUMt bR^:kW  d",&6~nSi]]:_p DDDQ9A^:3vtl;մkEh `OAB̄h>SIkGf61"#OWN߉r[>?Eu] g_q&U{*5jO%gg`5XZ{*zchpxkXLF朖Øɸ<^6ֱ|W>ouHrcKYK2ypz5Zdջд8OÛ0wl.Sһ9c4q!xL`j^:16 U-\9Y3 >vW0|t)m3%oV *Za DzV7b@"1 jqǨ$@}^)TsގRœ%%6e;|Ƒlb{E py]Dmk~Hvy4eÖzl Mh0P`{E]=/58fl/o9mOJv&GG0z`2QV3 mtXLF>+%692DDDĝj6qJ>/V3x7 \1>QIE[K9b W˶pҊ{e|pFo5f| z"n6Jʌak^.f-'z= ykwEYʹp]!ǰY8#7핍Ե:IA)4(iScog戁WE&Ff&a6;+)nдr34-IYUx7>iV7SNz\sRhod&DKAFp@ V3JfI1 MK೒Z;=%0>7Uf'c6Y[T&g6Fmk;K눍2:+ +("""}@fd1cDf6@=0y$,f#o*QkwYònmgJ^խN5J> DiMg*et NG~Z{j1c_JN vxu~&#^'n?Rahn蓰60t缂XMF\ |o8;8'b_m O|uX9D,8\v=fu77 H}x`(i3|@" 6:?C0|49:(?4(/5}TZV̂d%DSc]&۴F;S⨳;_WTBb8x~Ү{ katU4줡H|qs=PJvR&V @JL$ ާ5- e$D7iqvTDDD=) .S EeP#%crMSlh03M`b O btV~qQS~lDMk; ! XZ;:OhZ{&(G ]N KIy/eN\& Fei5eCq-Sِ;Z#Juv'uv'}FtGYz]5a*qDpϮf*}=խ$Dو;W!;)v.߇pX=)&N7U- 6{AGkn{}~mmFHkevEMb-d[R-8dHN;˃` .}>:\",!pÑ|9dky#^O#-6Df}'&XRb"!9-[HI1l+o8u.O~Z<%-6(b;81 HQn@|\Ogtl/%b"6Bl"=fȌD3,=+`2O49\T8ƀ(mL"3!M@ ?;:Glh%pYCbҰ6Ϗ'-6HKga%>k/I"fᴁP|_ZI$/5h8b#9PutEJL$NNwrdmD|oU NL\d^f&DȗC:?W2wl.wa՞J>[u|}~.ˍSc kY[9GJ-NVV2cx&q'z63,=QIXLAv;a[Ec&s:;nqk <ͯ;WLAF">`@png] CLj̄h Iu;YXLF;XST2X;:XRLj H͆ZO-vWB̄T:^ն04-N[ʔ;Զ::2>?rd@C[mN F҉9#7 ! RC{+{?9/V]U=kS~ȉpvzXVhbnC≲IdpJ́QfsgMLnrёY""""CDN磴̃UMd&@Kq=8󱻪bl_jXzr֎N񃝇;DDDGCDDDDD$l@DDDDD$l@DDDDD$l@DDDDD$l@DDDDD$l@DDDDD$l@DDDDD$l@DDDDD$l@DDDDD$l@DDDDD$l ~ߕH(H(H(H(H(H(H(H(H(H(H(H(H(H(H(H(H(H(H(H(H(H(Hؘ.UVf1 43g2vؐ_6lgچyWCF9r$s!%%%oڵ,Z_ʖ""""rc=Fdd$_n{n{9=\͛7yyy}^ZV\~;>3TSV_~x`9&& x衇=z4C @TTTȶX y嗹$33?)""""rLQyMM 7n}NN~:q?wo>GWEDDDDkȖ-[:t(0a۶m붽7x}AAAW_}uނM6|r*++dffr3jԨrOJuu5K,DNʅ^- mڴŋSUUELL &M/? -++f߾}|> ¼y9淋h6nțoɃ>,7|sR]]u]GRRżl6f͚,i&ϟUW]Ÿqd͚5<_XXȳ>ܹsx<]Gy{sf"##{?c 9nP\\L^^~;>/&???m׮]o.k!""Je]v"""""_ nw8[n;d̘1ő7͛ikk ۿ?r cǎ%::N?t.R/_u]ǘ1cKC~^}U*9HIIK.N 9ncԩőE]ĬYX`q_֬YCZZ =7X,˚5k[1LLn.]/o׮]\ve̘1"""[naŊu=""""ׯa0}ӦM6l+==F}}}p~''''S]]m9s. 9fII vSv{ɓC^o߾0i$ك*{gYh˖-+<9}YJ{-o>/_έފb n˙1cFIII\.BDDDD+_`zjv͆ VXXȪU8pNqROǵltvv0TWW0rd*..{キz^{뮻(((GzqXVϟϼy8p`uuu{zDDDDDү$66W\ŋ;w.^{-qqq v?Bt\DDDSٶ6&Mܹs{-1 wkDTTf}t| /LjjjȜ.Geu]$''c2pu]Uj W۴i۷on8c;N^u1lذS6-dI_s1LxWַgoˆ ضm<@C^}U;<.U9 cǎe߾}455Zfƍ'4L Su뒑Acc1#;;B< |̟?S2z^555K/q:a|kIOOg=r6nȴiӎ؉x<B\.,Yr+6 4q֭!ǎdbҥʶcǎxwp:\q<쳌7ےLJKLLz+x/]{444p8>G / ^HLLd̘1<֭[@bbb`0pW[oGaihhweʕ!eV+7|3K.7ߤvGNV:Ҳe˨kxӧOglڴfxꩧhjjh4=~?яxyihhbÍ7Ș1cNطz+ ,੧l.rssy'hnnuQ_Ǝ˭… yWfvmW ?1j(~xb~vdΜ9L8XZN~~X&Xkeƌ̚5  hll$%%3<;'|۪X"""""G25nٿ?_K"""""O@ii)ݶoذÇ+|B-yf?{nn7,ZիWswDDDDDNYu֯_ϊ+(//h4ϼy憎@DDDDD$l},PWWǾ}(..nֆjBLL 2tPRSSZ=R|TTTj**UddeewUB(WeٲelٲVcǎd2wU _z&;;//7o eeex|w5]YY~WCDDDD?{U=) ҫ4`A`YWw?]U (!@h iBzdGv ! u]^2{939suWa4HII-k˖-MD ܒ233 IIIdffC""""rK:zm?p@.2dgϞe׮]<̞=R֭[ dԨQxz IDATzRRRh,[ 6mЭ[7\\\8{,6l ++GTT@y7RLƍ9~8w}|kZ7WT?׋%CƍǞ={lnv rrrhժw}7d2sN1 2nݺe~gziM@zzE~jʨ ܒrrrnhnnnPZZ͛9{ek׮۷oƷ~ lرcpA M6%((z{+=77^"""""+ާk׮mIO?ĨQСC$&&Z[0ϹsleffWё(RSSmۧOrssٶmWp宦n% """""믿Vr)̛7ƍӺukҥKξfזDˋcǎc'NХK|}}ٷo ܒnhZd2+8z(;v~.\nݺ6С<7o7oյ/8qSNUJ\jFMu-熖ߥKn6 ^v{ҭ[7k|-[ݝӧOWv߾}~]c] u[RDDNaXңGf3O&&&%&&}݇ ΝcٲeU1OۗE]u"""K9W`^g!""""rdMiʔ); %""""6lXat6lXk([X>}j;N߾}kJ@DDDDJvj;Fv -mEy׫vJ@DDDDѣIHÆ =z4Jς'|ƍ 00O?8'N[d sՕM6=  %% o |͛#<<ɓ'ӯ_?/_άYlb9r$+WY֪U+>FIrr2SL'//_|IMM% #F0yK&.XÇdrr322:t(_Y`]tgϞL>VZ1p@>s֭/"c8::2h z)m۶`9ݺu''|ҥK),,[nwCOh>>m۲`^!""""r+cǎ@=lٓ9sP\\ÇQ_ػw/r}Y}]hݺ5}ӥKbcc)--qvm6eZ~=ӒAfɒ%G}Dff&?UǕ;g~AwAߏ %%%lٲxpvvM2rHvSO=dYf9sUVG}ʕ+yw򬺪.j5FDDDDĢƀ;w uWQۇ/fҥ~f֯_OϞ=ms<{xzz0ydٔo{Gzz:_|/ o&˗/'11׮]KmKMMr˂ 6mu/f۶m 8B.]/@LL Pjp]wdΝ @ll,iii,\~O?wlKMif͚Mtt4#++0 ո޽;5"&&ٌ/?``Ֆe2())l6_qO>$m۶%44Ժח-[b0hժP>Zdd$l-?Att4ӧO`zz:999BznWmDDDDD*.Xox322lYUlq[ne޼y9rt} ++ (`0X׭['O^yk*b˸^y뭷ظq#6ۘfKhdjI&֮O[nYm6|LATT ZDƑ#G2e ]:u%"gϞY[_+VаaC^u ,&ۈTTk #FgQڲe ر:zȐ!xxx`J?w^ dʔ)ѣR+e' .TZ^qYU:t` 335kөSԩ.:5tΏh"WHE; 󟉍%!!gyf]xxu:uТE >3<+EEE6۷hӹ;1BBBpww3b^z%7nLff&?ꫯX|y6hЀѣG|r^~eXg8puŕIWUlْ[2k,/^lɓyWXn7oh4Vj6""""":ӓӠAflkFpss#((lU-[W_%<?C=Daa!o&6"""""7]PPP30` p,HL@@.\h5FDDDDL@P\\'|ڵkIOO' >}裏ZM6"""""J@DDDDDnn_B[% """""b7J@DDDDDn(Q""""""vDDDDDDF ؍% """""b7J@DDDDDn(Q""""""vDDDDDDF ؍@VQRRBrr2yyyGD~Av8""םl6k;߻:DzٹC)IKK#**JIr\ǏӓEDnӨQED //CDn!v""םh4ە\WO&"$% """""b7J@DDDDDn(Q""""""vDDDDDDF ؍Smp9 < ҦM իvDԁ;w.ӓ3uT\\\j;7nǏ|̙ 4"fΜ_k;[MT v"W-!!G}Ç#py{=N:ſrq1rHƏ 0`K,YfwcƌYtʯM]%"""'$>>͛7ӿE̝;nݺsY5mڔcM.]d{cQkǿnADDusկsRZZZ6_~%G{;[uޝ` @߾}Yx1?wu{_m22d{fԩ.'F{SVVvO^n DGG3j(oߞ~ (o_bMNʻΜ9ӷo_yrrrHLLSN?~9s0~x.]ʀ?~$3f̠W^ ,OΡCdfftReܹ̘1 L&k֬&-o6ɉf͚k.q )))̙3$''SRRb:߷hذau 4 99F$$$`Μ9oMdd$[n%22'|?ѣGzj| ՓO>i-'>>YfqSPPg|}}Yx1OY|gM*..&;;?:sNZj… 4hf'?<.|G>}z1c70a^z%RRRc,X///8`=~͛1csaW|"""ru~W]N͛?~nXaaa˴iӆ0?>111DEEY/G!66iӦq=d̙Ç0`˖-LJwywww>VZΝ;۷/+Vݮu _x{{WZӧkTĉS 6˗'''\]]߮Q777Iih۶-VӧBFHKK?4⣏>b W_eӦM-^x'߾ڵkرc}<t 7ǢEhѢcƌwb |A[˗3i$ /СCkT"""rm~W HHH{//=$**[Gd //f;???:@hh뼼Mqq1ŕn,OXob?D ''zYQryپ};YYYx8;;[@TT3{l mܸq6OOeQ16Naa!Pĝ;wɓ's)JKKϷ&Wtͦ|GGGJJJȠe˖։ȍJ@&Mķ~kmXh .W^+lڴ߻:f28 yu@RRaaa6뒓k|mkP^=/_Χ~zMT $%%%U& ~{9^yz쉋nOv |OSqqq@S:v ɉsOϞ=ر#9994h Ν;w'"8^^^tԉ+W,OJJbϞ=']aȑԫW*`a0}]Fq!FuY\\ԩSgwcʺXׯOqqqxaaa>>̘1 ^zT'**>#Gc1k,RRR;ٓQF+׿p.P/2EEEL:իWcEqq1?{_?/'N?fɒ%Uܹs`„ L8zu)ݻwgҤI<4oޜ^zY|nݚgyE1uTJJJ &@&L_~L>g}v5k-kc0_\Djlt]x1O=wu쏫:h4ү_?fΜIϞ=)--њpݻɓ'uO b""r{ZYi`\8___>7r-ĉL4"fϞM^|؁e\mڴO>ֱ!QQQ[cر[L<GGGwO?]Q1 un"r#EDnEA"""""DDDDDDF ؍% """""b7J@DgggFcm!"шsm!"r)8|m!"Ua\wJ@D FzzZBDFIKKAu"NJJJHNN&//OI\5ggghР...uDDDDDDF]DDDDDn(Q""""""vDDDDDDF ؍% """""b7J@DDDDDnjuh?Z9l6k""""".X"""""b7J@DDDDDn(Q""""""vDDDDDDF ؍% """""b7J@DDDDDn(Q""""""vDDDDDDF ؍% """""b7J@DDDDDn(Q""""""vDDDDDDF ؍% """""b7J@DDDDDn(Q""""""vT[~_k"""""x3GuDDDDDDF ؍% """""b7J@DDDDDn(Q""""""vDDDDDDF ؍SmP?_RZFRV>ş$=vӝ $Mh Rv|~1O hSi]rV>molJ(rωK5θ8V32 :P_OrJؚJ Ed]LfP2RjMH.ৄk]w_Z1M8ş.޸w43p dZw W|\-d$ͩtukNz6Q p݉unΎLމWEQ ߢ>&rt(Y}.W/ G8o]X2hÐVa_\Sg|8bsĞ>k-e0}αQ]=' @`]>s]m#h\2qI?p2Sy롰#X4yF]*(f樮~/E͐VaxWld۱4'UNDDD^n_BA1.N$5_0_ݗ1Ǭ7d]XRV,/cO6%<008BFN!͂pxk>.۔hǑμ~VyĞ>׻qrpE=_܄%X:.r10 +m".G6JĹ\e&tW"C:$eq&+c)Y8UEMLO9OnQ A>@:]s(O,۝HE ՕJ. Ӎ^neAzZ[ՙq'8_LZvKw%!.nΎ.,SKMiM1{DjvL """>pvtFoהK>=I8[i]C?/:G.VeL]Xݫ''v[:y._~ENZvp0K&OAʳYv$I˗N"=ߛ$YHl=+Ǫy["-}g_SL:Zu dsT '2sݭ +*%Υcg 3r .,aLFd$.;_S""""()3z!Owau v裕9980c#68]KQH|=\8kCZ5ʼn+ى+()`ё2vl؎(39_ʸdTt+*)c;ShAxunΎ75wP'])mQϱ6Jy%:եe Ҧ?%,~:Xa`_&vo`eaRIu*IwD`2P.Q!~ܜ JUXZ_W1Q;jKJtCywK ]v?O@,mk͙aYQJz?>yEFOdTZ>.S/Pj2SXBʅ4Eex^ J~izqMʤcX ;d6SRfՙ6HbÁ$@uB9##xma_߃,,,+ړTOk [jZ)6Z^FB^W.몶@zN!م4 YEd[H:.pqxsvs[r5c@  eTF|[DDDD VMOH32Tϓ,=^`6 h02sTWha%gYgMJv>ooNr{`mʣEqUԯE}Vťer,չ/Hz6[դKtmLPX%_ȧqjYR])<Ĝ nf)3VJ~JHoPBpqt ǝȺ>I|ʧurp ߛ:Վ;MF8wMI=NˢoP<\qurd@Trnc' .섏 PduUٛI:4]eBbFFFÅ owؘ=ٴpXz2C#yb4eΎyҼ/YDDD`Tǰ@|=\1¶ƃ?C;#;l楕ѴkXl 9Eut(bPP3M8CZSdg؛dOJHE-8 &[6`|X-%=1 4݅ 32%e]ۓ:Ojv]"hjq3 nِz߆#'2sYu*"#~>İ j@lJTWCY͔T1&d6#.߆RIX6Q),)%!#y?1 om&ZOjknHՀ{4IrL""""`6ۣSPeCDDDDD⇛rt% """""b7J@DDDDDn(Q""""""vDDDDDDF ؍l6Ɓk"""""qk-)..ÊZ+U,% """""b7J@DDDDDn(Q""""""vDDD~ ܬvFDD8cƌqಲ+l6osNUXt)}:tǏc̙ѬY3fɓL>LڴiS>,[lť uMeLvŜ9s ^zìYXl`0J ϧ~J׮]nٲe sۛj*}]ׯ_ǬRSSy(,,yW}5ٳiڴ)uԹv˗/gݴiӦV}ѣ>}]p~J899qk娗믿n}]veРA |||pttK<6m`78v=SNqAZlIxx]G/tRk'''BBB0`۷ ooo<==o`2\ׯ_?FyC憷 Ǐyr$|gL2I <<<={F=>f͚ѰaCJJJ8pwߟѣG0h ~zzMqGNN:u}vu5T E:uh߾=eee>|kRZZÙ6mؼy3pÇ3|zN:źul6U '22bOpwwE7d2Gtt4k&MܰjՊVZݰ}}}ׯIII=zƍ[+*fcbblZ^zϞ=Ѯ]*m۶O*?SlZׯk׮gϞU'"RZWD~~>/L&իǘ1cuY>C^zKnsΟ?O֭K7MZXegg޽Ç믿… 7$$'2k֬JN4v˺ṳnݺu]nyx'y#::R7nѣ[.VbӦMtЁ#GL~ӧF~P\]]III!<< &Ǽyطo/"̟?=zpRRRhԨ?0nnn={%KLdd$yyyL.]qqqm۶5 gggymݺTkK747o΃>`駟aÆNj/ȉ'Xr%ҫW/zM~~>?<5/ PߟӧS\\ҥKٿ?oRqؼ?g̙3ylݺȑ#9y$111xxxpw[ם;wi& ᮻƲzj*ugٲe8p777:u+V;v?#FЫW*-[2d6mŋۛ7|:0qDkϢgϞ[߻f͚k׮eΝθq殻ʿ q9t>QQQ;w|ڶmwߍl޲e +V`ܸq\um4Y|9{ƍ3vXe=J˖-8p`:Zv-۶mŅcҺukgK.߿ロv]3l2~gMFٰak֬s8po\:uiܸ1?xqXxzzǸqpvv&;;˳nwqXd ԯ_Bzؽ{7ׯ'++PƎ{.Weee.]O޽IMMe֬Yd2n:v؁hYf7oooF#}F/fbĉt&h6mjM&۷/,[tЁիWcM`EDٙӧp{={k17 M=d2qq*TuЁaÆ1l0kغuk˻w5̙3,ZGGGFM^^16es=<;Wfԯ_Ν;[oΌFuLN\\\Xb'NKKKYft֍K]'88CsNf3 , 11֭[Nrru%KpFA۶m_ظqM-ZK.t҅oݺȑ#DFFD||3Μ9C׮]p,Y@^p|dffxb ܹ3O9󉍍}yf~J \]]kԗh4ZSu=h Ɛ!Cpvv`0кuk"""u-hܸ1lݺ 6ɠA8z(-l,dzADD;vMؾ};;vdذa:t-N:Eǎή_\\aaa3^l,X ҥ IIIUs.6mP\\L||=ԉ ˗/ɓY~=6l 88Ν;o>w?AVؽ{7˗/wiĈDDDؔYRRBrrVZ1tPׯ_ͱcǪ=o{ۈ 66(ߚ={6fb޼yf3+W_g֬Yٷo+W[g͚̙3YzrncǎOZ_;;;WEҍСC]Hd2oc0񡴴~PHJJbǎ k׎ Ν;cxppp-[!8p mڴϏo>4h 6 >LIIIuгgOFcx --x8}4YYY@,0xyyqwҥKJ  #::0íٓÇsQΝKZZuy91zhC~~>/"۷olXݙ6mΔsJ~8.6e<<<8~8<#xzzZ_Xo&NHddbȐ!Ӈm[o-14iW^aݕ7nܘ^{s_n֭wޕ߾}ɚhdք#,,llق'=H&MDf`Ν9sɄCMбcGK׮]y爏O>-_nݚFU{>Uyptt$%%cǎٴ(;֚ᇗW%>>RF_86lHBB111tؑ"##mccc/֭xzzZ?< l߾qѨQ#|||Xbu{oȑk׎&M0o<ߛ۷oŅɓ'[[xKAA2i$k2oimӴj 2/\l˫s7ϟ}DDnP1L|<f~G';wh4/ϛoK/h9 n3<#.GMܔ /:t???ڴiO駟ĉmn֮]۷/-[obbbX~=֛e4),,$,,z K˹s*moiɹ9TE0xyye}|Y[8UcIj,V,\Ə;ѬY5k0a„&,l)(( ZoD, ]sdeeQVVe9uX6QL3ffǢE1 ͵I+D_cǎѫW/9+V &&￟ƍ^ ,ZF]Pܻwo&LC/Ϲ3_3nwUp5׵%Y~=}δoަ鼺Ϟ3LILL{%44(MWZ'=aIf.Z/>;V{ ,XСC㎮ŋͥI&(((ooo6l؀#lذݻ[oN>Mjj&7Տ_ёD~5Oǐ!CIQUPg҂vZl\iӆ+Wn:rrrq9f5jDbb";v=ˎ;Xt)6ݫvņ ԩYYY;vgyrI秪zw}իiڴ)'Ol6ӥK~G>c<==پ};v{v#]v- 燫u2L\p`v˩S?~|>1X;~>lLE&!:TJt]~qz>=N뺯z]UP 4y㣺ҥ PXXAaСBl p)( bҤIFCWuN P( IDATHH@YY۷ܹs}AJJS da=7a"22pppB=͛P>}FFFybmUC\]]N&LZxz8sLɓq H RX #p D=z֭[ >B\""UTT`͚55.\(X;w.믿3fP&0x`?B7R)pQݻ~~~xWi&TVVo65R60PB5gm S kmm]Z }}}r,[ jplɔJ%߿/\oذIIIX|yxbF5K/$ Z]NNp^Ppww[oiݻw_5Tbb";,^AرiiiE{+ngϞLxx8L%j:'i5 ϣ`hC--ҥK8|0kRRtkuUUl #5ɹm˗qi 4'''xyya޽Xxq#b .. ,` "Ҡ AVV ɾstt= 777̟?_" kL&Ð!CH$曭PMHOOKlJo&z&EEE;wn y&XDDDDDuv0IC: GK&X DDDDD!M6""""""0h@H4 DDDDD$= Qs46 =DBDDDDDa!"""""0h@H4 DDDDD$""""" Dњ.CE ""`!j"mY Z&"jQ i# JCDDDMZZnǎM["""jr D"=kMLDBDDDDDa!"""""0h@H4 DDDDD$"""""M /F9|pcǎEaaaoږ)S`РA(**uߪy#//0|p.x)̿e&Y_l1bsšd}DDD-]=>CmFxRr ÃP^^7m8q&NzJJ 5T*zG٨*~Ϫ #!k 332;[wWDDD-Z -xb={ȑ#E+ݻQ^^333ѶIm5BBBjt*YM RiE-@"T P*zxZ4]4""V:rv؁l`pL'OǏŋ!H0h 'O.\1cN{!44xw0|pd29s:uG}///DZqF`ȑx"d̛7#GҥK5vHٸq#JKK6p@ܹs_}Ѯ];? ,D"ƍ#""000}r Xtijwލ#//=z~''>~uN6$ }@Y ^W!HuE>Q[&nuaA^pm,YQQQjnܸooobժUȨsHJJ%޽kRSSb deeOVÝB4Q...j[tt4 6ʕ+q5lذ6m͛7sk ~F]ͭi 077GPP֭[ k0`BOOݻflݺXn588۶mÊ+p>sЮ+d>$R=(PUZn86z}ð\D^%|cOMm"kXjJ%vܩ6ߊ+'`ʕ/m6|BP`޽{c߾}allLСCP(2e oߎ5i6S 'cǎ ;v C*!322p)XpvvƪU###0779RSSq ^N,YVVV'm7@!Jwy=ɓcxzz~~~{3.e$ji(|((^%j疦5.8 RDrr2d2<==T)ׯͫpvvdff;333HRHRHKhP5Ɋ}vZ5\㫯B\\zpR/11:::ի𚓓 ,4*..ի1k,Wm[MJʕ+""IuT_,P/fz}$ @&=:mQ*022Ieee^^^j:::7JR/)~6M.Xr%lٲ'O>Q Cйsgܹseo>|prr;w0iҤ'nD eḭIt C01l ϴn""֠7H$GEE/^888ͫznAdd$K.ݺu?j}۷QZZuSѣk6eggr$%% %&& } q YFkZZZrrr0m4U[VVVg9 `nn{k׮OjJj oooRX 92H2gDDDFj@f͚?OG<{l֬Y">k6]v˗__F_Ǿ}k.\v P(H,X^^^S5h HR\t K,1 #FիB"`ݺu n:L>۷JKKfaaOxWΝ+̉3gbͰ@߾}#44KصkWaȐ!ƍ}GZU%2rE?!շMD Æ òeXX[[F3f Ν;---̚5ƳDF`XXX`ŰltYlll駟bƍHJJѣ$4255ОZZZ9r$\RvNeꫯ;@*bذaBXp m6l۶MXfsڜ9svZlذG7O^oSQXX={b͚5y\9s 33|,,,[oa۶m}EQ*x <NjVS]  #"D0цexxxԘ>h >HCKK ++nQ]EWPP4zÇ#!!(c:O"""KiI{BKDIADDԌZ}'sXp!$ Ο?,Z3ftш4QujĪ??&]'Q[""""" DBDDDDDa!"""""<`QcM (ׯt1Q"Q3b!j"њ.Q'Q*JMDBDDDDDa!"""""0h@H4 DDDDD$""""" D gggkjDDDDD= lWT*2=wDBDDDDDa!"""""0h@H4 DDDDD$""""" DBDDDDDa!"""""0h@H4 DDDDD$""""" DBDDDDDa!"""""0h@H4"^(--uڵk1|\2"""""j -M*++333q] ?Py;=;;OXi!"""jn&t5HDD:ukdDCu ߹sgtJT*qʕ&]9VZJӅx?nnn /RBRRF*++޽ mmmuzzzDǎ˗/W\#esss{ VTT@WWWV*Ƽyio""""T*a``}"""yyyC^,̧ fŎRn݊#++ |vvv5kڲzzzͳSf4bY/ZADDDҴ&X*/p9xyy #XZZB. 6ꚍGУGH$:tz:[ZZ"++ ۷9Ѿ}{DDDDDmX ΐH$HHHkLС<<æMзo_xxxbbbp9""""6NT CDDDDDZ] ^ DDDDD$""""" DBDDDDDa!"""""0h@H4 DDDDD$""""" DBDDDDDa!"""""0h@H4 DDDDD$-Mm8;;[S&""""zYXXhd!"""""HJR~&6KDDDDDtuu5]րh@H4 DDDDD$""""" DBDD^YY4|iiH!"VZJj2J9"""/6C4cؽ{7}}}ѷe[UUUprr}-Xr%зoz]t)N>Ç?6srr駟zzu$QQQo`nnΝ;k ׯGPPFD2h7oƮ]0`z[N>oFFF֭[M6ںtݻkע =zx}h|޽;ڷo_|Dtt4Z8ܺu wiOAAƞ2ZZZٮFZl|gR0`F Dcccd2Qs /ܬ100upu666lyvY߿_]KK x---аYxb(Z 6 &LhȨr_uN7o^l=~~,\P-|_t w̝;%҂1 i=addTczee%~899k׮8rQQQkkkkȑ#_",, cƌyr󣨨o2QQQTEUUUqBCCQYY ___Vp@|}}۬x\ZZ;CCFR;Ovvvx._;v@__={˛mÆ B@qq1.]333mpvvn`ذaw֭[pppN:5j}-܎T ,ͳԩSױ|2j ΝC~~>{=@PP.^gggٳvZaذa w#"M}QRRe˖CP(ܹ3^zfݿ[l\<x\\\ͭEN:aĉBXҥK> ۶m˗ՖĬY~={6cǎ!//;vīZB飏>BII `ѢEXv- qQ\t pppĉѱcGĉׯn޼ mmm 6 /***dXYYAWWi`jjW^ˑ[b@ff&[oAOO޽{;;;#33׿#%%:::puukmmmaΜ98{ #TУG̜9K,A׮]b,_oÇ`Ȑ!())G}{{{,^@x333\=q5SN歫:::j_ +8s  <DFF'O׈8qī*\ȑ#(//AAA__UIIIزe ƍZ޽{cݻ#00111022B@@Yf{*ް޻ǭ_ EDD`cc)S*/K.¢FG]'!!7oF^bP*קOơC0e?y]QQʕ+&M:qiܺu {ƈ#jP;w:::4i\\\Ϭ]ɓ'ͭxPP=z3f_Cϟ.\Xo9T qeL2(,,Dbb"ڵkba>{@u{"11(++S; C~~>0iҤ'6;~5kVK.{022RիWann~~:T[ԩS ]]]̘18ramm OOO⬢BX.// tttp!ܾ}[''' 8( RO?d@__2{͛71n8ٳ?۳gOxyy /n޼ ;;;#../^ݽ{prrBQQ6oތ,x{{C__ą `DFFݻwGBBBy+*++q wFFF pݻ>>>((({!//(--'@ IDAT["&&Axx8NqΟ?; BJKK k֭LMMMaa!~' wޭ[}۷/=z8JRɓ>{+`kkN:sBPyyyQ^^^5䘨K"HMM0?~իWѣ8ulmmh}`kk[_5֯_wRÇga|2bbbWkׯ_uȑ#ד$,ZH][[&Jf$  ƌTQFAP`Æ H$>}:QYYz VVV077ݻwqܻwOb pk!J!Jqi?d#F@߾}ajj[իҥ ڙcq Z7ƍ$lذ999Bff&zꅙ3gܹ|գd2k j+077Gnp%t 666ŭ[qFdee u}8q"^z%`8ۨ@__ֆ\.GDDD}5w\ %%7o rpq6k,X[[[nEdd$P(0zhKpuuſo51ptt^5;88`ڵcp1wSSS 2F:t(VQQ_~YݺuCaa!N> CCCoD"fϞ ''' ""P(JP޽;Cŀ"../?U_pqq}SdDRRZ¤I e˖'~kJ7#F@~~>>Fk׮HLLDdd$<<< ;;;j:?{D\\ ?xL{{{СC4{o„ pss#~G ߛ*ϟ̙#\rtLLL0{l!̫j[pI T*Ѯ]V*矑ӧ022ƒo""XYY!;; vѡCԩSjO"""PQQ˗QQQL09998s }d2lڴ n-2_~d055E߾}all\EEEصk0k,P`Сݻ7ꋾHPyҨ\(++Cn݄mj]rssk̯h(**s333k\|ڢhVUU z|D۬a߾}رc$ 1cƌz6&[kzzz~annP__yyy %%%ҥVR-j:^߫PXX˫ڗǛVK,^<tttЩS'A[[sƍpqT ܹ%%%xabb: ;wGAII0$iUUH}_V9r$pq;v :ujTvk;Trrxyyy&222o ''GϞ666Bk;7mڤV.zLTTB^զݿ%%%ɓ,TAM4U#** #Gj}Utuu>^SݰSIHH7$;vl۷ҥKզ?Ǝ[cT6}uqqom"-24[nYYYʂ6 @(JXYY Sy}\rEP7CCCСC1b?=,,,p9t ݻw##55U~UX_ff&v \wjğֈǞ={PUUǏr;vDϞ=q :u :t@\\<<|ǎCQQQ} DTCGrr2<<<ߗ .`011Qk^/"** ǏG$O}LT{RSSOm <Gő#Gн{wBTb…©S}vӧ "4# /j8AAAIaooBى4!&&iii󃖖r9]ggg$&&Rm,'''={H$?0j(d2} %%婆BrrrfC?{&L@ii)"##aaa!c޼y ӧadd'V5{U4?a888u3d8>>w}.\)SpIcXx1$  CCCx{{ɓ000… 1fԩSq="55L3gΠSN裏8~86n܈9/^Dzz:yaȑXtƎ5?\YFu+++jA.]w߅7`ƍEphkk/@XX 1{l|_2|8q0l0|000\.ǠAl28pwA߾}vZ/k硣SoYΝ;ꫯvaX`$Iig(IB~PVBU!R]sQODDԖ[aaaXz5rrrЫW/ܾ}K,ATT|'O={ $$D7nXj222fxx8`iiwbڵTXYYYӧ.,B4Q&77ܹsi3g΄ƍ?W^y?3|}} 99Yڵk0`]v!<<V7|ϫʕv6l؀M6͛9;v,XP"88ݺuèQӠ( AAAXn֨XW;|HzPV$ 5plbaOz?Jjoq"""j-D |j*(JܹSm+VO>ʕ+jӷmۆO?~~~P(jz}1233QTTCAP`ʔ)ؾ};߯v Ƿ~TO-TEEа݋b̙̙31x`WWW޽;R)<ٳgcݻ7-[&̛SNaŊpvv3VZ]-T/Ypssѣq ann` u )dXYYkkDPo ݚ!rKPVPQBYJ(^-Mk] ""jqZ}Q*HNNL&'`ׯͫpvvdff;333HRhii ㋋V}ѠjUۨY000h^Rk(**>;v'|" )T*>V\1c`Ȑ!8s ryITCQUG8@j & K U{3QK&hkkJ]UUVVֻ'Mm|Сwww`޽e˖ 癶v󥪪 uT DRggtZ?R)eḭIt C01l ϴn""֠׀H$ۣ/DFFtҨ ѭ[7~?(--mudT*!Jakk [[[988իj^rݻ~Ν;=P^^$Dѱa}6lT6 ]c˚L6Mmk-KCY-!5O2gDDD̚5  ,+gVo͚5?>֭[xԦaܹߠ__F_T*Ů]ocԩj{ɓ7z#33˗/G||M艞&XDM&Xm`Qsb DBDDDDDa!"""""0h"jqvء"Q3a!e.5#"jQ4]"""jF DM$::ZE """j$t""""" G""""""0h@H4 DDDDD$""""" DBDDDDDa!"""""hijٚ4sB#ە(JFLDDDDD6""""""0h@H4 DDDDD$""""" DBDDDDDa!"""""0h@H4 DDDDD$""""" DBDDDDDa!"""""0h@H4 DDDDD$"""""M W?JKKkvZ̟?"CKhJDEEGL62 IDATܽ{WC"?;}cê?" "&喹RT:gyme9d1^3LMei/Ms\MD EDT@@TPu#rP{=˫w|>9Yδi /((c9VilLҲeK jՊ3g4QDjֆ6o֭[Ӻuk, & 9""""7x3q=?#$%%ѧO܀ԢE%33ÇDyy9˖-clܸBt邃/f׮]ט5kiӆg2|,YB||<_paš^O?%<<Drr2mڴYf}|tt4?#.]22339""""힘MHHƶ#GPVVV' X`Ǐgƌ̜9'OW_5~JN8AYYTVV{kȑ#Y`DDDУGX5`; ӦM#??ӧH9"""" @ӧ{5ݻH=ٳ۷ɓ'Lpp0< III={Ν;O?ڵ+`ggwtޝD!Fcbкuz=Pp q+爈4{fHUU> ?ʢm۶sQUUew) 2ihٲ%:tqٳgys+++qvv]Au*nuƴiӮ;iꟄwպ3b͍={OQQ...tڕcǎ9::bgWcbJtt4̙3n̔)Sj86o=""""w{fU>}HJJb׮]+XSQQaۀrܹ3&UVѵkWcR?4o___|}}i޼9+} @zd"==}ߢE Yp!dggh"zi<56LDFFBTTqnLL NNN|呝ͼy9 СC)} @퉉m۶?7i$yN۶m:ujczꅝ6'''f̘˗y뭷ٳ'p+}d.#"""""w)Q""""""6DDDDDDlF، """""b3 @DDDDDf(Q""""""6DDDDDDlF، """""b3 @DDDDDf(qh *i_| 6PUUѣ>}uVѣG3zFMj'N`ƍtbXLyy9|嗸ҥKfϞh<Lii) @-=zУGFԩS=zPNjꦮw7ZHbbbj}JDDDȮ](.._`ɒ%,qFhٲ%>h+BYY3f`ܹn:"44qѲeK֬YÖ-[ȑ#8::2x` Dee%/8;;GPPO=|g8pٳgSPPׯ3< gΜaҥLii)yyyo˗W(׎;oo8}1<:!??Ν;3ydL&/m۶ɉRfϞYz5999xyy1`HYY !!!̜9yÜ9s(//gVUUÇŋK޽)))aŊ@͇{ҥL& @II 1OEq%bbb8yd4ϟOJJ uV~:y1L8;;XJ#a5j<L&h߾Qt@hh(nnnر͛7̰a8z( .n^wrrrׯ۷gϞ=l۶z˖-޽hFEzz1,F8qhΟ?_'koo|甔˩SP>zIyy9iii@Mbcr޷~KZZ۷UVCk0 0">c\`nXe2Xr%ui&6oތ1118px׭[Ƕmh߾==z 99+W2p@#@3f ۷u͊ rrrjm2dC ܹs?~'Nプ-ZÃ-"b ݻw}5wy8>:իW[oGjj*)))lڴzj=..~kڼ\sWdff2c 㵣cCHٰa9;;;ld2OIUU< r)ӧk5#""? @LL {YfLEEE3L} '//]2ydNZh+bʕ+7AQVVٳٽ{uǨqtt:eV>~nԩENNӦMx]QQa4ΦLB`` ̟?D<<<0<# 4p5=1ǎC?$''yܹs,ƍٸqۛ ~z!&XdȐ!FѮ];Ο?qww緿-&`}Y:uꄛb6k9CCcǎDGGCѻwo^~e4hNXX!!! >?<呙YGaF '\3^43f C?7mےAbb"$&&\?%%嚟}sΑݻ8q"!!!xzzj*cرcC|gߛVwɉwFt xyyLJ'OңG|||j]BfOf{{{fΜYဇΝk""@AAf{-Z~zmVkI||<̞=2͛kFee%YYY;Bvaooχ~ѣGw2"** {{{ٳ'{+\\\2eJֆ ⡇[n@M/11M6qyqsU*++|2ڵ3Ұ={FÅ YƼ:O+XtjZW(t'|e˖_b2С&Mj\٪2iRTTu)++M6F /?g=:$z9<߫k-CJJJ^27Z"""GGzÇٺu+AAAFjz.\HYYZOuwڵk)++3$n6Æ ͛7qF|}}y',ӵҭNY'Igj{h-õuC 66͛7srssMaaqLC=kW?Zyj!~ĺ ~fc/\kߙ3g(++}0a5t5k!(--eƍ|ǼC&ggkQ[.!Ϲs?>P7"88ֱ"77y۬9w.P ٳ @e˖72ba…oooc_FF=m۶5CM~z زeK'?Oj ͚5x|g}[]8]F}}XϽUUU\x'|WWWX~=ׯ穧4n>[ .]˴mhXl)..{{'p 7ݢEEkc*((5vnD.]9 xbFMdZD~iBCC)((|5th?JkB8p O=|,Y;>n RȦMXx1DFF:o舛5C;Ưkkt=~Y5B;;;t?VUUћa2ܘSkh}͛Cٶm'N0愕\""wJNN1y̚5>5Ϊ\?Ư;; @nѣG&??|ݻ7-b`,ۥKcR3gؿ1?nj*J߾}c׮]jՊ;֬YCvvZxxq</^LEEgΜ[n&_:t%KP]]MQQ1,bɒ%X,ZO]\\\:\a޽։ׯܹs;v <+:uoZKFGG?Gp͆ϝԷo__|AXXIIIӻwoYz57o}lْ.]pamF-HKK#::Ǐ3rHcѭZh/^CF=Ã͛7coo+7o0`'OFF kr޽s1vjNHMMG1gڃa222j=ٳ'Wfƍ\p9cXhѢ!!!;vh\]]k }}ٳg˗/˫|$6oL^(..&33{b6>?~֯_?֭[ڵkر#X,^xbccٶm ,ݝݻwӽ{wh #ݰaC 5gggQRRBrr2%%%xyygh۶-Pԙ."RRR8qO>$TTTpAzAFFUUUVԩ;w$<<ñYf >ooovQ0֦tO ?P~z\]] 4^=ٙ~abT j"oȑ#1L$&&r :uk5HLLd21aɚ>>>?l:wo~*dgeҥҡC<<<(++Ύ|7\'''z]ܙ=zйsgξX=<<>}:Vb׮]xzz2n8ckq<1cp%Ldddd2ѹsg=jl{go9|02f*;; cXUVV?JykSNeŊ|xzz2iҤZٳglʺuXt)O>$>(~!UUUti&-/Kzcaextݬ .舫+8;;ǻb̙3F'33ٳgKc]hРAV׵ںBdd$5nסCBBBغu+#""{%M^ 8|X IDAT_d7=Pw!,,?y'7syNzW#"K!X"""""pAc!UwDDDDD Q""""""6DDDDDDlFLͮ& """""wNS[%"""""<%"""""6DDDDDDlF، """""b3 @DDDDDf(Q""""""6$''7uM "ww"""w' "rW)))7oʍi @DuK}w6#"""r)I`(Q""""""6DDDDDDlF، """""b3w̜9;w^skرc5ӧO';;KҼyFMK/'N$++~aÆ5Jz?<זKY 㶯p#ehށHw """w> ŋ?~ .dRr-=Oܹs\rEܴ7<㵶unʛokեK۾R^@uYosAHbJ+qE3fo y,^+Wc4Em֯~+Kd.; &{\`\A8x?YȍZv-_~%s1`?={0qD~\c=̙31L<ӿ~x9r$O<OfÆ dggޞ;vЪU+^ybccؼy3|eee 6{CRRǎcڴi 6Yf5ݻ_iӦ A8~8]yƎ?ЫW/׿2h ~af͚ŰaØ2e 111=zDy ɓ%99f͚cd>W\\l<20WKv.m {DDD~iI6m#tڕǏK/T~}Ȓ%KX~ÇӿΞ=oAnn5ܺu+s)Ν @vv6:oߞ۷cWTT ,??1b_5G9vqΝ;qtt>c޼y3w\?ѬY3va?d Ƃ '..eŊ۬XM6٤7ZA Te`w3;7}}g6ƿrUھ^ Eh"y>3x , .u믿̙5kj/x7y'1͵֭˖-믿ӓ<.\U0L8 |r}cccY|9ޝ*>Sx㟵-]}2ydLyy9nnn2g:DYY5g+x{{,.ROa{[nkNxx} ̘sZlj܏jf֧P88_w kZԓj***=OL>ѣG3g<<I۶mIMMѣGt^q-[W_}l6Oll,?nrxyYd _v?7oNii){{xzzp~~~8993b##;Ke8xSuG\Ӑ(;>>۷֭[3w\zU븑#GSL["<YYYoy 7ߤudffCa2IxzzxBhh('`ݺu{7t~DD}޿ܹsٱcӧO'77~W,fyTl'΃O4Z"""w:a>fO>:x***,{C:n#99޺%7///JJJnիWμyn=/n"""r-I~~>/"OǏ9cR!""҈!Xw֭[ /`2ؽ{7^^^̘1I&5uDDDDD/>hpuŪ;''DDDDDDlF، """""b3 @DDDDDf~`Ƚ/l,H#Q""w΂4" "rW7o^SgADDD;$99 """r3Y,KSgBDDDDD~ ، """""b3 @DDDDDf(Q""""""6DDDDDDlF،CS%\PPTI5I&biEDDDDGCDDDDDf(Q""""""6DDDDDDlF، """""b3 @DDDDDf(Q""""""6DDDDDDlF، """""b3 @DDDDDf(Q""""""6DDDDDDl@?2}t.]TssQZZj㜉ph ܨ*0`@yyy:ur%RO?mpi_PP-_s#"""e˖ @iժgΜi0 m[uX,L&sDDDDoFSgz~G"""HJJO>5 EKff&Çɉr-[Ƃ ظq#t/^̮]11k,<<ӧO_7sDDDD=TUUѧOklۻw/88{$ٳgٷo'O&88`y8{,;wٙ~ ӵkW\]]IIIΎK޽޽;DEECe & ƾbu7{5Ͽ@Vi,*|AE۶mIII瞣8ԩS8::dlkӦ ...ѲeK"##IMM%<<3tPܳg/VVVl=fU>"\\\ڵ+ǎ3sttήnǎb1hϟϙ3g'<<8.88)S:ťq %mnF{>DDDD6,>}Į]5V˜5=йsgL&Vk׮Ƥvi޼9Ҽys<==mW@=Dzz:}EDGGpBfѢExjl2$%%(ܘ/#;;ys@ǡClSP=C۶mqnҤI{m2uZ ;;;"""mNNN̘1˗/[oҳgOHIIW8b]GDDDDDs= """""rR""""""6DDDDDDlF، """""b3 @DDDDDf(Q""""""6DDDDDDlF، """""b3 @DDDDDf(Q""""""6T 4U"""""x~~~Mz@DDDDDfL 7E"""""8;;7IQ""""""6DDDDDDlF، """""b3 @DDweKlioFS$\]]}X,?σ>j˗/gDEEjSSSySN6OnGvv6s̡={6xYؾ};?mYXXțoɅ ڵm]nĻヒ/[n<űb dj<4e>>#+zwyw]v7|5kXtO>ܹs|2;w2܈By:vHͯyʕ+INNgϞMZDޒѣG9y +))i_Mnz [k;;;ݻ7Æ d2鉽Me Ҩ鸹ٴ\'Nয়~[n$_;w|r㵃 2HݽQҟ9s&f}fر #GYYYoiӦ5Jw^|'$$xbNzݠn'nnnu}O~𨳿O?N:ѶmZ-[ݻ߿?&L`ذa/aӦM9%".\W^7uNRRR#pW V͛7'22j>̆ bL>fغu+ѣ=ztq'NqFo:X,zrxTK\]]ҥ gntl|||Cn=ѣG]ˋp)=JhhQ[juS׻vbbb$11 ss{ZjuG}HMM%""d׮]/~qSkfÆ ߿뉈ԧ՛QVVƫJ-0ʹnݚYgΜO>^1;w;vW Zbܸq?_dFͬYppp୷/ 55ֹL2:}g %%7RTTD˖-yG4^y1csݝu֑@UU7-[flBTTGё3h *++y饗̞ٙ=ϟO~%//y\\\8s K.%''`JKKoEEE,_,WFv7|7|ӧa tܙɓ'c2x饗h۶-NNN2{l?իˋ0p@xW a̙@x̙Cyy9˗/xyyѪU+Ҍc''Z;Cnn.#F`ǎfƎKvv61a}g˖-ϣ>j4RRRXv-W\3+Vp!\\\իG['|˜1c0`@[n<#tؑE#**)SԪ{Vx6l >>˗/ĉGjqG}D׮]9{,eee3a,KzZ'y+W~ eh(9z(ݺucСuц صkNNN?030aX_L>Ν;yf֯_ϤIСC|\x^z{nBCCẏ;L8GGGΟ?OFF͚58Z=tR222 ˵Crr26mƏ!W׼'W_w3p@N>M\\Qf37ndϞ=TVVҩS'&N[*++ڵ+&L`ٲew|\\SL!**Vرc`e˖RDEEvZVDĖ3g/m!++ @If,zTEEE1j(FeDaaaxxxx5kFHH,\{{{ƍGii) ,ŋO쌳3&MՕkײuV1gyEEE1'''VZǍ}tԉ}_}Ͳ'%%cX9vaaactR9˜1cgΝ|ۥKbccO>ƾ#G/iiiݻw)JKKԩ.\࣏>"??ʕ+LDDeee,^\zMII +Vj>K.d21`JJJ9}4EEE,ZK.ɓ'k1|RRR$(([?ɋdRVVVW ƨQxGpttd2FzݥKBCCqsscǎl޼` ѣGYpurcC~h߾={a۶mƾƮ[laDGG3j(ӍaI7ĉDGGs:KMM]vx{{g>ϟ?XN:U<ѳgOIKKj}[h߾=ZZlfsʕv#Z.ʕ+ήsM6yfn:mFѣɬ\Ҙ1ch߾}kVTTSg֭[),,wU- 33rBi߾=)))@M{w!..>wbaռ[őJJJ 6m^Afڵ6/ܕ= ̘1xX%0t6l@pp0#GΎÇc6y1L|-Z+er ƍcРA1{lv}1)))2}t >>Nٯ:u*nnndeeôipww7^WTT)SHpp0'11f3< "<<@MO̱cС#u2w˾qF6nhfu߇z *++2dpk׎ϳ}vod"88g}N:F||BCC%U_AAAF]ω=a3v-trrJKKR!!!6Z3,o߮2clT9G= uAIқoi`Kc~\P_2331~~~6m};Ƭ\v}ѣ|l=iRmذAlo߾Z`l6BCCt;wl H޽{1ǏkŊ[ԿmذAuVlR]waժUJOO7_2E\ڵ VXXۧŋJFŋf9<1{w;v˫s@YF'NPJJV  OOOȑ#/0lLL6oެ{OOݻ5gCrvvV߾}ꪕ+WjݻZhΝ;oռys㏊1p7--M f;\ ,ЩSԡC: ֯_/gggyzzj7`ֱcnjKPرCJIIdu-UOg~F( СC.z쩕+Wjڵ***u l65o\JIIQLL<==~ݓ.?/_.U_~zGJNN֟>'K?~6p@}WZz:fiɊӷ~s[۶mSn4di||~9q6kLƍ3geeiݺuҥ19jɓ'k f 5J...*//?ݻСCt!cǎ%Ţ 6[o$UOt~zz Ж-[jڐtٿꦧ5kH<==f<<{h$11FT0?3ܹSC ѰadX uQ{VhhSDD1X300PJOOWNCx-ƍ%KhϞ=С|||TRR"'''M8Q}>so߾޽:uTy*VM4I+V֭[#F|A}gMPNVqqBCC/f.:wGyD_l٢`=xA#>^zr>vXZJ?ۧ+&&F*++`勱Z_$UwץG 1UÇT{TRR۷+""Bm۶UJJN>>}ژiMd6P-Z U^^ntUTTW^QEE4yd駟/ƌ0L`ӛo)ժAI۷W_}%KhԨQ뮻者R:tGfFZS>gm/ OVY*OOO瞓 3ͦ\Vrr&k}ƣnێǍ>B^裏6pƑ#G]5R:tHS^SFFOn_4mn:mqF 8 ը\_LnZJӧ1PcKsN\Ҙ599Y[&|\u]UU>@4fs{w}'I睉tQqqqZdN0#ֹ?jĉh@= )66VZfuM79 o:tHmܸQwVtt&LBk׵ lYFE=Py!T}KKKSffzqs֧zH:uhȢ"?Q"`?\*Z'hl 5T @4iu6`` i LC`@4!0 i\`׮] ]"&&A 7 ƉQ9yo7̙36@4:ݼy"@4J%4N̂4!0 i LC`4L:UyO>]säI%K߯Tspp'|R^^^_?]tݵkjҥZ` 5k٧,.>yUvIZywPꡆ4n>DFFԩS4)""xSrͯx;'N̙3<#FHf͚ӧ)tMrssuu ר鲕娪$0A~9$Rq )G\}2eg{kȄ 4tPh"9sFG``ڷo/Ij߾裏jӦ%jɩ&wiӦNuXK~,rh-L٬:\4thR}TWּy󔓓M0A$=tkӦM:snM:UE  M6K'Oְa$U?>v㕞'xB7|elR>$Iׯ;C :T;vPff{LCմi|tY6mh۶mzwVZ'~;IرcզMruuٳ5`M2E_~;h~z5kLǏȑ#%I}M8Qݖ-[/u})00Psȑ#5sLegg(TյkW͙3G~ZiZ5i$O?믿O?T֭3ꪏ>Hw}fΜie˖?O?EEz'U^^^uav8yVuZU%dqo:ys齽Jʫݹ.'"̾[裏K/fi __|Qj*sѫQFj:suU˖-ҥK뫬,iŊZ5w\-_ܡL\\/_Yf 9qz-mV۷W@@ϟ#F(88X#Gol3rH }Thh~'r8p@:}܄岝ɔK鿭O^9|dرzg?Iݺu37aW^yEaQ={EÇs]Fe˖i…矕)j,OHHĉu?.mڴQZZV^-ZHNb!䦛n?z믯nppnmݺUvi{OZR6m4o<9;;_pIcaU\|Tyb<#D(Eoo{9jjժf̘>}87l0effEcǎoWjjꫯ*$$uЫVZ)99Y7x,% @ Uc7n~mM6M:uU\\|EwOmܸQ[f̘-[hҤI:z~K8rH=Zo~a> (٬e+>}@AX4&}U>@11150@5\ ի\\\o>PhhhYpصkWVcT^^0'Pǎ5uM&nZyyy={/^۷G9sѣG 4n~dXk.nZ͚5h͒RD . $ƀHR@@"##}v_~E%%%5ܹs)Shԩ:|.\(l$eddDQQQЬYӧkذa;w=*IVM:b450qqIRHHK=e$Iǎhm&@*++կ_?رnǎի\\ۓ,//Ow#/tٚLlRϞ=}vC]tQJJj6l6#hUvvڵkc:lqe W֛KmhlL,~)11Q[nU\\1]HHˍqRuFYY$I:ubъ+ԥKcP{HH秠 O pkr{X,:p_cy+==]Z`zi<5X,իԻwocXiΜ9RzzfΜiٽ{g΁W&@pczs3Faaa5k)<<\ǏwXO>rrrRtt񝛛LӧO^f IDATᆱ={*&&F~pSZ@4]!0 i LC`@4!0 i LC`@4qisrrh 4fkX\Z@4!0 i= .jÇSNI|||6mɩ30 /Єh*,,u  ix/fo ]z|r=:qDgM2E_}Uߘꑞ)ShѢE]wڴizM@ǟ7|HRaaUPP`b.]ꂕ^{䤀WCbM7|#I~wE2222~uU )))I_M/4iڴi'O_K/)hV6oެJI-6MEEE$___Y,R7o]wc5b^zJT||*++5|pM4ɴzlܸQҕ Çïhgڵk]bdXPͮnj߾;*nZ;w6$'h>l|iذal%x`oo!),,ÇնmuM2lR#FT݄ /h׮]>|M&k3gmHHƎ7x~Ǎh%%%iڵW-t]w{>*))$M2E3f̐+ܹSj߾F-ZhժUoԻwo/ruuM7ݤnAz*wwweee)""BGV@@>#Oz畓ٳgk:zG}TՒ%KvکXYYY?|-_\rssSTT~8-[>$}g:v:u$I:~z-eggSNzGdXO+<<\nnn*..?4\R5dgUddN*I:u/L˗/?,lR?y뭷tQvmڲeV+!!A^^^7ߨ@!!!뮻ԱcGI-W֙3gԥKrJJJ駟j߾}P>}4|UtwjpBs=UV)!!A!!!t\g-ZHGU߾}uI}駒}iɒ%X,zWԮ];cҥK3/y\ξ_-**_2㞵L_pS([@5e㳫k]%IP||ڵkaÆIzV~mY,=Uee}Q*((HG?cǎ9h}'XI?f͚w)99[nQϞ=ٳg맟~2Մ;$:tHTyyy`РA;~[ǏWvvԥK=#]xN>-ggg5kLvߠ iF;wT6mal?h >\yegg5o\>qgΜш#t 7D?mۦ={^g$OOOM4I*//kqKc=&ooosyyƎ0kNgVBB|||dZunPTTInIIIQt}f_֮]t-8ԣ}1cFutssSii͛7K.$+::Z:tVZK*22WF@ջwo9;;+ @={oi…رc+55U7xv*I PBB֭[B9sFTUUu:UTT*ojg|ǘU>DWVj'IFҲe4oI`?n{=$IT~~[UUn(BIjǹۻDϗsUUWڏU'OT~i է6fRVVeeeJMM5i$uY;w6/,,ԪUFWC5Z>$ݱf͚5X}ϧQ-Z{z6MWqq?:u|!mذA뮻o߮5khUDDF)<;bƍ-[(==]#F̙3SOe+;;[>P pK~kXTVVb~g,NUVU!Y-'uuzox㿒()/p~w>",X@[o飏>K/$ͦ; /^|EIҪUϙ3GF%PյkW-[LK.TTT+Vjܹs|r994i5kV}>p;7l4DwJVϼ >^UUUG}Ts5}7G%KHZnaÆ?4>jaǎ;oY_u뭷^᣽ `Z*9$[-٫# reU9Ͻz\|$WWWfK%RUVV^p[~!E|^Й3g$I͚5Ә1ckGZlO>뫻[R奛oeLqf15|ΐ5|HR7G4Q'qM)hb(22R׮]ԧOرCԾ}{u%I $y]~)ڴi#IڶmbccUTTTc4˫GѲeKO%m3__K.Bqrd%p*O5v\DƎgyFӟԭ[7㏒q9+?׾}~;,5jõghuȑ#l2-\P?233eZ 8qpl*9$(U,'9% zqU B馛s)00PwVV4c aaÆ)33S...;vlw~JMMUpp^}UԹ.zWժU+%''ob1Waaa,Uֲ,U|(:V]+bw(&&ƴ+!!Azۧ? 1vBݝ- LC`@4,Xy5tBШ ]p@4*3gl*+ԓ]v5t=f5t%\ i LC`@4!0 i\*7Hfk\s4!0 i LC`@4!0 i LC`@4!0 i L$/I&3fЄ T\\lrE TYYgeeȑ# P#udH-}o߾]-[l+_}Iwnnn2eN>^{Mz쩘IRRR~+wpUbl ] ׆&"0 i LC`@4!0 i LC`@4!0KCPE׼)bl QpYYYC @{K,!0 i LC`@4!0M [-'/heIƊ͵.?*z/B2 Ь妖{_*#$e_iO1j;؜5_EJ)L!^v.Za|{+XnO1Ul "Bn.BZn{\%idu o}g5z.c'{wU}=ْL,d!U@@QTDE\Zkui{]mok[v񶮵ZmU,(N!$/3,Y$D)!G IDATz><r9sfsѠr&<*n F~(#'ʶp7oݮgz у/& J}Y㐎7m:lZs>$ѣ~u,.$șx.jw6:٣rm}^޸ONM#2Ajzm:t\ qYi^yO% LӽR$I*JwnL\-^uJN3&У{ܦL$=f~Pv-R OsqA-ZO]2As+46']*Wns$Kjjm$!%;cWns?+6eYyS4g̰~WTj/^[.G]GwwhVQ>"٭+S0ReGf)ff C{EUVOJUؤ;cq'+nļ ->ZOolPˣTw,vÊmjuc/ٯ$Iz1=늩7@E~FU4xT44U_b'8ʦ}:TUo\$陷 [o_mwږ|Baies藯WUG/Ugߔjw~{:V̢\C'*m5+f;.\Ⱥy]<:IWc%=:WG⒮Ǫ}zSW'WkKբ9o׻{O^|G3 soՌnŦy[WnT?h篾!˽]WLC_6{,]{Q4BZ2c?m_QS vK8{jV=xi͞Rm+$5kl==^r@h%:z{7ܦ-UҒdƘ~?5{}f}qg$kgiwVOަr56ww-_K΀xq%Ųu򴵟5^݋ݬǿXwΛ'yuqz[Voͫ&.UnjbW:ykWMWmhm֒c{oѸt=Hvw+uIzKC/wnL :Zݨ%7%闯=5>/]^$-Eŧ+p!Z?ӵ=WxIujZ49?SrHEꆃ;@ӓcцyX%:T2{,ESdtqavr3Df呫ūUɲY49?Ke}en?.ڮGnCU+PnjcZ0q6>޵ tokzN~op\߻eL}ۭhUMES3d15>/]6Y5>}?a :v䮐&IY)JvFb6cɡi3Ff$iL'iWYCuqJVaV&t^$%J'=:&|i>1kbV(VקΓ֒ IceD铳ƩU5nzk y&gjle%))6ZTlmkwYn3I?\ 1vI-yYt]}A1+7nJv[B2ɬdGVzȖ5*J)q1wj"Z*JuƖ6 Iu^w`P?~yZ}YlR(+gX6t& {ڴqqH 5}Di!IO~y]pUmٯ_޹G@mjѓܪ#U 6_W]-^%v}HS-^)&:n>}szr߹2=z?T/\9M 1^l݇'}Ҧ!ݎdRZ?hE͊2D&BūXM1SUy:B'ftΟӠ8C6d Y^&_l)Z:~ zwQ54=GѢEJtF;䌖yݷW%jB^j]֟VNZ||]'ifއ%;Yt%zLkGȲucd':O;onT~zh&Ӏ^n'5f9mv͚>@y@!IѪonp% %sww[cjZPE>C_mX|~!Yz0 !X zO|F}3nU4x40Go/C',Ѳ- Uiվ㵧l R[ڴ|>V@æów X%$u\9qm]mUV/Բ-,=KfR1Z8p@]-^$ra5{} ͢F+[%??Sﺫxչ׋hڈ]YD@Pέ y޷r=rxuw*YZ%KY SjPLTEk) kdfD\-^պې=1vkٖU]C'vS0zqsR#ۻj=&;50[os&jB}k*/-ƋWs~ICbx%%J~ keIL-aSo]^;K~cnJq/麛0{T~HFwϟ6 g.mZ8,Cs a3l~i~>Vlܫ9c5:;MSE]9q~C!2sb~;rz5cdQײiuA2Y'֮fwlEn1F/+Hr:$f}b{c4,-Q_f)fhHB>r@mJ߼Q|} ғ[.dLIݿ2T~nG_ ͯ-^ټ_)]wb6}ekh{; 2L8A{$o}UǛ)9ZS7ԎJ}Nw/ܶ@yݞ(LϿ 5wܹ 35?vikqU= U|ZNilpm З,7Gd7onԭH5\v|>_ڏX B  f2"5o`@0!0 a ctgR%?,צMGYSR4c.=##EׯU+TZQs{ cihXJR:um~uLyYOܠ]G+z+dg&g+/VZ3S8Gj%&&ѺUUv gǚ)/L3M^_{@m~NpD[-E=9?b0h?u7˿p!WpsЪ]lɕ.!Iv=rC8#24n+͛#])D\ ҇w=a@%GOG2"]yW[|ca5#6*9 \e08@0!0 a.2s -()т]}&=Ǝ9o[B t駵nmgVZi/ɓz; jΝg}p3Lzh GKY0&[CES+V=dbmՀ](.1j9rD޹SfC~CmWjtF(U[rOJ"]oL>ѠHbQbtEjz[.0[㇦Ņ:PҦ5oQfq$=ZԐhm;ǗX=p+=7ғ.՘LM&m9tLVU=}%2M5 \ߋ)zwGyoɼSK뛊ݎ.; v?nb%IfCcݹSLQQ)p%F=7ikYUT#ҥ]f))~Vi:[;2b#`ZMOO9ɱ~Hv^w\{+#XCaj;Q<')VSR:^wTy%ʬsWgڦ{'O35,=EiJIήCY-:2vM*ԡ5*$ӂo?V.; ZZp0zh-Țq|*M}9լZ_Jf$irnΖ-ʬmܿhEK&kdz|ҚXE7M-P~jj=mj %u z\䦩=jwmu$Xd%)&=l(-CS jrC_3F6>3PkUjsiyiY.5JT}%II1v-dZڵlW5)aշNMNSWyCG"JUק O^}LK껿,Zz$x/]>N[Jkca}}×՜,EzчJ֕ccWE8ꦎKGfj Y-f6՝xJ(=QIN=nzpUߣ)i9<]y4%֡_9QuF I5scױf}[mSKջ5kxgb6黋خiC?m8x i衲FV:直Rb]v㯭w>uRcUH6V7Q857I>yPs'iӁΛXWV;$*t  x<&$ȑk՞K|7mRٓO*sSKja|!U;ʸzj8y* <|`{y1I2w\mWVd29J?\YKi#e6iGy Mجd:^pXn0%+u4(Kq=gd v鑕;3J֚7%IѺqr^U)?:mzv]1zrœe1;Ҷcӧ)z}z|9^1ج$iA]LѤ+7LX~j~filz׷sjWEWܭEYJq:d6٣tY?~}V?gznz/WЉƖ>Uvauq秼Y1nޣYɒh2ba@F瘝c{{~]fBnh*ӏ^ۢU.~q$颼4gOخ>=dNjԻ*?)>cV)O^ߦ62u)w|=}i[VSYG?~}41Z29_˺m=yW56iz!Ijgu٤SF뵍ڦb͟<ݑ7%c dtTe$;z2H'"K|ƌL&ZJ5X#M9=e1cyww-:o{YLpaZSY3L'.5*:Ҏ:,QJIPXRe64:3I;e֤{\^P56<^urݎ:qjtƭ54=Fu5nh׸Q^Fo* UjQǫhM&JҊjnMw7~hRU_V'wOe9Y4!;EoSK_Mm>Y\WbOA{thXjbBPH rTzrX{* ye|Ww+, skF~zW RG>TR/0c# s؟u7)'Uuƭ`(*V( :/eLQFBLD_ߣLMՑ&r) i*H?%w|?LadqkYh('˴= g+qr匢a:tFfչZ5wi[5cvyN>/6<3U]p留G ;|LfZULN6\{mG㖙{N{L G]DO4(aՌt}y8b7{cu)ۉI&qR0.Hk_%e1X~oe)Z0&GbqkpVI[0&nRhL&%Fd6kl29l}. y>?Vpɤo׼(9Q}pX}rX-h?>&ߏ]ez}w RhB2c'u<9VJ n9vO׏sڭr N654 `HOI1v5l=ObMEzp&D(ӏ>8VK^xw^xwғu3ؗn=EPHӤz{HVl(kon٧ ޞ#;P=8RY{u>"tϫ:P{Lr!I2EEd=Vʺ&*"bb;Z[mZ4!O/n-/[(=Q2QM'Sycl(ຓ';t2W}=^B}k\ y2Lݚ=f\އ:u Uw*{*͇ $I- IDATCa9VSlîK{ۇP㤷ڹ-|})덻ͧ6~S x/.6_@[jKGjbOn6B/o?o:XҌgUv$Mwg9un))Unx7֮n/Fu=r. 16v [QC+w=^big=CciJpNZt q_j|>-hZ{D_\tzSƖm-~<㯯 n;'u2#ͪՈyw\7g߾H.pk&;M+8V})zhΜ=h=˥_q\Cp9J|Uj>^.!X8<^U9cdDYt(e?+%A@.5!!e _4em3έhE Fl+/V\݀qvuő.㌘p8 WWW+11oчVM՝kjOWҥgdDh =r1[h&_WAihXJ|~^nS6jW 54 ## Cuו+5>r~+A@HA9 D`@0!0 a"@f"+l}l R{{l6[ڏXq:z αp8,+/:LA577) E cl6ft****buD4,!0 a C`@0!0 a C`@0!0 a C`@0!0 a C`@0!0 a C`@0!0 a C`@0!0 a C`@0!0 a C`@0!0 a C`r6#IENDB`django-ordered-model-3.7.4/static/pizza.png000066400000000000000000001744561440510626600206660ustar00rootroot00000000000000PNG  IHDR 3:S pHYsaa?i IDATxy|TI&JBB  ZwZZkwmVZE E7D%a e2Lf100dq~>>ιg=s>g z(H"""""DDDDDDFD """""5 @DDDDD$jH(Q""""""QDDDDDDFD """""5 @DDDDD$jH(Q""""""QDDDDDD< <~qf^4:|+7de o0cȿ>ah7_׮㊧&sdyxͼ!{o=u66}6D9潼.l ~pT',jҢ47{?Jl {gsjܸz|*b,~~??YDxDDyg)Liu5 lv9}a 3!/2R{e}awyElItm倈q>؞µj)"""f@DLRb->\,oeyL ͥ'3:xNooIMP{l$6sfw5&Ǘ6]??ō3)҅o`Mu"O,ʏ3`*KrŔ:& $!Ki! \` WpgvV'2s:rh[Ⱊ?[䦻Di+sn)|cR=Cp٣Z_o|F_@A 6r–etsϜm\x.y%ͤy_5""< "r\l7Ci=bvsʙi-k$?oTEJV͜1M,'$0o2[iÆ SC#[e|1:-<*gWe8l}ǚD>fjᾹeܻp$jBuMF}V3w$n9r0P9Vi ]`91V'%\tB#'o G2vmMqaI3 IX\55ՉmY6Z-,ٖʟ?FfBx{n:yۛxhi51<&&׋c1Cܿ8f:{,̳ۙwBCߚY?[3hwYg,+d}tV~k$iwT/`v&wF)vI<29%Pq:Qi7WY8 It).~F1L%^$7S{ciuYI̢: ӺlrH>.ODs}͓+rᬝaI 6/. [GMG N+onf>6-q7`5#?WƾˏVb~2ÃKio{ߚzF_f>d#&:9ˌ4O]ċk0_YĖ`#/'zd8/ =v4 mMv`1HOVm{,ޚƏ^.+y)=}-,H8sL ^?SwGsAX""G+-7pEv'40>Ar'qt+cn- siݸJ.O9kdyo  a;oDD @Dfb87lc ~HV~Vo\sX׍w{S. bR 5r:4>ΣBr^]f?-m2k괰,2*a ?>ܤ4-PrX7_ٞ}uDD' @DVl Pyv['UL*] 0$xi= g8O~3j#P=^#IW+6`0!]0>֤N&:cq_>gnPyr@DvѸFW$a׈nVW%)3q8Zt:`869IfgKlز}` G%X2{?׽ ^IR`c]hŵCNGroꩵai]9V(DDyL璉z48ܕ^,y=^ېE `5dieQY:mw|9+?~m7άK7P[/ہgS_))n^vei>W{pVOAǎ8,Mv>w;vOhInq!WMSv~3okֽF8n&q2cILa;5;<"QA_=NYݽ&~F17άb0~H2z9$|FwWkg{?͡=KOGg1>QW"buC'"rLz<>7# x5ܽ`$N9h |n{o@Iɱ^Ex|5i#Q""_ 6ɱ^>@E`^G`HrFQ+;'WFm㝖`ȗ…:m<,u5YTeb PˋkEDDu @DDDDD$j4/"""""QDDDDDDFD """""5 @DDDDD$jH(Q""""""QDDDDDDFD """""5 @DDDDD$j̃ >:KN7pΉ̛w]|=)%l)ΜAz_/gyi+O1>טP-'!Ό?Vp ˷mOm;)Oh@J%ȭXﭡiRG(#NdXOS3B{eQ-_;%[/*?fg}7:xp5.f# $^_o;/>%CeYi᦯qq9|kȿwUclxP[7>gίM{W7_ }lʪy}%عkg2,3e=gV.9sM><ʪyߦ#tږ+_7G7b6qrzlWd幟|h~FRKNĝ 9'f|_O̒B}̙:+gM%fagC x=8"~ٴt: ^yʆV챡d4RŸ^{~?x ^]݋yk6\4.>y".vַD<טl7thr>bEiW5X8Td4. -ю^mg. "G~qy9'Yl /#3<7מ=o>|e?``baiVZ:2)cwCmI:.?b >{]\zZ.<\Ms ('^l2yKo;JB AYn+^?{&e ae*j;sr^ŏ9-Ɩ] 7YϵqԱ,X1^X<ϰ!~Y|gCǒ챸=ݓuϣS'Ir}9o9_[zPB|N+ {=${,|i|~?7*^t:PH/| Ly ys'iF-__}!<&*HO/߽m5jjƊ:~qy>daq36?;CqS/`~y\ӡܚU)c%p˟Aݍ_[=K{opٓ2Yq| hffI!s\?>od{,^}![QMQn&w<~nnkBz _hSDdph Q㥡M8 WuM82 1Xy5>e[Zٲ@U"5Ӈ=~]P'c5Ii+M >4hpح 2 ̚,&N֧Ζ]\QϽϖoaԽi!?+l{^'fHZ)c^?].7m)F_;K @J`0@~f*]FbGECߧ 1:=^yڇj,i}r!8}lx}~Z*4pΝ2חo rÎ=֕3#wr XlVa |q#s2Ce rx8oʘPhc.߿x㦡A9,^WƉ#0fX6+j۳ﭤA og C# ҝlU3'fCNN)WӾ/>Rο|J\g/feYkwTw>ɣM"r QdCE'cbN*Icvms^vP.6~OScn X3VGgdAâՍ|o@.6gޫCmیѩ$ƙ5 I1{r&﬎vGψ@}kn&fɬ,]7N<@ۣEc[.κkњR[n87PZwǯ^o{Ԣ8{hut3,3)E( a8{ld.`R0-cyi>䙗,G e_Vq1²&M{5;xO`0pQ|%'{e1G*ܸ?_;~vXh`on:iqXLf.'B*~0*N.~u[=wmɬ|+hu8{>߀6azplVVSŖ{ د';!!FsO33%>=p_q6?k㮿 o_9i Qd}E%y L)NfEYnG/&ds,L3)΍ w9DDBC d4`e=LӲX~ %Ě1:D+jמ= >1#bXư٠kJ }CR?pɩM;8}|/bC[~ƝKvj" -'s'pv~|OxX?NbȳQ].7^i{g2}&!?+|23mmKO*nE $c`\{|ߏt8]Mjm +|VuXf*5ݽ^V)bE;z1 $Zk &$Y2ذ kbFI*#sŜ>+֮^X[/,f1b2|O2ͭY3X2CqƄt,f#qw<;@yٓ3ʋYQwyžҚld̰id:Żhu'ZF~߳hu)W5vu٬q#XWlz>:)͠opi=)h4OG]Gvjp _o> 瞄h`0pi= .ʔ||?{m'b_$,ӈ?ܴ$#%q@7o3iKbᏰ|pɺL]YtfSZwŬ<~|a.e<(rϔïYx?v%ߚS3?``Ei+wݿuMd%FGj6)|augOʤU \?;QCC.;}(=>ϯ퀵ǻk ͤ'Z#wpn{ڛzTfI=5{6VxXt^sa6w-SPC.ecn$jE8SpG;kϙ`]y5/~wY,倈Hh D """""5 @DDDDD$jH(Q""""""QDDDDDDFD """""5 @DDDDD$jH(Q""""""Qc1c`ADDDDD$4""""""QDDDDDDFD """""5 @DDDDD$jH(Q""""""QDDDDDDFDy0/Ƴ%e5ag e\N*\~Zv` ~~nq\1(Ulo+'`C`5 Mj2rg`7Y&ػp{}QnёeYQ8epy$Z=ݽwgcj|,/̟@]G:W$@?؜Tyh}#5mN6մu(IQZFǐ8f|G=m}=jt0.7UZ)t=z}v񲳹DDDQr|MWu +;5tmkM$;߫mLȱ 9FN:{MNQowpx|t=v/s!"""@1̝X@AZ^/Xd1eKrjQ6Vջyk-NA^J<-N7IE'3~hnջXV܉J6bL.P`Ff~s mut3"YɤDzlG\V7zH 1\^۩pRTN-ags^ Φ]L"#=!Ef$RlfcMaRZFcg?Gg,ں$ZIe]U3q6Ff&a4p=^HAZ"VΞ^6V6b6m8`'hSpa=qCӈib2Ye# 1VN+9.; P%HI<^V .}IG$ڤBߍ3Gk y`:6cl"j)#qxX^vX)jp]$Z|cHnncK};w;v'@c{d%Ƒce{c!5)֊UFoL;Y+rӎYA zrVI.F焕 rSOERl @^_W?wXW[k&b,&Nc:11/Oxe&0t*[4:\|Ɉ Ƿ;kttS<$m4(J℡fg==`{.d')#9;xMnòLnt{MO=ܼ;@Hǻ k |@0pwm*.3*Hd{c;g}W5sfPrTuElf6ִR`xz"M;ʛ:H(Hd]Us bCM =/^ZzHQ:?ם͝`50 ѶP?mk`LNpv(;9 @T{ |y z!H10@IqԶ9*Z\8>@@d@^HvRs& 'ַqzq.M].ڜ|kY&645Һ6״~ jZ9qX:l e4w/eͮ~?4%Ӌsxꓲ>\MVbsYGf39?X+qHݏ8L2f1qRN6vdx˾d$bu?M|sjKB߳\)!Jٰ=ڜ=$G~=T>K|@3qyf$24%9 6 am wgp-e"""rlYr?gqwi[حC@tumܷp-%ɔdpVP^Xu({<^LF#6)bk#|w.3\:e+w6#+1%CaKfq҈,Ync¶.>Rj5vĎd1V& d]U3 gK܃s޿_ ChZ$%"OK -*np\`O0 }c""""AGڈnra:{zkFG&8g-ں&'饺2{-c󈵚XgWlcUE#3G?;PMmd&&9XO.< 5l/pĖvj۝Pnl$7O{ׇ/'.ð=5>^u #tЁ>W-Tf4Hf`t=RPǑKW/awN@b~޷n/1~3HDDDG}'KIv gæ6> #>uc-fƳ>е|EI\4̄XKMiw0zH Iא8ܽUYq1 1b,=X+KjB6 -d023d6xD馮L$őb1&'d;m@0~p2<=+Iv%t{YOJI26?@  3!XKpfc%),Ď2J#fᄡiPX Md&Rf0#̄Xv6m$ƑKFB,ҁଣ?4Bp CH eNMDDD@~?\V |q8^nmu}ΗV\3sE[sy߯k=bs'pqx}~Y}oQ9ZMwî﫾;|$LK`kf +f}\xu%KJkHq":\,-e֨c,:{LqV2csRA'laCM ㇦q!tzp` >orNIv @0 ` [d,&[Zn"'ΌY]Wb22.7 H[wrՕM¨!)t=l;\AFf&c1e}u32Q8GN ?g04#CsK DDD$//@;V3S 21 ,Qt-h0KLbHF<>0N-uDDD_%r8\^mS4 IYd&2<=`~hQ9d'ى)HK$\%"""QKpy~v< *k$'dz}>*ZOi]EYۂԾzב]"&"""> "r:{zYvHGKDDDDD$jH(Q""""""QDDDDDDFD """""5 @DDDDD$jH@`!"""""_Q""""""QDDDDDDFD """""5 @DDDDD$jH(Q""""""QDDDDDDFD """""5 @DDDDD$jH(Q""""""QDDDDDDFD """""5 @DDDDD$j̃ҥKY|9͘f ƙgɄ ٳgRkN+V7׿5FJ9: z?Lll,_~99998JKKyꩧ8ә7o`7󨗗I'CDDDDjcǎ [oQ[[KJJ 3g Y PWWG||<ӦMn=UUUl߾ψ#7o?̔)SZԩS7oV5Twռ{<裬[.y _p\5;RDDDD?444;`I&1iҤV\ܹskikk穧o{7X|9^z)#G$m6|I>>z RSSgfqYgY'xK.'yG'|srWzYb> wq>ΕW^IFF555lذsVVVp89sfO>=ƍIII؆iӦuV|>_?w4iҤ>}ĉ]bYx17pz .o|aڗ""""" h`$$$D,l}Xt);wra0ym6~F#dffF(TTTPQQG/Gwww3\nOOO<pƩ H$$$$ɒ%,Xsre``ӦM̟?v9]]]L6s[grCHOOA}c_oo/O<sa}})""""_>deeEj_k֬aƍ\}ՇtnK/}?O3l6n\\MMMu\.WYZ 6pw^n3gΜ"RDDDD|5d„ l߾~^iv|vΦΑGYY^ן* 2`6}Y*aV\_X/A @4i=\ իWs)SRRznz j ׯ_z„ L&.\اnWW6m:mׇ֧R@ O>ĉlc/IOOXK.?OرI]]>=w|ޔƏ7ioog'%%;^kƇ~ᠥ~%KյZ\wu,\W_}6zzz(--׫'uuuRQQ'ƌ3rEg7͟?"f̘o//EDDDgPs@ w}҂b!??ko^y{1 /G}&g„ p -bݺuݔ0as&i(/^z~֣?yyy\| vS |>/6_ vSwߥj1說xw @DDDDU[[ڵkGkR[[;mP"""""ǭKv:' @DDDDҢWTUU2hqi۶mTsΡ+W̙CSSV?1<^x=\0 WUv;x<Z[[y?~<3gjoM[[[%%%n wr7h"/ ?ؾIKK;9RqwUUyǿi7 Ҩ H E, `T^(#8eD@ 2PJ^ޓ{?290@wu[nùOˣ} >ya6_ظq#666ztڕ~=z{>T IDATx t%Ώ`U)//Daa!s/::ڵBCCkmkX8x ~~~ٳZl?lٲk?}z@DDDDTPPpp t嬡b͚5 2p9tуq.dffXW^;;;8qDՋ|֭[GEEya 6 EDDDDD~Zs@&==>͛ӡCzŋ=Z,?W!WWW<Ȇ uGӓ;w`U߾}{f3gҥ8p/''__|||IP>qtt GرcK}]ks. """"rUrww_k(..oN׮]ڶm3IIIݹs'5hnnn ,*sرKҥKׯݻwbĦM~wC½ދd"33/#5kлwo-Zt=g ov.BDDDD2żyرc{@4KDDDDJ>>>4k֬˸4k֬(UW^]ewލz|je\6 j@DDDDXT k2@DDDDfggСCҬY3]cO*,,d…|dddG߾}y衇 G핧曙5k+UVK/qm6v9"""rg]UEGGwYhBƎˇ~HJJ eee?~>z肾\:Dll,deeKll,Vj[[.¥gغukc""""0;;;n>BBBK.$$QF1`ꞳQw4i҄S˶mx9z(saɍYqdԩ S S5;ptإдiSFIff&ѣSPP@yyycwApuu͍pZlocUFUVVF~(,,G4}'̞=GGGV^Mjj*w}7}>>>2h -[FfƘ1cӧK,a̙5j>>q3az~{fddpm3|zz)Sh߾=_tڕ>o;;;ĉʝ֭c۷wwwvʣ>Q… Yx1tW_}<>ZmΝܹsԩϯ+DDDDDFrA ޽{u=z`ٔw^c=$88}3;Oعs';w&,,믿***h޼9ڵfgV'7xO>'''N8ܹs瞻q>Ξ=ٳ]va2(++dǎdgg@YY9-[dlذ'b6iժǏgʕ0w\/_o T꺆gj6$33u~~~Oq.gΝGxbnF, ~-=z1W^aԨQ0fz톇[oΧ~ ɓyWXd :g| ݺuĉn޽?>?ޞ>uѯ_?Yx1?37x#6mz '|œ9sHHH --  ЧO֬YW_}E@@@Z곍H1!֪U+6nƍ $;;PBw}t֍H6mڄbӓ?@plrζf3eee5X,nw„ tԉ`c'm۶ƆU[֖m^{72ejLOO'//Tz!L&g5ox322j :wۻm]>}a6dggU}}}9z(ls@/3ƙKaa!o&=5X,5z֣QmDDDDDNh= -Z0>]ƺuUs jPGСC$ؾM6ڒ-܂b1z ٙbFwɓi޼9YYYo?gɒ%uСCYd /"$~,W}ȸϦm۶]3gGs95fM)//UmDDDDD5$t͛C=DHHFoM /]t ZOj۶-ӧO',,b7oHJJb`22e AAA8::Kqڴid<3?MRZZJ@@Gfu-.Ug3yd ?;`„ iݺ5FDDDDZ}٤1qD8k|1XOee%+Vod2qaxyW۷ov"NJ\\orYH.Zzm#""""R ry(--e…|7C^7nl#""""RMDDDDDD&tz(((((((((((((((((7v"W2RRR(((+`2gcX,]ȕDK+Tyy9N"--((4ÇB@@@c""Wt lRDD怈4 x{{SPPe48P^^aW"Ҡ4LDJ """""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5]b,X;PTTDϞ=ؼys'rv͜9sسg... 0c2z뮻8|p3f̠PQÚ1cNNN<]U ΂  ~ƍΟ'N:Ż˱cx7wufĈ'|BV{/Æ ߿oL}ȕ ;v >>}6v)"dΜ9tڕg}XֲeKN6n_of3?ӓF;t76v """WsƆ9sPQQQ6}C[ny|ƺnݺ1`ϟO\\{棏>~`РA|ͼFf[ooCN˹1bPXXƍ2dHaaatܙ5kU=}=Xmbccٸq#P5pƌۗ޽{sϑǡC̞=#Fxb1bf͢XfϞm[o@rr2<ݻwn?s=W[ׯ禛n7ߤW^[9s0aNJϞ=0`+V !!aÆѽ{wƏɓ'k3rHuСCYzu>sOϞ=yWxXx1Po_Gr-,^X:O?͌3.EDD쮘-BJJ _|E| :%%%~dgg3emXx13gN?f-o|jՊ͛7#P\\LYYǏ'%%2]RYYIfj !%%^+߿sqz-"""Xv-L0?C_Us&L`cfΜ}GQQ_䣏>⩧?fɒ%}i\}t/BY`g̙ow!==sĉ6l˖-cȑL޽8L||aݻ8 yqvv+W_~w,]ggg^rVk;IIIjȒ%KI&`oo#k #4?t +WRRR”)S0LDFFƇ~XkGs2|縸8Oի-[[[&MooAyFm~)F"66AŢEhӦ Æ waϞ=tRn&~j=$K,aȤIuEDD\Q$(({>𐨨(֮]ܹsIOOl6PPPPc;///j:@ppp HJJZ7_՟^-mz@^^zO&MՎׯ';; λ'**Ν;3k,ť^mU;=|53@Ud̘1;v pB׮]kogg@YYm۶:0zhVXa@T[h ,gϞL6իWbZV=ߟ{ƺ^eڒLhhhu)))~mK@,Y|QLZ7!rIVCؿ?>,ӦMGL=dwk?+kM޶mPl.]prrcbooOff&у  ## ?渺k,ONNf֭&M)8 6mbllls]"##ILLXm6]&!!Mҷo_L&5ʞ={jS} lw;SDObg͚Onʋ/T{yOd21g2229r$ > wycCu6ݺucѼ[<3nݚ={s:tO>ɢE?~N21cu)م"[dW;HMre`P=]/)clŸ-pw2_v%qd^s}kkZzZ^PZΌoWG吱)2͔W7*mPa6_68.,ڰمƲDtfϝ[ۇLai[$~o o-2IH:i6ȋQ!agsugl(m=¦j!"?D3DgV]7WGu ef%g$*U~*ؗ7;((-q3md2c Z’ZDqkPܛPPZκi?Vk;k뫶STޖZqּ6,gl٦?@LZsdf&-Xgog˄>j%[S^Y;D !ct3_&=6u|__7D?=+)@F.N{3έFLliRgJ:UϺ{ S8OyC'X66U<\H.xv!fvfp:M/ZW̮S䗔 : $IJ+*9_[{F:_iE8d,wsƳCGo;©RrX]B}qrnnq_8Fy!UKUm;(4sLV IDAT"U{Rhb?we@9rcd%oh LfZ˕YZf:ŮSD^pM=]8Ys|<7;nΤjI&͙|j޴l"|2ݤڝl %lmlhG:8jiE<~굱a[r1a~Ʋ0?&erziM=]8o .* Ƀ's q3-.cXLF%gjWDDDb\>5 J3'r }_s/n|@u Iy4y4q,hbkvv*dgGiE%wDrgL$f KX( cg*)ǸS8N';W׀v͸U0Վcdޏݦ)Oiܜ!O~=@YCF5G2j9TSD 341c@Yo7r3lO"&ԏ>\lPViс2۝w2|A.ܟq=1燝z^CIۚ\i[{ϥǪ60iٯӝytv6[\Jl!bl))Tk&&J~nN\.d@qyJDy2s$_8z=@""""U`GJVCzpCDKA!>tjÌ!70c zҹم J.0`U-׍B,i +ݜk,?]@uOƓJ+* N p}tk`,u,,ۜ R=L/%~g? U16;Il1a~l:zƺic<8dt6T9sjmr-vfhm.nnŸjHf>{4ɵpg|'O[y`gc +62iٯLZ+3@ >M2_*::΀j<"R*,'j5OлM0QA^l pw&ם}_R^mykokKAM9!=ʍ쫆&:M˦w`8hoG\Ty%ưG q`M}()?uuٞEs?"}U8Gai9;GĄ3c5)FG5& E?*^i䅃-^MiɉYDDD`WLMzG!>IpfǟV3[,L^f$$e֘(_RNbZ6:1j)*u{1[ۇWR{=z>]?3zp,چ0XKd4lmmq} ܝMU=)9>م-~>p^1~qmƟonGYa(r>)ˆ l@z.7:[祴TZ,1'lp>E};Ra=9ow e*؟?3ZOTcݜvSȧ/*{oAQYۓDDDDbAA"""""b]g~i-W,)((((((((X,Kc8==1+"""""@@@@HiiicVDDDDDGGF9`(((((((((7vջwo6lPkرcyg޽3|pJJJ4wssc̞=ooozM7ޠcǎ >u֑ɓyי7ow=zf͚{ǩSh׮3gΤ{V:"" c޽\t[ҥ <vtЁaÆaccc#88Ҷm[رc53x` Xz_``  "**Jg{d^z J^(++c$$$`6i׮wy'L4͛#PZZʳ>Kdd$qqq{5i2x׭z8߿#͛7gСzoc=z0|pcٶ>|8_|Eǟ:u*SN'?-[2j(/LZ2m4233gDDDݻcBBBx(,,dڴi 2{YǤI;|ܹ<ݛzaÆg|||.i'N'44aÆ}v.\+DEEѵkWquu[n1Ʋyf}yZl C 1/#gL>&MX[l1HJJ 5l۶~ȷ~KNNGTT޽{s}!<<[[ ~|M{93f@ח~gc2iݺ5mڴaٲe~rbHϞ=W^0qDf39{DDD;/H @|||XDD֭b0f݉^`͚5F%::h~W}}}ܹ3/"W6d2z6lll0L~:tŅr>3ӍIFjj*$$$ɓ'((( !!ݻsmPYYW_}EJJ ]ta׮]$&&ҹsgvލ-iӦNtt4aaalذ^zU2T6N ݉zMl6c˩hKxطoUIUV$&&۔Cjj*v2>0PTTĚ5kpppiӦuGaΜ9~~~ 2Y<]= ӟ?>w}7<+W$''q强}4h׿psscРA_WRRRj,9r$={Z/u=gOЪU nl.]ʑ#Gx'e}Ȑ!r-|g/ lwÃ#GZϚ5k>}9ۘ5k :Xh"k(((`С۷uNbb";wdرu e@xIIIwޡu nW_}ݻsM7ѤIbk:[]|ggb`ccsQǭ r-X,mvIs>뇳s+O?/rϟl n~ҢE?ke@lll ooo8p@է&MIOOё7xlݺoooƍg|-/’%K0~ݛYfQPPSg۵-]t;C˖-9|0wuݺu?drIfΜܹs9p7Zu_ 7#7np뭷2|N:ELL FbFƻヒ#-- oooԩS 裏@zz:YYYUay~"""3f wyL&0o>>x{{=<<ϸ>eeet҅[uVtb'OZ"55,cYuiii,rrrOPPPk\W鄆 /__5(--d2Z;::R\\|mDDDD\Ņɓ'3{l?ck[))),[ ;;;f3k׮e߾}}mxGy퍣#wf׮]fcɓ'IOO'55__߳.;???rrrHJJ2n<lذႯQPP{%==prr";;LGũSfee%:t 99r"##Cq)//'??ՕxdddpĉvIJJ"33w}۷ckkK~~cT} ʋ-uN_j̛7'_MII ӦMcĉDFFŔ)S(..8::2j(V\ɇ~Í`ʌ3'66ΦM6HOO駟w8ロcǎ/Ӯ];nbbb0ͼ+FF>}prr⥗^"33wupp}tܹP>}ɴi(//gРAՋ }Yۗ0OΊ+(((us]\f̘ApppSWqqq8;;3}tlmmiӦ lڴ[tNJJKbXY]ORkY~=:t`$%%_rϩ[Ҷm[|Mڴií^Dž(,,&22jdڴiu:Uرc]_Eeĉ9s _~%衇8r[nmǹꫯgˋ^xѥH3VZZX}aɉ2U$Ro/|ٌljb0Z$$$\Eh`u O8 W!K *r' 8:{ҿ,4W&jmnAI}EGGӧOf36m"--?uTRSSIMM%==THHH1|iYEر#[l}˖-tN9f-M֊<*~,!䧴6] E/RSSm+++ρUɥ o߾PQU IDATBj %%0QEXv-t8_2f֬YSulCxx8AAA~AEEE馛HJJbӶm[Ǝ˴i0 kٳOOO{bccqssc]nF}k޵ipe)< ';a)9RFe>ZylDZn/l߾~z|}}k]\DUmxZh*SN#^=_&ooobcc}HmE g˖-`cy?>;wf͚5̜97r- h4zb0c  G}DJJ g&88ѣG~yON֭yXp!>>>,Y:͛Yz5QQQtԉ+Wο_NNNo8,[_c !F y{{Ŋ+x &**}!4woECbL:A7m*6Kү_?G L eeeu]} >Vw})S0|pϟw͗_~=܃[rJ\]]j=fX"88WWW:tILL>}Xv-ӧOg0z, ow~Ν;<#лwok>s>:5\×_~СC^~(R |RQMNn!""""Ҵ5ҪU+FEBB]t &ҥK',,iӆm۶իYziӦP{ŋb >S?\~￟BGnXpbkcqY̙C@@Xz5M7ObVPYxV}ށѵ3F=%"""UӿG,EnnnO8tQQQ L,eg}#C+;]q k;iE'p +6\\Zgym!"""D(xI?o+"""""v""""""v""""""v""""""v""""""v""""""vmbbb]@ -PddKJrt """"B]U$>>%H#VEHˠoQQQQQQQQi威5Hq VꈁKKK1...W`((((((H_c{AAՈ!KwСC3f |3AFEE\mĉr- 5Vׄ ظq#3gΤk׮ 67o_婧Uwȑ#ذaqqq<ݻ>W\Ƕow}ǼycŊq=֭[F[֭[9wcƌ!&&ÇӡCLٿ?>h5жm[}Yk׮ 6 gg9}t*++yW馛馛psskTL&7nՊ`hԸ51 $$$0rHV+Wd1b[k 1EEEڵ/ Ν[oE\\cǎeΝ8q+6DDDDZ&@ ^^^xyyӧ9~8ܹv9sxwHOOҥKY~=x{{3e0O?ĦMd 2+VPXXHNNL:z낶͛7AXXt҅'O2a {Gbb"fL,Xի9~8L2 */// ::2qDz)eȑ]xl++WˋoUV7k׎-ZDyy9O>$MAAmoy;FΝmڴadeeaX7o|w5_ZZ;yGظq#`RSSٽ{wmU˹{ׯ;w޽3gҭ[7;Ç?>z"--߯_?ƏΝ;m$$$ロ> ̙;vP\\Lxx8v'''OZJϞ=IHH`޽ocܹ899eX,̚5 WWWmFjj*=5}]n6={6渦~oNQQҶm[tm̙3য়~"99n֞T!"""" ȹs={6/ݻwٙ~ &M_Oر#Ç@tt48Nnn.уݻc4mWWWl75VNh߾=eeedffE``mk׮׏ŋs1fs@`` V2wNv$44:t@JJJ}deeO ၯ/ڵ8HHH !!0۸C׮]III!;;Vwzz:dgg[yϏjs\Ss=ǀj2Jy7T5""""ry5Fxx8<)999O? &bѣL0Cbb"O>d7ޘL&<X,Izz:k׮ֶӧm?~m۲h"4mۖ3gCq_0N]󞑑AFF푱*5ӧbʕ۷ȹsl+7Uʕ+6l={j+D""""r4b4qxll,Çꫯ())!22pdSTTT8&|?w}qE<%%˗H߾}kmMhh(gŊ$&&JOOg899ѯ_IJJ_{ 0}bXxWm?tP\\\XlYYYuiZqvvgϞӧaÆŒ%K(//;dȐ!TTTa6l,]-[PXXx8u{~~>˗/l6W6l,]@hh({&>>۵\s O ..p^~eN>""""WjZ1pmB555}RMm?УG>3Μ9Ü9sDu*--%11뮻7|Pƍg:zF\YYIdd$3gDFFcѡCƔ,"""Ҥ8d&TVVr!-ZDRRm߲xb;RGC|67'N$66=CUB"""""iDDDDDD=V.R߿%H zDDDD*r;xG """"W*呛ۨ###LՈHSH~~~wW")P^^^ .&&""""MZ~~> 5?F+!"""2xzz:iF-X"""""b7 """""b7 """""b7 ""bSXXȖ-[9s&}fHDDDHIIa„ ڵdVKfH߂%""qFڴiGG#""͔`20L.Cwީɓ<[C]cVH,ݻy9|0r-Oѥ]vǏ端刈HۀQu7zll6cZ1 XV."r\{@nj3ڵ+o&/dX,G' `->%{̯$d~}YiylfӦMK=ɤJzz:Pcp"Ҳ\7o矧wp ,[={wzXֲ4,EGVcݙ[8ݙFiDzz:۷3TPQ)))u/"-UVYYwf…;t̙31v믿ίJN9s&^cϞ=xzz3˖-/M6L2e˖ХK_֭[dĈ̙3777ʸyذaIIIݛŋòe/x?vd2qiػw/w}qX|9mۖc2m4rXNN%X֢c`pki2X˱;F "r5 iii6j[٨iF}ί2yz=Ui"66׈ TTT`6/8#/30}tʎ;3g֭K.߿vN֭!66^z]YbK,a,Yvڵk;w.DDD3<ôixGOݻwd2QTTĴi cڵ:uE `X1c ⣏>"%%ٳgѣ/ysrrj ! GgK'!`[ ""Wh^ɓ'7hM!W^z9ifR^^@6m<禛nGyD֮]˂ ӧ<획72enV^xƌ@rr2۶mwߥ{K}To߾?~2ѣ#G`2l 4477j}VZpbbbxmujokMMMFWWW"""3f C%..:6Ua<7*>2OQRy班XgoADDDD nݚSHzyyDEEp>\6@TVVzn41 d?8lժŔ0|&LAjc3f|;1b%_{K-kv늧ݺ6 H]Տ`9991d6lرc1T[Aҥ vmBB]|ڪU+f3$,, f)))رc\{#GP\\l{}1+Vh4SOUk a˖-Ve݋dee=uL4ɶR\\l[j,EG$ \g(=|^) sh{Q}H\+ 3fٳ̝;pa6lɓ'm~a[֭[GRR֭cΝ}:F#F0k֬KbOWk_jaaakDEE1i$xGy/Ãŋb >S?LjjNX+z,bhAD4{k/Yiii߿?B\'''[HHH#..777q%T}}~!&| "**}5ʜm`0FW4|ʉ#$tr8dܫ~Jyw8y$SLW^y!C\I0{o^;~e{HqR~a2xxꩧ _ttŸ́|#""""-V@jIDD.EDDDD ؍؍؍؍؍qt """")@.ADDDDZ((**%H uUxG """""`ZVG \ZZaEDDDDpqqqȸ,V877QCx`ZYDDDDDZ=%"""""v""""""v""""""v""""""v""""""v""""""v"-R^^^X,;W#"""r8/E\\֭Օ#Fp=n:ݹ{E8sLI&1dȐؚj`̚5]?'O2k,[[ռ7;׳m6MFXXX}ӓI&]p,66;vp:xWfwy???ƌSxsqzj;F`` SNСC۷QcH͚tpwwgܹ9s7x뮻[ogg? [/Ugck.ggg֯__5VШqkb0ؽ{7wqV{^qcȑ^5B[^|E+7oo[ne|;vkJ 7ѣGk}^x~4\\\_{Ǟ={'hooo{?E IDAT6n܈bᮻb+;wl>}:m1׏7x]r &N7̚5kHHHl6Add$=zO<u駟ĠAx衇x' חѣGoM'|b[7wy'o&̟?rfΜ @@@iiideeEEEZ#Gc=j7 77Ʉ'%%%Z_~.]O4O?m۶L0~V+Vtڕ=zpQ?NPPխ[7뮻뮻ZL&[=oQ= n&N_WZz:w /EhӦ wuX,.\3;v쨱{=Ǝ˔)S ((HRRRJyy9Ǐq!~gyzѣG_Xh}!55?`>lK.7󟄄0w\Ϗ?Xc|_Z_Ϟ={ؽ{7 \,X7of۶mX,yjRll,)))DDDPRR5f^x3g?ۮX7oӵkWn6[8}4f&==#Gо}{N:U!"""" HAAO=/zٙ]vɓhݺ56lkaȑ7xLJJJj'''rzEϞ=1P~kcǎ7;w^wuߟ9B뜇ӹsgV+???֭tؑgGFFck]v槬~gC~l$##|}} ٳdeeڪNMM!++zͻ?o߾oZZ3|n破jGΝ?>:uO?ίyH4ҦM͛ǫʓO>Ϝ9ÿ/&N8>Oqq1wf޽̜9Λ|\\\8pb --gҮ]ZjGnn.IIIcǎβew}/qڷoO֭!33 JVVuYYY_ӧ)//'$$P۷ɓdee“ɔSPP`;m۶<3L|.\HQQw}7III!"""rVE4e5}RMmk.z>;w(Nٳ=znݺq}ٽ(,,_'$$Q5WVV`y.8^QQ x'ԩScJ4栢0o{w#w^G$""͌ʕ+Yl۷o[ogѥH3"""9s~;pti""ҌkxEDz@f*K]Sy<--?vj|iY2qDN8qA%K5j'dĈoٳ,] b4jmԛK=l6c6Z V+ |i92a„jm766E}jcu֭}K,E' C+F' R cns-} 4o̘1? 8H=DGGӧOشiECť?uT[P1 PR5=_DZfZ^^^UvE7n:t"} XKӨ,<"ʼV4ϟ6s([~Jt*i>}:wǏwt9RUa"==۳i&.SN%55 p0QEh+ u9}4QQQݻ>?k.ΝO?[oEQQF'<ZylDZBfΜwq~i~Qjb6k\٨U~q)aKMlllM EEEL60֮]˩SXhGΝ;֭[yW9s Xx1[neٲe|nݺ1֭[Ǽy>}:oX,f̘A裏HIIa3zh{NE? #X2Z5btit"ri6n9y$k֬{̚5ˁɥފŘ:uj6oUm_~.Af@ymo6.\֭[)))!""DHHY@ iӆnݺ~z>СCqqqq'2f&Mğ'*++1 DEE+:t`$&&6G5 * &Va\;{2lK*PXÛ o;iOHGyGyeHRV>DDj,ĉmBoӦ G{L&۱}+PTTtՎ]6IbX(-- WWW"""8prw6=CUqdV6(|d#V3N5if@6_& ''jm+ZSYY3f஻"""www^x+:`QYxk ɭ 6]ܯM( ^f`ł#.~j6!!!lٲrػw/~~~xzz^l?uL4ɶJR\\klN* `->Nm.kpo7:z4ΕeŔ^+շ4Nڌ5 ŋ9y$qqqZI&]Rܹs&??^c`2x9x ?عs' };͇ kZ -CDDDDf@L&m%bҥ<<×t}XX7x#s!>>^c{xxxb1cVXAPPsmݺu1jԨ lݺsq̞=tϣ>ڨ1DDDDfM:mۖg}dڵ+Æ ٹ}N>J^}Un&n&Ugck.ƍkjb05nM  9Jbb>FA֭/BLQQv>v|ܹ'Nr* ///?~vi;wΜ9;qҥ_D2e F\]]য়~bӦMTVV2zh Š+(,,$''@N[ouA͛ ,,htɓ'0a #11Lff& ,`?~Lu_8q"O=2rH֮]KNNaaa<|g+WΪUHKKڵkGNN-'|Φ@۶m7ǎsL<Jtt4yyy8;;Aii)1119r`LRg\Ӽ۷;wҹsgq ڴiý[c1118qknݺqqNիiiiu~1~xvi %!!!}|G1g|8 ̤qrss)//Gth g9:uD)++#33///m/]v_~,^cǎa6뜇Zн{wڵkGff&ӡCRRRj#++`|||9|}}i׮%%%FBB ((vJJJ ٶ)((7 ;;zͻ~~~TMOO'00???{9 Pa2A*++EADDDD.&@'22?%'''駟2aX,|w=z &PRRž={HLL'&9x 233IOO'55vV???rss9}ǏӶm[-ZD~~>?Cl6s9{,frssʲk׎SN]chJzə3g(//'((Pf_l=lח)((·m2sLz!n Ʃk322Ȱ=2V~8}4YYY\}a49wmjb|A͛ǟgx߮hϯMⱱTTTCxx8櫯H ӓŋS\\LQQQL&|A>s}]ƍ/))),_@[k[mBCC߿?+V 118Wzz:ɉ~5xƏORR/2ݻwg«jC²eʪOՊ3={O>66 ///,YByy9wy'C  a dҥlٲ ƩkY|9fapuueҥ BCCݻ7$$$خk8}4p~nBӧCDDD 1XV#QoRb~zg}ƙ3g3gΕ(N$&&ruoʸq^GC[oܨ+++d̙xzz^pH{1:tИEDDD4W@J:ĢEHJJWB[/^7wqCh?\nFĉСC>DDDD@ZfODDDDDDFDDDDDDFDDDDDDFDDDDDDFDDDDDDFDDDDDDFDDDDDDFDDDDDDFDDDDDD즕 Twt R^z9\5.A~#>>%U ry6H.S5"""Ҕ%&&_k<<<ݻHv -WHπu?|@kJ .A} ؍؍؍͆ X~=yyytڕYfѥKG%""͈V@DD]vf-[Ɨ_~ɠAptY""(<쳄mƯJYYKfD`}7ҿL&ytTL&>IBdAAPA۪ҪmݿjkmSZ,Uu_jkV ETAY¾Cdd $!lw|9~>{׽{m믷:}ܹNw\>ڟ9sfOt~K ^{5i$y睊0yp{=~o 뮻(/ v9h[:RRd,$0 mܸDꫯ$'<'x”L3 Ҝ9spEQ-S K YBOKTPP!pv{xGsz-9`6x5l0h… mܹbX"//eNt~YqH||z޽{?/R;v~I?n89M󦦦jƌ'Og`x -B2|ffbrTfBamڴId5gСCA mq$LJwﮅ rΝ|!I ':?㬸m$8p@Zr}Yegg+))IvN*I~T޽[z饗4~xu]Zp4|p=#zdEEEoٳ%I?t:_JyСC9ru릪*ozK$i߾}u)**J3gԭ*ŢgyFѣ-Z$á[nEf ?Tii _iii'ZVVVvBm fd %QUqtfz7SO)))I}pdaqF[ĉΏtO?=eH}{߯ &hŊzJ IDAT4p@%''KVZ|P6K=#?9sk+e˖i0a]SSg:裏j޽z4~c5vX;~3.$XB7p^|E-]T\8_/|Pzҫ;S 4ZVVl17|-zYR!bjZ @gwWHv;dŘ;wIݴp&m446CBBGFF>fO>M6n1ժp=CںuUWWӧ扎>zvhhl6jjjTQQ 6,0nkРAڻwo۪5GBH‡II*͕Wul_2_\{RkٳeXԥKEFFڵK/#hĉ3$$*m{<~+C):::p M7>O^Ċn-5>Ds&E"I$b Js}txuffz衋.HҩOjj{=y@Hov\\騭Uxxq92dk8p@}v]pimnɽO ]p8veFe:BihNӮ$\/ŋdIReee0J=a!LLLLXM>=pȕӧ+6660_ttt0J=ai|/bjeZeX]=&tIKK >^\srrrxntt222a|AZVVVfZݒ%DVG2*77޴ժst3an%P~~YB"duHaLgW@ !l4b~(S$_.D-f,aI'^$I՞2ú%_]`@.]V4c }|_zGW_ ,{u7ߔ$/ /xԤVKLX3H f0|'>$&WǼoh~qMIסȃ>(-I}!IJHH?h]zѳ1119s$)44T袋CBB]__j_g 3ÇzfGHDoY#tI}rkuy] eUa]suҵk&}3uDDDN?]/5imz\)$Ietx[U)Uݩ?}Jmp:p-x;$"70%[0Y,!Cvz:0/!BY{1!;*RblYC0knP…pmCAXx@= LC`@41k @:{,%"tBv :TYn]Kp ,a.@S4!0 i LC`@ 5%&&_aF0: F$_`0 i LC`@4!0M _SoyP_봷a.=޲f_\yĺr}ɷM'?"}7Η׆<*-e^WH{hBy_c;՝ yFe$Foݦ~]iիSbBm΄!goIZa쪔vۣ_\o)G۽*!;O{8u+ 'FYej>y}`Ӫ 9yZii0fYZhPn۟ːt>hHi%oح%gH]<I:TV2w2-}Ir2ؒq}zJwԄiڢlWîsze$dqep JgWz8}KyHZ (aڭҦ}JrFGSGkXZzVlϕ!V7L&Izc}~bt߬PgeTi~Cu\?cLvlZb(2,Tל7Xo.ߤjٮp{p}]UI_Qz[|%kbG"ᔑ40I?yn \Uz9fnګ|QnoKՏP>ܥsWV=n zGCus5_jk/nP^Yק<8mW3|5{(=1^a+I3 Ў_+U~]t%&2K6֫egwΖ$}c~r{uE4eHFCGPi;/(-\]K+ϗ$y<>xVm_Skߐ(T.ᔑU/Թ}3'*jiqߔ*a +r~~'v4lQ{)wt^ 5SUz%I&6>ߒ R5>ߒx˾-f~릪U[U ҍSF@Iym_輾=O}{KV{P3 kw\:o>BݜzkuE4e*W%/԰u ӫ_׊͒^l~݋ghef$Uil=+[ݶuzvh2;g?_wҺ骱[}/6?{gLГVoTxsx3@2 mWV>0^Yj^9pjs`ZBte~vzrJU~nڪ*=wL8ev,,+ֽUJB^,I fMTjg |H )Hس2)ty]4~I `Y< W~rVkQDXMQۣ?uY-6RJvkwcC{%ꜤF84"IQr|"5 Z7kڜ& WîVlD#t^B4k̀X=LYK݇d/w$79m֘sRNחr4}T?ܥʪk-6iy}>=/T\YzOGjY[\nEۛ OITQEjwZs@o-IƜr6$ۯRblqۓ_EkwO7NoP{?,Ԧ?]ZOq컪J R#y"*q7ێ32iG4v5nuhv_]s^,S7>K\WM^I-reD#>pVP^ .mQv`}Moi؉|zbJ͝U=I/7%w/jsۭd} PQEWBphӭ}Pǫz_O5(~{ݴ3Z:S!'O>_}ow]=y:TZ[T^㖧ާov6r^ߞZv|~C%5~> !Vdjf#"vꝬ}^^O[h}!߿3DžԷz]-rSӢ;4==Kl"tوmzUERmWoܣ*m۔_Vz_gh馽)t߿ܨ l?P}媩߫j9=|9Kz޵_5Wbm\zwVy8;>_?mɗ-ĪH>jSy١N]Z=W~fa=AqqHjr\ZhH^KnaEkwH62;ڱO>ygFбu+ ٚ:w3Q7Eo}uˋ^] F*y%-D^2V_\YtrUZWן?F7rF8&LK߭y.SG \j%ifo>aH<{uMy٦I5 ʫu>_kyuEwPy[cԏkL7.(Ր^Iv'V1AamWs -N=&I=TvpJjթ.?&'[l~OӦ>wُџTFbs,FwQ3ZGvTھy+pk&WE_GB#H?NPWwUە5@'t\f҆< vßo^9a':&>r:Z Jr֨WjfOoh˾l^C9'E~[rF:tό mzYtzZ!`85 p#0 i LC`@4A%tN/111(t> `@4!0 i LC`@4!0 i LC`@4!0 i LC`@4sϧ*y<`լVv:,a󩬬L b F@`zR^^.Ir8n$)666(ǣ`utJaaaxq=qz}qcDG叿4C*k_!H.]qY|KNIeK@'і}][MM}ٜ}`u-.v 튧(%,mѵe.5!4!0 i L)yKhZVee5=hP?K S Wr4To#N{?]7vZ,wX%ƄR;躱?;}IDAT}vNX=0})*wXEmAm~ԙ)p":޲2U+I*߸QVCyD\ye+Kj@R!+[+"إuzt惥* v)ER-:\nOm]Z5G뼁VgX-ʝʯQJ\E+s_NwpFꭵ{\+1^NeXv>=x˚Wj/xs'k}I>E\?4WwsՔc㻟j-gnڡN{bVDz$phjƍW}イ4lxϞkeO$ _4geִ,uV#!g]-۪>Ժ"O].ӛ7YqaMimeizyvԕ҂]Z6$%^]c?~@ vxϸH%0}Ӂ}`;l!VGWkϡ"]?x]5W)zp9).Fi J_l-{Ml}`%I{545+|H ȷٻj5h|f:io4YphoO׿V޳GG[&+t۷}۠x]<0Ea: *h8ا&NRͪ wc*Vw0E_SqQzuK՞VG#Sމz҄H~z0=5uNX]>$UqaWZ2T^9fZ۾}O4qlNҐ٬=0}~zN׻oԮ:BCZ[1—۵H-XI5 ]H*%Ila| JIwi~Z37apզ&}5BYq0S}eBccHJRWh}j.^ܗ_V3Y.Kev͛\-X/PҕW_W']FpE:vSr~4i#hUV7X-ڰD{rxm:P"Ðf K3®/YVԔ~J}Lߓ$oSO.ݤ'>ިdg&N Lew֣Hcu -ޔ?~QnsTףU;uрJ$.ժ?~Qff5>&HK+e[lm#%_ғK7{ll8;kx#'e[%ZSV.^ZOXK8z&[ttXKKVBCVE?_JL}nGУWkє-[4eTxJl˔) ofޓ@ɶh~w(]{n+a/ibaņەS\!PZBtӆ%X5g>v@nOx$pp؆۪ԫS=d*#L]#A{ ˵-Ln/pE96:VQ[]eX409NKWUWt$ܐZ7_5t{MvAXk=*(R]B44%Am٧:*j=h~=|%KW]O9%t{5ʡ.ъ UkˡRj=rtXΊyԷxf=p{geHɤAsMO wToӶ96폷56g/Ӟrڜ"X5:V<>>؜Jhh{Ԛ]B;]َsLpkmdaPx),ЇY CKњ]/oҏ.HciBWJ;h\9TVU޻hx?-ߺWGݽ>^zV]iqD{b:;[={ʨW\ѰGfpɬ|FmOW_Sb_':?iRm9XhGƦ'ɃϷ-gDf MSL1,2pP0U:j- l2ciC,vo4mPlŢ#3."g]VEa6Y-U7N|~mtDC~pŢjw`Zi[9BCZ!ur6?&.[QMZ9W]b4}h/uwFhVKt^ ՠ@`lu4ҖϱmP7sîҪWۣ0Uy[D5aW$~3stzcr{mjmD4^Pj;C>e2SuGO)߯ckx$Ia6CCն@ݮjֽ[R֜Z*j=cEa{ZO0EZu;-X؞אZ6xZ"Hܒ*jl=-gS^Ѧ%z{VhmN?rQJ\tӯFI__.I-(co}6e$%FS48|"^:uRqy=\gnk~_#)ŢumڳN;t*W/J2mIR}*\D\qc(2#CCzJOo,"4'շb ӘwUҌ$%[t&MRyUbtKvB-V]c•Q&TB5>axZUy5WWm@|~ChTm hJ5#f6 '٦[_SO3,}!ͪiXJuAdY-m ,`e$*nSբ!=B&ۨ%>}źtPO9BCiSy[<>%D9܇М*~MC!Vb9<]1{˵7Un.>C5gt5ܷreb]دvms6(ր$eHc5{d59pND9d A=URց?0SW|Dl!'vUXYQ%F(}uvS۸G\fOӷm߶~O%ޯ]АzRۖӭᄉ#4DK>MJ5c]=afr]:zٯjV֨F%Zk.;*m=ξHKm?D4t+ nQnQr֭ӚkUŖAzCKX*s}cjܹB~[| IrY5Ֆ{U%~+vA=u NݐM{ 7 }UZEaƆ#ՎrtT3ulVv滴jP|uf//j),or%Z!'pf'0eCRuTsiwaoR/JGFOYSHh}>5IUw9lH/;mB,m^[S3]>J^_kr UR{aS.y}[T4?3\/ַMc}noW6Zۏ+kjtE`Y^\]~} +2̦ܒJmNr]ns~8^\ K48ʬx@y}M])՛A=u>mcֶo[mJҮ&_\:B}ժ +k,]9,Mv[2sڼl(vM./oh"xtٹǫYm{ugÇk.Gk܏s/3zW#HѬ@Ngg#Ns5ߤۃ]F`CK@'ˇ:fZ,+DWԼ2;s_s'vGon9\.[ƙi`vǓ(/.CS 8t!X8*^msǓ*eUzs`vjٮC? `@4$K`Юػv v %* mѵe 7%>*"%@ƌ v s`[]:kڲMaB%ː`pR:dI>v!46V2|˴ 3άP?#:7]D馋b1 Fr:'|]~\eW[\|+BtQܘ1J>%%ͨp{t!eT>,fSzBOV#MTd!? )-!ZN`?/*+|MYUZUs+  衛..1'ݎRbbi:lpr@:,i LC`@4 ZZ o aZw"h=v{SnZA QQQzr\ 00vz:,F}>xUpֳZ튊RHHHjй,!0 i LC`@4!0 i LC`@4!0 i LC`@4!0 i LC`@4!0 i LC`@4!0 i LC`@4!0 i LC`@4!0 i LC`@4!0 i LC`MIENDB`django-ordered-model-3.7.4/tests/000077500000000000000000000000001440510626600166555ustar00rootroot00000000000000django-ordered-model-3.7.4/tests/__init__.py000066400000000000000000000000001440510626600207540ustar00rootroot00000000000000django-ordered-model-3.7.4/tests/admin.py000066400000000000000000000035151440510626600203230ustar00rootroot00000000000000from django.contrib import admin from ordered_model.admin import ( OrderedModelAdmin, OrderedTabularInline, OrderedStackedInline, OrderedInlineModelAdminMixin, ) from .models import ( Item, PizzaToppingsThroughModel, Pizza, PizzaProxy, Topping, CustomPKGroupItem, CustomPKGroup, ) # README example for OrderedModelAdmin class ItemAdmin(OrderedModelAdmin): list_display = ("name", "move_up_down_links") # README example for TabularInline class PizzaToppingTabularInline(OrderedTabularInline): model = PizzaToppingsThroughModel fields = ("topping", "order", "move_up_down_links") readonly_fields = ("order", "move_up_down_links") ordering = ("order",) extra = 1 class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): model = Pizza list_display = ("name",) inlines = (PizzaToppingTabularInline,) # README example for StackedInline class PizzaToppingStackedInline(OrderedStackedInline): model = PizzaToppingsThroughModel fields = ("topping", "move_up_down_links") readonly_fields = ("move_up_down_links",) ordering = ("order",) extra = 1 class PizzaProxyAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): model = PizzaProxy list_display = ("name",) inlines = (PizzaToppingStackedInline,) class CustomPKGroupItemInline(OrderedTabularInline): model = CustomPKGroupItem fields = ("name", "order", "move_up_down_links") readonly_fields = ("order", "move_up_down_links") class CustomPKGroupAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): model = CustomPKGroup inlines = (CustomPKGroupItemInline,) admin.site.register(Item, ItemAdmin) admin.site.register(Pizza, PizzaAdmin) admin.site.register(PizzaProxy, PizzaProxyAdmin) admin.site.register(Topping) admin.site.register(CustomPKGroup, CustomPKGroupAdmin) django-ordered-model-3.7.4/tests/autoauth.py000066400000000000000000000004451440510626600210640ustar00rootroot00000000000000from django.contrib.auth.models import User class AutoAuthenticationMiddleware(object): def __init__(self, get_response): self.get_response = get_response def __call__(self, request): request.user = User.objects.filter()[0] return self.get_response(request) django-ordered-model-3.7.4/tests/drf.py000066400000000000000000000024241440510626600200040ustar00rootroot00000000000000from rest_framework import routers, serializers, viewsets from ordered_model.serializers import OrderedModelSerializer from tests.models import CustomItem, CustomOrderFieldModel class ItemSerializer(OrderedModelSerializer): class Meta: model = CustomItem fields = "__all__" class ItemViewSet(viewsets.ModelViewSet): queryset = CustomItem.objects.all() serializer_class = ItemSerializer class CustomOrderFieldModelSerializer(OrderedModelSerializer): class Meta: model = CustomOrderFieldModel fields = "__all__" class CustomOrderFieldModelViewSet(viewsets.ModelViewSet): queryset = CustomOrderFieldModel.objects.all() serializer_class = CustomOrderFieldModelSerializer class RenamedItemSerializer(OrderedModelSerializer): renamedOrder = serializers.IntegerField(source="order") class Meta: model = CustomItem fields = ("pkid", "name", "renamedOrder") class RenamedItemViewSet(viewsets.ModelViewSet): queryset = CustomItem.objects.all() serializer_class = RenamedItemSerializer router = routers.DefaultRouter() router.register(r"items", ItemViewSet) router.register(r"customorderfieldmodels", CustomOrderFieldModelViewSet) router.register(r"renameditems", RenamedItemViewSet, basename="renameditem") django-ordered-model-3.7.4/tests/fixtures/000077500000000000000000000000001440510626600205265ustar00rootroot00000000000000django-ordered-model-3.7.4/tests/fixtures/screenshot-sample-data.json000066400000000000000000000030421440510626600257630ustar00rootroot00000000000000[ { "model": "tests.item", "pk": 1, "fields": { "order": 0, "name": "John Lennon" } }, { "model": "tests.item", "pk": 2, "fields": { "order": 1, "name": "Paul McCartney" } }, { "model": "tests.item", "pk": 3, "fields": { "order": 2, "name": "George Harrison" } }, { "model": "tests.item", "pk": 4, "fields": { "order": 3, "name": "Ringo Starr" } }, { "model": "tests.topping", "pk": 1, "fields": { "name": "Mozzarella" } }, { "model": "tests.topping", "pk": 2, "fields": { "name": "Gorgonzola" } }, { "model": "tests.topping", "pk": 3, "fields": { "name": "Fontina" } }, { "model": "tests.topping", "pk": 4, "fields": { "name": "Parmigiano" } }, { "model": "tests.pizza", "pk": 1, "fields": { "name": "Quattro Formaggi" } }, { "model": "tests.pizzatoppingsthroughmodel", "pk": 1, "fields": { "order": 0, "pizza": 1, "topping": 1 } }, { "model": "tests.pizzatoppingsthroughmodel", "pk": 2, "fields": { "order": 1, "pizza": 1, "topping": 2 } }, { "model": "tests.pizzatoppingsthroughmodel", "pk": 3, "fields": { "order": 2, "pizza": 1, "topping": 3 } }, { "model": "tests.pizzatoppingsthroughmodel", "pk": 4, "fields": { "order": 3, "pizza": 1, "topping": 4 } } ] django-ordered-model-3.7.4/tests/fixtures/test_items.json000066400000000000000000000017001440510626600235770ustar00rootroot00000000000000[ { "pk": 1, "model": "tests.item", "fields": { "name": "1", "order": 0 } }, { "pk": 2, "model": "tests.item", "fields": { "name": "2", "order": 1 } }, { "pk": 3, "model": "tests.item", "fields": { "name": "3", "order": 2 } }, { "pk": 4, "model": "tests.item", "fields": { "name": "4", "order": 3 } }, { "pk": 1, "model": "tests.customorderfieldmodel", "fields": { "name": "1", "sort_order": 0 } }, { "pk": 2, "model": "tests.customorderfieldmodel", "fields": { "name": "2", "sort_order": 1 } }, { "pk": 3, "model": "tests.customorderfieldmodel", "fields": { "name": "3", "sort_order": 2 } }, { "pk": 4, "model": "tests.customorderfieldmodel", "fields": { "name": "4", "sort_order": 3 } } ] django-ordered-model-3.7.4/tests/models.py000066400000000000000000000075341440510626600205230ustar00rootroot00000000000000from django.db import models from ordered_model.models import OrderedModel, OrderedModelBase # test simple automatic ordering class Item(OrderedModel): name = models.CharField(max_length=100) # test Answer.order_with_respect_to being a tuple class Question(models.Model): pass class TestUser(models.Model): pass class Answer(OrderedModel): question = models.ForeignKey( Question, on_delete=models.CASCADE, related_name="answers" ) user = models.ForeignKey(TestUser, on_delete=models.CASCADE, related_name="answers") order_with_respect_to = ("question", "user") class Meta: ordering = ("question", "user", "order") def __unicode__(self): return "Answer #{0:d} of question #{1:d} for user #{2:d}".format( self.order, self.question_id, self.user_id ) # test ordering whilst overriding the automatic primary key (ie. not models.Model.id) class CustomItem(OrderedModel): pkid = models.CharField(max_length=100, primary_key=True) name = models.CharField(max_length=100) modified = models.DateTimeField(null=True, blank=True) # test ordering over custom ordering field (ie. not OrderedModel.order) class CustomOrderFieldModel(OrderedModelBase): sort_order = models.PositiveIntegerField(editable=False, db_index=True) name = models.CharField(max_length=100) order_field_name = "sort_order" class Meta: ordering = ("sort_order",) # test ThroughModel ordering with Pizzas/Topping class Topping(models.Model): name = models.CharField(max_length=100) def __str__(self): return self.name class Pizza(models.Model): name = models.CharField(max_length=100) toppings = models.ManyToManyField(Topping, through="PizzaToppingsThroughModel") def __str__(self): return self.name class PizzaToppingsThroughModel(OrderedModel): pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE) topping = models.ForeignKey(Topping, on_delete=models.CASCADE) order_with_respect_to = "pizza" class Meta: ordering = ("pizza", "order") # Admin only allows each model class to be registered once. However you can register a proxy class, # and (for presentation purposes only) rename it to match the existing in Admin class PizzaProxy(Pizza): class Meta: proxy = True verbose_name = "Pizza" verbose_name_plural = "Pizzas" # test many-one where the item has custom PK class CustomPKGroup(models.Model): name = models.CharField(max_length=100) class CustomPKGroupItem(OrderedModel): group = models.ForeignKey(CustomPKGroup, on_delete=models.CASCADE) name = models.CharField(max_length=100, primary_key=True) order_with_respect_to = "group" # test ordering on a base class (with order_class_path) # ie. OpenQuestion and GroupedItem can be ordered wrt each other class BaseQuestion(OrderedModel): order_class_path = __module__ + ".BaseQuestion" question = models.TextField(max_length=100) class Meta: ordering = ("order",) class MultipleChoiceQuestion(BaseQuestion): good_answer = models.TextField(max_length=100) wrong_answer1 = models.TextField(max_length=100) wrong_answer2 = models.TextField(max_length=100) wrong_answer3 = models.TextField(max_length=100) class OpenQuestion(BaseQuestion): answer = models.TextField(max_length=100) # test grouping by a foreign model field (group__user) class ItemGroup(models.Model): user = models.ForeignKey( TestUser, on_delete=models.CASCADE, related_name="item_groups" ) class GroupedItem(OrderedModel): group = models.ForeignKey(ItemGroup, on_delete=models.CASCADE, related_name="items") order_with_respect_to = "group__user" class CascadedParentModel(models.Model): pass class CascadedOrderedModel(OrderedModel): parent = models.ForeignKey(to=CascadedParentModel, on_delete=models.CASCADE) django-ordered-model-3.7.4/tests/settings.py000066400000000000000000000025601440510626600210720ustar00rootroot00000000000000import django import os DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db.sqlite3"}} ROOT_URLCONF = "tests.urls" INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.sessions", "ordered_model", "rest_framework", "tests", ] SECRET_KEY = "topsecret" DEBUG = True MIDDLEWARE = [ "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ] }, } ] REST_FRAMEWORK = {"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"]} STATIC_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "staticfiles") STATIC_URL = "/static/" DEFAULT_AUTO_FIELD = "django.db.models.AutoField" django-ordered-model-3.7.4/tests/settings_autoauth.py000066400000000000000000000001371440510626600230020ustar00rootroot00000000000000from tests.settings import * MIDDLEWARE.append("tests.autoauth.AutoAuthenticationMiddleware") django-ordered-model-3.7.4/tests/tests.py000066400000000000000000001523321440510626600203770ustar00rootroot00000000000000import uuid from io import StringIO from django.contrib.auth.models import User from django.core.management import call_command from django.core import checks from django.db import models from django.db.models.signals import post_delete from django.dispatch import Signal from django.utils.timezone import now from django.urls import reverse from django.test import TestCase, SimpleTestCase from django.test.utils import isolate_apps, override_system_checks from django import VERSION from rest_framework.test import APIRequestFactory, APITestCase from rest_framework import status from tests.drf import ItemViewSet, router from tests.utils import assertNumQueries from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet from tests.models import ( Answer, Item, Question, CustomItem, CustomOrderFieldModel, CustomPKGroupItem, CustomPKGroup, Pizza, Topping, PizzaToppingsThroughModel, BaseQuestion, OpenQuestion, MultipleChoiceQuestion, ItemGroup, GroupedItem, TestUser, CascadedParentModel, CascadedOrderedModel, ) class OrderGenerationTests(TestCase): def test_second_order_generation(self): first_item = Item.objects.create() self.assertEqual(first_item.order, 0) second_item = Item.objects.create() self.assertEqual(second_item.order, 1) class ModelTestCase(TestCase): fixtures = ["test_items.json"] def assertNames(self, names): self.assertEqual( list(enumerate(names)), [(i.order, i.name) for i in Item.objects.all()] ) def test_inserting_new_models(self): Item.objects.create(name="Wurble") self.assertNames(["1", "2", "3", "4", "Wurble"]) def test_previous(self): self.assertEqual(Item.objects.get(pk=4).previous(), Item.objects.get(pk=3)) def test_previous_first(self): self.assertEqual(Item.objects.get(pk=1).previous(), None) def test_previous_with_gap(self): self.assertEqual(Item.objects.get(pk=3).previous(), Item.objects.get(pk=2)) def test_next(self): self.assertEqual(Item.objects.get(pk=1).next(), Item.objects.get(pk=2)) def test_next_last(self): self.assertEqual(Item.objects.get(pk=4).next(), None) def test_next_with_gap(self): self.assertEqual(Item.objects.get(pk=2).next(), Item.objects.get(pk=3)) def test_up(self): Item.objects.get(pk=4).up() self.assertNames(["1", "2", "4", "3"]) def test_up_first(self): Item.objects.get(pk=1).up() self.assertNames(["1", "2", "3", "4"]) def test_up_with_gap(self): Item.objects.get(pk=3).up() self.assertNames(["1", "3", "2", "4"]) def test_down(self): Item.objects.get(pk=1).down() self.assertNames(["2", "1", "3", "4"]) def test_down_last(self): Item.objects.get(pk=4).down() self.assertNames(["1", "2", "3", "4"]) def test_down_with_gap(self): Item.objects.get(pk=2).down() self.assertNames(["1", "3", "2", "4"]) def test_to(self): Item.objects.get(pk=4).to(0) self.assertNames(["4", "1", "2", "3"]) Item.objects.get(pk=4).to(2) self.assertNames(["1", "2", "4", "3"]) Item.objects.get(pk=3).to(1) self.assertNames(["1", "3", "2", "4"]) def test_to_not_int(self): with self.assertRaises(TypeError): Item.objects.get(pk=4).to("1") def test_top(self): Item.objects.get(pk=4).top() self.assertNames(["4", "1", "2", "3"]) Item.objects.get(pk=2).top() self.assertNames(["2", "4", "1", "3"]) def test_bottom(self): Item.objects.get(pk=1).bottom() self.assertNames(["2", "3", "4", "1"]) Item.objects.get(pk=3).bottom() self.assertNames(["2", "4", "1", "3"]) def test_above(self): Item.objects.get(pk=3).above(Item.objects.get(pk=1)) self.assertNames(["3", "1", "2", "4"]) Item.objects.get(pk=4).above(Item.objects.get(pk=1)) self.assertNames(["3", "4", "1", "2"]) def test_above_self(self): Item.objects.get(pk=3).above(Item.objects.get(pk=3)) self.assertNames(["1", "2", "3", "4"]) def test_below(self): Item.objects.get(pk=1).below(Item.objects.get(pk=3)) self.assertNames(["2", "3", "1", "4"]) Item.objects.get(pk=3).below(Item.objects.get(pk=4)) self.assertNames(["2", "1", "4", "3"]) def test_below_self(self): Item.objects.get(pk=2).below(Item.objects.get(pk=2)) self.assertNames(["1", "2", "3", "4"]) def test_delete(self): Item.objects.get(pk=2).delete() self.assertNames(["1", "3", "4"]) Item.objects.get(pk=3).up() self.assertNames(["3", "1", "4"]) class OrderWithRespectToTests(TestCase): def setUp(self): q1 = Question.objects.create() q2 = Question.objects.create() u0 = TestUser.objects.create() self.q1_a1 = q1.answers.create(user=u0) self.q2_a1 = q2.answers.create(user=u0) self.q1_a2 = q1.answers.create(user=u0) self.q2_a2 = q2.answers.create(user=u0) with assertNumQueries(self, 1): Answer.objects.get(id=self.q1_a1.id) def test_saved_order(self): self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a1.pk, 0), (self.q1_a2.pk, 1), (self.q2_a1.pk, 0), (self.q2_a2.pk, 1), ], ) def test_previous(self): self.assertEqual(self.q1_a2.previous(), self.q1_a1) def test_previous_first(self): self.assertEqual(self.q2_a1.previous(), None) def test_next(self): self.assertEqual(self.q2_a1.next(), self.q2_a2) def test_next_last(self): self.assertEqual(self.q1_a2.next(), None) def test_swap(self): with self.assertRaises(ValueError): self.q1_a1.swap(self.q2_a1) self.q1_a1.swap(self.q1_a2) self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a2.pk, 0), (self.q1_a1.pk, 1), (self.q2_a1.pk, 0), (self.q2_a2.pk, 1), ], ) def test_up(self): self.q1_a2.up() self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a2.pk, 0), (self.q1_a1.pk, 1), (self.q2_a1.pk, 0), (self.q2_a2.pk, 1), ], ) def test_down(self): self.q2_a1.down() self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a1.pk, 0), (self.q1_a2.pk, 1), (self.q2_a2.pk, 0), (self.q2_a1.pk, 1), ], ) def test_to(self): self.q2_a1.to(1) self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a1.pk, 0), (self.q1_a2.pk, 1), (self.q2_a2.pk, 0), (self.q2_a1.pk, 1), ], ) def test_above(self): with self.assertRaises(ValueError): self.q1_a2.above(self.q2_a1) self.q1_a2.above(self.q1_a1) self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a2.pk, 0), (self.q1_a1.pk, 1), (self.q2_a1.pk, 0), (self.q2_a2.pk, 1), ], ) def test_below(self): with self.assertRaises(ValueError): self.q2_a1.below(self.q1_a2) self.q2_a1.below(self.q2_a2) self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a1.pk, 0), (self.q1_a2.pk, 1), (self.q2_a2.pk, 0), (self.q2_a1.pk, 1), ], ) def test_top(self): self.q1_a2.top() self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a2.pk, 0), (self.q1_a1.pk, 1), (self.q2_a1.pk, 0), (self.q2_a2.pk, 1), ], ) def test_bottom(self): self.q2_a1.bottom() self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_a1.pk, 0), (self.q1_a2.pk, 1), (self.q2_a2.pk, 0), (self.q2_a1.pk, 1), ], ) class OrderWithRespectToReorderTests(TestCase): def setUp(self): q1 = Question.objects.create() self.u0 = TestUser.objects.create() self.u1 = TestUser.objects.create() self.u0_a1 = q1.answers.create(user=self.u0) self.u0_a2 = q1.answers.create(user=self.u0) self.u0_a3 = q1.answers.create(user=self.u0) self.u1_a1 = q1.answers.create(user=self.u1) self.u1_a2 = q1.answers.create(user=self.u1) self.u1_a3 = q1.answers.create(user=self.u1) with assertNumQueries(self, 1): Answer.objects.get(id=self.u0_a1.id) def test_reorder_when_field_value_changed(self): self.u0_a2.user = self.u1 with assertNumQueries(self, 3): self.u0_a2.save() self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.u0_a1.pk, 0), (self.u0_a3.pk, 1), (self.u1_a1.pk, 0), (self.u1_a2.pk, 1), (self.u1_a3.pk, 2), (self.u0_a2.pk, 3), ], ) class CustomPKTest(TestCase): def setUp(self): self.item1 = CustomItem.objects.create(pkid=str(uuid.uuid4()), name="1") self.item2 = CustomItem.objects.create(pkid=str(uuid.uuid4()), name="2") self.item3 = CustomItem.objects.create(pkid=str(uuid.uuid4()), name="3") self.item4 = CustomItem.objects.create(pkid=str(uuid.uuid4()), name="4") def test_saved_order(self): self.assertSequenceEqual( CustomItem.objects.values_list("pk", "order"), [ (self.item1.pk, 0), (self.item2.pk, 1), (self.item3.pk, 2), (self.item4.pk, 3), ], ) def test_order_to_extra_update(self): modified_time = now() self.item1.to(3, extra_update={"modified": modified_time}) self.assertSequenceEqual( CustomItem.objects.values_list("pk", "order", "modified"), [ (self.item2.pk, 0, modified_time), (self.item3.pk, 1, modified_time), (self.item4.pk, 2, modified_time), # This one is the primary item being operated on and modified would be # handled via auto_now or something (self.item1.pk, 3, None), ], ) def test_bottom_extra_update(self): modified_time = now() self.item1.bottom(extra_update={"modified": modified_time}) self.assertSequenceEqual( CustomItem.objects.values_list("pk", "order", "modified"), [ (self.item2.pk, 0, modified_time), (self.item3.pk, 1, modified_time), (self.item4.pk, 2, modified_time), # This one is the primary item being operated on and modified would be # handled via auto_now or something (self.item1.pk, 3, None), ], ) def test_top_extra_update(self): modified_time = now() self.item4.top(extra_update={"modified": modified_time}) self.assertSequenceEqual( CustomItem.objects.values_list("pk", "order", "modified"), [ (self.item4.pk, 0, None), (self.item1.pk, 1, modified_time), (self.item2.pk, 2, modified_time), # This one is the primary item being operated on and modified would be # handled via auto_now or something (self.item3.pk, 3, modified_time), ], ) def test_below_extra_update(self): modified_time = now() self.item1.below(self.item4, extra_update={"modified": modified_time}) self.assertSequenceEqual( CustomItem.objects.values_list("pk", "order", "modified"), [ (self.item2.pk, 0, modified_time), (self.item3.pk, 1, modified_time), (self.item4.pk, 2, modified_time), # This one is the primary item being operated on and modified would be # handled via auto_now or something (self.item1.pk, 3, None), ], ) def test_above_extra_update(self): modified_time = now() self.item4.above(self.item1, extra_update={"modified": modified_time}) self.assertSequenceEqual( CustomItem.objects.values_list("pk", "order", "modified"), [ (self.item4.pk, 0, None), (self.item1.pk, 1, modified_time), (self.item2.pk, 2, modified_time), # This one is the primary item being operated on and modified would be # handled via auto_now or something (self.item3.pk, 3, modified_time), ], ) def test_delete_extra_update(self): modified_time = now() self.item1.delete(extra_update={"modified": modified_time}) self.assertSequenceEqual( CustomItem.objects.values_list("pk", "order", "modified"), [ (self.item2.pk, 0, modified_time), (self.item3.pk, 1, modified_time), (self.item4.pk, 2, modified_time), ], ) class CustomOrderFieldTest(TestCase): fixtures = ["test_items.json"] def assertNames(self, names): self.assertEqual( list(enumerate(names)), [(i.sort_order, i.name) for i in CustomOrderFieldModel.objects.all()], ) def test_inserting_new_models(self): CustomOrderFieldModel.objects.create(name="Wurble") self.assertNames(["1", "2", "3", "4", "Wurble"]) def test_previous(self): self.assertEqual( CustomOrderFieldModel.objects.get(pk=4).previous(), CustomOrderFieldModel.objects.get(pk=3), ) def test_previous_first(self): self.assertEqual(CustomOrderFieldModel.objects.get(pk=1).previous(), None) def test_previous_with_gap(self): self.assertEqual( CustomOrderFieldModel.objects.get(pk=3).previous(), CustomOrderFieldModel.objects.get(pk=2), ) def test_next(self): self.assertEqual( CustomOrderFieldModel.objects.get(pk=1).next(), CustomOrderFieldModel.objects.get(pk=2), ) def test_next_last(self): self.assertEqual(CustomOrderFieldModel.objects.get(pk=4).next(), None) def test_next_with_gap(self): self.assertEqual( CustomOrderFieldModel.objects.get(pk=2).next(), CustomOrderFieldModel.objects.get(pk=3), ) def test_up(self): CustomOrderFieldModel.objects.get(pk=4).up() self.assertNames(["1", "2", "4", "3"]) def test_up_first(self): CustomOrderFieldModel.objects.get(pk=1).up() self.assertNames(["1", "2", "3", "4"]) def test_up_with_gap(self): CustomOrderFieldModel.objects.get(pk=3).up() self.assertNames(["1", "3", "2", "4"]) def test_down(self): CustomOrderFieldModel.objects.get(pk=1).down() self.assertNames(["2", "1", "3", "4"]) def test_down_last(self): CustomOrderFieldModel.objects.get(pk=4).down() self.assertNames(["1", "2", "3", "4"]) def test_down_with_gap(self): CustomOrderFieldModel.objects.get(pk=2).down() self.assertNames(["1", "3", "2", "4"]) def test_to(self): CustomOrderFieldModel.objects.get(pk=4).to(0) self.assertNames(["4", "1", "2", "3"]) CustomOrderFieldModel.objects.get(pk=4).to(2) self.assertNames(["1", "2", "4", "3"]) CustomOrderFieldModel.objects.get(pk=3).to(1) self.assertNames(["1", "3", "2", "4"]) def test_top(self): CustomOrderFieldModel.objects.get(pk=4).top() self.assertNames(["4", "1", "2", "3"]) CustomOrderFieldModel.objects.get(pk=2).top() self.assertNames(["2", "4", "1", "3"]) def test_bottom(self): CustomOrderFieldModel.objects.get(pk=1).bottom() self.assertNames(["2", "3", "4", "1"]) CustomOrderFieldModel.objects.get(pk=3).bottom() self.assertNames(["2", "4", "1", "3"]) def test_above(self): CustomOrderFieldModel.objects.get(pk=3).above( CustomOrderFieldModel.objects.get(pk=1) ) self.assertNames(["3", "1", "2", "4"]) CustomOrderFieldModel.objects.get(pk=4).above( CustomOrderFieldModel.objects.get(pk=1) ) self.assertNames(["3", "4", "1", "2"]) def test_above_self(self): CustomOrderFieldModel.objects.get(pk=3).above( CustomOrderFieldModel.objects.get(pk=3) ) self.assertNames(["1", "2", "3", "4"]) def test_below(self): CustomOrderFieldModel.objects.get(pk=1).below( CustomOrderFieldModel.objects.get(pk=3) ) self.assertNames(["2", "3", "1", "4"]) CustomOrderFieldModel.objects.get(pk=3).below( CustomOrderFieldModel.objects.get(pk=4) ) self.assertNames(["2", "1", "4", "3"]) def test_below_self(self): CustomOrderFieldModel.objects.get(pk=2).below( CustomOrderFieldModel.objects.get(pk=2) ) self.assertNames(["1", "2", "3", "4"]) def test_delete(self): CustomOrderFieldModel.objects.get(pk=2).delete() self.assertNames(["1", "3", "4"]) CustomOrderFieldModel.objects.get(pk=3).up() self.assertNames(["3", "1", "4"]) class OrderedModelAdminTest(TestCase): def setUp(self): User.objects.create_superuser("admin", "a@example.com", "admin") self.assertTrue(self.client.login(username="admin", password="admin")) Item.objects.create(name="item1") Item.objects.create(name="item2") Item.objects.create(name="item3") self.ham = Topping.objects.create(name="Ham") self.pineapple = Topping.objects.create(name="Pineapple") self.pizza = Pizza.objects.create(name="Hawaiian Pizza") self.pizza_to_ham = PizzaToppingsThroughModel.objects.create( pizza=self.pizza, topping=self.ham ) self.pizza_to_pineapple = PizzaToppingsThroughModel.objects.create( pizza=self.pizza, topping=self.pineapple ) def test_move_links(self): res = self.client.get("/admin/tests/item/") self.assertEqual(res.status_code, 200) self.assertIn("/admin/tests/item/1/move-up/", str(res.content)) self.assertIn("/admin/tests/item/1/move-down/", str(res.content)) self.assertIn("/admin/tests/item/1/move-top/", str(res.content)) self.assertIn("/admin/tests/item/1/move-bottom/", str(res.content)) def test_move_invalid_direction(self): res = self.client.get("/admin/tests/item/1/move-middle/") self.assertEqual(res.status_code, 404) def test_move_down(self): self.assertEqual(Item.objects.get(name="item1").order, 0) self.assertEqual(Item.objects.get(name="item2").order, 1) res = self.client.get("/admin/tests/item/1/move-down/") self.assertRedirects(res, "/admin/tests/item/") self.assertEqual(Item.objects.get(name="item1").order, 1) self.assertEqual(Item.objects.get(name="item2").order, 0) def test_move_up(self): self.assertEqual(Item.objects.get(name="item1").order, 0) self.assertEqual(Item.objects.get(name="item2").order, 1) res = self.client.get("/admin/tests/item/2/move-up/") self.assertRedirects(res, "/admin/tests/item/") self.assertEqual(Item.objects.get(name="item1").order, 1) self.assertEqual(Item.objects.get(name="item2").order, 0) def test_move_top(self): self.assertEqual(Item.objects.get(name="item1").order, 0) self.assertEqual(Item.objects.get(name="item2").order, 1) self.assertEqual(Item.objects.get(name="item3").order, 2) res = self.client.get("/admin/tests/item/3/move-top/") self.assertRedirects(res, "/admin/tests/item/") self.assertEqual(Item.objects.get(name="item1").order, 1) self.assertEqual(Item.objects.get(name="item2").order, 2) self.assertEqual(Item.objects.get(name="item3").order, 0) def test_move_bottom(self): self.assertEqual(Item.objects.get(name="item1").order, 0) self.assertEqual(Item.objects.get(name="item2").order, 1) self.assertEqual(Item.objects.get(name="item3").order, 2) res = self.client.get("/admin/tests/item/1/move-bottom/") self.assertRedirects(res, "/admin/tests/item/") self.assertEqual(Item.objects.get(name="item1").order, 2) self.assertEqual(Item.objects.get(name="item2").order, 0) self.assertEqual(Item.objects.get(name="item3").order, 1) def test_move_up_down_links_ordered_inline(self): # model list res = self.client.get("/admin/tests/pizza/") self.assertContains( res, text="/admin/tests/pizza/{}/change/".format(self.pizza.id) ) # model page including inlines res = self.client.get("/admin/tests/pizza/{}/change/".format(self.pizza.id)) self.assertContains( res, text=''.format( self.pizza.id, self.pizza_to_ham.id ), ) self.assertContains( res, text=''.format( self.pizza.id, self.pizza_to_pineapple.id ), ) # click the move-up link self.assertEqual(self.pizza_to_ham.order, 0) self.assertEqual(self.pizza_to_pineapple.order, 1) res = self.client.get( "/admin/tests/pizza/{}/pizzatoppingsthroughmodel/{}/move-up/".format( self.pizza.id, self.pizza_to_pineapple.id ), follow=True, ) self.pizza_to_ham.refresh_from_db() self.pizza_to_pineapple.refresh_from_db() self.assertEqual(self.pizza_to_ham.order, 1) self.assertEqual(self.pizza_to_pineapple.order, 0) self.assertEqual(res.status_code, 200) def test_move_up_down_proxy_stacked_inline(self): res = self.client.get("/admin/tests/pizzaproxy/") self.assertContains( res, text="/admin/tests/pizzaproxy/{}/change/".format(self.pizza.id) ) res = self.client.get( "/admin/tests/pizzaproxy/{}/change/".format(self.pizza.id) ) self.assertContains( res, text=''.format( self.pizza.id, self.pizza_to_ham.id ), ) class OrderWithRespectToTestsManyToMany(TestCase): def setUp(self): self.t1 = Topping.objects.create(name="tomatoe") self.t2 = Topping.objects.create(name="mozarella") self.t3 = Topping.objects.create(name="anchovy") self.t4 = Topping.objects.create(name="mushrooms") self.t5 = Topping.objects.create(name="ham") self.p1 = Pizza.objects.create(name="Napoli") # tomatoe, mozarella, anchovy self.p2 = Pizza.objects.create( name="Regina" ) # tomatoe, mozarella, mushrooms, ham # Now put the toppings on the pizza self.p1_t1 = PizzaToppingsThroughModel(pizza=self.p1, topping=self.t1) with assertNumQueries(self, 2): self.p1_t1.save() self.p1_t2 = PizzaToppingsThroughModel(pizza=self.p1, topping=self.t2) self.p1_t2.save() self.p1_t3 = PizzaToppingsThroughModel(pizza=self.p1, topping=self.t3) self.p1_t3.save() self.p2_t1 = PizzaToppingsThroughModel(pizza=self.p2, topping=self.t1) self.p2_t1.save() self.p2_t2 = PizzaToppingsThroughModel(pizza=self.p2, topping=self.t2) self.p2_t2.save() self.p2_t3 = PizzaToppingsThroughModel(pizza=self.p2, topping=self.t4) self.p2_t3.save() self.p2_t4 = PizzaToppingsThroughModel() self.p2_t4.pizza = self.p2 self.p2_t4.topping = self.t5 self.p2_t4.save() def test_saved_order(self): self.assertSequenceEqual( PizzaToppingsThroughModel.objects.values_list("topping__pk", "order"), [ (self.p1_t1.topping.pk, 0), (self.p1_t2.topping.pk, 1), (self.p1_t3.topping.pk, 2), (self.p2_t1.topping.pk, 0), (self.p2_t2.topping.pk, 1), (self.p2_t3.topping.pk, 2), (self.p2_t4.topping.pk, 3), ], ) def test_swap(self): with self.assertRaises(ValueError): self.p1_t1.swap(self.p2_t1) def test_previous(self): self.assertEqual(self.p1_t2.previous(), self.p1_t1) def test_previous_first(self): self.assertEqual(self.p2_t1.previous(), None) def test_down(self): self.assertEqual(self.p2_t1.next(), self.p2_t2) def test_down_last(self): self.assertEqual(self.p1_t3.next(), None) def test_up(self): self.p1_t2.up() self.assertSequenceEqual( PizzaToppingsThroughModel.objects.values_list("topping__pk", "order"), [ (self.p1_t2.topping.pk, 0), (self.p1_t1.topping.pk, 1), (self.p1_t3.topping.pk, 2), (self.p2_t1.topping.pk, 0), (self.p2_t2.topping.pk, 1), (self.p2_t3.topping.pk, 2), (self.p2_t4.topping.pk, 3), ], ) def test_down(self): self.p2_t1.down() self.assertSequenceEqual( PizzaToppingsThroughModel.objects.values_list("topping__pk", "order"), [ (self.p1_t1.topping.pk, 0), (self.p1_t2.topping.pk, 1), (self.p1_t3.topping.pk, 2), (self.p2_t2.topping.pk, 0), (self.p2_t1.topping.pk, 1), (self.p2_t3.topping.pk, 2), (self.p2_t4.topping.pk, 3), ], ) def test_to(self): self.p2_t1.to(1) self.assertSequenceEqual( PizzaToppingsThroughModel.objects.values_list("topping__pk", "order"), [ (self.p1_t1.topping.pk, 0), (self.p1_t2.topping.pk, 1), (self.p1_t3.topping.pk, 2), (self.p2_t2.topping.pk, 0), (self.p2_t1.topping.pk, 1), (self.p2_t3.topping.pk, 2), (self.p2_t4.topping.pk, 3), ], ) def test_above(self): with self.assertRaises(ValueError): self.p1_t2.above(self.p2_t1) self.p1_t2.above(self.p1_t1) self.assertSequenceEqual( PizzaToppingsThroughModel.objects.values_list("topping__pk", "order"), [ (self.p1_t2.topping.pk, 0), (self.p1_t1.topping.pk, 1), (self.p1_t3.topping.pk, 2), (self.p2_t1.topping.pk, 0), (self.p2_t2.topping.pk, 1), (self.p2_t3.topping.pk, 2), (self.p2_t4.topping.pk, 3), ], ) def test_below(self): with self.assertRaises(ValueError): self.p2_t1.below(self.p1_t2) self.p2_t1.below(self.p2_t2) self.assertSequenceEqual( PizzaToppingsThroughModel.objects.values_list("topping__pk", "order"), [ (self.p1_t1.topping.pk, 0), (self.p1_t2.topping.pk, 1), (self.p1_t3.topping.pk, 2), (self.p2_t2.topping.pk, 0), (self.p2_t1.topping.pk, 1), (self.p2_t3.topping.pk, 2), (self.p2_t4.topping.pk, 3), ], ) def test_top(self): self.p1_t3.top() self.assertSequenceEqual( PizzaToppingsThroughModel.objects.values_list("topping__pk", "order"), [ (self.p1_t3.topping.pk, 0), (self.p1_t1.topping.pk, 1), (self.p1_t2.topping.pk, 2), (self.p2_t1.topping.pk, 0), (self.p2_t2.topping.pk, 1), (self.p2_t3.topping.pk, 2), (self.p2_t4.topping.pk, 3), ], ) def test_bottom(self): self.p2_t1.bottom() self.assertSequenceEqual( PizzaToppingsThroughModel.objects.values_list("topping__pk", "order"), [ (self.p1_t1.topping.pk, 0), (self.p1_t2.topping.pk, 1), (self.p1_t3.topping.pk, 2), (self.p2_t2.topping.pk, 0), (self.p2_t3.topping.pk, 1), (self.p2_t4.topping.pk, 2), (self.p2_t1.topping.pk, 3), ], ) class MultiOrderWithRespectToTests(TestCase): def setUp(self): q1 = Question.objects.create() q2 = Question.objects.create() u1 = TestUser.objects.create() u2 = TestUser.objects.create() self.q1_u1_a1 = q1.answers.create(user=u1) self.q2_u1_a1 = q2.answers.create(user=u1) self.q1_u1_a2 = q1.answers.create(user=u1) self.q2_u1_a2 = q2.answers.create(user=u1) self.q1_u2_a1 = q1.answers.create(user=u2) self.q2_u2_a1 = q2.answers.create(user=u2) self.q1_u2_a2 = q1.answers.create(user=u2) self.q2_u2_a2 = q2.answers.create(user=u2) def test_saved_order(self): self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ (self.q1_u1_a1.pk, 0), (self.q1_u1_a2.pk, 1), (self.q1_u2_a1.pk, 0), (self.q1_u2_a2.pk, 1), (self.q2_u1_a1.pk, 0), (self.q2_u1_a2.pk, 1), (self.q2_u2_a1.pk, 0), (self.q2_u2_a2.pk, 1), ], ) def test_swap_fails(self): with self.assertRaises(ValueError): self.q1_u1_a1.swap(self.q2_u1_a2) class OrderWithRespectToRelatedModelFieldTests(TestCase): def setUp(self): self.u1 = TestUser.objects.create() self.u2 = TestUser.objects.create() self.u1_g1 = self.u1.item_groups.create() self.u2_g1 = self.u2.item_groups.create() self.u2_g2 = self.u2.item_groups.create() self.u1_g2 = self.u1.item_groups.create() self.u2_g2_i1 = self.u2_g2.items.create() self.u2_g1_i1 = self.u2_g1.items.create() self.u1_g1_i1 = self.u1_g1.items.create() self.u1_g2_i1 = self.u1_g2.items.create() def test_saved_order(self): self.assertSequenceEqual( GroupedItem.objects.filter(group__user=self.u1).values_list("pk", "order"), [(self.u1_g1_i1.pk, 0), (self.u1_g2_i1.pk, 1)], ) self.assertSequenceEqual( GroupedItem.objects.filter(group__user=self.u2).values_list("pk", "order"), [(self.u2_g2_i1.pk, 0), (self.u2_g1_i1.pk, 1)], ) def test_swap(self): i2 = self.u1_g1.items.create() self.assertSequenceEqual( GroupedItem.objects.filter(group__user=self.u1).values_list("pk", "order"), [(self.u1_g1_i1.pk, 0), (self.u1_g2_i1.pk, 1), (i2.pk, 2)], ) i2.swap(self.u1_g1_i1) self.assertSequenceEqual( GroupedItem.objects.filter(group__user=self.u1).values_list("pk", "order"), [(i2.pk, 0), (self.u1_g2_i1.pk, 1), (self.u1_g1_i1.pk, 2)], ) def test_swap_fails_between_users(self): with self.assertRaises(ValueError): self.u1_g1_i1.swap(self.u2_g1_i1) def test_above_between_groups(self): i2 = self.u1_g2.items.create() i2.above(self.u1_g1_i1) self.assertSequenceEqual( GroupedItem.objects.filter(group__user=self.u1).values_list("pk", "order"), [(i2.pk, 0), (self.u1_g1_i1.pk, 1), (self.u1_g2_i1.pk, 2)], ) class PolymorphicOrderGenerationTests(TestCase): def test_order_of_baselist(self): o1 = OpenQuestion.objects.create() self.assertEqual(o1.order, 0) o1.save() m1 = MultipleChoiceQuestion.objects.create() self.assertEqual(m1.order, 1) m1.save() m2 = MultipleChoiceQuestion.objects.create() self.assertEqual(m2.order, 2) m2.save() o2 = OpenQuestion.objects.create() self.assertEqual(o2.order, 3) o2.save() m2.up() self.assertEqual(m2.order, 1) m1.refresh_from_db() self.assertEqual(m1.order, 2) o2.up() self.assertEqual(o2.order, 2) m1.refresh_from_db() self.assertEqual(m1.order, 3) def test_returns_polymorphic(self): o1 = OpenQuestion.objects.create() self.assertIsInstance(o1, OpenQuestion) class BulkCreateTests(TestCase): def test(self): Item.objects.bulk_create([Item(name="1")]) self.assertEqual(Item.objects.get(name="1").order, 0) def test_multiple(self): Item.objects.bulk_create([Item(name="1"), Item(name="2")]) self.assertEqual(Item.objects.get(name="1").order, 0) self.assertEqual(Item.objects.get(name="2").order, 1) def test_with_existing(self): Item.objects.create() Item.objects.bulk_create([Item(name="1")]) self.assertEqual(Item.objects.get(name="1").order, 1) def test_with_multiple_existing(self): Item.objects.create() Item.objects.create() Item.objects.bulk_create([Item(name="1")]) self.assertEqual(Item.objects.get(name="1").order, 2) def test_order_field_name(self): CustomOrderFieldModel.objects.bulk_create([CustomOrderFieldModel(name="1")]) self.assertEqual(CustomOrderFieldModel.objects.get(name="1").sort_order, 0) def test_order_with_respect_to(self): hawaiian_pizza = Pizza.objects.create(name="Hawaiian Pizza") napoli_pizza = Pizza.objects.create(name="Napoli") topping = Topping.objects.create(name="mozarella") PizzaToppingsThroughModel.objects.create(pizza=napoli_pizza, topping=topping) PizzaToppingsThroughModel.objects.bulk_create( [PizzaToppingsThroughModel(pizza=hawaiian_pizza, topping=topping)] ) self.assertEqual( PizzaToppingsThroughModel.objects.get(pizza=hawaiian_pizza).order, 0 ) def test_order_with_respect_to_multiple(self): hawaiian_pizza = Pizza.objects.create(name="Hawaiian Pizza") napoli_pizza = Pizza.objects.create(name="Napoli") mozarella = Topping.objects.create(name="mozarella") pineapple = Topping.objects.create(name="Pineapple") PizzaToppingsThroughModel.objects.create(pizza=napoli_pizza, topping=mozarella) PizzaToppingsThroughModel.objects.bulk_create( [ PizzaToppingsThroughModel(pizza=hawaiian_pizza, topping=mozarella), PizzaToppingsThroughModel(pizza=hawaiian_pizza, topping=pineapple), ] ) self.assertSequenceEqual( PizzaToppingsThroughModel.objects.filter(pizza=hawaiian_pizza).values_list( "order", flat=True ), [0, 1], ) class OrderedModelAdminWithCustomPKInlineTest(TestCase): def setUp(self): User.objects.create_superuser("admin", "a@example.com", "admin") self.assertTrue(self.client.login(username="admin", password="admin")) group = CustomPKGroup.objects.create(name="g1") CustomPKGroupItem.objects.create(name="g1 i1", group=group) CustomPKGroupItem.objects.create(name="g1 i2", group=group) group = CustomPKGroup.objects.create(name="g2") CustomPKGroupItem.objects.create(name="g2 i1", group=group) def test_move_links(self): res = self.client.get("/admin/tests/custompkgroup/1/change", follow=True) self.assertContains(res, text="CustomPKGroupItem object (g1 i1)") self.assertContains(res, text="CustomPKGroupItem object (g1 i2)") # Check for the inline column header # see Django release notes https://docs.djangoproject.com/en/dev/releases/2.2/#django-contrib-admin # What’s new in Django 2.2 > Minor features > django.contrib.admin > Addd a CSS class to the column headers of TabularInline if VERSION >= (2, 2): self.assertContains( res, text='Move', html=True ) else: self.assertContains( res, text="Move", html=True ) # pragma: no cover # Check move up/down links self.assertContains( res, text='', ) self.assertContains( res, text='', ) class ReorderModelTestCase(TestCase): fixtures = ["test_items.json"] def test_reorder_with_no_respect_to(self): """ Test that 'reorder_model' changes the order of OpenQuestions when they overlap. """ OpenQuestion.objects.create(order=0) OpenQuestion.objects.create(order=0) out = StringIO() call_command("reorder_model", "tests.OpenQuestion", verbosity=1, stdout=out) self.assertSequenceEqual( OpenQuestion.objects.values_list("order", flat=True).order_by("order"), [0, 1], ) self.assertIn( "changing order of tests.OpenQuestion (2) from 0 to 1", out.getvalue() ) def test_reorder_with_respect_to(self): """ Test that when 'with_respect_to' is used 'reorder_model' changes to values of the 'order' field to unique values. """ user1 = TestUser.objects.create() group1 = ItemGroup.objects.create(user=user1) GroupedItem.objects.create(group=group1, order=0) GroupedItem.objects.create(group=group1, order=1) GroupedItem.objects.create(group=group1, order=1) GroupedItem.objects.create(group=group1, order=3) GroupedItem.objects.create(group=group1, order=4) user2 = TestUser.objects.create() group2 = ItemGroup.objects.create(user=user2) GroupedItem.objects.create(group=group2) GroupedItem.objects.create(group=group2) GroupedItem.objects.create(group=group2) out = StringIO() call_command("reorder_model", "tests.GroupedItem", verbosity=1, stdout=out) self.assertSequenceEqual( GroupedItem.objects.filter(group=group1) .values_list("order", flat=True) .order_by("order"), [0, 1, 2, 3, 4], ) self.assertSequenceEqual( GroupedItem.objects.filter(group=group2) .values_list("order", flat=True) .order_by("order"), [0, 1, 2], ) self.assertEqual( "changing order of tests.GroupedItem (3) from 1 to 2\n", out.getvalue() ) def test_reorder_with_respect_to_tuple(self): u1 = TestUser.objects.create() u2 = TestUser.objects.create() q1 = Question.objects.create() q2 = Question.objects.create() for q in (q1, q2): for u in (u1, u2): Answer.objects.create(user=u, question=q, order=0) Answer.objects.create(user=u, question=q, order=0) self.assertSequenceEqual( Answer.objects.filter(user=u2, question=q1).values_list("order", flat=True), [0, 0], ) out = StringIO() call_command("reorder_model", "tests.Answer", verbosity=1, stdout=out) self.assertSequenceEqual( Answer.objects.filter(user=u2, question=q1).values_list("order", flat=True), [0, 1], ) self.assertEqual( ( "changing order of tests.Answer (2) from 0 to 1\n" + "changing order of tests.Answer (4) from 0 to 1\n" + "changing order of tests.Answer (6) from 0 to 1\n" + "changing order of tests.Answer (8) from 0 to 1\n" ), out.getvalue(), ) out = StringIO() call_command("reorder_model", "tests.Answer", verbosity=1, stdout=out) self.assertEqual("", out.getvalue()) def test_reorder_with_custom_order_field(self): """ Test that 'reorder_model' changes the order of OpenQuestions when they overlap. """ out = StringIO() CustomOrderFieldModel.objects.create(name="5", sort_order=0) call_command( "reorder_model", "tests.CustomOrderFieldModel", verbosity=1, stdout=out ) self.assertSequenceEqual( CustomOrderFieldModel.objects.values_list("sort_order", flat=True).order_by( "sort_order" ), [0, 1, 2, 3, 4], ) self.assertIn( "changing order of tests.CustomOrderFieldModel (5) from 0 to 1", out.getvalue(), ) def test_shows_alternatives(self): out = StringIO() call_command("reorder_model", "test.Missing", verbosity=1, stdout=out) self.assertIn("Model 'test.Missing' is not an ordered model", out.getvalue()) self.assertIn("tests.BaseQuestion", out.getvalue()) out = StringIO() call_command("reorder_model", verbosity=1, stdout=out) self.assertIn("tests.BaseQuestion", out.getvalue()) def test_delete_bypass(self): OpenQuestion.objects.create(answer="1", order=0) OpenQuestion.objects.create(answer="2", order=1) OpenQuestion.objects.create(answer="3", order=2) OpenQuestion.objects.create(answer="4", order=3) # bypass our OrderedModel delete logic to leave a hole in ordering # remove signal handlers # print(post_delete.receivers) self.assertTrue( post_delete.disconnect( sender=OpenQuestion, dispatch_uid=OpenQuestion.__name__ ) ) self.assertTrue( post_delete.disconnect( sender=BaseQuestion, dispatch_uid=BaseQuestion.__name__ ) ) # delete on the queryset fires post_delete, but does not call model.delete() OpenQuestion.objects.filter(answer="3").delete() post_delete.connect( OpenQuestion._on_ordered_model_delete, sender=OpenQuestion, dispatch_uid=OpenQuestion.__name__, ) self.assertEqual([0, 1, 3], [i.order for i in OpenQuestion.objects.all()]) self.assertEqual( ["1", "2", "4"], [i.answer for i in OpenQuestion.objects.all()] ) # repair out = StringIO() call_command("reorder_model", "tests.OpenQuestion", stdout=out) self.assertEqual([0, 1, 2], [i.order for i in OpenQuestion.objects.all()]) self.assertEqual( ["1", "2", "4"], [i.answer for i in OpenQuestion.objects.all()] ) self.assertEqual( "changing order of tests.OpenQuestion (4) from 3 to 2\n", out.getvalue() ) class DRFTestCase(APITestCase): fixtures = ["test_items.json"] def setUp(self): self.item1 = CustomItem.objects.create(pkid="a", name="1") self.item2 = CustomItem.objects.create(pkid="b", name="2") def test_create_shuffles_down(self): data = {"name": "3", "pkid": "c", "order": "0"} response = self.client.post(reverse("customitem-list"), data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(CustomItem.objects.count(), 3) self.assertEqual( response.data, {"pkid": "c", "name": "3", "modified": None, "order": 0} ) self.assertEqual(CustomItem.objects.get(pkid="a").order, 1) self.assertEqual(CustomItem.objects.get(pkid="b").order, 2) # check DRF exposes the modified value response = self.client.get( reverse("customitem-detail", kwargs={"pk": "b"}), {}, format="json" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.data, {"pkid": "b", "name": "2", "modified": None, "order": 2} ) def test_patch_shuffles_down(self): self.item3 = CustomItem.objects.create(pkid="c", name="3") # re-order an item response = self.client.patch( reverse("customitem-detail", kwargs={"pk": "b"}), {"order": 2, "name": "x"}, format="json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.data, {"pkid": "b", "name": "x", "modified": None, "order": 2} ) self.assertEqual(CustomItem.objects.count(), 3) self.assertEqual(CustomItem.objects.get(pkid="a").order, 0) self.assertEqual(CustomItem.objects.get(pkid="c").order, 1) self.assertEqual(CustomItem.objects.get(pkid="b").order, 2) def test_custom_order_field_model(self): response = self.client.get( reverse("customorderfieldmodel-detail", kwargs={"pk": 1}), {}, format="json" ) self.assertEqual(response.data, {"id": 1, "name": "1", "sort_order": 0}) # re-order a lower item to top response = self.client.patch( reverse("customorderfieldmodel-detail", kwargs={"pk": 2}), {"sort_order": 0}, format="json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {"id": 2, "name": "2", "sort_order": 0}) # check old first item is pushed down response = self.client.get( reverse("customorderfieldmodel-detail", kwargs={"pk": 1}), {}, format="json" ) self.assertEqual(response.data, {"id": 1, "name": "1", "sort_order": 1}) def test_serializer_renames_order_field(self): response = self.client.get( reverse("renameditem-detail", kwargs={"pk": "b"}), {}, format="json" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {"pkid": "b", "name": "2", "renamedOrder": 1}) # move b to top response = self.client.patch( reverse("renameditem-detail", kwargs={"pk": "b"}), {"renamedOrder": 0}, format="json", ) self.assertEqual(response.data, {"pkid": "b", "name": "2", "renamedOrder": 0}) self.assertEqual(CustomItem.objects.get(pkid="b").order, 0) self.assertEqual(CustomItem.objects.get(pkid="a").order, 1) @isolate_apps("tests", attr_name="apps") @override_system_checks([checks.model_checks.check_all_models]) class ChecksTest(SimpleTestCase): def test_no_inherited_ordering(self): class TestModel(OrderedModel): class Meta: verbose_name = "unordered" self.maxDiff = None self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Error( msg="OrderedModelBase subclass needs Meta.ordering specified.", hint="If you have overwritten Meta, try inheriting with Meta(OrderedModel.Meta).", obj="ChecksTest.test_no_inherited_ordering..TestModel", id="ordered_model.E001", ) ], ) def test_explicit_ordering(self): class TestModel2(OrderedModel): class Meta: verbose_name = "unordered" ordering = ["order"] self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) def test_inherited_ordering(self): class TestModel(OrderedModel): class Meta(OrderedModel.Meta): verbose_name = "unordered" self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) def test_bad_owrt(self): class TestModel(OrderedModel): order_with_respect_to = 7 self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Error( msg="OrderedModelBase subclass order_with_respect_to value invalid. Expected tuple, str or None.", obj="ChecksTest.test_bad_owrt..TestModel", id="ordered_model.E002", ) ], ) def test_bad_manager(self): class BadModelManager(models.Manager.from_queryset(models.QuerySet)): pass class TestModel(OrderedModel): objects = BadModelManager() self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Error( msg="OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager.", obj="ChecksTest.test_bad_manager..TestModel", id="ordered_model.E003", ) ], ) def test_builtin_manager_to_queryset(self): class TestModel(OrderedModel): objects = OrderedModelQuerySet.as_manager() self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Warning( msg="OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager. This is not ideal but will work.", obj="ChecksTest.test_builtin_manager_to_queryset..TestModel", id="ordered_model.W003", ) ], ) def test_bad_queryset(self): # I've swapped the inheritance order here so that the models.QuerySet is returned class BadQSModelManager( models.Manager.from_queryset(models.QuerySet), OrderedModelManager ): pass class TestModel(OrderedModel): objects = BadQSModelManager() self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Error( msg="OrderedModelBase subclass ModelManager did not return a QuerySet inheriting from OrderedModelQuerySet.", obj="ChecksTest.test_bad_queryset..TestModel", id="ordered_model.E004", ) ], ) def test_owrt_not_foreign_key(self): class TestModel(OrderedModel): name = models.CharField(max_length=100) order_with_respect_to = "name" self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Error( msg="OrderedModel order_with_respect_to specifies field 'name' (within 'name') which is not a ForeignKey. This is unsupported.", obj="ChecksTest.test_owrt_not_foreign_key..TestModel", id="ordered_model.E005", ) ], ) def test_owrt_not_immediate_foreign_key(self): class TestTargetModel(OrderedModel): name = models.CharField(max_length=100) class TestModel(OrderedModel): target = models.ForeignKey(to=TestTargetModel, on_delete=models.CASCADE) order_with_respect_to = "target__name" self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Error( msg="OrderedModel order_with_respect_to specifies field 'name' (within 'target__name') which is not a ForeignKey. This is unsupported.", obj="ChecksTest.test_owrt_not_immediate_foreign_key..TestModel", id="ordered_model.E005", ) ], ) class TestCascadedDelete(TestCase): def test_that_model_when_deleted_by_cascade_still_maintains_ordering(self): parent_for_order_0_child = CascadedParentModel.objects.create() child_with_order_0 = CascadedOrderedModel.objects.create( parent=parent_for_order_0_child ) parent__for_order_1_child = CascadedParentModel.objects.create() child_with_order_1 = CascadedOrderedModel.objects.create( parent=parent__for_order_1_child ) parent_for_order_2_child = CascadedParentModel.objects.create() child_with_order_2 = CascadedOrderedModel.objects.create( parent=parent_for_order_2_child ) # Delete positition 1 parent, now there's a hole, which child_with_order_2 should take parent__for_order_1_child.delete() # Refresh children from db child_with_order_0.refresh_from_db() child_with_order_2.refresh_from_db() # Assert the hole has been filled self.assertEqual(child_with_order_0.order, 0) self.assertEqual(child_with_order_2.order, 1) django-ordered-model-3.7.4/tests/urls.py000066400000000000000000000003661440510626600202210ustar00rootroot00000000000000from django.urls import path, include from django.contrib import admin from tests.drf import router admin.autodiscover() admin.site.enable_nav_sidebar = False urlpatterns = [path("admin/", admin.site.urls), path("api/", include(router.urls))] django-ordered-model-3.7.4/tests/utils.py000066400000000000000000000024041440510626600203670ustar00rootroot00000000000000# Query count helpers, copied from django source # Added here for compatibility (django<3.1) from django.db import DEFAULT_DB_ALIAS, connections from django.test.utils import CaptureQueriesContext class _AssertNumQueriesContext(CaptureQueriesContext): def __init__(self, test_case, num, connection): self.test_case = test_case self.num = num super().__init__(connection) def __exit__(self, exc_type, exc_value, traceback): super().__exit__(exc_type, exc_value, traceback) if exc_type is not None: return executed = len(self) self.test_case.assertEqual( executed, self.num, "%d queries executed, %d expected\nCaptured queries were:\n%s" % ( executed, self.num, "\n".join( "%d. %s" % (i, query["sql"]) for i, query in enumerate(self.captured_queries, start=1) ), ), ) def assertNumQueries(self, num, func=None, *args, using=DEFAULT_DB_ALIAS, **kwargs): conn = connections[using] context = _AssertNumQueriesContext(self, num, conn) if func is None: return context with context: func(*args, **kwargs) django-ordered-model-3.7.4/tox.ini000066400000000000000000000025251440510626600170320ustar00rootroot00000000000000[tox] envlist = py{35,36,37,38,39,310}-django22 py{36,37,38,39,310}-django30 py{36,37,38,39,310}-django31 py{36,37,38,39,310}-django32 py{38,39,310}-django40 py{38,39,310}-django41 py{310}-djangoupstream py{310}-drfupstream black [gh-actions] python = 3.4: py34 3.5: py35 3.6: py36 3.7: py37 3.8: py38 3.9: py39 3.10: py310 [testenv] deps = django22: Django~=2.2.17 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django32: Django>=3.2,<4.0 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 djangoupstream: https://github.com/django/django/archive/main.tar.gz drfupstream: Django~=3.2.0 drfupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz django22: djangorestframework~=3.12.0 django30,django31,django32: djangorestframework~=3.12.0 django40: djangorestframework~=3.13.0 django41: djangorestframework~=3.13.0 djangoupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz coverage commands = coverage run {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs} [testenv:black] basepython = python3 skip_install = true deps = black>=19.10b0 commands = black --check --diff ordered_model/ tests/ setup.py [flake8] max-line-length = 100