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 django.http import HttpResponse
from djangorestframework import status from djangorestframework import status
from djangorestframework.renderers import BaseRenderer
from djangorestframework.resources import Resource, FormResource, ModelResource 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 import as_tuple, MSIE_USER_AGENT_REGEX
from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence from djangorestframework.utils.mediatypes import is_form_media_type, order_by_precedence
@ -27,11 +26,7 @@ __all__ = (
# Reverse URL lookup behavior # Reverse URL lookup behavior
'InstanceMixin', 'InstanceMixin',
# Model behavior mixins # Model behavior mixins
'ReadModelMixin', 'ModelMixin',
'CreateModelMixin',
'UpdateModelMixin',
'DeleteModelMixin',
'ListModelMixin'
) )
@ -267,25 +262,32 @@ class ResponseMixin(object):
def _determine_renderer(self, request): def _determine_renderer(self, request):
""" """
Determines the appropriate renderer for the output, given the client's 'Accept' header, Determines the appropriate renderer for the output, given the client's
and the :attr:`renderers` set on this class. 'Accept' header, and the :attr:`renderers` set on this class.
Returns a 2-tuple of `(renderer, media_type)` 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 # Use _accept parameter override
accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)] accept_list = [request.GET.get(self._ACCEPT_QUERY_PARAM)]
elif (self._IGNORE_IE_ACCEPT_HEADER and 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'])): 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', '*/*'] accept_list = ['text/html', '*/*']
elif request.META.has_key('HTTP_ACCEPT'):
elif 'HTTP_USER_AGENT' in request.META:
# Use standard HTTP Accept negotiation # 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: else:
# No accept header specified # No accept header specified
accept_list = ['*/*'] accept_list = ['*/*']
@ -481,48 +483,86 @@ class InstanceMixin(object):
########## Model Mixins ########## ########## Model Mixins ##########
class ReadModelMixin(object): class ModelMixin(object):
""" def get_model(self):
Behavior to read a `model` instance on GET requests """
""" Return the model class for this view.
def get(self, request, *args, **kwargs): """
model = self.resource.model 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: 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 args:
# If we have any none kwargs then assume the last represents the primrary key return queryset.get(pk=args[-1], **kwargs)
self.model_instance = model.objects.get(pk=args[-1], **kwargs)
else: else:
# Otherwise assume the kwargs uniquely identify the model return queryset.get(**kwargs)
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)
except model.DoesNotExist: 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): if instance:
""" for (key, val) in self.CONTENT.items():
Behavior to create a `model` instance on POST requests setattr(instance, key, val)
""" else:
def post(self, request, *args, **kwargs): instance = self.get_model()(**self.CONTENT)
model = self.resource.model
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 # Copy the dict to keep self.CONTENT intact
content = dict(self.CONTENT) content = dict(self.CONTENT)
m2m_data = {} m2m_data = {}
for field in model._meta.fields: 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' # translate 'related_field' kwargs into 'related_field_id'
kwargs[field.name + '_id'] = kwargs[field.name] kwargs[field.name + '_id'] = kwargs[field.name]
del kwargs[field.name] del kwargs[field.name]
for field in model._meta.many_to_many: for field in model._meta.many_to_many:
if content.has_key(field.name): if field.name in content:
m2m_data[field.name] = ( m2m_data[field.name] = (
field.m2m_reverse_field_name(), content[field.name] field.m2m_reverse_field_name(), content[field.name]
) )
@ -549,90 +589,30 @@ class CreateModelMixin(object):
data[m2m_data[fieldname][0]] = related_item data[m2m_data[fieldname][0]] = related_item
manager.through(**data).save() manager.through(**data).save()
headers = {} return instance
if hasattr(instance, 'get_absolute_url'):
headers['Location'] = self.resource(self).url(instance)
return Response(status.HTTP_201_CREATED, instance, headers)
class UpdateModelMixin(object): def destroy(self, request, *args, **kwargs):
""" """
Behavior to update a `model` instance on PUT requests Return a model instance or None.
""" """
def put(self, request, *args, **kwargs): instance = self.get_instance(*args, **kwargs)
model = self.resource.model
# 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 if instance:
try: instance.delete()
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)
for (key, val) in self.CONTENT.items(): return instance
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
class DeleteModelMixin(object): def list(self, request, *args, **kwargs):
""" """
Behavior to delete a `model` instance on DELETE requests Return a list of instances.
""" """
def delete(self, request, *args, **kwargs): queryset = self.get_queryset()
model = self.resource.model ordering = self.get_ordering()
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
if ordering: if ordering:
args = as_tuple(ordering) assert(hasattr(ordering, '__iter__'))
queryset = queryset.order_by(*args) queryset = queryset.order_by(*args)
return queryset.filter(**kwargs) return queryset.filter(**kwargs)

