diff -Nru python-django-ordered-model-3.5/CHANGES.md python-django-ordered-model-3.6/CHANGES.md --- python-django-ordered-model-3.5/CHANGES.md 2022-01-12 19:14:58.000000000 +0000 +++ python-django-ordered-model-3.6/CHANGES.md 2022-05-30 14:01:39.000000000 +0000 @@ -1,6 +1,12 @@ Change log ========== +3.6 - 2022-05-30 +---------- + +- Add `serializers.OrderedModelSerializer` to allow Django Rest Framework to re-order models (#251 #264) +- Add tox builder for Django 4.0, drop building against 2.0 and 2.1 due to DRF compatibility. + 3.5 - 2022-01-12 ---------------- diff -Nru python-django-ordered-model-3.5/debian/changelog python-django-ordered-model-3.6/debian/changelog --- python-django-ordered-model-3.5/debian/changelog 2022-01-18 10:34:17.000000000 +0000 +++ python-django-ordered-model-3.6/debian/changelog 2022-08-01 20:48:03.000000000 +0000 @@ -1,3 +1,11 @@ +python-django-ordered-model (3.6-1) unstable; urgency=low + + * New upstream release. + * Add python3-djangorestframework to Build-Depends, required by tests. + * Bump Standards-Version to 4.6.1.0. + + -- Michael Fladischer Mon, 01 Aug 2022 20:48:03 +0000 + python-django-ordered-model (3.5-1) unstable; urgency=low * New upstream release. diff -Nru python-django-ordered-model-3.5/debian/control python-django-ordered-model-3.6/debian/control --- python-django-ordered-model-3.5/debian/control 2022-01-18 10:34:17.000000000 +0000 +++ python-django-ordered-model-3.6/debian/control 2022-08-01 20:48:03.000000000 +0000 @@ -10,9 +10,10 @@ python3-all, python3-babel, python3-django, + python3-djangorestframework, python3-setuptools, python3-six, -Standards-Version: 4.6.0.1 +Standards-Version: 4.6.1.0 Vcs-Browser: https://salsa.debian.org/python-team/packages/python-django-ordered-model Vcs-Git: https://salsa.debian.org/python-team/packages/python-django-ordered-model.git Homepage: https://github.com/bfirsh/django-ordered-model diff -Nru python-django-ordered-model-3.5/ordered_model/serializers.py python-django-ordered-model-3.6/ordered_model/serializers.py --- python-django-ordered-model-3.5/ordered_model/serializers.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-ordered-model-3.6/ordered_model/serializers.py 2022-05-30 14:01:39.000000000 +0000 @@ -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 -Nru python-django-ordered-model-3.5/README.md python-django-ordered-model-3.6/README.md --- python-django-ordered-model-3.5/README.md 2022-01-12 19:14:58.000000000 +0000 +++ python-django-ordered-model-3.6/README.md 2022-05-30 14:01:39.000000000 +0000 @@ -29,6 +29,13 @@ $ 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 ----- @@ -46,6 +53,8 @@ ``` +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`: @@ -224,7 +233,7 @@ 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)`, @@ -233,18 +242,25 @@ * `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`: @@ -359,6 +375,29 @@ - ``: 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 ---------- @@ -378,16 +417,17 @@ 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 diff -Nru python-django-ordered-model-3.5/setup.py python-django-ordered-model-3.6/setup.py --- python-django-ordered-model-3.5/setup.py 2022-01-12 19:14:58.000000000 +0000 +++ python-django-ordered-model-3.6/setup.py 2022-05-30 14:01:39.000000000 +0000 @@ -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", diff -Nru python-django-ordered-model-3.5/tests/drf.py python-django-ordered-model-3.6/tests/drf.py --- python-django-ordered-model-3.5/tests/drf.py 1970-01-01 00:00:00.000000000 +0000 +++ python-django-ordered-model-3.6/tests/drf.py 2022-05-30 14:01:39.000000000 +0000 @@ -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 -Nru python-django-ordered-model-3.5/tests/settings.py python-django-ordered-model-3.6/tests/settings.py --- python-django-ordered-model-3.5/tests/settings.py 2022-01-12 19:14:58.000000000 +0000 +++ python-django-ordered-model-3.6/tests/settings.py 2022-05-30 14:01:39.000000000 +0000 @@ -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 -Nru python-django-ordered-model-3.5/tests/tests.py python-django-ordered-model-3.6/tests/tests.py --- python-django-ordered-model-3.5/tests/tests.py 2022-01-12 19:14:58.000000000 +0000 +++ python-django-ordered-model-3.6/tests/tests.py 2022-05-30 14:01:39.000000000 +0000 @@ -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 @@ 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 -Nru python-django-ordered-model-3.5/tests/urls.py python-django-ordered-model-3.6/tests/urls.py --- python-django-ordered-model-3.5/tests/urls.py 2022-01-12 19:14:58.000000000 +0000 +++ python-django-ordered-model-3.6/tests/urls.py 2022-05-30 14:01:39.000000000 +0000 @@ -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 -Nru python-django-ordered-model-3.5/tox.ini python-django-ordered-model-3.6/tox.ini --- python-django-ordered-model-3.5/tox.ini 2022-01-12 19:14:58.000000000 +0000 +++ python-django-ordered-model-3.6/tox.ini 2022-05-30 14:01:39.000000000 +0000 @@ -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,13 +20,18 @@ [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 commands = coverage run {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs}