From 87734be5f41de921ac32ad1f6664db243aab6d07 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Sep 2014 12:17:21 +0100 Subject: [PATCH] Configuration correctness tests on ModelSerializer --- rest_framework/serializers.py | 30 ++++++- ...d_mappings.py => test_model_serializer.py} | 85 +++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) rename tests/{test_model_field_mappings.py => test_model_serializer.py} (77%) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 99dcc349f5..9f3e53fd05 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -10,7 +10,7 @@ 2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ -from django.core.exceptions import ValidationError +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db import models from django.utils import six from django.utils.datastructures import SortedDict @@ -358,6 +358,7 @@ def _get_base_fields(self): model = getattr(self.Meta, 'model') fields = getattr(self.Meta, 'fields', None) depth = getattr(self.Meta, 'depth', 0) + extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) @@ -405,9 +406,32 @@ def _get_base_fields(self): if not issubclass(field_cls, HyperlinkedRelatedField): kwargs.pop('view_name', None) - else: - assert False, 'Field name `%s` is not valid.' % field_name + elif hasattr(model, field_name): + # Create a read only field for model methods and properties. + field_cls = ReadOnlyField + kwargs = {} + else: + raise ImproperlyConfigured( + 'Field name `%s` is not valid for model `%s`.' % + (field_name, model.__class__.__name__) + ) + + # Check that any fields declared on the class are + # also explicity included in `Meta.fields`. + missing_fields = set(declared_fields.keys()) - set(fields) + if missing_fields: + missing_field = list(missing_fields)[0] + raise ImproperlyConfigured( + 'Field `%s` has been declared on serializer `%s`, but ' + 'is missing from `Meta.fields`.' % + (missing_field, self.__class__.__name__) + ) + + # Populate any kwargs defined in `Meta.extra_kwargs` + kwargs.update(extra_kwargs.get(field_name, {})) + + # Create the serializer field. ret[field_name] = field_cls(**kwargs) return ret diff --git a/tests/test_model_field_mappings.py b/tests/test_model_serializer.py similarity index 77% rename from tests/test_model_field_mappings.py rename to tests/test_model_serializer.py index 6daa574e52..63e3645289 100644 --- a/tests/test_model_field_mappings.py +++ b/tests/test_model_serializer.py @@ -5,6 +5,7 @@ These tests deal with ensuring that we correctly map the model fields onto an appropriate set of serializer fields for each case. """ +from django.core.exceptions import ImproperlyConfigured from django.db import models from django.test import TestCase from rest_framework import serializers @@ -37,6 +38,9 @@ class RegularFieldsModel(models.Model): time_field = models.TimeField() url_field = models.URLField(max_length=100) + def method(self): + return 'method' + class TestRegularFieldMappings(TestCase): def test_regular_fields(self): @@ -69,6 +73,87 @@ class Meta: self.assertEqual(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 = ('pk', 'char_field') + extra_kwargs = {'char_field': {'default': 'extra'}} + + expected = dedent(""" + TestSerializer(): + pk = IntegerField(label='Auto field', 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() + expected = 'Field name `invalid` is not valid for model `ModelBase`.' + assert str(excinfo.exception) == expected + + def test_missing_field(self): + class TestSerializer(serializers.ModelSerializer): + missing = serializers.ReadOnlyField() + + class Meta: + model = RegularFieldsModel + fields = ('auto_field',) + + with self.assertRaises(ImproperlyConfigured) as excinfo: + TestSerializer() + expected = ( + 'Field `missing` has been declared on serializer ' + '`TestSerializer`, but is missing from `Meta.fields`.' + ) + assert str(excinfo.exception) == expected + # Testing relational field mappings