mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-22 14:32:55 +03:00
Getting more resourceful - read, create, update and delete methods on ModelMixin
This commit is contained in:
parent
d69581e2af
commit
e8126c3a91
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user