From a44fdc2117907fda22eb8f4f481b7614f242206d Mon Sep 17 00:00:00 2001 From: Dominik Bruhn Date: Tue, 4 Jan 2022 15:44:22 +0100 Subject: [PATCH 01/74] Django 4.0 compatibility: Fix ChangeList constructor for Admin (#256) * Django 4.0 compatibility Starting Django 4.0 the `ChangeList` constructor requires a last argument to provide the `search_help_text`: * fix lint, add release notes Co-authored-by: Chris Shucksmith --- CHANGES.md | 6 ++++++ ordered_model/admin.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index fa2dcc32..5bb4373c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ Change log ========== +Unreleased +---------- + +- Django 4.0 compatibility: Fix ChangeList constructor for Admin (#256) + + 3.4.3 - 2021-04-20 ------------------ diff --git a/ordered_model/admin.py b/ordered_model/admin.py index d7089e30..74d66a8c 100644 --- a/ordered_model/admin.py +++ b/ordered_model/admin.py @@ -45,6 +45,9 @@ def _get_changelist(self, request): if VERSION >= (2, 1): args = args + (self.sortable_by,) + if VERSION >= (4, 0): + args = args + (self.search_help_text,) + return ChangeList(*args) @csrf_protect_m From ec7de30e948d37175edf99c9a0585c03cf71e71f Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Tue, 4 Jan 2022 20:40:14 +0530 Subject: [PATCH 02/74] Use assertEqual instead of assertEquals for Python 3.11 compatibility. (#255) --- CHANGES.md | 1 + tests/tests.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5bb4373c..8768a7c3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ Unreleased ---------- - Django 4.0 compatibility: Fix ChangeList constructor for Admin (#256) +- Remove usage of `assertEquals` in tests (#255) 3.4.3 - 2021-04-20 diff --git a/tests/tests.py b/tests/tests.py index 01908989..9eac0255 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1064,7 +1064,7 @@ def test_reorder_with_respect_to(self): [0, 1, 2], ) - self.assertEquals( + self.assertEqual( "changing order of tests.GroupedItem (3) from 1 to 2\n", out.getvalue() ) @@ -1111,6 +1111,6 @@ def test_delete_bypass(self): ["1", "2", "4"], [i.answer for i in OpenQuestion.objects.all()] ) - self.assertEquals( + self.assertEqual( "changing order of tests.OpenQuestion (4) from 3 to 2\n", out.getvalue() ) From 406210e603b7e370401f92f280e669c6767e5529 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 19 Apr 2021 13:20:45 +0100 Subject: [PATCH 03/74] add screenshots of Admin to README --- CHANGES.md | 1 + README.md | 31 +++++++++++++++++++------------ static/items.png | Bin 0 -> 32791 bytes static/pizza-stacked.png | Bin 0 -> 93154 bytes static/pizza.png | Bin 0 -> 63790 bytes 5 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 static/items.png create mode 100644 static/pizza-stacked.png create mode 100644 static/pizza.png diff --git a/CHANGES.md b/CHANGES.md index 8768a7c3..129b0add 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Unreleased - Django 4.0 compatibility: Fix ChangeList constructor for Admin (#256) - Remove usage of `assertEquals` in tests (#255) +- Add admin screenshots to README 3.4.3 - 2021-04-20 diff --git a/README.md b/README.md index 87def2ac..0909547c 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,6 @@ 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. @@ -249,7 +247,7 @@ class Item(OrderedModel): Custom ordering field --------------------- -Extending `OrderedModel` creates a `models.PositiveIntegerField` field called `order` and the appropriate migrations. If you wish to use an existing model field to store the ordering, you can set the attribute `order_field_name` to match your field name: +Extending `OrderedModel` creates a `models.PositiveIntegerField` field called `order` and the appropriate migrations. It customises the default `class Meta` to then order returned querysets by this field. If you wish to use an existing model field to store the ordering, subclass `OrderedModelBase` instead and set the attribute `order_field_name` to match your field name and the `ordering` attribute on `Meta`: ```python class MyModel(OrderedModelBase): @@ -260,7 +258,7 @@ class MyModel(OrderedModelBase): class Meta: ordering = ("sort_order",) ``` - +Setting `order_field_name` is specific for this library to know which field to change when ordering actions are taken. The `Meta` `ordering` line is existing Django functionality to use a field for sorting. See `tests/models.py` object `CustomOrderFieldModel` for an example. @@ -282,6 +280,9 @@ class ItemAdmin(OrderedModelAdmin): admin.site.register(Item, ItemAdmin) ``` +![ItemAdmin screenshot](./static/items.png) + + For a many-to-many relationship you need one of the following inlines. `OrderedTabularInline` or `OrderedStackedInline` just like the django admin. @@ -294,22 +295,26 @@ from ordered_model.admin import OrderedTabularInline, OrderedInlineModelAdminMix from models import Pizza, PizzaToppingsThroughModel -class PizzaToppingsThroughModelTabularInline(OrderedTabularInline): +class PizzaToppingsTabularInline(OrderedTabularInline): model = PizzaToppingsThroughModel fields = ('topping', 'order', 'move_up_down_links',) readonly_fields = ('order', 'move_up_down_links',) - extra = 1 ordering = ('order',) + extra = 1 class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): + model = Pizza list_display = ('name', ) - inlines = (PizzaToppingsThroughModelTabularInline, ) + inlines = (PizzaToppingsTabularInline, ) admin.site.register(Pizza, PizzaAdmin) ``` +![PizzaAdmin screenshot](./static/pizza.png) + + For the `OrderedStackedInline` it will look like this: ```python @@ -318,22 +323,24 @@ from ordered_model.admin import OrderedStackedInline, OrderedInlineModelAdminMix from models import Pizza, PizzaToppingsThroughModel -class PizzaToppingsThroughModelStackedInline(OrderedStackedInline): +class PizzaToppingsStackedInline(OrderedStackedInline): model = PizzaToppingsThroughModel - fields = ('topping', 'order', 'move_up_down_links',) - readonly_fields = ('order', 'move_up_down_links',) - extra = 1 + fields = ('topping', 'move_up_down_links',) + readonly_fields = ('move_up_down_links',) ordering = ('order',) + extra = 1 class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): list_display = ('name', ) - inlines = (PizzaToppingsThroughModelStackedInline, ) + inlines = (PizzaToppingsStackedInline, ) admin.site.register(Pizza, PizzaAdmin) ``` +![PizzaAdmin screenshot](./static/pizza-stacked.png) + **Note:** `OrderedModelAdmin` requires the inline subclasses of `OrderedTabularInline` and `OrderedStackedInline` to be listed on `inlines` so that we register appropriate URL routes. If you are using Django 3.0 feature `get_inlines()` or `get_inline_instances()` to return the list of inlines dynamically, consider it a filter and still add them to `inlines` or you might encounter a “No Reverse Match” error when accessing model change view. Re-ordering models diff --git a/static/items.png b/static/items.png new file mode 100644 index 0000000000000000000000000000000000000000..909b1b8f99df73c19a6641d5911c079eb2ecb93a GIT binary patch literal 32791 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
  • Df4@9YJH_rc~=MN>$5eCw3| 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 literal 0 HcmV?d00001 diff --git a/static/pizza-stacked.png b/static/pizza-stacked.png new file mode 100644 index 0000000000000000000000000000000000000000..9236f02b808ad7b1c9e25f9611713a53509ff5c0 GIT binary patch literal 93154 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 O$ezD+F751Jcm5Xup}7qJ literal 0 HcmV?d00001 diff --git a/static/pizza.png b/static/pizza.png new file mode 100644 index 0000000000000000000000000000000000000000..22710ea5dee48e6511b53e956e4ed88c68a2ae97 GIT binary patch literal 63790 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 literal 0 HcmV?d00001 From 48d22b59f8bed97a6dc1983477c2c6e527276ded Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 19 Apr 2021 13:22:03 +0100 Subject: [PATCH 04/74] add script to refresh screenshots plus sample data --- script/take_screenshots.sh | 46 +++++++++ tests/admin.py | 4 +- tests/autoauth.py | 10 ++ tests/fixtures/screenshot-sample-data.json | 105 +++++++++++++++++++++ tests/settings_autoauth.py | 3 + tests/urls.py | 1 + 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100755 script/take_screenshots.sh create mode 100644 tests/autoauth.py create mode 100644 tests/fixtures/screenshot-sample-data.json create mode 100644 tests/settings_autoauth.py diff --git a/script/take_screenshots.sh b/script/take_screenshots.sh new file mode 100755 index 00000000..3383c8e3 --- /dev/null +++ b/script/take_screenshots.sh @@ -0,0 +1,46 @@ +# requires ubuntu +# sudo apt install cutycapt xvfb + +set -x + +# https://stackoverflow.com/questions/24390488/django-admin-without-authentication +# https://askubuntu.com/questions/75058/how-can-i-take-a-full-page-screenshot-of-a-webpage-from-the-command-line + +# delete test DB if it exists +rm -f testdb +rm -Rf tests/staticfiles +mkdir -p tests/migrations tests/staticfiles +touch tests/migrations/__init__.py +mkdir -p static +killall django-admin + +function djangoadmin() { + django-admin $1 --pythonpath=. --settings=tests.settings --skip-checks $2 +} +djangoadmin "makemigrations" +djangoadmin "migrate" +# requires Django > 3.0 +DJANGO_SUPERUSER_PASSWORD=password DJANGO_SUPERUSER_EMAIL="x@test.com" DJANGO_SUPERUSER_USERNAME=admin \ + djangoadmin "createsuperuser" "--no-input" +djangoadmin "collectstatic" + +# to refresh sample data, use runserver then this export command +# django-admin dumpdata --pythonpath=. --settings=tests.settings tests --output tests/fixtures/screenshot-sample-data.json --indent 4 + +djangoadmin "loaddata" "screenshot-sample-data" +django-admin runserver --pythonpath=. --settings=tests.settings_autoauth 7000 & +sleep 2 + +function capture() { + xvfb-run --server-args="-screen 0, 1024x768x24" cutycapt --url=http://localhost:7000/$1 --out=static/$2 +} +capture "admin/tests/item/" "items.png" +capture "admin/tests/pizza/1/change/" "pizza.png" +capture "admin/tests/pizzaproxy/1/change/" "pizza-stacked.png" + +sleep 1 +killall django-admin +rm -Rf tests/migrations +rm -Rf tests/staticfiles + + diff --git a/tests/admin.py b/tests/admin.py index 63f7ff43..77e92de5 100644 --- a/tests/admin.py +++ b/tests/admin.py @@ -40,8 +40,8 @@ class PizzaAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): # README example for StackedInline class PizzaToppingStackedInline(OrderedStackedInline): model = PizzaToppingsThroughModel - fields = ("topping", "order", "move_up_down_links") - readonly_fields = ("order", "move_up_down_links") + fields = ("topping", "move_up_down_links") + readonly_fields = ("move_up_down_links",) ordering = ("order",) extra = 1 diff --git a/tests/autoauth.py b/tests/autoauth.py new file mode 100644 index 00000000..fd6f3ef7 --- /dev/null +++ b/tests/autoauth.py @@ -0,0 +1,10 @@ +from django.contrib.auth.models import User + + +class AutoAuthenticationMiddleware(object): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + request.user = User.objects.filter()[0] + return self.get_response(request) diff --git a/tests/fixtures/screenshot-sample-data.json b/tests/fixtures/screenshot-sample-data.json new file mode 100644 index 00000000..ba2ea468 --- /dev/null +++ b/tests/fixtures/screenshot-sample-data.json @@ -0,0 +1,105 @@ +[ +{ + "model": "tests.item", + "pk": 1, + "fields": { + "order": 0, + "name": "John Lennon" + } +}, +{ + "model": "tests.item", + "pk": 2, + "fields": { + "order": 1, + "name": "Paul McCartney" + } +}, +{ + "model": "tests.item", + "pk": 3, + "fields": { + "order": 2, + "name": "George Harrison" + } +}, +{ + "model": "tests.item", + "pk": 4, + "fields": { + "order": 3, + "name": "Ringo Starr" + } +}, +{ + "model": "tests.topping", + "pk": 1, + "fields": { + "name": "Mozzarella" + } +}, +{ + "model": "tests.topping", + "pk": 2, + "fields": { + "name": "Gorgonzola" + } +}, +{ + "model": "tests.topping", + "pk": 3, + "fields": { + "name": "Fontina" + } +}, +{ + "model": "tests.topping", + "pk": 4, + "fields": { + "name": "Parmigiano" + } +}, +{ + "model": "tests.pizza", + "pk": 1, + "fields": { + "name": "Quattro Formaggi" + } +}, +{ + "model": "tests.pizzatoppingsthroughmodel", + "pk": 1, + "fields": { + "order": 0, + "pizza": 1, + "topping": 1 + } +}, +{ + "model": "tests.pizzatoppingsthroughmodel", + "pk": 2, + "fields": { + "order": 1, + "pizza": 1, + "topping": 2 + } +}, +{ + "model": "tests.pizzatoppingsthroughmodel", + "pk": 3, + "fields": { + "order": 2, + "pizza": 1, + "topping": 3 + } +}, +{ + "model": "tests.pizzatoppingsthroughmodel", + "pk": 4, + "fields": { + "order": 3, + "pizza": 1, + "topping": 4 + } +} +] diff --git a/tests/settings_autoauth.py b/tests/settings_autoauth.py new file mode 100644 index 00000000..952b1d3e --- /dev/null +++ b/tests/settings_autoauth.py @@ -0,0 +1,3 @@ +from tests.settings import * + +MIDDLEWARE.append("tests.autoauth.AutoAuthenticationMiddleware") diff --git a/tests/urls.py b/tests/urls.py index 2706b25e..a878437c 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -2,5 +2,6 @@ from django.contrib import admin admin.autodiscover() +admin.site.enable_nav_sidebar = False urlpatterns = [path("admin/", admin.site.urls)] From 423fbfd01ee66716bbb66043f71ee0fa2f7531cf Mon Sep 17 00:00:00 2001 From: Yuekui Li Date: Tue, 11 Jan 2022 16:16:05 -0800 Subject: [PATCH 05/74] Fix re-order command for custom order field models --- ordered_model/management/commands/reorder_model.py | 2 +- tests/tests.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ordered_model/management/commands/reorder_model.py b/ordered_model/management/commands/reorder_model.py index eb228305..32d510b5 100644 --- a/ordered_model/management/commands/reorder_model.py +++ b/ordered_model/management/commands/reorder_model.py @@ -79,7 +79,7 @@ def reorder_queryset(self, queryset): if self.verbosity: self.stdout.write( "changing order of {} ({}) from {} to {}".format( - model._meta.label, obj.pk, obj.order, order + model._meta.label, obj.pk, getattr(obj, order_field_name), order ) ) setattr(obj, order_field_name, order) diff --git a/tests/tests.py b/tests/tests.py index 9eac0255..7c306d45 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1074,9 +1074,17 @@ def test_reorder_with_custom_order_field(self): when they overlap. """ out = StringIO() + CustomOrderFieldModel.objects.create(name="5", sort_order=0) call_command( "reorder_model", "tests.CustomOrderFieldModel", verbosity=1, stdout=out ) + self.assertSequenceEqual( + CustomOrderFieldModel.objects.values_list("sort_order", flat=True).order_by("sort_order"), + [0, 1, 2, 3, 4], + ) + self.assertIn( + "changing order of tests.CustomOrderFieldModel (5) from 0 to 1", out.getvalue() + ) def test_shows_alternatives(self): out = StringIO() From e47f3020f0e21bcfe439cec9458a312116915332 Mon Sep 17 00:00:00 2001 From: Yuekui Li Date: Tue, 11 Jan 2022 16:29:32 -0800 Subject: [PATCH 06/74] Fix lint error --- ordered_model/management/commands/reorder_model.py | 5 ++++- tests/tests.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ordered_model/management/commands/reorder_model.py b/ordered_model/management/commands/reorder_model.py index 32d510b5..8ca58713 100644 --- a/ordered_model/management/commands/reorder_model.py +++ b/ordered_model/management/commands/reorder_model.py @@ -79,7 +79,10 @@ def reorder_queryset(self, queryset): if self.verbosity: self.stdout.write( "changing order of {} ({}) from {} to {}".format( - model._meta.label, obj.pk, getattr(obj, order_field_name), order + model._meta.label, + obj.pk, + getattr(obj, order_field_name), + order, ) ) setattr(obj, order_field_name, order) diff --git a/tests/tests.py b/tests/tests.py index 7c306d45..fc71c1fd 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1079,11 +1079,14 @@ def test_reorder_with_custom_order_field(self): "reorder_model", "tests.CustomOrderFieldModel", verbosity=1, stdout=out ) self.assertSequenceEqual( - CustomOrderFieldModel.objects.values_list("sort_order", flat=True).order_by("sort_order"), + CustomOrderFieldModel.objects.values_list("sort_order", flat=True).order_by( + "sort_order" + ), [0, 1, 2, 3, 4], ) self.assertIn( - "changing order of tests.CustomOrderFieldModel (5) from 0 to 1", out.getvalue() + "changing order of tests.CustomOrderFieldModel (5) from 0 to 1", + out.getvalue(), ) def test_shows_alternatives(self): From 61e0b0efc75701960b1a77bc5b50acc4d089305a Mon Sep 17 00:00:00 2001 From: Yuekui Date: Wed, 12 Jan 2022 10:57:46 -0800 Subject: [PATCH 07/74] Fix re-order command for custom order field models --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 129b0add..8ac4e800 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Unreleased - Django 4.0 compatibility: Fix ChangeList constructor for Admin (#256) - Remove usage of `assertEquals` in tests (#255) - Add admin screenshots to README +- Fix reorder command for custom order field models 3.4.3 - 2021-04-20 From 5d11d7f7a80891c589574822912429233e4fdd41 Mon Sep 17 00:00:00 2001 From: Yuekui Date: Wed, 12 Jan 2022 11:01:50 -0800 Subject: [PATCH 08/74] Add PR # in changes --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8ac4e800..39c5c2bb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,7 @@ Unreleased - Django 4.0 compatibility: Fix ChangeList constructor for Admin (#256) - Remove usage of `assertEquals` in tests (#255) - Add admin screenshots to README -- Fix reorder command for custom order field models +- Fix reorder command for custom order field models (#257) 3.4.3 - 2021-04-20 From 6e9a73711608e40f0beec7f405681ffb3fb69149 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 12 Jan 2022 19:10:17 +0000 Subject: [PATCH 09/74] release 3.5 --- CHANGES.md | 6 +++--- README.md | 1 + setup.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 39c5c2bb..b88e25e4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,12 @@ Change log ========== -Unreleased ----------- +3.5 - 2022-01-12 +---------------- - Django 4.0 compatibility: Fix ChangeList constructor for Admin (#256) - Remove usage of `assertEquals` in tests (#255) -- Add admin screenshots to README +- Add admin screenshots to README (#245) - Fix reorder command for custom order field models (#257) diff --git a/README.md b/README.md index 0909547c..95dc6439 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,7 @@ Compatibility with Django and Python |django-ordered-model version | Django version | Python version |-----------------------------|---------------------|-------------------- +| **3.5.x** | **3.x**, **4.x** | **3.5** and above | **3.4.x** | **2.x**, **3.x** | **3.5** and above | **3.3.x** | **2.x** | **3.4** and above | **3.2.x** | **2.x** | **3.4** and above diff --git a/setup.py b/setup.py index 8a6901c7..7c6a685b 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", - version="3.4.3", + version="3.5", 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", From 7d8597e11fbc1e53e6188a8ca3925f2ca2f9eeb8 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Sat, 19 Feb 2022 18:19:50 +0000 Subject: [PATCH 10/74] document migrations needed, fixes #265 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 95dc6439..ba862bf5 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ class Item(OrderedModel): ``` +Then run the usual `$ ./manage.py makemigrations` and `$ ./manage.py migrate` to update your database schema. + Model instances now have a set of methods to move them relative to each other. To demonstrate those methods we create two instances of `Item`: From 6d1926a74cb5b02f7c7cdc035b5095a6a58940e5 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 16 Feb 2022 16:58:38 +0000 Subject: [PATCH 11/74] mention subclassing OrderedModelQuerySet in docs --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ba862bf5..0caef6a8 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ class OpenQuestion(BaseQuestion): Custom Manager and QuerySet ----------------- -When your model your extends `OrderedModel`, it inherits a custom `ModelManager` instance, `OrderedModelManager`, which provides additional operations on the resulting `QuerySet`. For example an `OrderedModel` subclass called `Item` that returns a queryset from `Item.objects.all()` supports the following functions: +When your model your extends `OrderedModel`, it inherits a custom `ModelManager` instance which in turn provides additional operations on the resulting `QuerySet`. For example if `Item` is an `OrderedModel` subclass, the queryset `Item.objects.all()` has functions: * `above_instance(object)`, * `below_instance(object)`, @@ -235,18 +235,25 @@ When your model your extends `OrderedModel`, it inherits a custom `ModelManager` * `above(index)`, * `below(index)` -If your model defines a custom `ModelManager` such as `ItemManager` below, you may wish to extend `OrderedModelManager` to retain those functions, as follows: +If your `Model` uses a custom `ModelManager` (such as `ItemManager` below) please have it extend `OrderedModelManager`. + +If your `ModelManager` returns a custom `QuerySet` (such as `ItemQuerySet` below) please have it extend `OrderedModelQuerySet`. ```python -from ordered_model.models import OrderedModelManager, OrderedModel +from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet -class ItemManager(OrderedModelManager): +class ItemQuerySet(OrderedModelQuerySet): pass +class ItemManager(OrderedModelManager): + def get_queryset(self): + return ItemQuerySet(self.model, using=self._db) + class Item(OrderedModel): objects = ItemManager() ``` + Custom ordering field --------------------- Extending `OrderedModel` creates a `models.PositiveIntegerField` field called `order` and the appropriate migrations. It customises the default `class Meta` to then order returned querysets by this field. If you wish to use an existing model field to store the ordering, subclass `OrderedModelBase` instead and set the attribute `order_field_name` to match your field name and the `ordering` attribute on `Meta`: From 33dcca3c1f0d056323010da086220772c0902534 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 16 Feb 2022 17:52:48 +0000 Subject: [PATCH 12/74] add OrderedModelSerializer and tests for Django Rest Framework (DRF) compatibility --- CHANGES.md | 6 ++ README.md | 23 ++++++++ ordered_model/serializers.py | 103 +++++++++++++++++++++++++++++++++++ tests/drf.py | 44 +++++++++++++++ tests/settings.py | 2 + tests/tests.py | 86 +++++++++++++++++++++++++++++ tests/urls.py | 6 +- tox.ini | 1 + 8 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 ordered_model/serializers.py create mode 100644 tests/drf.py diff --git a/CHANGES.md b/CHANGES.md index b88e25e4..4b5156f6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ Change log ========== +Unreleased +---------- + +- Add `serializers.OrderedModelSerializer` to allow Django Rest Framework to re-order models (#251 #264) + + 3.5 - 2022-01-12 ---------------- diff --git a/README.md b/README.md index 0caef6a8..96f1638a 100644 --- a/README.md +++ b/README.md @@ -368,6 +368,29 @@ re-order one or more models. - ``: Name of the model that's an OrderedModel. +Django Rest Framework +--------------------- + +To support updating ordering fields by Django Rest Framework, we include a serializer `OrderedModelSerializer` that intercepts writes to the ordering field, and calls `OrderedModel.to()` method to effect a re-ordering: + + from rest_framework import routers, serializers, viewsets + from ordered_model.serializers import OrderedModelSerializer + from tests.models import CustomItem + + class ItemSerializer(serializers.HyperlinkedModelSerializer, OrderedModelSerializer): + class Meta: + model = CustomItem + fields = ['pkid', 'name', 'modified', 'order'] + + class ItemViewSet(viewsets.ModelViewSet): + queryset = CustomItem.objects.all() + serializer_class = ItemSerializer + + router = routers.DefaultRouter() + router.register(r'items', ItemViewSet) + +Note that you need to include the 'order' field (or your custom field name) in the `Serializer`'s `fields` list, either explicitly or using `__all__`. See [ordered_model/serializers.py](ordered_model/serializers.py) for the implementation. + Test suite ---------- diff --git a/ordered_model/serializers.py b/ordered_model/serializers.py new file mode 100644 index 00000000..0d6cc41d --- /dev/null +++ b/ordered_model/serializers.py @@ -0,0 +1,103 @@ +from rest_framework import serializers, fields + + +class OrderedModelSerializer(serializers.ModelSerializer): + """ + A ModelSerializer to provide a serializer that can be update and create + objects in a specific order. + + Typically a `models.PositiveIntegerField` field called `order` is used to + store the order of the Model objects. This field can be customized by setting + the `order_field_name` attribute on the Model class. + + This serializer will move the object to the correct + order if the ordering field is passed in the validated data. + """ + + def get_order_field(self): + """ + Return the field name for the ordering field. + + If inheriting from `OrderedModelBase`, the `order_field_name` attribute + must be set on the Model class. If inheriting from `OrderedModel`, the + `order_field_name` attribute is not required, as the `OrderedModel` + has the `order_field_name` attribute defaulting to 'order'. + + Returns: + str: The field name for the ordering field. + + Raises: + AttributeError: If the `order_field_name` attribute is not set, + either on the Model class or on the serializer's Meta class. + """ + + ModelClass = self.Meta.model # pylint: disable=no-member,invalid-name + order_field_name = getattr(ModelClass, "order_field_name") + + if not order_field_name: + raise AttributeError( + "The `order_field_name` attribute must be set to use the " + "OrderedModelSerializer. Either inherit from OrderedModel " + "(to use the default `order` field) or inherit from " + "`OrderedModelBase` and set the `order_field_name` attribute " + "on the " + ModelClass.__name__ + " Model class." + ) + + return order_field_name + + def get_fields(self): + # make sure that DRF considers the ordering field writable + order_field = self.get_order_field() + d = super().get_fields() + for name, field in d.items(): + if name == order_field: + if field.read_only: + d[name] = fields.IntegerField() + return d + + def update(self, instance, validated_data): + """ + Update the instance. + + If the `order_field_name` attribute is passed in the validated data, + the instance will be moved to the specified order. + + Returns: + Model: The updated instance. + """ + + order = None + order_field = self.get_order_field() + + if order_field in validated_data: + order = validated_data.pop(order_field) + + instance = super().update(instance, validated_data) + + if order is not None: + instance.to(order) + + return instance + + def create(self, validated_data): + """ + Create a new instance. + + If the `order_field_name` attribute is passed in the validated data, + the instance will be created at the specified order. + + Returns: + Model: The created instance. + """ + order = None + order_field = self.get_order_field() + + if order_field in validated_data: + order = validated_data.pop(order_field) + + instance = super().create(validated_data) + + if order is not None: + instance.to(order) + + return instance diff --git a/tests/drf.py b/tests/drf.py new file mode 100644 index 00000000..8eb9eb17 --- /dev/null +++ b/tests/drf.py @@ -0,0 +1,44 @@ +from rest_framework import routers, serializers, viewsets +from ordered_model.serializers import OrderedModelSerializer +from tests.models import CustomItem, CustomOrderFieldModel + + +class ItemSerializer(OrderedModelSerializer): + class Meta: + model = CustomItem + fields = "__all__" + + +class ItemViewSet(viewsets.ModelViewSet): + queryset = CustomItem.objects.all() + serializer_class = ItemSerializer + + +class CustomOrderFieldModelSerializer(OrderedModelSerializer): + class Meta: + model = CustomOrderFieldModel + fields = "__all__" + + +class CustomOrderFieldModelViewSet(viewsets.ModelViewSet): + queryset = CustomOrderFieldModel.objects.all() + serializer_class = CustomOrderFieldModelSerializer + + +class RenamedItemSerializer(OrderedModelSerializer): + renamedOrder = serializers.IntegerField(source="order") + + class Meta: + model = CustomItem + fields = ("pkid", "name", "renamedOrder") + + +class RenamedItemViewSet(viewsets.ModelViewSet): + queryset = CustomItem.objects.all() + serializer_class = RenamedItemSerializer + + +router = routers.DefaultRouter() +router.register(r"items", ItemViewSet) +router.register(r"customorderfieldmodels", CustomOrderFieldModelViewSet) +router.register(r"renameditems", RenamedItemViewSet, basename="renameditem") diff --git a/tests/settings.py b/tests/settings.py index 7207b5fc..940c6092 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -11,6 +11,7 @@ "django.contrib.staticfiles", "django.contrib.sessions", "ordered_model", + "rest_framework", "tests", ] SECRET_KEY = "topsecret" @@ -36,5 +37,6 @@ }, } ] +REST_FRAMEWORK = {"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"]} STATIC_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "staticfiles") STATIC_URL = "/static/" diff --git a/tests/tests.py b/tests/tests.py index fc71c1fd..a1847340 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -4,9 +4,14 @@ from django.contrib.auth.models import User from django.core.management import call_command from django.utils.timezone import now +from django.urls import reverse from django.test import TestCase from django import VERSION +from rest_framework.test import APIRequestFactory, APITestCase +from rest_framework import status +from tests.drf import ItemViewSet, router + from tests.models import ( Answer, Item, @@ -1125,3 +1130,84 @@ def test_delete_bypass(self): self.assertEqual( "changing order of tests.OpenQuestion (4) from 3 to 2\n", out.getvalue() ) + + +class DRFTestCase(APITestCase): + fixtures = ["test_items.json"] + + def setUp(self): + self.item1 = CustomItem.objects.create(pkid="a", name="1") + self.item2 = CustomItem.objects.create(pkid="b", name="2") + + def test_create_shuffles_down(self): + data = {"name": "3", "pkid": "c", "order": "0"} + response = self.client.post(reverse("customitem-list"), data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(CustomItem.objects.count(), 3) + self.assertEqual( + response.data, {"pkid": "c", "name": "3", "modified": None, "order": 0} + ) + self.assertEqual(CustomItem.objects.get(pkid="a").order, 1) + self.assertEqual(CustomItem.objects.get(pkid="b").order, 2) + + # check DRF exposes the modified value + response = self.client.get( + reverse("customitem-detail", kwargs={"pk": "b"}), {}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data, {"pkid": "b", "name": "2", "modified": None, "order": 2} + ) + + def test_patch_shuffles_down(self): + self.item3 = CustomItem.objects.create(pkid="c", name="3") + + # re-order an item + response = self.client.patch( + reverse("customitem-detail", kwargs={"pk": "b"}), + {"order": 2, "name": "x"}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data, {"pkid": "b", "name": "x", "modified": None, "order": 2} + ) + self.assertEqual(CustomItem.objects.count(), 3) + self.assertEqual(CustomItem.objects.get(pkid="a").order, 0) + self.assertEqual(CustomItem.objects.get(pkid="c").order, 1) + self.assertEqual(CustomItem.objects.get(pkid="b").order, 2) + + def test_custom_order_field_model(self): + response = self.client.get( + reverse("customorderfieldmodel-detail", kwargs={"pk": 1}), {}, format="json" + ) + self.assertEqual(response.data, {"id": 1, "name": "1", "sort_order": 0}) + # re-order a lower item to top + response = self.client.patch( + reverse("customorderfieldmodel-detail", kwargs={"pk": 2}), + {"sort_order": 0}, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {"id": 2, "name": "2", "sort_order": 0}) + # check old first item is pushed down + response = self.client.get( + reverse("customorderfieldmodel-detail", kwargs={"pk": 1}), {}, format="json" + ) + self.assertEqual(response.data, {"id": 1, "name": "1", "sort_order": 1}) + + def test_serializer_renames_order_field(self): + response = self.client.get( + reverse("renameditem-detail", kwargs={"pk": "b"}), {}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {"pkid": "b", "name": "2", "renamedOrder": 1}) + # move b to top + response = self.client.patch( + reverse("renameditem-detail", kwargs={"pk": "b"}), + {"renamedOrder": 0}, + format="json", + ) + self.assertEqual(response.data, {"pkid": "b", "name": "2", "renamedOrder": 0}) + self.assertEqual(CustomItem.objects.get(pkid="b").order, 0) + self.assertEqual(CustomItem.objects.get(pkid="a").order, 1) diff --git a/tests/urls.py b/tests/urls.py index a878437c..b938bebb 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,7 +1,9 @@ -from django.urls import path +from django.urls import path, include from django.contrib import admin +from tests.drf import router + admin.autodiscover() admin.site.enable_nav_sidebar = False -urlpatterns = [path("admin/", admin.site.urls)] +urlpatterns = [path("admin/", admin.site.urls), path("api/", include(router.urls))] diff --git a/tox.ini b/tox.ini index d45a3c6d..7910747f 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,7 @@ deps = django32: Django~=3.2.0 djangoupstream: https://github.com/django/django/archive/main.tar.gz coverage + djangorestframework commands = coverage run {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs} From b0b7a1eb176de510d6e0bd45b3db8b2c1001ee14 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 21 Feb 2022 18:30:47 +0000 Subject: [PATCH 13/74] add tox builder for drfupstream, drop django20, django21, add django40 builders --- CHANGES.md | 2 +- tox.ini | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4b5156f6..200c93cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ Unreleased ---------- - Add `serializers.OrderedModelSerializer` to allow Django Rest Framework to re-order models (#251 #264) - +- Add tox builder for Django 4.0, drop building against 2.0 and 2.1 due to DRF compatibility. 3.5 - 2022-01-12 ---------------- diff --git a/tox.ini b/tox.ini index 7910747f..91ebab32 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] envlist = - py{34,35,36,37}-django20 - py{35,36,37}-django21 py{35,36,37,38,39}-django22 py{36,37,38,39}-django30 py{36,37,38,39}-django31 py{36,37,38,39}-django32 + py{38,39}-django40 py{38,39}-djangoupstream + py{38,39}-drfupstream black [gh-actions] @@ -20,15 +20,19 @@ python = [testenv] deps = - django20: Django~=2.0.0 - django21: Django~=2.1.0 django22: Django~=2.2.17 django30: Django~=3.0.11 django31: Django~=3.1.3 django32: Django~=3.2.0 + django40: Django~=4.0.0 djangoupstream: https://github.com/django/django/archive/main.tar.gz + + drfupstream: Django~=3.2.0 + drfupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz + django22: djangorestframework~=3.12.0 + django30,django31,django32: djangorestframework~=3.12.0 + django40,djangoupstream: djangorestframework~=3.13.0 coverage - djangorestframework commands = coverage run {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs} From 03925229f012d3f288482f8e020f8c153db79634 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 30 May 2022 14:43:37 +0100 Subject: [PATCH 14/74] release 3.6 bump as minor release since users might have compat issues if have defined own serializer classes with same name. --- CHANGES.md | 2 +- README.md | 7 +++++++ setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 200c93cf..d172eb03 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ Change log ========== -Unreleased +3.6 - 2022-05-30 ---------- - Add `serializers.OrderedModelSerializer` to allow Django Rest Framework to re-order models (#251 #264) diff --git a/README.md b/README.md index 96f1638a..b0434a9b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,13 @@ Or if you have checked out the repository: $ python setup.py install ``` +Or to use the latest development code from our master branch: + +```bash +$ pip uninstall django-ordered-model +$ pip install git+git://github.com/django-ordered-model/django-ordered-model.git +``` + Usage ----- diff --git a/setup.py b/setup.py index 7c6a685b..b2fcb9a5 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", - version="3.5", + version="3.6", 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", From c673db651b815b0b4654af20c0818d2255b8bcdd Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 30 May 2022 14:57:25 +0100 Subject: [PATCH 15/74] add supported DRF versions to README --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b0434a9b..b9656505 100644 --- a/README.md +++ b/README.md @@ -417,16 +417,17 @@ $ tox Compatibility with Django and Python ----------------------------------------- -|django-ordered-model version | Django version | Python version -|-----------------------------|---------------------|-------------------- -| **3.5.x** | **3.x**, **4.x** | **3.5** and above -| **3.4.x** | **2.x**, **3.x** | **3.5** and above -| **3.3.x** | **2.x** | **3.4** and above -| **3.2.x** | **2.x** | **3.4** and above -| **3.1.x** | **2.x** | **3.4** and above -| **3.0.x** | **2.x** | **3.4** and above -| **2.1.x** | **1.x** | **2.7** to **3.6** -| **2.0.x** | **1.x** | **2.7** to **3.6** +|django-ordered-model version | Django version | Python version | DRF (optional) +|-----------------------------|---------------------|-------------------|---------------- +| **3.6.x** | **3.x**, **4.x** | **3.5** and above | 3.12 and above +| **3.5.x** | **3.x**, **4.x** | **3.5** and above | - +| **3.4.x** | **2.x**, **3.x** | **3.5** and above | - +| **3.3.x** | **2.x** | **3.4** and above | - +| **3.2.x** | **2.x** | **3.4** and above | - +| **3.1.x** | **2.x** | **3.4** and above | - +| **3.0.x** | **2.x** | **3.4** and above | - +| **2.1.x** | **1.x** | **2.7** to 3.6 | - +| **2.0.x** | **1.x** | **2.7** to 3.6 | - Maintainers From a1c35d3af4ad58001c26119bdcf1c543be4daf25 Mon Sep 17 00:00:00 2001 From: Yuekui Li Date: Wed, 28 Sep 2022 16:56:45 -0700 Subject: [PATCH 16/74] Use bulk update for performance --- ordered_model/management/commands/reorder_model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ordered_model/management/commands/reorder_model.py b/ordered_model/management/commands/reorder_model.py index 8ca58713..8507dd95 100644 --- a/ordered_model/management/commands/reorder_model.py +++ b/ordered_model/management/commands/reorder_model.py @@ -1,6 +1,6 @@ from django.apps import apps from django.core.management import BaseCommand, CommandError -from django.db import DatabaseError, transaction +from django.db import transaction from ordered_model.models import OrderedModelBase @@ -73,6 +73,7 @@ def reorder(self, model): def reorder_queryset(self, queryset): model = queryset.model order_field_name = model.order_field_name + bulk_update_list = [] for order, obj in enumerate(queryset): if getattr(obj, order_field_name) != order: @@ -86,4 +87,5 @@ def reorder_queryset(self, queryset): ) ) setattr(obj, order_field_name, order) - obj.save() + bulk_update_list.append(obj) + model.objects.bulk_update(bulk_update_list, [order_field_name]) From c0fc91c5acdc11b56bfb04c2162e4060d79993ba Mon Sep 17 00:00:00 2001 From: Yuekui Li Date: Wed, 28 Sep 2022 16:57:41 -0700 Subject: [PATCH 17/74] Add Python 3.10, fix broken tests due to DRF upstream --- tox.ini | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tox.ini b/tox.ini index 91ebab32..d7591be8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] envlist = - py{35,36,37,38,39}-django22 - py{36,37,38,39}-django30 - py{36,37,38,39}-django31 - py{36,37,38,39}-django32 - py{38,39}-django40 - py{38,39}-djangoupstream - py{38,39}-drfupstream + py{35,36,37,38,39,310}-django22 + py{36,37,38,39,310}-django30 + py{36,37,38,39,310}-django31 + py{36,37,38,39,310}-django32 + py{38,39,310}-django40 + py{38,39,310}-djangoupstream + py{38,39,310}-drfupstream black [gh-actions] @@ -17,6 +17,7 @@ python = 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 [testenv] deps = @@ -31,7 +32,7 @@ deps = drfupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz django22: djangorestframework~=3.12.0 django30,django31,django32: djangorestframework~=3.12.0 - django40,djangoupstream: djangorestframework~=3.13.0 + django40,djangoupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz coverage commands = coverage run {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs} From a9a9785f3b9dfb4cacdd196b06c7825888a6da18 Mon Sep 17 00:00:00 2001 From: Yuekui Li Date: Wed, 28 Sep 2022 17:07:51 -0700 Subject: [PATCH 18/74] Add Python 3.10 in Github actions --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55239ad3..75ad914b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10'] steps: - uses: actions/checkout@v1 From e45a1ad018e99d04739943b3c1448f617b751c56 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Tue, 11 Oct 2022 12:07:49 +0100 Subject: [PATCH 19/74] keep pinned drf version for django40 --- CHANGES.md | 6 ++++++ tox.ini | 11 ++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d172eb03..ec9b7c17 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ Change log ========== +Unreleased +---------- + +- Use bulk update method in `reorder_model` management command for performance (#273) +- Add tox builder for python 3.10, use upstream DRF with upstream django + 3.6 - 2022-05-30 ---------- diff --git a/tox.ini b/tox.ini index d7591be8..c00da2f2 100644 --- a/tox.ini +++ b/tox.ini @@ -22,17 +22,18 @@ python = [testenv] deps = django22: Django~=2.2.17 - django30: Django~=3.0.11 - django31: Django~=3.1.3 - django32: Django~=3.2.0 - django40: Django~=4.0.0 + django30: Django>=3.0,<3.1 + django31: Django>=3.1,<3.2 + django32: Django>=3.2,<4.0 + django40: Django>=4.0,<4.1 djangoupstream: https://github.com/django/django/archive/main.tar.gz drfupstream: Django~=3.2.0 drfupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz django22: djangorestframework~=3.12.0 django30,django31,django32: djangorestframework~=3.12.0 - django40,djangoupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz + django40: djangorestframework~=3.13.0 + djangoupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz coverage commands = coverage run {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs} From 8df95eb9b4854cf96773151f469bdecb1a225a45 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 16 Jan 2023 11:09:04 +0000 Subject: [PATCH 20/74] add a note about mulitple inheritance linking to #270 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b9656505..7ae42e07 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ class Item(OrderedModel): objects = ItemManager() ``` +If another Django plugin requires you to use specific `Model`, `QuerySet` or `ModelManager` classes, you might need to construct intermediate classes using multiple inheritance, [see an example in issue 270](https://github.com/django-ordered-model/django-ordered-model/issues/270). Custom ordering field --------------------- From a651dcd3b3c497d1bd58386c922f21ca007f0a6a Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 16 Jan 2023 11:16:47 +0000 Subject: [PATCH 21/74] pin ubuntu on LTS --- .github/workflows/distribute.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/distribute.yml b/.github/workflows/distribute.yml index 96f2eb89..03fb4357 100644 --- a/.github/workflows/distribute.yml +++ b/.github/workflows/distribute.yml @@ -6,7 +6,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v1 - name: Set up Python From 894fcc9fcc4cf240a5bb5399919e69a51282c1d0 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 16 Jan 2023 11:22:37 +0000 Subject: [PATCH 22/74] add os selection to python-version matrix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75ad914b..127401ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10'] From 0f9fafb8df750076e3eba0fc8082815d9f100bb3 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 27 Feb 2023 08:55:21 +0000 Subject: [PATCH 23/74] djangoupstream requires py310 or above --- tests/admin.py | 1 + tox.ini | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/admin.py b/tests/admin.py index 77e92de5..2920f68b 100644 --- a/tests/admin.py +++ b/tests/admin.py @@ -17,6 +17,7 @@ CustomPKGroup, ) + # README example for OrderedModelAdmin class ItemAdmin(OrderedModelAdmin): list_display = ("name", "move_up_down_links") diff --git a/tox.ini b/tox.ini index c00da2f2..16bde43b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,9 @@ envlist = py{36,37,38,39,310}-django31 py{36,37,38,39,310}-django32 py{38,39,310}-django40 - py{38,39,310}-djangoupstream - py{38,39,310}-drfupstream + py{38,39,310}-django41 + py{310}-djangoupstream + py{310}-drfupstream black [gh-actions] @@ -26,6 +27,7 @@ deps = django31: Django>=3.1,<3.2 django32: Django>=3.2,<4.0 django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 djangoupstream: https://github.com/django/django/archive/main.tar.gz drfupstream: Django~=3.2.0 @@ -33,6 +35,7 @@ deps = django22: djangorestframework~=3.12.0 django30,django31,django32: djangorestframework~=3.12.0 django40: djangorestframework~=3.13.0 + django41: djangorestframework~=3.13.0 djangoupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz coverage commands = From 2c679dae5a0d62c5cc32418034d4ee3f65f1b547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Paduszy=C5=84ski?= <92403542+paduszyk@users.noreply.github.com> Date: Sun, 26 Feb 2023 21:33:49 +0100 Subject: [PATCH 24/74] Update AssertionError message in `OrderedModelQuerySet` class Manager regards models, not model admin as suggested by previous message. --- ordered_model/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ordered_model/models.py b/ordered_model/models.py index bf2e99bf..09d486f5 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -27,7 +27,7 @@ def _get_order_with_respect_to(self): if order_with_respect_to is None: raise AssertionError( ( - 'ordered model admin "{0}" has not specified "order_with_respect_to"; note that this ' + 'ordered model "{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) From 5679991a80efa9ae09ff916e5fafe8da324a30b1 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 1 Mar 2023 09:45:52 +0000 Subject: [PATCH 25/74] cleanup AutoField warnings in test suite --- tests/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/settings.py b/tests/settings.py index 940c6092..0bb27997 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -40,3 +40,4 @@ REST_FRAMEWORK = {"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"]} STATIC_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "staticfiles") STATIC_URL = "/static/" +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" From b9ba8eb342e58fb5f103755ecf475492116a758e Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 1 Mar 2023 09:00:46 +0000 Subject: [PATCH 26/74] Add model warning if OrderedModelBase subclass Meta fails to declare ordering --- CHANGES.md | 1 + ordered_model/models.py | 17 ++++++++++++++++ tests/tests.py | 43 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ec9b7c17..128fc7f9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Unreleased - Use bulk update method in `reorder_model` management command for performance (#273) - Add tox builder for python 3.10, use upstream DRF with upstream django +- Emit a system Check failure if a subclass of `OrderedModelBase` fails to specify `Meta.ordering` 3.6 - 2022-05-30 ---------- diff --git a/ordered_model/models.py b/ordered_model/models.py index 09d486f5..069e9e05 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -1,5 +1,6 @@ from functools import partial, reduce +from django.core import checks from django.db import models from django.db.models import Max, Min, F from django.db.models.constants import LOOKUP_SEP @@ -307,6 +308,22 @@ def bottom(self, extra_update=None): o = self.get_ordering_queryset().get_max_order() self.to(o, extra_update=extra_update) + @classmethod + def check(cls, **kwargs): + errors = super().check(**kwargs) + + ordering = getattr(cls._meta, "ordering", None) + if ordering is None or len(ordering) < 1: + errors.append( + checks.Warning( + "OrderedModelBase subclass needs Meta.ordering specified.", + hint="If you have overwritten Meta, try inheriting with Meta(OrderedModel.Meta).", + obj=str(cls.__qualname__), + id="ordered_model.W001", + ) + ) + return errors + class OrderedModel(OrderedModelBase): """ diff --git a/tests/tests.py b/tests/tests.py index a1847340..1bba20cf 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -3,15 +3,19 @@ from django.contrib.auth.models import User from django.core.management import call_command +from django.core import checks from django.utils.timezone import now from django.urls import reverse -from django.test import TestCase +from django.test import TestCase, SimpleTestCase +from django.test.utils import isolate_apps, override_system_checks from django import VERSION from rest_framework.test import APIRequestFactory, APITestCase from rest_framework import status from tests.drf import ItemViewSet, router +from ordered_model.models import OrderedModel + from tests.models import ( Answer, Item, @@ -1211,3 +1215,40 @@ def test_serializer_renames_order_field(self): self.assertEqual(response.data, {"pkid": "b", "name": "2", "renamedOrder": 0}) self.assertEqual(CustomItem.objects.get(pkid="b").order, 0) self.assertEqual(CustomItem.objects.get(pkid="a").order, 1) + + +@isolate_apps("tests", attr_name="apps") +@override_system_checks([checks.model_checks.check_all_models]) +class ChecksTest(SimpleTestCase): + def test_no_inherited_ordering(self): + class TestModel(OrderedModel): + class Meta: + verbose_name = "unordered" + + self.maxDiff = None + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [ + checks.Warning( + msg="OrderedModelBase subclass needs Meta.ordering specified.", + hint="If you have overwritten Meta, try inheriting with Meta(OrderedModel.Meta).", + obj="ChecksTest.test_no_inherited_ordering..TestModel", + id="ordered_model.W001", + ) + ], + ) + + def test_explicit_ordering(self): + class TestModel2(OrderedModel): + class Meta: + verbose_name = "unordered" + ordering = ["order"] + + self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) + + def test_inherited_ordering(self): + class TestModel(OrderedModel): + class Meta(OrderedModel.Meta): + verbose_name = "unordered" + + self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) From 75601f50544a729eaaa181091ae6ce772e62bf81 Mon Sep 17 00:00:00 2001 From: Andrew Curcie Date: Wed, 5 May 2021 14:59:44 -0400 Subject: [PATCH 27/74] cache order_with_respect_to field values for comparison - if order_with_respect_to fields have changed on save then reorder model - handle ObjectDoesNotExist in get_lookup_value --- ordered_model/models.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/ordered_model/models.py b/ordered_model/models.py index 069e9e05..84464ef1 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -1,6 +1,7 @@ from functools import partial, reduce from django.core import checks +from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models import Max, Min, F from django.db.models.constants import LOOKUP_SEP @@ -9,7 +10,10 @@ def get_lookup_value(obj, field): - return reduce(lambda i, f: getattr(i, f), field.split(LOOKUP_SEP), obj) + try: + return reduce(lambda i, f: getattr(i, f), field.split(LOOKUP_SEP), obj) + except ObjectDoesNotExist: + return None class OrderedModelQuerySet(models.QuerySet): @@ -149,10 +153,28 @@ class OrderedModelBase(models.Model): order_field_name = None order_with_respect_to = None order_class_path = None + original_order_with_respect_to_fks = None class Meta: abstract = True + def __init__(self, *args, **kwargs): + super(OrderedModelBase, self).__init__(*args, **kwargs) + attrs = self._attrs() + self._original_order_with_respect_to_fks = ( + {get_lookup_value(self, name) for name in self._attrs()} if attrs else set() + ) + + def _attrs(self): + if not self.order_with_respect_to: + return None + t = ( + self.order_with_respect_to + if type(self.order_with_respect_to) is tuple + else (self.order_with_respect_to,) + ) + return t + def _validate_ordering_reference(self, ref): if self.order_with_respect_to is not None: self_kwargs = ( @@ -195,10 +217,18 @@ def next(self): def save(self, *args, **kwargs): order_field_name = self.order_field_name - if getattr(self, order_field_name) is None: + if getattr(self, order_field_name) is None or ( + self._attrs() is not None + and {get_lookup_value(self, name) for name in self._attrs()} + != self._original_order_with_respect_to_fks + ): order = self.get_ordering_queryset().get_next_order() setattr(self, order_field_name, order) super().save(*args, **kwargs) + attrs = self._attrs() + self._original_order_with_respect_to_fks = ( + {get_lookup_value(self, name) for name in attrs} if attrs else set() + ) def delete(self, *args, extra_update=None, **kwargs): qs = self.get_ordering_queryset() From 80520010e1ef2a50174c98079d9abde6790e1831 Mon Sep 17 00:00:00 2001 From: Andrew Curcie Date: Wed, 5 May 2021 16:11:02 -0400 Subject: [PATCH 28/74] add test to ensure reordering occurs appropriately - when the value of a order_with_respect_to field changes the model should be added to the bottom of the new list --- tests/tests.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index 1bba20cf..524a2c55 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -285,6 +285,34 @@ def test_bottom(self): ) +class OrderWithRespectToReorderTests(TestCase): + def setUp(self): + q1 = Question.objects.create() + self.u0 = TestUser.objects.create() + self.u1 = TestUser.objects.create() + self.u0_a1 = q1.answers.create(user=self.u0) + self.u0_a2 = q1.answers.create(user=self.u0) + self.u0_a3 = q1.answers.create(user=self.u0) + self.u1_a1 = q1.answers.create(user=self.u1) + self.u1_a2 = q1.answers.create(user=self.u1) + self.u1_a3 = q1.answers.create(user=self.u1) + + def test_reorder_when_field_value_changed(self): + self.u0_a2.user = self.u1 + self.u0_a2.save() + self.assertSequenceEqual( + Answer.objects.values_list("pk", "order"), + [ + (self.u0_a1.pk, 0), + (self.u0_a3.pk, 2), + (self.u1_a1.pk, 0), + (self.u1_a2.pk, 1), + (self.u1_a3.pk, 2), + (self.u0_a2.pk, 3), + ], + ) + + class CustomPKTest(TestCase): def setUp(self): self.item1 = CustomItem.objects.create(pkid=str(uuid.uuid4()), name="1") From ddcf96885c19a2fa4de3d97481c9e5eaaed39b71 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Thu, 2 Mar 2023 20:14:40 +0000 Subject: [PATCH 29/74] reimplement order_with_respect_to handling, fix re-ordering --- CHANGES.md | 1 + ordered_model/models.py | 144 ++++++++++++++++------------------------ tests/tests.py | 2 +- 3 files changed, 59 insertions(+), 88 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 128fc7f9..f6b93cb1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Unreleased - Use bulk update method in `reorder_model` management command for performance (#273) - Add tox builder for python 3.10, use upstream DRF with upstream django - Emit a system Check failure if a subclass of `OrderedModelBase` fails to specify `Meta.ordering` +- Updating fields within `order_with_respect_to` now adjusts ordering accordingly (#198) 3.6 - 2022-05-30 ---------- diff --git a/ordered_model/models.py b/ordered_model/models.py index 84464ef1..579c913e 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -24,21 +24,6 @@ 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 "{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( @@ -95,41 +80,18 @@ def increase_order(self, **extra_kwargs): def bulk_create(self, objs, *args, **kwargs): order_field_name = self._get_order_field_name() - order_with_respect_to = self.model.order_with_respect_to + order_with_respect_to = self.model.get_order_with_respect_to() objs = list(objs) - 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) + order_with_respect_to_mapping = {} + for obj in objs: + key = obj._wrt_map() + if key in order_with_respect_to_mapping: + order_with_respect_to_mapping[key] += 1 + else: + order_with_respect_to_mapping[key] = self.filter(**key).get_next_order() + setattr(obj, order_field_name, order_with_respect_to_mapping[key]) return super().bulk_create(objs, *args, **kwargs) - 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)): pass @@ -153,55 +115,53 @@ class OrderedModelBase(models.Model): order_field_name = None order_with_respect_to = None order_class_path = None - original_order_with_respect_to_fks = None class Meta: abstract = True def __init__(self, *args, **kwargs): super(OrderedModelBase, self).__init__(*args, **kwargs) - attrs = self._attrs() - self._original_order_with_respect_to_fks = ( - {get_lookup_value(self, name) for name in self._attrs()} if attrs else set() - ) + self._original_wrt_map = self._wrt_map() - def _attrs(self): - if not self.order_with_respect_to: - return None - t = ( - self.order_with_respect_to - if type(self.order_with_respect_to) is tuple - else (self.order_with_respect_to,) - ) - return t + def _wrt_map(self): + d = {} + for a in self.get_order_with_respect_to(): + d[a] = get_lookup_value(self, a) + return d + + @classmethod + def get_order_with_respect_to(cls): + if type(cls.order_with_respect_to) is tuple: + return cls.order_with_respect_to + elif type(cls.order_with_respect_to) is str: + return (cls.order_with_respect_to,) + elif cls.order_with_respect_to is None: + return tuple() + else: + raise AssertionError("Invalid value for model.order_with_respect_to") def _validate_ordering_reference(self, ref): - if self.order_with_respect_to is not None: - self_kwargs = ( - self._meta.default_manager._get_order_with_respect_to_filter_kwargs( - self + if self._wrt_map() != ref._wrt_map(): + raise ValueError( + "{0!r} can only be swapped with instances of {1!r} with equal {2!s} fields.".format( + self, + self._meta.default_manager.model, + " and ".join( + ["'{}'".format(o) for o in self.get_order_with_respect_to()] + ), ) ) - ref_kwargs = ( - ref._meta.default_manager._get_order_with_respect_to_filter_kwargs(ref) - ) - if self_kwargs != ref_kwargs: - raise ValueError( - "{0!r} can only be swapped with instances of {1!r} with equal {2!s} fields.".format( - self, - self._meta.default_manager.model, - " and ".join(["'{}'".format(o) for o in self_kwargs]), - ) - ) - def get_ordering_queryset(self, qs=None): + def get_ordering_queryset(self, qs=None, wrt=None): if qs is None: if self.order_class_path: model = import_string(self.order_class_path) qs = model._meta.default_manager.all() else: qs = self._meta.default_manager.all() - return qs.filter_by_order_with_respect_to(self) + if wrt: + return qs.filter(**wrt) + return qs.filter(**self._wrt_map()) def previous(self): """ @@ -217,18 +177,19 @@ def next(self): def save(self, *args, **kwargs): order_field_name = self.order_field_name - if getattr(self, order_field_name) is None or ( - self._attrs() is not None - and {get_lookup_value(self, name) for name in self._attrs()} - != self._original_order_with_respect_to_fks - ): + wrt_changed = self._wrt_map() != self._original_wrt_map + + if wrt_changed: + # do 'delete' using old wrt values! + qs = self.get_ordering_queryset(wrt=self._original_wrt_map) + qs.above_instance(self).decrease_order() + + if getattr(self, order_field_name) is None or wrt_changed: order = self.get_ordering_queryset().get_next_order() setattr(self, order_field_name, order) super().save(*args, **kwargs) - attrs = self._attrs() - self._original_order_with_respect_to_fks = ( - {get_lookup_value(self, name) for name in attrs} if attrs else set() - ) + + self._original_wrt_map = self._wrt_map() def delete(self, *args, extra_update=None, **kwargs): qs = self.get_ordering_queryset() @@ -352,6 +313,15 @@ def check(cls, **kwargs): id="ordered_model.W001", ) ) + owrt = getattr(cls, "order_with_respect_to") + if not (type(owrt) is tuple or type(owrt) is str or owrt is None): + errors.append( + checks.Error( + "OrderedModelBase subclass order_with_respect_to value invalid. Expected tuple, str or None", + obj=str(cls.__qualname__), + id="ordered_model.E001", + ) + ) return errors diff --git a/tests/tests.py b/tests/tests.py index 524a2c55..3a7e28ac 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -304,7 +304,7 @@ def test_reorder_when_field_value_changed(self): Answer.objects.values_list("pk", "order"), [ (self.u0_a1.pk, 0), - (self.u0_a3.pk, 2), + (self.u0_a3.pk, 1), (self.u1_a1.pk, 0), (self.u1_a2.pk, 1), (self.u1_a3.pk, 2), From af71ab6b9161941c36838701adc1203109a69de3 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Thu, 2 Mar 2023 20:28:56 +0000 Subject: [PATCH 30/74] dict keys need to be immutable --- CHANGES.md | 2 +- ordered_model/admin.py | 5 +---- ordered_model/models.py | 6 ++++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f6b93cb1..0cba834e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,7 @@ Unreleased - Use bulk update method in `reorder_model` management command for performance (#273) - Add tox builder for python 3.10, use upstream DRF with upstream django - Emit a system Check failure if a subclass of `OrderedModelBase` fails to specify `Meta.ordering` -- Updating fields within `order_with_respect_to` now adjusts ordering accordingly (#198) +- Updating the value of fields within `order_with_respect_to` now adjusts ordering accordingly (#198) 3.6 - 2022-05-30 ---------- diff --git a/ordered_model/admin.py b/ordered_model/admin.py index 74d66a8c..0cc360c0 100644 --- a/ordered_model/admin.py +++ b/ordered_model/admin.py @@ -209,10 +209,7 @@ def move_up_down_links(self, obj): # 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._meta.default_manager._get_order_with_respect_to_filter_kwargs(obj) - or [] - ) + order_with_respect_to = obj._wrt_map() or [] fields = [ str(value.pk) for value in order_with_respect_to.values() diff --git a/ordered_model/models.py b/ordered_model/models.py index 579c913e..fc33a93f 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -84,11 +84,13 @@ def bulk_create(self, objs, *args, **kwargs): objs = list(objs) order_with_respect_to_mapping = {} for obj in objs: - key = obj._wrt_map() + key = frozenset(obj._wrt_map().items()) if key in order_with_respect_to_mapping: order_with_respect_to_mapping[key] += 1 else: - order_with_respect_to_mapping[key] = self.filter(**key).get_next_order() + order_with_respect_to_mapping[key] = self.filter( + **obj._wrt_map() + ).get_next_order() setattr(obj, order_field_name, order_with_respect_to_mapping[key]) return super().bulk_create(objs, *args, **kwargs) From f148961cf569432358ecbebe875ce9f9df303832 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 3 Mar 2023 15:05:35 +0000 Subject: [PATCH 31/74] release 3se 3.7 --- CHANGES.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 0cba834e..913b6358 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ Change log Unreleased ---------- +3.7 - 2023-03-03 +---------- + - Use bulk update method in `reorder_model` management command for performance (#273) - Add tox builder for python 3.10, use upstream DRF with upstream django - Emit a system Check failure if a subclass of `OrderedModelBase` fails to specify `Meta.ordering` diff --git a/setup.py b/setup.py index b2fcb9a5..34023834 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", - version="3.6", + version="3.7", 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", From 567b2cbf3ebc7a96607e3f9dbcc1f004fa4bd758 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Sun, 5 Mar 2023 17:25:28 +0000 Subject: [PATCH 32/74] rebase/adapt cascaded deletes by signals --- CHANGES.md | 2 ++ README.md | 1 + ordered_model/__init__.py | 1 + ordered_model/apps.py | 11 +++++++++++ ordered_model/models.py | 4 ++++ ordered_model/signals.py | 29 +++++++++++++++++++++++++++ tests/models.py | 8 ++++++++ tests/tests.py | 41 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 97 insertions(+) create mode 100644 ordered_model/apps.py create mode 100644 ordered_model/signals.py diff --git a/CHANGES.md b/CHANGES.md index 913b6358..fd21e342 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ Change log Unreleased ---------- +- Cascaded deletes of `OrderedModel` instances now handled using signals (#182) + 3.7 - 2023-03-03 ---------- diff --git a/README.md b/README.md index 7ae42e07..f409be21 100644 --- a/README.md +++ b/README.md @@ -405,6 +405,7 @@ Test suite To run the tests against your current environment, use: ```bash +$ pip install djangorestframework $ django-admin test --pythonpath=. --settings=tests.settings ``` diff --git a/ordered_model/__init__.py b/ordered_model/__init__.py index e69de29b..852967ac 100644 --- a/ordered_model/__init__.py +++ b/ordered_model/__init__.py @@ -0,0 +1 @@ +default_app_config = "ordered_model.apps.OrderedModelConfig" diff --git a/ordered_model/apps.py b/ordered_model/apps.py new file mode 100644 index 00000000..812a21e8 --- /dev/null +++ b/ordered_model/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + + +class OrderedModelConfig(AppConfig): + name = "ordered_model" + label = "ordered_model" + + def ready(self): + # This import has side effects + # noinspection PyUnresolvedReferences + from .signals import on_ordered_model_delete diff --git a/ordered_model/models.py b/ordered_model/models.py index fc33a93f..4b11eccf 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -194,6 +194,10 @@ def save(self, *args, **kwargs): self._original_wrt_map = self._wrt_map() def delete(self, *args, extra_update=None, **kwargs): + # Flag re-ordering performed so that post_delete signal + # does not duplicate the re-ordering. See signals.py + self._was_deleted_via_delete_method = True + qs = self.get_ordering_queryset() extra_update = {} if extra_update is None else extra_update qs.above_instance(self).decrease_order(**extra_update) diff --git a/ordered_model/signals.py b/ordered_model/signals.py new file mode 100644 index 00000000..42b0dd7a --- /dev/null +++ b/ordered_model/signals.py @@ -0,0 +1,29 @@ +from django.db.models.signals import post_delete +from django.dispatch import receiver +from ordered_model.models import OrderedModelBase +from django.db.models import F + + +@receiver(post_delete, dispatch_uid="on_ordered_model_delete") +def on_ordered_model_delete(sender, instance, **kwargs): + """ + This signal makes sure that when an OrderedModelBase is deleted via cascade database deletes. + """ + + """ + We're only interested in subclasses of OrderedModelBase. + We want to be able to support 'extra_kwargs' on the delete() + method, which we can't do if we do all our work in the signal. We add a property to signal whether or not + the model's .delete() method was called, because if so - we don't need to do any more work. + """ + if not issubclass(sender, OrderedModelBase): + return + if getattr(instance, "_was_deleted_via_delete_method", False): + return + + extra_update = kwargs.get("extra_update", None) + + # Copy of upshuffle logic from OrderedModelBase.delete + qs = instance.get_ordering_queryset() + extra_update = {} if extra_update is None else extra_update + qs.above_instance(instance).decrease_order(**extra_update) diff --git a/tests/models.py b/tests/models.py index ba6b1e3e..2113835d 100644 --- a/tests/models.py +++ b/tests/models.py @@ -126,3 +126,11 @@ class ItemGroup(models.Model): class GroupedItem(OrderedModel): group = models.ForeignKey(ItemGroup, on_delete=models.CASCADE, related_name="items") order_with_respect_to = "group__user" + + +class CascadedParentModel(models.Model): + pass + + +class CascadedOrderedModel(OrderedModel): + parent = models.ForeignKey(to=CascadedParentModel, on_delete=models.CASCADE) diff --git a/tests/tests.py b/tests/tests.py index 3a7e28ac..9d5a5c50 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -4,17 +4,21 @@ from django.contrib.auth.models import User from django.core.management import call_command from django.core import checks +from django.db.models.signals import post_delete +from django.dispatch import Signal from django.utils.timezone import now from django.urls import reverse from django.test import TestCase, SimpleTestCase from django.test.utils import isolate_apps, override_system_checks from django import VERSION + from rest_framework.test import APIRequestFactory, APITestCase from rest_framework import status from tests.drf import ItemViewSet, router from ordered_model.models import OrderedModel +from ordered_model.signals import on_ordered_model_delete from tests.models import ( Answer, @@ -32,6 +36,8 @@ ItemGroup, GroupedItem, TestUser, + CascadedParentModel, + CascadedOrderedModel, ) @@ -1143,7 +1149,11 @@ def test_delete_bypass(self): OpenQuestion.objects.create(answer="4", order=3) # bypass our OrderedModel delete logic to leave a hole in ordering + self.assertTrue(post_delete.disconnect(dispatch_uid="on_ordered_model_delete")) OpenQuestion.objects.filter(answer="3").delete() + post_delete.connect( + on_ordered_model_delete, dispatch_uid="on_ordered_model_delete" + ) self.assertEqual([0, 1, 3], [i.order for i in OpenQuestion.objects.all()]) self.assertEqual( @@ -1280,3 +1290,34 @@ class Meta(OrderedModel.Meta): verbose_name = "unordered" self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) + + +class TestCascadedDelete(TestCase): + def test_that_model_when_deleted_by_cascade_still_maintains_ordering(self): + parent_for_order_0_child = CascadedParentModel.objects.create() + child_with_order_0 = CascadedOrderedModel.objects.create( + parent=parent_for_order_0_child + ) + + parent__for_order_1_child = CascadedParentModel.objects.create() + child_with_order_1 = CascadedOrderedModel.objects.create( + parent=parent__for_order_1_child + ) + + parent_for_order_2_child = CascadedParentModel.objects.create() + child_with_order_2 = CascadedOrderedModel.objects.create( + parent=parent_for_order_2_child + ) + + # Delete positition 1 parent, now there's a hole, which child_with_order_2 should take + parent__for_order_1_child.delete() + + # Refresh children from db + child_with_order_0.refresh_from_db() + child_with_order_2.refresh_from_db() + + print(repr(CascadedOrderedModel.objects.all())) + + # Assert the hole has been filled + self.assertEqual(child_with_order_0.order, 0) + self.assertEqual(child_with_order_2.order, 1) From c11a17837d8172f3ea8c33282c6469f0b144f8a8 Mon Sep 17 00:00:00 2001 From: Gustav Wengel Date: Wed, 13 Mar 2019 15:07:01 +0100 Subject: [PATCH 33/74] Update signals.py --- ordered_model/signals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ordered_model/signals.py b/ordered_model/signals.py index 42b0dd7a..1b846312 100644 --- a/ordered_model/signals.py +++ b/ordered_model/signals.py @@ -7,7 +7,8 @@ @receiver(post_delete, dispatch_uid="on_ordered_model_delete") def on_ordered_model_delete(sender, instance, **kwargs): """ - This signal makes sure that when an OrderedModelBase is deleted via cascade database deletes. + This signal makes sure that when an OrderedModelBase is deleted via cascade database deletes, the models + keep order. """ """ From b224b917d7c3ecd82370d205e4dfbea84fbb270b Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 6 Mar 2023 16:52:24 +0000 Subject: [PATCH 34/74] fix: model create WRT broken in admin --- CHANGES.md | 1 + ordered_model/models.py | 4 ++-- tests/tests.py | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fd21e342..a37bbf3a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Change log Unreleased ---------- +- Fix for `model.save()` falsely detecting WRT change from admin create since 3.7 - Cascaded deletes of `OrderedModel` instances now handled using signals (#182) 3.7 - 2023-03-03 diff --git a/ordered_model/models.py b/ordered_model/models.py index 4b11eccf..d3314854 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -181,8 +181,8 @@ def save(self, *args, **kwargs): order_field_name = self.order_field_name wrt_changed = self._wrt_map() != self._original_wrt_map - if wrt_changed: - # do 'delete' using old wrt values! + if wrt_changed and getattr(self, order_field_name) is not None: + # do delete-like upshuffle using original_wrt values! qs = self.get_ordering_queryset(wrt=self._original_wrt_map) qs.above_instance(self).decrease_order() diff --git a/tests/tests.py b/tests/tests.py index 9d5a5c50..00bcbf24 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -696,7 +696,9 @@ def setUp(self): 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 = PizzaToppingsThroughModel() + self.p2_t4.pizza = self.p2 + self.p2_t4.topping = self.t5 self.p2_t4.save() def test_saved_order(self): @@ -1316,8 +1318,6 @@ def test_that_model_when_deleted_by_cascade_still_maintains_ordering(self): child_with_order_0.refresh_from_db() child_with_order_2.refresh_from_db() - print(repr(CascadedOrderedModel.objects.all())) - # Assert the hole has been filled self.assertEqual(child_with_order_0.order, 0) self.assertEqual(child_with_order_2.order, 1) From a7ce6e44a75a7fa7fd5914015d88844097237776 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 6 Mar 2023 16:52:51 +0000 Subject: [PATCH 35/74] release 3.7.1 patch release --- CHANGES.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a37bbf3a..6b5b522a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ Change log Unreleased ---------- +3.7.1 - 2023-03-06 +---------- + - Fix for `model.save()` falsely detecting WRT change from admin create since 3.7 - Cascaded deletes of `OrderedModel` instances now handled using signals (#182) diff --git a/setup.py b/setup.py index 34023834..1e9dea50 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", - version="3.7", + version="3.7.1", 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", From c4630dca2bdafd6bac1bd2328081f524953872cf Mon Sep 17 00:00:00 2001 From: ibaguio Date: Sun, 12 Mar 2023 01:39:29 +0800 Subject: [PATCH 36/74] fix: utilize order_wrt _id instead of object the newly introduced OrderedModelBase._wrt_map method gets the value of the fields defined in order_with_respect_to. this forces django to query the object from the database, but only to be used for comparison. It is optimal to just compare the _id instead of the actual model object. --- ordered_model/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ordered_model/models.py b/ordered_model/models.py index d3314854..8bfa7c44 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -127,8 +127,11 @@ def __init__(self, *args, **kwargs): def _wrt_map(self): d = {} - for a in self.get_order_with_respect_to(): - d[a] = get_lookup_value(self, a) + for order_field_name in self.get_order_with_respect_to(): + if not order_field_name.endswith("_id"): + order_field_name = order_field_name + "_id" + d[order_field_name] = get_lookup_value(self, order_field_name) + return d @classmethod From 34ab3716b992b517a37ea756e2fc91e8c0c1380c Mon Sep 17 00:00:00 2001 From: ibaguio Date: Sun, 12 Mar 2023 02:10:33 +0800 Subject: [PATCH 37/74] Add query count tests --- tests/tests.py | 15 +++++++++++++-- tests/utils.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tests/utils.py diff --git a/tests/tests.py b/tests/tests.py index 00bcbf24..9823f000 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -16,6 +16,7 @@ from rest_framework.test import APIRequestFactory, APITestCase from rest_framework import status from tests.drf import ItemViewSet, router +from tests.utils import assertNumQueries from ordered_model.models import OrderedModel from ordered_model.signals import on_ordered_model_delete @@ -164,6 +165,9 @@ def setUp(self): self.q1_a2 = q1.answers.create(user=u0) self.q2_a2 = q2.answers.create(user=u0) + with assertNumQueries(self, 1): + Answer.objects.get(id=self.q1_a1.id) + def test_saved_order(self): self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), @@ -303,9 +307,14 @@ def setUp(self): self.u1_a2 = q1.answers.create(user=self.u1) self.u1_a3 = q1.answers.create(user=self.u1) + with assertNumQueries(self, 1): + Answer.objects.get(id=self.u0_a1.id) + def test_reorder_when_field_value_changed(self): self.u0_a2.user = self.u1 - self.u0_a2.save() + with assertNumQueries(self, 3): + self.u0_a2.save() + self.assertSequenceEqual( Answer.objects.values_list("pk", "order"), [ @@ -685,7 +694,9 @@ def setUp(self): ) # 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() + with assertNumQueries(self, 2): + self.p1_t1.save() + self.p1_t2 = PizzaToppingsThroughModel(pizza=self.p1, topping=self.t2) self.p1_t2.save() self.p1_t3 = PizzaToppingsThroughModel(pizza=self.p1, topping=self.t3) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..a23caebb --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,37 @@ +# Query count helpers, copied from django source +# Added here for compatibility (django<3.1) +from django.db import DEFAULT_DB_ALIAS, connections +from django.test.utils import CaptureQueriesContext + + +class _AssertNumQueriesContext(CaptureQueriesContext): + def __init__(self, test_case, num, connection): + self.test_case = test_case + self.num = num + super().__init__(connection) + + def __exit__(self, exc_type, exc_value, traceback): + super().__exit__(exc_type, exc_value, traceback) + if exc_type is not None: + return + executed = len(self) + self.test_case.assertEqual( + executed, self.num, + "%d queries executed, %d expected\nCaptured queries were:\n%s" % ( + executed, self.num, + '\n'.join( + '%d. %s' % (i, query['sql']) for i, query in enumerate(self.captured_queries, start=1) + ) + ) + ) + + +def assertNumQueries(self, num, func=None, *args, using=DEFAULT_DB_ALIAS, **kwargs): + conn = connections[using] + + context = _AssertNumQueriesContext(self, num, conn) + if func is None: + return context + + with context: + func(*args, **kwargs) \ No newline at end of file From 2878689e1454beca98e64cad0f1942da177bfa1f Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 13 Mar 2023 16:10:22 +0000 Subject: [PATCH 38/74] run black tests/utils.py --- tests/utils.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index a23caebb..67e92b93 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,13 +16,17 @@ def __exit__(self, exc_type, exc_value, traceback): return executed = len(self) self.test_case.assertEqual( - executed, self.num, - "%d queries executed, %d expected\nCaptured queries were:\n%s" % ( - executed, self.num, - '\n'.join( - '%d. %s' % (i, query['sql']) for i, query in enumerate(self.captured_queries, start=1) - ) - ) + executed, + self.num, + "%d queries executed, %d expected\nCaptured queries were:\n%s" + % ( + executed, + self.num, + "\n".join( + "%d. %s" % (i, query["sql"]) + for i, query in enumerate(self.captured_queries, start=1) + ), + ), ) @@ -34,4 +38,4 @@ def assertNumQueries(self, num, func=None, *args, using=DEFAULT_DB_ALIAS, **kwar return context with context: - func(*args, **kwargs) \ No newline at end of file + func(*args, **kwargs) From 275091d5af70ce57069b73ed4ea1c5a914fa5c23 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Tue, 14 Mar 2023 11:43:40 +0000 Subject: [PATCH 39/74] add system Check that OrderedQuerySet and OrderedModelManager subclasses are returned --- ordered_model/models.py | 24 ++++++++++++++--- tests/tests.py | 57 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/ordered_model/models.py b/ordered_model/models.py index 8bfa7c44..c0cb3b32 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -315,20 +315,36 @@ def check(cls, **kwargs): ordering = getattr(cls._meta, "ordering", None) if ordering is None or len(ordering) < 1: errors.append( - checks.Warning( + checks.Error( "OrderedModelBase subclass needs Meta.ordering specified.", hint="If you have overwritten Meta, try inheriting with Meta(OrderedModel.Meta).", obj=str(cls.__qualname__), - id="ordered_model.W001", + id="ordered_model.E001", ) ) owrt = getattr(cls, "order_with_respect_to") if not (type(owrt) is tuple or type(owrt) is str or owrt is None): errors.append( checks.Error( - "OrderedModelBase subclass order_with_respect_to value invalid. Expected tuple, str or None", + "OrderedModelBase subclass order_with_respect_to value invalid. Expected tuple, str or None.", obj=str(cls.__qualname__), - id="ordered_model.E001", + id="ordered_model.E002", + ) + ) + if not issubclass(cls.objects.__class__, OrderedModelManager): + errors.append( + checks.Error( + "OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager.", + obj=str(cls.__qualname__), + id="ordered_model.E003", + ) + ) + if not issubclass(cls.objects.none().__class__, OrderedModelQuerySet): + errors.append( + checks.Error( + "OrderedModelBase subclass ModelManager did not return a QuerySet inheriting from OrderedModelQuerySet.", + obj=str(cls.__qualname__), + id="ordered_model.E004", ) ) return errors diff --git a/tests/tests.py b/tests/tests.py index 9823f000..a10727ba 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import User from django.core.management import call_command from django.core import checks +from django.db import models from django.db.models.signals import post_delete from django.dispatch import Signal from django.utils.timezone import now @@ -18,7 +19,7 @@ from tests.drf import ItemViewSet, router from tests.utils import assertNumQueries -from ordered_model.models import OrderedModel +from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet from ordered_model.signals import on_ordered_model_delete from tests.models import ( @@ -1280,11 +1281,11 @@ class Meta: self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ - checks.Warning( + checks.Error( msg="OrderedModelBase subclass needs Meta.ordering specified.", hint="If you have overwritten Meta, try inheriting with Meta(OrderedModel.Meta).", obj="ChecksTest.test_no_inherited_ordering..TestModel", - id="ordered_model.W001", + id="ordered_model.E001", ) ], ) @@ -1304,6 +1305,56 @@ class Meta(OrderedModel.Meta): self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) + def test_bad_ort(self): + class TestModel(OrderedModel): + order_with_respect_to = 7 + + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [ + checks.Error( + msg="OrderedModelBase subclass order_with_respect_to value invalid. Expected tuple, str or None.", + obj="ChecksTest.test_bad_ort..TestModel", + id="ordered_model.E002", + ) + ], + ) + + def test_bad_manager(self): + class BadModelManager(models.Manager.from_queryset(OrderedModelQuerySet)): + pass + class TestModel(OrderedModel): + objects = BadModelManager() + + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [ + checks.Error( + msg="OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager.", + obj="ChecksTest.test_bad_manager..TestModel", + id="ordered_model.E003", + ) + ], + ) + + def test_bad_queryset(self): + # I've swapped the inheritance order here so that the models.QuerySet is returned + class BadQSModelManager(models.Manager.from_queryset(models.QuerySet), OrderedModelManager): + pass + class TestModel(OrderedModel): + objects = BadQSModelManager() + + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [ + checks.Error( + msg="OrderedModelBase subclass ModelManager did not return a QuerySet inheriting from OrderedModelQuerySet.", + obj="ChecksTest.test_bad_queryset..TestModel", + id="ordered_model.E004", + ) + ], + ) + class TestCascadedDelete(TestCase): def test_that_model_when_deleted_by_cascade_still_maintains_ordering(self): From 34f267813ffedc226bfefe54a2efe62956a81fef Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Tue, 14 Mar 2023 12:57:49 +0000 Subject: [PATCH 40/74] check that order_with_respect_to values are ForeignKey fields --- ordered_model/models.py | 38 ++++++++++++++++++++++++++++++++-- tests/tests.py | 45 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/ordered_model/models.py b/ordered_model/models.py index c0cb3b32..cc76e091 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -1,9 +1,10 @@ from functools import partial, reduce from django.core import checks -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist from django.db import models from django.db.models import Max, Min, F +from django.db.models.fields.related import ForeignKey from django.db.models.constants import LOOKUP_SEP from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ @@ -143,7 +144,7 @@ def get_order_with_respect_to(cls): elif cls.order_with_respect_to is None: return tuple() else: - raise AssertionError("Invalid value for model.order_with_respect_to") + raise ValueError("Invalid value for model.order_with_respect_to") def _validate_ordering_reference(self, ref): if self._wrt_map() != ref._wrt_map(): @@ -347,6 +348,39 @@ def check(cls, **kwargs): id="ordered_model.E004", ) ) + + # each field may be an FK, or recursively an FK ref to an FK + try: + for wrt_field in cls.get_order_with_respect_to(): + mc = cls + for p in wrt_field.split(LOOKUP_SEP): + try: + f = mc._meta.get_field(p) + if not isinstance(f, ForeignKey): + errors.append( + checks.Error( + "OrderedModel order_with_respect_to specifies field '{0}' (within '{1}') which is not a ForeignKey. This is unsupported.".format( + p, wrt_field + ), + obj=str(cls.__qualname__), + id="ordered_model.E005", + ) + ) + break + mc = f.remote_field.model + except FieldDoesNotExist: + errors.append( + checks.Error( + "OrderedModel order_with_respect_to specifies field '{0}' (within '{1}') which does not exist.".format( + p, wrt_field + ), + obj=str(cls.__qualname__), + id="ordered_model.E006", + ) + ) + except ValueError: + # already handled by type checks for E002 + pass return errors diff --git a/tests/tests.py b/tests/tests.py index a10727ba..d10e85bb 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1305,7 +1305,7 @@ class Meta(OrderedModel.Meta): self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) - def test_bad_ort(self): + def test_bad_owrt(self): class TestModel(OrderedModel): order_with_respect_to = 7 @@ -1314,7 +1314,7 @@ class TestModel(OrderedModel): [ checks.Error( msg="OrderedModelBase subclass order_with_respect_to value invalid. Expected tuple, str or None.", - obj="ChecksTest.test_bad_ort..TestModel", + obj="ChecksTest.test_bad_owrt..TestModel", id="ordered_model.E002", ) ], @@ -1323,6 +1323,7 @@ class TestModel(OrderedModel): def test_bad_manager(self): class BadModelManager(models.Manager.from_queryset(OrderedModelQuerySet)): pass + class TestModel(OrderedModel): objects = BadModelManager() @@ -1339,8 +1340,11 @@ class TestModel(OrderedModel): def test_bad_queryset(self): # I've swapped the inheritance order here so that the models.QuerySet is returned - class BadQSModelManager(models.Manager.from_queryset(models.QuerySet), OrderedModelManager): + class BadQSModelManager( + models.Manager.from_queryset(models.QuerySet), OrderedModelManager + ): pass + class TestModel(OrderedModel): objects = BadQSModelManager() @@ -1355,6 +1359,41 @@ class TestModel(OrderedModel): ], ) + def test_owrt_not_foreign_key(self): + class TestModel(OrderedModel): + name = models.CharField(max_length=100) + order_with_respect_to = "name" + + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [ + checks.Error( + msg="OrderedModel order_with_respect_to specifies field 'name' (within 'name') which is not a ForeignKey. This is unsupported.", + obj="ChecksTest.test_owrt_not_foreign_key..TestModel", + id="ordered_model.E005", + ) + ], + ) + + def test_owrt_not_immediate_foreign_key(self): + class TestTargetModel(OrderedModel): + name = models.CharField(max_length=100) + + class TestModel(OrderedModel): + target = models.ForeignKey(to=TestTargetModel, on_delete=models.CASCADE) + order_with_respect_to = "target__name" + + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [ + checks.Error( + msg="OrderedModel order_with_respect_to specifies field 'name' (within 'target__name') which is not a ForeignKey. This is unsupported.", + obj="ChecksTest.test_owrt_not_immediate_foreign_key..TestModel", + id="ordered_model.E005", + ) + ], + ) + class TestCascadedDelete(TestCase): def test_that_model_when_deleted_by_cascade_still_maintains_ordering(self): From f43e93f0623c2fa01cf2353a8fa861060f6440a1 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Tue, 14 Mar 2023 14:35:34 +0000 Subject: [PATCH 41/74] add slowpath for admin that queries FK objects --- ordered_model/admin.py | 18 +++++++----------- ordered_model/models.py | 16 +++++++++++----- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/ordered_model/admin.py b/ordered_model/admin.py index 0cc360c0..2536339e 100644 --- a/ordered_model/admin.py +++ b/ordered_model/admin.py @@ -209,17 +209,13 @@ def move_up_down_links(self, obj): # 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._wrt_map() or [] - fields = [ - str(value.pk) - for value in order_with_respect_to.values() - if ( - type(value) == self.parent_model - or issubclass(self.parent_model, type(value)) - ) - and value is not None - and value.pk is not None - ] + fields = [] + for value in obj._get_related_objects(): + # Note 'a class is considered a subclass of itself' pydocs + if issubclass(self.parent_model, type(value)): + if value is not None and value.pk is not None: + fields.append(str(value.pk)) + order_obj_name = fields[0] if len(fields) > 0 else None model_info = self._get_model_info() diff --git a/ordered_model/models.py b/ordered_model/models.py index cc76e091..c508581e 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -128,13 +128,19 @@ def __init__(self, *args, **kwargs): def _wrt_map(self): d = {} - for order_field_name in self.get_order_with_respect_to(): - if not order_field_name.endswith("_id"): - order_field_name = order_field_name + "_id" - d[order_field_name] = get_lookup_value(self, order_field_name) - + for order_wrt_name in self.get_order_with_respect_to(): + # we know order_wrt_name is a ForeignKey, so use a cheaper _id lookup + field_path = order_wrt_name + "_id" + d[order_wrt_name] = get_lookup_value(self, field_path) return d + def _get_related_objects(self): + # slow path, for use in the admin which requires the objects + # expected to generate extra queries + return [ + get_lookup_value(self, name) for name in self.get_order_with_respect_to() + ] + @classmethod def get_order_with_respect_to(cls): if type(cls.order_with_respect_to) is tuple: From 71d6e074432e52ae972acf80c6015673c42eca07 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Tue, 14 Mar 2023 15:05:33 +0000 Subject: [PATCH 42/74] update docs prior to release --- CHANGES.md | 7 +++++++ README.md | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6b5b522a..dc6414d4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,13 @@ Change log Unreleased ---------- +3.7.2 - 2023-03-14 +---------- +- Fix a performance regression (unnecessary queries) in the WRT change detection (#286) +- Add a Check that `order_with_respect_to` specifies only ForeignKey fields +- Add a Check that our subclasses of ModelManager and QuerySet are used (#286) + + 3.7.1 - 2023-03-06 ---------- diff --git a/README.md b/README.md index f409be21..2be08965 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,8 @@ class GroupedItem(OrderedModel): Here items are put into groups that have some general information used by its items, but the ordering of the items is independent of the group the item is in. +In all cases `order_with_respect_to` must specify a `ForeignKey` field on the model, or a Django Check `E002`, `E005` or `E006` error will be raised with further help. + When you want ordering on the baseclass instead of subclasses in an ordered list of objects of various classes, specify the full module path of the base class: ```python @@ -242,9 +244,9 @@ When your model your extends `OrderedModel`, it inherits a custom `ModelManager` * `above(index)`, * `below(index)` -If your `Model` uses a custom `ModelManager` (such as `ItemManager` below) please have it extend `OrderedModelManager`. +If your `Model` uses a custom `ModelManager` (such as `ItemManager` below) please have it extend `OrderedModelManager`, or else Django Check `E003` will be raised. -If your `ModelManager` returns a custom `QuerySet` (such as `ItemQuerySet` below) please have it extend `OrderedModelQuerySet`. +If your `ModelManager` returns a custom `QuerySet` (such as `ItemQuerySet` below) please have it extend `OrderedModelQuerySet`, or Django Check `E004` will be raised. ```python from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet From ed8337bdd0abb1bb5d0d00a2f2f36b830cce9223 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Tue, 14 Mar 2023 15:25:24 +0000 Subject: [PATCH 43/74] update version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1e9dea50..76017d8f 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", - version="3.7.1", + version="3.7.2", 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", From ab72d563f83bcec7d4a454c615b0c46c9e30d13f Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Tue, 14 Mar 2023 18:51:35 +0000 Subject: [PATCH 44/74] Fix `reorder_model` management command re-ordering with multiple `order_with_respect_to` values --- CHANGES.md | 2 + .../management/commands/reorder_model.py | 24 ++++-------- tests/tests.py | 39 +++++++++++++++++++ 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index dc6414d4..583338bb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ Change log Unreleased ---------- +- Fix `reorder_model` management command re-ordering with multiple `order_with_respect_to` values + 3.7.2 - 2023-03-14 ---------- - Fix a performance regression (unnecessary queries) in the WRT change detection (#286) diff --git a/ordered_model/management/commands/reorder_model.py b/ordered_model/management/commands/reorder_model.py index 8507dd95..45d74be5 100644 --- a/ordered_model/management/commands/reorder_model.py +++ b/ordered_model/management/commands/reorder_model.py @@ -5,17 +5,6 @@ from ordered_model.models import OrderedModelBase -def get_order_with_respect_to(model): - if not model.order_with_respect_to: - return None - if isinstance(model.order_with_respect_to, str): - return model.order_with_respect_to - assert ( - len(model.order_with_respect_to) <= 1 - ), "re-ordering with more than 1 fields in order_with_respect_to is not supported" - return model.order_with_respect_to[0] - - class Command(BaseCommand): help = "Re-do the ordering of a certain Model" @@ -54,17 +43,18 @@ def handle(self, *args, **options): self.reorder(model) def reorder(self, model): - if model.order_with_respect_to: - order_with_respect_to = get_order_with_respect_to(model) - rel_kwargs = {"{}__isnull".format(order_with_respect_to): False} + owrt = model.get_order_with_respect_to() + if owrt: + rel_kwargs = dict([("{}__isnull".format(k), False) for k in owrt]) relation_to_list = ( - model.objects.order_by(order_with_respect_to) - .values_list(order_with_respect_to, flat=True) + model.objects.order_by(*owrt) + .values_list(*owrt) .filter(**rel_kwargs) .distinct() ) for relation_to in relation_to_list: - kwargs = {order_with_respect_to: relation_to} + kwargs = dict([(k, v) for k, v in zip(owrt, relation_to)]) + # print('re-ordering: {}'.format(kwargs)) self.reorder_queryset(model.objects.filter(**kwargs)) else: self.reorder_queryset(model.objects.all()) diff --git a/tests/tests.py b/tests/tests.py index d10e85bb..444c4895 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1125,6 +1125,45 @@ def test_reorder_with_respect_to(self): "changing order of tests.GroupedItem (3) from 1 to 2\n", out.getvalue() ) + def test_reorder_with_respect_to_tuple(self): + u1 = TestUser.objects.create() + u2 = TestUser.objects.create() + q1 = Question.objects.create() + q2 = Question.objects.create() + + for q in (q1, q2): + for u in (u1, u2): + Answer.objects.create(user=u, question=q, order=0) + Answer.objects.create(user=u, question=q, order=0) + + self.assertSequenceEqual( + Answer.objects.filter(user=u2, question=q1).values_list("order", flat=True), + [0, 0], + ) + + out = StringIO() + call_command("reorder_model", "tests.Answer", verbosity=1, stdout=out) + + self.assertSequenceEqual( + Answer.objects.filter(user=u2, question=q1).values_list("order", flat=True), + [0, 1], + ) + + self.assertEqual( + ( + "changing order of tests.Answer (2) from 0 to 1\n" + + "changing order of tests.Answer (4) from 0 to 1\n" + + "changing order of tests.Answer (6) from 0 to 1\n" + + "changing order of tests.Answer (8) from 0 to 1\n" + ), + out.getvalue(), + ) + + out = StringIO() + call_command("reorder_model", "tests.Answer", verbosity=1, stdout=out) + + self.assertEqual("", out.getvalue()) + def test_reorder_with_custom_order_field(self): """ Test that 'reorder_model' changes the order of OpenQuestions From 743cb6d720a3fd31f53828193d352408e8f0d5a1 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 15 Mar 2023 23:37:44 +0000 Subject: [PATCH 45/74] rework cascaded/queryset delete detection to avoid global signal listener Restrict signal handler 'senders' to subclasses of `OrderedModelBase` to avoid query count regression due to `Collector.can_fast_delete` logic in `models/deletion.py fixes #288 --- CHANGES.md | 3 +++ ordered_model/apps.py | 13 +++++++++---- ordered_model/models.py | 19 +++++++++++++++++++ ordered_model/signals.py | 30 ------------------------------ setup.py | 2 +- tests/tests.py | 22 +++++++++++++++++++--- 6 files changed, 51 insertions(+), 38 deletions(-) delete mode 100644 ordered_model/signals.py diff --git a/CHANGES.md b/CHANGES.md index 583338bb..004a43c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ Change log Unreleased ---------- +3.7.3 - 2023-03-15 +---------- +- Restrict signal handler 'senders' to subclasses of `OrderedModelBase` to avoid query count regression due to `Collector.can_fast_delete` logic in `models/deletion.py` (#288) - Fix `reorder_model` management command re-ordering with multiple `order_with_respect_to` values 3.7.2 - 2023-03-14 diff --git a/ordered_model/apps.py b/ordered_model/apps.py index 812a21e8..cd5ef707 100644 --- a/ordered_model/apps.py +++ b/ordered_model/apps.py @@ -1,4 +1,5 @@ -from django.apps import AppConfig +from django.apps import AppConfig, apps +from django.db.models.signals import post_delete class OrderedModelConfig(AppConfig): @@ -6,6 +7,10 @@ class OrderedModelConfig(AppConfig): label = "ordered_model" def ready(self): - # This import has side effects - # noinspection PyUnresolvedReferences - from .signals import on_ordered_model_delete + from .models import OrderedModelBase + + for cls in apps.get_models(): + if issubclass(cls, OrderedModelBase): + post_delete.connect( + cls._on_ordered_model_delete, sender=cls, dispatch_uid=cls.__name__ + ) diff --git a/ordered_model/models.py b/ordered_model/models.py index c508581e..04f10a8d 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -141,6 +141,25 @@ def _get_related_objects(self): get_lookup_value(self, name) for name in self.get_order_with_respect_to() ] + @classmethod + def _on_ordered_model_delete(cls, sender=None, instance=None, **kwargs): + """ + This signal handler makes sure that when an OrderedModelBase is deleted via + cascade database deletes, or queryset delete that the models keep order. + """ + + if getattr(instance, "_was_deleted_via_delete_method", False): + return + + extra_update = kwargs.get("extra_update", None) + + # Copy of upshuffle logic from OrderedModelBase.delete + qs = instance.get_ordering_queryset() + extra_update = {} if extra_update is None else extra_update + qs.above_instance(instance).decrease_order(**extra_update) + + setattr(instance, "_was_deleted_via_delete_method", True) + @classmethod def get_order_with_respect_to(cls): if type(cls.order_with_respect_to) is tuple: diff --git a/ordered_model/signals.py b/ordered_model/signals.py deleted file mode 100644 index 1b846312..00000000 --- a/ordered_model/signals.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db.models.signals import post_delete -from django.dispatch import receiver -from ordered_model.models import OrderedModelBase -from django.db.models import F - - -@receiver(post_delete, dispatch_uid="on_ordered_model_delete") -def on_ordered_model_delete(sender, instance, **kwargs): - """ - This signal makes sure that when an OrderedModelBase is deleted via cascade database deletes, the models - keep order. - """ - - """ - We're only interested in subclasses of OrderedModelBase. - We want to be able to support 'extra_kwargs' on the delete() - method, which we can't do if we do all our work in the signal. We add a property to signal whether or not - the model's .delete() method was called, because if so - we don't need to do any more work. - """ - if not issubclass(sender, OrderedModelBase): - return - if getattr(instance, "_was_deleted_via_delete_method", False): - return - - extra_update = kwargs.get("extra_update", None) - - # Copy of upshuffle logic from OrderedModelBase.delete - qs = instance.get_ordering_queryset() - extra_update = {} if extra_update is None else extra_update - qs.above_instance(instance).decrease_order(**extra_update) diff --git a/setup.py b/setup.py index 76017d8f..91a782b0 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", - version="3.7.2", + version="3.7.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", diff --git a/tests/tests.py b/tests/tests.py index 444c4895..f7e98376 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -20,7 +20,7 @@ from tests.utils import assertNumQueries from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet -from ordered_model.signals import on_ordered_model_delete + from tests.models import ( Answer, @@ -33,6 +33,7 @@ Pizza, Topping, PizzaToppingsThroughModel, + BaseQuestion, OpenQuestion, MultipleChoiceQuestion, ItemGroup, @@ -1202,10 +1203,25 @@ def test_delete_bypass(self): OpenQuestion.objects.create(answer="4", order=3) # bypass our OrderedModel delete logic to leave a hole in ordering - self.assertTrue(post_delete.disconnect(dispatch_uid="on_ordered_model_delete")) + # remove signal handlers + # print(post_delete.receivers) + self.assertTrue( + post_delete.disconnect( + sender=OpenQuestion, dispatch_uid=OpenQuestion.__name__ + ) + ) + self.assertTrue( + post_delete.disconnect( + sender=BaseQuestion, dispatch_uid=BaseQuestion.__name__ + ) + ) + + # delete on the queryset fires post_delete, but does not call model.delete() OpenQuestion.objects.filter(answer="3").delete() post_delete.connect( - on_ordered_model_delete, dispatch_uid="on_ordered_model_delete" + OpenQuestion._on_ordered_model_delete, + sender=OpenQuestion, + dispatch_uid=OpenQuestion.__name__, ) self.assertEqual([0, 1, 3], [i.order for i in OpenQuestion.objects.all()]) From 7b9c89f75acc8216e45931e29ca7c1e360802ff7 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 17 Mar 2023 15:39:23 +0000 Subject: [PATCH 46/74] relax OrderedModelManager check to a Warning if it returns correct OrderedModelQuerySet --- CHANGES.md | 5 +++++ ordered_model/models.py | 25 ++++++++++++++++++------- setup.py | 2 +- tests/tests.py | 17 ++++++++++++++++- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 004a43c6..0ae0dd33 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,11 @@ Change log Unreleased ---------- +3.7.4 - 2023-03-17 +---------- +- Relax Check for `OrderedModelManager` to a `Warning`, if the manager returns the correct queryset (#290) + + 3.7.3 - 2023-03-15 ---------- - Restrict signal handler 'senders' to subclasses of `OrderedModelBase` to avoid query count regression due to `Collector.can_fast_delete` logic in `models/deletion.py` (#288) diff --git a/ordered_model/models.py b/ordered_model/models.py index 04f10a8d..c00754c8 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -358,14 +358,25 @@ def check(cls, **kwargs): ) ) if not issubclass(cls.objects.__class__, OrderedModelManager): - errors.append( - checks.Error( - "OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager.", - obj=str(cls.__qualname__), - id="ordered_model.E003", + # Not using our Manager. This is an Error if the queryset is also wrong, or + # a Warning if our own QuerySet is returned. + if issubclass(cls.objects.none().__class__, OrderedModelQuerySet): + errors.append( + checks.Warning( + "OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager. This is not ideal but will work.", + obj=str(cls.__qualname__), + id="ordered_model.W003", + ) ) - ) - if not issubclass(cls.objects.none().__class__, OrderedModelQuerySet): + else: + errors.append( + checks.Error( + "OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager.", + obj=str(cls.__qualname__), + id="ordered_model.E003", + ) + ) + elif not issubclass(cls.objects.none().__class__, OrderedModelQuerySet): errors.append( checks.Error( "OrderedModelBase subclass ModelManager did not return a QuerySet inheriting from OrderedModelQuerySet.", diff --git a/setup.py b/setup.py index 91a782b0..22ab5391 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", - version="3.7.3", + version="3.7.4", description="Allows Django models to be ordered and provides a simple admin interface for reordering them.", author="Ben Firshman", author_email="ben@firshman.co.uk", diff --git a/tests/tests.py b/tests/tests.py index f7e98376..b19644c3 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1376,7 +1376,7 @@ class TestModel(OrderedModel): ) def test_bad_manager(self): - class BadModelManager(models.Manager.from_queryset(OrderedModelQuerySet)): + class BadModelManager(models.Manager.from_queryset(models.QuerySet)): pass class TestModel(OrderedModel): @@ -1393,6 +1393,21 @@ class TestModel(OrderedModel): ], ) + def test_builtin_manager_to_queryset(self): + class TestModel(OrderedModel): + objects = OrderedModelQuerySet.as_manager() + + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [ + checks.Warning( + msg="OrderedModelBase subclass has a ModelManager that does not inherit from OrderedModelManager. This is not ideal but will work.", + obj="ChecksTest.test_builtin_manager_to_queryset..TestModel", + id="ordered_model.W003", + ) + ], + ) + def test_bad_queryset(self): # I've swapped the inheritance order here so that the models.QuerySet is returned class BadQSModelManager( From 5e03a93823c4cded172d37dcfd4743d57b60b0e6 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 31 Mar 2023 11:02:29 +0100 Subject: [PATCH 47/74] rename duplicate test methods #296 --- tests/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index b19644c3..e0d9bcb5 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -738,10 +738,10 @@ def test_previous(self): def test_previous_first(self): self.assertEqual(self.p2_t1.previous(), None) - def test_down(self): + def test_next(self): self.assertEqual(self.p2_t1.next(), self.p2_t2) - def test_down_last(self): + def test_next_last(self): self.assertEqual(self.p1_t3.next(), None) def test_up(self): From 715217264735f668a1396e4efd5144d201aa75d7 Mon Sep 17 00:00:00 2001 From: Joris Jansen Date: Mon, 5 Jun 2023 15:56:47 +0200 Subject: [PATCH 48/74] Remove extra 'your' in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2be08965..e643b3bc 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ class GroupedItem(OrderedModel): Here items are put into groups that have some general information used by its items, but the ordering of the items is independent of the group the item is in. -In all cases `order_with_respect_to` must specify a `ForeignKey` field on the model, or a Django Check `E002`, `E005` or `E006` error will be raised with further help. +In all cases `order_with_respect_to` must specify a `ForeignKey` field on the model, or a Django Check `E002`, `E005` or `E006` error will be raised with further help. When you want ordering on the baseclass instead of subclasses in an ordered list of objects of various classes, specify the full module path of the base class: @@ -235,7 +235,7 @@ class OpenQuestion(BaseQuestion): Custom Manager and QuerySet ----------------- -When your model your extends `OrderedModel`, it inherits a custom `ModelManager` instance which in turn provides additional operations on the resulting `QuerySet`. For example if `Item` is an `OrderedModel` subclass, the queryset `Item.objects.all()` has functions: +When your model extends `OrderedModel`, it inherits a custom `ModelManager` instance which in turn provides additional operations on the resulting `QuerySet`. For example if `Item` is an `OrderedModel` subclass, the queryset `Item.objects.all()` has functions: * `above_instance(object)`, * `below_instance(object)`, From 3e2c0c448049f94588d359c838032c5123fdca6c Mon Sep 17 00:00:00 2001 From: Solomon Hawk Date: Wed, 28 Jun 2023 11:15:13 -0400 Subject: [PATCH 49/74] Return super().delete() in OrderedModel#delete - this makes django-ordered-model play nicely with other libraries that rely on the return value of model#delete such as django-safedelete --- ordered_model/models.py | 2 +- tests/tests.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ordered_model/models.py b/ordered_model/models.py index c00754c8..ef1a9cc5 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -230,7 +230,7 @@ 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) + return super().delete(*args, **kwargs) def swap(self, replacement): """ diff --git a/tests/tests.py b/tests/tests.py index e0d9bcb5..c295ef4b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -151,7 +151,10 @@ def test_below_self(self): self.assertNames(["1", "2", "3", "4"]) def test_delete(self): - Item.objects.get(pk=2).delete() + deleted = Item.objects.get(pk=2).delete() + # the default return value of delete is (num_deleted, deleted_count_per_model) + # https://github.com/django/django/blob/main/django/db/models/deletion.py#L522 + self.assertEqual(deleted, (1, {"tests.Item": 1})) self.assertNames(["1", "3", "4"]) Item.objects.get(pk=3).up() self.assertNames(["3", "1", "4"]) From df489a0eb4193e99754778539bbd59c44e9ef206 Mon Sep 17 00:00:00 2001 From: mkuehne Date: Thu, 20 Jul 2023 17:43:12 +0200 Subject: [PATCH 50/74] #306 do not rely on upshuffle logic for potential out of order bulk operations --- ordered_model/models.py | 15 +++++++++------ tests/tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ordered_model/models.py b/ordered_model/models.py index ef1a9cc5..29e8183f 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -151,12 +151,15 @@ def _on_ordered_model_delete(cls, sender=None, instance=None, **kwargs): if getattr(instance, "_was_deleted_via_delete_method", False): return - extra_update = kwargs.get("extra_update", None) - - # Copy of upshuffle logic from OrderedModelBase.delete - qs = instance.get_ordering_queryset() - extra_update = {} if extra_update is None else extra_update - qs.above_instance(instance).decrease_order(**extra_update) + # upshuffle logic from OrderedModelBase.delete can't be used here because signal + # handlers run per instance, but not necessarily in the right order + qs = instance.get_ordering_queryset().only("pk", instance.order_field_name) + to_update = set() + for i, item in enumerate(qs): + if getattr(item, instance.order_field_name) != i: + setattr(item, instance.order_field_name, i) + to_update.add(item) + qs.bulk_update(to_update, (instance.order_field_name,)) setattr(instance, "_was_deleted_via_delete_method", True) diff --git a/tests/tests.py b/tests/tests.py index c295ef4b..b0c1c8fd 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1495,3 +1495,35 @@ def test_that_model_when_deleted_by_cascade_still_maintains_ordering(self): # Assert the hole has been filled self.assertEqual(child_with_order_0.order, 0) self.assertEqual(child_with_order_2.order, 1) + + def test_that_model_when_multiple_unordered_deleted_by_cascade_still_maintain_ordering( + self, + ): + parent_for_order_1_and_0_child = CascadedParentModel.objects.create() + # reverse the order on the first two children + child_with_order_1 = CascadedOrderedModel.objects.create( + parent=parent_for_order_1_and_0_child, + order=1, + ) + child_with_order_0 = CascadedOrderedModel.objects.create( + parent=parent_for_order_1_and_0_child, + order=0, + ) + parent_for_order_2_and_3_child = CascadedParentModel.objects.create() + child_with_order_2 = CascadedOrderedModel.objects.create( + parent=parent_for_order_2_and_3_child + ) + child_with_order_3 = CascadedOrderedModel.objects.create( + parent=parent_for_order_2_and_3_child + ) + + # Delete positition 0 and 1 parent, now there's a hole of two, which child_with_order_2 and 3 should take + parent_for_order_1_and_0_child.delete() + + # Refresh children from db + child_with_order_2.refresh_from_db() + child_with_order_3.refresh_from_db() + + # Assert the hole has been filled + self.assertEqual(child_with_order_2.order, 0) + self.assertEqual(child_with_order_3.order, 1) From 3df23a82641d05737993063289c8204c9573de2a Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 18 Oct 2023 21:08:40 +0100 Subject: [PATCH 51/74] update CHANGES.md --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 0ae0dd33..75484d3a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,8 @@ Change log Unreleased ---------- +- Fix `post_delete` signal triggered upshuffles to do a potentially expensive full reordering of the owrt group (#307) + 3.7.4 - 2023-03-17 ---------- @@ -14,6 +16,7 @@ Unreleased - Restrict signal handler 'senders' to subclasses of `OrderedModelBase` to avoid query count regression due to `Collector.can_fast_delete` logic in `models/deletion.py` (#288) - Fix `reorder_model` management command re-ordering with multiple `order_with_respect_to` values + 3.7.2 - 2023-03-14 ---------- - Fix a performance regression (unnecessary queries) in the WRT change detection (#286) From 1df98a48858948a764459c7cdd9a5c16d72d72e9 Mon Sep 17 00:00:00 2001 From: Solomon Hawk Date: Wed, 28 Jun 2023 11:08:36 -0400 Subject: [PATCH 52/74] Add `--batch_size` arg to management command (for bulk_update) --- .../management/commands/reorder_model.py | 8 ++++- tests/tests.py | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/ordered_model/management/commands/reorder_model.py b/ordered_model/management/commands/reorder_model.py index 45d74be5..1be77ba2 100644 --- a/ordered_model/management/commands/reorder_model.py +++ b/ordered_model/management/commands/reorder_model.py @@ -10,6 +10,7 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("model_name", type=str, nargs="*") + parser.add_argument("--batch_size", type=int, nargs=1, default=1000) def handle(self, *args, **options): """ @@ -17,6 +18,8 @@ def handle(self, *args, **options): try re-ordering to a working state. """ self.verbosity = options["verbosity"] + self.batch_size = options["batch_size"] + orderedmodels = [ m._meta.label for m in apps.get_models() if issubclass(m, OrderedModelBase) ] @@ -78,4 +81,7 @@ def reorder_queryset(self, queryset): ) setattr(obj, order_field_name, order) bulk_update_list.append(obj) - model.objects.bulk_update(bulk_update_list, [order_field_name]) + + model.objects.bulk_update( + bulk_update_list, [order_field_name], batch_size=self.batch_size + ) diff --git a/tests/tests.py b/tests/tests.py index b0c1c8fd..06cf95fd 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1245,6 +1245,42 @@ def test_delete_bypass(self): "changing order of tests.OpenQuestion (4) from 3 to 2\n", out.getvalue() ) + def test_reorder_with_custom_batch_size(self): + """ + Test that 'reorder_model' can be called with a valid `batch_size` argument. + """ + OpenQuestion.objects.create(order=0) + OpenQuestion.objects.create(order=0) + out = StringIO() + call_command( + "reorder_model", "tests.OpenQuestion", verbosity=1, stdout=out, batch_size=2 + ) + + self.assertSequenceEqual( + OpenQuestion.objects.values_list("order", flat=True).order_by("order"), + [0, 1], + ) + self.assertIn( + "changing order of tests.OpenQuestion (2) from 0 to 1", out.getvalue() + ) + + def test_reorder_with_invalid_custom_batch_size(self): + """ + Test that 'reorder_model' raises a TypeError if a non-int value is passed + as the `batch_size` argument. + """ + OpenQuestion.objects.create(order=0) + OpenQuestion.objects.create(order=0) + + with self.assertRaises(TypeError): + call_command( + "reorder_model", + "tests.OpenQuestion", + verbosity=1, + stdout=StringIO(), + batch_size="2", + ) + class DRFTestCase(APITestCase): fixtures = ["test_items.json"] From a3294b9f7e5e9a5f81a7f26bc25f184295374edb Mon Sep 17 00:00:00 2001 From: Solomon Hawk Date: Wed, 18 Oct 2023 16:35:59 -0400 Subject: [PATCH 53/74] Set default batch_size to None --- ordered_model/management/commands/reorder_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ordered_model/management/commands/reorder_model.py b/ordered_model/management/commands/reorder_model.py index 1be77ba2..bcbc5db4 100644 --- a/ordered_model/management/commands/reorder_model.py +++ b/ordered_model/management/commands/reorder_model.py @@ -10,7 +10,7 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("model_name", type=str, nargs="*") - parser.add_argument("--batch_size", type=int, nargs=1, default=1000) + parser.add_argument("--batch_size", type=int, nargs=1, default=None) def handle(self, *args, **options): """ From 40414feb0b7b86b45d7ea65c75cf63d7b58ad50c Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 18 Oct 2023 21:52:13 +0100 Subject: [PATCH 54/74] update CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 75484d3a..edef9814 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Change log Unreleased ---------- - Fix `post_delete` signal triggered upshuffles to do a potentially expensive full reordering of the owrt group (#307) +- Support passing custom `--batch_size` to `reorder_model` management command (#303) 3.7.4 - 2023-03-17 From d4941cac2b0c5dc8d3f6da25cfa0f3b09cd35c43 Mon Sep 17 00:00:00 2001 From: Jonah George Date: Fri, 13 Oct 2023 13:32:33 -0700 Subject: [PATCH 55/74] Fix Django 4 deprecation warning --- ordered_model/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ordered_model/__init__.py b/ordered_model/__init__.py index 852967ac..c81b7b7c 100644 --- a/ordered_model/__init__.py +++ b/ordered_model/__init__.py @@ -1 +1,4 @@ -default_app_config = "ordered_model.apps.OrderedModelConfig" +import django + +if django.VERSION < (3, 2): + default_app_config = "ordered_model.apps.OrderedModelConfig" From c834f9ce2ab70f1e0e7a419147db5c8ccb090a9e Mon Sep 17 00:00:00 2001 From: Jonah George Date: Wed, 18 Oct 2023 12:09:38 -0700 Subject: [PATCH 56/74] Update ordered_model/__init__.py Co-authored-by: Chris Shucksmith --- ordered_model/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ordered_model/__init__.py b/ordered_model/__init__.py index c81b7b7c..6b2bca2c 100644 --- a/ordered_model/__init__.py +++ b/ordered_model/__init__.py @@ -1,4 +1,4 @@ import django if django.VERSION < (3, 2): - default_app_config = "ordered_model.apps.OrderedModelConfig" + default_app_config = "ordered_model.apps.OrderedModelConfig" From a970d684e3f15c0d68de5a5615a53416fa38821e Mon Sep 17 00:00:00 2001 From: Nik Nyby Date: Mon, 26 Jun 2023 11:05:28 -0400 Subject: [PATCH 57/74] Update github actions versions --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 127401ed..d04813c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,9 +13,9 @@ jobs: python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10'] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 4570e28607c79db4f02d01972a2bde3060fbbb19 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 29 Jan 2024 11:01:39 +0000 Subject: [PATCH 58/74] fix: upstream black change to formatting rules --- ordered_model/admin.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ordered_model/admin.py b/ordered_model/admin.py index 2536339e..481ac27a 100644 --- a/ordered_model/admin.py +++ b/ordered_model/admin.py @@ -91,9 +91,11 @@ def move_view(self, request, object_id, direction): 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 "", + ( + ("?" + iri_to_uri(request.META.get("QUERY_STRING", ""))) + if request.META.get("QUERY_STRING", "") + else "" + ), ) return HttpResponseRedirect(redir_path) @@ -196,9 +198,11 @@ def move_view(self, request, admin_id, object_id, direction): 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 "", + ( + ("?" + iri_to_uri(request.META.get("QUERY_STRING", ""))) + if request.META.get("QUERY_STRING", "") + else "" + ), ) return HttpResponseRedirect(redir_path) From 48f74edb3e5b481d44a2340a93e870d79a2f91be Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 29 Jan 2024 21:17:09 +0000 Subject: [PATCH 59/74] fix build icon --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e643b3bc..822c72ec 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ django-ordered-model ==================== -[![Build Status](https://secure.travis-ci.org/bfirsh/django-ordered-model.png?branch=master)](https://travis-ci.org/bfirsh/django-ordered-model) +[![Build Status](https://github.com/django-ordered-model/django-ordered-model/actions/workflows/test.yml/badge.svg)](https://github.com/django-ordered-model/django-ordered-model/actions/workflows/test.yml) [![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 From fc9e23a7cf4a14b04b332856a626ca2d5d43d9cc Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 2 Feb 2024 15:11:00 +0000 Subject: [PATCH 60/74] disprove issue 196 --- tests/models.py | 12 ++++++++++++ tests/tests.py | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/tests/models.py b/tests/models.py index 2113835d..e778bbed 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,6 +1,7 @@ from django.db import models from ordered_model.models import OrderedModel, OrderedModelBase +import uuid # test simple automatic ordering @@ -134,3 +135,14 @@ class CascadedParentModel(models.Model): class CascadedOrderedModel(OrderedModel): parent = models.ForeignKey(to=CascadedParentModel, on_delete=models.CASCADE) + + +class Flow(models.Model): + pass + + +class StateMachine(OrderedModel): + id = models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True) + name = models.CharField(max_length=32) + flow = models.ForeignKey(Flow, on_delete=models.PROTECT, null=True, blank=True) + order_with_respect_to = "flow" diff --git a/tests/tests.py b/tests/tests.py index 06cf95fd..c9d0e6ca 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -41,6 +41,8 @@ TestUser, CascadedParentModel, CascadedOrderedModel, + Flow, + StateMachine, ) @@ -857,6 +859,30 @@ def test_bottom(self): ) +class ConstructorTest(TestCase): + def test_constructors_issue196(self): + self.f1 = Flow.objects.create() + + self.sm1 = StateMachine(name="a", flow_id=self.f1.id) + self.sm1.save() + + self.sm2 = StateMachine() + self.sm2.name = "b" + self.sm2.flow = self.f1 + self.sm2.save() + + self.sm3 = StateMachine.objects.create(name="c", flow=self.f1) + + self.assertSequenceEqual( + StateMachine.objects.values_list("flow__pk", "order", "name"), + [ + (self.f1.pk, 0, "a"), + (self.f1.pk, 1, "b"), + (self.f1.pk, 2, "c"), + ], + ) + + class MultiOrderWithRespectToTests(TestCase): def setUp(self): q1 = Question.objects.create() From 9b3b021932340901bfe8bc0eadbab7efb9ed2e72 Mon Sep 17 00:00:00 2001 From: Nik Nyby Date: Mon, 26 Jun 2023 11:04:26 -0400 Subject: [PATCH 61/74] GitHub Actions: Test on python 3.11 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d04813c6..34db1979 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10'] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11'] steps: - uses: actions/checkout@v3 From 071da018ac382f46af384936913f83ae898a3cca Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 2 Feb 2024 15:23:41 +0000 Subject: [PATCH 62/74] test django 4.1 on python 3.11 --- CHANGES.md | 1 + tox.ini | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index edef9814..e1b93cf4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ Unreleased ---------- - Fix `post_delete` signal triggered upshuffles to do a potentially expensive full reordering of the owrt group (#307) - Support passing custom `--batch_size` to `reorder_model` management command (#303) +- Add tox builder for python 3.11, Django 4.1 and above 3.7.4 - 2023-03-17 diff --git a/tox.ini b/tox.ini index 16bde43b..4c7c3cc0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,8 +5,8 @@ envlist = py{36,37,38,39,310}-django31 py{36,37,38,39,310}-django32 py{38,39,310}-django40 - py{38,39,310}-django41 - py{310}-djangoupstream + py{38,39,310,311}-django41 + py{310,311}-djangoupstream py{310}-drfupstream black @@ -19,6 +19,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [testenv] deps = From 8e5d6413fd6183ba82be7131e4d45e4dbb0dfc70 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 6 Mar 2023 17:07:30 +0000 Subject: [PATCH 63/74] add fields.OrderedManyToManyField to order related models by through model Meta Assuming a 'through' model is provided to the field, it will be queried while constructing QuerySets for related models and the through Model Meta ordering clauses will be applied. Use in place of fields.ManyToManyField. --- CHANGES.md | 2 +- README.md | 26 +++++++++++++++++++++ ordered_model/fields.py | 48 +++++++++++++++++++++++++++++++++++++ tests/models.py | 16 +++++++++++++ tests/tests.py | 52 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 ordered_model/fields.py diff --git a/CHANGES.md b/CHANGES.md index e1b93cf4..d0def4d3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Unreleased - Fix `post_delete` signal triggered upshuffles to do a potentially expensive full reordering of the owrt group (#307) - Support passing custom `--batch_size` to `reorder_model` management command (#303) - Add tox builder for python 3.11, Django 4.1 and above - +- Add `ordered_model.fields.OrderedManyToManyField` which respects `Meta.ordering` when following ManyToMany related fields. (#277) 3.7.4 - 2023-03-17 ---------- diff --git a/README.md b/README.md index 822c72ec..2c838130 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,32 @@ class OpenQuestion(BaseQuestion): answer = models.TextField(max_length=100) ``` +Ordering of ManyToMany Relationship query results +----------------- + +Django ManyToMany relationships created by `ManyToManyField` [do not respect `Meta.ordering` on the intermediate model](https://code.djangoproject.com/ticket/30460) in results fetched from the 'members' queryset. For example with our usual `Pizza` example, getting the `Toppings` for a `hawaiian_pizza` instance using `PizzaToppingsThroughModel.objects.filter(pizza=hawaiian_pizza).all()` is correctly ordered (by the ThroughModel `Meta.ordering`). However `hawaiian_pizza.toppings.all()` is not, and returns the objects following the 'to' model ordering. + +To work around this, explicitly add an ordering clause, e.g. with `hawaiian_pizza.toppings.all().order_by('pizzatoppingsthroughmodel__order')` or use our `OrderedManyToManyField` which does this by default: + +```python +from ordered_model.fields import OrderedManyToManyField + +class Pizza(models.Model): + name = models.CharField(max_length=100) + toppings = OrderedManyToManyField(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") +``` + +With this definition `hawaiian_pizza.toppings.all()` returns toppings in order. + Custom Manager and QuerySet ----------------- When your model extends `OrderedModel`, it inherits a custom `ModelManager` instance which in turn provides additional operations on the resulting `QuerySet`. For example if `Item` is an `OrderedModel` subclass, the queryset `Item.objects.all()` has functions: diff --git a/ordered_model/fields.py b/ordered_model/fields.py new file mode 100644 index 00000000..44eec833 --- /dev/null +++ b/ordered_model/fields.py @@ -0,0 +1,48 @@ +from django.db.models.fields.related_descriptors import ( + ManyToManyDescriptor, + create_forward_many_to_many_manager, +) +from django.utils.functional import cached_property +from django.db import models + +# OrderedManyToManyField can be used in place of ManyToManyField and will +# sort the returned data by the model Meta ordering when traversing child +# objects + + +def create_sorted_forward_many_to_many_manager(superclass, rel, reverse): + cls = create_forward_many_to_many_manager(superclass, rel, reverse) + + class SortedManyRelatedManager(cls): + def get_queryset(self): + qs = super().get_queryset() + m = rel.through._meta + if m.ordering: + # import pdb; pdb.set_trace() + ors = [m.model_name + "__" + field for field in m.ordering] + qs = qs.order_by(*ors) + return qs + + return SortedManyRelatedManager + + +class SortedManyToManyDescriptor(ManyToManyDescriptor): + def __init__(self, field): + super().__init__(field.remote_field) + + @cached_property + def related_manager_cls(self): + related_model = self.rel.related_model if self.reverse else self.rel.model + + return create_sorted_forward_many_to_many_manager( + related_model._default_manager.__class__, + self.rel, + reverse=self.reverse, + ) + + +class OrderedManyToManyField(models.ManyToManyField): + def contribute_to_class(self, cls, name, **kwargs): + super().contribute_to_class(cls, name, **kwargs) + # print(f"contributed to {cls} {name} remote_field={self.remote_field}") + setattr(cls, self.name, SortedManyToManyDescriptor(self)) diff --git a/tests/models.py b/tests/models.py index e778bbed..2ff0fa34 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,6 +1,7 @@ from django.db import models from ordered_model.models import OrderedModel, OrderedModelBase +from ordered_model.fields import OrderedManyToManyField import uuid @@ -146,3 +147,18 @@ class StateMachine(OrderedModel): name = models.CharField(max_length=32) flow = models.ForeignKey(Flow, on_delete=models.PROTECT, null=True, blank=True) order_with_respect_to = "flow" + + +# Duplicate Pizza models using OrderedManyToManyField +class PizzaOM2M(models.Model): + name = models.CharField(max_length=100) + toppings = OrderedManyToManyField(Topping, through="PizzaOM2MToppingsThroughModel") + + +class PizzaOM2MToppingsThroughModel(OrderedModel): + pizza = models.ForeignKey(PizzaOM2M, on_delete=models.CASCADE) + topping = models.ForeignKey(Topping, on_delete=models.CASCADE) + order_with_respect_to = "pizza" + + class Meta: + ordering = ("pizza", "order") diff --git a/tests/tests.py b/tests/tests.py index c9d0e6ca..66e58480 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -31,8 +31,10 @@ CustomPKGroupItem, CustomPKGroup, Pizza, + PizzaOM2M, Topping, PizzaToppingsThroughModel, + PizzaOM2MToppingsThroughModel, BaseQuestion, OpenQuestion, MultipleChoiceQuestion, @@ -733,6 +735,30 @@ def test_saved_order(self): ], ) + def test_members_order_issue277(self): + # make order differ from pk order + self.p1_t3.top() # anchovy, tomatoe, mozarella, + + # ManyToMany relationship iterates by 'to' model order, ie. PK of topping + l1 = self.p1.toppings.all().values_list("name", flat=True) + self.assertEqual(list(l1), ["tomatoe", "mozarella", "anchovy"]) # pk order + + # Through model ordering is ordered correctly + l2 = ( + PizzaToppingsThroughModel.objects.filter(pizza=self.p1) + .all() + .values_list("topping__name", flat=True) + ) + self.assertEqual(list(l2), ["anchovy", "tomatoe", "mozarella"]) # ordered + + # explicit ordering works + l3 = ( + self.p1.toppings.all() + .order_by("pizzatoppingsthroughmodel__order") + .values_list("name", flat=True) + ) + self.assertEqual(list(l3), ["anchovy", "tomatoe", "mozarella"]) # ordered + def test_swap(self): with self.assertRaises(ValueError): self.p1_t1.swap(self.p2_t1) @@ -883,6 +909,32 @@ def test_constructors_issue196(self): ) +class OrderWithRespectToTestsOrderedManyToManyField(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.p1 = PizzaOM2M.objects.create(name="Napoli") + # tomatoe, mozarella, anchovy + self.p1_t1 = PizzaOM2MToppingsThroughModel.objects.create( + pizza=self.p1, topping=self.t1 + ) + self.p1_t2 = PizzaOM2MToppingsThroughModel.objects.create( + pizza=self.p1, topping=self.t2 + ) + self.p1_t3 = PizzaOM2MToppingsThroughModel.objects.create( + pizza=self.p1, topping=self.t3 + ) + + def test_members_order_issue277(self): + # make order differ from pk order + self.p1_t3.top() # anchovy, tomatoe, mozarella, + + # OrderedManyToMany relationship iterates by ordered model order + l1 = self.p1.toppings.all().values_list("name", flat=True) + self.assertEqual(list(l1), ["anchovy", "tomatoe", "mozarella"]) + + class MultiOrderWithRespectToTests(TestCase): def setUp(self): q1 = Question.objects.create() From 94d91e56b04af4cf9add7f573f6468d14c0fd8e5 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 5 Feb 2024 16:16:26 +0000 Subject: [PATCH 64/74] Relax check that `order_with_respect_to` entries final element must be a `ForeignKey` - it can be any `Field` instance (#298) This required re-working the optimisation in c4630dca2bdafd6bac1bd2328081f524953872cf to handle OWRT paths ending on a non ForeignKey field. --- CHANGES.md | 2 + ordered_model/models.py | 41 ++++++++++++----- tests/models.py | 22 +++++++++ tests/tests.py | 98 ++++++++++++++++++++++++++++++++++++++--- tox.ini | 4 +- 5 files changed, 147 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d0def4d3..d7f7167a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,8 @@ Unreleased - Support passing custom `--batch_size` to `reorder_model` management command (#303) - Add tox builder for python 3.11, Django 4.1 and above - Add `ordered_model.fields.OrderedManyToManyField` which respects `Meta.ordering` when following ManyToMany related fields. (#277) +- Relax check that `order_with_respect_to` entries final element must be a `ForeignKey` - it can be any `Field` instance (#298) + 3.7.4 - 2023-03-17 ---------- diff --git a/ordered_model/models.py b/ordered_model/models.py index 29e8183f..330301df 100644 --- a/ordered_model/models.py +++ b/ordered_model/models.py @@ -10,9 +10,24 @@ from django.utils.translation import gettext_lazy as _ -def get_lookup_value(obj, field): +def get_lookup_value(obj, wrt_field, use_fkid=True): + # starting with obj, traverse the wrt_field path and return the value of the + # final field. if field_path *ends* at a ForeignKey, and use_fkid=True, return the pk + # of the fk rather than build the object itself. try: - return reduce(lambda i, f: getattr(i, f), field.split(LOOKUP_SEP), obj) + mc = type(obj) + path = wrt_field.split(LOOKUP_SEP) + leafindex = len(path) - 1 + for depth, p in enumerate(path): + f = mc._meta.get_field(p) + if depth == leafindex and use_fkid and isinstance(f, ForeignKey): + return getattr(obj, p + "_id") + elif depth == leafindex: + return getattr(obj, p) + else: + mc = f.remote_field.model + obj = getattr(obj, p) + except ObjectDoesNotExist: return None @@ -129,16 +144,15 @@ def __init__(self, *args, **kwargs): def _wrt_map(self): d = {} for order_wrt_name in self.get_order_with_respect_to(): - # we know order_wrt_name is a ForeignKey, so use a cheaper _id lookup - field_path = order_wrt_name + "_id" - d[order_wrt_name] = get_lookup_value(self, field_path) + d[order_wrt_name] = get_lookup_value(self, order_wrt_name, use_fkid=True) return d def _get_related_objects(self): # slow path, for use in the admin which requires the objects # expected to generate extra queries return [ - get_lookup_value(self, name) for name in self.get_order_with_respect_to() + get_lookup_value(self, name, use_fkid=False) + for name in self.get_order_with_respect_to() ] @classmethod @@ -388,17 +402,19 @@ def check(cls, **kwargs): ) ) - # each field may be an FK, or recursively an FK ref to an FK + # each field may be an FK ref, until the leaf which may be an FK ref or another type of field try: for wrt_field in cls.get_order_with_respect_to(): mc = cls - for p in wrt_field.split(LOOKUP_SEP): + path = wrt_field.split(LOOKUP_SEP) + leafindex = len(path) - 1 + for depth, p in enumerate(path): try: f = mc._meta.get_field(p) - if not isinstance(f, ForeignKey): + if depth < leafindex and not isinstance(f, ForeignKey): errors.append( checks.Error( - "OrderedModel order_with_respect_to specifies field '{0}' (within '{1}') which is not a ForeignKey. This is unsupported.".format( + "OrderedModel order_with_respect_to specifies intermediate field '{0}' (within '{1}') which is not a ForeignKey. This is unsupported.".format( p, wrt_field ), obj=str(cls.__qualname__), @@ -406,7 +422,10 @@ def check(cls, **kwargs): ) ) break - mc = f.remote_field.model + + if isinstance(f, ForeignKey): + mc = f.remote_field.model + except FieldDoesNotExist: errors.append( checks.Error( diff --git a/tests/models.py b/tests/models.py index 2ff0fa34..4a265d17 100644 --- a/tests/models.py +++ b/tests/models.py @@ -162,3 +162,25 @@ class PizzaOM2MToppingsThroughModel(OrderedModel): class Meta: ordering = ("pizza", "order") + + +# issue 298 +class Training(models.Model): + pass + + +class TrainingExercise(OrderedModel, models.Model): + WARMUP = 1 + MAINPART = 2 + ENDPART = 3 + TrainingChoices = [ + (WARMUP, "WarmUp"), + (MAINPART, "MainPart"), + (ENDPART, "EndPart"), + ] + training = models.ForeignKey(Training, on_delete=models.CASCADE) + stage = models.PositiveSmallIntegerField(choices=TrainingChoices) + order_with_respect_to = ("training", "stage") + + class Meta: + ordering = ("training", "stage") diff --git a/tests/tests.py b/tests/tests.py index 66e58480..6c3f6e87 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -45,6 +45,8 @@ CascadedOrderedModel, Flow, StateMachine, + Training, + TrainingExercise, ) @@ -1020,6 +1022,47 @@ def test_above_between_groups(self): ) +class TestOrderWithRespectToNonFKFieldsTest(TestCase): + def setUp(self): + self.t1 = Training.objects.create() + self.t2 = Training.objects.create() + tc = TrainingExercise + self.tc = tc + self.te1_wu_0 = TrainingExercise.objects.create( + training=self.t1, stage=tc.WARMUP + ) + self.te1_wu_1 = TrainingExercise.objects.create( + training=self.t1, stage=tc.WARMUP + ) + self.te2_wu_0 = TrainingExercise.objects.create( + training=self.t2, stage=tc.WARMUP + ) + self.te2_mp_0 = TrainingExercise.objects.create( + training=self.t2, stage=tc.MAINPART + ) + self.te2_mp_1 = TrainingExercise.objects.create( + training=self.t2, stage=tc.MAINPART + ) + + def test_move_between_groups(self): + tc = self.tc + self.te2_mp_1.stage = TrainingExercise.WARMUP + self.te2_mp_1.save() + + self.assertSequenceEqual( + TrainingExercise.objects.all().values_list( + "pk", "training", "stage", "order" + ), + [ + (1, self.t1.pk, tc.WARMUP, 0), + (2, self.t1.pk, tc.WARMUP, 1), + (3, self.t2.pk, tc.WARMUP, 0), + (5, self.t2.pk, tc.WARMUP, 1), + (4, self.t2.pk, tc.MAINPART, 0), + ], + ) + + class PolymorphicOrderGenerationTests(TestCase): def test_order_of_baselist(self): o1 = OpenQuestion.objects.create() @@ -1551,20 +1594,29 @@ class TestModel(OrderedModel): name = models.CharField(max_length=100) order_with_respect_to = "name" + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [], + ) + + def test_owrt_not_exist(self): + class TestModel(OrderedModel): + order_with_respect_to = "name" + self.assertEqual( checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Error( - msg="OrderedModel order_with_respect_to specifies field 'name' (within 'name') which is not a ForeignKey. This is unsupported.", - obj="ChecksTest.test_owrt_not_foreign_key..TestModel", - id="ordered_model.E005", + msg="OrderedModel order_with_respect_to specifies field 'name' (within 'name') which does not exist.", + obj="ChecksTest.test_owrt_not_exist..TestModel", + id="ordered_model.E006", ) ], ) - def test_owrt_not_immediate_foreign_key(self): + def test_owrt_leaf_not_exist(self): class TestTargetModel(OrderedModel): - name = models.CharField(max_length=100) + pass class TestModel(OrderedModel): target = models.ForeignKey(to=TestTargetModel, on_delete=models.CASCADE) @@ -1574,13 +1626,45 @@ class TestModel(OrderedModel): checks.run_checks(app_configs=self.apps.get_app_configs()), [ checks.Error( - msg="OrderedModel order_with_respect_to specifies field 'name' (within 'target__name') which is not a ForeignKey. This is unsupported.", - obj="ChecksTest.test_owrt_not_immediate_foreign_key..TestModel", + msg="OrderedModel order_with_respect_to specifies field 'name' (within 'target__name') which does not exist.", + obj="ChecksTest.test_owrt_leaf_not_exist..TestModel", + id="ordered_model.E006", + ) + ], + ) + + def test_owrt_intermediate_not_fk(self): + class TestModel(OrderedModel): + target = models.CharField(max_length=100) + order_with_respect_to = "target__name" + + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [ + checks.Error( + msg="OrderedModel order_with_respect_to specifies intermediate field 'target' (within 'target__name') which is not a ForeignKey. This is unsupported.", + obj="ChecksTest.test_owrt_intermediate_not_fk..TestModel", id="ordered_model.E005", ) ], ) + def test_owrt_deep(self): + class TestTargetModel(OrderedModel): + name = models.CharField(max_length=100) + + class TestMiddleModel(OrderedModel): + target = models.ForeignKey(to=TestTargetModel, on_delete=models.CASCADE) + + class TestModel(OrderedModel): + middle = models.ForeignKey(to=TestMiddleModel, on_delete=models.CASCADE) + order_with_respect_to = "middle__target__name" + + self.assertEqual( + checks.run_checks(app_configs=self.apps.get_app_configs()), + [], + ) + class TestCascadedDelete(TestCase): def test_that_model_when_deleted_by_cascade_still_maintains_ordering(self): diff --git a/tox.ini b/tox.ini index 4c7c3cc0..91d43c98 100644 --- a/tox.ini +++ b/tox.ini @@ -38,9 +38,9 @@ deps = django40: djangorestframework~=3.13.0 django41: djangorestframework~=3.13.0 djangoupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz - coverage commands = - coverage run {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs} + {envbindir}/django-admin check --pythonpath=. --settings=tests.settings + {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs} [testenv:black] basepython = python3 From 8477a18496cf1b6dad91e8bb7ac053cc92fcf0b9 Mon Sep 17 00:00:00 2001 From: kylepollina Date: Sun, 3 Mar 2024 15:32:09 -0800 Subject: [PATCH 65/74] Update README.md --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2c838130..1cc7dea6 100644 --- a/README.md +++ b/README.md @@ -408,21 +408,23 @@ Django Rest Framework To support updating ordering fields by Django Rest Framework, we include a serializer `OrderedModelSerializer` that intercepts writes to the ordering field, and calls `OrderedModel.to()` method to effect a re-ordering: - from rest_framework import routers, serializers, viewsets - from ordered_model.serializers import OrderedModelSerializer - from tests.models import CustomItem +```python +from rest_framework import routers, serializers, viewsets +from ordered_model.serializers import OrderedModelSerializer +from tests.models import CustomItem - class ItemSerializer(serializers.HyperlinkedModelSerializer, OrderedModelSerializer): - class Meta: - model = CustomItem - fields = ['pkid', 'name', 'modified', 'order'] +class ItemSerializer(serializers.HyperlinkedModelSerializer, OrderedModelSerializer): + class Meta: + model = CustomItem + fields = ['pkid', 'name', 'modified', 'order'] - class ItemViewSet(viewsets.ModelViewSet): - queryset = CustomItem.objects.all() - serializer_class = ItemSerializer +class ItemViewSet(viewsets.ModelViewSet): + queryset = CustomItem.objects.all() + serializer_class = ItemSerializer - router = routers.DefaultRouter() - router.register(r'items', ItemViewSet) +router = routers.DefaultRouter() +router.register(r'items', ItemViewSet) +``` Note that you need to include the 'order' field (or your custom field name) in the `Serializer`'s `fields` list, either explicitly or using `__all__`. See [ordered_model/serializers.py](ordered_model/serializers.py) for the implementation. From a6dc605fd3f06ee13e62d27f109e9ccd3af3bfac Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 12 Jul 2024 07:20:00 -0500 Subject: [PATCH 66/74] chore(OrderedInlineModelAdminMixin): Fix typo in docstring --- ordered_model/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ordered_model/admin.py b/ordered_model/admin.py index 481ac27a..44738c1d 100644 --- a/ordered_model/admin.py +++ b/ordered_model/admin.py @@ -144,7 +144,7 @@ def move_up_down_links(self, obj): class OrderedInlineModelAdminMixin: """ - ModelAdminMixin for classes that contain OrderedInilines + ModelAdminMixin for classes that contain OrderedInlines. """ def get_urls(self): From fe04e8f27489eeded93e0d0fc531885981206c51 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 15 Jul 2024 20:04:08 +0100 Subject: [PATCH 67/74] update handle pip/pypi TLSv1 deprecation --- .github/workflows/test.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34db1979..de931f02 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,13 +11,20 @@ jobs: strategy: matrix: python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11'] - + include: + - pip-trusted-host: '' + # Relax security checks for Python 3.5 only. (https://github.com/actions/setup-python/issues/866) + - python-version: '3.5' + pip-trusted-host: 'pypi.python.org pypi.org files.pythonhosted.org' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + env: + PIP_TRUSTED_HOST: ${{ matrix.pip-trusted-host }} + PIP_DISABLE_PIP_VERSION_CHECK: 1 - name: Install dependencies run: | python -m pip install --upgrade pip From 24ac19ad084a6bed24d333ed6296f282bb25fa51 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 15 Jul 2024 20:22:06 +0100 Subject: [PATCH 68/74] test Django 4.2, upgrade drfupstream to django>4.2 --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 91d43c98..f91bd073 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = py{36,37,38,39,310}-django32 py{38,39,310}-django40 py{38,39,310,311}-django41 + py{38,39,310,311}-django42 py{310,311}-djangoupstream py{310}-drfupstream black @@ -29,14 +30,16 @@ deps = django32: Django>=3.2,<4.0 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 + django42: Django>=4.2,<4.3 djangoupstream: https://github.com/django/django/archive/main.tar.gz - drfupstream: Django~=3.2.0 + drfupstream: Django~=4.2.0 drfupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz django22: djangorestframework~=3.12.0 django30,django31,django32: djangorestframework~=3.12.0 django40: djangorestframework~=3.13.0 django41: djangorestframework~=3.13.0 + django42: djangorestframework~=3.15.0 djangoupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz commands = {envbindir}/django-admin check --pythonpath=. --settings=tests.settings From 36506ada7fc926475aa10903b608c31c49fc947e Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Mon, 15 Jul 2024 19:58:04 +0100 Subject: [PATCH 69/74] add test case proposed on #320 (already fixed) --- tests/models.py | 15 +++++++++++++++ tests/tests.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/tests/models.py b/tests/models.py index 4a265d17..f35b23f7 100644 --- a/tests/models.py +++ b/tests/models.py @@ -184,3 +184,18 @@ class TrainingExercise(OrderedModel, models.Model): class Meta: ordering = ("training", "stage") + + +# issue 320 parent/child models +class Foobar(models.Model): + name = models.CharField(max_length=100) + + +class ParentModel(OrderedModel): + name = models.CharField(max_length=100) + foobar = models.ForeignKey(Foobar, on_delete=models.CASCADE) + order_with_respect_to = "foobar" + + +class ChildModel(ParentModel): + age = models.IntegerField() diff --git a/tests/tests.py b/tests/tests.py index 6c3f6e87..14fad3b3 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -47,6 +47,9 @@ StateMachine, Training, TrainingExercise, + Foobar, + ChildModel, + ParentModel, ) @@ -1725,3 +1728,34 @@ def test_that_model_when_multiple_unordered_deleted_by_cascade_still_maintain_or # Assert the hole has been filled self.assertEqual(child_with_order_2.order, 0) self.assertEqual(child_with_order_3.order, 1) + + +## @pytest.mark.django_db +class ParentChildModelTests(TestCase): + def test_parent_child_order(self): + foobar = Foobar.objects.create(name="foobar") + child1 = ChildModel.objects.create(name="child1", foobar=foobar, age=1) + child2 = ChildModel.objects.create(name="child2", foobar=foobar, age=2) + child3 = ChildModel.objects.create(name="child3", foobar=foobar, age=3) + child4 = ChildModel.objects.create(name="child4", foobar=foobar, age=4) + + # This is the order of the children at the start + assert child1.order == 0 + assert child2.order == 1 + assert child3.order == 2 + assert child4.order == 3 + + # Delete the first child + # This causes the parent to be deleted as well + child1.delete() + + # Refresh the db + child2.refresh_from_db() + child3.refresh_from_db() + child4.refresh_from_db() + + # The order of the children should be updated + # The expected order + assert child2.order == 0 + assert child3.order == 1 + assert child4.order == 2 From 2a9d37bf17a3dca185833670570451c817e7e330 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 1 Nov 2024 10:55:04 +0000 Subject: [PATCH 70/74] version 3.8 alpha --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d7f7167a..3a83499d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ Change log ========== -Unreleased +3.8.0 ---------- - Fix `post_delete` signal triggered upshuffles to do a potentially expensive full reordering of the owrt group (#307) - Support passing custom `--batch_size` to `reorder_model` management command (#303) From 0711da89e9860330ef0738d14502ea9d273dcf7f Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 1 Nov 2024 11:04:40 +0000 Subject: [PATCH 71/74] bump github actions due to deprecation --- .github/workflows/distribute.yml | 6 +++--- .github/workflows/lint.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/distribute.yml b/.github/workflows/distribute.yml index 03fb4357..60f38049 100644 --- a/.github/workflows/distribute.yml +++ b/.github/workflows/distribute.yml @@ -8,9 +8,9 @@ jobs: build: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.8 - name: Install dependencies @@ -20,7 +20,7 @@ jobs: run: python setup.py sdist bdist_wheel - name: Run twine check run: twine check dist/* - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4.4.3 with: name: django-ordered-model-dist path: dist diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c7d21b5e..9ab9f6b5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 - uses: psf/black@stable From c0a1870a219daf1e854d348bfe15ea13e090a94d Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 1 Nov 2024 11:19:30 +0000 Subject: [PATCH 72/74] bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 22ab5391..1b3cccd0 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ name="django-ordered-model", long_description=long_description, long_description_content_type="text/markdown", - version="3.7.4", + version="3.8.0-alpha", 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", From b3383ab58e3586d9fe7f0358ef1829fc8907b1b9 Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Wed, 6 Nov 2024 16:15:21 +0000 Subject: [PATCH 73/74] add version badge --- README.md | 2 +- setup.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cc7dea6..c6bd2838 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ django-ordered-model ==================== -[![Build Status](https://github.com/django-ordered-model/django-ordered-model/actions/workflows/test.yml/badge.svg)](https://github.com/django-ordered-model/django-ordered-model/actions/workflows/test.yml) +![Python versions](https://img.shields.io/pypi/pyversions/django-ordered-model.svg) [![Build Status](https://github.com/django-ordered-model/django-ordered-model/actions/workflows/test.yml/badge.svg)](https://github.com/django-ordered-model/django-ordered-model/actions/workflows/test.yml) [![PyPI version](https://badge.fury.io/py/django-ordered-model.svg)](https://badge.fury.io/py/django-ordered-model) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) diff --git a/setup.py b/setup.py index 1b3cccd0..1d59b95c 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,14 @@ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], zip_safe=False, package_data={ From f4a338ca4b9e1c7d9edb677fddc6c2743552232e Mon Sep 17 00:00:00 2001 From: Chris Shucksmith Date: Fri, 15 Nov 2024 15:06:39 +0000 Subject: [PATCH 74/74] add test run on Django 5.0, 5.1 --- .github/workflows/test.yml | 2 +- README.md | 2 ++ tox.ini | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de931f02..5818e09d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11'] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11', '3.12'] include: - pip-trusted-host: '' # Relax security checks for Python 3.5 only. (https://github.com/actions/setup-python/issues/866) diff --git a/README.md b/README.md index c6bd2838..f18e72d4 100644 --- a/README.md +++ b/README.md @@ -450,6 +450,8 @@ Compatibility with Django and Python |django-ordered-model version | Django version | Python version | DRF (optional) |-----------------------------|---------------------|-------------------|---------------- +| **3.8.x** | **3.x**, **4.x**, **5.x** | **3.10** to **3.12** | 3.15 and above +| **3.7.x** | **3.x**, **4.x** | **3.5** and above | 3.12 and above | **3.6.x** | **3.x**, **4.x** | **3.5** and above | 3.12 and above | **3.5.x** | **3.x**, **4.x** | **3.5** and above | - | **3.4.x** | **2.x**, **3.x** | **3.5** and above | - diff --git a/tox.ini b/tox.ini index f91bd073..6d7870c0 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,9 @@ envlist = py{38,39,310}-django40 py{38,39,310,311}-django41 py{38,39,310,311}-django42 - py{310,311}-djangoupstream + py{310,311,312}-django50 + py{310,311,312}-django51 + py{310,311,312}-djangoupstream py{310}-drfupstream black @@ -21,6 +23,7 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 [testenv] deps = @@ -31,6 +34,8 @@ deps = django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 django42: Django>=4.2,<4.3 + django50: Django>=5.0,<5.1 + django51: Django>=5.1,<5.2 djangoupstream: https://github.com/django/django/archive/main.tar.gz drfupstream: Django~=4.2.0 @@ -40,6 +45,8 @@ deps = django40: djangorestframework~=3.13.0 django41: djangorestframework~=3.13.0 django42: djangorestframework~=3.15.0 + django50: djangorestframework~=3.15.0 + django51: djangorestframework~=3.15.0 djangoupstream: https://github.com/encode/django-rest-framework/archive/master.tar.gz commands = {envbindir}/django-admin check --pythonpath=. --settings=tests.settings