Skip to content
Snippets Groups Projects
Commit acc7df83 authored by Michael Fladischer's avatar Michael Fladischer
Browse files

New upstream version 3.6

parent 2f723bdb
No related branches found
No related tags found
No related merge requests found
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
----------------
......
......@@ -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
-----
......@@ -46,6 +53,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`:
......@@ -224,7 +233,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)`,
......@@ -233,18 +242,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`:
......@@ -359,6 +375,29 @@ re-order one or more models.
- `<model_name>`: 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 @@ $ 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 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
......@@ -12,7 +12,7 @@ setup(
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 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")
......@@ -11,6 +11,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"django.contrib.sessions",
"ordered_model",
"rest_framework",
"tests",
]
SECRET_KEY = "topsecret"
......@@ -36,5 +37,6 @@ TEMPLATES = [
},
}
]
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/"
......@@ -4,9 +4,14 @@ from io import StringIO
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 @@ class ReorderModelTestCase(TestCase):
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)
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))]
[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 @@ 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
commands =
coverage run {envbindir}/django-admin test --pythonpath=. --settings=tests.settings {posargs}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment