aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMark Aaron Shirley2012-11-21 09:37:22 -0800
committerMark Aaron Shirley2012-11-21 09:37:22 -0800
commit0876bed96304c3c2125e0de67736d40bfe921cf7 (patch)
tree4ba4c0cceea3ca001665560619313aed1c9624b8
parent1adfc41dc7c7e50edbf72f87ebf62bae33eb212c (diff)
parentb0bad35ef0972ec26ff808d81b1f43f16683898d (diff)
downloaddjango-rest-framework-0876bed96304c3c2125e0de67736d40bfe921cf7.tar.bz2
Merge remote-tracking branch 'upstream/master' into partial-update
-rw-r--r--docs/api-guide/fields.md30
-rw-r--r--docs/api-guide/serializers.md2
-rw-r--r--docs/topics/credits.md2
-rw-r--r--docs/topics/release-notes.md7
-rw-r--r--rest_framework/fields.py43
-rw-r--r--rest_framework/renderers.py3
-rw-r--r--rest_framework/serializers.py27
-rw-r--r--rest_framework/tests/models.py5
-rw-r--r--rest_framework/tests/serializer.py62
9 files changed, 160 insertions, 21 deletions
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index d1c31ecc..1d4c34cb 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -153,6 +153,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.
@@ -324,5 +334,25 @@ 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. 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
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 624c4159..19efde3c 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']
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index bd9e4f48..955870d2 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
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index ec83387f..c641a1b3 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -6,8 +6,11 @@
## Master
-* Added `obtain_token_view` to get tokens when using `TokenAuthentication`
-* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`
+* Added `RegexField`.
+* Added `SerializerMethodField`.
+* Serializer performance improvements.
+* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
+* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
## 2.1.3
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index b61dcb42..7b7b7148 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 io import BytesIO
@@ -804,6 +805,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'
@@ -1032,3 +1061,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/renderers.py b/rest_framework/renderers.py
index db1bce39..550963cb 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,
@@ -326,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
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 229c1b2c..53dcec16 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -104,6 +104,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
@@ -112,18 +113,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()
@@ -134,8 +135,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
@@ -182,7 +182,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)
@@ -195,7 +195,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:
@@ -210,7 +210,7 @@ class BaseSerializer(Field):
Run `validate_<fieldname>()` 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:
@@ -336,16 +336,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
@@ -357,6 +351,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:
@@ -466,7 +461,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
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 3704cda7..c35861c6 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -165,3 +165,8 @@ 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)
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 882f769c..61a05da1 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:
@@ -270,6 +277,25 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']})
+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()
@@ -534,6 +560,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):