diff options
| author | James Rutherford | 2015-03-11 10:38:03 +0000 | 
|---|---|---|
| committer | James Rutherford | 2015-03-11 10:38:03 +0000 | 
| commit | 4a2d27975ab5249269aebafd803be87a2107092b (patch) | |
| tree | 55b524c93b02eef404304f734be98871bbb1324f /tests/test_model_serializer.py | |
| parent | 856dc855c952746f566a6a8de263afe951362dfb (diff) | |
| parent | dc56e5a0f41fdd6350e91a5749023d086bd1640f (diff) | |
| download | django-rest-framework-4a2d27975ab5249269aebafd803be87a2107092b.tar.bz2 | |
Merge pull request #1 from tomchristie/master
Merge in from upstream
Diffstat (limited to 'tests/test_model_serializer.py')
| -rw-r--r-- | tests/test_model_serializer.py | 641 | 
1 files changed, 641 insertions, 0 deletions
| diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py new file mode 100644 index 00000000..bce2008a --- /dev/null +++ b/tests/test_model_serializer.py @@ -0,0 +1,641 @@ +""" +The `ModelSerializer` and `HyperlinkedModelSerializer` classes are essentially +shortcuts for automatically creating serializers based on a given model class. + +These tests deal with ensuring that we correctly map the model fields onto +an appropriate set of serializer fields for each case. +""" +from __future__ import unicode_literals +from django.core.exceptions import ImproperlyConfigured +from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator +from django.db import models +from django.test import TestCase +from django.utils import six +from rest_framework import serializers +from rest_framework.compat import unicode_repr + + +def dedent(blocktext): +    return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]]) + + +# Tests for regular field mappings. +# --------------------------------- + +class CustomField(models.Field): +    """ +    A custom model field simply for testing purposes. +    """ +    pass + + +class OneFieldModel(models.Model): +    char_field = models.CharField(max_length=100) + + +class RegularFieldsModel(models.Model): +    """ +    A model class for testing regular flat fields. +    """ +    auto_field = models.AutoField(primary_key=True) +    big_integer_field = models.BigIntegerField() +    boolean_field = models.BooleanField(default=False) +    char_field = models.CharField(max_length=100) +    comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100) +    date_field = models.DateField() +    datetime_field = models.DateTimeField() +    decimal_field = models.DecimalField(max_digits=3, decimal_places=1) +    email_field = models.EmailField(max_length=100) +    float_field = models.FloatField() +    integer_field = models.IntegerField() +    null_boolean_field = models.NullBooleanField() +    positive_integer_field = models.PositiveIntegerField() +    positive_small_integer_field = models.PositiveSmallIntegerField() +    slug_field = models.SlugField(max_length=100) +    small_integer_field = models.SmallIntegerField() +    text_field = models.TextField() +    time_field = models.TimeField() +    url_field = models.URLField(max_length=100) +    custom_field = CustomField() + +    def method(self): +        return 'method' + + +COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')) + + +class FieldOptionsModel(models.Model): +    value_limit_field = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)]) +    length_limit_field = models.CharField(validators=[MinLengthValidator(3)], max_length=12) +    blank_field = models.CharField(blank=True, max_length=10) +    null_field = models.IntegerField(null=True) +    default_field = models.IntegerField(default=0) +    descriptive_field = models.IntegerField(help_text='Some help text', verbose_name='A label') +    choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES) + + +class TestModelSerializer(TestCase): +    def test_create_method(self): +        class TestSerializer(serializers.ModelSerializer): +            non_model_field = serializers.CharField() + +            class Meta: +                model = OneFieldModel +                fields = ('char_field', 'non_model_field') + +        serializer = TestSerializer(data={ +            'char_field': 'foo', +            'non_model_field': 'bar', +        }) +        serializer.is_valid() +        with self.assertRaises(TypeError) as excinfo: +            serializer.save() +        msginitial = 'Got a `TypeError` when calling `OneFieldModel.objects.create()`.' +        assert str(excinfo.exception).startswith(msginitial) + + +class TestRegularFieldMappings(TestCase): +    def test_regular_fields(self): +        """ +        Model fields should map to their equivelent serializer fields. +        """ +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RegularFieldsModel + +        expected = dedent(""" +            TestSerializer(): +                auto_field = IntegerField(read_only=True) +                big_integer_field = IntegerField() +                boolean_field = BooleanField(required=False) +                char_field = CharField(max_length=100) +                comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>]) +                date_field = DateField() +                datetime_field = DateTimeField() +                decimal_field = DecimalField(decimal_places=1, max_digits=3) +                email_field = EmailField(max_length=100) +                float_field = FloatField() +                integer_field = IntegerField() +                null_boolean_field = NullBooleanField(required=False) +                positive_integer_field = IntegerField() +                positive_small_integer_field = IntegerField() +                slug_field = SlugField(max_length=100) +                small_integer_field = IntegerField() +                text_field = CharField(style={'base_template': 'textarea.html'}) +                time_field = TimeField() +                url_field = URLField(max_length=100) +                custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>) +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_field_options(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = FieldOptionsModel + +        expected = dedent(""" +            TestSerializer(): +                id = IntegerField(label='ID', read_only=True) +                value_limit_field = IntegerField(max_value=10, min_value=1) +                length_limit_field = CharField(max_length=12, min_length=3) +                blank_field = CharField(allow_blank=True, max_length=10, required=False) +                null_field = IntegerField(allow_null=True, required=False) +                default_field = IntegerField(required=False) +                descriptive_field = IntegerField(help_text='Some help text', label='A label') +                choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')]) +        """) +        if six.PY2: +            # This particular case is too awkward to resolve fully across +            # both py2 and py3. +            expected = expected.replace( +                "('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')", +                "(u'red', u'Red'), (u'blue', u'Blue'), (u'green', u'Green')" +            ) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_method_field(self): +        """ +        Properties and methods on the model should be allowed as `Meta.fields` +        values, and should map to `ReadOnlyField`. +        """ +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RegularFieldsModel +                fields = ('auto_field', 'method') + +        expected = dedent(""" +            TestSerializer(): +                auto_field = IntegerField(read_only=True) +                method = ReadOnlyField() +        """) +        self.assertEqual(repr(TestSerializer()), expected) + +    def test_pk_fields(self): +        """ +        Both `pk` and the actual primary key name are valid in `Meta.fields`. +        """ +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RegularFieldsModel +                fields = ('pk', 'auto_field') + +        expected = dedent(""" +            TestSerializer(): +                pk = IntegerField(label='Auto field', read_only=True) +                auto_field = IntegerField(read_only=True) +        """) +        self.assertEqual(repr(TestSerializer()), expected) + +    def test_extra_field_kwargs(self): +        """ +        Ensure `extra_kwargs` are passed to generated fields. +        """ +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RegularFieldsModel +                fields = ('auto_field', 'char_field') +                extra_kwargs = {'char_field': {'default': 'extra'}} + +        expected = dedent(""" +            TestSerializer(): +                auto_field = IntegerField(read_only=True) +                char_field = CharField(default='extra', max_length=100) +        """) +        self.assertEqual(repr(TestSerializer()), expected) + +    def test_invalid_field(self): +        """ +        Field names that do not map to a model field or relationship should +        raise a configuration errror. +        """ +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RegularFieldsModel +                fields = ('auto_field', 'invalid') + +        with self.assertRaises(ImproperlyConfigured) as excinfo: +            TestSerializer().fields +        expected = 'Field name `invalid` is not valid for model `RegularFieldsModel`.' +        assert str(excinfo.exception) == expected + +    def test_missing_field(self): +        """ +        Fields that have been declared on the serializer class must be included +        in the `Meta.fields` if it exists. +        """ +        class TestSerializer(serializers.ModelSerializer): +            missing = serializers.ReadOnlyField() + +            class Meta: +                model = RegularFieldsModel +                fields = ('auto_field',) + +        with self.assertRaises(AssertionError) as excinfo: +            TestSerializer().fields +        expected = ( +            "The field 'missing' was declared on serializer TestSerializer, " +            "but has not been included in the 'fields' option." +        ) +        assert str(excinfo.exception) == expected + +    def test_missing_superclass_field(self): +        """ +        Fields that have been declared on a parent of the serializer class may +        be excluded from the `Meta.fields` option. +        """ +        class TestSerializer(serializers.ModelSerializer): +            missing = serializers.ReadOnlyField() + +            class Meta: +                model = RegularFieldsModel + +        class ChildSerializer(TestSerializer): +            missing = serializers.ReadOnlyField() + +            class Meta: +                model = RegularFieldsModel +                fields = ('auto_field',) + +        ChildSerializer().fields + + +# Tests for relational field mappings. +# ------------------------------------ + +class ForeignKeyTargetModel(models.Model): +    name = models.CharField(max_length=100) + + +class ManyToManyTargetModel(models.Model): +    name = models.CharField(max_length=100) + + +class OneToOneTargetModel(models.Model): +    name = models.CharField(max_length=100) + + +class ThroughTargetModel(models.Model): +    name = models.CharField(max_length=100) + + +class Supplementary(models.Model): +    extra = models.IntegerField() +    forwards = models.ForeignKey('ThroughTargetModel') +    backwards = models.ForeignKey('RelationalModel') + + +class RelationalModel(models.Model): +    foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='reverse_foreign_key') +    many_to_many = models.ManyToManyField(ManyToManyTargetModel, related_name='reverse_many_to_many') +    one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one') +    through = models.ManyToManyField(ThroughTargetModel, through=Supplementary, related_name='reverse_through') + + +class TestRelationalFieldMappings(TestCase): +    def test_pk_relations(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RelationalModel + +        expected = dedent(""" +            TestSerializer(): +                id = IntegerField(label='ID', read_only=True) +                foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all()) +                one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all(), validators=[<UniqueValidator(queryset=RelationalModel.objects.all())>]) +                many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all()) +                through = PrimaryKeyRelatedField(many=True, read_only=True) +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_nested_relations(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RelationalModel +                depth = 1 + +        expected = dedent(""" +            TestSerializer(): +                id = IntegerField(label='ID', read_only=True) +                foreign_key = NestedSerializer(read_only=True): +                    id = IntegerField(label='ID', read_only=True) +                    name = CharField(max_length=100) +                one_to_one = NestedSerializer(read_only=True): +                    id = IntegerField(label='ID', read_only=True) +                    name = CharField(max_length=100) +                many_to_many = NestedSerializer(many=True, read_only=True): +                    id = IntegerField(label='ID', read_only=True) +                    name = CharField(max_length=100) +                through = NestedSerializer(many=True, read_only=True): +                    id = IntegerField(label='ID', read_only=True) +                    name = CharField(max_length=100) +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_hyperlinked_relations(self): +        class TestSerializer(serializers.HyperlinkedModelSerializer): +            class Meta: +                model = RelationalModel + +        expected = dedent(""" +            TestSerializer(): +                url = HyperlinkedIdentityField(view_name='relationalmodel-detail') +                foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail') +                one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), validators=[<UniqueValidator(queryset=RelationalModel.objects.all())>], view_name='onetoonetargetmodel-detail') +                many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail') +                through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail') +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_nested_hyperlinked_relations(self): +        class TestSerializer(serializers.HyperlinkedModelSerializer): +            class Meta: +                model = RelationalModel +                depth = 1 + +        expected = dedent(""" +            TestSerializer(): +                url = HyperlinkedIdentityField(view_name='relationalmodel-detail') +                foreign_key = NestedSerializer(read_only=True): +                    url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail') +                    name = CharField(max_length=100) +                one_to_one = NestedSerializer(read_only=True): +                    url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail') +                    name = CharField(max_length=100) +                many_to_many = NestedSerializer(many=True, read_only=True): +                    url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail') +                    name = CharField(max_length=100) +                through = NestedSerializer(many=True, read_only=True): +                    url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail') +                    name = CharField(max_length=100) +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_pk_reverse_foreign_key(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = ForeignKeyTargetModel +                fields = ('id', 'name', 'reverse_foreign_key') + +        expected = dedent(""" +            TestSerializer(): +                id = IntegerField(label='ID', read_only=True) +                name = CharField(max_length=100) +                reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all()) +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_pk_reverse_one_to_one(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = OneToOneTargetModel +                fields = ('id', 'name', 'reverse_one_to_one') + +        expected = dedent(""" +            TestSerializer(): +                id = IntegerField(label='ID', read_only=True) +                name = CharField(max_length=100) +                reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all()) +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_pk_reverse_many_to_many(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = ManyToManyTargetModel +                fields = ('id', 'name', 'reverse_many_to_many') + +        expected = dedent(""" +            TestSerializer(): +                id = IntegerField(label='ID', read_only=True) +                name = CharField(max_length=100) +                reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all()) +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + +    def test_pk_reverse_through(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = ThroughTargetModel +                fields = ('id', 'name', 'reverse_through') + +        expected = dedent(""" +            TestSerializer(): +                id = IntegerField(label='ID', read_only=True) +                name = CharField(max_length=100) +                reverse_through = PrimaryKeyRelatedField(many=True, read_only=True) +        """) +        self.assertEqual(unicode_repr(TestSerializer()), expected) + + +class TestIntegration(TestCase): +    def setUp(self): +        self.foreign_key_target = ForeignKeyTargetModel.objects.create( +            name='foreign_key' +        ) +        self.one_to_one_target = OneToOneTargetModel.objects.create( +            name='one_to_one' +        ) +        self.many_to_many_targets = [ +            ManyToManyTargetModel.objects.create( +                name='many_to_many (%d)' % idx +            ) for idx in range(3) +        ] +        self.instance = RelationalModel.objects.create( +            foreign_key=self.foreign_key_target, +            one_to_one=self.one_to_one_target, +        ) +        self.instance.many_to_many = self.many_to_many_targets +        self.instance.save() + +    def test_pk_retrival(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RelationalModel + +        serializer = TestSerializer(self.instance) +        expected = { +            'id': self.instance.pk, +            'foreign_key': self.foreign_key_target.pk, +            'one_to_one': self.one_to_one_target.pk, +            'many_to_many': [item.pk for item in self.many_to_many_targets], +            'through': [] +        } +        self.assertEqual(serializer.data, expected) + +    def test_pk_create(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RelationalModel + +        new_foreign_key = ForeignKeyTargetModel.objects.create( +            name='foreign_key' +        ) +        new_one_to_one = OneToOneTargetModel.objects.create( +            name='one_to_one' +        ) +        new_many_to_many = [ +            ManyToManyTargetModel.objects.create( +                name='new many_to_many (%d)' % idx +            ) for idx in range(3) +        ] +        data = { +            'foreign_key': new_foreign_key.pk, +            'one_to_one': new_one_to_one.pk, +            'many_to_many': [item.pk for item in new_many_to_many], +        } + +        # Serializer should validate okay. +        serializer = TestSerializer(data=data) +        assert serializer.is_valid() + +        # Creating the instance, relationship attributes should be set. +        instance = serializer.save() +        assert instance.foreign_key.pk == new_foreign_key.pk +        assert instance.one_to_one.pk == new_one_to_one.pk +        assert [ +            item.pk for item in instance.many_to_many.all() +        ] == [ +            item.pk for item in new_many_to_many +        ] +        assert list(instance.through.all()) == [] + +        # Representation should be correct. +        expected = { +            'id': instance.pk, +            'foreign_key': new_foreign_key.pk, +            'one_to_one': new_one_to_one.pk, +            'many_to_many': [item.pk for item in new_many_to_many], +            'through': [] +        } +        self.assertEqual(serializer.data, expected) + +    def test_pk_update(self): +        class TestSerializer(serializers.ModelSerializer): +            class Meta: +                model = RelationalModel + +        new_foreign_key = ForeignKeyTargetModel.objects.create( +            name='foreign_key' +        ) +        new_one_to_one = OneToOneTargetModel.objects.create( +            name='one_to_one' +        ) +        new_many_to_many = [ +            ManyToManyTargetModel.objects.create( +                name='new many_to_many (%d)' % idx +            ) for idx in range(3) +        ] +        data = { +            'foreign_key': new_foreign_key.pk, +            'one_to_one': new_one_to_one.pk, +            'many_to_many': [item.pk for item in new_many_to_many], +        } + +        # Serializer should validate okay. +        serializer = TestSerializer(self.instance, data=data) +        assert serializer.is_valid() + +        # Creating the instance, relationship attributes should be set. +        instance = serializer.save() +        assert instance.foreign_key.pk == new_foreign_key.pk +        assert instance.one_to_one.pk == new_one_to_one.pk +        assert [ +            item.pk for item in instance.many_to_many.all() +        ] == [ +            item.pk for item in new_many_to_many +        ] +        assert list(instance.through.all()) == [] + +        # Representation should be correct. +        expected = { +            'id': self.instance.pk, +            'foreign_key': new_foreign_key.pk, +            'one_to_one': new_one_to_one.pk, +            'many_to_many': [item.pk for item in new_many_to_many], +            'through': [] +        } +        self.assertEqual(serializer.data, expected) + + +# Tests for bulk create using `ListSerializer`. + +class BulkCreateModel(models.Model): +    name = models.CharField(max_length=10) + + +class TestBulkCreate(TestCase): +    def test_bulk_create(self): +        class BasicModelSerializer(serializers.ModelSerializer): +            class Meta: +                model = BulkCreateModel +                fields = ('name',) + +        class BulkCreateSerializer(serializers.ListSerializer): +            child = BasicModelSerializer() + +        data = [{'name': 'a'}, {'name': 'b'}, {'name': 'c'}] +        serializer = BulkCreateSerializer(data=data) +        assert serializer.is_valid() + +        # Objects are returned by save(). +        instances = serializer.save() +        assert len(instances) == 3 +        assert [item.name for item in instances] == ['a', 'b', 'c'] + +        # Objects have been created in the database. +        assert BulkCreateModel.objects.count() == 3 +        assert list(BulkCreateModel.objects.values_list('name', flat=True)) == ['a', 'b', 'c'] + +        # Serializer returns correct data. +        assert serializer.data == data + + +class TestMetaClassModel(models.Model): +    text = models.CharField(max_length=100) + + +class TestSerializerMetaClass(TestCase): +    def test_meta_class_fields_option(self): +        class ExampleSerializer(serializers.ModelSerializer): +            class Meta: +                model = TestMetaClassModel +                fields = 'text' + +        with self.assertRaises(TypeError) as result: +            ExampleSerializer().fields + +        exception = result.exception +        assert str(exception).startswith( +            "The `fields` option must be a list or tuple" +        ) + +    def test_meta_class_exclude_option(self): +        class ExampleSerializer(serializers.ModelSerializer): +            class Meta: +                model = TestMetaClassModel +                exclude = 'text' + +        with self.assertRaises(TypeError) as result: +            ExampleSerializer().fields + +        exception = result.exception +        assert str(exception).startswith( +            "The `exclude` option must be a list or tuple" +        ) + +    def test_meta_class_fields_and_exclude_options(self): +        class ExampleSerializer(serializers.ModelSerializer): +            class Meta: +                model = TestMetaClassModel +                fields = ('text',) +                exclude = ('text',) + +        with self.assertRaises(AssertionError) as result: +            ExampleSerializer().fields + +        exception = result.exception +        self.assertEqual( +            str(exception), +            "Cannot set both 'fields' and 'exclude' options on serializer ExampleSerializer." +        ) | 
