pax_global_header00006660000000000000000000000064135125307660014522gustar00rootroot0000000000000052 comment=9eef0c77dbf5c4cbb5a41eadbf9e135ddb979da2 django-ordered-model-3.3.0/000077500000000000000000000000001351253076600155075ustar00rootroot00000000000000django-ordered-model-3.3.0/.gitignore000066400000000000000000000000731351253076600174770ustar00rootroot00000000000000/build *.pyc MANIFEST /dist /django_ordered_model.egg-info django-ordered-model-3.3.0/.travis.yml000066400000000000000000000025241351253076600176230ustar00rootroot00000000000000language: python # python 3.7 not available for trusty dist: xenial python: - "3.4" - "3.5" - "3.6" - "3.7" env: - DJANGO="Django>=2.0,<2.1" - DJANGO="Django>=2.1,<2.2" - DJANGO="Django>=2.2,<3.0" - DJANGO="https://github.com/django/django/archive/master.tar.gz" matrix: exclude: - python: "3.4" env: DJANGO="Django>=2.1,<2.2" - python: "3.4" env: DJANGO="Django>=2.2,<3.0" - python: "3.4" env: DJANGO="https://github.com/django/django/archive/master.tar.gz" - python: "3.5" env: DJANGO="https://github.com/django/django/archive/master.tar.gz" include: - name: black python: "3.7" before_script: - pip install black script: black --check --diff ordered_model/ tests/ setup.py allow_failures: - env: DJANGO="https://github.com/django/django/archive/master.tar.gz" install: - pip install $DJANGO - python setup.py sdist bdist_wheel - pip install dist/* - pip install codecov script: script/test-command after_success: - codecov deploy: provider: pypi user: "django-ordered-model" distributions: sdist bdist_wheel password: secure: dmhx2Z6M6k4O0pZsZ6oVpIkWf7/Vv4x36MDX35d8BmaDuD23rQZakm7ScMfHfrnJ8xTVcYsLQSciqMQyov1QfKa4/51YrJrZk63W2iAp1IjTb6FlPofbvworNcuXvEFbemYGHTeRW5S6+T6PPFN7YSJhhMJpyBIVu7rMBTk/bKE= on: tags: true skip_existing: true django-ordered-model-3.3.0/CHANGES.md000066400000000000000000000102261351253076600171020ustar00rootroot00000000000000Change log ========== 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.3.0/Dockerfile000066400000000000000000000001711351253076600175000ustar00rootroot00000000000000FROM python:3 WORKDIR /code COPY requirements.txt /code/requirements.txt RUN pip install -r requirements.txt ADD . /code django-ordered-model-3.3.0/LICENSE000066400000000000000000000026741351253076600165250ustar00rootroot00000000000000Copyright (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.3.0/MANIFEST.in000066400000000000000000000001371351253076600172460ustar00rootroot00000000000000include MANIFEST.in *.md LICENSE *.sh requirements.txt recursive-include ordered_model *.json django-ordered-model-3.3.0/README.md000066400000000000000000000231151351253076600167700ustar00rootroot00000000000000django-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 ------------ ```bash $ python setup.py install ``` You can use Pip: ```bash $ pip install django-ordered-model ``` 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) class Meta(OrderedModel.Meta): pass ``` 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() ``` previous() and next() get the neighbouring objects directly above of below within the ordered stack depending on the direction. ## 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. 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 ----------------- ```python from ordered_model.models import OrderedModelManager, OrderedModel class ItemManager(OrderedModelManager): pass class Item(OrderedModel): objects = ItemManager() ``` 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) ``` 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 PizzaToppingsThroughModelTabularInline(OrderedTabularInline): model = PizzaToppingsThroughModel fields = ('topping', 'order', 'move_up_down_links',) readonly_fields = ('order', 'move_up_down_links',) extra = 1 ordering = ('order',) class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): list_display = ('name', ) inlines = (PizzaToppingsThroughModelTabularInline, ) admin.site.register(Pizza, PizzaAdmin) ``` 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 PizzaToppingsThroughModelStackedInline(OrderedStackedInline): model = PizzaToppingsThroughModel fields = ('topping', 'order', 'move_up_down_links',) readonly_fields = ('order', 'move_up_down_links',) extra = 1 ordering = ('order',) class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): list_display = ('name', ) inlines = (PizzaToppingsThroughModelStackedInline, ) admin.site.register(Pizza, PizzaAdmin) ``` Test suite ---------- Requires Docker. ```bash $ script/test ``` Compatibility with Django and Python ----------------------------------------- |django-ordered-model version | Django version | Python version |-----------------------------|---------------------|-------------------- | **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.3.0/ordered_model/000077500000000000000000000000001351253076600203135ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/__init__.py000066400000000000000000000000001351253076600224120ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/admin.py000066400000000000000000000224231351253076600217600ustar00rootroot00000000000000import operator from functools import update_wrapper, reduce from urllib.parse import urlencode from django.db import models from django.core.paginator import Paginator 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, lookup_needs_distinct 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,) 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, self.admin_site).get_urls() + urls return urls class OrderedInlineMixin(BaseOrderedModelAdmin): 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}_{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.id: return "" # Find the fields which refer to the parent model of this inline, and # use one of them if they aren't None. order_with_respect_to = obj._get_order_with_respect_to_filter_kwargs() or [] fields = [ str(value.pk) for value in order_with_respect_to.values() if value.__class__ is self.parent_model and value is not None and value.pk is not None ] order_obj_name = fields[0] if len(fields) > 0 else None model_info = self._get_model_info() if order_obj_name: 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( "{admin_name}:{app}_{model}_change_order_inline".format( admin_name=self.admin_site.name, **model_info ), args=[order_obj_name, obj.id, "up"], ), "down": reverse( "{admin_name}:{app}_{model}_change_order_inline".format( admin_name=self.admin_site.name, **model_info ), args=[order_obj_name, obj.id, "down"], ), "top": reverse( "{admin_name}:{app}_{model}_change_order_inline".format( admin_name=self.admin_site.name, **model_info ), args=[order_obj_name, obj.id, "top"], ), "bottom": reverse( "{admin_name}:{app}_{model}_change_order_inline".format( admin_name=self.admin_site.name, **model_info ), args=[order_obj_name, obj.id, "bottom"], ), }, "query_string": self.request_query_string, }, ) return "" move_up_down_links.short_description = _("Move") class OrderedTabularInline(OrderedInlineMixin, admin.TabularInline): pass class OrderedStackedInline(OrderedInlineMixin, admin.StackedInline): pass django-ordered-model-3.3.0/ordered_model/locale/000077500000000000000000000000001351253076600215525ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/de/000077500000000000000000000000001351253076600221425ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/de/LC_MESSAGES/000077500000000000000000000000001351253076600237275ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/de/LC_MESSAGES/django.mo000066400000000000000000000007111351253076600255250ustar00rootroot00000000000000,<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.3.0/ordered_model/locale/de/LC_MESSAGES/django.po000066400000000000000000000012401351253076600255260ustar00rootroot00000000000000# 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.3.0/ordered_model/locale/fr/000077500000000000000000000000001351253076600221615ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/fr/LC_MESSAGES/000077500000000000000000000000001351253076600237465ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/fr/LC_MESSAGES/django.po000066400000000000000000000013371351253076600255540ustar00rootroot00000000000000# 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.3.0/ordered_model/locale/it/000077500000000000000000000000001351253076600221665ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/it/LC_MESSAGES/000077500000000000000000000000001351253076600237535ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/it/LC_MESSAGES/django.mo000066400000000000000000000021001351253076600255430ustar00rootroot00000000000000DlHhd@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.3.0/ordered_model/locale/it/LC_MESSAGES/django.po000066400000000000000000000012401351253076600255520ustar00rootroot00000000000000# 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.3.0/ordered_model/locale/pl/000077500000000000000000000000001351253076600221655ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/pl/LC_MESSAGES/000077500000000000000000000000001351253076600237525ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/pl/LC_MESSAGES/django.mo000066400000000000000000000010071351253076600255470ustar00rootroot00000000000000,<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.3.0/ordered_model/locale/pl/LC_MESSAGES/django.po000066400000000000000000000013411351253076600255530ustar00rootroot00000000000000# 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.3.0/ordered_model/locale/zh_Hans/000077500000000000000000000000001351253076600231445ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/zh_Hans/LC_MESSAGES/000077500000000000000000000000001351253076600247315ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/locale/zh_Hans/LC_MESSAGES/django.mo000066400000000000000000000007371351253076600265370ustar00rootroot000000000000004L`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.3.0/ordered_model/locale/zh_Hans/LC_MESSAGES/django.po000066400000000000000000000014001351253076600265260ustar00rootroot00000000000000# 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.3.0/ordered_model/models.py000066400000000000000000000277331351253076600221640ustar00rootroot00000000000000from functools import partial, reduce from django.db import models from django.db.models import Max, Min, F 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): return reduce(lambda i, f: getattr(i, f), field.split(LOOKUP_SEP), obj) 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_order_with_respect_to(self): model = self.model order_with_respect_to = model.order_with_respect_to if isinstance(order_with_respect_to, str): order_with_respect_to = (order_with_respect_to,) if order_with_respect_to is None: raise AssertionError( ( 'ordered model admin "{0}" has not specified "order_with_respect_to"; note that this ' "should go in the model body, and is not to be confused with the Meta property of the same name, " "which is independent Django functionality" ).format(model) ) return order_with_respect_to 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, batch_size=None): order_field_name = self._get_order_field_name() order_with_respect_to = self.model.order_with_respect_to if order_with_respect_to: order_with_respect_to_mapping = {} order_with_respect_to = self._get_order_with_respect_to() for obj in objs: key = tuple( get_lookup_value(obj, field) for field in order_with_respect_to ) if key in order_with_respect_to_mapping: order_with_respect_to_mapping[key] += 1 else: order_with_respect_to_mapping[ key ] = self.filter_by_order_with_respect_to(obj).get_next_order() setattr(obj, order_field_name, order_with_respect_to_mapping[key]) else: for order, obj in enumerate(objs, self.get_next_order()): setattr(obj, order_field_name, order) super().bulk_create(objs, batch_size=batch_size) def _get_order_with_respect_to_filter_kwargs(self, ref): order_with_respect_to = self._get_order_with_respect_to() _get_lookup_value = partial(get_lookup_value, ref) return {field: _get_lookup_value(field) for field in order_with_respect_to} _get_order_with_respect_to_filter_kwargs.queryset_only = False def filter_by_order_with_respect_to(self, ref): order_with_respect_to = self.model.order_with_respect_to if order_with_respect_to: filter_kwargs = self._get_order_with_respect_to_filter_kwargs(ref) return self.filter(**filter_kwargs) return self class OrderedModelManager(models.Manager.from_queryset(OrderedModelQuerySet)): def _get_model(self): order_class_path = self.model.order_class_path if order_class_path: return import_string(order_class_path) return self.model def get_queryset(self): model = self._get_model() return self._queryset_class(model=model, using=self._db, hints=self._hints) 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 polymorpic classes """ objects = OrderedModelManager() order_field_name = None order_with_respect_to = None order_class_path = None class Meta: abstract = True def _get_order_with_respect_to_filter_kwargs(self): return self._meta.default_manager._get_order_with_respect_to_filter_kwargs(self) def _validate_ordering_reference(self, ref): valid = self.order_with_respect_to is None or ( self._get_order_with_respect_to_filter_kwargs() == ref._get_order_with_respect_to_filter_kwargs() ) if not valid: raise ValueError( "{0!r} can only be swapped with instances of {1!r} with equal {2!s} fields.".format( self, self._meta.default_manager._get_model(), " and ".join( [ "'{}'".format(o) for o in self._get_order_with_respect_to_filter_kwargs() ] ), ) ) def get_ordering_queryset(self, qs=None): qs = qs or self._meta.default_manager.all() return qs.filter_by_order_with_respect_to(self) 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 if getattr(self, order_field_name) is None: order = self.get_ordering_queryset().get_next_order() setattr(self, order_field_name, order) super().save(*args, **kwargs) def delete(self, *args, extra_update=None, **kwargs): 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) 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.3.0/ordered_model/static/000077500000000000000000000000001351253076600216025ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/static/ordered_model/000077500000000000000000000000001351253076600244065ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/static/ordered_model/arrow-bottom.gif000066400000000000000000000001201351253076600275220ustar00rootroot00000000000000GIF89a Ȳɱ虙!, XJ5Xyaj\ ;django-ordered-model-3.3.0/ordered_model/static/ordered_model/arrow-down.gif000066400000000000000000000001201351253076600271650ustar00rootroot00000000000000GIF89a Ȳɱ虙!, XJ5Xyaj\bg ;django-ordered-model-3.3.0/ordered_model/static/ordered_model/arrow-top.gif000066400000000000000000000015111351253076600270250ustar00rootroot00000000000000GIF89a Ȳɱ虙!, & (ЀA 4X 0 Dh|H A;django-ordered-model-3.3.0/ordered_model/static/ordered_model/arrow-up.gif000066400000000000000000000015061351253076600266530ustar00rootroot00000000000000GIF89a Ȳɱ虙!, # 4@` 0  ​"ܸ@@;django-ordered-model-3.3.0/ordered_model/templates/000077500000000000000000000000001351253076600223115ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/templates/ordered_model/000077500000000000000000000000001351253076600251155ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/templates/ordered_model/admin/000077500000000000000000000000001351253076600262055ustar00rootroot00000000000000django-ordered-model-3.3.0/ordered_model/templates/ordered_model/admin/order_controls.html000066400000000000000000000006741351253076600321400ustar00rootroot00000000000000{% load static %} django-ordered-model-3.3.0/requirements.txt000066400000000000000000000000071351253076600207700ustar00rootroot00000000000000Django django-ordered-model-3.3.0/script/000077500000000000000000000000001351253076600170135ustar00rootroot00000000000000django-ordered-model-3.3.0/script/release000077500000000000000000000001131351253076600203540ustar00rootroot00000000000000#!/bin/bash set -e git tag $1 git push --tags python setup.py sdist upload django-ordered-model-3.3.0/script/test000077500000000000000000000006111351253076600177160ustar00rootroot00000000000000#!/bin/sh docker build -t django-ordered-model -f - . << EOS FROM python:3 WORKDIR /code RUN pip install django==2.0 VOLUME /code EOS docker run -v $PWD:/code django-ordered-model script/test-command docker build -t django-ordered-model -f - . << EOS FROM python:3 WORKDIR /code RUN pip install django==2.1 VOLUME /code EOS docker run -v $PWD:/code django-ordered-model script/test-command django-ordered-model-3.3.0/script/test-command000077500000000000000000000001351351253076600213330ustar00rootroot00000000000000#!/bin/sh coverage run `which django-admin.py` test --pythonpath=. --settings=tests.settings django-ordered-model-3.3.0/setup.py000066400000000000000000000030201351253076600172140ustar00rootroot00000000000000#!/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.3.0", 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/bfirsh/django-ordered-model", packages=["ordered_model"], 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.3.0/tests/000077500000000000000000000000001351253076600166515ustar00rootroot00000000000000django-ordered-model-3.3.0/tests/__init__.py000066400000000000000000000000001351253076600207500ustar00rootroot00000000000000django-ordered-model-3.3.0/tests/admin.py000066400000000000000000000013651351253076600203200ustar00rootroot00000000000000from django.contrib import admin from ordered_model.admin import ( OrderedModelAdmin, OrderedTabularInline, OrderedInlineModelAdminMixin, ) from .models import Item, PizzaToppingsThroughModel, Pizza class ItemAdmin(OrderedModelAdmin): list_display = ("name", "move_up_down_links") class PizzaToppingTabularInline(OrderedTabularInline): model = PizzaToppingsThroughModel fields = ("order", "move_up_down_links") readonly_fields = ("order", "move_up_down_links") ordering = ("order",) class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): model = Pizza list_display = ("name",) inlines = (PizzaToppingTabularInline,) admin.site.register(Item, ItemAdmin) admin.site.register(Pizza, PizzaAdmin) django-ordered-model-3.3.0/tests/fixtures/000077500000000000000000000000001351253076600205225ustar00rootroot00000000000000django-ordered-model-3.3.0/tests/fixtures/test_items.json000066400000000000000000000017001351253076600235730ustar00rootroot00000000000000[ { "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.3.0/tests/models.py000066400000000000000000000047531351253076600205170ustar00rootroot00000000000000from django.db import models from ordered_model.models import OrderedModel, OrderedModelBase class Item(OrderedModel): name = models.CharField(max_length=100) 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 ) class CustomItem(OrderedModel): id = models.CharField(max_length=100, primary_key=True) name = models.CharField(max_length=100) modified = models.DateTimeField(null=True, blank=True) 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",) 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") 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) 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" django-ordered-model-3.3.0/tests/settings.py000066400000000000000000000020231351253076600210600ustar00rootroot00000000000000import django DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3"}} ROOT_URLCONF = "tests.urls" INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.messages", "django.contrib.sessions", "ordered_model", "tests", ] SECRET_KEY = "topsecret" 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", ] }, } ] django-ordered-model-3.3.0/tests/tests.py000066400000000000000000001002651351253076600203710ustar00rootroot00000000000000import uuid from django.contrib.auth.models import User from django.utils.timezone import now from django.test import TestCase from tests.models import ( Answer, Item, Question, CustomItem, CustomOrderFieldModel, Pizza, Topping, PizzaToppingsThroughModel, OpenQuestion, MultipleChoiceQuestion, ItemGroup, GroupedItem, TestUser, ) 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) 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 CustomPKTest(TestCase): def setUp(self): self.item1 = CustomItem.objects.create(id=str(uuid.uuid4()), name="1") self.item2 = CustomItem.objects.create(id=str(uuid.uuid4()), name="2") self.item3 = CustomItem.objects.create(id=str(uuid.uuid4()), name="3") self.item4 = CustomItem.objects.create(id=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): res = self.client.get("/admin/tests/pizza/") self.assertEqual(res.status_code, 200) 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.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) 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) 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(pizza=self.p2, 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 PolymorpicOrderGenerationTests(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) 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], ) django-ordered-model-3.3.0/tests/urls.py000066400000000000000000000002051351253076600202050ustar00rootroot00000000000000from django.urls import path from django.contrib import admin admin.autodiscover() urlpatterns = [path("admin/", admin.site.urls)] django-ordered-model-3.3.0/tox.ini000066400000000000000000000000371351253076600170220ustar00rootroot00000000000000[flake8] max-line-length = 100