From 6f9228ae59fdb0348325e746b284d707d32878a8 Mon Sep 17 00:00:00 2001 From: enrico Date: Tue, 30 Aug 2022 09:55:27 +0800 Subject: [PATCH] Two reproducible issues in DRF nested serializers with many to many through model --- tests/conftest.py | 1 + tests/issue/__init__.py | 0 tests/issue/migrations/0001_initial.py | 42 ++++++++++++++++ tests/issue/migrations/__init__.py | 0 tests/issue/models.py | 15 ++++++ tests/issue/serializers.py | 30 ++++++++++++ tests/issue/test_issue.py | 66 ++++++++++++++++++++++++++ tests/issue/urls.py | 10 ++++ tests/issue/views.py | 13 +++++ 9 files changed, 177 insertions(+) create mode 100644 tests/issue/__init__.py create mode 100644 tests/issue/migrations/0001_initial.py create mode 100644 tests/issue/migrations/__init__.py create mode 100644 tests/issue/models.py create mode 100644 tests/issue/serializers.py create mode 100644 tests/issue/test_issue.py create mode 100644 tests/issue/urls.py create mode 100644 tests/issue/views.py diff --git a/tests/conftest.py b/tests/conftest.py index 79cabd5e1..1c30b1c2f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,6 +63,7 @@ def pytest_configure(config): 'rest_framework.authtoken', 'tests.authentication', 'tests.generic_relations', + 'tests.issue', 'tests.importable', 'tests', ), diff --git a/tests/issue/__init__.py b/tests/issue/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/issue/migrations/0001_initial.py b/tests/issue/migrations/0001_initial.py new file mode 100644 index 000000000..8656bec67 --- /dev/null +++ b/tests/issue/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 3.1.13 on 2022-01-23 09:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=10)), + ], + ), + migrations.CreateModel( + name='ItemAmount', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.IntegerField()), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue.item')), + ], + ), + migrations.CreateModel( + name='Summary', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('items', models.ManyToManyField(through='issue.ItemAmount', to='issue.Item')), + ], + ), + migrations.AddField( + model_name='itemamount', + name='summary', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue.summary'), + ), + ] diff --git a/tests/issue/migrations/__init__.py b/tests/issue/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/issue/models.py b/tests/issue/models.py new file mode 100644 index 000000000..6c568f2f3 --- /dev/null +++ b/tests/issue/models.py @@ -0,0 +1,15 @@ +from django.db import models + + +class Item(models.Model): + name = models.CharField(max_length=10) + + +class ItemAmount(models.Model): + summary = models.ForeignKey('Summary', on_delete=models.CASCADE) + item = models.ForeignKey(Item, on_delete=models.CASCADE) + amount = models.IntegerField() + + +class Summary(models.Model): + items = models.ManyToManyField(Item, through=ItemAmount) diff --git a/tests/issue/serializers.py b/tests/issue/serializers.py new file mode 100644 index 000000000..b94a1b091 --- /dev/null +++ b/tests/issue/serializers.py @@ -0,0 +1,30 @@ +from rest_framework import serializers + +from . import models + + +class ItemAmountSerializer(serializers.ModelSerializer): + item = serializers.PrimaryKeyRelatedField(queryset=models.Item.objects.all()) + + class Meta: + model = models.ItemAmount + fields = ('item', 'amount') + + +class SummarySerializer(serializers.ModelSerializer): + items = ItemAmountSerializer(many=True) + + def create(self, validated_data): + items = validated_data.pop('items') + instance = super().create(validated_data) + for item in items: + instance.items.add( + item['item'], through_defaults=dict( + amount=item['amount'] + ) + ) + return instance + + class Meta: + model = models.Summary + fields = ('items', ) diff --git a/tests/issue/test_issue.py b/tests/issue/test_issue.py new file mode 100644 index 000000000..72ca491d3 --- /dev/null +++ b/tests/issue/test_issue.py @@ -0,0 +1,66 @@ +from django.test import TestCase, override_settings +from django.urls import reverse + +from rest_framework.test import APIClient +from rest_framework.utils import json + +from . import models, serializers + + +class TestSerializer(TestCase): + def test_create(self): + item = models.Item.objects.create(name='test') + data = { + "items": [ + { + "item": item.id, + "amount": 100, + } + ], + } + serializer = serializers.SummarySerializer(data=data) + serializer.is_valid(raise_exception=True) + expected_data = { + "items": [ + { + "item": item, + "amount": 100, + } + ], + } + assert serializer.validated_data == expected_data + serializer.save() + assert models.Summary.objects.count() == 1 + + +@override_settings(ROOT_URLCONF='tests.issue.urls') +class TestIssueViewSet(TestCase): + def test_create(self): + api_client = APIClient() + item = models.Item.objects.create(name='test') + data = { + "items": [ + { + "item": item.id, + "amount": 100, + } + ], + } + response = api_client.post(reverse('summary-list'), data) + print(response.content) + assert response.status_code == 201 + + def test_create_with_json(self): + api_client = APIClient() + item = models.Item.objects.create(name='test') + data = { + "items": [ + { + "item": item.id, + "amount": 100, + } + ], + } + response = api_client.post(reverse('summary-list'), json.dumps(data), content_type='application/json') + print(response.content) + assert response.status_code == 201 diff --git a/tests/issue/urls.py b/tests/issue/urls.py new file mode 100644 index 000000000..1ea6e0142 --- /dev/null +++ b/tests/issue/urls.py @@ -0,0 +1,10 @@ +from rest_framework.routers import DefaultRouter + +from . import views + +app_name = "issue" +router = DefaultRouter() + +router.register(r'summary', views.SummaryViewSet, basename='summary') + +urlpatterns = router.urls diff --git a/tests/issue/views.py b/tests/issue/views.py new file mode 100644 index 000000000..f5d5647ab --- /dev/null +++ b/tests/issue/views.py @@ -0,0 +1,13 @@ +from rest_framework import viewsets +from rest_framework.permissions import AllowAny + +from . import models, serializers + + +class SummaryViewSet(viewsets.ModelViewSet): + """ + A simple ViewSet for viewing and editing accounts. + """ + queryset = models.Summary.objects.all() + serializer_class = serializers.SummarySerializer + permission_classes = [AllowAny]