diff --git a/docs/community/contributing.md b/docs/community/contributing.md index aceff45ac2..797bf72e38 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -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 diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5ca1ad55f1..290534300b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1090,6 +1090,13 @@ def get_fields(self): # 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: diff --git a/tests/test_serializer.py b/tests/test_serializer.py index cefa2ee38a..ed8a749118 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -6,12 +6,14 @@ 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 @@ def setup_method(self): class ExampleSerializer(serializers.Serializer): char = serializers.CharField() integer = serializers.IntegerField() + self.Serializer = ExampleSerializer def test_valid_serializer(self): @@ -774,3 +777,35 @@ def test_nested_key(self): 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