View File

@ -1,24 +1,16 @@
from django import forms from django import forms
from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch from django.core.urlresolvers import reverse, get_urlconf, get_resolver, NoReverseMatch
from django.db import models 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.response import ErrorResponse
from djangorestframework.serializer import Serializer, _SkipField from djangorestframework.serializer import Serializer, _SkipField
from djangorestframework.utils import as_tuple from djangorestframework.utils import as_tuple
import decimal
import inspect
import re
class BaseResource(Serializer): 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 fields = None
include = None include = None
@ -31,7 +23,8 @@ class BaseResource(Serializer):
def validate_request(self, data, files=None): def validate_request(self, data, files=None):
""" """
Given the request content return the cleaned, validated content. 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 return data
@ -45,18 +38,20 @@ class BaseResource(Serializer):
class Resource(BaseResource): class Resource(BaseResource):
""" """
A Resource determines how a python object maps to some serializable data. 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 attribute refers to the Django Model which this Resource maps
# (The Model's class, rather than an instance of the Model) # to. (The Model's class, rather than an instance of the Model)
model = None model = None
# By default the set of returned fields will be the set of: # By default the set of returned fields will be the set of:
# #
# 0. All the fields on the model, excluding 'id'. # 0. All the fields on the model, excluding 'id'.
# 1. All the properties on the model. # 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, # If you wish to override this behaviour,
# you should explicitly set the fields attribute on your class. # you should explicitly set the fields attribute on your class.
@ -66,42 +61,50 @@ class Resource(BaseResource):
class FormResource(Resource): class FormResource(Resource):
""" """
Resource class that uses forms for validation. 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 On calling :meth:`validate_request` this validator may set a
view, which may be used by some renderers. :attr:`bound_form_instance` attribute on the view, which may be used by
some renderers.
""" """
form = None form = None
""" """
The :class:`Form` class that should be used for request validation. 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): def validate_request(self, data, files=None):
""" """
Given some content as input return some cleaned, validated content. 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 validation, with an additional constraint that *no extra unknown fields* may be supplied. 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. On failure the :exc:`response.ErrorResponse` content is a dict which
If the :obj:`'errors'` key exists it is a list of strings of non-field errors. may contain :obj:`'errors'` and :obj:`'field-errors'` keys.
If the :obj:`'field-errors'` key exists it is a dict of ``{'field name as string': ['errors as strings', ...]}``. 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) return self._validate(data, files)
def _validate(self, data, files, allowed_extra_fields=(), fake_data=None): 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 allowed_extra_fields is a list of fields which are not defined by the
expect to see on the input. 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 fake_data is a string that should be used as an extra key, as a kludge
to be populated when an empty dict is supplied in `data` 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. # We'd like nice error messages even if no content is supplied.
@ -127,7 +130,8 @@ class FormResource(Resource):
form_fields_set = set(bound_form.fields.keys()) form_fields_set = set(bound_form.fields.keys())
allowed_extra_fields_set = set(allowed_extra_fields) 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 = seen_fields_set - (form_fields_set | allowed_extra_fields_set)
unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh. unknown_fields = unknown_fields - set(('csrfmiddlewaretoken', '_accept', '_method')) # TODO: Ugh.
@ -178,7 +182,6 @@ class FormResource(Resource):
# Return HTTP 400 response (BAD REQUEST) # Return HTTP 400 response (BAD REQUEST)
raise ErrorResponse(400, detail) raise ErrorResponse(400, detail)
def get_form_class(self, method=None): def get_form_class(self, method=None):
""" """
Returns the form class used to validate this resource. Returns the form class used to validate this resource.
@ -200,7 +203,6 @@ class FormResource(Resource):
return form return form
def get_bound_form(self, data=None, files=None, method=None): def get_bound_form(self, data=None, files=None, method=None):
""" """
Given some content return a Django form bound to that content. Given some content return a Django form bound to that content.
@ -217,7 +219,6 @@ class FormResource(Resource):
return form() return form()
#class _RegisterModelResource(type): #class _RegisterModelResource(type):
# """ # """
# Auto register new ModelResource classes into ``_model_to_resource`` # Auto register new ModelResource classes into ``_model_to_resource``
@ -230,11 +231,12 @@ class FormResource(Resource):
# return resource_cls # return resource_cls
class ModelResource(FormResource): class ModelResource(FormResource):
""" """
Resource class that uses forms for validation and otherwise falls back to a model form if no form is set. Resource class that uses forms for validation and otherwise falls back to a
Also provides a :meth:`get_bound_form` method which may be used by some renderers. 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
@ -245,14 +247,16 @@ class ModelResource(FormResource):
The form class that should be used for request validation. The form class that should be used for request validation.
If set to :const:`None` then the default model form validation will be used. 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 model = None
""" """
The model class which this resource maps to. 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 fields = None
@ -261,22 +265,27 @@ class ModelResource(FormResource):
May be any of: 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 model.
The name of an attribute on the resource. 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 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') 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',) 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): 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 self.model = getattr(view, 'model', None) or self.model
def validate_request(self, data, files=None): def validate_request(self, data, files=None):
""" """
Given some content as input return some cleaned, validated content. 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, Validation is standard form or model form validation,
with an additional constraint that no extra unknown fields may be supplied, with an additional constraint that no extra unknown fields may be
and that all fields specified by the fields class attribute must be supplied, supplied, and that all fields specified by the fields class attribute
even if they are not validated by the form/model form. 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. On failure the ErrorResponse content is a dict which may contain
If the :obj:`'errors'` key exists it is a list of strings of non-field errors. :obj:`'errors'` and :obj:`'field-errors'` keys.
If the ''field-errors'` key exists it is a dict of {field name as string: list of errors as strings}. 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): def get_bound_form(self, data=None, files=None, method=None):
""" """
Given some content return a ``Form`` instance bound to that content. 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 If the :attr:`form` class attribute has been explicitly set then that
to create the Form, otherwise the model will be used to create a ModelForm. class will be used
to create the Form, otherwise the model will be used to create a
ModelForm.
""" """
form = self.get_form_class(method) form = self.get_form_class(method)
@ -339,14 +353,16 @@ class ModelResource(FormResource):
return form() return form()
def url(self, instance): 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. 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. This method can be overridden if you need to set the resource url
reversing explicitly.
""" """
if not hasattr(self, 'view_callable'): if not hasattr(self, 'view_callable'):
@ -363,7 +379,9 @@ class ModelResource(FormResource):
# Note: defaults = tuple_item[2] for django >= 1.3 # Note: defaults = tuple_item[2] for django >= 1.3
for result, params in possibility: 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 = {} instance_attrs = {}
for param in params: for param in params:
@ -381,7 +399,6 @@ class ModelResource(FormResource):
pass pass
raise _SkipField raise _SkipField
@property @property
def _model_fields_set(self): def _model_fields_set(self):
""" """
@ -389,7 +406,7 @@ class ModelResource(FormResource):
""" """
model_fields = set(field.name for field in self.model._meta.fields) 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.fields))
return model_fields - set(as_tuple(self.exclude)) return model_fields - set(as_tuple(self.exclude))

