mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-04-28 13:03:45 +03:00
Drop out resources & mixins
This commit is contained in:
parent
87b363f7bc
commit
aed26b218e
|
@ -1,388 +0,0 @@
|
||||||
"""
|
|
||||||
The :mod:`mixins` module provides a set of reusable `mixin`
|
|
||||||
classes that can be added to a `View`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.core.paginator import Paginator
|
|
||||||
from django.db.models.fields.related import ForeignKey
|
|
||||||
from urlobject import URLObject
|
|
||||||
|
|
||||||
from djangorestframework import status
|
|
||||||
from djangorestframework.renderers import BaseRenderer
|
|
||||||
from djangorestframework.resources import Resource, FormResource, ModelResource
|
|
||||||
from djangorestframework.response import Response, ImmediateResponse
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
# Base behavior mixins
|
|
||||||
'ResourceMixin',
|
|
||||||
# Model behavior mixins
|
|
||||||
'ReadModelMixin',
|
|
||||||
'CreateModelMixin',
|
|
||||||
'UpdateModelMixin',
|
|
||||||
'DeleteModelMixin',
|
|
||||||
'ListModelMixin',
|
|
||||||
'PaginatorMixin'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
########## Resource Mixin ##########
|
|
||||||
|
|
||||||
class ResourceMixin(object):
|
|
||||||
"""
|
|
||||||
Provides request validation and response filtering behavior.
|
|
||||||
|
|
||||||
Should be a class as described in the :mod:`resources` module.
|
|
||||||
|
|
||||||
The :obj:`resource` is an object that maps a view onto it's representation on the server.
|
|
||||||
|
|
||||||
It provides validation on the content of incoming requests,
|
|
||||||
and filters the object representation into a serializable object for the response.
|
|
||||||
"""
|
|
||||||
resource = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def CONTENT(self):
|
|
||||||
"""
|
|
||||||
Returns the cleaned, validated request content.
|
|
||||||
|
|
||||||
May raise an :class:`response.ImmediateResponse` with status code 400 (Bad Request).
|
|
||||||
"""
|
|
||||||
if not hasattr(self, '_content'):
|
|
||||||
self._content = self.validate_request(self.request.DATA, self.request.FILES)
|
|
||||||
return self._content
|
|
||||||
|
|
||||||
@property
|
|
||||||
def PARAMS(self):
|
|
||||||
"""
|
|
||||||
Returns the cleaned, validated query parameters.
|
|
||||||
|
|
||||||
May raise an :class:`response.ImmediateResponse` with status code 400 (Bad Request).
|
|
||||||
"""
|
|
||||||
return self.validate_request(self.request.GET)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _resource(self):
|
|
||||||
if self.resource:
|
|
||||||
return self.resource(self)
|
|
||||||
elif getattr(self, 'model', None):
|
|
||||||
return ModelResource(self)
|
|
||||||
elif getattr(self, 'form', None):
|
|
||||||
return FormResource(self)
|
|
||||||
elif getattr(self, '%s_form' % self.request.method.lower(), None):
|
|
||||||
return FormResource(self)
|
|
||||||
return Resource(self)
|
|
||||||
|
|
||||||
def validate_request(self, data, files=None):
|
|
||||||
"""
|
|
||||||
Given the request *data* and optional *files*, return the cleaned, validated content.
|
|
||||||
May raise an :class:`response.ImmediateResponse` with status code 400 (Bad Request) on failure.
|
|
||||||
"""
|
|
||||||
return self._resource.validate_request(data, files)
|
|
||||||
|
|
||||||
def filter_response(self, obj):
|
|
||||||
"""
|
|
||||||
Given the response content, filter it into a serializable object.
|
|
||||||
"""
|
|
||||||
return self._resource.filter_response(obj)
|
|
||||||
|
|
||||||
def get_bound_form(self, content=None, method=None):
|
|
||||||
if hasattr(self._resource, 'get_bound_form'):
|
|
||||||
return self._resource.get_bound_form(content, method=method)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
########## Model Mixins ##########
|
|
||||||
|
|
||||||
class ModelMixin(object):
|
|
||||||
""" Implements mechanisms used by other classes (like *ModelMixin group) to
|
|
||||||
define a query that represents Model instances the Mixin is working with.
|
|
||||||
|
|
||||||
If a *ModelMixin is going to retrive an instance (or queryset) using args and kwargs
|
|
||||||
passed by as URL arguments, it should provied arguments to objects.get and objects.filter
|
|
||||||
methods wrapped in by `build_query`
|
|
||||||
|
|
||||||
If a *ModelMixin is going to create/update an instance get_instance_data
|
|
||||||
handles the instance data creation/preaparation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
queryset = None
|
|
||||||
|
|
||||||
def get_query_kwargs(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Return a dict of kwargs that will be used to build the
|
|
||||||
model instance retrieval or to filter querysets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
kwargs = dict(kwargs)
|
|
||||||
|
|
||||||
# If the URLconf includes a .(?P<format>\w+) pattern to match against
|
|
||||||
# a .json, .xml suffix, then drop the 'format' kwarg before
|
|
||||||
# constructing the query.
|
|
||||||
if BaseRenderer._FORMAT_QUERY_PARAM in kwargs:
|
|
||||||
del kwargs[BaseRenderer._FORMAT_QUERY_PARAM]
|
|
||||||
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def get_instance_data(self, model, content, **kwargs):
|
|
||||||
"""
|
|
||||||
Returns the dict with the data for model instance creation/update.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
- model: model class (django.db.models.Model subclass) to work with
|
|
||||||
- content: a dictionary with instance data
|
|
||||||
- kwargs: a dict of URL provided keyword arguments
|
|
||||||
|
|
||||||
The create/update queries are created basicly with the contet provided
|
|
||||||
with POST/PUT HTML methods and kwargs passed in the URL. This methods
|
|
||||||
simply merges the URL data and the content preaparing the ready-to-use
|
|
||||||
data dictionary.
|
|
||||||
"""
|
|
||||||
|
|
||||||
tmp = dict(kwargs)
|
|
||||||
|
|
||||||
for field in model._meta.fields:
|
|
||||||
if isinstance(field, ForeignKey) and field.name in tmp:
|
|
||||||
# translate 'related_field' kwargs into 'related_field_id'
|
|
||||||
tmp[field.name + '_id'] = tmp[field.name]
|
|
||||||
del tmp[field.name]
|
|
||||||
|
|
||||||
all_kw_args = dict(content.items() + tmp.items())
|
|
||||||
|
|
||||||
return all_kw_args
|
|
||||||
|
|
||||||
def get_instance(self, **kwargs):
|
|
||||||
"""
|
|
||||||
Get a model instance for read/update/delete requests.
|
|
||||||
"""
|
|
||||||
return self.get_queryset().get(**kwargs)
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""
|
|
||||||
Return the queryset for this view.
|
|
||||||
"""
|
|
||||||
return getattr(self.resource, 'queryset',
|
|
||||||
self.resource.model.objects.all())
|
|
||||||
|
|
||||||
def get_ordering(self):
|
|
||||||
"""
|
|
||||||
Return the ordering for this view.
|
|
||||||
"""
|
|
||||||
return getattr(self.resource, 'ordering', None)
|
|
||||||
|
|
||||||
|
|
||||||
class ReadModelMixin(ModelMixin):
|
|
||||||
"""
|
|
||||||
Behavior to read a `model` instance on GET requests
|
|
||||||
"""
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
model = self.resource.model
|
|
||||||
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.model_instance = self.get_instance(**query_kwargs)
|
|
||||||
except model.DoesNotExist:
|
|
||||||
raise ImmediateResponse(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
return Response(self.model_instance)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateModelMixin(ModelMixin):
|
|
||||||
"""
|
|
||||||
Behavior to create a `model` instance on POST requests
|
|
||||||
"""
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
model = self.resource.model
|
|
||||||
|
|
||||||
# Copy the dict to keep self.CONTENT intact
|
|
||||||
content = dict(self.CONTENT)
|
|
||||||
m2m_data = {}
|
|
||||||
|
|
||||||
for field in model._meta.many_to_many:
|
|
||||||
if field.name in content:
|
|
||||||
m2m_data[field.name] = (
|
|
||||||
field.m2m_reverse_field_name(), content[field.name]
|
|
||||||
)
|
|
||||||
del content[field.name]
|
|
||||||
|
|
||||||
instance = model(**self.get_instance_data(model, content, *args, **kwargs))
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
for fieldname in m2m_data:
|
|
||||||
manager = getattr(instance, fieldname)
|
|
||||||
|
|
||||||
if hasattr(manager, 'add'):
|
|
||||||
manager.add(*m2m_data[fieldname][1])
|
|
||||||
else:
|
|
||||||
data = {}
|
|
||||||
data[manager.source_field_name] = instance
|
|
||||||
|
|
||||||
for related_item in m2m_data[fieldname][1]:
|
|
||||||
data[m2m_data[fieldname][0]] = related_item
|
|
||||||
manager.through(**data).save()
|
|
||||||
|
|
||||||
response = Response(instance, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
# Set headers
|
|
||||||
if hasattr(self.resource, 'url'):
|
|
||||||
response['Location'] = self.resource(self).url(instance)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateModelMixin(ModelMixin):
|
|
||||||
"""
|
|
||||||
Behavior to update a `model` instance on PUT requests
|
|
||||||
"""
|
|
||||||
def put(self, request, *args, **kwargs):
|
|
||||||
model = self.resource.model
|
|
||||||
query_kwargs = self.get_query_kwargs(request, *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:
|
|
||||||
self.model_instance = self.get_instance(**query_kwargs)
|
|
||||||
|
|
||||||
for (key, val) in self.CONTENT.items():
|
|
||||||
setattr(self.model_instance, key, val)
|
|
||||||
except model.DoesNotExist:
|
|
||||||
self.model_instance = model(**self.get_instance_data(model, self.CONTENT, *args, **kwargs))
|
|
||||||
self.model_instance.save()
|
|
||||||
return Response(self.model_instance)
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteModelMixin(ModelMixin):
|
|
||||||
"""
|
|
||||||
Behavior to delete a `model` instance on DELETE requests
|
|
||||||
"""
|
|
||||||
def delete(self, request, *args, **kwargs):
|
|
||||||
model = self.resource.model
|
|
||||||
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
|
|
||||||
|
|
||||||
try:
|
|
||||||
instance = self.get_instance(**query_kwargs)
|
|
||||||
except model.DoesNotExist:
|
|
||||||
raise ImmediateResponse(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
instance.delete()
|
|
||||||
return Response()
|
|
||||||
|
|
||||||
|
|
||||||
class ListModelMixin(ModelMixin):
|
|
||||||
"""
|
|
||||||
Behavior to list a set of `model` instances on GET requests
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
queryset = self.get_queryset()
|
|
||||||
ordering = self.get_ordering()
|
|
||||||
query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
|
|
||||||
|
|
||||||
queryset = queryset.filter(**query_kwargs)
|
|
||||||
if ordering:
|
|
||||||
queryset = queryset.order_by(*ordering)
|
|
||||||
|
|
||||||
return Response(queryset)
|
|
||||||
|
|
||||||
|
|
||||||
########## Pagination Mixins ##########
|
|
||||||
|
|
||||||
class PaginatorMixin(object):
|
|
||||||
"""
|
|
||||||
Adds pagination support to GET requests
|
|
||||||
Obviously should only be used on lists :)
|
|
||||||
|
|
||||||
A default limit can be set by setting `limit` on the object. This will also
|
|
||||||
be used as the maximum if the client sets the `limit` GET param
|
|
||||||
"""
|
|
||||||
limit = 20
|
|
||||||
|
|
||||||
def get_limit(self):
|
|
||||||
"""
|
|
||||||
Helper method to determine what the `limit` should be
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
limit = int(self.request.GET.get('limit', self.limit))
|
|
||||||
return min(limit, self.limit)
|
|
||||||
except ValueError:
|
|
||||||
return self.limit
|
|
||||||
|
|
||||||
def url_with_page_number(self, page_number):
|
|
||||||
"""
|
|
||||||
Constructs a url used for getting the next/previous urls
|
|
||||||
"""
|
|
||||||
url = URLObject(self.request.get_full_path())
|
|
||||||
url = url.set_query_param('page', str(page_number))
|
|
||||||
|
|
||||||
limit = self.get_limit()
|
|
||||||
if limit != self.limit:
|
|
||||||
url = url.set_query_param('limit', str(limit))
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
def next(self, page):
|
|
||||||
"""
|
|
||||||
Returns a url to the next page of results (if any)
|
|
||||||
"""
|
|
||||||
if not page.has_next():
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.url_with_page_number(page.next_page_number())
|
|
||||||
|
|
||||||
def previous(self, page):
|
|
||||||
""" Returns a url to the previous page of results (if any) """
|
|
||||||
if not page.has_previous():
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.url_with_page_number(page.previous_page_number())
|
|
||||||
|
|
||||||
def serialize_page_info(self, page):
|
|
||||||
"""
|
|
||||||
This is some useful information that is added to the response
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'next': self.next(page),
|
|
||||||
'page': page.number,
|
|
||||||
'pages': page.paginator.num_pages,
|
|
||||||
'per_page': self.get_limit(),
|
|
||||||
'previous': self.previous(page),
|
|
||||||
'total': page.paginator.count,
|
|
||||||
}
|
|
||||||
|
|
||||||
def filter_response(self, obj):
|
|
||||||
"""
|
|
||||||
Given the response content, paginate and then serialize.
|
|
||||||
|
|
||||||
The response is modified to include to useful data relating to the number
|
|
||||||
of objects, number of pages, next/previous urls etc. etc.
|
|
||||||
|
|
||||||
The serialised objects are put into `results` on this new, modified
|
|
||||||
response
|
|
||||||
"""
|
|
||||||
|
|
||||||
# We don't want to paginate responses for anything other than GET requests
|
|
||||||
if self.request.method.upper() != 'GET':
|
|
||||||
return self._resource.filter_response(obj)
|
|
||||||
|
|
||||||
paginator = Paginator(obj, self.get_limit())
|
|
||||||
|
|
||||||
try:
|
|
||||||
page_num = int(self.request.GET.get('page', '1'))
|
|
||||||
except ValueError:
|
|
||||||
raise ImmediateResponse(
|
|
||||||
{'detail': 'That page contains no results'},
|
|
||||||
status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
if page_num not in paginator.page_range:
|
|
||||||
raise ImmediateResponse(
|
|
||||||
{'detail': 'That page contains no results'},
|
|
||||||
status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
page = paginator.page(page_num)
|
|
||||||
|
|
||||||
serialized_object_list = self._resource.filter_response(page.object_list)
|
|
||||||
serialized_page_info = self.serialize_page_info(page)
|
|
||||||
|
|
||||||
serialized_page_info['results'] = serialized_object_list
|
|
||||||
|
|
||||||
return serialized_page_info
|
|
|
@ -18,7 +18,6 @@ __all__ = (
|
||||||
'IsUserOrIsAnonReadOnly',
|
'IsUserOrIsAnonReadOnly',
|
||||||
'PerUserThrottling',
|
'PerUserThrottling',
|
||||||
'PerViewThrottling',
|
'PerViewThrottling',
|
||||||
'PerResourceThrottling'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
|
||||||
|
@ -253,16 +252,3 @@ class PerViewThrottling(BaseThrottle):
|
||||||
|
|
||||||
def get_cache_key(self):
|
def get_cache_key(self):
|
||||||
return 'throttle_view_%s' % self.view.__class__.__name__
|
return 'throttle_view_%s' % self.view.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
class PerResourceThrottling(BaseThrottle):
|
|
||||||
"""
|
|
||||||
Limits the rate of API calls that may be used against all views on
|
|
||||||
a given resource.
|
|
||||||
|
|
||||||
The class name of the resource is used as a unique identifier to
|
|
||||||
throttle against.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_cache_key(self):
|
|
||||||
return 'throttle_resource_%s' % self.view.resource.__class__.__name__
|
|
||||||
|
|
|
@ -1,343 +0,0 @@
|
||||||
from django import forms
|
|
||||||
from djangorestframework.response import ImmediateResponse
|
|
||||||
from djangorestframework.serializer import Serializer
|
|
||||||
from djangorestframework.utils import as_tuple
|
|
||||||
|
|
||||||
|
|
||||||
class BaseResource(Serializer):
|
|
||||||
"""
|
|
||||||
Base class for all Resource classes, which simply defines the interface
|
|
||||||
they provide.
|
|
||||||
"""
|
|
||||||
fields = None
|
|
||||||
include = None
|
|
||||||
exclude = None
|
|
||||||
|
|
||||||
def __init__(self, view=None, depth=None, stack=[], **kwargs):
|
|
||||||
super(BaseResource, self).__init__(depth, stack, **kwargs)
|
|
||||||
self.view = view
|
|
||||||
self.request = getattr(view, 'request', None)
|
|
||||||
|
|
||||||
def validate_request(self, data, files=None):
|
|
||||||
"""
|
|
||||||
Given the request content return the cleaned, validated content.
|
|
||||||
Typically raises a :exc:`response.ImmediateResponse` 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.
|
|
||||||
"""
|
|
||||||
return self.serialize(obj)
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# If you wish to override this behaviour,
|
|
||||||
# you should explicitly set the fields attribute on your class.
|
|
||||||
fields = None
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
allow_unknown_form_fields = False
|
|
||||||
"""
|
|
||||||
Flag to check for unknown fields when validating a form. If set to false and
|
|
||||||
we receive request data that is not expected by the form it raises an
|
|
||||||
:exc:`response.ImmediateResponse` with status code 400. If set to true, only
|
|
||||||
expected fields are validated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def validate_request(self, data, files=None):
|
|
||||||
"""
|
|
||||||
Given some content as input return some cleaned, validated content.
|
|
||||||
Raises a :exc:`response.ImmediateResponse` 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
|
|
||||||
if :attr:`self.allow_unknown_form_fields` is ``False``.
|
|
||||||
|
|
||||||
On failure the :exc:`response.ImmediateResponse` 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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
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 {}
|
|
||||||
|
|
||||||
seen_fields_set = set(data.keys())
|
|
||||||
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...
|
|
||||||
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 (self.allow_unknown_form_fields or not unknown_fields):
|
|
||||||
# Validation succeeded...
|
|
||||||
cleaned_data = bound_form.cleaned_data
|
|
||||||
|
|
||||||
# Add in any extra fields to the cleaned content...
|
|
||||||
for key in (allowed_extra_fields_set & seen_fields_set) - set(cleaned_data.keys()):
|
|
||||||
cleaned_data[key] = data[key]
|
|
||||||
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
# Validation failed...
|
|
||||||
detail = {}
|
|
||||||
|
|
||||||
if not bound_form.errors and not unknown_fields:
|
|
||||||
# is_valid() was False, but errors was empty.
|
|
||||||
# If we havn't already done so attempt revalidation with some fake data
|
|
||||||
# to force django to give us an errors dict.
|
|
||||||
if fake_data is None:
|
|
||||||
return self._validate(data, files, allowed_extra_fields, '_fake_data')
|
|
||||||
|
|
||||||
# If we've already set fake_dict and we're still here, fallback gracefully.
|
|
||||||
detail = {u'errors': [u'No content was supplied.']}
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Add any non-field errors
|
|
||||||
if bound_form.non_field_errors():
|
|
||||||
detail[u'errors'] = bound_form.non_field_errors()
|
|
||||||
|
|
||||||
# Add standard field errors
|
|
||||||
field_errors = dict(
|
|
||||||
(key, map(unicode, val))
|
|
||||||
for (key, val)
|
|
||||||
in bound_form.errors.iteritems()
|
|
||||||
if not key.startswith('__')
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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 ImmediateResponse(detail, status=400)
|
|
||||||
|
|
||||||
def get_form_class(self, method=None):
|
|
||||||
"""
|
|
||||||
Returns the form class used to validate this resource.
|
|
||||||
"""
|
|
||||||
# A form on the view overrides a form on the resource.
|
|
||||||
form = getattr(self.view, 'form', None) or self.form
|
|
||||||
|
|
||||||
# Use the requested method or determine the request method
|
|
||||||
if method is None and hasattr(self.view, 'request') and hasattr(self.view, 'method'):
|
|
||||||
method = self.view.method
|
|
||||||
elif method is None and hasattr(self.view, 'request'):
|
|
||||||
method = self.view.request.method
|
|
||||||
|
|
||||||
# A method form on the view or resource overrides the general case.
|
|
||||||
# Method forms are attributes like `get_form` `post_form` `put_form`.
|
|
||||||
if method:
|
|
||||||
form = getattr(self, '%s_form' % method.lower(), form)
|
|
||||||
form = getattr(self.view, '%s_form' % method.lower(), form)
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
def get_bound_form(self, data=None, files=None, method=None):
|
|
||||||
"""
|
|
||||||
Given some content return a Django form bound to that content.
|
|
||||||
If form validation is turned off (:attr:`form` class attribute is :const:`None`) then returns :const:`None`.
|
|
||||||
"""
|
|
||||||
form = self.get_form_class(method)
|
|
||||||
|
|
||||||
if not form:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if data is not None or files is not None:
|
|
||||||
return form(data, files)
|
|
||||||
|
|
||||||
return form()
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
form = None
|
|
||||||
"""
|
|
||||||
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`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = None
|
|
||||||
"""
|
|
||||||
The model class which this resource maps to.
|
|
||||||
|
|
||||||
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 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)``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
exclude = ('id', 'pk')
|
|
||||||
"""
|
|
||||||
The list of fields to exclude. This is only used if :attr:`fields` is not set.
|
|
||||||
"""
|
|
||||||
|
|
||||||
include = ()
|
|
||||||
"""
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Allow :attr:`form` and :attr:`model` attributes set on the
|
|
||||||
:class:`View` to override the :attr:`form` and :attr:`model`
|
|
||||||
attributes set on the :class:`Resource`.
|
|
||||||
"""
|
|
||||||
super(ModelResource, self).__init__(view, depth, stack, **kwargs)
|
|
||||||
|
|
||||||
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.ImmediateResponse` 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.
|
|
||||||
|
|
||||||
On failure the ImmediateResponse 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)
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
form = self.get_form_class(method)
|
|
||||||
|
|
||||||
if not form and self.model:
|
|
||||||
# Fall back to ModelForm which we create on the fly
|
|
||||||
class OnTheFlyModelForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = self.model
|
|
||||||
#fields = tuple(self._model_fields_set)
|
|
||||||
|
|
||||||
form = OnTheFlyModelForm
|
|
||||||
|
|
||||||
# Both form and model not set? Okay bruv, whatevs...
|
|
||||||
if not form:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Instantiate the ModelForm as appropriate
|
|
||||||
if data is not None or files is not None:
|
|
||||||
if issubclass(form, forms.ModelForm) and hasattr(self.view, 'model_instance'):
|
|
||||||
# Bound to an existing model instance
|
|
||||||
return form(data, files, instance=self.view.model_instance)
|
|
||||||
else:
|
|
||||||
return form(data, files)
|
|
||||||
|
|
||||||
return form()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _model_fields_set(self):
|
|
||||||
"""
|
|
||||||
Return a set containing the names of validated fields on the model.
|
|
||||||
"""
|
|
||||||
model_fields = set(field.name for field in self.model._meta.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):
|
|
||||||
"""
|
|
||||||
Returns a set containing the names of validated properties on the model.
|
|
||||||
"""
|
|
||||||
property_fields = set(attr for attr in dir(self.model) if
|
|
||||||
isinstance(getattr(self.model, attr, None), property)
|
|
||||||
and not attr.startswith('_'))
|
|
||||||
|
|
||||||
if self.fields:
|
|
||||||
return property_fields & set(as_tuple(self.fields))
|
|
||||||
|
|
||||||
return property_fields.union(set(as_tuple(self.include))) - set(as_tuple(self.exclude))
|
|
|
@ -1,283 +0,0 @@
|
||||||
"""
|
|
||||||
Customizable serialization.
|
|
||||||
"""
|
|
||||||
from django.db import models
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from django.utils.encoding import smart_unicode, is_protected_type, smart_str
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
import types
|
|
||||||
|
|
||||||
|
|
||||||
# We register serializer classes, so that we can refer to them by their
|
|
||||||
# class names, if there are cyclical serialization heirachys.
|
|
||||||
_serializers = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _field_to_tuple(field):
|
|
||||||
"""
|
|
||||||
Convert an item in the `fields` attribute into a 2-tuple.
|
|
||||||
"""
|
|
||||||
if isinstance(field, (tuple, list)):
|
|
||||||
return (field[0], field[1])
|
|
||||||
return (field, None)
|
|
||||||
|
|
||||||
|
|
||||||
def _fields_to_list(fields):
|
|
||||||
"""
|
|
||||||
Return a list of field tuples.
|
|
||||||
"""
|
|
||||||
return [_field_to_tuple(field) for field in fields or ()]
|
|
||||||
|
|
||||||
|
|
||||||
class _SkipField(Exception):
|
|
||||||
"""
|
|
||||||
Signals that a serialized field should be ignored.
|
|
||||||
We use this mechanism as the default behavior for ensuring
|
|
||||||
that we don't infinitely recurse when dealing with nested data.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class _RegisterSerializer(type):
|
|
||||||
"""
|
|
||||||
Metaclass to register serializers.
|
|
||||||
"""
|
|
||||||
def __new__(cls, name, bases, attrs):
|
|
||||||
# Build the class and register it.
|
|
||||||
ret = super(_RegisterSerializer, cls).__new__(cls, name, bases, attrs)
|
|
||||||
_serializers[name] = ret
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class Serializer(object):
|
|
||||||
"""
|
|
||||||
Converts python objects into plain old native types suitable for
|
|
||||||
serialization. In particular it handles models and querysets.
|
|
||||||
|
|
||||||
The output format is specified by setting a number of attributes
|
|
||||||
on the class.
|
|
||||||
|
|
||||||
You may also override any of the serialization methods, to provide
|
|
||||||
for more flexible behavior.
|
|
||||||
|
|
||||||
Valid output types include anything that may be directly rendered into
|
|
||||||
json, xml etc...
|
|
||||||
"""
|
|
||||||
__metaclass__ = _RegisterSerializer
|
|
||||||
|
|
||||||
fields = ()
|
|
||||||
"""
|
|
||||||
Specify the fields to be serialized on a model or dict.
|
|
||||||
Overrides `include` and `exclude`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
include = ()
|
|
||||||
"""
|
|
||||||
Fields to add to the default set to be serialized on a model/dict.
|
|
||||||
"""
|
|
||||||
|
|
||||||
exclude = ()
|
|
||||||
"""
|
|
||||||
Fields to remove from the default set to be serialized on a model/dict.
|
|
||||||
"""
|
|
||||||
|
|
||||||
rename = {}
|
|
||||||
"""
|
|
||||||
A dict of key->name to use for the field keys.
|
|
||||||
"""
|
|
||||||
|
|
||||||
related_serializer = None
|
|
||||||
"""
|
|
||||||
The default serializer class to use for any related models.
|
|
||||||
"""
|
|
||||||
|
|
||||||
depth = None
|
|
||||||
"""
|
|
||||||
The maximum depth to serialize to, or `None`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, depth=None, stack=[], **kwargs):
|
|
||||||
if depth is not None:
|
|
||||||
self.depth = depth
|
|
||||||
self.stack = stack
|
|
||||||
|
|
||||||
def get_fields(self, obj):
|
|
||||||
fields = self.fields
|
|
||||||
|
|
||||||
# If `fields` is not set, we use the default fields and modify
|
|
||||||
# them with `include` and `exclude`
|
|
||||||
if not fields:
|
|
||||||
default = self.get_default_fields(obj)
|
|
||||||
include = self.include or ()
|
|
||||||
exclude = self.exclude or ()
|
|
||||||
fields = set(default + list(include)) - set(exclude)
|
|
||||||
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def get_default_fields(self, obj):
|
|
||||||
"""
|
|
||||||
Return the default list of field names/keys for a model instance/dict.
|
|
||||||
These are used if `fields` is not given.
|
|
||||||
"""
|
|
||||||
if isinstance(obj, models.Model):
|
|
||||||
opts = obj._meta
|
|
||||||
return [field.name for field in opts.fields + opts.many_to_many]
|
|
||||||
else:
|
|
||||||
return obj.keys()
|
|
||||||
|
|
||||||
def get_related_serializer(self, info):
|
|
||||||
# If an element in `fields` is a 2-tuple of (str, tuple)
|
|
||||||
# then the second element of the tuple is the fields to
|
|
||||||
# set on the related serializer
|
|
||||||
if isinstance(info, (list, tuple)):
|
|
||||||
class OnTheFlySerializer(self.__class__):
|
|
||||||
fields = info
|
|
||||||
return OnTheFlySerializer
|
|
||||||
|
|
||||||
# If an element in `fields` is a 2-tuple of (str, Serializer)
|
|
||||||
# then the second element of the tuple is the Serializer
|
|
||||||
# class to use for that field.
|
|
||||||
elif isinstance(info, type) and issubclass(info, Serializer):
|
|
||||||
return info
|
|
||||||
|
|
||||||
# If an element in `fields` is a 2-tuple of (str, str)
|
|
||||||
# then the second element of the tuple is the name of the Serializer
|
|
||||||
# class to use for that field.
|
|
||||||
#
|
|
||||||
# Black magic to deal with cyclical Serializer dependancies.
|
|
||||||
# Similar to what Django does for cyclically related models.
|
|
||||||
elif isinstance(info, str) and info in _serializers:
|
|
||||||
return _serializers[info]
|
|
||||||
|
|
||||||
# Otherwise use `related_serializer` or fall back to `Serializer`
|
|
||||||
return getattr(self, 'related_serializer') or Serializer
|
|
||||||
|
|
||||||
def serialize_key(self, key):
|
|
||||||
"""
|
|
||||||
Keys serialize to their string value,
|
|
||||||
unless they exist in the `rename` dict.
|
|
||||||
"""
|
|
||||||
return self.rename.get(smart_str(key), smart_str(key))
|
|
||||||
|
|
||||||
def serialize_val(self, key, obj, related_info):
|
|
||||||
"""
|
|
||||||
Convert a model field or dict value into a serializable representation.
|
|
||||||
"""
|
|
||||||
related_serializer = self.get_related_serializer(related_info)
|
|
||||||
|
|
||||||
if self.depth is None:
|
|
||||||
depth = None
|
|
||||||
elif self.depth <= 0:
|
|
||||||
return self.serialize_max_depth(obj)
|
|
||||||
else:
|
|
||||||
depth = self.depth - 1
|
|
||||||
|
|
||||||
if any([obj is elem for elem in self.stack]):
|
|
||||||
return self.serialize_recursion(obj)
|
|
||||||
else:
|
|
||||||
stack = self.stack[:]
|
|
||||||
stack.append(obj)
|
|
||||||
|
|
||||||
return related_serializer(depth=depth, stack=stack).serialize(obj)
|
|
||||||
|
|
||||||
def serialize_max_depth(self, obj):
|
|
||||||
"""
|
|
||||||
Determine how objects should be serialized once `depth` is exceeded.
|
|
||||||
The default behavior is to ignore the field.
|
|
||||||
"""
|
|
||||||
raise _SkipField
|
|
||||||
|
|
||||||
def serialize_recursion(self, obj):
|
|
||||||
"""
|
|
||||||
Determine how objects should be serialized if recursion occurs.
|
|
||||||
The default behavior is to ignore the field.
|
|
||||||
"""
|
|
||||||
raise _SkipField
|
|
||||||
|
|
||||||
def serialize_model(self, instance):
|
|
||||||
"""
|
|
||||||
Given a model instance or dict, serialize it to a dict..
|
|
||||||
"""
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
fields = self.get_fields(instance)
|
|
||||||
|
|
||||||
# serialize each required field
|
|
||||||
for fname, related_info in _fields_to_list(fields):
|
|
||||||
try:
|
|
||||||
# we first check for a method 'fname' on self,
|
|
||||||
# 'fname's signature must be 'def fname(self, instance)'
|
|
||||||
meth = getattr(self, fname, None)
|
|
||||||
if (inspect.ismethod(meth) and
|
|
||||||
len(inspect.getargspec(meth)[0]) == 2):
|
|
||||||
obj = meth(instance)
|
|
||||||
elif hasattr(instance, '__contains__') and fname in instance:
|
|
||||||
# then check for a key 'fname' on the instance
|
|
||||||
obj = instance[fname]
|
|
||||||
elif hasattr(instance, smart_str(fname)):
|
|
||||||
# finally check for an attribute 'fname' on the instance
|
|
||||||
obj = getattr(instance, fname)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key = self.serialize_key(fname)
|
|
||||||
val = self.serialize_val(fname, obj, related_info)
|
|
||||||
data[key] = val
|
|
||||||
except _SkipField:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def serialize_iter(self, obj):
|
|
||||||
"""
|
|
||||||
Convert iterables into a serializable representation.
|
|
||||||
"""
|
|
||||||
return [self.serialize(item) for item in obj]
|
|
||||||
|
|
||||||
def serialize_func(self, obj):
|
|
||||||
"""
|
|
||||||
Convert no-arg methods and functions into a serializable representation.
|
|
||||||
"""
|
|
||||||
return self.serialize(obj())
|
|
||||||
|
|
||||||
def serialize_manager(self, obj):
|
|
||||||
"""
|
|
||||||
Convert a model manager into a serializable representation.
|
|
||||||
"""
|
|
||||||
return self.serialize_iter(obj.all())
|
|
||||||
|
|
||||||
def serialize_fallback(self, obj):
|
|
||||||
"""
|
|
||||||
Convert any unhandled object into a serializable representation.
|
|
||||||
"""
|
|
||||||
return smart_unicode(obj, strings_only=True)
|
|
||||||
|
|
||||||
def serialize(self, obj):
|
|
||||||
"""
|
|
||||||
Convert any object into a serializable representation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if isinstance(obj, (dict, models.Model)):
|
|
||||||
# Model instances & dictionaries
|
|
||||||
return self.serialize_model(obj)
|
|
||||||
elif isinstance(obj, (tuple, list, set, QuerySet, types.GeneratorType)):
|
|
||||||
# basic iterables
|
|
||||||
return self.serialize_iter(obj)
|
|
||||||
elif isinstance(obj, models.Manager):
|
|
||||||
# Manager objects
|
|
||||||
return self.serialize_manager(obj)
|
|
||||||
elif inspect.isfunction(obj) and not inspect.getargspec(obj)[0]:
|
|
||||||
# function with no args
|
|
||||||
return self.serialize_func(obj)
|
|
||||||
elif inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1:
|
|
||||||
# bound method
|
|
||||||
return self.serialize_func(obj)
|
|
||||||
|
|
||||||
# Protected types are passed through as is.
|
|
||||||
# (i.e. Primitives like None, numbers, dates, and Decimals.)
|
|
||||||
if is_protected_type(obj):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# All other values are converted to string.
|
|
||||||
return self.serialize_fallback(obj)
|
|
|
@ -1,35 +1,34 @@
|
||||||
from django.test import TestCase
|
# from django.test import TestCase
|
||||||
from django import forms
|
# from django import forms
|
||||||
|
|
||||||
from djangorestframework.compat import RequestFactory
|
# from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.views import View
|
# from djangorestframework.views import View
|
||||||
from djangorestframework.resources import FormResource
|
# from djangorestframework.response import Response
|
||||||
from djangorestframework.response import Response
|
|
||||||
|
|
||||||
import StringIO
|
# import StringIO
|
||||||
|
|
||||||
class UploadFilesTests(TestCase):
|
|
||||||
"""Check uploading of files"""
|
|
||||||
def setUp(self):
|
|
||||||
self.factory = RequestFactory()
|
|
||||||
|
|
||||||
def test_upload_file(self):
|
# class UploadFilesTests(TestCase):
|
||||||
|
# """Check uploading of files"""
|
||||||
|
# def setUp(self):
|
||||||
|
# self.factory = RequestFactory()
|
||||||
|
|
||||||
class FileForm(forms.Form):
|
# def test_upload_file(self):
|
||||||
file = forms.FileField()
|
|
||||||
|
|
||||||
class MockView(View):
|
# class FileForm(forms.Form):
|
||||||
permissions = ()
|
# file = forms.FileField()
|
||||||
form = FileForm
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
# class MockView(View):
|
||||||
return Response({'FILE_NAME': self.CONTENT['file'].name,
|
# permissions = ()
|
||||||
'FILE_CONTENT': self.CONTENT['file'].read()})
|
# form = FileForm
|
||||||
|
|
||||||
file = StringIO.StringIO('stuff')
|
# def post(self, request, *args, **kwargs):
|
||||||
file.name = 'stuff.txt'
|
# return Response({'FILE_NAME': self.CONTENT['file'].name,
|
||||||
request = self.factory.post('/', {'file': file})
|
# 'FILE_CONTENT': self.CONTENT['file'].read()})
|
||||||
view = MockView.as_view()
|
|
||||||
response = view(request)
|
|
||||||
self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"})
|
|
||||||
|
|
||||||
|
# file = StringIO.StringIO('stuff')
|
||||||
|
# file.name = 'stuff.txt'
|
||||||
|
# request = self.factory.post('/', {'file': file})
|
||||||
|
# view = MockView.as_view()
|
||||||
|
# response = view(request)
|
||||||
|
# self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"})
|
||||||
|
|
|
@ -1,286 +1,285 @@
|
||||||
"""Tests for the mixin module"""
|
# """Tests for the mixin module"""
|
||||||
from django.test import TestCase
|
# from django.test import TestCase
|
||||||
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, ReadModelMixin
|
||||||
from djangorestframework.mixins import CreateModelMixin, PaginatorMixin, ReadModelMixin
|
# from djangorestframework.resources import ModelResource
|
||||||
from djangorestframework.resources import ModelResource
|
# from djangorestframework.response import Response, ImmediateResponse
|
||||||
from djangorestframework.response import Response, ImmediateResponse
|
# from djangorestframework.tests.models import CustomUser
|
||||||
from djangorestframework.tests.models import CustomUser
|
# from djangorestframework.tests.testcases import TestModelsTestCase
|
||||||
from djangorestframework.tests.testcases import TestModelsTestCase
|
# from djangorestframework.views import View
|
||||||
from djangorestframework.views import View
|
|
||||||
|
|
||||||
|
|
||||||
class TestModelRead(TestModelsTestCase):
|
# class TestModelRead(TestModelsTestCase):
|
||||||
"""Tests on ReadModelMixin"""
|
# """Tests on ReadModelMixin"""
|
||||||
|
|
||||||
def setUp(self):
|
# def setUp(self):
|
||||||
super(TestModelRead, self).setUp()
|
# super(TestModelRead, self).setUp()
|
||||||
self.req = RequestFactory()
|
# self.req = RequestFactory()
|
||||||
|
|
||||||
def test_read(self):
|
# def test_read(self):
|
||||||
Group.objects.create(name='other group')
|
# Group.objects.create(name='other group')
|
||||||
group = Group.objects.create(name='my group')
|
# group = Group.objects.create(name='my group')
|
||||||
|
|
||||||
class GroupResource(ModelResource):
|
# class GroupResource(ModelResource):
|
||||||
model = Group
|
# model = Group
|
||||||
|
|
||||||
request = self.req.get('/groups')
|
# request = self.req.get('/groups')
|
||||||
mixin = ReadModelMixin()
|
# mixin = ReadModelMixin()
|
||||||
mixin.resource = GroupResource
|
# mixin.resource = GroupResource
|
||||||
|
|
||||||
response = mixin.get(request, id=group.id)
|
# response = mixin.get(request, id=group.id)
|
||||||
self.assertEquals(group.name, response.raw_content.name)
|
# self.assertEquals(group.name, response.raw_content.name)
|
||||||
|
|
||||||
def test_read_404(self):
|
# def test_read_404(self):
|
||||||
class GroupResource(ModelResource):
|
# class GroupResource(ModelResource):
|
||||||
model = Group
|
# model = Group
|
||||||
|
|
||||||
request = self.req.get('/groups')
|
# request = self.req.get('/groups')
|
||||||
mixin = ReadModelMixin()
|
# mixin = ReadModelMixin()
|
||||||
mixin.resource = GroupResource
|
# mixin.resource = GroupResource
|
||||||
|
|
||||||
self.assertRaises(ImmediateResponse, mixin.get, request, id=12345)
|
# self.assertRaises(ImmediateResponse, mixin.get, request, id=12345)
|
||||||
|
|
||||||
|
|
||||||
class TestModelCreation(TestModelsTestCase):
|
# class TestModelCreation(TestModelsTestCase):
|
||||||
"""Tests on CreateModelMixin"""
|
# """Tests on CreateModelMixin"""
|
||||||
|
|
||||||
def setUp(self):
|
# def setUp(self):
|
||||||
super(TestModelsTestCase, self).setUp()
|
# super(TestModelsTestCase, self).setUp()
|
||||||
self.req = RequestFactory()
|
# self.req = RequestFactory()
|
||||||
|
|
||||||
def test_creation(self):
|
# def test_creation(self):
|
||||||
self.assertEquals(0, Group.objects.count())
|
# self.assertEquals(0, Group.objects.count())
|
||||||
|
|
||||||
class GroupResource(ModelResource):
|
# class GroupResource(ModelResource):
|
||||||
model = Group
|
# model = Group
|
||||||
|
|
||||||
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 = CreateModelMixin()
|
||||||
mixin.resource = GroupResource
|
# mixin.resource = GroupResource
|
||||||
mixin.CONTENT = form_data
|
# mixin.CONTENT = form_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
# response = mixin.post(request)
|
||||||
self.assertEquals(1, Group.objects.count())
|
# self.assertEquals(1, Group.objects.count())
|
||||||
self.assertEquals('foo', response.raw_content.name)
|
# self.assertEquals('foo', response.raw_content.name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation(self):
|
# def test_creation_with_m2m_relation(self):
|
||||||
class UserResource(ModelResource):
|
# class UserResource(ModelResource):
|
||||||
model = User
|
# model = User
|
||||||
|
|
||||||
def url(self, instance):
|
# def url(self, instance):
|
||||||
return "/users/%i" % instance.id
|
# return "/users/%i" % instance.id
|
||||||
|
|
||||||
group = Group(name='foo')
|
# group = Group(name='foo')
|
||||||
group.save()
|
# group.save()
|
||||||
|
|
||||||
form_data = {
|
# form_data = {
|
||||||
'username': 'bar',
|
# 'username': 'bar',
|
||||||
'password': 'baz',
|
# 'password': 'baz',
|
||||||
'groups': [group.id]
|
# 'groups': [group.id]
|
||||||
}
|
# }
|
||||||
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 = CreateModelMixin()
|
||||||
mixin.resource = UserResource
|
# mixin.resource = UserResource
|
||||||
mixin.CONTENT = cleaned_data
|
# mixin.CONTENT = cleaned_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
# response = mixin.post(request)
|
||||||
self.assertEquals(1, User.objects.count())
|
# self.assertEquals(1, User.objects.count())
|
||||||
self.assertEquals(1, response.raw_content.groups.count())
|
# self.assertEquals(1, response.raw_content.groups.count())
|
||||||
self.assertEquals('foo', response.raw_content.groups.all()[0].name)
|
# self.assertEquals('foo', response.raw_content.groups.all()[0].name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation_through(self):
|
# def test_creation_with_m2m_relation_through(self):
|
||||||
"""
|
# """
|
||||||
Tests creation where the m2m relation uses a through table
|
# Tests creation where the m2m relation uses a through table
|
||||||
"""
|
# """
|
||||||
class UserResource(ModelResource):
|
# class UserResource(ModelResource):
|
||||||
model = CustomUser
|
# model = CustomUser
|
||||||
|
|
||||||
def url(self, instance):
|
# def url(self, instance):
|
||||||
return "/customusers/%i" % instance.id
|
# return "/customusers/%i" % instance.id
|
||||||
|
|
||||||
form_data = {'username': 'bar0', 'groups': []}
|
# form_data = {'username': 'bar0', 'groups': []}
|
||||||
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 = CreateModelMixin()
|
||||||
mixin.resource = UserResource
|
# mixin.resource = UserResource
|
||||||
mixin.CONTENT = cleaned_data
|
# mixin.CONTENT = cleaned_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
# response = mixin.post(request)
|
||||||
self.assertEquals(1, CustomUser.objects.count())
|
# self.assertEquals(1, CustomUser.objects.count())
|
||||||
self.assertEquals(0, response.raw_content.groups.count())
|
# self.assertEquals(0, response.raw_content.groups.count())
|
||||||
|
|
||||||
group = Group(name='foo1')
|
# group = Group(name='foo1')
|
||||||
group.save()
|
# group.save()
|
||||||
|
|
||||||
form_data = {'username': 'bar1', 'groups': [group.id]}
|
# form_data = {'username': 'bar1', 'groups': [group.id]}
|
||||||
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 = CreateModelMixin()
|
||||||
mixin.resource = UserResource
|
# mixin.resource = UserResource
|
||||||
mixin.CONTENT = cleaned_data
|
# mixin.CONTENT = cleaned_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
# response = mixin.post(request)
|
||||||
self.assertEquals(2, CustomUser.objects.count())
|
# self.assertEquals(2, CustomUser.objects.count())
|
||||||
self.assertEquals(1, response.raw_content.groups.count())
|
# self.assertEquals(1, response.raw_content.groups.count())
|
||||||
self.assertEquals('foo1', response.raw_content.groups.all()[0].name)
|
# self.assertEquals('foo1', response.raw_content.groups.all()[0].name)
|
||||||
|
|
||||||
group2 = Group(name='foo2')
|
# group2 = Group(name='foo2')
|
||||||
group2.save()
|
# group2.save()
|
||||||
|
|
||||||
form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
|
# form_data = {'username': 'bar2', 'groups': [group.id, group2.id]}
|
||||||
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 = CreateModelMixin()
|
||||||
mixin.resource = UserResource
|
# mixin.resource = UserResource
|
||||||
mixin.CONTENT = cleaned_data
|
# mixin.CONTENT = cleaned_data
|
||||||
|
|
||||||
response = mixin.post(request)
|
# response = mixin.post(request)
|
||||||
self.assertEquals(3, CustomUser.objects.count())
|
# self.assertEquals(3, CustomUser.objects.count())
|
||||||
self.assertEquals(2, response.raw_content.groups.count())
|
# self.assertEquals(2, response.raw_content.groups.count())
|
||||||
self.assertEquals('foo1', response.raw_content.groups.all()[0].name)
|
# self.assertEquals('foo1', response.raw_content.groups.all()[0].name)
|
||||||
self.assertEquals('foo2', response.raw_content.groups.all()[1].name)
|
# self.assertEquals('foo2', response.raw_content.groups.all()[1].name)
|
||||||
|
|
||||||
|
|
||||||
class MockPaginatorView(PaginatorMixin, View):
|
# class MockPaginatorView(PaginatorMixin, View):
|
||||||
total = 60
|
# total = 60
|
||||||
|
|
||||||
def get(self, request):
|
# def get(self, request):
|
||||||
return Response(range(0, self.total))
|
# return Response(range(0, self.total))
|
||||||
|
|
||||||
def post(self, request):
|
# def post(self, request):
|
||||||
return Response({'status': 'OK'}, status=status.HTTP_201_CREATED)
|
# return Response({'status': 'OK'}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
class TestPagination(TestCase):
|
# class TestPagination(TestCase):
|
||||||
def setUp(self):
|
# def setUp(self):
|
||||||
self.req = RequestFactory()
|
# self.req = RequestFactory()
|
||||||
|
|
||||||
def test_default_limit(self):
|
# def test_default_limit(self):
|
||||||
""" Tests if pagination works without overwriting the limit """
|
# """ Tests if pagination works without overwriting the limit """
|
||||||
request = self.req.get('/paginator')
|
# request = self.req.get('/paginator')
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
# self.assertEqual(MockPaginatorView.total, content['total'])
|
||||||
self.assertEqual(MockPaginatorView.limit, content['per_page'])
|
# self.assertEqual(MockPaginatorView.limit, content['per_page'])
|
||||||
|
|
||||||
self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
|
# self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
|
||||||
|
|
||||||
def test_overwriting_limit(self):
|
# def test_overwriting_limit(self):
|
||||||
""" Tests if the limit can be overwritten """
|
# """ Tests if the limit can be overwritten """
|
||||||
limit = 10
|
# limit = 10
|
||||||
|
|
||||||
request = self.req.get('/paginator')
|
# request = self.req.get('/paginator')
|
||||||
response = MockPaginatorView.as_view(limit=limit)(request)
|
# response = MockPaginatorView.as_view(limit=limit)(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(content['per_page'], limit)
|
# self.assertEqual(content['per_page'], limit)
|
||||||
|
|
||||||
self.assertEqual(range(0, limit), content['results'])
|
# self.assertEqual(range(0, limit), content['results'])
|
||||||
|
|
||||||
def test_limit_param(self):
|
# def test_limit_param(self):
|
||||||
""" Tests if the client can set the limit """
|
# """ Tests if the client can set the limit """
|
||||||
from math import ceil
|
# from math import ceil
|
||||||
|
|
||||||
limit = 5
|
# limit = 5
|
||||||
num_pages = int(ceil(MockPaginatorView.total / float(limit)))
|
# num_pages = int(ceil(MockPaginatorView.total / float(limit)))
|
||||||
|
|
||||||
request = self.req.get('/paginator/?limit=%d' % limit)
|
# request = self.req.get('/paginator/?limit=%d' % limit)
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
# self.assertEqual(MockPaginatorView.total, content['total'])
|
||||||
self.assertEqual(limit, content['per_page'])
|
# self.assertEqual(limit, content['per_page'])
|
||||||
self.assertEqual(num_pages, content['pages'])
|
# self.assertEqual(num_pages, content['pages'])
|
||||||
|
|
||||||
def test_exceeding_limit(self):
|
# def test_exceeding_limit(self):
|
||||||
""" Makes sure the client cannot exceed the default limit """
|
# """ Makes sure the client cannot exceed the default limit """
|
||||||
from math import ceil
|
# from math import ceil
|
||||||
|
|
||||||
limit = MockPaginatorView.limit + 10
|
# limit = MockPaginatorView.limit + 10
|
||||||
num_pages = int(ceil(MockPaginatorView.total / float(limit)))
|
# num_pages = int(ceil(MockPaginatorView.total / float(limit)))
|
||||||
|
|
||||||
request = self.req.get('/paginator/?limit=%d' % limit)
|
# request = self.req.get('/paginator/?limit=%d' % limit)
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(MockPaginatorView.total, content['total'])
|
# self.assertEqual(MockPaginatorView.total, content['total'])
|
||||||
self.assertNotEqual(limit, content['per_page'])
|
# self.assertNotEqual(limit, content['per_page'])
|
||||||
self.assertNotEqual(num_pages, content['pages'])
|
# self.assertNotEqual(num_pages, content['pages'])
|
||||||
self.assertEqual(MockPaginatorView.limit, content['per_page'])
|
# self.assertEqual(MockPaginatorView.limit, content['per_page'])
|
||||||
|
|
||||||
def test_only_works_for_get(self):
|
# def test_only_works_for_get(self):
|
||||||
""" Pagination should only work for GET requests """
|
# """ Pagination should only work for GET requests """
|
||||||
request = self.req.post('/paginator', data={'content': 'spam'})
|
# request = self.req.post('/paginator', data={'content': 'spam'})
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
# self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
self.assertEqual(None, content.get('per_page'))
|
# self.assertEqual(None, content.get('per_page'))
|
||||||
self.assertEqual('OK', content['status'])
|
# self.assertEqual('OK', content['status'])
|
||||||
|
|
||||||
def test_non_int_page(self):
|
# def test_non_int_page(self):
|
||||||
""" Tests that it can handle invalid values """
|
# """ Tests that it can handle invalid values """
|
||||||
request = self.req.get('/paginator/?page=spam')
|
# request = self.req.get('/paginator/?page=spam')
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
def test_page_range(self):
|
# def test_page_range(self):
|
||||||
""" Tests that the page range is handle correctly """
|
# """ Tests that the page range is handle correctly """
|
||||||
request = self.req.get('/paginator/?page=0')
|
# request = self.req.get('/paginator/?page=0')
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
request = self.req.get('/paginator/')
|
# request = self.req.get('/paginator/')
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
|
# self.assertEqual(range(0, MockPaginatorView.limit), content['results'])
|
||||||
|
|
||||||
num_pages = content['pages']
|
# num_pages = content['pages']
|
||||||
|
|
||||||
request = self.req.get('/paginator/?page=%d' % num_pages)
|
# request = self.req.get('/paginator/?page=%d' % num_pages)
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results'])
|
# self.assertEqual(range(MockPaginatorView.limit*(num_pages-1), MockPaginatorView.total), content['results'])
|
||||||
|
|
||||||
request = self.req.get('/paginator/?page=%d' % (num_pages + 1,))
|
# request = self.req.get('/paginator/?page=%d' % (num_pages + 1,))
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
# self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
def test_existing_query_parameters_are_preserved(self):
|
# def test_existing_query_parameters_are_preserved(self):
|
||||||
""" Tests that existing query parameters are preserved when
|
# """ Tests that existing query parameters are preserved when
|
||||||
generating next/previous page links """
|
# generating next/previous page links """
|
||||||
request = self.req.get('/paginator/?foo=bar&another=something')
|
# request = self.req.get('/paginator/?foo=bar&another=something')
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
# self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertTrue('foo=bar' in content['next'])
|
# self.assertTrue('foo=bar' in content['next'])
|
||||||
self.assertTrue('another=something' in content['next'])
|
# self.assertTrue('another=something' in content['next'])
|
||||||
self.assertTrue('page=2' in content['next'])
|
# self.assertTrue('page=2' in content['next'])
|
||||||
|
|
||||||
def test_duplicate_parameters_are_not_created(self):
|
# def test_duplicate_parameters_are_not_created(self):
|
||||||
""" Regression: ensure duplicate "page" parameters are not added to
|
# """ Regression: ensure duplicate "page" parameters are not added to
|
||||||
paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """
|
# paginated URLs. So page 1 should contain ?page=2, not ?page=1&page=2 """
|
||||||
request = self.req.get('/paginator/?page=1')
|
# request = self.req.get('/paginator/?page=1')
|
||||||
response = MockPaginatorView.as_view()(request)
|
# response = MockPaginatorView.as_view()(request)
|
||||||
content = response.raw_content
|
# content = response.raw_content
|
||||||
self.assertTrue('page=2' in content['next'])
|
# self.assertTrue('page=2' in content['next'])
|
||||||
self.assertFalse('page=1' in content['next'])
|
# self.assertFalse('page=1' in content['next'])
|
||||||
|
|
|
@ -1,90 +1,90 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
# from django.conf.urls.defaults import patterns, url
|
||||||
from django.forms import ModelForm
|
# from django.forms import ModelForm
|
||||||
from django.contrib.auth.models import Group, User
|
# from django.contrib.auth.models import Group, User
|
||||||
from djangorestframework.resources import ModelResource
|
# from djangorestframework.resources import ModelResource
|
||||||
from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
# from djangorestframework.views import ListOrCreateModelView, InstanceModelView
|
||||||
from djangorestframework.tests.models import CustomUser
|
# from djangorestframework.tests.models import CustomUser
|
||||||
from djangorestframework.tests.testcases import TestModelsTestCase
|
# from djangorestframework.tests.testcases import TestModelsTestCase
|
||||||
|
|
||||||
|
|
||||||
class GroupResource(ModelResource):
|
# class GroupResource(ModelResource):
|
||||||
model = Group
|
# model = Group
|
||||||
|
|
||||||
|
|
||||||
class UserForm(ModelForm):
|
# class UserForm(ModelForm):
|
||||||
class Meta:
|
# class Meta:
|
||||||
model = User
|
# model = User
|
||||||
exclude = ('last_login', 'date_joined')
|
# exclude = ('last_login', 'date_joined')
|
||||||
|
|
||||||
|
|
||||||
class UserResource(ModelResource):
|
# class UserResource(ModelResource):
|
||||||
model = User
|
# model = User
|
||||||
form = UserForm
|
# form = UserForm
|
||||||
|
|
||||||
|
|
||||||
class CustomUserResource(ModelResource):
|
# class CustomUserResource(ModelResource):
|
||||||
model = CustomUser
|
# model = CustomUser
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
# urlpatterns = patterns('',
|
||||||
url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'),
|
# url(r'^users/$', ListOrCreateModelView.as_view(resource=UserResource), name='users'),
|
||||||
url(r'^users/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=UserResource)),
|
# url(r'^users/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=UserResource)),
|
||||||
url(r'^customusers/$', ListOrCreateModelView.as_view(resource=CustomUserResource), name='customusers'),
|
# url(r'^customusers/$', ListOrCreateModelView.as_view(resource=CustomUserResource), name='customusers'),
|
||||||
url(r'^customusers/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=CustomUserResource)),
|
# url(r'^customusers/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=CustomUserResource)),
|
||||||
url(r'^groups/$', ListOrCreateModelView.as_view(resource=GroupResource), name='groups'),
|
# url(r'^groups/$', ListOrCreateModelView.as_view(resource=GroupResource), name='groups'),
|
||||||
url(r'^groups/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=GroupResource)),
|
# url(r'^groups/(?P<id>[0-9]+)/$', InstanceModelView.as_view(resource=GroupResource)),
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
class ModelViewTests(TestModelsTestCase):
|
# class ModelViewTests(TestModelsTestCase):
|
||||||
"""Test the model views djangorestframework provides"""
|
# """Test the model views djangorestframework provides"""
|
||||||
urls = 'djangorestframework.tests.modelviews'
|
# urls = 'djangorestframework.tests.modelviews'
|
||||||
|
|
||||||
def test_creation(self):
|
# def test_creation(self):
|
||||||
"""Ensure that a model object can be created"""
|
# """Ensure that a model object can be created"""
|
||||||
self.assertEqual(0, Group.objects.count())
|
# self.assertEqual(0, Group.objects.count())
|
||||||
|
|
||||||
response = self.client.post('/groups/', {'name': 'foo'})
|
# response = self.client.post('/groups/', {'name': 'foo'})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 201)
|
# self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(1, Group.objects.count())
|
# self.assertEqual(1, Group.objects.count())
|
||||||
self.assertEqual('foo', Group.objects.all()[0].name)
|
# self.assertEqual('foo', Group.objects.all()[0].name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation(self):
|
# def test_creation_with_m2m_relation(self):
|
||||||
"""Ensure that a model object with a m2m relation can be created"""
|
# """Ensure that a model object with a m2m relation can be created"""
|
||||||
group = Group(name='foo')
|
# group = Group(name='foo')
|
||||||
group.save()
|
# group.save()
|
||||||
self.assertEqual(0, User.objects.count())
|
# self.assertEqual(0, User.objects.count())
|
||||||
|
|
||||||
response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]})
|
# response = self.client.post('/users/', {'username': 'bar', 'password': 'baz', 'groups': [group.id]})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 201)
|
# self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(1, User.objects.count())
|
# self.assertEqual(1, User.objects.count())
|
||||||
|
|
||||||
user = User.objects.all()[0]
|
# user = User.objects.all()[0]
|
||||||
self.assertEqual('bar', user.username)
|
# self.assertEqual('bar', user.username)
|
||||||
self.assertEqual('baz', user.password)
|
# self.assertEqual('baz', user.password)
|
||||||
self.assertEqual(1, user.groups.count())
|
# self.assertEqual(1, user.groups.count())
|
||||||
|
|
||||||
group = user.groups.all()[0]
|
# group = user.groups.all()[0]
|
||||||
self.assertEqual('foo', group.name)
|
# self.assertEqual('foo', group.name)
|
||||||
|
|
||||||
def test_creation_with_m2m_relation_through(self):
|
# def test_creation_with_m2m_relation_through(self):
|
||||||
"""
|
# """
|
||||||
Ensure that a model object with a m2m relation can be created where that
|
# Ensure that a model object with a m2m relation can be created where that
|
||||||
relation uses a through table
|
# relation uses a through table
|
||||||
"""
|
# """
|
||||||
group = Group(name='foo')
|
# group = Group(name='foo')
|
||||||
group.save()
|
# group.save()
|
||||||
self.assertEqual(0, User.objects.count())
|
# self.assertEqual(0, User.objects.count())
|
||||||
|
|
||||||
response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]})
|
# response = self.client.post('/customusers/', {'username': 'bar', 'groups': [group.id]})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 201)
|
# self.assertEqual(response.status_code, 201)
|
||||||
self.assertEqual(1, CustomUser.objects.count())
|
# self.assertEqual(1, CustomUser.objects.count())
|
||||||
|
|
||||||
user = CustomUser.objects.all()[0]
|
# user = CustomUser.objects.all()[0]
|
||||||
self.assertEqual('bar', user.username)
|
# self.assertEqual('bar', user.username)
|
||||||
self.assertEqual(1, user.groups.count())
|
# self.assertEqual(1, user.groups.count())
|
||||||
|
|
||||||
group = user.groups.all()[0]
|
# group = user.groups.all()[0]
|
||||||
self.assertEqual('foo', group.name)
|
# self.assertEqual('foo', group.name)
|
||||||
|
|
|
@ -1,160 +1,161 @@
|
||||||
"""Tests for the resource module"""
|
# """Tests for the resource module"""
|
||||||
from django.db import models
|
# from django.db import models
|
||||||
from django.test import TestCase
|
# from django.test import TestCase
|
||||||
from django.utils.translation import ugettext_lazy
|
# from django.utils.translation import ugettext_lazy
|
||||||
from djangorestframework.serializer import Serializer
|
# from djangorestframework.serializer import Serializer
|
||||||
|
|
||||||
import datetime
|
# import datetime
|
||||||
import decimal
|
# import decimal
|
||||||
|
|
||||||
class TestObjectToData(TestCase):
|
|
||||||
"""
|
|
||||||
Tests for the Serializer class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.serializer = Serializer()
|
|
||||||
self.serialize = self.serializer.serialize
|
|
||||||
|
|
||||||
def test_decimal(self):
|
|
||||||
"""Decimals need to be converted to a string representation."""
|
|
||||||
self.assertEquals(self.serialize(decimal.Decimal('1.5')), decimal.Decimal('1.5'))
|
|
||||||
|
|
||||||
def test_function(self):
|
|
||||||
"""Functions with no arguments should be called."""
|
|
||||||
def foo():
|
|
||||||
return 1
|
|
||||||
self.assertEquals(self.serialize(foo), 1)
|
|
||||||
|
|
||||||
def test_method(self):
|
|
||||||
"""Methods with only a ``self`` argument should be called."""
|
|
||||||
class Foo(object):
|
|
||||||
def foo(self):
|
|
||||||
return 1
|
|
||||||
self.assertEquals(self.serialize(Foo().foo), 1)
|
|
||||||
|
|
||||||
def test_datetime(self):
|
|
||||||
"""datetime objects are left as-is."""
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
self.assertEquals(self.serialize(now), now)
|
|
||||||
|
|
||||||
def test_dict_method_name_collision(self):
|
|
||||||
"""dict with key that collides with dict method name"""
|
|
||||||
self.assertEquals(self.serialize({'items': 'foo'}), {'items': u'foo'})
|
|
||||||
self.assertEquals(self.serialize({'keys': 'foo'}), {'keys': u'foo'})
|
|
||||||
self.assertEquals(self.serialize({'values': 'foo'}), {'values': u'foo'})
|
|
||||||
|
|
||||||
def test_ugettext_lazy(self):
|
|
||||||
self.assertEquals(self.serialize(ugettext_lazy('foobar')), u'foobar')
|
|
||||||
|
|
||||||
|
|
||||||
class TestFieldNesting(TestCase):
|
# class TestObjectToData(TestCase):
|
||||||
"""
|
# """
|
||||||
Test nesting the fields in the Serializer class
|
# Tests for the Serializer class.
|
||||||
"""
|
# """
|
||||||
def setUp(self):
|
|
||||||
self.serializer = Serializer()
|
|
||||||
self.serialize = self.serializer.serialize
|
|
||||||
|
|
||||||
class M1(models.Model):
|
# def setUp(self):
|
||||||
field1 = models.CharField(max_length=256)
|
# self.serializer = Serializer()
|
||||||
field2 = models.CharField(max_length=256)
|
# self.serialize = self.serializer.serialize
|
||||||
|
|
||||||
class M2(models.Model):
|
# def test_decimal(self):
|
||||||
field = models.OneToOneField(M1)
|
# """Decimals need to be converted to a string representation."""
|
||||||
|
# self.assertEquals(self.serialize(decimal.Decimal('1.5')), decimal.Decimal('1.5'))
|
||||||
|
|
||||||
class M3(models.Model):
|
# def test_function(self):
|
||||||
field = models.ForeignKey(M1)
|
# """Functions with no arguments should be called."""
|
||||||
|
# def foo():
|
||||||
|
# return 1
|
||||||
|
# self.assertEquals(self.serialize(foo), 1)
|
||||||
|
|
||||||
self.m1 = M1(field1='foo', field2='bar')
|
# def test_method(self):
|
||||||
self.m2 = M2(field=self.m1)
|
# """Methods with only a ``self`` argument should be called."""
|
||||||
self.m3 = M3(field=self.m1)
|
# class Foo(object):
|
||||||
|
# def foo(self):
|
||||||
|
# return 1
|
||||||
|
# self.assertEquals(self.serialize(Foo().foo), 1)
|
||||||
|
|
||||||
|
# def test_datetime(self):
|
||||||
|
# """datetime objects are left as-is."""
|
||||||
|
# now = datetime.datetime.now()
|
||||||
|
# self.assertEquals(self.serialize(now), now)
|
||||||
|
|
||||||
|
# def test_dict_method_name_collision(self):
|
||||||
|
# """dict with key that collides with dict method name"""
|
||||||
|
# self.assertEquals(self.serialize({'items': 'foo'}), {'items': u'foo'})
|
||||||
|
# self.assertEquals(self.serialize({'keys': 'foo'}), {'keys': u'foo'})
|
||||||
|
# self.assertEquals(self.serialize({'values': 'foo'}), {'values': u'foo'})
|
||||||
|
|
||||||
|
# def test_ugettext_lazy(self):
|
||||||
|
# self.assertEquals(self.serialize(ugettext_lazy('foobar')), u'foobar')
|
||||||
|
|
||||||
|
|
||||||
def test_tuple_nesting(self):
|
# class TestFieldNesting(TestCase):
|
||||||
"""
|
# """
|
||||||
Test tuple nesting on `fields` attr
|
# Test nesting the fields in the Serializer class
|
||||||
"""
|
# """
|
||||||
class SerializerM2(Serializer):
|
# def setUp(self):
|
||||||
fields = (('field', ('field1',)),)
|
# self.serializer = Serializer()
|
||||||
|
# self.serialize = self.serializer.serialize
|
||||||
|
|
||||||
class SerializerM3(Serializer):
|
# class M1(models.Model):
|
||||||
fields = (('field', ('field2',)),)
|
# field1 = models.CharField(max_length=256)
|
||||||
|
# field2 = models.CharField(max_length=256)
|
||||||
|
|
||||||
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
# class M2(models.Model):
|
||||||
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
# field = models.OneToOneField(M1)
|
||||||
|
|
||||||
|
# class M3(models.Model):
|
||||||
|
# field = models.ForeignKey(M1)
|
||||||
|
|
||||||
|
# self.m1 = M1(field1='foo', field2='bar')
|
||||||
|
# self.m2 = M2(field=self.m1)
|
||||||
|
# self.m3 = M3(field=self.m1)
|
||||||
|
|
||||||
|
|
||||||
def test_serializer_class_nesting(self):
|
# def test_tuple_nesting(self):
|
||||||
"""
|
# """
|
||||||
Test related model serialization
|
# Test tuple nesting on `fields` attr
|
||||||
"""
|
# """
|
||||||
class NestedM2(Serializer):
|
# class SerializerM2(Serializer):
|
||||||
fields = ('field1', )
|
# fields = (('field', ('field1',)),)
|
||||||
|
|
||||||
class NestedM3(Serializer):
|
# class SerializerM3(Serializer):
|
||||||
fields = ('field2', )
|
# fields = (('field', ('field2',)),)
|
||||||
|
|
||||||
class SerializerM2(Serializer):
|
# self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
||||||
fields = [('field', NestedM2)]
|
# self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
||||||
|
|
||||||
class SerializerM3(Serializer):
|
|
||||||
fields = [('field', NestedM3)]
|
|
||||||
|
|
||||||
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
# def test_serializer_class_nesting(self):
|
||||||
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
# """
|
||||||
|
# Test related model serialization
|
||||||
|
# """
|
||||||
|
# class NestedM2(Serializer):
|
||||||
|
# fields = ('field1', )
|
||||||
|
|
||||||
def test_serializer_no_fields(self):
|
# class NestedM3(Serializer):
|
||||||
"""
|
# fields = ('field2', )
|
||||||
Test related serializer works when the fields attr isn't present. Fix for
|
|
||||||
#178.
|
|
||||||
"""
|
|
||||||
class NestedM2(Serializer):
|
|
||||||
fields = ('field1', )
|
|
||||||
|
|
||||||
class NestedM3(Serializer):
|
# class SerializerM2(Serializer):
|
||||||
fields = ('field2', )
|
# fields = [('field', NestedM2)]
|
||||||
|
|
||||||
class SerializerM2(Serializer):
|
# class SerializerM3(Serializer):
|
||||||
include = [('field', NestedM2)]
|
# fields = [('field', NestedM3)]
|
||||||
exclude = ('id', )
|
|
||||||
|
|
||||||
class SerializerM3(Serializer):
|
# self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
||||||
fields = [('field', NestedM3)]
|
# self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
||||||
|
|
||||||
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
# def test_serializer_no_fields(self):
|
||||||
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
# """
|
||||||
|
# Test related serializer works when the fields attr isn't present. Fix for
|
||||||
|
# #178.
|
||||||
|
# """
|
||||||
|
# class NestedM2(Serializer):
|
||||||
|
# fields = ('field1', )
|
||||||
|
|
||||||
def test_serializer_classname_nesting(self):
|
# class NestedM3(Serializer):
|
||||||
"""
|
# fields = ('field2', )
|
||||||
Test related model serialization
|
|
||||||
"""
|
|
||||||
class SerializerM2(Serializer):
|
|
||||||
fields = [('field', 'NestedM2')]
|
|
||||||
|
|
||||||
class SerializerM3(Serializer):
|
# class SerializerM2(Serializer):
|
||||||
fields = [('field', 'NestedM3')]
|
# include = [('field', NestedM2)]
|
||||||
|
# exclude = ('id', )
|
||||||
|
|
||||||
class NestedM2(Serializer):
|
# class SerializerM3(Serializer):
|
||||||
fields = ('field1', )
|
# fields = [('field', NestedM3)]
|
||||||
|
|
||||||
class NestedM3(Serializer):
|
# self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
||||||
fields = ('field2', )
|
# self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
||||||
|
|
||||||
self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
# def test_serializer_classname_nesting(self):
|
||||||
self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
# """
|
||||||
|
# Test related model serialization
|
||||||
|
# """
|
||||||
|
# class SerializerM2(Serializer):
|
||||||
|
# fields = [('field', 'NestedM2')]
|
||||||
|
|
||||||
def test_serializer_overridden_hook_method(self):
|
# class SerializerM3(Serializer):
|
||||||
"""
|
# fields = [('field', 'NestedM3')]
|
||||||
Test serializing a model instance which overrides a class method on the
|
|
||||||
serializer. Checks for correct behaviour in odd edge case.
|
|
||||||
"""
|
|
||||||
class SerializerM2(Serializer):
|
|
||||||
fields = ('overridden', )
|
|
||||||
|
|
||||||
def overridden(self):
|
# class NestedM2(Serializer):
|
||||||
return False
|
# fields = ('field1', )
|
||||||
|
|
||||||
self.m2.overridden = True
|
# class NestedM3(Serializer):
|
||||||
self.assertEqual(SerializerM2().serialize_model(self.m2),
|
# fields = ('field2', )
|
||||||
{'overridden': True})
|
|
||||||
|
# self.assertEqual(SerializerM2().serialize(self.m2), {'field': {'field1': u'foo'}})
|
||||||
|
# self.assertEqual(SerializerM3().serialize(self.m3), {'field': {'field2': u'bar'}})
|
||||||
|
|
||||||
|
# def test_serializer_overridden_hook_method(self):
|
||||||
|
# """
|
||||||
|
# Test serializing a model instance which overrides a class method on the
|
||||||
|
# serializer. Checks for correct behaviour in odd edge case.
|
||||||
|
# """
|
||||||
|
# class SerializerM2(Serializer):
|
||||||
|
# fields = ('overridden', )
|
||||||
|
|
||||||
|
# def overridden(self):
|
||||||
|
# return False
|
||||||
|
|
||||||
|
# self.m2.overridden = True
|
||||||
|
# self.assertEqual(SerializerM2().serialize_model(self.m2),
|
||||||
|
# {'overridden': True})
|
||||||
|
|
|
@ -8,8 +8,7 @@ from django.core.cache import cache
|
||||||
|
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.views import View
|
from djangorestframework.views import View
|
||||||
from djangorestframework.permissions import PerUserThrottling, PerViewThrottling, PerResourceThrottling
|
from djangorestframework.permissions import PerUserThrottling, PerViewThrottling
|
||||||
from djangorestframework.resources import FormResource
|
|
||||||
from djangorestframework.response import Response
|
from djangorestframework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,11 +24,6 @@ class MockView_PerViewThrottling(MockView):
|
||||||
permission_classes = (PerViewThrottling,)
|
permission_classes = (PerViewThrottling,)
|
||||||
|
|
||||||
|
|
||||||
class MockView_PerResourceThrottling(MockView):
|
|
||||||
permission_classes = (PerResourceThrottling,)
|
|
||||||
resource = FormResource
|
|
||||||
|
|
||||||
|
|
||||||
class MockView_MinuteThrottling(MockView):
|
class MockView_MinuteThrottling(MockView):
|
||||||
throttle = '3/min'
|
throttle = '3/min'
|
||||||
|
|
||||||
|
@ -98,12 +92,6 @@ class ThrottlingTests(TestCase):
|
||||||
"""
|
"""
|
||||||
self.ensure_is_throttled(MockView_PerViewThrottling, 503)
|
self.ensure_is_throttled(MockView_PerViewThrottling, 503)
|
||||||
|
|
||||||
def test_request_throttling_is_per_resource(self):
|
|
||||||
"""
|
|
||||||
Ensure request rate is limited globally per Resource for PerResourceThrottles
|
|
||||||
"""
|
|
||||||
self.ensure_is_throttled(MockView_PerResourceThrottling, 503)
|
|
||||||
|
|
||||||
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
|
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
|
||||||
"""
|
"""
|
||||||
Ensure the response returns an X-Throttle field with status and next attributes
|
Ensure the response returns an X-Throttle field with status and next attributes
|
||||||
|
|
|
@ -1,330 +1,329 @@
|
||||||
from django import forms
|
# from django import forms
|
||||||
from django.db import models
|
# from django.db import models
|
||||||
from django.test import TestCase
|
# from django.test import TestCase
|
||||||
from djangorestframework.resources import FormResource, ModelResource
|
# from djangorestframework.response import ImmediateResponse
|
||||||
from djangorestframework.response import ImmediateResponse
|
# from djangorestframework.views import View
|
||||||
from djangorestframework.views import View
|
|
||||||
|
|
||||||
|
|
||||||
class TestDisabledValidations(TestCase):
|
# class TestDisabledValidations(TestCase):
|
||||||
"""Tests on FormValidator with validation disabled by setting form to None"""
|
# """Tests on FormValidator with validation disabled by setting form to None"""
|
||||||
|
|
||||||
def test_disabled_form_validator_returns_content_unchanged(self):
|
|
||||||
"""If the view's form attribute is None then FormValidator(view).validate_request(content, None)
|
|
||||||
should just return the content unmodified."""
|
|
||||||
class DisabledFormResource(FormResource):
|
|
||||||
form = None
|
|
||||||
|
|
||||||
class MockView(View):
|
|
||||||
resource = DisabledFormResource
|
|
||||||
|
|
||||||
view = MockView()
|
|
||||||
content = {'qwerty': 'uiop'}
|
|
||||||
self.assertEqual(FormResource(view).validate_request(content, None), content)
|
|
||||||
|
|
||||||
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
|
||||||
"""If the view's form attribute is None on then
|
|
||||||
FormValidator(view).get_bound_form(content) should just return None."""
|
|
||||||
class DisabledFormResource(FormResource):
|
|
||||||
form = None
|
|
||||||
|
|
||||||
class MockView(View):
|
|
||||||
resource = DisabledFormResource
|
|
||||||
|
|
||||||
view = MockView()
|
|
||||||
content = {'qwerty': 'uiop'}
|
|
||||||
self.assertEqual(FormResource(view).get_bound_form(content), None)
|
|
||||||
|
|
||||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
|
||||||
"""If the view's form is None and does not have a Resource with a model set then
|
|
||||||
ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
|
||||||
|
|
||||||
class DisabledModelFormView(View):
|
|
||||||
resource = ModelResource
|
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
|
||||||
content = {'qwerty': 'uiop'}
|
|
||||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
|
||||||
|
|
||||||
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
|
||||||
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
|
||||||
class DisabledModelFormView(View):
|
|
||||||
resource = ModelResource
|
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
|
||||||
content = {'qwerty': 'uiop'}
|
|
||||||
self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNonFieldErrors(TestCase):
|
|
||||||
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
|
||||||
|
|
||||||
def test_validate_failed_due_to_non_field_error_returns_appropriate_message(self):
|
|
||||||
"""If validation fails with a non-field error, ensure the response a non-field error"""
|
|
||||||
class MockForm(forms.Form):
|
|
||||||
field1 = forms.CharField(required=False)
|
|
||||||
field2 = forms.CharField(required=False)
|
|
||||||
ERROR_TEXT = 'You may not supply both field1 and field2'
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
|
||||||
raise forms.ValidationError(self.ERROR_TEXT)
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
class MockResource(FormResource):
|
|
||||||
form = MockForm
|
|
||||||
|
|
||||||
class MockView(View):
|
|
||||||
pass
|
|
||||||
|
|
||||||
view = MockView()
|
|
||||||
content = {'field1': 'example1', 'field2': 'example2'}
|
|
||||||
try:
|
|
||||||
MockResource(view).validate_request(content, None)
|
|
||||||
except ImmediateResponse, exc:
|
|
||||||
response = exc.response
|
|
||||||
self.assertEqual(response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
|
||||||
else:
|
|
||||||
self.fail('ImmediateResponse was not raised')
|
|
||||||
|
|
||||||
|
|
||||||
class TestFormValidation(TestCase):
|
|
||||||
"""Tests which check basic form validation.
|
|
||||||
Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
|
|
||||||
(ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
|
|
||||||
def setUp(self):
|
|
||||||
class MockForm(forms.Form):
|
|
||||||
qwerty = forms.CharField(required=True)
|
|
||||||
|
|
||||||
class MockFormResource(FormResource):
|
|
||||||
form = MockForm
|
|
||||||
|
|
||||||
class MockModelResource(ModelResource):
|
|
||||||
form = MockForm
|
|
||||||
|
|
||||||
class MockFormView(View):
|
|
||||||
resource = MockFormResource
|
|
||||||
|
|
||||||
class MockModelFormView(View):
|
|
||||||
resource = MockModelResource
|
|
||||||
|
|
||||||
self.MockFormResource = MockFormResource
|
|
||||||
self.MockModelResource = MockModelResource
|
|
||||||
self.MockFormView = MockFormView
|
|
||||||
self.MockModelFormView = MockModelFormView
|
|
||||||
|
|
||||||
def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
|
||||||
"""If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
|
||||||
content = {'qwerty': 'uiop'}
|
|
||||||
self.assertEqual(validator.validate_request(content, None), content)
|
|
||||||
|
|
||||||
def validation_failure_raises_response_exception(self, validator):
|
|
||||||
"""If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
|
||||||
content = {}
|
|
||||||
self.assertRaises(ImmediateResponse, validator.validate_request, content, None)
|
|
||||||
|
|
||||||
def validation_does_not_allow_extra_fields_by_default(self, validator):
|
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
|
||||||
self.assertRaises(ImmediateResponse, validator.validate_request, content, None)
|
|
||||||
|
|
||||||
def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
|
||||||
"""If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
|
||||||
validator._validate(content, None, allowed_extra_fields=('extra',))
|
|
||||||
|
|
||||||
def validation_allows_unknown_fields_if_explicitly_allowed(self, validator):
|
|
||||||
"""If we set ``unknown_form_fields`` on the form resource, then don't
|
|
||||||
raise errors on unexpected request data"""
|
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
|
||||||
validator.allow_unknown_form_fields = True
|
|
||||||
self.assertEqual({'qwerty': u'uiop'},
|
|
||||||
validator.validate_request(content, None),
|
|
||||||
"Resource didn't accept unknown fields.")
|
|
||||||
validator.allow_unknown_form_fields = False
|
|
||||||
|
|
||||||
def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
|
||||||
"""If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
|
|
||||||
content = {'qwerty': 'uiop'}
|
|
||||||
self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content)
|
|
||||||
|
|
||||||
def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
|
|
||||||
"""If validation fails due to no content, ensure the response contains a single non-field error"""
|
|
||||||
content = {}
|
|
||||||
try:
|
|
||||||
validator.validate_request(content, None)
|
|
||||||
except ImmediateResponse, exc:
|
|
||||||
response = exc.response
|
|
||||||
self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
|
||||||
else:
|
|
||||||
self.fail('ResourceException was not raised')
|
|
||||||
|
|
||||||
def validation_failed_due_to_field_error_returns_appropriate_message(self, validator):
|
|
||||||
"""If validation fails due to a field error, ensure the response contains a single field error"""
|
|
||||||
content = {'qwerty': ''}
|
|
||||||
try:
|
|
||||||
validator.validate_request(content, None)
|
|
||||||
except ImmediateResponse, exc:
|
|
||||||
response = exc.response
|
|
||||||
self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
|
||||||
else:
|
|
||||||
self.fail('ResourceException was not raised')
|
|
||||||
|
|
||||||
def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator):
|
|
||||||
"""If validation fails due to an invalid field, ensure the response contains a single field error"""
|
|
||||||
content = {'qwerty': 'uiop', 'extra': 'extra'}
|
|
||||||
try:
|
|
||||||
validator.validate_request(content, None)
|
|
||||||
except ImmediateResponse, exc:
|
|
||||||
response = exc.response
|
|
||||||
self.assertEqual(response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
|
|
||||||
else:
|
|
||||||
self.fail('ResourceException was not raised')
|
|
||||||
|
|
||||||
def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
|
||||||
"""If validation for multiple reasons, ensure the response contains each error"""
|
|
||||||
content = {'qwerty': '', 'extra': 'extra'}
|
|
||||||
try:
|
|
||||||
validator.validate_request(content, None)
|
|
||||||
except ImmediateResponse, exc:
|
|
||||||
response = exc.response
|
|
||||||
self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
|
|
||||||
'extra': ['This field does not exist.']}})
|
|
||||||
else:
|
|
||||||
self.fail('ResourceException was not raised')
|
|
||||||
|
|
||||||
# Tests on FormResource
|
|
||||||
|
|
||||||
def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
|
|
||||||
|
|
||||||
def test_form_validation_failure_raises_response_exception(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_failure_raises_response_exception(validator)
|
|
||||||
|
|
||||||
def test_validation_does_not_allow_extra_fields_by_default(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_does_not_allow_extra_fields_by_default(validator)
|
|
||||||
|
|
||||||
def test_validation_allows_extra_fields_if_explicitly_set(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_allows_extra_fields_if_explicitly_set(validator)
|
|
||||||
|
|
||||||
def test_validation_allows_unknown_fields_if_explicitly_allowed(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_allows_unknown_fields_if_explicitly_allowed(validator)
|
|
||||||
|
|
||||||
def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
|
|
||||||
|
|
||||||
def test_validation_failed_due_to_no_content_returns_appropriate_message(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
|
|
||||||
|
|
||||||
def test_validation_failed_due_to_field_error_returns_appropriate_message(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
|
|
||||||
|
|
||||||
def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
|
|
||||||
|
|
||||||
def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
|
||||||
validator = self.MockFormResource(self.MockFormView())
|
|
||||||
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
|
|
||||||
|
|
||||||
# Same tests on ModelResource
|
|
||||||
|
|
||||||
def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
|
|
||||||
|
|
||||||
def test_modelform_validation_failure_raises_response_exception(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_failure_raises_response_exception(validator)
|
|
||||||
|
|
||||||
def test_modelform_validation_does_not_allow_extra_fields_by_default(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_does_not_allow_extra_fields_by_default(validator)
|
|
||||||
|
|
||||||
def test_modelform_validation_allows_extra_fields_if_explicitly_set(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_allows_extra_fields_if_explicitly_set(validator)
|
|
||||||
|
|
||||||
def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
|
|
||||||
|
|
||||||
def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
|
|
||||||
|
|
||||||
def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
|
|
||||||
|
|
||||||
def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
|
|
||||||
|
|
||||||
def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
|
||||||
validator = self.MockModelResource(self.MockModelFormView())
|
|
||||||
self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
|
|
||||||
|
|
||||||
|
|
||||||
class TestModelFormValidator(TestCase):
|
|
||||||
"""Tests specific to ModelFormValidatorMixin"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Create a validator for a model with two fields and a property."""
|
|
||||||
class MockModel(models.Model):
|
|
||||||
qwerty = models.CharField(max_length=256)
|
|
||||||
uiop = models.CharField(max_length=256, blank=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def readonly(self):
|
|
||||||
return 'read only'
|
|
||||||
|
|
||||||
class MockResource(ModelResource):
|
|
||||||
model = MockModel
|
|
||||||
|
|
||||||
class MockView(View):
|
# def test_disabled_form_validator_returns_content_unchanged(self):
|
||||||
resource = MockResource
|
# """If the view's form attribute is None then FormValidator(view).validate_request(content, None)
|
||||||
|
# should just return the content unmodified."""
|
||||||
|
# class DisabledFormResource(FormResource):
|
||||||
|
# form = None
|
||||||
|
|
||||||
|
# class MockView(View):
|
||||||
|
# resource = DisabledFormResource
|
||||||
|
|
||||||
self.validator = MockResource(MockView)
|
# view = MockView()
|
||||||
|
# content = {'qwerty': 'uiop'}
|
||||||
|
# self.assertEqual(FormResource(view).validate_request(content, None), content)
|
||||||
|
|
||||||
|
# def test_disabled_form_validator_get_bound_form_returns_none(self):
|
||||||
|
# """If the view's form attribute is None on then
|
||||||
|
# FormValidator(view).get_bound_form(content) should just return None."""
|
||||||
|
# class DisabledFormResource(FormResource):
|
||||||
|
# form = None
|
||||||
|
|
||||||
|
# class MockView(View):
|
||||||
|
# resource = DisabledFormResource
|
||||||
|
|
||||||
|
# view = MockView()
|
||||||
|
# content = {'qwerty': 'uiop'}
|
||||||
|
# self.assertEqual(FormResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
|
# def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||||
|
# """If the view's form is None and does not have a Resource with a model set then
|
||||||
|
# ModelFormValidator(view).validate_request(content, None) should just return the content unmodified."""
|
||||||
|
|
||||||
|
# class DisabledModelFormView(View):
|
||||||
|
# resource = ModelResource
|
||||||
|
|
||||||
|
# view = DisabledModelFormView()
|
||||||
|
# content = {'qwerty': 'uiop'}
|
||||||
|
# self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
|
# def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
||||||
|
# """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||||
|
# class DisabledModelFormView(View):
|
||||||
|
# resource = ModelResource
|
||||||
|
|
||||||
|
# view = DisabledModelFormView()
|
||||||
|
# content = {'qwerty': 'uiop'}
|
||||||
|
# self.assertEqual(ModelResource(view).get_bound_form(content), None)
|
||||||
|
|
||||||
|
|
||||||
|
# class TestNonFieldErrors(TestCase):
|
||||||
|
# """Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
||||||
|
|
||||||
|
# def test_validate_failed_due_to_non_field_error_returns_appropriate_message(self):
|
||||||
|
# """If validation fails with a non-field error, ensure the response a non-field error"""
|
||||||
|
# class MockForm(forms.Form):
|
||||||
|
# field1 = forms.CharField(required=False)
|
||||||
|
# field2 = forms.CharField(required=False)
|
||||||
|
# ERROR_TEXT = 'You may not supply both field1 and field2'
|
||||||
|
|
||||||
|
# def clean(self):
|
||||||
|
# if 'field1' in self.cleaned_data and 'field2' in self.cleaned_data:
|
||||||
|
# raise forms.ValidationError(self.ERROR_TEXT)
|
||||||
|
# return self.cleaned_data
|
||||||
|
|
||||||
|
# class MockResource(FormResource):
|
||||||
|
# form = MockForm
|
||||||
|
|
||||||
|
# class MockView(View):
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# view = MockView()
|
||||||
|
# content = {'field1': 'example1', 'field2': 'example2'}
|
||||||
|
# try:
|
||||||
|
# MockResource(view).validate_request(content, None)
|
||||||
|
# except ImmediateResponse, exc:
|
||||||
|
# response = exc.response
|
||||||
|
# self.assertEqual(response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||||
|
# else:
|
||||||
|
# self.fail('ImmediateResponse was not raised')
|
||||||
|
|
||||||
|
|
||||||
|
# class TestFormValidation(TestCase):
|
||||||
|
# """Tests which check basic form validation.
|
||||||
|
# Also includes the same set of tests with a ModelFormValidator for which the form has been explicitly set.
|
||||||
|
# (ModelFormValidator should behave as FormValidator if a form is set rather than relying on the default ModelForm)"""
|
||||||
|
# def setUp(self):
|
||||||
|
# class MockForm(forms.Form):
|
||||||
|
# qwerty = forms.CharField(required=True)
|
||||||
|
|
||||||
|
# class MockFormResource(FormResource):
|
||||||
|
# form = MockForm
|
||||||
|
|
||||||
|
# class MockModelResource(ModelResource):
|
||||||
|
# form = MockForm
|
||||||
|
|
||||||
|
# class MockFormView(View):
|
||||||
|
# resource = MockFormResource
|
||||||
|
|
||||||
|
# class MockModelFormView(View):
|
||||||
|
# resource = MockModelResource
|
||||||
|
|
||||||
|
# self.MockFormResource = MockFormResource
|
||||||
|
# self.MockModelResource = MockModelResource
|
||||||
|
# self.MockFormView = MockFormView
|
||||||
|
# self.MockModelFormView = MockModelFormView
|
||||||
|
|
||||||
|
# def validation_returns_content_unchanged_if_already_valid_and_clean(self, validator):
|
||||||
|
# """If the content is already valid and clean then validate(content) should just return the content unmodified."""
|
||||||
|
# content = {'qwerty': 'uiop'}
|
||||||
|
# self.assertEqual(validator.validate_request(content, None), content)
|
||||||
|
|
||||||
|
# def validation_failure_raises_response_exception(self, validator):
|
||||||
|
# """If form validation fails a ResourceException 400 (Bad Request) should be raised."""
|
||||||
|
# content = {}
|
||||||
|
# self.assertRaises(ImmediateResponse, validator.validate_request, content, None)
|
||||||
|
|
||||||
|
# def validation_does_not_allow_extra_fields_by_default(self, validator):
|
||||||
|
# """If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
|
# It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
|
# broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
|
# content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
|
# self.assertRaises(ImmediateResponse, validator.validate_request, content, None)
|
||||||
|
|
||||||
|
# def validation_allows_extra_fields_if_explicitly_set(self, validator):
|
||||||
|
# """If we include an allowed_extra_fields paramater on _validate, then allow fields with those names."""
|
||||||
|
# content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
|
# validator._validate(content, None, allowed_extra_fields=('extra',))
|
||||||
|
|
||||||
|
# def validation_allows_unknown_fields_if_explicitly_allowed(self, validator):
|
||||||
|
# """If we set ``unknown_form_fields`` on the form resource, then don't
|
||||||
|
# raise errors on unexpected request data"""
|
||||||
|
# content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
|
# validator.allow_unknown_form_fields = True
|
||||||
|
# self.assertEqual({'qwerty': u'uiop'},
|
||||||
|
# validator.validate_request(content, None),
|
||||||
|
# "Resource didn't accept unknown fields.")
|
||||||
|
# validator.allow_unknown_form_fields = False
|
||||||
|
|
||||||
|
# def validation_does_not_require_extra_fields_if_explicitly_set(self, validator):
|
||||||
|
# """If we include an allowed_extra_fields paramater on _validate, then do not fail if we do not have fields with those names."""
|
||||||
|
# content = {'qwerty': 'uiop'}
|
||||||
|
# self.assertEqual(validator._validate(content, None, allowed_extra_fields=('extra',)), content)
|
||||||
|
|
||||||
|
# def validation_failed_due_to_no_content_returns_appropriate_message(self, validator):
|
||||||
|
# """If validation fails due to no content, ensure the response contains a single non-field error"""
|
||||||
|
# content = {}
|
||||||
|
# try:
|
||||||
|
# validator.validate_request(content, None)
|
||||||
|
# except ImmediateResponse, exc:
|
||||||
|
# response = exc.response
|
||||||
|
# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||||
|
# else:
|
||||||
|
# self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
|
# def validation_failed_due_to_field_error_returns_appropriate_message(self, validator):
|
||||||
|
# """If validation fails due to a field error, ensure the response contains a single field error"""
|
||||||
|
# content = {'qwerty': ''}
|
||||||
|
# try:
|
||||||
|
# validator.validate_request(content, None)
|
||||||
|
# except ImmediateResponse, exc:
|
||||||
|
# response = exc.response
|
||||||
|
# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.']}})
|
||||||
|
# else:
|
||||||
|
# self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
|
# def validation_failed_due_to_invalid_field_returns_appropriate_message(self, validator):
|
||||||
|
# """If validation fails due to an invalid field, ensure the response contains a single field error"""
|
||||||
|
# content = {'qwerty': 'uiop', 'extra': 'extra'}
|
||||||
|
# try:
|
||||||
|
# validator.validate_request(content, None)
|
||||||
|
# except ImmediateResponse, exc:
|
||||||
|
# response = exc.response
|
||||||
|
# self.assertEqual(response.raw_content, {'field_errors': {'extra': ['This field does not exist.']}})
|
||||||
|
# else:
|
||||||
|
# self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
|
# def validation_failed_due_to_multiple_errors_returns_appropriate_message(self, validator):
|
||||||
|
# """If validation for multiple reasons, ensure the response contains each error"""
|
||||||
|
# content = {'qwerty': '', 'extra': 'extra'}
|
||||||
|
# try:
|
||||||
|
# validator.validate_request(content, None)
|
||||||
|
# except ImmediateResponse, exc:
|
||||||
|
# response = exc.response
|
||||||
|
# self.assertEqual(response.raw_content, {'field_errors': {'qwerty': ['This field is required.'],
|
||||||
|
# 'extra': ['This field does not exist.']}})
|
||||||
|
# else:
|
||||||
|
# self.fail('ResourceException was not raised')
|
||||||
|
|
||||||
|
# # Tests on FormResource
|
||||||
|
|
||||||
|
# def test_form_validation_returns_content_unchanged_if_already_valid_and_clean(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
|
||||||
|
|
||||||
|
# def test_form_validation_failure_raises_response_exception(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_failure_raises_response_exception(validator)
|
||||||
|
|
||||||
|
# def test_validation_does_not_allow_extra_fields_by_default(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_does_not_allow_extra_fields_by_default(validator)
|
||||||
|
|
||||||
|
# def test_validation_allows_extra_fields_if_explicitly_set(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_allows_extra_fields_if_explicitly_set(validator)
|
||||||
|
|
||||||
|
# def test_validation_allows_unknown_fields_if_explicitly_allowed(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_allows_unknown_fields_if_explicitly_allowed(validator)
|
||||||
|
|
||||||
|
# def test_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
|
||||||
|
|
||||||
|
# def test_validation_failed_due_to_no_content_returns_appropriate_message(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
|
||||||
|
|
||||||
|
# def test_validation_failed_due_to_field_error_returns_appropriate_message(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
|
||||||
|
|
||||||
|
# def test_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
|
||||||
|
|
||||||
|
# def test_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
||||||
|
# validator = self.MockFormResource(self.MockFormView())
|
||||||
|
# self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
|
||||||
|
|
||||||
|
# # Same tests on ModelResource
|
||||||
|
|
||||||
|
# def test_modelform_validation_returns_content_unchanged_if_already_valid_and_clean(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_returns_content_unchanged_if_already_valid_and_clean(validator)
|
||||||
|
|
||||||
|
# def test_modelform_validation_failure_raises_response_exception(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_failure_raises_response_exception(validator)
|
||||||
|
|
||||||
|
# def test_modelform_validation_does_not_allow_extra_fields_by_default(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_does_not_allow_extra_fields_by_default(validator)
|
||||||
|
|
||||||
|
# def test_modelform_validation_allows_extra_fields_if_explicitly_set(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_allows_extra_fields_if_explicitly_set(validator)
|
||||||
|
|
||||||
|
# def test_modelform_validation_does_not_require_extra_fields_if_explicitly_set(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_does_not_require_extra_fields_if_explicitly_set(validator)
|
||||||
|
|
||||||
|
# def test_modelform_validation_failed_due_to_no_content_returns_appropriate_message(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_failed_due_to_no_content_returns_appropriate_message(validator)
|
||||||
|
|
||||||
|
# def test_modelform_validation_failed_due_to_field_error_returns_appropriate_message(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_failed_due_to_field_error_returns_appropriate_message(validator)
|
||||||
|
|
||||||
|
# def test_modelform_validation_failed_due_to_invalid_field_returns_appropriate_message(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_failed_due_to_invalid_field_returns_appropriate_message(validator)
|
||||||
|
|
||||||
|
# def test_modelform_validation_failed_due_to_multiple_errors_returns_appropriate_message(self):
|
||||||
|
# validator = self.MockModelResource(self.MockModelFormView())
|
||||||
|
# self.validation_failed_due_to_multiple_errors_returns_appropriate_message(validator)
|
||||||
|
|
||||||
|
|
||||||
|
# class TestModelFormValidator(TestCase):
|
||||||
|
# """Tests specific to ModelFormValidatorMixin"""
|
||||||
|
|
||||||
|
# def setUp(self):
|
||||||
|
# """Create a validator for a model with two fields and a property."""
|
||||||
|
# class MockModel(models.Model):
|
||||||
|
# qwerty = models.CharField(max_length=256)
|
||||||
|
# uiop = models.CharField(max_length=256, blank=True)
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def readonly(self):
|
||||||
|
# return 'read only'
|
||||||
|
|
||||||
|
# class MockResource(ModelResource):
|
||||||
|
# model = MockModel
|
||||||
|
|
||||||
def test_property_fields_are_allowed_on_model_forms(self):
|
# class MockView(View):
|
||||||
"""Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
|
# resource = MockResource
|
||||||
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'}
|
|
||||||
self.assertEqual(self.validator.validate_request(content, None), content)
|
|
||||||
|
|
||||||
def test_property_fields_are_not_required_on_model_forms(self):
|
# self.validator = MockResource(MockView)
|
||||||
"""Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
|
|
||||||
content = {'qwerty': 'example', 'uiop': 'example'}
|
|
||||||
self.assertEqual(self.validator.validate_request(content, None), content)
|
|
||||||
|
|
||||||
def test_extra_fields_not_allowed_on_model_forms(self):
|
# def test_property_fields_are_allowed_on_model_forms(self):
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
# """Validation on ModelForms may include property fields that exist on the Model to be included in the input."""
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
# content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'}
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
# self.assertEqual(self.validator.validate_request(content, None), content)
|
||||||
content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
|
|
||||||
self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None)
|
|
||||||
|
|
||||||
def test_validate_requires_fields_on_model_forms(self):
|
# def test_property_fields_are_not_required_on_model_forms(self):
|
||||||
"""If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
# """Validation on ModelForms does not require property fields that exist on the Model to be included in the input."""
|
||||||
It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
# content = {'qwerty': 'example', 'uiop': 'example'}
|
||||||
broken clients more easily (eg submitting content with a misnamed field)"""
|
# self.assertEqual(self.validator.validate_request(content, None), content)
|
||||||
content = {'readonly': 'read only'}
|
|
||||||
self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None)
|
|
||||||
|
|
||||||
def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
# def test_extra_fields_not_allowed_on_model_forms(self):
|
||||||
"""Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
# """If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
content = {'qwerty': 'example', 'readonly': 'read only'}
|
# It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
self.validator.validate_request(content, None)
|
# broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
|
# content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'}
|
||||||
|
# self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
def test_model_form_validator_uses_model_forms(self):
|
# def test_validate_requires_fields_on_model_forms(self):
|
||||||
self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
# """If some (otherwise valid) content includes fields that are not in the form then validation should fail.
|
||||||
|
# It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up
|
||||||
|
# broken clients more easily (eg submitting content with a misnamed field)"""
|
||||||
|
# content = {'readonly': 'read only'}
|
||||||
|
# self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None)
|
||||||
|
|
||||||
|
# def test_validate_does_not_require_blankable_fields_on_model_forms(self):
|
||||||
|
# """Test standard ModelForm validation behaviour - fields with blank=True are not required."""
|
||||||
|
# content = {'qwerty': 'example', 'readonly': 'read only'}
|
||||||
|
# self.validator.validate_request(content, None)
|
||||||
|
|
||||||
|
# def test_model_form_validator_uses_model_forms(self):
|
||||||
|
# self.assertTrue(isinstance(self.validator.get_bound_form(), forms.ModelForm))
|
||||||
|
|
|
@ -1,135 +1,128 @@
|
||||||
from django.core.urlresolvers import reverse
|
# from django.core.urlresolvers import reverse
|
||||||
from django.conf.urls.defaults import patterns, url, include
|
# from django.conf.urls.defaults import patterns, url, include
|
||||||
from django.http import HttpResponse
|
# from django.http import HttpResponse
|
||||||
from django.test import TestCase
|
# from django.test import TestCase
|
||||||
from django import forms
|
# from django.utils import simplejson as json
|
||||||
from django.db import models
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
|
|
||||||
from djangorestframework.resources import ModelResource
|
# from djangorestframework.views import View
|
||||||
from djangorestframework.views import (
|
|
||||||
View,
|
|
||||||
ListOrCreateModelView,
|
|
||||||
InstanceModelView
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MockView(View):
|
# class MockView(View):
|
||||||
"""This is a basic mock view"""
|
# """This is a basic mock view"""
|
||||||
pass
|
# pass
|
||||||
|
|
||||||
|
|
||||||
class MockViewFinal(View):
|
# class MockViewFinal(View):
|
||||||
"""View with final() override"""
|
# """View with final() override"""
|
||||||
|
|
||||||
def final(self, request, response, *args, **kwargs):
|
# def final(self, request, response, *args, **kwargs):
|
||||||
return HttpResponse('{"test": "passed"}', content_type="application/json")
|
# return HttpResponse('{"test": "passed"}', content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
class ResourceMockView(View):
|
# # class ResourceMockView(View):
|
||||||
"""This is a resource-based mock view"""
|
# # """This is a resource-based mock view"""
|
||||||
|
|
||||||
class MockForm(forms.Form):
|
# # class MockForm(forms.Form):
|
||||||
foo = forms.BooleanField(required=False)
|
# # foo = forms.BooleanField(required=False)
|
||||||
bar = forms.IntegerField(help_text='Must be an integer.')
|
# # bar = forms.IntegerField(help_text='Must be an integer.')
|
||||||
baz = forms.CharField(max_length=32)
|
# # baz = forms.CharField(max_length=32)
|
||||||
|
|
||||||
form = MockForm
|
# # form = MockForm
|
||||||
|
|
||||||
|
|
||||||
class MockResource(ModelResource):
|
# # class MockResource(ModelResource):
|
||||||
"""This is a mock model-based resource"""
|
# # """This is a mock model-based resource"""
|
||||||
|
|
||||||
class MockResourceModel(models.Model):
|
# # class MockResourceModel(models.Model):
|
||||||
foo = models.BooleanField()
|
# # foo = models.BooleanField()
|
||||||
bar = models.IntegerField(help_text='Must be an integer.')
|
# # bar = models.IntegerField(help_text='Must be an integer.')
|
||||||
baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
|
# # baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
|
||||||
|
|
||||||
model = MockResourceModel
|
# # model = MockResourceModel
|
||||||
fields = ('foo', 'bar', 'baz')
|
# # fields = ('foo', 'bar', 'baz')
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
# urlpatterns = patterns('',
|
||||||
url(r'^mock/$', MockView.as_view()),
|
# url(r'^mock/$', MockView.as_view()),
|
||||||
url(r'^mock/final/$', MockViewFinal.as_view()),
|
# url(r'^mock/final/$', MockViewFinal.as_view()),
|
||||||
url(r'^resourcemock/$', ResourceMockView.as_view()),
|
# # url(r'^resourcemock/$', ResourceMockView.as_view()),
|
||||||
url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)),
|
# # url(r'^model/$', ListOrCreateModelView.as_view(resource=MockResource)),
|
||||||
url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MockResource)),
|
# # url(r'^model/(?P<pk>[^/]+)/$', InstanceModelView.as_view(resource=MockResource)),
|
||||||
url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')),
|
# url(r'^restframework/', include('djangorestframework.urls', namespace='djangorestframework')),
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
class BaseViewTests(TestCase):
|
# class BaseViewTests(TestCase):
|
||||||
"""Test the base view class of djangorestframework"""
|
# """Test the base view class of djangorestframework"""
|
||||||
urls = 'djangorestframework.tests.views'
|
# urls = 'djangorestframework.tests.views'
|
||||||
|
|
||||||
def test_view_call_final(self):
|
# def test_view_call_final(self):
|
||||||
response = self.client.options('/mock/final/')
|
# response = self.client.options('/mock/final/')
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], "application/json")
|
# self.assertEqual(response['Content-Type'].split(';')[0], "application/json")
|
||||||
data = json.loads(response.content)
|
# data = json.loads(response.content)
|
||||||
self.assertEqual(data['test'], 'passed')
|
# self.assertEqual(data['test'], 'passed')
|
||||||
|
|
||||||
def test_options_method_simple_view(self):
|
# def test_options_method_simple_view(self):
|
||||||
response = self.client.options('/mock/')
|
# response = self.client.options('/mock/')
|
||||||
self._verify_options_response(response,
|
# self._verify_options_response(response,
|
||||||
name='Mock',
|
# name='Mock',
|
||||||
description='This is a basic mock view')
|
# description='This is a basic mock view')
|
||||||
|
|
||||||
def test_options_method_resource_view(self):
|
# def test_options_method_resource_view(self):
|
||||||
response = self.client.options('/resourcemock/')
|
# response = self.client.options('/resourcemock/')
|
||||||
self._verify_options_response(response,
|
# self._verify_options_response(response,
|
||||||
name='Resource Mock',
|
# name='Resource Mock',
|
||||||
description='This is a resource-based mock view',
|
# description='This is a resource-based mock view',
|
||||||
fields={'foo': 'BooleanField',
|
# fields={'foo': 'BooleanField',
|
||||||
'bar': 'IntegerField',
|
# 'bar': 'IntegerField',
|
||||||
'baz': 'CharField',
|
# 'baz': 'CharField',
|
||||||
})
|
# })
|
||||||
|
|
||||||
def test_options_method_model_resource_list_view(self):
|
# def test_options_method_model_resource_list_view(self):
|
||||||
response = self.client.options('/model/')
|
# response = self.client.options('/model/')
|
||||||
self._verify_options_response(response,
|
# self._verify_options_response(response,
|
||||||
name='Mock List',
|
# name='Mock List',
|
||||||
description='This is a mock model-based resource',
|
# description='This is a mock model-based resource',
|
||||||
fields={'foo': 'BooleanField',
|
# fields={'foo': 'BooleanField',
|
||||||
'bar': 'IntegerField',
|
# 'bar': 'IntegerField',
|
||||||
'baz': 'CharField',
|
# 'baz': 'CharField',
|
||||||
})
|
# })
|
||||||
|
|
||||||
def test_options_method_model_resource_detail_view(self):
|
# def test_options_method_model_resource_detail_view(self):
|
||||||
response = self.client.options('/model/0/')
|
# response = self.client.options('/model/0/')
|
||||||
self._verify_options_response(response,
|
# self._verify_options_response(response,
|
||||||
name='Mock Instance',
|
# name='Mock Instance',
|
||||||
description='This is a mock model-based resource',
|
# description='This is a mock model-based resource',
|
||||||
fields={'foo': 'BooleanField',
|
# fields={'foo': 'BooleanField',
|
||||||
'bar': 'IntegerField',
|
# 'bar': 'IntegerField',
|
||||||
'baz': 'CharField',
|
# 'baz': 'CharField',
|
||||||
})
|
# })
|
||||||
|
|
||||||
def _verify_options_response(self, response, name, description, fields=None, status=200,
|
# def _verify_options_response(self, response, name, description, fields=None, status=200,
|
||||||
mime_type='application/json'):
|
# mime_type='application/json'):
|
||||||
self.assertEqual(response.status_code, status)
|
# self.assertEqual(response.status_code, status)
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
# self.assertEqual(response['Content-Type'].split(';')[0], mime_type)
|
||||||
data = json.loads(response.content)
|
# data = json.loads(response.content)
|
||||||
self.assertTrue('application/json' in data['renders'])
|
# self.assertTrue('application/json' in data['renders'])
|
||||||
self.assertEqual(name, data['name'])
|
# self.assertEqual(name, data['name'])
|
||||||
self.assertEqual(description, data['description'])
|
# self.assertEqual(description, data['description'])
|
||||||
if fields is None:
|
# if fields is None:
|
||||||
self.assertFalse(hasattr(data, 'fields'))
|
# self.assertFalse(hasattr(data, 'fields'))
|
||||||
else:
|
# else:
|
||||||
self.assertEqual(data['fields'], fields)
|
# self.assertEqual(data['fields'], fields)
|
||||||
|
|
||||||
|
|
||||||
class ExtraViewsTests(TestCase):
|
# class ExtraViewsTests(TestCase):
|
||||||
"""Test the extra views djangorestframework provides"""
|
# """Test the extra views djangorestframework provides"""
|
||||||
urls = 'djangorestframework.tests.views'
|
# urls = 'djangorestframework.tests.views'
|
||||||
|
|
||||||
def test_login_view(self):
|
# def test_login_view(self):
|
||||||
"""Ensure the login view exists"""
|
# """Ensure the login view exists"""
|
||||||
response = self.client.get(reverse('djangorestframework:login'))
|
# response = self.client.get(reverse('djangorestframework:login'))
|
||||||
self.assertEqual(response.status_code, 200)
|
# self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
# self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
||||||
|
|
||||||
def test_logout_view(self):
|
# def test_logout_view(self):
|
||||||
"""Ensure the logout view exists"""
|
# """Ensure the logout view exists"""
|
||||||
response = self.client.get(reverse('djangorestframework:logout'))
|
# response = self.client.get(reverse('djangorestframework:logout'))
|
||||||
self.assertEqual(response.status_code, 200)
|
# self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
# self.assertEqual(response['Content-Type'].split(';')[0], 'text/html')
|
||||||
|
|
|
@ -13,8 +13,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||||
from djangorestframework.compat import View as DjangoView, apply_markdown
|
from djangorestframework.compat import View as DjangoView, apply_markdown
|
||||||
from djangorestframework.response import Response, ImmediateResponse
|
from djangorestframework.response import Response, ImmediateResponse
|
||||||
from djangorestframework.request import Request
|
from djangorestframework.request import Request
|
||||||
from djangorestframework.mixins import *
|
from djangorestframework import renderers, parsers, authentication, permissions, status
|
||||||
from djangorestframework import resources, renderers, parsers, authentication, permissions, status
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -29,7 +28,7 @@ __all__ = (
|
||||||
def _remove_trailing_string(content, trailing):
|
def _remove_trailing_string(content, trailing):
|
||||||
"""
|
"""
|
||||||
Strip trailing component `trailing` from `content` if it exists.
|
Strip trailing component `trailing` from `content` if it exists.
|
||||||
Used when generating names from view/resource classes.
|
Used when generating names from view classes.
|
||||||
"""
|
"""
|
||||||
if content.endswith(trailing) and content != trailing:
|
if content.endswith(trailing) and content != trailing:
|
||||||
return content[:-len(trailing)]
|
return content[:-len(trailing)]
|
||||||
|
@ -54,40 +53,26 @@ def _remove_leading_indent(content):
|
||||||
def _camelcase_to_spaces(content):
|
def _camelcase_to_spaces(content):
|
||||||
"""
|
"""
|
||||||
Translate 'CamelCaseNames' to 'Camel Case Names'.
|
Translate 'CamelCaseNames' to 'Camel Case Names'.
|
||||||
Used when generating names from view/resource classes.
|
Used when generating names from view classes.
|
||||||
"""
|
"""
|
||||||
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
|
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
|
||||||
return re.sub(camelcase_boundry, ' \\1', content).strip()
|
return re.sub(camelcase_boundry, ' \\1', content).strip()
|
||||||
|
|
||||||
|
|
||||||
_resource_classes = (
|
class View(DjangoView):
|
||||||
None,
|
|
||||||
resources.Resource,
|
|
||||||
resources.FormResource,
|
|
||||||
resources.ModelResource
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class View(ResourceMixin, DjangoView):
|
|
||||||
"""
|
"""
|
||||||
Handles incoming requests and maps them to REST operations.
|
Handles incoming requests and maps them to REST operations.
|
||||||
Performs request deserialization, response serialization, authentication and input validation.
|
Performs request deserialization, response serialization, authentication and input validation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
resource = None
|
|
||||||
"""
|
|
||||||
The resource to use when validating requests and filtering responses,
|
|
||||||
or `None` to use default behaviour.
|
|
||||||
"""
|
|
||||||
|
|
||||||
renderers = renderers.DEFAULT_RENDERERS
|
renderers = renderers.DEFAULT_RENDERERS
|
||||||
"""
|
"""
|
||||||
List of renderer classes the resource can serialize the response with, ordered by preference.
|
List of renderer classes the view can serialize the response with, ordered by preference.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parsers = parsers.DEFAULT_PARSERS
|
parsers = parsers.DEFAULT_PARSERS
|
||||||
"""
|
"""
|
||||||
List of parser classes the resource can parse the request with.
|
List of parser classes the view can parse the request with.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
authentication = (authentication.UserLoggedInAuthentication,
|
authentication = (authentication.UserLoggedInAuthentication,
|
||||||
|
@ -132,17 +117,8 @@ class View(ResourceMixin, DjangoView):
|
||||||
Return the resource or view class name for use as this view's name.
|
Return the resource or view class name for use as this view's name.
|
||||||
Override to customize.
|
Override to customize.
|
||||||
"""
|
"""
|
||||||
# If this view has a resource that's been overridden, then use that resource for the name
|
name = self.__class__.__name__
|
||||||
if getattr(self, 'resource', None) not in _resource_classes:
|
name = _remove_trailing_string(name, 'View')
|
||||||
name = self.resource.__name__
|
|
||||||
name = _remove_trailing_string(name, 'Resource')
|
|
||||||
name += getattr(self, '_suffix', '')
|
|
||||||
|
|
||||||
# If it's a view class with no resource then grok the name from the class name
|
|
||||||
else:
|
|
||||||
name = self.__class__.__name__
|
|
||||||
name = _remove_trailing_string(name, 'View')
|
|
||||||
|
|
||||||
return _camelcase_to_spaces(name)
|
return _camelcase_to_spaces(name)
|
||||||
|
|
||||||
def get_description(self, html=False):
|
def get_description(self, html=False):
|
||||||
|
@ -150,20 +126,8 @@ class View(ResourceMixin, DjangoView):
|
||||||
Return the resource or view docstring for use as this view's description.
|
Return the resource or view docstring for use as this view's description.
|
||||||
Override to customize.
|
Override to customize.
|
||||||
"""
|
"""
|
||||||
|
description = self.__doc__ or ''
|
||||||
description = None
|
|
||||||
|
|
||||||
# If this view has a resource that's been overridden,
|
|
||||||
# then try to use the resource's docstring
|
|
||||||
if getattr(self, 'resource', None) not in _resource_classes:
|
|
||||||
description = self.resource.__doc__
|
|
||||||
|
|
||||||
# Otherwise use the view docstring
|
|
||||||
if not description:
|
|
||||||
description = self.__doc__ or ''
|
|
||||||
|
|
||||||
description = _remove_leading_indent(description)
|
description = _remove_leading_indent(description)
|
||||||
|
|
||||||
if html:
|
if html:
|
||||||
return self.markup_description(description)
|
return self.markup_description(description)
|
||||||
return description
|
return description
|
||||||
|
@ -184,7 +148,7 @@ class View(ResourceMixin, DjangoView):
|
||||||
a handler method.
|
a handler method.
|
||||||
"""
|
"""
|
||||||
content = {
|
content = {
|
||||||
'detail': "Method '%s' not allowed on this resource." % request.method
|
'detail': "Method '%s' not allowed." % request.method
|
||||||
}
|
}
|
||||||
raise ImmediateResponse(content, status.HTTP_405_METHOD_NOT_ALLOWED)
|
raise ImmediateResponse(content, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
|
@ -283,10 +247,6 @@ class View(ResourceMixin, DjangoView):
|
||||||
|
|
||||||
response = handler(request, *args, **kwargs)
|
response = handler(request, *args, **kwargs)
|
||||||
|
|
||||||
if isinstance(response, Response):
|
|
||||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
|
||||||
response.raw_content = self.filter_response(response.raw_content)
|
|
||||||
|
|
||||||
except ImmediateResponse, exc:
|
except ImmediateResponse, exc:
|
||||||
response = exc.response
|
response = exc.response
|
||||||
|
|
||||||
|
@ -307,31 +267,3 @@ class View(ResourceMixin, DjangoView):
|
||||||
field_name_types[name] = field.__class__.__name__
|
field_name_types[name] = field.__class__.__name__
|
||||||
content['fields'] = field_name_types
|
content['fields'] = field_name_types
|
||||||
raise ImmediateResponse(content, status=status.HTTP_200_OK)
|
raise ImmediateResponse(content, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class ModelView(View):
|
|
||||||
"""
|
|
||||||
A RESTful view that maps to a model in the database.
|
|
||||||
"""
|
|
||||||
resource = resources.ModelResource
|
|
||||||
|
|
||||||
|
|
||||||
class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView):
|
|
||||||
"""
|
|
||||||
A view which provides default operations for read/update/delete against a model instance.
|
|
||||||
"""
|
|
||||||
_suffix = 'Instance'
|
|
||||||
|
|
||||||
|
|
||||||
class ListModelView(ListModelMixin, ModelView):
|
|
||||||
"""
|
|
||||||
A view which provides default operations for list, against a model in the database.
|
|
||||||
"""
|
|
||||||
_suffix = 'List'
|
|
||||||
|
|
||||||
|
|
||||||
class ListOrCreateModelView(ListModelMixin, CreateModelMixin, ModelView):
|
|
||||||
"""
|
|
||||||
A view which provides default operations for list and create, against a model in the database.
|
|
||||||
"""
|
|
||||||
_suffix = 'List'
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user