Skip to content
34 changes: 34 additions & 0 deletions docs/community/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
37 changes: 36 additions & 1 deletion tests/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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