View File

@ -4,7 +4,7 @@ from django.utils import simplejson as json
from djangorestframework import status from djangorestframework import status
from djangorestframework.compat import RequestFactory from djangorestframework.compat import RequestFactory
from django.contrib.auth.models import Group, User 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.resources import ModelResource
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework.tests.models import CustomUser from djangorestframework.tests.models import CustomUser
@ -25,7 +25,7 @@ class TestModelCreation(TestCase):
form_data = {'name': 'foo'} form_data = {'name': 'foo'}
request = self.req.post('/groups', data=form_data) request = self.req.post('/groups', data=form_data)
mixin = CreateModelMixin() mixin = ModelMixin()
mixin.resource = GroupResource mixin.resource = GroupResource
mixin.CONTENT = form_data mixin.CONTENT = form_data
@ -51,7 +51,7 @@ class TestModelCreation(TestCase):
request = self.req.post('/groups', data=form_data) request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data) cleaned_data = dict(form_data)
cleaned_data['groups'] = [group] cleaned_data['groups'] = [group]
mixin = CreateModelMixin() mixin = ModelMixin()
mixin.resource = UserResource mixin.resource = UserResource
mixin.CONTENT = cleaned_data mixin.CONTENT = cleaned_data
@ -74,7 +74,7 @@ class TestModelCreation(TestCase):
request = self.req.post('/groups', data=form_data) request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data) cleaned_data = dict(form_data)
cleaned_data['groups'] = [] cleaned_data['groups'] = []
mixin = CreateModelMixin() mixin = ModelMixin()
mixin.resource = UserResource mixin.resource = UserResource
mixin.CONTENT = cleaned_data mixin.CONTENT = cleaned_data
@ -105,7 +105,7 @@ class TestModelCreation(TestCase):
request = self.req.post('/groups', data=form_data) request = self.req.post('/groups', data=form_data)
cleaned_data = dict(form_data) cleaned_data = dict(form_data)
cleaned_data['groups'] = [group, group2] cleaned_data['groups'] = [group, group2]
mixin = CreateModelMixin() mixin = ModelMixin()
mixin.resource = UserResource mixin.resource = UserResource
mixin.CONTENT = cleaned_data mixin.CONTENT = cleaned_data

