django-ordered-model-1.4.3/0000775000175000017500000000000013151322627016347 5ustar travistravis00000000000000django-ordered-model-1.4.3/PKG-INFO0000664000175000017500000000124513151322627017446 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: django-ordered-model Version: 1.4.3 Summary: Allows Django models to be ordered and provides a simple admin interface for reordering them. Home-page: http://github.com/bfirsh/django-ordered-model Author: Ben Firshman Author-email: ben@firshman.co.uk License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Requires: Django django-ordered-model-1.4.3/setup.py0000664000175000017500000000263613151322612020062 0ustar travistravis00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup with open('requirements.txt') as f: requires = f.read().splitlines() setup( name='django-ordered-model', version='1.4.3', 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', 'ordered_model.tests', ], 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 :: 2', 'Programming Language :: Python :: 3', ], zip_safe = False, package_data={'ordered_model': ['static/ordered_model/arrow-up.gif', 'static/ordered_model/arrow-down.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-1.4.3/CHANGES.md0000664000175000017500000000543413151322612017741 0ustar travistravis00000000000000Change log ========== 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-1.4.3/django_ordered_model.egg-info/0000775000175000017500000000000013151322627024167 5ustar travistravis00000000000000django-ordered-model-1.4.3/django_ordered_model.egg-info/PKG-INFO0000664000175000017500000000124513151322627025266 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: django-ordered-model Version: 1.4.3 Summary: Allows Django models to be ordered and provides a simple admin interface for reordering them. Home-page: http://github.com/bfirsh/django-ordered-model Author: Ben Firshman Author-email: ben@firshman.co.uk License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Requires: Django django-ordered-model-1.4.3/django_ordered_model.egg-info/top_level.txt0000664000175000017500000000001613151322627026716 0ustar travistravis00000000000000ordered_model django-ordered-model-1.4.3/django_ordered_model.egg-info/dependency_links.txt0000664000175000017500000000000113151322627030235 0ustar travistravis00000000000000 django-ordered-model-1.4.3/django_ordered_model.egg-info/not-zip-safe0000664000175000017500000000000113151322616026413 0ustar travistravis00000000000000 django-ordered-model-1.4.3/django_ordered_model.egg-info/SOURCES.txt0000664000175000017500000000164113151322627026055 0ustar travistravis00000000000000CHANGES.md LICENSE MANIFEST.in README.md requirements.txt setup.py django_ordered_model.egg-info/PKG-INFO django_ordered_model.egg-info/SOURCES.txt django_ordered_model.egg-info/dependency_links.txt django_ordered_model.egg-info/not-zip-safe django_ordered_model.egg-info/top_level.txt ordered_model/__init__.py ordered_model/admin.py ordered_model/models.py ordered_model/locale/de/LC_MESSAGES/django.mo ordered_model/locale/de/LC_MESSAGES/django.po ordered_model/locale/pl/LC_MESSAGES/django.mo ordered_model/locale/pl/LC_MESSAGES/django.po ordered_model/static/ordered_model/arrow-down.gif ordered_model/static/ordered_model/arrow-up.gif ordered_model/templates/ordered_model/admin/order_controls.html ordered_model/tests/__init__.py ordered_model/tests/admin.py ordered_model/tests/models.py ordered_model/tests/settings.py ordered_model/tests/tests.py ordered_model/tests/urls.py ordered_model/tests/fixtures/test_items.jsondjango-ordered-model-1.4.3/requirements.txt0000664000175000017500000000000713151322612021622 0ustar travistravis00000000000000Django django-ordered-model-1.4.3/MANIFEST.in0000664000175000017500000000013713151322612020100 0ustar travistravis00000000000000include MANIFEST.in *.md LICENSE *.sh requirements.txt recursive-include ordered_model *.json django-ordered-model-1.4.3/LICENSE0000664000175000017500000000267413151322612017357 0ustar travistravis00000000000000Copyright (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-1.4.3/setup.cfg0000664000175000017500000000004613151322627020170 0ustar travistravis00000000000000[egg_info] tag_build = tag_date = 0 django-ordered-model-1.4.3/README.md0000664000175000017500000001505613151322612017627 0ustar travistravis00000000000000django-ordered-model ==================== [![Build Status](https://secure.travis-ci.org/bfirsh/django-ordered-model.png?branch=master)](https://travis-ci.org/bfirsh/django-ordered-model) 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/ Requires: * Django >=1.8 * Python 2.7 or >=3.3 Installation ------------ $ python setup.py install You can use Pip: $ 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 ``` 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 neighouring 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()} ``` ## 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 seperate 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') ``` 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) ``` 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 the following in the admin.py file: ```python from django.contrib import admin from ordered_model.admin import OrderedTabularInline from models import Pizza, PizzaToppingsThroughModel class PizzaToppingsThroughModelInline(OrderedTabularInline): model = PizzaToppingsThroughModel fields = ('topping', 'order', 'move_up_down_links',) readonly_fields = ('order', 'move_up_down_links',) extra = 1 ordering = ('order',) class PizzaAdmin(admin.ModelAdmin): list_display = ('name', ) inlines = (PizzaToppingsThroughModelInline, ) def get_urls(self): urls = super(PizzaAdmin, self).get_urls() for inline in self.inlines: if hasattr(inline, 'get_urls'): urls = inline.get_urls(self) + urls return urls admin.site.register(Pizza, PizzaAdmin) ``` Test suite ---------- Requires Docker. $ script/test django-ordered-model-1.4.3/ordered_model/0000775000175000017500000000000013151322627021153 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/tests/0000775000175000017500000000000013151322627022315 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/tests/settings.py0000664000175000017500000000172613151322612024527 0ustar travistravis00000000000000DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3' } } ROOT_URLCONF = 'ordered_model.tests.urls' INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'ordered_model', 'ordered_model.tests', ] SECRET_KEY = 'topsecret' MIDDLEWARE_CLASSES = ( '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-ordered-model-1.4.3/ordered_model/tests/urls.py0000664000175000017500000000022113151322612023641 0ustar travistravis00000000000000from django.conf.urls import url from django.contrib import admin admin.autodiscover() urlpatterns = [ url(r'^admin/', admin.site.urls), ] django-ordered-model-1.4.3/ordered_model/tests/fixtures/0000775000175000017500000000000013151322627024166 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/tests/fixtures/test_items.json0000664000175000017500000000170013151322612027231 0ustar travistravis00000000000000[ { "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-1.4.3/ordered_model/tests/tests.py0000664000175000017500000005172013151322612024030 0ustar travistravis00000000000000from django.contrib import admin from django.contrib.auth.models import User from django.utils.timezone import now from django.test import TestCase import uuid from ordered_model.tests.models import ( Answer, Item, Question, CustomItem, CustomOrderFieldModel, Pizza, Topping, PizzaToppingsThroughModel, OpenQuestion, MultipleChoiceQuestion ) from ordered_model.tests.models import TestUser from .admin import ItemAdmin 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_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_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_swap(self): with self.assertRaises(ValueError): self.q1_a1.swap([self.q2_a1]) 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_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 = User.objects.create_superuser("admin", "a@example.com", "admin") self.assertTrue(self.client.login(username="admin", password="admin")) item1 = Item.objects.create(name='item1') item2 = Item.objects.create(name='item2') def test_move_up_down_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)) 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) 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_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 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) django-ordered-model-1.4.3/ordered_model/tests/admin.py0000664000175000017500000000035113151322612023750 0ustar travistravis00000000000000from 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) django-ordered-model-1.4.3/ordered_model/tests/__init__.py0000664000175000017500000000000013151322612024406 0ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/tests/models.py0000664000175000017500000000422113151322612024143 0ustar travistravis00000000000000from 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 u"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) django-ordered-model-1.4.3/ordered_model/locale/0000775000175000017500000000000013151322627022412 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/locale/de/0000775000175000017500000000000013151322627023002 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/locale/de/LC_MESSAGES/0000775000175000017500000000000013151322627024567 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/locale/de/LC_MESSAGES/django.mo0000664000175000017500000000071113151322612026357 0ustar travistravis00000000000000Þ•,<PQjVÁMoveProject-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-1.4.3/ordered_model/locale/de/LC_MESSAGES/django.po0000664000175000017500000000124013151322612026360 0ustar travistravis00000000000000# 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-1.4.3/ordered_model/locale/pl/0000775000175000017500000000000013151322627023025 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/locale/pl/LC_MESSAGES/0000775000175000017500000000000013151322627024612 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/locale/pl/LC_MESSAGES/django.mo0000664000175000017500000000100713151322612026401 0ustar travistravis00000000000000Þ•,<PQ¤V û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-1.4.3/ordered_model/locale/pl/LC_MESSAGES/django.po0000664000175000017500000000134113151322612026405 0ustar travistravis00000000000000# 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-1.4.3/ordered_model/templates/0000775000175000017500000000000013151322627023151 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/templates/ordered_model/0000775000175000017500000000000013151322627025755 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/templates/ordered_model/admin/0000775000175000017500000000000013151322627027045 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/templates/ordered_model/admin/order_controls.html0000664000175000017500000000034413151322612032764 0ustar travistravis00000000000000{% load static %} django-ordered-model-1.4.3/ordered_model/static/0000775000175000017500000000000013151322627022442 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/static/ordered_model/0000775000175000017500000000000013151322627025246 5ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/static/ordered_model/arrow-down.gif0000664000175000017500000000012013151322612030017 0ustar travistravis00000000000000GIF89a ¢ÈÈȲ²²ÉÉɱ±±èèè™™™ÿÿÿ!ù, XºJ5ˆXˆya„jú\ç…bg‰ ;django-ordered-model-1.4.3/ordered_model/static/ordered_model/arrow-up.gif0000664000175000017500000000150613151322612027505 0ustar travistravis00000000000000GIF89a ÷ÈÈȲ²²ÉÉɱ±±èèè™™™!ù, # 4@` Á 0 áÀ à​"ܸ‘@@;django-ordered-model-1.4.3/ordered_model/admin.py0000664000175000017500000002751113151322612022615 0ustar travistravis00000000000000from functools import update_wrapper from django.conf.urls import url from django.core.paginator import Paginator from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_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.views.main import ChangeList try: from django.urls import reverse except ImportError: # Django < 1.10 from django.core.urlresolvers import reverse from django.utils.encoding import (escape_uri_path, iri_to_uri) class OrderedModelAdmin(admin.ModelAdmin): def get_urls(self): def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) return update_wrapper(wrapper, view) return [ url(r'^(.+)/move-(up)/$', wrap(self.move_view), name='{app}_{model}_order_up'.format(**self._get_model_info())), url(r'^(.+)/move-(down)/$', wrap(self.move_view), name='{app}_{model}_order_down'.format(**self._get_model_info())), ] + super(OrderedModelAdmin, self).get_urls() def _get_changelist(self, request): list_display = self.get_list_display(request) list_display_links = self.get_list_display_links(request, list_display) cl = ChangeList(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) return cl request_query_string = '' def changelist_view(self, request, extra_context=None): cl = self._get_changelist(request) self.request_query_string = cl.get_query_string() return super(OrderedModelAdmin, self).changelist_view(request, extra_context) def move_view(self, request, object_id, direction): qs = self._get_changelist(request).get_queryset(request) obj = get_object_or_404(self.model, pk=unquote(object_id)) obj.move(direction, qs) # 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}_order_up".format( admin_name=self.admin_site.name, **model_info), args=[obj.pk, 'up']), 'down': reverse("{admin_name}:{app}_{model}_order_down".format( admin_name=self.admin_site.name, **model_info), args=[obj.pk, 'down']), }, 'query_string': self.request_query_string }) move_up_down_links.allow_tags = True move_up_down_links.short_description = _(u'Move') def _get_model_info(self): return { 'app': self.model._meta.app_label, 'model': self.model._meta.model_name, } class OrderedTabularInline(admin.TabularInline): ordering = None list_display = ('__str__',) list_display_links = () list_filter = () list_select_related = False list_per_page = 100 list_max_show_all = 200 list_editable = () search_fields = () date_hierarchy = None paginator = Paginator preserve_filters = True @classmethod def get_model_info(cls): return dict(app=cls.model._meta.app_label, model=cls.model._meta.model_name) @classmethod def get_urls(cls, model_admin): def wrap(view): def wrapper(*args, **kwargs): return model_admin.admin_site.admin_view(view)(*args, **kwargs) return update_wrapper(wrapper, view) return [ url(r'^(.+)/{model}/(.+)/move-(up)/$'.format(**cls.get_model_info()), wrap(cls.move_view), name='{app}_{model}_order_up_inline'.format(**cls.get_model_info())), url(r'^(.+)/{model}/(.+)/move-(down)/$'.format(**cls.get_model_info()), wrap(cls.move_view), name='{app}_{model}_order_down_inline'.format(**cls.get_model_info())), ] @classmethod def get_list_display(cls, request): """ Return a sequence containing the fields to be displayed on the changelist. """ return cls.list_display @classmethod def get_list_display_links(cls, request, list_display): """ Return a sequence containing the fields to be displayed as links on the changelist. The list_display parameter is the list of fields returned by get_list_display(). """ if cls.list_display_links or not list_display: return cls.list_display_links else: # Use only the first item in list_display as link return list(list_display)[:1] @classmethod def _get_changelist(cls, request): list_display = cls.get_list_display(request) list_display_links = cls.get_list_display_links(request, list_display) cl = ChangeList(request, cls.model, list_display, list_display_links, cls.list_filter, cls.date_hierarchy, cls.search_fields, cls.list_select_related, cls.list_per_page, cls.list_max_show_all, cls.list_editable, cls) return cl request_query_string = '' @classmethod def changelist_view(cls, request, extra_context=None): cl = cls._get_changelist(request) cls.request_query_string = cl.get_query_string() return super(OrderedTabularInline, cls).changelist_view(request, extra_context) @classmethod def get_queryset(cls, request): """ Returns a QuerySet of all model instances that can be edited by the admin site. This is used by changelist_view. """ qs = cls.model._default_manager.get_queryset() # TODO: this should be handled by some parameter to the ChangeList. ordering = cls.get_ordering(request) if ordering: qs = qs.order_by(*ordering) return qs @classmethod def get_ordering(cls, request): """ Hook for specifying field ordering. """ return cls.ordering or () # otherwise we might try to *None, which is bad ;) @classmethod def get_paginator(cls, request, queryset, per_page, orphans=0, allow_empty_first_page=True): return cls.paginator(queryset, per_page, orphans, allow_empty_first_page) @classmethod def get_search_fields(cls, request): """ Returns a sequence containing the fields to be searched whenever somebody submits a search query. """ return cls.search_fields @classmethod def get_search_results(cls, request, queryset, search_term): """ Returns a tuple containing a queryset to implement the search, and a boolean indicating if the results may contain duplicates. """ # Apply keyword searches. def construct_search(field_name): if field_name.startswith('^'): return "{0!s}__istartswith".format(field_name[1:]) elif field_name.startswith('='): return "{0!s}__iexact".format(field_name[1:]) elif field_name.startswith('@'): return "{0!s}__search".format(field_name[1:]) else: return "{0!s}__icontains".format(field_name) use_distinct = False search_fields = cls.get_search_fields(request) if search_fields and search_term: orm_lookups = [construct_search(str(search_field)) for search_field in search_fields] for bit in search_term.split(): or_queries = [models.Q(**{orm_lookup: bit}) for orm_lookup in orm_lookups] queryset = queryset.filter(reduce(operator.or_, or_queries)) if not use_distinct: for search_spec in orm_lookups: if lookup_needs_distinct(cls.opts, search_spec): use_distinct = True break return queryset, use_distinct @classmethod def move_view(cls, request, admin_id, object_id, direction): qs = cls._get_changelist(request).get_queryset(request) obj = get_object_or_404(cls.model, pk=unquote(object_id)) obj.move(direction, qs) # 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) @classmethod def get_preserved_filters(cls, request): """ Returns the preserved filters querystring. """ match = request.resolver_match if cls.preserve_filters and match: opts = cls.model._meta current_url = '{0!s}:{1!s}'.format(match.app_name, match.url_name) changelist_url = 'admin:{0!s}_{1!s}_changelist'.format(opts.app_label, opts.model_name) if current_url == changelist_url: preserved_filters = request.GET.urlencode() else: preserved_filters = request.GET.get('_changelist_filters') if preserved_filters: return urlencode({'_changelist_filters': preserved_filters}) return '' 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() or [] parent_model = self.parent_model._meta fields = [ str(value.pk) for field_name, value in order_with_respect_to 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 if order_obj_name: return render_to_string("ordered_model/admin/order_controls.html", { 'app_label': self.model._meta.app_label, 'model_name': self.model._meta.model_name, 'module_name': self.model._meta.model_name, # backwards compat 'object_id': obj.id, 'urls': { 'up': reverse("admin:{app}_{model}_order_up_inline".format( admin_name=self.admin_site.name, **self.get_model_info()), args=[order_obj_name, obj.id, 'up']), 'down': reverse("admin:{app}_{model}_order_down_inline".format( admin_name=self.admin_site.name, **self.get_model_info()), args=[order_obj_name, obj.id, 'down']), }, 'query_string': self.request_query_string }) return '' move_up_down_links.allow_tags = True move_up_down_links.short_description = _(u'Move') django-ordered-model-1.4.3/ordered_model/__init__.py0000664000175000017500000000000013151322612023244 0ustar travistravis00000000000000django-ordered-model-1.4.3/ordered_model/models.py0000664000175000017500000002464013151322612023010 0ustar travistravis00000000000000import warnings from django.db import models from django.db.models import Max, Min, F from django.utils.translation import ugettext as _ from django.utils import six """ Convert a string containing module.submodule.classname to a Class. """ def _order_model_get_class( classpath ): parts = classpath.split('.') module = ".".join(parts[:-1]) m = __import__( module ) for comp in parts[1:]: m = getattr(m, comp) return m 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 """ order_field_name = None order_with_respect_to = None order_class_path = None class Meta: abstract = True def _get_class_for_ordering_queryset(self): if self.order_class_path: return _order_model_get_class(self.order_class_path) return self.__class__ def _get_order_with_respect_to(self): if isinstance(self.order_with_respect_to, six.string_types): self.order_with_respect_to = (self.order_with_respect_to,) if self.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(self)) return [(field, getattr(self, field)) for field in self.order_with_respect_to] def _valid_ordering_reference(self, reference): return self.order_with_respect_to is None or ( self._get_order_with_respect_to() == reference._get_order_with_respect_to() ) def get_ordering_queryset(self, qs=None): qs = qs or self._get_class_for_ordering_queryset().objects.all() order_with_respect_to = self.order_with_respect_to if order_with_respect_to: order_values = self._get_order_with_respect_to() qs = qs.filter(**dict(order_values)) return qs def save(self, *args, **kwargs): if getattr(self, self.order_field_name) is None: c = self.get_ordering_queryset().aggregate(Max(self.order_field_name)).get(self.order_field_name + '__max') setattr(self, self.order_field_name, 0 if c is None else c + 1) super(OrderedModelBase, self).save(*args, **kwargs) def delete(self, *args, **kwargs): qs = self.get_ordering_queryset() update_kwargs = {self.order_field_name: F(self.order_field_name) - 1} extra = kwargs.pop('extra_update', None) if extra: update_kwargs.update(extra) qs.filter(**{self.order_field_name + '__gt': getattr(self, self.order_field_name)})\ .update(**update_kwargs) super(OrderedModelBase, self).delete(*args, **kwargs) def _move(self, up, qs=None): qs = self.get_ordering_queryset(qs) if up: qs = qs.order_by('-' + self.order_field_name)\ .filter(**{self.order_field_name + '__lt': getattr(self, self.order_field_name)}) else: qs = qs.filter(**{self.order_field_name + '__gt': getattr(self, self.order_field_name)}) try: replacement = qs[0] except IndexError: # already first/last return order, replacement_order = getattr(self, self.order_field_name), getattr(replacement, self.order_field_name) setattr(self, self.order_field_name, replacement_order) setattr(replacement, self.order_field_name, order) self.save() replacement.save() def move(self, direction, qs=None): warnings.warn( _("The method move() is deprecated and will be removed in the next release."), DeprecationWarning ) if direction == 'up': self.up() else: self.down() def move_down(self): """ Move this object down one position. """ warnings.warn( _("The method move_down() is deprecated and will be removed in the next release. Please use down() instead!"), DeprecationWarning ) return self.down() def move_up(self): """ Move this object up one position. """ warnings.warn( _("The method move_up() is deprecated and will be removed in the next release. Please use up() instead!"), DeprecationWarning ) return self.up() def swap(self, qs): """ Swap the positions of this object with a reference object. """ try: replacement = qs[0] except IndexError: # already first/last return if not self._valid_ordering_reference(replacement): raise ValueError( "{0!r} can only be swapped with instances of {1!r} with equal {2!s} fields.".format( self, self._get_class_for_ordering_queryset(), ' and '.join(["'{}'".format(o[0]) for o in self._get_order_with_respect_to()]) ) ) order, replacement_order = getattr(self, self.order_field_name), getattr(replacement, self.order_field_name) setattr(self, self.order_field_name, replacement_order) setattr(replacement, self.order_field_name, order) self.save() replacement.save() def up(self): """ Move this object up one position. """ self.swap(self.get_ordering_queryset() .filter(**{self.order_field_name + '__lt': getattr(self, self.order_field_name)}) .order_by('-' + self.order_field_name)) def down(self): """ Move this object down one position. """ self.swap(self.get_ordering_queryset().filter(**{self.order_field_name + '__gt': getattr(self, self.order_field_name)})) def to(self, order, extra_update=None): """ Move object to a certain position, updating all affected objects to move accordingly up or down. """ if order is None or getattr(self, self.order_field_name) == order: # object is already at desired position return qs = self.get_ordering_queryset() if getattr(self, self.order_field_name) > order: update_kwargs = {self.order_field_name: F(self.order_field_name) + 1} if extra_update: update_kwargs.update(extra_update) qs.filter(**{self.order_field_name + '__lt': getattr(self, self.order_field_name), self.order_field_name + '__gte': order})\ .update(**update_kwargs) else: update_kwargs = {self.order_field_name: F(self.order_field_name) - 1} if extra_update: update_kwargs.update(extra_update) qs.filter(**{self.order_field_name + '__gt': getattr(self, self.order_field_name), self.order_field_name + '__lte': order})\ .update(**update_kwargs) setattr(self, self.order_field_name, order) self.save() def above(self, ref, extra_update=None): """ Move this object above the referenced object. """ if not self._valid_ordering_reference(ref): raise ValueError( "{0!r} can only be swapped with instances of {1!r} with equal {2!s} fields.".format( self, self._get_class_for_ordering_queryset(), ' and '.join(["'{}'".format(o[0]) for o in self._get_order_with_respect_to()]) ) ) if getattr(self, self.order_field_name) == getattr(ref, self.order_field_name): return if getattr(self, self.order_field_name) > getattr(ref, self.order_field_name): o = getattr(ref, self.order_field_name) else: o = self.get_ordering_queryset()\ .filter(**{self.order_field_name + '__lt': getattr(ref, self.order_field_name)})\ .aggregate(Max(self.order_field_name))\ .get(self.order_field_name + '__max') or 0 self.to(o, extra_update=extra_update) def below(self, ref, extra_update=None): """ Move this object below the referenced object. """ if not self._valid_ordering_reference(ref): raise ValueError( "{0!r} can only be swapped with instances of {1!r} with equal {2!s} fields.".format( self, self._get_class_for_ordering_queryset(), ' and '.join(["'{}'".format(o[0]) for o in self._get_order_with_respect_to()]) ) ) if getattr(self, self.order_field_name) == getattr(ref, self.order_field_name): return if getattr(self, self.order_field_name) > getattr(ref, self.order_field_name): o = self.get_ordering_queryset()\ .filter(**{self.order_field_name + '__gt': getattr(ref, self.order_field_name)})\ .aggregate(Min(self.order_field_name))\ .get(self.order_field_name + '__min') or 0 else: o = getattr(ref, self.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().aggregate(Min(self.order_field_name)).get(self.order_field_name + '__min') 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().aggregate(Max(self.order_field_name)).get(self.order_field_name + '__max') 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(editable=False, db_index=True) order_field_name = 'order' class Meta: abstract = True ordering = ('order',)