From de5b071d677074ab3b6b33a843c4b05ba2052a6b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Mon, 19 Nov 2012 17:22:17 +0000 Subject: [PATCH 01/13] Add SerializerMethodField --- docs/api-guide/fields.md | 6 ++++++ rest_framework/fields.py | 14 ++++++++++++ rest_framework/tests/serializer.py | 34 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index d1c31ecc7..b19c324ad 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -324,5 +324,11 @@ This field is always read-only. * `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. * `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. +# Other Fields + +## SerializerMethodField + +This is a read-only field gets its value by calling a method on the serializer class it's attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. + [cite]: http://www.python.org/dev/peps/pep-0020/ [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c68c39b59..d1e9c45d8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1019,3 +1019,17 @@ class ImageField(FileField): if hasattr(f, 'seek') and callable(f.seek): f.seek(0) return f + + +class SerializerMethodField(Field): + """ + A field that gets its value by calling a method on the serializer it's attached to. + """ + + def __init__(self, method_name): + self.method_name = method_name + super(SerializerMethodField, self).__init__() + + def field_to_native(self, obj, field_name): + value = getattr(self.parent, self.method_name)(obj) + return self.to_native(value) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index fb1be7eb0..cc6e9d5cf 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -497,6 +497,40 @@ class ManyRelatedTests(TestCase): self.assertEqual(serializer.data, expected) +class SerializerMethodFieldTests(TestCase): + def setUp(self): + + class BoopSerializer(serializers.Serializer): + beep = serializers.SerializerMethodField('get_beep') + boop = serializers.Field() + boop_count = serializers.SerializerMethodField('get_boop_count') + + def get_beep(self, obj): + return 'hello!' + + def get_boop_count(self, obj): + return len(obj.boop) + + self.serializer_class = BoopSerializer + + def test_serializer_method_field(self): + + class MyModel(object): + boop = ['a', 'b', 'c'] + + source_data = MyModel() + + serializer = self.serializer_class(source_data) + + expected = { + 'beep': u'hello!', + 'boop': [u'a', u'b', u'c'], + 'boop_count': 3, + } + + self.assertEqual(serializer.data, expected) + + # Test for issue #324 class BlankFieldTests(TestCase): def setUp(self): From 3ab8c4966d065e930bd6e8bc6c26934ae5c5918c Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Mon, 19 Nov 2012 17:24:08 +0000 Subject: [PATCH 02/13] Tweaks to SerializerMethodField docs --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b19c324ad..ebfb5d47d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -328,7 +328,7 @@ This field is always read-only. ## SerializerMethodField -This is a read-only field gets its value by calling a method on the serializer class it's attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. +This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. [cite]: http://www.python.org/dev/peps/pep-0020/ [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS From 2cf0fda2ae5cf596946df77675ce10d68587a8bd Mon Sep 17 00:00:00 2001 From: "jedavis83@gmail.com" Date: Mon, 19 Nov 2012 22:09:40 -0800 Subject: [PATCH 03/13] Cache default fields per serializer instance for improved performance --- rest_framework/serializers.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 397866a76..46ffc0494 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -103,6 +103,7 @@ class BaseSerializer(Field): self.init_data = data self.init_files = files self.object = instance + self.default_fields = self.get_default_fields() self._data = None self._files = None @@ -111,18 +112,18 @@ class BaseSerializer(Field): ##### # Methods to determine which fields to use when (de)serializing objects. - def default_fields(self, nested=False): + def get_default_fields(self): """ Return the complete set of default fields for the object, as a dict. """ return {} - def get_fields(self, nested=False): + def get_fields(self): """ Returns the complete set of fields for the object as a dict. This will be the set of any explicitly declared fields, - plus the set of fields returned by default_fields(). + plus the set of fields returned by get_default_fields(). """ ret = SortedDict() @@ -133,8 +134,7 @@ class BaseSerializer(Field): field.initialize(parent=self, field_name=key) # Add in the default fields - fields = self.default_fields(nested) - for key, val in fields.items(): + for key, val in self.default_fields.items(): if key not in ret: ret[key] = val @@ -181,7 +181,7 @@ class BaseSerializer(Field): ret = self._dict_class() ret.fields = {} - fields = self.get_fields(nested=bool(self.opts.depth)) + fields = self.get_fields() for field_name, field in fields.items(): key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) @@ -194,7 +194,7 @@ class BaseSerializer(Field): Core of deserialization, together with `restore_object`. Converts a dictionary of data into a dictionary of deserialized fields. """ - fields = self.get_fields(nested=bool(self.opts.depth)) + fields = self.get_fields() reverted_data = {} for field_name, field in fields.items(): try: @@ -209,7 +209,7 @@ class BaseSerializer(Field): Run `validate_()` and `validate()` methods on the serializer """ # TODO: refactor this so we're not determining the fields again - fields = self.get_fields(nested=bool(self.opts.depth)) + fields = self.get_fields() for field_name, field in fields.items(): try: @@ -332,16 +332,10 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions - def default_fields(self, nested=False): + def get_default_fields(self): """ Return all the fields that should be serialized for the model. """ - # TODO: Modify this so that it's called on init, and drop - # serialize/obj/data arguments. - # - # We *could* provide a hook for dynamic fields, but - # it'd be nice if the default was to generate fields statically - # at the point of __init__ cls = self.opts.model opts = get_concrete_model(cls)._meta @@ -353,6 +347,7 @@ class ModelSerializer(Serializer): fields += [field for field in opts.many_to_many if field.serialize] ret = SortedDict() + nested = bool(self.opts.depth) is_pk = True # First field in the list is the pk for model_field in fields: From 5f4c385a86b877217c1e1bc2eaff58206eabb747 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Tue, 20 Nov 2012 13:25:21 +0000 Subject: [PATCH 04/13] Add example use of SerializerMethodField to docs --- docs/api-guide/fields.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index ebfb5d47d..914d08618 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -328,7 +328,21 @@ This field is always read-only. ## SerializerMethodField -This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. +This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: + + from rest_framework import serializers + from django.contrib.auth.models import User + from django.utils.timezone import now + + class UserSerializer(serializers.ModelSerializer): + + days_since_joined = serializers.SerializerMethodField('get_days_since_joined') + + class Meta: + model = User + + def get_days_since_joined(self, obj): + return (now() - obj.date_joined).days [cite]: http://www.python.org/dev/peps/pep-0020/ [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS From 86484668f689864aa54e127a8107bdee55240cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 20 Nov 2012 15:38:50 +0100 Subject: [PATCH 05/13] added RegexField --- docs/api-guide/fields.md | 10 ++++++++++ docs/topics/release-notes.md | 1 + rest_framework/fields.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0485b158f..cb30a52e8 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -141,6 +141,16 @@ A text representation, validates the text to be a valid e-mail address. Corresponds to `django.db.models.fields.EmailField` +## RegexField + +A text representation, that validates the given value matches against a certain regular expression. + +Uses Django's `django.core.validators.RegexValidator` for validation. + +Corresponds to `django.forms.fields.RegexField` + +**Signature:** `RegexField(regex, max_length=None, min_length=None)` + ## DateField A date representation. diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 35e8a8b35..e4e676358 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,6 +7,7 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. +* Added `RegexField`. ## 2.1.2 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4c2064261..071746de3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,6 +1,7 @@ import copy import datetime import inspect +import re import warnings from django.core import validators @@ -768,6 +769,34 @@ class EmailField(CharField): return result +class RegexField(CharField): + type_name = 'RegexField' + + def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs): + super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) + self.regex = regex + + def _get_regex(self): + return self._regex + + def _set_regex(self, regex): + if isinstance(regex, basestring): + regex = re.compile(regex) + self._regex = regex + if hasattr(self, '_regex_validator') and self._regex_validator in self.validators: + self.validators.remove(self._regex_validator) + self._regex_validator = validators.RegexValidator(regex=regex) + self.validators.append(self._regex_validator) + + regex = property(_get_regex, _set_regex) + + def __deepcopy__(self, memo): + result = copy.copy(self) + memo[id(self)] = result + result.validators = self.validators[:] + return result + + class DateField(WritableField): type_name = 'DateField' From 3227a357cec2475b8295a67e9fd66f644ea5b0cd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 20 Nov 2012 23:19:11 +0000 Subject: [PATCH 06/13] Added @irrelative for the mighty fine work. --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index bd9e4f487..955870d25 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -63,6 +63,7 @@ The following people have helped make REST framework great. * Rob Romano - [robromano] * Eugene Mechanism - [mechanism] * Jonas Liljestrand - [jonlil] +* Justin Davis - [irrelative] Many thanks to everyone who's contributed to the project. @@ -161,3 +162,4 @@ To contact the author directly: [robromano]: https://github.com/robromano [mechanism]: https://github.com/mechanism [jonlil]: https://github.com/jonlil +[irrelative]: https://github.com/irrelative From 3268c67343f6fc6364a0127a7bfabeb907a4751d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 20 Nov 2012 23:33:56 +0000 Subject: [PATCH 07/13] Update docs/topics/release-notes.md --- docs/topics/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index ec83387f5..0b8a7a8f6 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,6 +6,8 @@ ## Master +* Added `SerializerMethodField` +* Serializer performance improvements. * Added `obtain_token_view` to get tokens when using `TokenAuthentication` * Bugfix: Django 1.5 configurable user support for `TokenAuthentication` From ed713d0354b67bdc64de9346b9a72e1adfced76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 21 Nov 2012 11:07:08 +0100 Subject: [PATCH 08/13] added tests --- rest_framework/tests/models.py | 4 ++++ rest_framework/tests/serializer.py | 28 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index cbdc765c8..f6e5333b1 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -154,3 +154,7 @@ class BlankFieldModel(RESTFrameworkModel): # Model for issue #380 class OptionalRelationModel(RESTFrameworkModel): other = models.ForeignKey('OptionalRelationModel', blank=True, null=True) + +# Model for RegexField +class Book(RESTFrameworkModel): + isbn = models.CharField(max_length=13) \ No newline at end of file diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 059593a90..ad100e539 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -2,7 +2,7 @@ import datetime from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (ActionItem, Anchor, BasicModel, - BlankFieldModel, BlogPost, CallableDefaultValueModel, DefaultValueModel, + BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel) @@ -40,6 +40,13 @@ class CommentSerializer(serializers.Serializer): return instance +class BookSerializer(serializers.ModelSerializer): + isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'}) + + class Meta: + model = Book + + class ActionItemSerializer(serializers.ModelSerializer): class Meta: model = ActionItem @@ -240,6 +247,25 @@ class ValidationTests(TestCase): self.assertEquals(serializer.errors, {}) +class RegexValidationTest(TestCase): + def test_create_failed(self): + serializer = BookSerializer(data={'isbn': '1234567890'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + + serializer = BookSerializer(data={'isbn': '12345678901234'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + + serializer = BookSerializer(data={'isbn': 'abcdefghijklm'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + + def test_create_success(self): + serializer = BookSerializer(data={'isbn': '1234567890123'}) + self.assertTrue(serializer.is_valid()) + + class MetadataTests(TestCase): def test_empty(self): serializer = CommentSerializer() From 03100168ff96dd4a09ee7c8a5a63b294abe99dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 21 Nov 2012 11:57:00 +0100 Subject: [PATCH 09/13] added missing line --- rest_framework/tests/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 70523fc0d..c35861c6c 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -166,6 +166,7 @@ class BlankFieldModel(RESTFrameworkModel): class OptionalRelationModel(RESTFrameworkModel): other = models.ForeignKey('OptionalRelationModel', blank=True, null=True) + # Model for RegexField class Book(RESTFrameworkModel): isbn = models.CharField(max_length=13) From 9459289d7d388074045b726225cb6e140f3c18c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 21 Nov 2012 13:35:20 +0100 Subject: [PATCH 10/13] updated comparison due to pep8 programming recommendations http://www.python.org/dev/peps/pep-0008/#programming-recommendations --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index a95891449..048c12006 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -34,7 +34,7 @@ Declaring a serializer looks very similar to declaring a form: created = serializers.DateTimeField() def restore_object(self, attrs, instance=None): - if instance: + if instance is not None: instance.title = attrs['title'] instance.content = attrs['content'] instance.created = attrs['created'] From 834f31ae4d77378f8a56b8647564b50c56bcabb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 21 Nov 2012 14:58:04 +0100 Subject: [PATCH 11/13] added RegexField to field_mapping in BrowsableAPIRenderer --- rest_framework/renderers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index db1bce39d..27340745a 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -312,6 +312,7 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.DateTimeField: forms.DateTimeField, serializers.DateField: forms.DateField, serializers.EmailField: forms.EmailField, + serializers.RegexField: forms.RegexField, serializers.CharField: forms.CharField, serializers.ChoiceField: forms.ChoiceField, serializers.BooleanField: forms.BooleanField, From 774d687a311813a45ac9b2d3e1570c8bbca092fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 21 Nov 2012 14:58:33 +0100 Subject: [PATCH 12/13] updated comparison due to pep8 programming recommendations http://www.python.org/dev/peps/pep-0008/#programming-recommendations --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f7918c4c3..9f4964fae 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -460,7 +460,7 @@ class ModelSerializer(Serializer): """ self.m2m_data = {} - if instance: + if instance is not None: for key, val in attrs.items(): setattr(instance, key, val) return instance From b0bad35ef0972ec26ff808d81b1f43f16683898d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Nov 2012 17:32:20 +0000 Subject: [PATCH 13/13] Tweak to work with serializer performance improvement --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 27340745a..550963cb2 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -327,7 +327,7 @@ class BrowsableAPIRenderer(BaseRenderer): } fields = {} - for k, v in serializer.get_fields(True).items(): + for k, v in serializer.get_fields().items(): if getattr(v, 'read_only', True): continue