mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +03:00
Workin on
This commit is contained in:
parent
c1036c1753
commit
d934824bff
|
@ -15,7 +15,7 @@ class APIException(Exception):
|
|||
Subclasses should provide `.status_code` and `.default_detail` properties.
|
||||
"""
|
||||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
default_detail = ''
|
||||
default_detail = 'A server error occured'
|
||||
|
||||
def __init__(self, detail=None):
|
||||
self.detail = detail or self.default_detail
|
||||
|
@ -29,6 +29,11 @@ class ParseError(APIException):
|
|||
default_detail = 'Malformed request.'
|
||||
|
||||
|
||||
class ValidationError(APIException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_detail = 'Invalid data in request.'
|
||||
|
||||
|
||||
class AuthenticationFailed(APIException):
|
||||
status_code = status.HTTP_401_UNAUTHORIZED
|
||||
default_detail = 'Incorrect authentication credentials.'
|
||||
|
@ -54,7 +59,7 @@ class MethodNotAllowed(APIException):
|
|||
|
||||
class NotAcceptable(APIException):
|
||||
status_code = status.HTTP_406_NOT_ACCEPTABLE
|
||||
default_detail = "Could not satisfy the request's Accept header"
|
||||
default_detail = "Could not satisfy the request Accept header"
|
||||
|
||||
def __init__(self, detail=None, available_renderers=None):
|
||||
self.detail = detail or self.default_detail
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.utils import html
|
||||
import inspect
|
||||
|
||||
|
@ -59,10 +60,6 @@ def set_value(dictionary, keys, value):
|
|||
dictionary[keys[-1]] = value
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SkipField(Exception):
|
||||
pass
|
||||
|
||||
|
@ -204,6 +201,22 @@ class Field(object):
|
|||
msg = self._MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
|
||||
raise AssertionError(msg)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
instance = super(Field, cls).__new__(cls)
|
||||
instance._args = args
|
||||
instance._kwargs = kwargs
|
||||
return instance
|
||||
|
||||
def __repr__(self):
|
||||
arg_string = ', '.join([repr(val) for val in self._args])
|
||||
kwarg_string = ', '.join([
|
||||
'%s=%s' % (key, repr(val)) for key, val in self._kwargs.items()
|
||||
])
|
||||
if arg_string and kwarg_string:
|
||||
arg_string += ', '
|
||||
class_name = self.__class__.__name__
|
||||
return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
|
||||
|
||||
|
||||
class BooleanField(Field):
|
||||
MESSAGES = {
|
||||
|
@ -308,6 +321,11 @@ class IntegerField(Field):
|
|||
'invalid_integer': 'A valid integer is required.'
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.max_value = kwargs.pop('max_value')
|
||||
self.min_value = kwargs.pop('min_value')
|
||||
super(CharField, self).__init__(**kwargs)
|
||||
|
||||
def to_native(self, data):
|
||||
try:
|
||||
data = int(str(data))
|
||||
|
|
|
@ -27,7 +27,7 @@ def strict_positive_int(integer_string, cutoff=None):
|
|||
|
||||
def get_object_or_404(queryset, *filter_args, **filter_kwargs):
|
||||
"""
|
||||
Same as Django's standard shortcut, but make sure to raise 404
|
||||
Same as Django's standard shortcut, but make sure to also raise 404
|
||||
if the filter_kwargs don't match the required types.
|
||||
"""
|
||||
try:
|
||||
|
@ -249,34 +249,6 @@ class GenericAPIView(views.APIView):
|
|||
#
|
||||
# The are not called by GenericAPIView directly,
|
||||
# but are used by the mixin methods.
|
||||
|
||||
def pre_save(self, obj):
|
||||
"""
|
||||
Placeholder method for calling before saving an object.
|
||||
|
||||
May be used to set attributes on the object that are implicit
|
||||
in either the request, or the url.
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_save(self, obj, created=False):
|
||||
"""
|
||||
Placeholder method for calling after saving an object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def pre_delete(self, obj):
|
||||
"""
|
||||
Placeholder method for calling before deleting an object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_delete(self, obj):
|
||||
"""
|
||||
Placeholder method for calling after deleting an object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def metadata(self, request):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
|
|
|
@ -19,14 +19,10 @@ class CreateModelMixin(object):
|
|||
"""
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.DATA)
|
||||
|
||||
if serializer.is_valid():
|
||||
self.object = serializer.save()
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED,
|
||||
headers=headers)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
def get_success_headers(self, data):
|
||||
try:
|
||||
|
@ -40,15 +36,12 @@ class ListModelMixin(object):
|
|||
List a queryset.
|
||||
"""
|
||||
def list(self, request, *args, **kwargs):
|
||||
self.object_list = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Switch between paginated or standard style responses
|
||||
page = self.paginate_queryset(self.object_list)
|
||||
instance = self.filter_queryset(self.get_queryset())
|
||||
page = self.paginate_queryset(instance)
|
||||
if page is not None:
|
||||
serializer = self.get_pagination_serializer(page)
|
||||
else:
|
||||
serializer = self.get_serializer(self.object_list, many=True)
|
||||
|
||||
serializer = self.get_serializer(instance, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
@ -57,8 +50,8 @@ class RetrieveModelMixin(object):
|
|||
Retrieve a model instance.
|
||||
"""
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
serializer = self.get_serializer(self.object)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
|
@ -68,22 +61,52 @@ class UpdateModelMixin(object):
|
|||
"""
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
self.object = self.get_object_or_none()
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(self.object, data=request.DATA, partial=partial)
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if self.object is None:
|
||||
class DestroyModelMixin(object):
|
||||
"""
|
||||
Destroy a model instance.
|
||||
"""
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
instance.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
# The AllowPUTAsCreateMixin was previously the default behaviour
|
||||
# for PUT requests. This has now been removed and must be *explictly*
|
||||
# included if it is the behavior that you want.
|
||||
# For more info see: ...
|
||||
|
||||
class AllowPUTAsCreateMixin(object):
|
||||
"""
|
||||
The following mixin class may be used in order to support PUT-as-create
|
||||
behavior for incoming requests.
|
||||
"""
|
||||
def update(self, request, *args, **kwargs):
|
||||
partial = kwargs.pop('partial', False)
|
||||
instance = self.get_object_or_none()
|
||||
serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
if instance is None:
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
lookup_value = self.kwargs[lookup_url_kwarg]
|
||||
extras = {self.lookup_field: lookup_value}
|
||||
self.object = serializer.save(extras=extras)
|
||||
serializer.save(extras=extras)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
self.object = serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
kwargs['partial'] = True
|
||||
|
@ -103,15 +126,3 @@ class UpdateModelMixin(object):
|
|||
# PATCH requests where the object does not exist should still
|
||||
# return a 404 response.
|
||||
raise
|
||||
|
||||
|
||||
class DestroyModelMixin(object):
|
||||
"""
|
||||
Destroy a model instance.
|
||||
"""
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
self.pre_delete(obj)
|
||||
obj.delete()
|
||||
self.post_delete(obj)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
@ -13,7 +13,8 @@ response content is handled by parsers and renderers.
|
|||
from django.db import models
|
||||
from django.utils import six
|
||||
from collections import namedtuple, OrderedDict
|
||||
from rest_framework.fields import empty, set_value, Field, SkipField, ValidationError
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import empty, set_value, Field, SkipField
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils import html
|
||||
import copy
|
||||
|
@ -34,43 +35,53 @@ FieldResult = namedtuple('FieldResult', ['field', 'value', 'error'])
|
|||
|
||||
|
||||
class BaseSerializer(Field):
|
||||
"""
|
||||
The BaseSerializer class provides a minimal class which may be used
|
||||
for writing custom serializer implementations.
|
||||
"""
|
||||
|
||||
def __init__(self, instance=None, data=None, **kwargs):
|
||||
super(BaseSerializer, self).__init__(**kwargs)
|
||||
self.instance = instance
|
||||
self._initial_data = data
|
||||
|
||||
def to_native(self, data):
|
||||
raise NotImplementedError()
|
||||
raise NotImplementedError('`to_native()` must be implemented.')
|
||||
|
||||
def to_primative(self, instance):
|
||||
raise NotImplementedError()
|
||||
raise NotImplementedError('`to_primative()` must be implemented.')
|
||||
|
||||
def update(self, instance):
|
||||
raise NotImplementedError()
|
||||
def update(self, instance, attrs):
|
||||
raise NotImplementedError('`update()` must be implemented.')
|
||||
|
||||
def create(self):
|
||||
raise NotImplementedError()
|
||||
def create(self, attrs):
|
||||
raise NotImplementedError('`create()` must be implemented.')
|
||||
|
||||
def save(self, extras=None):
|
||||
if extras is not None:
|
||||
self._validated_data.update(extras)
|
||||
self.validated_data.update(extras)
|
||||
|
||||
if self.instance is not None:
|
||||
self.update(self.instance)
|
||||
self.update(self.instance, self._validated_data)
|
||||
else:
|
||||
self.instance = self.create()
|
||||
self.instance = self.create(self._validated_data)
|
||||
|
||||
return self.instance
|
||||
|
||||
def is_valid(self):
|
||||
def is_valid(self, raise_exception=False):
|
||||
if not hasattr(self, '_validated_data'):
|
||||
try:
|
||||
self._validated_data = self.to_native(self._initial_data)
|
||||
except ValidationError as exc:
|
||||
self._validated_data = {}
|
||||
self._errors = exc.args[0]
|
||||
return False
|
||||
self._errors = exc.detail
|
||||
else:
|
||||
self._errors = {}
|
||||
return True
|
||||
|
||||
if self._errors and raise_exception:
|
||||
raise ValidationError(self._errors)
|
||||
|
||||
return not bool(self._errors)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
|
@ -184,14 +195,20 @@ class Serializer(BaseSerializer):
|
|||
"""
|
||||
Dict of native values <- Dict of primitive datatypes.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
raise ValidationError({'non_field_errors': ['Invalid data']})
|
||||
|
||||
ret = {}
|
||||
errors = {}
|
||||
fields = [field for field in self.fields.values() if not field.read_only]
|
||||
|
||||
for field in fields:
|
||||
validate_method = getattr(self, 'validate_' + field.field_name, None)
|
||||
primitive_value = field.get_value(data)
|
||||
try:
|
||||
validated_value = field.validate(primitive_value)
|
||||
if validate_method is not None:
|
||||
validated_value = validate_method(validated_value)
|
||||
except ValidationError as exc:
|
||||
errors[field.field_name] = str(exc)
|
||||
except SkipField:
|
||||
|
@ -202,6 +219,7 @@ class Serializer(BaseSerializer):
|
|||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
# TODO: 'Non field errors'
|
||||
return self.validate(ret)
|
||||
|
||||
def to_primative(self, instance):
|
||||
|
@ -340,12 +358,12 @@ class ModelSerializer(Serializer):
|
|||
self.opts = self._options_class(self.Meta)
|
||||
super(ModelSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
def create(self):
|
||||
def create(self, attrs):
|
||||
ModelClass = self.opts.model
|
||||
return ModelClass.objects.create(**self.validated_data)
|
||||
return ModelClass.objects.create(**attrs)
|
||||
|
||||
def update(self, obj):
|
||||
for attr, value in self.validated_data.items():
|
||||
def update(self, obj, attrs):
|
||||
for attr, value in attrs.items():
|
||||
setattr(obj, attr, value)
|
||||
obj.save()
|
||||
|
||||
|
|
|
@ -360,18 +360,15 @@ class TestInstanceView(TestCase):
|
|||
|
||||
def test_put_to_deleted_instance(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
if it does not currently exist.
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should return 404 if
|
||||
an object does not currently exist.
|
||||
"""
|
||||
self.objects.get(id=1).delete()
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/1', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
|
||||
updated = self.objects.get(id=1)
|
||||
self.assertEqual(updated.text, 'foobar')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_put_to_filtered_out_instance(self):
|
||||
"""
|
||||
|
@ -382,35 +379,7 @@ class TestInstanceView(TestCase):
|
|||
filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
|
||||
request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
|
||||
response = self.view(request, pk=filtered_out_pk).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_put_as_create_on_id_based_url(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
at the requested url if it doesn't exist.
|
||||
"""
|
||||
data = {'text': 'foobar'}
|
||||
# pk fields can not be created on demand, only the database can set the pk for a new object
|
||||
request = factory.put('/5', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
response = self.view(request, pk=5).render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
new_obj = self.objects.get(pk=5)
|
||||
self.assertEqual(new_obj.text, 'foobar')
|
||||
|
||||
def test_put_as_create_on_slug_based_url(self):
|
||||
"""
|
||||
PUT requests to RetrieveUpdateDestroyAPIView should create an object
|
||||
at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
|
||||
"""
|
||||
data = {'text': 'foobar'}
|
||||
request = factory.put('/test_slug', data, format='json')
|
||||
with self.assertNumQueries(2):
|
||||
response = self.slug_based_view(request, slug='test_slug').render()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data, {'slug': 'test_slug', 'text': 'foobar'})
|
||||
new_obj = SlugBasedModel.objects.get(slug='test_slug')
|
||||
self.assertEqual(new_obj.text, 'foobar')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_patch_cannot_create_an_object(self):
|
||||
"""
|
||||
|
|
|
@ -48,11 +48,10 @@ class ShouldValidateModel(models.Model):
|
|||
class ShouldValidateModelSerializer(serializers.ModelSerializer):
|
||||
renamed = serializers.CharField(source='should_validate_field', required=False)
|
||||
|
||||
def validate_renamed(self, attrs, source):
|
||||
value = attrs[source]
|
||||
def validate_renamed(self, value):
|
||||
if len(value) < 3:
|
||||
raise serializers.ValidationError('Minimum 3 characters.')
|
||||
return attrs
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
model = ShouldValidateModel
|
||||
|
|
|
@ -1,42 +1,41 @@
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
# from django.db import models
|
||||
# from django.test import TestCase
|
||||
# from rest_framework import serializers
|
||||
|
||||
|
||||
class ExampleModel(models.Model):
|
||||
email = models.EmailField(max_length=100)
|
||||
password = models.CharField(max_length=100)
|
||||
# class ExampleModel(models.Model):
|
||||
# email = models.EmailField(max_length=100)
|
||||
# password = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class WriteOnlyFieldTests(TestCase):
|
||||
def test_write_only_fields(self):
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
password = serializers.CharField(write_only=True)
|
||||
# class WriteOnlyFieldTests(TestCase):
|
||||
# def test_write_only_fields(self):
|
||||
# class ExampleSerializer(serializers.Serializer):
|
||||
# email = serializers.EmailField()
|
||||
# password = serializers.CharField(write_only=True)
|
||||
|
||||
data = {
|
||||
'email': 'foo@example.com',
|
||||
'password': '123'
|
||||
}
|
||||
serializer = ExampleSerializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.object, data)
|
||||
self.assertEquals(serializer.data, {'email': 'foo@example.com'})
|
||||
# data = {
|
||||
# 'email': 'foo@example.com',
|
||||
# 'password': '123'
|
||||
# }
|
||||
# serializer = ExampleSerializer(data=data)
|
||||
# self.assertTrue(serializer.is_valid())
|
||||
# self.assertEquals(serializer.validated_data, data)
|
||||
# self.assertEquals(serializer.data, {'email': 'foo@example.com'})
|
||||
|
||||
def test_write_only_fields_meta(self):
|
||||
class ExampleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ExampleModel
|
||||
fields = ('email', 'password')
|
||||
write_only_fields = ('password',)
|
||||
# def test_write_only_fields_meta(self):
|
||||
# class ExampleSerializer(serializers.ModelSerializer):
|
||||
# class Meta:
|
||||
# model = ExampleModel
|
||||
# fields = ('email', 'password')
|
||||
# write_only_fields = ('password',)
|
||||
|
||||
data = {
|
||||
'email': 'foo@example.com',
|
||||
'password': '123'
|
||||
}
|
||||
serializer = ExampleSerializer(data=data)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertTrue(isinstance(serializer.object, ExampleModel))
|
||||
self.assertEquals(serializer.object.email, data['email'])
|
||||
self.assertEquals(serializer.object.password, data['password'])
|
||||
self.assertEquals(serializer.data, {'email': 'foo@example.com'})
|
||||
# data = {
|
||||
# 'email': 'foo@example.com',
|
||||
# 'password': '123'
|
||||
# }
|
||||
# serializer = ExampleSerializer(data=data)
|
||||
# self.assertTrue(serializer.is_valid())
|
||||
# self.assertTrue(isinstance(serializer.object, ExampleModel))
|
||||
# self.assertEquals(serializer.validated_data, data)
|
||||
# self.assertEquals(serializer.data, {'email': 'foo@example.com'})
|
||||
|
|
Loading…
Reference in New Issue
Block a user