Getting more resourceful - read, create, update and delete methods on ModelMixin

This commit is contained in:
Tom Christie 2011-12-14 10:48:19 +00:00
parent d69581e2af
commit e8126c3a91
4 changed files with 247 additions and 215 deletions

View File

@ -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)

View File

@ -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):
"""

View File

@ -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

View File

@ -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)