Validation on ManyToManyField when default=None (#9790)

* Added validation on ManyToMany relations when default=None and tests
* Some clarifications in contributing.md
This commit is contained in:
Genaro Camele 2025-10-14 03:22:23 -03:00 committed by GitHub
parent c0f3649224
commit 577bb3c819
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 77 additions and 1 deletions

View File

@ -81,12 +81,45 @@ To run the tests, clone the repository, and then:
# Run the tests
./runtests.py
---
**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.
For example, with TestCase:
from django.test import TestCase
class MyDatabaseTest(TestCase):
def test_something(self):
# Your test code here
pass
Or with decorator:
import pytest
@pytest.mark.django_db()
class MyDatabaseTest:
def test_something(self):
# Your test code here
pass
You can reuse existing models defined in `tests/models.py` for your tests.
---
### Test options
Run using a more concise output style.
./runtests.py -q
If you do not want the output to be captured (for example, to see print statements directly), you can use the `-s` flag.
./runtests.py -s
Run the tests for a given test case.
./runtests.py MyTestCase
@ -99,6 +132,7 @@ Shorter form to run the tests for a given test method.
./runtests.py test_this_method
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.
### Running against multiple environments

View File

@ -1090,6 +1090,13 @@ class ModelSerializer(Serializer):
# Determine the fields that should be included on the serializer.
fields = {}
# If it's a ManyToMany field, and the default is None, then raises an exception to prevent exceptions on .set()
for field_name in declared_fields.keys():
if field_name in info.relations and info.relations[field_name].to_many and declared_fields[field_name].default is None:
raise ValueError(
f"The field '{field_name}' on serializer '{self.__class__.__name__}' is a ManyToMany field and cannot have a default value of None."
)
for field_name in field_names:
# If the field is explicitly declared on the class then use that.
if field_name in declared_fields:

View File

@ -6,12 +6,14 @@ from collections.abc import Mapping
import pytest
from django.db import models
from django.test import TestCase
from rest_framework import exceptions, fields, relations, serializers
from rest_framework.fields import Field
from .models import (
ForeignKeyTarget, NestedForeignKeySource, NullableForeignKeySource
ForeignKeyTarget, ManyToManySource, ManyToManyTarget,
NestedForeignKeySource, NullableForeignKeySource
)
from .utils import MockObject
@ -64,6 +66,7 @@ class TestSerializer:
class ExampleSerializer(serializers.Serializer):
char = serializers.CharField()
integer = serializers.IntegerField()
self.Serializer = ExampleSerializer
def test_valid_serializer(self):
@ -774,3 +777,35 @@ class TestSetValueMethod:
ret = {'a': 1}
self.s.set_value(ret, ['x', 'y'], 2)
assert ret == {'a': 1, 'x': {'y': 2}}
class TestWarningManyToMany(TestCase):
def test_warning_many_to_many(self):
"""Tests that using a PrimaryKeyRelatedField for a ManyToMany field breaks with default=None."""
class ManyToManySourceSerializer(serializers.ModelSerializer):
targets = serializers.PrimaryKeyRelatedField(
many=True,
queryset=ManyToManyTarget.objects.all(),
default=None
)
class Meta:
model = ManyToManySource
fields = '__all__'
# Instantiates serializer without 'value' field to force using the default=None for the ManyToMany relation
serializer = ManyToManySourceSerializer(data={
"name": "Invalid Example",
})
error_msg = "The field 'targets' on serializer 'ManyToManySourceSerializer' is a ManyToMany field and cannot have a default value of None."
# Calls to get_fields() should raise a ValueError
with pytest.raises(ValueError) as exc_info:
serializer.get_fields()
assert str(exc_info.value) == error_msg
# Calls to is_valid() should behave the same
with pytest.raises(ValueError) as exc_info:
serializer.is_valid(raise_exception=True)
assert str(exc_info.value) == error_msg