From e8126c3a9165cdc904660f4dc0ace4d2cba86ddf Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 14 Dec 2011 10:48:19 +0000 Subject: [PATCH] Getting more resourceful - read, create, update and delete methods on ModelMixin --- djangorestframework/mixins.py | 208 +++++++++++++--------------- djangorestframework/resources.py | 187 +++++++++++++------------ djangorestframework/tests/mixins.py | 10 +- djangorestframework/views.py | 57 ++++++-- 4 files changed, 247 insertions(+), 215 deletions(-) diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index b1a634a07..93a094e6f 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -9,9 +9,8 @@ from django.db.models.fields.related import ForeignKey from django.http import HttpResponse from djangorestframework import status -from djangorestframework.renderers import BaseRenderer from djangorestframework.resources import Resource, FormResource, ModelResource -from djangorestframework.response import Response, ErrorResponse +from djangorestframework.response import ErrorResponse from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence @@ -27,11 +26,7 @@ __all__ = ( # Reverse URL lookup behavior 'InstanceMixin', # Model behavior mixins - 'ReadModelMixin', - 'CreateModelMixin', - 'UpdateModelMixin', - 'DeleteModelMixin', - 'ListModelMixin' + 'ModelMixin', ) @@ -267,25 +262,32 @@ class ResponseMixin(object): def _determine_renderer(self, request): """ - Determines the appropriate renderer for the output, given the client's 'Accept' header, - and the :attr:`renderers` set on this class. + Determines the appropriate renderer for the output, given the client's + 'Accept' header, and the :attr:`renderers` set on this class. Returns a 2-tuple of `(renderer, media_type)` - See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + See: RFC 2616, Section 14 + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html """ - if self._ACCEPT_QUERY_PARAM and request.GET.get(self._ACCEPT_QUERY_PARAM, None): + if (self._ACCEPT_QUERY_PARAM and + request.GET.get(self._ACCEPT_QUERY_PARAM, None)): # Use _accept parameter override accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)] + elif (self._IGNORE_IE_ACCEPT_HEADER and - request.META.has_key('HTTP_USER_AGENT') and + 'HTTP_USER_AGENT' in request.META and MSIE_USER_AGENT_REGEX.match(request.META['HTTP_USER_AGENT'])): - # Ignore MSIE's broken accept behavior and do something sensible instead + # Ignore MSIE's broken accept behavior and do something sensible + # instead. accept_list = ['text/html', '*/*'] - elif request.META.has_key('HTTP_ACCEPT'): + + elif 'HTTP_USER_AGENT' in request.META: # Use standard HTTP Accept negotiation - accept_list = [token.strip() for token in request.META["HTTP_ACCEPT"].split(',')] + accept_list = [token.strip() for token in + request.META["HTTP_ACCEPT"].split(',')] + else: # No accept header specified accept_list = ['*/*'] @@ -481,48 +483,86 @@ class InstanceMixin(object): ########## Model Mixins ########## -class ReadModelMixin(object): - """ - Behavior to read a `model` instance on GET requests - """ - def get(self, request, *args, **kwargs): - model = self.resource.model +class ModelMixin(object): + def get_model(self): + """ + Return the model class for this view. + """ + return getattr(self, 'model', self.resource.model) + + def get_queryset(self): + """ + Return the queryset that should be used when retrieving or listing + instances. + """ + return getattr(self, 'queryset', + getattr(self.resource, 'queryset', + self._get_model().objects.all())) + + def get_ordering(self): + """ + Return the ordering that should be used when listing instances. + """ + return getattr(self, 'ordering', + getattr(self.resource, 'ordering', + None)) + + def get_instance(self, *args, **kwargs): + """ + Return a model instance or None. + """ + model = self.get_model() + queryset = self.get_queryset() + kwargs = self._filter_kwargs(kwargs) try: + # If we have any positional args then assume the last + # represents the primary key. Otherwise assume the named kwargs + # uniquely identify the instance. if args: - # If we have any none kwargs then assume the last represents the primrary key - self.model_instance = model.objects.get(pk=args[-1], **kwargs) + return queryset.get(pk=args[-1], **kwargs) else: - # Otherwise assume the kwargs uniquely identify the model - filtered_keywords = kwargs.copy() - if BaseRenderer._FORMAT_QUERY_PARAM in filtered_keywords: - del filtered_keywords[BaseRenderer._FORMAT_QUERY_PARAM] - self.model_instance = model.objects.get(**filtered_keywords) + return queryset.get(**kwargs) except model.DoesNotExist: - raise ErrorResponse(status.HTTP_404_NOT_FOUND) + return None - return self.model_instance + def read(self, request, *args, **kwargs): + instance = self.get_instance(*args, **kwargs) + return instance + def update(self, request, *args, **kwargs): + """ + Return a model instance. + """ + instance = self.get_instance(*args, **kwargs) -class CreateModelMixin(object): - """ - Behavior to create a `model` instance on POST requests - """ - def post(self, request, *args, **kwargs): - model = self.resource.model + if instance: + for (key, val) in self.CONTENT.items(): + setattr(instance, key, val) + else: + instance = self.get_model()(**self.CONTENT) + + instance.save() + return instance + + def create(self, request, *args, **kwargs): + """ + Return a model instance. + """ + model = self._get_model() # Copy the dict to keep self.CONTENT intact content = dict(self.CONTENT) m2m_data = {} for field in model._meta.fields: - if isinstance(field, ForeignKey) and kwargs.has_key(field.name): + if isinstance(field, ForeignKey) and field.name in kwargs: # translate 'related_field' kwargs into 'related_field_id' kwargs[field.name + '_id'] = kwargs[field.name] del kwargs[field.name] for field in model._meta.many_to_many: - if content.has_key(field.name): + if field.name in content: m2m_data[field.name] = ( field.m2m_reverse_field_name(), content[field.name] ) @@ -549,90 +589,30 @@ class CreateModelMixin(object): data[m2m_data[fieldname][0]] = related_item manager.through(**data).save() - headers = {} - if hasattr(instance, 'get_absolute_url'): - headers['Location'] = self.resource(self).url(instance) - return Response(status.HTTP_201_CREATED, instance, headers) + return instance -class UpdateModelMixin(object): - """ - Behavior to update a `model` instance on PUT requests - """ - def put(self, request, *args, **kwargs): - model = self.resource.model + def destroy(self, request, *args, **kwargs): + """ + Return a model instance or None. + """ + instance = self.get_instance(*args, **kwargs) - # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url - try: - if args: - # If we have any none kwargs then assume the last represents the primary key - self.model_instance = model.objects.get(pk=args[-1], **kwargs) - else: - # Otherwise assume the kwargs uniquely identify the model - self.model_instance = model.objects.get(**kwargs) + if instance: + instance.delete() - for (key, val) in self.CONTENT.items(): - setattr(self.model_instance, key, val) - except model.DoesNotExist: - self.model_instance = model(**self.CONTENT) - self.model_instance.save() - - self.model_instance.save() - return self.model_instance + return instance -class DeleteModelMixin(object): - """ - Behavior to delete a `model` instance on DELETE requests - """ - def delete(self, request, *args, **kwargs): - model = self.resource.model - - try: - if args: - # If we have any none kwargs then assume the last represents the primrary key - instance = model.objects.get(pk=args[-1], **kwargs) - else: - # Otherwise assume the kwargs uniquely identify the model - instance = model.objects.get(**kwargs) - except model.DoesNotExist: - raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {}) - - instance.delete() - return - - -class ListModelMixin(object): - """ - Behavior to list a set of `model` instances on GET requests - """ - - # NB. Not obvious to me if it would be better to set this on the resource? - # - # Presumably it's more useful to have on the view, because that way you can - # have multiple views across different querysets mapping to the same resource. - # - # Perhaps it ought to be: - # - # 1) View.queryset - # 2) if None fall back to Resource.queryset - # 3) if None fall back to Resource.model.objects.all() - # - # Any feedback welcomed. - queryset = None - - def get(self, request, *args, **kwargs): - model = self.resource.model - - queryset = self.queryset if self.queryset is not None else model.objects.all() - - if hasattr(self, 'resource'): - ordering = getattr(self.resource, 'ordering', None) - else: - ordering = None + def list(self, request, *args, **kwargs): + """ + Return a list of instances. + """ + queryset = self.get_queryset() + ordering = self.get_ordering() if ordering: - args = as_tuple(ordering) + assert(hasattr(ordering, '__iter__')) queryset = queryset.order_by(*args) return queryset.filter(**kwargs) diff --git a/djangorestframework/resources.py b/djangorestframework/resources.py index 5770d07f9..dc11c31f8 100644 --- a/djangorestframework/resources.py +++ b/djangorestframework/resources.py @@ -1,24 +1,16 @@ from django import forms from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch from django.db import models -from django.db.models.query import QuerySet -from django.db.models.fields.related import RelatedField -from django.utils.encoding import smart_unicode from djangorestframework.response import ErrorResponse from djangorestframework.serializer import Serializer, _SkipField from djangorestframework.utils import as_tuple -import decimal -import inspect -import re - - - class BaseResource(Serializer): """ - Base class for all Resource classes, which simply defines the interface they provide. + Base class for all Resource classes, which simply defines the interface + they provide. """ fields = None include = None @@ -31,10 +23,11 @@ class BaseResource(Serializer): def validate_request(self, data, files=None): """ Given the request content return the cleaned, validated content. - Typically raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure. + Typically raises a :exc:`response.ErrorResponse` with status code 400 + (Bad Request) on failure. """ return data - + def filter_response(self, obj): """ Given the response content, filter it into a serializable object. @@ -45,18 +38,20 @@ class BaseResource(Serializer): class Resource(BaseResource): """ A Resource determines how a python object maps to some serializable data. - Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets. + Objects that a resource can act on include plain Python object instances, + Django Models, and Django QuerySets. """ - - # The model attribute refers to the Django Model which this Resource maps to. - # (The Model's class, rather than an instance of the Model) + + # The model attribute refers to the Django Model which this Resource maps + # to. (The Model's class, rather than an instance of the Model) model = None - + # By default the set of returned fields will be the set of: # # 0. All the fields on the model, excluding 'id'. # 1. All the properties on the model. - # 2. The absolute_url of the model, if a get_absolute_url method exists for the model. + # 2. The absolute_url of the model, if a get_absolute_url method exists for + # the model. # # If you wish to override this behaviour, # you should explicitly set the fields attribute on your class. @@ -66,60 +61,68 @@ class Resource(BaseResource): class FormResource(Resource): """ Resource class that uses forms for validation. - Also provides a :meth:`get_bound_form` method which may be used by some renderers. + Also provides a :meth:`get_bound_form` method which may be used by some + renderers. - On calling :meth:`validate_request` this validator may set a :attr:`bound_form_instance` attribute on the - view, which may be used by some renderers. + On calling :meth:`validate_request` this validator may set a + :attr:`bound_form_instance` attribute on the view, which may be used by + some renderers. """ form = None """ The :class:`Form` class that should be used for request validation. - This can be overridden by a :attr:`form` attribute on the :class:`views.View`. + This can be overridden by a :attr:`form` attribute on the + :class:`views.View`. """ - def validate_request(self, data, files=None): """ Given some content as input return some cleaned, validated content. - Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure. - - Validation is standard form validation, with an additional constraint that *no extra unknown fields* may be supplied. + Raises a :exc:`response.ErrorResponse` with status code 400 + # (Bad Request) on failure. - On failure the :exc:`response.ErrorResponse` content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys. - If the :obj:`'errors'` key exists it is a list of strings of non-field errors. - If the :obj:`'field-errors'` key exists it is a dict of ``{'field name as string': ['errors as strings', ...]}``. + Validation is standard form validation, with an additional constraint + that *no extra unknown fields* may be supplied. + + On failure the :exc:`response.ErrorResponse` content is a dict which + may contain :obj:`'errors'` and :obj:`'field-errors'` keys. + If the :obj:`'errors'` key exists it is a list of strings of non-field + errors. + If the :obj:`'field-errors'` key exists it is a dict of + ``{'field name as string': ['errors as strings', ...]}``. """ return self._validate(data, files) - def _validate(self, data, files, allowed_extra_fields=(), fake_data=None): """ - Wrapped by validate to hide the extra flags that are used in the implementation. + Wrapped by validate to hide the extra flags that are used in the + implementation. - allowed_extra_fields is a list of fields which are not defined by the form, but which we still - expect to see on the input. - - fake_data is a string that should be used as an extra key, as a kludge to force .errors - to be populated when an empty dict is supplied in `data` + allowed_extra_fields is a list of fields which are not defined by the + form, but which we still expect to see on the input. + + fake_data is a string that should be used as an extra key, as a kludge + to force `.errors` to be populated when an empty dict is supplied in + `data` """ - + # We'd like nice error messages even if no content is supplied. # Typically if an empty dict is given to a form Django will # return .is_valid() == False, but .errors == {} # - # To get around this case we revalidate with some fake data. + # To get around this case we revalidate with some fake data. if fake_data: data[fake_data] = '_fake_data' allowed_extra_fields = tuple(allowed_extra_fields) + ('_fake_data',) - + bound_form = self.get_bound_form(data, files) if bound_form is None: return data - + self.view.bound_form_instance = bound_form - + data = data and data or {} files = files and files or {} @@ -127,10 +130,11 @@ class FormResource(Resource): form_fields_set = set(bound_form.fields.keys()) allowed_extra_fields_set = set(allowed_extra_fields) - # In addition to regular validation we also ensure no additional fields are being passed in... + # In addition to regular validation we also ensure no additional fields + # are being passed in... unknown_fields = seen_fields_set - (form_fields_set | allowed_extra_fields_set) unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh. - + # Check using both regular validation, and our stricter no additional fields rule if bound_form.is_valid() and not unknown_fields: # Validation succeeded... @@ -155,7 +159,7 @@ class FormResource(Resource): # If we've already set fake_dict and we're still here, fallback gracefully. detail = {u'errors': [u'No content was supplied.']} - else: + else: # Add any non-field errors if bound_form.non_field_errors(): detail[u'errors'] = bound_form.non_field_errors() @@ -171,14 +175,13 @@ class FormResource(Resource): # Add any unknown field errors for key in unknown_fields: field_errors[key] = [u'This field does not exist.'] - + if field_errors: detail[u'field-errors'] = field_errors # Return HTTP 400 response (BAD REQUEST) raise ErrorResponse(400, detail) - def get_form_class(self, method=None): """ Returns the form class used to validate this resource. @@ -199,7 +202,6 @@ class FormResource(Resource): form = getattr(self.view, '%s_form' % method.lower(), form) return form - def get_bound_form(self, data=None, files=None, method=None): """ @@ -217,7 +219,6 @@ class FormResource(Resource): return form() - #class _RegisterModelResource(type): # """ # Auto register new ModelResource classes into ``_model_to_resource`` @@ -230,14 +231,15 @@ class FormResource(Resource): # return resource_cls - class ModelResource(FormResource): """ - Resource class that uses forms for validation and otherwise falls back to a model form if no form is set. - Also provides a :meth:`get_bound_form` method which may be used by some renderers. + Resource class that uses forms for validation and otherwise falls back to a + model form if no form is set. + Also provides a :meth:`get_bound_form` method which may be used by some + renderers. """ - # Auto-register new ModelResource classes into _model_to_resource + # Auto-register new ModelResource classes into _model_to_resource #__metaclass__ = _RegisterModelResource form = None @@ -245,38 +247,45 @@ class ModelResource(FormResource): The form class that should be used for request validation. If set to :const:`None` then the default model form validation will be used. - This can be overridden by a :attr:`form` attribute on the :class:`views.View`. + This can be overridden by a :attr:`form` attribute on the + :class:`views.View`. """ model = None """ The model class which this resource maps to. - This can be overridden by a :attr:`model` attribute on the :class:`views.View`. + This can be overridden by a :attr:`model` attribute on the + :class:`views.View`. """ fields = None """ The list of fields to use on the output. - + May be any of: - - The name of a model field. To view nested resources, give the field as a tuple of ("fieldName", resource) where `resource` may be any of ModelResource reference, the name of a ModelResourc reference as a string or a tuple of strings representing fields on the nested model. + + The name of a model field. To view nested resources, give the field as a + tuple of ("fieldName", resource) where `resource` may be any of + ModelResource reference, the name of a ModelResourc reference as a string + or a tuple of strings representing fields on the nested model. The name of an attribute on the model. The name of an attribute on the resource. The name of a method on the model, with a signature like ``func(self)``. - The name of a method on the resource, with a signature like ``func(self, instance)``. + The name of a method on the resource, with a signature like + ``func(self, instance)``. """ - + exclude = ('id', 'pk') """ - The list of fields to exclude. This is only used if :attr:`fields` is not set. + The list of fields to exclude. This is only used if :attr:`fields` is not + set. """ - include = ('url',) """ - The list of extra fields to include. This is only used if :attr:`fields` is not set. + The list of extra fields to include. This is only used if :attr:`fields` + is not set. """ def __init__(self, view=None, depth=None, stack=[], **kwargs): @@ -289,30 +298,35 @@ class ModelResource(FormResource): self.model = getattr(view, 'model', None) or self.model - def validate_request(self, data, files=None): """ Given some content as input return some cleaned, validated content. - Raises a :exc:`response.ErrorResponse` with status code 400 (Bad Request) on failure. - + Raises a :exc:`response.ErrorResponse` with status code 400 + (Bad Request) on failure. + Validation is standard form or model form validation, - with an additional constraint that no extra unknown fields may be supplied, - and that all fields specified by the fields class attribute must be supplied, - even if they are not validated by the form/model form. + with an additional constraint that no extra unknown fields may be + supplied, and that all fields specified by the fields class attribute + must be supplied, even if they are not validated by the Form/ModelForm. - On failure the ErrorResponse content is a dict which may contain :obj:`'errors'` and :obj:`'field-errors'` keys. - If the :obj:`'errors'` key exists it is a list of strings of non-field errors. - If the ''field-errors'` key exists it is a dict of {field name as string: list of errors as strings}. + On failure the ErrorResponse content is a dict which may contain + :obj:`'errors'` and :obj:`'field-errors'` keys. + If the :obj:`'errors'` key exists it is a list of strings of non-field + errors. + If the ''field-errors'` key exists it is a dict of + `{field name as string: list of errors as strings}`. """ - return self._validate(data, files, allowed_extra_fields=self._property_fields_set) - + return self._validate(data, files, + allowed_extra_fields=self._property_fields_set) def get_bound_form(self, data=None, files=None, method=None): """ Given some content return a ``Form`` instance bound to that content. - If the :attr:`form` class attribute has been explicitly set then that class will be used - to create the Form, otherwise the model will be used to create a ModelForm. + If the :attr:`form` class attribute has been explicitly set then that + class will be used + to create the Form, otherwise the model will be used to create a + ModelForm. """ form = self.get_form_class(method) @@ -339,18 +353,20 @@ class ModelResource(FormResource): return form() - def url(self, instance): """ - Attempts to reverse resolve the url of the given model *instance* for this resource. + Attempts to reverse resolve the url of the given model *instance* for + this resource. - Requires a ``View`` with :class:`mixins.InstanceMixin` to have been created for this resource. - - This method can be overridden if you need to set the resource url reversing explicitly. + Requires a ``View`` with :class:`mixins.InstanceMixin` to have been + created for this resource. + + This method can be overridden if you need to set the resource url + reversing explicitly. """ if not hasattr(self, 'view_callable'): - raise _SkipField + raise _SkipField # dis does teh magicks... urlconf = get_urlconf() @@ -363,7 +379,9 @@ class ModelResource(FormResource): # Note: defaults = tuple_item[2] for django >= 1.3 for result, params in possibility: - #instance_attrs = dict([ (param, getattr(instance, param)) for param in params if hasattr(instance, param) ]) + # instance_attrs = dict([ (param, getattr(instance, param)) + # for param in params + # if hasattr(instance, param) ]) instance_attrs = {} for param in params: @@ -381,7 +399,6 @@ class ModelResource(FormResource): pass raise _SkipField - @property def _model_fields_set(self): """ @@ -389,11 +406,11 @@ class ModelResource(FormResource): """ model_fields = set(field.name for field in self.model._meta.fields) - if fields: + if self.fields: return model_fields & set(as_tuple(self.fields)) return model_fields - set(as_tuple(self.exclude)) - + @property def _property_fields_set(self): """ diff --git a/djangorestframework/tests/mixins.py b/djangorestframework/tests/mixins.py index 65cf4a45a..0ccef5d3a 100644 --- a/djangorestframework/tests/mixins.py +++ b/djangorestframework/tests/mixins.py @@ -4,7 +4,7 @@ from django.utils import simplejson as json from djangorestframework import status from djangorestframework.compat import RequestFactory from django.contrib.auth.models import Group, User -from djangorestframework.mixins import CreateModelMixin, PaginatorMixin +from djangorestframework.mixins import PaginatorMixin from djangorestframework.resources import ModelResource from djangorestframework.response import Response from djangorestframework.tests.models import CustomUser @@ -25,7 +25,7 @@ class TestModelCreation(TestCase): form_data = {'name': 'foo'} request = self.req.post('/groups', data=form_data) - mixin = CreateModelMixin() + mixin = ModelMixin() mixin.resource = GroupResource mixin.CONTENT = form_data @@ -51,7 +51,7 @@ class TestModelCreation(TestCase): request = self.req.post('/groups', data=form_data) cleaned_data = dict(form_data) cleaned_data['groups'] = [group] - mixin = CreateModelMixin() + mixin = ModelMixin() mixin.resource = UserResource mixin.CONTENT = cleaned_data @@ -74,7 +74,7 @@ class TestModelCreation(TestCase): request = self.req.post('/groups', data=form_data) cleaned_data = dict(form_data) cleaned_data['groups'] = [] - mixin = CreateModelMixin() + mixin = ModelMixin() mixin.resource = UserResource mixin.CONTENT = cleaned_data @@ -105,7 +105,7 @@ class TestModelCreation(TestCase): request = self.req.post('/groups', data=form_data) cleaned_data = dict(form_data) cleaned_data['groups'] = [group, group2] - mixin = CreateModelMixin() + mixin = ModelMixin() mixin.resource = UserResource mixin.CONTENT = cleaned_data diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 0a3594047..9045c1061 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -25,7 +25,6 @@ __all__ = ( ) - class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ Handles incoming requests and maps them to REST operations. @@ -59,7 +58,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ permissions = ( permissions.FullAnonAccess, ) - @classmethod def as_view(cls, **initkwargs): """ @@ -71,7 +69,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): view.cls_instance = cls(**initkwargs) return view - @property def allowed_methods(self): """ @@ -79,7 +76,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ return [method.upper() for method in self.http_method_names if hasattr(self, method)] - def http_method_not_allowed(self, request, *args, **kwargs): """ Return an HTTP 405 error if an operation is called which does not have a handler method. @@ -87,7 +83,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED, {'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) - def initial(self, request, *args, **kargs): """ Hook for any code that needs to run prior to anything else. @@ -96,14 +91,12 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): """ pass - def add_header(self, field, value): """ Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class. """ self.headers[field] = value - # Note: session based authentication is explicitly CSRF validated, # all other authentication is CSRF exempt. @csrf_exempt @@ -183,26 +176,68 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): return response_obj -class ModelView(View): +class ModelView(ModelMixin, View): """ A RESTful view that maps to a model in the database. """ resource = resources.ModelResource -class InstanceModelView(InstanceMixin, ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): + def _filter_kwargs(self, kwargs): + kwargs = kwargs.copy() + if BaseRenderer._FORMAT_QUERY_PARAM in kwargs: + del kwargs[BaseRenderer._FORMAT_QUERY_PARAM] + return kwargs + + +class InstanceModelView(ModelView): """ A view which provides default operations for read/update/delete against a model instance. """ _suffix = 'Instance' -class ListModelView(ListModelMixin, ModelView): + def get(self, request, *args, **kwargs): + instance = self.read(request, *args, **self._filter_kwargs(kwargs)) + + if not instance: + raise ErrorResponse(status.HTTP_404_NOT_FOUND) + + return instance + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **self._filter_kwargs(kwargs)) + + def delete(self, request, *args, **kwargs): + instance = self.destroy(request, *args, **self._filter_kwargs(kwargs)) + + if not instance: + raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {}) + + return None + + +class ListModelView(ModelView): """ A view which provides default operations for list, against a model in the database. """ _suffix = 'List' -class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView): + def get(self, request, *args, **kwargs): + return self.list(request, *args, **self._filter_kwargs(kwargs)) + + +class ListOrCreateModelView(ModelView): """ A view which provides default operations for list and create, against a model in the database. """ _suffix = 'List' + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **self._filter_kwargs(kwargs)) + + def post(self, request, *args, **kwargs): + instance = self.create(request, *args, **self._filter_kwargs(kwargs)) + + headers = {} + if hasattr(instance, 'get_absolute_url'): + headers['Location'] = self.resource(self).url(instance) + return Response(status.HTTP_201_CREATED, instance, headers)