diff options
| author | Tom Christie | 2014-09-05 16:29:46 +0100 |
|---|---|---|
| committer | Tom Christie | 2014-09-05 16:29:46 +0100 |
| commit | d934824bff21e4a11226af61efba319be227f4f0 (patch) | |
| tree | b5c856cc1fdb9245bfa59db450fc2c228835e3b9 /rest_framework | |
| parent | c1036c17533a3091401ff90f825571f0e6125eca (diff) | |
| download | django-rest-framework-d934824bff21e4a11226af61efba319be227f4f0.tar.bz2 | |
Workin on
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/exceptions.py | 9 | ||||
| -rw-r--r-- | rest_framework/fields.py | 26 | ||||
| -rw-r--r-- | rest_framework/generics.py | 30 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 83 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 64 |
5 files changed, 118 insertions, 94 deletions
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index ad52d172..852a08b1 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -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 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 838aa3b0..d18551b3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -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)) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 6705cbb2..c2c59154 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -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. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 359740ce..14a6b44b 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -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() - 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) + 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) 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) + + def partial_update(self, request, *args, **kwargs): + kwargs['partial'] = True + return self.update(request, *args, **kwargs) + - serializer = self.get_serializer(self.object, data=request.DATA, partial=partial) +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) - if not serializer.is_valid(): - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - if self.object is None: +# 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) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c38d8968..49eb6ce9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -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): - 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 = {} - return True + 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.detail + else: + self._errors = {} + + 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() |