View File

@ -25,7 +25,6 @@ __all__ = (
) )
class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
Handles incoming requests and maps them to REST operations. Handles incoming requests and maps them to REST operations.
@ -59,7 +58,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
permissions = ( permissions.FullAnonAccess, ) permissions = ( permissions.FullAnonAccess, )
@classmethod @classmethod
def as_view(cls, **initkwargs): def as_view(cls, **initkwargs):
""" """
@ -71,7 +69,6 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
view.cls_instance = cls(**initkwargs) view.cls_instance = cls(**initkwargs)
return view return view
@property @property
def allowed_methods(self): 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)] return [method.upper() for method in self.http_method_names if hasattr(self, method)]
def http_method_not_allowed(self, request, *args, **kwargs): 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. 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, raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) {'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
def initial(self, request, *args, **kargs): def initial(self, request, *args, **kargs):
""" """
Hook for any code that needs to run prior to anything else. Hook for any code that needs to run prior to anything else.
@ -96,14 +91,12 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
""" """
pass pass
def add_header(self, field, value): def add_header(self, field, value):
""" """
Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class. Add *field* and *value* to the :attr:`headers` attribute of the :class:`View` class.
""" """
self.headers[field] = value self.headers[field] = value
# Note: session based authentication is explicitly CSRF validated, # Note: session based authentication is explicitly CSRF validated,
# all other authentication is CSRF exempt. # all other authentication is CSRF exempt.
@csrf_exempt @csrf_exempt
@ -183,26 +176,68 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView):
return response_obj return response_obj
class ModelView(View): class ModelView(ModelMixin, View):
""" """
A RESTful view that maps to a model in the database. A RESTful view that maps to a model in the database.
""" """
resource = resources.ModelResource 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. A view which provides default operations for read/update/delete against a model instance.
""" """
_suffix = '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. A view which provides default operations for list, against a model in the database.
""" """
_suffix = 'List' _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. A view which provides default operations for list and create, against a model in the database.
""" """
_suffix = 'List' _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)