Skip to content

Commit 577bb3c

Browse files
authored
Validation on ManyToManyField when default=None (#9790)
* Added validation on ManyToMany relations when default=None and tests * Some clarifications in contributing.md
1 parent c0f3649 commit 577bb3c

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

docs/community/contributing.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,45 @@ To run the tests, clone the repository, and then:
8181
# Run the tests
8282
./runtests.py
8383

84+
---
85+
86+
**Note:** if your tests require access to the database, do not forget to inherit from `django.test.TestCase` or use the `@pytest.mark.django_db()` decorator.
87+
88+
For example, with TestCase:
89+
90+
from django.test import TestCase
91+
92+
class MyDatabaseTest(TestCase):
93+
def test_something(self):
94+
# Your test code here
95+
pass
96+
97+
Or with decorator:
98+
99+
import pytest
100+
101+
@pytest.mark.django_db()
102+
class MyDatabaseTest:
103+
def test_something(self):
104+
# Your test code here
105+
pass
106+
107+
You can reuse existing models defined in `tests/models.py` for your tests.
108+
109+
---
110+
84111
### Test options
85112

86113
Run using a more concise output style.
87114

88115
./runtests.py -q
89116

117+
118+
If you do not want the output to be captured (for example, to see print statements directly), you can use the `-s` flag.
119+
120+
./runtests.py -s
121+
122+
90123
Run the tests for a given test case.
91124

92125
./runtests.py MyTestCase
@@ -99,6 +132,7 @@ Shorter form to run the tests for a given test method.
99132

100133
./runtests.py test_this_method
101134

135+
102136
Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input.
103137

104138
### Running against multiple environments

rest_framework/serializers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,13 @@ def get_fields(self):
10901090
# Determine the fields that should be included on the serializer.
10911091
fields = {}
10921092

1093+
# If it's a ManyToMany field, and the default is None, then raises an exception to prevent exceptions on .set()
1094+
for field_name in declared_fields.keys():
1095+
if field_name in info.relations and info.relations[field_name].to_many and declared_fields[field_name].default is None:
1096+
raise ValueError(
1097+
f"The field '{field_name}' on serializer '{self.__class__.__name__}' is a ManyToMany field and cannot have a default value of None."
1098+
)
1099+
10931100
for field_name in field_names:
10941101
# If the field is explicitly declared on the class then use that.
10951102
if field_name in declared_fields:

tests/test_serializer.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66

77
import pytest
88
from django.db import models
9+
from django.test import TestCase
910

1011
from rest_framework import exceptions, fields, relations, serializers
1112
from rest_framework.fields import Field
1213

1314
from .models import (
14-
ForeignKeyTarget, NestedForeignKeySource, NullableForeignKeySource
15+
ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
16+
NestedForeignKeySource, NullableForeignKeySource
1517
)
1618
from .utils import MockObject
1719

@@ -64,6 +66,7 @@ def setup_method(self):
6466
class ExampleSerializer(serializers.Serializer):
6567
char = serializers.CharField()
6668
integer = serializers.IntegerField()
69+
6770
self.Serializer = ExampleSerializer
6871

6972
def test_valid_serializer(self):
@@ -774,3 +777,35 @@ def test_nested_key(self):
774777
ret = {'a': 1}
775778
self.s.set_value(ret, ['x', 'y'], 2)
776779
assert ret == {'a': 1, 'x': {'y': 2}}
780+
781+
782+
class TestWarningManyToMany(TestCase):
783+
def test_warning_many_to_many(self):
784+
"""Tests that using a PrimaryKeyRelatedField for a ManyToMany field breaks with default=None."""
785+
class ManyToManySourceSerializer(serializers.ModelSerializer):
786+
targets = serializers.PrimaryKeyRelatedField(
787+
many=True,
788+
queryset=ManyToManyTarget.objects.all(),
789+
default=None
790+
)
791+
792+
class Meta:
793+
model = ManyToManySource
794+
fields = '__all__'
795+
796+
# Instantiates serializer without 'value' field to force using the default=None for the ManyToMany relation
797+
serializer = ManyToManySourceSerializer(data={
798+
"name": "Invalid Example",
799+
})
800+
801+
error_msg = "The field 'targets' on serializer 'ManyToManySourceSerializer' is a ManyToMany field and cannot have a default value of None."
802+
803+
# Calls to get_fields() should raise a ValueError
804+
with pytest.raises(ValueError) as exc_info:
805+
serializer.get_fields()
806+
assert str(exc_info.value) == error_msg
807+
808+
# Calls to is_valid() should behave the same
809+
with pytest.raises(ValueError) as exc_info:
810+
serializer.is_valid(raise_exception=True)
811+
assert str(exc_info.value) == error_msg

0 commit comments

Comments
 (0)