mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 16:24:18 +03:00
Decouple views and resources
This commit is contained in:
parent
8756664e06
commit
d373b3a067
|
@ -1,6 +1,6 @@
|
||||||
"""The :mod:`authentication` modules provides for pluggable authentication behaviour.
|
"""The :mod:`authentication` modules provides for pluggable authentication behaviour.
|
||||||
|
|
||||||
Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.Resource` or Django :class:`View` class.
|
Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.BaseView` or Django :class:`View` class.
|
||||||
|
|
||||||
The set of authentication which are use is then specified by setting the :attr:`authentication` attribute on the class, and listing a set of authentication classes.
|
The set of authentication which are use is then specified by setting the :attr:`authentication` attribute on the class, and listing a set of authentication classes.
|
||||||
"""
|
"""
|
||||||
|
@ -25,10 +25,10 @@ class BaseAuthenticator(object):
|
||||||
be some more complicated token, for example authentication tokens which are signed
|
be some more complicated token, for example authentication tokens which are signed
|
||||||
against a particular set of permissions for a given user, over a given timeframe.
|
against a particular set of permissions for a given user, over a given timeframe.
|
||||||
|
|
||||||
The default permission checking on Resource will use the allowed_methods attribute
|
The default permission checking on View will use the allowed_methods attribute
|
||||||
for permissions if the authentication context is not None, and use anon_allowed_methods otherwise.
|
for permissions if the authentication context is not None, and use anon_allowed_methods otherwise.
|
||||||
|
|
||||||
The authentication context is available to the method calls eg Resource.get(request)
|
The authentication context is available to the method calls eg View.get(request)
|
||||||
by accessing self.auth in order to allow them to apply any more fine grained permission
|
by accessing self.auth in order to allow them to apply any more fine grained permission
|
||||||
checking at the point the response is being generated.
|
checking at the point the response is being generated.
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
""""""
|
||||||
from djangorestframework.utils.mediatypes import MediaType
|
from djangorestframework.utils.mediatypes import MediaType
|
||||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ErrorResponse
|
||||||
|
@ -12,6 +13,14 @@ from decimal import Decimal
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['RequestMixin',
|
||||||
|
'ResponseMixin',
|
||||||
|
'AuthMixin',
|
||||||
|
'ReadModelMixin',
|
||||||
|
'CreateModelMixin',
|
||||||
|
'UpdateModelMixin',
|
||||||
|
'DeleteModelMixin',
|
||||||
|
'ListModelMixin']
|
||||||
|
|
||||||
########## Request Mixin ##########
|
########## Request Mixin ##########
|
||||||
|
|
||||||
|
@ -250,7 +259,7 @@ class RequestMixin(object):
|
||||||
########## ResponseMixin ##########
|
########## ResponseMixin ##########
|
||||||
|
|
||||||
class ResponseMixin(object):
|
class ResponseMixin(object):
|
||||||
"""Adds behaviour for pluggable Renderers to a :class:`.Resource` or Django :class:`View`. class.
|
"""Adds behaviour for pluggable Renderers to a :class:`.BaseView` or Django :class:`View`. class.
|
||||||
|
|
||||||
Default behaviour is to use standard HTTP Accept header content negotiation.
|
Default behaviour is to use standard HTTP Accept header content negotiation.
|
||||||
Also supports overidding the content type by specifying an _accept= parameter in the URL.
|
Also supports overidding the content type by specifying an _accept= parameter in the URL.
|
||||||
|
@ -259,32 +268,8 @@ class ResponseMixin(object):
|
||||||
ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
|
||||||
REWRITE_IE_ACCEPT_HEADER = True
|
REWRITE_IE_ACCEPT_HEADER = True
|
||||||
|
|
||||||
#request = None
|
|
||||||
#response = None
|
|
||||||
renderers = ()
|
renderers = ()
|
||||||
|
|
||||||
#def render_to_response(self, obj):
|
|
||||||
# if isinstance(obj, Response):
|
|
||||||
# response = obj
|
|
||||||
# elif response_obj is not None:
|
|
||||||
# response = Response(status.HTTP_200_OK, obj)
|
|
||||||
# else:
|
|
||||||
# response = Response(status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
# response.cleaned_content = self._filter(response.raw_content)
|
|
||||||
|
|
||||||
# self._render(response)
|
|
||||||
|
|
||||||
|
|
||||||
#def filter(self, content):
|
|
||||||
# """
|
|
||||||
# Filter the response content.
|
|
||||||
# """
|
|
||||||
# for filterer_cls in self.filterers:
|
|
||||||
# filterer = filterer_cls(self)
|
|
||||||
# content = filterer.filter(content)
|
|
||||||
# return content
|
|
||||||
|
|
||||||
|
|
||||||
def render(self, response):
|
def render(self, response):
|
||||||
"""Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
|
"""Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
|
||||||
|
@ -318,7 +303,7 @@ class ResponseMixin(object):
|
||||||
|
|
||||||
def _determine_renderer(self, request):
|
def _determine_renderer(self, request):
|
||||||
"""Return the appropriate renderer for the output, given the client's 'Accept' header,
|
"""Return the appropriate renderer for the output, given the client's 'Accept' header,
|
||||||
and the content types that this Resource knows how to serve.
|
and the content types that this mixin knows how to serve.
|
||||||
|
|
||||||
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
|
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
|
||||||
|
|
||||||
|
@ -415,17 +400,6 @@ class AuthMixin(object):
|
||||||
return auth
|
return auth
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# TODO?
|
|
||||||
#@property
|
|
||||||
#def user(self):
|
|
||||||
# if not has_attr(self, '_user'):
|
|
||||||
# auth = self.auth
|
|
||||||
# if isinstance(auth, User...):
|
|
||||||
# self._user = auth
|
|
||||||
# else:
|
|
||||||
# self._user = getattr(auth, 'user', None)
|
|
||||||
# return self._user
|
|
||||||
|
|
||||||
def check_permissions(self):
|
def check_permissions(self):
|
||||||
if not self.permissions:
|
if not self.permissions:
|
||||||
return
|
return
|
||||||
|
@ -443,14 +417,15 @@ class AuthMixin(object):
|
||||||
class ReadModelMixin(object):
|
class ReadModelMixin(object):
|
||||||
"""Behaviour to read a model instance on GET requests"""
|
"""Behaviour to read a model instance on GET requests"""
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
model = self.resource.model
|
||||||
try:
|
try:
|
||||||
if args:
|
if args:
|
||||||
# If we have any none kwargs then assume the last represents the primrary key
|
# If we have any none kwargs then assume the last represents the primrary key
|
||||||
instance = self.model.objects.get(pk=args[-1], **kwargs)
|
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||||
else:
|
else:
|
||||||
# Otherwise assume the kwargs uniquely identify the model
|
# Otherwise assume the kwargs uniquely identify the model
|
||||||
instance = self.model.objects.get(**kwargs)
|
instance = model.objects.get(**kwargs)
|
||||||
except self.model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
@ -459,17 +434,18 @@ class ReadModelMixin(object):
|
||||||
class CreateModelMixin(object):
|
class CreateModelMixin(object):
|
||||||
"""Behaviour to create a model instance on POST requests"""
|
"""Behaviour to create a model instance on POST requests"""
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
model = self.resource.model
|
||||||
# translated 'related_field' kwargs into 'related_field_id'
|
# translated 'related_field' kwargs into 'related_field_id'
|
||||||
for related_name in [field.name for field in self.model._meta.fields if isinstance(field, RelatedField)]:
|
for related_name in [field.name for field in model._meta.fields if isinstance(field, RelatedField)]:
|
||||||
if kwargs.has_key(related_name):
|
if kwargs.has_key(related_name):
|
||||||
kwargs[related_name + '_id'] = kwargs[related_name]
|
kwargs[related_name + '_id'] = kwargs[related_name]
|
||||||
del kwargs[related_name]
|
del kwargs[related_name]
|
||||||
|
|
||||||
all_kw_args = dict(self.CONTENT.items() + kwargs.items())
|
all_kw_args = dict(self.CONTENT.items() + kwargs.items())
|
||||||
if args:
|
if args:
|
||||||
instance = self.model(pk=args[-1], **all_kw_args)
|
instance = model(pk=args[-1], **all_kw_args)
|
||||||
else:
|
else:
|
||||||
instance = self.model(**all_kw_args)
|
instance = model(**all_kw_args)
|
||||||
instance.save()
|
instance.save()
|
||||||
headers = {}
|
headers = {}
|
||||||
if hasattr(instance, 'get_absolute_url'):
|
if hasattr(instance, 'get_absolute_url'):
|
||||||
|
@ -480,19 +456,20 @@ class CreateModelMixin(object):
|
||||||
class UpdateModelMixin(object):
|
class UpdateModelMixin(object):
|
||||||
"""Behaviour to update a model instance on PUT requests"""
|
"""Behaviour to update a model instance on PUT requests"""
|
||||||
def put(self, request, *args, **kwargs):
|
def put(self, request, *args, **kwargs):
|
||||||
|
model = self.resource.model
|
||||||
# TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
|
# 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:
|
try:
|
||||||
if args:
|
if args:
|
||||||
# If we have any none kwargs then assume the last represents the primrary key
|
# If we have any none kwargs then assume the last represents the primrary key
|
||||||
instance = self.model.objects.get(pk=args[-1], **kwargs)
|
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||||
else:
|
else:
|
||||||
# Otherwise assume the kwargs uniquely identify the model
|
# Otherwise assume the kwargs uniquely identify the model
|
||||||
instance = self.model.objects.get(**kwargs)
|
instance = model.objects.get(**kwargs)
|
||||||
|
|
||||||
for (key, val) in self.CONTENT.items():
|
for (key, val) in self.CONTENT.items():
|
||||||
setattr(instance, key, val)
|
setattr(instance, key, val)
|
||||||
except self.model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
instance = self.model(**self.CONTENT)
|
instance = model(**self.CONTENT)
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
instance.save()
|
instance.save()
|
||||||
|
@ -502,14 +479,15 @@ class UpdateModelMixin(object):
|
||||||
class DeleteModelMixin(object):
|
class DeleteModelMixin(object):
|
||||||
"""Behaviour to delete a model instance on DELETE requests"""
|
"""Behaviour to delete a model instance on DELETE requests"""
|
||||||
def delete(self, request, *args, **kwargs):
|
def delete(self, request, *args, **kwargs):
|
||||||
|
model = self.resource.model
|
||||||
try:
|
try:
|
||||||
if args:
|
if args:
|
||||||
# If we have any none kwargs then assume the last represents the primrary key
|
# If we have any none kwargs then assume the last represents the primrary key
|
||||||
instance = self.model.objects.get(pk=args[-1], **kwargs)
|
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||||
else:
|
else:
|
||||||
# Otherwise assume the kwargs uniquely identify the model
|
# Otherwise assume the kwargs uniquely identify the model
|
||||||
instance = self.model.objects.get(**kwargs)
|
instance = model.objects.get(**kwargs)
|
||||||
except self.model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
||||||
|
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
|
|
@ -1,356 +0,0 @@
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.db.models import Model
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from django.db.models.fields.related import RelatedField
|
|
||||||
|
|
||||||
from djangorestframework.response import Response, ErrorResponse
|
|
||||||
from djangorestframework.resource import Resource
|
|
||||||
from djangorestframework import status, validators
|
|
||||||
|
|
||||||
import decimal
|
|
||||||
import inspect
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class ModelResource(Resource):
|
|
||||||
"""A specialized type of Resource, for resources that map directly to a Django Model.
|
|
||||||
Useful things this provides:
|
|
||||||
|
|
||||||
0. Default input validation based on ModelForms.
|
|
||||||
1. Nice serialization of returned Models and QuerySets.
|
|
||||||
2. A default set of create/read/update/delete operations."""
|
|
||||||
|
|
||||||
# List of validators to validate, cleanup and type-ify the request content
|
|
||||||
validators = (validators.ModelFormValidator,)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# By default the form used with be a ModelForm for self.model
|
|
||||||
# If you wish to override this behaviour or provide a sub-classed ModelForm
|
|
||||||
# you should explicitly set the form attribute on your class.
|
|
||||||
form = None
|
|
||||||
|
|
||||||
# By default the set of input fields will be the same as the set of output fields
|
|
||||||
# If you wish to override this behaviour you should explicitly set the
|
|
||||||
# form_fields attribute on your class.
|
|
||||||
#form_fields = None
|
|
||||||
|
|
||||||
|
|
||||||
#def get_form(self, content=None):
|
|
||||||
# """Return a form that may be used in validation and/or rendering an html renderer"""
|
|
||||||
# if self.form:
|
|
||||||
# return super(self.__class__, self).get_form(content)
|
|
||||||
#
|
|
||||||
# elif self.model:
|
|
||||||
#
|
|
||||||
# class NewModelForm(ModelForm):
|
|
||||||
# class Meta:
|
|
||||||
# model = self.model
|
|
||||||
# fields = self.form_fields if self.form_fields else None
|
|
||||||
#
|
|
||||||
# if content and isinstance(content, Model):
|
|
||||||
# return NewModelForm(instance=content)
|
|
||||||
# elif content:
|
|
||||||
# return NewModelForm(content)
|
|
||||||
#
|
|
||||||
# return NewModelForm()
|
|
||||||
#
|
|
||||||
# return None
|
|
||||||
|
|
||||||
|
|
||||||
#def cleanup_request(self, data, form_instance):
|
|
||||||
# """Override cleanup_request to drop read-only fields from the input prior to validation.
|
|
||||||
# This ensures that we don't error out with 'non-existent field' when these fields are supplied,
|
|
||||||
# and allows for a pragmatic approach to resources which include read-only elements.
|
|
||||||
#
|
|
||||||
# I would actually like to be strict and verify the value of correctness of the values in these fields,
|
|
||||||
# although that gets tricky as it involves validating at the point that we get the model instance.
|
|
||||||
#
|
|
||||||
# See here for another example of this approach:
|
|
||||||
# http://fedoraproject.org/wiki/Cloud_APIs_REST_Style_Guide
|
|
||||||
# https://www.redhat.com/archives/rest-practices/2010-April/thread.html#00041"""
|
|
||||||
# read_only_fields = set(self.fields) - set(self.form_instance.fields)
|
|
||||||
# input_fields = set(data.keys())
|
|
||||||
#
|
|
||||||
# clean_data = {}
|
|
||||||
# for key in input_fields - read_only_fields:
|
|
||||||
# clean_data[key] = data[key]
|
|
||||||
#
|
|
||||||
# return super(ModelResource, self).cleanup_request(clean_data, form_instance)
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup_response(self, data):
|
|
||||||
"""A munging of Piston's pre-serialization. Returns a dict"""
|
|
||||||
|
|
||||||
def _any(thing, fields=()):
|
|
||||||
"""
|
|
||||||
Dispatch, all types are routed through here.
|
|
||||||
"""
|
|
||||||
ret = None
|
|
||||||
|
|
||||||
if isinstance(thing, QuerySet):
|
|
||||||
ret = _qs(thing, fields=fields)
|
|
||||||
elif isinstance(thing, (tuple, list)):
|
|
||||||
ret = _list(thing)
|
|
||||||
elif isinstance(thing, dict):
|
|
||||||
ret = _dict(thing)
|
|
||||||
elif isinstance(thing, int):
|
|
||||||
ret = thing
|
|
||||||
elif isinstance(thing, bool):
|
|
||||||
ret = thing
|
|
||||||
elif isinstance(thing, type(None)):
|
|
||||||
ret = thing
|
|
||||||
elif isinstance(thing, decimal.Decimal):
|
|
||||||
ret = str(thing)
|
|
||||||
elif isinstance(thing, Model):
|
|
||||||
ret = _model(thing, fields=fields)
|
|
||||||
#elif isinstance(thing, HttpResponse): TRC
|
|
||||||
# raise HttpStatusCode(thing)
|
|
||||||
elif inspect.isfunction(thing):
|
|
||||||
if not inspect.getargspec(thing)[0]:
|
|
||||||
ret = _any(thing())
|
|
||||||
elif hasattr(thing, '__rendertable__'):
|
|
||||||
f = thing.__rendertable__
|
|
||||||
if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
|
|
||||||
ret = _any(f())
|
|
||||||
else:
|
|
||||||
ret = unicode(thing) # TRC TODO: Change this back!
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def _fk(data, field):
|
|
||||||
"""
|
|
||||||
Foreign keys.
|
|
||||||
"""
|
|
||||||
return _any(getattr(data, field.name))
|
|
||||||
|
|
||||||
def _related(data, fields=()):
|
|
||||||
"""
|
|
||||||
Foreign keys.
|
|
||||||
"""
|
|
||||||
return [ _model(m, fields) for m in data.iterator() ]
|
|
||||||
|
|
||||||
def _m2m(data, field, fields=()):
|
|
||||||
"""
|
|
||||||
Many to many (re-route to `_model`.)
|
|
||||||
"""
|
|
||||||
return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
|
|
||||||
|
|
||||||
|
|
||||||
def _method_fields(data, fields):
|
|
||||||
if not data:
|
|
||||||
return { }
|
|
||||||
|
|
||||||
has = dir(data)
|
|
||||||
ret = dict()
|
|
||||||
|
|
||||||
for field in fields:
|
|
||||||
if field in has:
|
|
||||||
ret[field] = getattr(data, field)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def _model(data, fields=()):
|
|
||||||
"""
|
|
||||||
Models. Will respect the `fields` and/or
|
|
||||||
`exclude` on the handler (see `typemapper`.)
|
|
||||||
"""
|
|
||||||
ret = { }
|
|
||||||
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
|
|
||||||
handler = None # TRC
|
|
||||||
get_absolute_url = False
|
|
||||||
|
|
||||||
if handler or fields:
|
|
||||||
v = lambda f: getattr(data, f.attname)
|
|
||||||
|
|
||||||
if not fields:
|
|
||||||
"""
|
|
||||||
Fields was not specified, try to find teh correct
|
|
||||||
version in the typemapper we were sent.
|
|
||||||
"""
|
|
||||||
mapped = self.in_typemapper(type(data), self.anonymous)
|
|
||||||
get_fields = set(mapped.fields)
|
|
||||||
exclude_fields = set(mapped.exclude).difference(get_fields)
|
|
||||||
|
|
||||||
if not get_fields:
|
|
||||||
get_fields = set([ f.attname.replace("_id", "", 1)
|
|
||||||
for f in data._meta.fields ])
|
|
||||||
|
|
||||||
# sets can be negated.
|
|
||||||
for exclude in exclude_fields:
|
|
||||||
if isinstance(exclude, basestring):
|
|
||||||
get_fields.discard(exclude)
|
|
||||||
|
|
||||||
elif isinstance(exclude, re._pattern_type):
|
|
||||||
for field in get_fields.copy():
|
|
||||||
if exclude.match(field):
|
|
||||||
get_fields.discard(field)
|
|
||||||
|
|
||||||
get_absolute_url = True
|
|
||||||
|
|
||||||
else:
|
|
||||||
get_fields = set(fields)
|
|
||||||
if 'absolute_url' in get_fields: # MOVED (TRC)
|
|
||||||
get_absolute_url = True
|
|
||||||
|
|
||||||
met_fields = _method_fields(handler, get_fields) # TRC
|
|
||||||
|
|
||||||
for f in data._meta.local_fields:
|
|
||||||
if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
|
|
||||||
if not f.rel:
|
|
||||||
if f.attname in get_fields:
|
|
||||||
ret[f.attname] = _any(v(f))
|
|
||||||
get_fields.remove(f.attname)
|
|
||||||
else:
|
|
||||||
if f.attname[:-3] in get_fields:
|
|
||||||
ret[f.name] = _fk(data, f)
|
|
||||||
get_fields.remove(f.name)
|
|
||||||
|
|
||||||
for mf in data._meta.many_to_many:
|
|
||||||
if mf.serialize and mf.attname not in met_fields:
|
|
||||||
if mf.attname in get_fields:
|
|
||||||
ret[mf.name] = _m2m(data, mf)
|
|
||||||
get_fields.remove(mf.name)
|
|
||||||
|
|
||||||
# try to get the remainder of fields
|
|
||||||
for maybe_field in get_fields:
|
|
||||||
|
|
||||||
if isinstance(maybe_field, (list, tuple)):
|
|
||||||
model, fields = maybe_field
|
|
||||||
inst = getattr(data, model, None)
|
|
||||||
|
|
||||||
if inst:
|
|
||||||
if hasattr(inst, 'all'):
|
|
||||||
ret[model] = _related(inst, fields)
|
|
||||||
elif callable(inst):
|
|
||||||
if len(inspect.getargspec(inst)[0]) == 1:
|
|
||||||
ret[model] = _any(inst(), fields)
|
|
||||||
else:
|
|
||||||
ret[model] = _model(inst, fields)
|
|
||||||
|
|
||||||
elif maybe_field in met_fields:
|
|
||||||
# Overriding normal field which has a "resource method"
|
|
||||||
# so you can alter the contents of certain fields without
|
|
||||||
# using different names.
|
|
||||||
ret[maybe_field] = _any(met_fields[maybe_field](data))
|
|
||||||
|
|
||||||
else:
|
|
||||||
maybe = getattr(data, maybe_field, None)
|
|
||||||
if maybe:
|
|
||||||
if callable(maybe):
|
|
||||||
if len(inspect.getargspec(maybe)[0]) == 1:
|
|
||||||
ret[maybe_field] = _any(maybe())
|
|
||||||
else:
|
|
||||||
ret[maybe_field] = _any(maybe)
|
|
||||||
else:
|
|
||||||
pass # TRC
|
|
||||||
#handler_f = getattr(handler or self.handler, maybe_field, None)
|
|
||||||
#
|
|
||||||
#if handler_f:
|
|
||||||
# ret[maybe_field] = _any(handler_f(data))
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Add absolute_url if it exists
|
|
||||||
get_absolute_url = True
|
|
||||||
|
|
||||||
# Add all the fields
|
|
||||||
for f in data._meta.fields:
|
|
||||||
if f.attname != 'id':
|
|
||||||
ret[f.attname] = _any(getattr(data, f.attname))
|
|
||||||
|
|
||||||
# Add all the propertiess
|
|
||||||
klass = data.__class__
|
|
||||||
for attr in dir(klass):
|
|
||||||
if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property):
|
|
||||||
#if attr.endswith('_url') or attr.endswith('_uri'):
|
|
||||||
# ret[attr] = self.make_absolute(_any(getattr(data, attr)))
|
|
||||||
#else:
|
|
||||||
ret[attr] = _any(getattr(data, attr))
|
|
||||||
#fields = dir(data.__class__) + ret.keys()
|
|
||||||
#add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
|
|
||||||
#print add_ons
|
|
||||||
###print dir(data.__class__)
|
|
||||||
#from django.db.models import Model
|
|
||||||
#model_fields = dir(Model)
|
|
||||||
|
|
||||||
#for attr in dir(data):
|
|
||||||
## #if attr.startswith('_'):
|
|
||||||
## # continue
|
|
||||||
# if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
|
|
||||||
# print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
|
|
||||||
|
|
||||||
#for k in add_ons:
|
|
||||||
# ret[k] = _any(getattr(data, k))
|
|
||||||
|
|
||||||
# TRC
|
|
||||||
# resouce uri
|
|
||||||
#if self.in_typemapper(type(data), self.anonymous):
|
|
||||||
# handler = self.in_typemapper(type(data), self.anonymous)
|
|
||||||
# if hasattr(handler, 'resource_uri'):
|
|
||||||
# url_id, fields = handler.resource_uri()
|
|
||||||
# ret['resource_uri'] = permalink( lambda: (url_id,
|
|
||||||
# (getattr(data, f) for f in fields) ) )()
|
|
||||||
|
|
||||||
# TRC
|
|
||||||
#if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
|
|
||||||
# try: ret['resource_uri'] = data.get_api_url()
|
|
||||||
# except: pass
|
|
||||||
|
|
||||||
# absolute uri
|
|
||||||
if hasattr(data, 'get_absolute_url') and get_absolute_url:
|
|
||||||
try: ret['absolute_url'] = data.get_absolute_url()
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
#for key, val in ret.items():
|
|
||||||
# if key.endswith('_url') or key.endswith('_uri'):
|
|
||||||
# ret[key] = self.add_domain(val)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def _qs(data, fields=()):
|
|
||||||
"""
|
|
||||||
Querysets.
|
|
||||||
"""
|
|
||||||
return [ _any(v, fields) for v in data ]
|
|
||||||
|
|
||||||
def _list(data):
|
|
||||||
"""
|
|
||||||
Lists.
|
|
||||||
"""
|
|
||||||
return [ _any(v) for v in data ]
|
|
||||||
|
|
||||||
def _dict(data):
|
|
||||||
"""
|
|
||||||
Dictionaries.
|
|
||||||
"""
|
|
||||||
return dict([ (k, _any(v)) for k, v in data.iteritems() ])
|
|
||||||
|
|
||||||
# Kickstart the seralizin'.
|
|
||||||
return _any(data, self.fields)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InstanceModelResource(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelResource):
|
|
||||||
"""A view which provides default operations for read/update/delete against a model instance."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ListOrCreateModelResource(CreateModelMixin, ListModelMixin, ModelResource):
|
|
||||||
"""A Resource which provides default operations for list and create."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ListModelResource(ListModelMixin, ModelResource):
|
|
||||||
"""Resource with default operations for list."""
|
|
||||||
pass
|
|
|
@ -11,6 +11,12 @@ class BasePermission(object):
|
||||||
def has_permission(self, auth):
|
def has_permission(self, auth):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class FullAnonAccess(BasePermission):
|
||||||
|
""""""
|
||||||
|
def has_permission(self, auth):
|
||||||
|
return True
|
||||||
|
|
||||||
class IsAuthenticated(BasePermission):
|
class IsAuthenticated(BasePermission):
|
||||||
""""""
|
""""""
|
||||||
def has_permission(self, auth):
|
def has_permission(self, auth):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Renderers are used to serialize a Resource's output into specific media types.
|
"""Renderers are used to serialize a View's output into specific media types.
|
||||||
django-rest-framework also provides HTML and PlainText renderers that help self-document the API,
|
django-rest-framework also provides HTML and PlainText renderers that help self-document the API,
|
||||||
by serializing the output along with documentation regarding the Resource, output status and headers,
|
by serializing the output along with documentation regarding the Resource, output status and headers,
|
||||||
and providing forms and links depending on the allowed methods, renderers and parsers on the Resource.
|
and providing forms and links depending on the allowed methods, renderers and parsers on the Resource.
|
||||||
|
|
|
@ -1,130 +1,251 @@
|
||||||
from django.core.urlresolvers import set_script_prefix
|
from django.db.models import Model
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.db.models.query import QuerySet
|
||||||
|
from django.db.models.fields.related import RelatedField
|
||||||
|
|
||||||
from djangorestframework.compat import View
|
import decimal
|
||||||
from djangorestframework.response import Response, ErrorResponse
|
import inspect
|
||||||
from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
|
import re
|
||||||
from djangorestframework import renderers, parsers, authentication, permissions, validators, status
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Figure how out references and named urls need to work nicely
|
class Resource(object):
|
||||||
# TODO: POST on existing 404 URL, PUT on existing 404 URL
|
"""A Resource determines how an object maps to a serializable entity.
|
||||||
#
|
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets."""
|
||||||
# NEXT: Exceptions on func() -> 500, tracebacks renderted if settings.DEBUG
|
|
||||||
|
|
||||||
__all__ = ['Resource']
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
|
|
||||||
"""Handles incoming requests and maps them to REST operations.
|
|
||||||
Performs request deserialization, response serialization, authentication and input validation."""
|
|
||||||
|
|
||||||
# List of renderers the resource can serialize the response with, ordered by preference.
|
|
||||||
renderers = ( renderers.JSONRenderer,
|
|
||||||
renderers.DocumentingHTMLRenderer,
|
|
||||||
renderers.DocumentingXHTMLRenderer,
|
|
||||||
renderers.DocumentingPlainTextRenderer,
|
|
||||||
renderers.XMLRenderer )
|
|
||||||
|
|
||||||
# List of parsers the resource can parse the request with.
|
|
||||||
parsers = ( parsers.JSONParser,
|
|
||||||
parsers.FormParser,
|
|
||||||
parsers.MultipartParser )
|
|
||||||
|
|
||||||
# List of validators to validate, cleanup and normalize the request content
|
|
||||||
validators = ( validators.FormValidator, )
|
|
||||||
|
|
||||||
# List of all authenticating methods to attempt.
|
|
||||||
authentication = ( authentication.UserLoggedInAuthenticator,
|
|
||||||
authentication.BasicAuthenticator )
|
|
||||||
|
|
||||||
# List of all permissions required to access the resource
|
# The model attribute refers to the Django Model which this Resource maps to.
|
||||||
permissions = ()
|
# (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
|
||||||
|
|
||||||
# Optional form for input validation and presentation of HTML formatted responses.
|
@classmethod
|
||||||
form = None
|
def object_to_serializable(self, data):
|
||||||
|
"""A (horrible) munging of Piston's pre-serialization. Returns a dict"""
|
||||||
|
|
||||||
# Allow name and description for the Resource to be set explicitly,
|
def _any(thing, fields=()):
|
||||||
# overiding the default classname/docstring behaviour.
|
"""
|
||||||
# These are used for documentation in the standard html and text renderers.
|
Dispatch, all types are routed through here.
|
||||||
name = None
|
"""
|
||||||
description = None
|
ret = None
|
||||||
|
|
||||||
|
if isinstance(thing, QuerySet):
|
||||||
|
ret = _qs(thing, fields=fields)
|
||||||
|
elif isinstance(thing, (tuple, list)):
|
||||||
|
ret = _list(thing)
|
||||||
|
elif isinstance(thing, dict):
|
||||||
|
ret = _dict(thing)
|
||||||
|
elif isinstance(thing, int):
|
||||||
|
ret = thing
|
||||||
|
elif isinstance(thing, bool):
|
||||||
|
ret = thing
|
||||||
|
elif isinstance(thing, type(None)):
|
||||||
|
ret = thing
|
||||||
|
elif isinstance(thing, decimal.Decimal):
|
||||||
|
ret = str(thing)
|
||||||
|
elif isinstance(thing, Model):
|
||||||
|
ret = _model(thing, fields=fields)
|
||||||
|
#elif isinstance(thing, HttpResponse): TRC
|
||||||
|
# raise HttpStatusCode(thing)
|
||||||
|
elif inspect.isfunction(thing):
|
||||||
|
if not inspect.getargspec(thing)[0]:
|
||||||
|
ret = _any(thing())
|
||||||
|
elif hasattr(thing, '__rendertable__'):
|
||||||
|
f = thing.__rendertable__
|
||||||
|
if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
|
||||||
|
ret = _any(f())
|
||||||
|
else:
|
||||||
|
ret = unicode(thing) # TRC TODO: Change this back!
|
||||||
|
|
||||||
@property
|
return ret
|
||||||
def allowed_methods(self):
|
|
||||||
return [method.upper() for method in self.http_method_names if hasattr(self, method)]
|
|
||||||
|
|
||||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
def _fk(data, field):
|
||||||
"""Return an HTTP 405 error if an operation is called which does not have a handler method."""
|
"""
|
||||||
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
|
Foreign keys.
|
||||||
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
"""
|
||||||
|
return _any(getattr(data, field.name))
|
||||||
|
|
||||||
def cleanup_response(self, data):
|
def _related(data, fields=()):
|
||||||
"""Perform any resource-specific data filtering prior to the standard HTTP
|
"""
|
||||||
content-type serialization.
|
Foreign keys.
|
||||||
|
"""
|
||||||
Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.
|
return [ _model(m, fields) for m in data.iterator() ]
|
||||||
|
|
||||||
|
def _m2m(data, field, fields=()):
|
||||||
|
"""
|
||||||
|
Many to many (re-route to `_model`.)
|
||||||
|
"""
|
||||||
|
return [ _model(m, fields) for m in getattr(data, field.name).iterator() ]
|
||||||
|
|
||||||
TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into
|
|
||||||
the RendererMixin and Renderer classes."""
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
def _method_fields(data, fields):
|
||||||
|
if not data:
|
||||||
|
return { }
|
||||||
|
|
||||||
|
has = dir(data)
|
||||||
|
ret = dict()
|
||||||
|
|
||||||
|
for field in fields:
|
||||||
|
if field in has:
|
||||||
|
ret[field] = getattr(data, field)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
# Note: session based authentication is explicitly CSRF validated,
|
def _model(data, fields=()):
|
||||||
# all other authentication is CSRF exempt.
|
"""
|
||||||
@csrf_exempt
|
Models. Will respect the `fields` and/or
|
||||||
def dispatch(self, request, *args, **kwargs):
|
`exclude` on the handler (see `typemapper`.)
|
||||||
try:
|
"""
|
||||||
self.request = request
|
ret = { }
|
||||||
self.args = args
|
#handler = self.in_typemapper(type(data), self.anonymous) # TRC
|
||||||
self.kwargs = kwargs
|
handler = None # TRC
|
||||||
|
get_absolute_url = False
|
||||||
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
|
|
||||||
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
|
if fields:
|
||||||
set_script_prefix(prefix)
|
v = lambda f: getattr(data, f.attname)
|
||||||
|
|
||||||
try:
|
|
||||||
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
|
|
||||||
# self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
|
|
||||||
self.perform_form_overloading()
|
|
||||||
|
|
||||||
# Authenticate and check request is has the relevant permissions
|
|
||||||
self.check_permissions()
|
|
||||||
|
|
||||||
# Get the appropriate handler method
|
|
||||||
if self.method.lower() in self.http_method_names:
|
|
||||||
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
|
|
||||||
else:
|
|
||||||
handler = self.http_method_not_allowed
|
|
||||||
|
|
||||||
response_obj = handler(request, *args, **kwargs)
|
|
||||||
|
|
||||||
# Allow return value to be either Response, or an object, or None
|
|
||||||
if isinstance(response_obj, Response):
|
|
||||||
response = response_obj
|
|
||||||
elif response_obj is not None:
|
|
||||||
response = Response(status.HTTP_200_OK, response_obj)
|
|
||||||
else:
|
|
||||||
response = Response(status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
|
||||||
response.cleaned_content = self.cleanup_response(response.raw_content)
|
|
||||||
|
|
||||||
except ErrorResponse, exc:
|
|
||||||
response = exc.response
|
|
||||||
|
|
||||||
# Always add these headers.
|
|
||||||
#
|
|
||||||
# TODO - this isn't actually the correct way to set the vary header,
|
|
||||||
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
|
||||||
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
|
||||||
response.headers['Vary'] = 'Authenticate, Accept'
|
|
||||||
|
|
||||||
return self.render(response)
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
|
get_fields = set(fields)
|
||||||
|
if 'absolute_url' in get_fields: # MOVED (TRC)
|
||||||
|
get_absolute_url = True
|
||||||
|
|
||||||
|
met_fields = _method_fields(handler, get_fields) # TRC
|
||||||
|
|
||||||
|
for f in data._meta.local_fields:
|
||||||
|
if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
|
||||||
|
if not f.rel:
|
||||||
|
if f.attname in get_fields:
|
||||||
|
ret[f.attname] = _any(v(f))
|
||||||
|
get_fields.remove(f.attname)
|
||||||
|
else:
|
||||||
|
if f.attname[:-3] in get_fields:
|
||||||
|
ret[f.name] = _fk(data, f)
|
||||||
|
get_fields.remove(f.name)
|
||||||
|
|
||||||
|
for mf in data._meta.many_to_many:
|
||||||
|
if mf.serialize and mf.attname not in met_fields:
|
||||||
|
if mf.attname in get_fields:
|
||||||
|
ret[mf.name] = _m2m(data, mf)
|
||||||
|
get_fields.remove(mf.name)
|
||||||
|
|
||||||
|
# try to get the remainder of fields
|
||||||
|
for maybe_field in get_fields:
|
||||||
|
|
||||||
|
if isinstance(maybe_field, (list, tuple)):
|
||||||
|
model, fields = maybe_field
|
||||||
|
inst = getattr(data, model, None)
|
||||||
|
|
||||||
|
if inst:
|
||||||
|
if hasattr(inst, 'all'):
|
||||||
|
ret[model] = _related(inst, fields)
|
||||||
|
elif callable(inst):
|
||||||
|
if len(inspect.getargspec(inst)[0]) == 1:
|
||||||
|
ret[model] = _any(inst(), fields)
|
||||||
|
else:
|
||||||
|
ret[model] = _model(inst, fields)
|
||||||
|
|
||||||
|
elif maybe_field in met_fields:
|
||||||
|
# Overriding normal field which has a "resource method"
|
||||||
|
# so you can alter the contents of certain fields without
|
||||||
|
# using different names.
|
||||||
|
ret[maybe_field] = _any(met_fields[maybe_field](data))
|
||||||
|
|
||||||
|
else:
|
||||||
|
maybe = getattr(data, maybe_field, None)
|
||||||
|
if maybe:
|
||||||
|
if callable(maybe):
|
||||||
|
if len(inspect.getargspec(maybe)[0]) == 1:
|
||||||
|
ret[maybe_field] = _any(maybe())
|
||||||
|
else:
|
||||||
|
ret[maybe_field] = _any(maybe)
|
||||||
|
else:
|
||||||
|
pass # TRC
|
||||||
|
#handler_f = getattr(handler or self.handler, maybe_field, None)
|
||||||
|
#
|
||||||
|
#if handler_f:
|
||||||
|
# ret[maybe_field] = _any(handler_f(data))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Add absolute_url if it exists
|
||||||
|
get_absolute_url = True
|
||||||
|
|
||||||
|
# Add all the fields
|
||||||
|
for f in data._meta.fields:
|
||||||
|
if f.attname != 'id':
|
||||||
|
ret[f.attname] = _any(getattr(data, f.attname))
|
||||||
|
|
||||||
|
# Add all the propertiess
|
||||||
|
klass = data.__class__
|
||||||
|
for attr in dir(klass):
|
||||||
|
if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property):
|
||||||
|
#if attr.endswith('_url') or attr.endswith('_uri'):
|
||||||
|
# ret[attr] = self.make_absolute(_any(getattr(data, attr)))
|
||||||
|
#else:
|
||||||
|
ret[attr] = _any(getattr(data, attr))
|
||||||
|
#fields = dir(data.__class__) + ret.keys()
|
||||||
|
#add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')]
|
||||||
|
#print add_ons
|
||||||
|
###print dir(data.__class__)
|
||||||
|
#from django.db.models import Model
|
||||||
|
#model_fields = dir(Model)
|
||||||
|
|
||||||
|
#for attr in dir(data):
|
||||||
|
## #if attr.startswith('_'):
|
||||||
|
## # continue
|
||||||
|
# if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'):
|
||||||
|
# print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields
|
||||||
|
|
||||||
|
#for k in add_ons:
|
||||||
|
# ret[k] = _any(getattr(data, k))
|
||||||
|
|
||||||
|
# TRC
|
||||||
|
# resouce uri
|
||||||
|
#if self.in_typemapper(type(data), self.anonymous):
|
||||||
|
# handler = self.in_typemapper(type(data), self.anonymous)
|
||||||
|
# if hasattr(handler, 'resource_uri'):
|
||||||
|
# url_id, fields = handler.resource_uri()
|
||||||
|
# ret['resource_uri'] = permalink( lambda: (url_id,
|
||||||
|
# (getattr(data, f) for f in fields) ) )()
|
||||||
|
|
||||||
|
# TRC
|
||||||
|
#if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
|
||||||
|
# try: ret['resource_uri'] = data.get_api_url()
|
||||||
|
# except: pass
|
||||||
|
|
||||||
|
# absolute uri
|
||||||
|
if hasattr(data, 'get_absolute_url') and get_absolute_url:
|
||||||
|
try: ret['absolute_url'] = data.get_absolute_url()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
#for key, val in ret.items():
|
||||||
|
# if key.endswith('_url') or key.endswith('_uri'):
|
||||||
|
# ret[key] = self.add_domain(val)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _qs(data, fields=()):
|
||||||
|
"""
|
||||||
|
Querysets.
|
||||||
|
"""
|
||||||
|
return [ _any(v, fields) for v in data ]
|
||||||
|
|
||||||
|
def _list(data):
|
||||||
|
"""
|
||||||
|
Lists.
|
||||||
|
"""
|
||||||
|
return [ _any(v) for v in data ]
|
||||||
|
|
||||||
|
def _dict(data):
|
||||||
|
"""
|
||||||
|
Dictionaries.
|
||||||
|
"""
|
||||||
|
return dict([ (k, _any(v)) for k, v in data.iteritems() ])
|
||||||
|
|
||||||
|
# Kickstart the seralizin'.
|
||||||
|
return _any(data, self.fields)
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,6 @@ class Response(object):
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(BaseException):
|
class ErrorResponse(BaseException):
|
||||||
"""An exception representing an HttpResponse that should be returned immediatley."""
|
"""An exception representing an HttpResponse that should be returned immediately."""
|
||||||
def __init__(self, status, content=None, headers={}):
|
def __init__(self, status, content=None, headers={}):
|
||||||
self.response = Response(status, content=content, headers=headers)
|
self.response = Response(status, content=content, headers=headers)
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<div id="content" class="colM">
|
<div id="content" class="colM">
|
||||||
|
|
||||||
<div id="content-main">
|
<div id="content-main">
|
||||||
<form method="post" action="{% url djangorestframework.views.api_login %}" id="login-form">
|
<form method="post" action="{% url djangorestframework.utils.staticviews.api_login %}" id="login-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="id_username">Username:</label> {{ form.username }}
|
<label for="id_username">Username:</label> {{ form.username }}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.resource import Resource
|
from djangorestframework.views import BaseView
|
||||||
|
|
||||||
|
|
||||||
# See: http://www.useragentstring.com/
|
# See: http://www.useragentstring.com/
|
||||||
|
@ -19,15 +19,15 @@ class UserAgentMungingTest(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
permissions = ()
|
permissions = ()
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return {'a':1, 'b':2, 'c':3}
|
return {'a':1, 'b':2, 'c':3}
|
||||||
|
|
||||||
self.req = RequestFactory()
|
self.req = RequestFactory()
|
||||||
self.MockResource = MockResource
|
self.MockView = MockView
|
||||||
self.view = MockResource.as_view()
|
self.view = MockView.as_view()
|
||||||
|
|
||||||
def test_munge_msie_accept_header(self):
|
def test_munge_msie_accept_header(self):
|
||||||
"""Send MSIE user agent strings and ensure that we get an HTML response,
|
"""Send MSIE user agent strings and ensure that we get an HTML response,
|
||||||
|
@ -42,7 +42,7 @@ class UserAgentMungingTest(TestCase):
|
||||||
def test_dont_rewrite_msie_accept_header(self):
|
def test_dont_rewrite_msie_accept_header(self):
|
||||||
"""Turn off REWRITE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
"""Turn off REWRITE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure
|
||||||
that we get a JSON response if we set a */* accept header."""
|
that we get a JSON response if we set a */* accept header."""
|
||||||
view = self.MockResource.as_view(REWRITE_IE_ACCEPT_HEADER=False)
|
view = self.MockView.as_view(REWRITE_IE_ACCEPT_HEADER=False)
|
||||||
|
|
||||||
for user_agent in (MSIE_9_USER_AGENT,
|
for user_agent in (MSIE_9_USER_AGENT,
|
||||||
MSIE_8_USER_AGENT,
|
MSIE_8_USER_AGENT,
|
||||||
|
|
|
@ -6,19 +6,19 @@ from django.test import Client, TestCase
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.resource import Resource
|
from djangorestframework.views import BaseView
|
||||||
from djangorestframework import permissions
|
from djangorestframework import permissions
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
permissions = ( permissions.IsAuthenticated, )
|
permissions = ( permissions.IsAuthenticated, )
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
return {'a':1, 'b':2, 'c':3}
|
return {'a':1, 'b':2, 'c':3}
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^$', MockResource.as_view()),
|
(r'^$', MockView.as_view()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
from django.conf.urls.defaults import patterns, url
|
from django.conf.urls.defaults import patterns, url
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
||||||
from djangorestframework.resource import Resource
|
from djangorestframework.views import BaseView
|
||||||
|
|
||||||
class Root(Resource):
|
class Root(BaseView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ResourceRoot(Resource):
|
class ResourceRoot(BaseView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ResourceInstance(Resource):
|
class ResourceInstance(BaseView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class NestedResourceRoot(Resource):
|
class NestedResourceRoot(BaseView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class NestedResourceInstance(Resource):
|
class NestedResourceInstance(BaseView):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.resource import Resource
|
from djangorestframework.views import BaseView
|
||||||
from djangorestframework.compat import apply_markdown
|
from djangorestframework.compat import apply_markdown
|
||||||
from djangorestframework.utils.description import get_name, get_description
|
from djangorestframework.utils.description import get_name, get_description
|
||||||
|
|
||||||
|
@ -32,23 +32,23 @@ MARKED_DOWN = """<h2>an example docstring</h2>
|
||||||
<h2 id="hash_style_header">hash style header</h2>"""
|
<h2 id="hash_style_header">hash style header</h2>"""
|
||||||
|
|
||||||
|
|
||||||
class TestResourceNamesAndDescriptions(TestCase):
|
class TestViewNamesAndDescriptions(TestCase):
|
||||||
def test_resource_name_uses_classname_by_default(self):
|
def test_resource_name_uses_classname_by_default(self):
|
||||||
"""Ensure Resource names are based on the classname by default."""
|
"""Ensure Resource names are based on the classname by default."""
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
pass
|
pass
|
||||||
self.assertEquals(get_name(MockResource()), 'Mock Resource')
|
self.assertEquals(get_name(MockView()), 'Mock View')
|
||||||
|
|
||||||
def test_resource_name_can_be_set_explicitly(self):
|
def test_resource_name_can_be_set_explicitly(self):
|
||||||
"""Ensure Resource names can be set using the 'name' class attribute."""
|
"""Ensure Resource names can be set using the 'name' class attribute."""
|
||||||
example = 'Some Other Name'
|
example = 'Some Other Name'
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
name = example
|
name = example
|
||||||
self.assertEquals(get_name(MockResource()), example)
|
self.assertEquals(get_name(MockView()), example)
|
||||||
|
|
||||||
def test_resource_description_uses_docstring_by_default(self):
|
def test_resource_description_uses_docstring_by_default(self):
|
||||||
"""Ensure Resource names are based on the docstring by default."""
|
"""Ensure Resource names are based on the docstring by default."""
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
"""an example docstring
|
"""an example docstring
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
@ -64,28 +64,28 @@ class TestResourceNamesAndDescriptions(TestCase):
|
||||||
|
|
||||||
# hash style header #"""
|
# hash style header #"""
|
||||||
|
|
||||||
self.assertEquals(get_description(MockResource()), DESCRIPTION)
|
self.assertEquals(get_description(MockView()), DESCRIPTION)
|
||||||
|
|
||||||
def test_resource_description_can_be_set_explicitly(self):
|
def test_resource_description_can_be_set_explicitly(self):
|
||||||
"""Ensure Resource descriptions can be set using the 'description' class attribute."""
|
"""Ensure Resource descriptions can be set using the 'description' class attribute."""
|
||||||
example = 'Some other description'
|
example = 'Some other description'
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
"""docstring"""
|
"""docstring"""
|
||||||
description = example
|
description = example
|
||||||
self.assertEquals(get_description(MockResource()), example)
|
self.assertEquals(get_description(MockView()), example)
|
||||||
|
|
||||||
def test_resource_description_does_not_require_docstring(self):
|
def test_resource_description_does_not_require_docstring(self):
|
||||||
"""Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
|
"""Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute."""
|
||||||
example = 'Some other description'
|
example = 'Some other description'
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
description = example
|
description = example
|
||||||
self.assertEquals(get_description(MockResource()), example)
|
self.assertEquals(get_description(MockView()), example)
|
||||||
|
|
||||||
def test_resource_description_can_be_empty(self):
|
def test_resource_description_can_be_empty(self):
|
||||||
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
|
"""Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string"""
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
pass
|
pass
|
||||||
self.assertEquals(get_description(MockResource()), '')
|
self.assertEquals(get_description(MockView()), '')
|
||||||
|
|
||||||
def test_markdown(self):
|
def test_markdown(self):
|
||||||
"""Ensure markdown to HTML works as expected"""
|
"""Ensure markdown to HTML works as expected"""
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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.resource import Resource
|
from djangorestframework.views import BaseView
|
||||||
import StringIO
|
import StringIO
|
||||||
|
|
||||||
class UploadFilesTests(TestCase):
|
class UploadFilesTests(TestCase):
|
||||||
|
@ -15,7 +15,7 @@ class UploadFilesTests(TestCase):
|
||||||
class FileForm(forms.Form):
|
class FileForm(forms.Form):
|
||||||
file = forms.FileField
|
file = forms.FileField
|
||||||
|
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
permissions = ()
|
permissions = ()
|
||||||
form = FileForm
|
form = FileForm
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class UploadFilesTests(TestCase):
|
||||||
file = StringIO.StringIO('stuff')
|
file = StringIO.StringIO('stuff')
|
||||||
file.name = 'stuff.txt'
|
file.name = 'stuff.txt'
|
||||||
request = self.factory.post('/', {'file': file})
|
request = self.factory.post('/', {'file': file})
|
||||||
view = MockResource.as_view()
|
view = MockView.as_view()
|
||||||
response = view(request)
|
response = view(request)
|
||||||
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
|
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
..
|
..
|
||||||
>>> from djangorestframework.parsers import FormParser
|
>>> from djangorestframework.parsers import FormParser
|
||||||
>>> from djangorestframework.compat import RequestFactory
|
>>> from djangorestframework.compat import RequestFactory
|
||||||
>>> from djangorestframework.resource import Resource
|
>>> from djangorestframework.views import BaseView
|
||||||
>>> from StringIO import StringIO
|
>>> from StringIO import StringIO
|
||||||
>>> from urllib import urlencode
|
>>> from urllib import urlencode
|
||||||
>>> req = RequestFactory().get('/')
|
>>> req = RequestFactory().get('/')
|
||||||
>>> some_resource = Resource()
|
>>> some_view = BaseView()
|
||||||
>>> some_resource.request = req # Make as if this request had been dispatched
|
>>> some_view.request = req # Make as if this request had been dispatched
|
||||||
|
|
||||||
FormParser
|
FormParser
|
||||||
============
|
============
|
||||||
|
@ -24,7 +24,7 @@ Here is some example data, which would eventually be sent along with a post requ
|
||||||
|
|
||||||
Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter :
|
||||||
|
|
||||||
>>> FormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'}
|
>>> FormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'}
|
||||||
True
|
True
|
||||||
|
|
||||||
However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` :
|
||||||
|
@ -36,7 +36,7 @@ However, you can customize this behaviour by subclassing :class:`parsers.FormPar
|
||||||
|
|
||||||
This new parser only flattens the lists of parameters that contain a single value.
|
This new parser only flattens the lists of parameters that contain a single value.
|
||||||
|
|
||||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||||
True
|
True
|
||||||
|
|
||||||
.. note:: The same functionality is available for :class:`parsers.MultipartParser`.
|
.. note:: The same functionality is available for :class:`parsers.MultipartParser`.
|
||||||
|
@ -61,7 +61,7 @@ The browsers usually strip the parameter completely. A hack to avoid this, and t
|
||||||
|
|
||||||
:class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
:class:`parsers.FormParser` strips the values ``_empty`` from all the lists.
|
||||||
|
|
||||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1'}
|
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1'}
|
||||||
True
|
True
|
||||||
|
|
||||||
Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it.
|
||||||
|
@ -71,7 +71,7 @@ Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a lis
|
||||||
... def is_a_list(self, key, val_list):
|
... def is_a_list(self, key, val_list):
|
||||||
... return key == 'key2'
|
... return key == 'key2'
|
||||||
...
|
...
|
||||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []}
|
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []}
|
||||||
True
|
True
|
||||||
|
|
||||||
Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`.
|
||||||
|
@ -81,7 +81,7 @@ from tempfile import TemporaryFile
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.parsers import MultipartParser
|
from djangorestframework.parsers import MultipartParser
|
||||||
from djangorestframework.resource import Resource
|
from djangorestframework.views import BaseView
|
||||||
from djangorestframework.utils.mediatypes import MediaType
|
from djangorestframework.utils.mediatypes import MediaType
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
@ -122,9 +122,9 @@ class TestMultipartParser(TestCase):
|
||||||
def test_multipartparser(self):
|
def test_multipartparser(self):
|
||||||
"""Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters."""
|
"""Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters."""
|
||||||
post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
|
post_req = RequestFactory().post('/', self.body, content_type=self.content_type)
|
||||||
resource = Resource()
|
view = BaseView()
|
||||||
resource.request = post_req
|
view.request = post_req
|
||||||
parsed = MultipartParser(resource).parse(StringIO(self.body))
|
parsed = MultipartParser(view).parse(StringIO(self.body))
|
||||||
self.assertEqual(parsed['key1'], 'val1')
|
self.assertEqual(parsed['key1'], 'val1')
|
||||||
self.assertEqual(parsed.FILES['file1'].read(), 'blablabla')
|
self.assertEqual(parsed.FILES['file1'].read(), 'blablabla')
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ from django.core.urlresolvers import reverse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
from djangorestframework.resource import Resource
|
from djangorestframework.views import BaseView
|
||||||
|
|
||||||
|
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
|
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
|
||||||
permissions = ()
|
permissions = ()
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ class MockResource(Resource):
|
||||||
return reverse('another')
|
return reverse('another')
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$', MockResource.as_view()),
|
url(r'^$', MockView.as_view()),
|
||||||
url(r'^another$', MockResource.as_view(), name='another'),
|
url(r'^another$', MockView.as_view(), name='another'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@ from django.test import TestCase
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.resource import Resource
|
from djangorestframework.views import BaseView
|
||||||
from djangorestframework.permissions import Throttling
|
from djangorestframework.permissions import Throttling
|
||||||
|
|
||||||
|
|
||||||
class MockResource(Resource):
|
class MockView(BaseView):
|
||||||
permissions = ( Throttling, )
|
permissions = ( Throttling, )
|
||||||
throttle = (3, 1) # 3 requests per second
|
throttle = (3, 1) # 3 requests per second
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ class MockResource(Resource):
|
||||||
return 'foo'
|
return 'foo'
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^$', MockResource.as_view()),
|
(r'^$', MockView.as_view()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ from django.test import TestCase
|
||||||
from djangorestframework.compat import RequestFactory
|
from djangorestframework.compat import RequestFactory
|
||||||
from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator
|
from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator
|
||||||
from djangorestframework.response import ErrorResponse
|
from djangorestframework.response import ErrorResponse
|
||||||
|
from djangorestframework.views import BaseView
|
||||||
|
from djangorestframework.resource import Resource
|
||||||
|
|
||||||
|
|
||||||
class TestValidatorMixinInterfaces(TestCase):
|
class TestValidatorMixinInterfaces(TestCase):
|
||||||
|
@ -20,7 +22,7 @@ class TestDisabledValidations(TestCase):
|
||||||
def test_disabled_form_validator_returns_content_unchanged(self):
|
def test_disabled_form_validator_returns_content_unchanged(self):
|
||||||
"""If the view's form attribute is None then FormValidator(view).validate(content)
|
"""If the view's form attribute is None then FormValidator(view).validate(content)
|
||||||
should just return the content unmodified."""
|
should just return the content unmodified."""
|
||||||
class DisabledFormView(object):
|
class DisabledFormView(BaseView):
|
||||||
form = None
|
form = None
|
||||||
|
|
||||||
view = DisabledFormView()
|
view = DisabledFormView()
|
||||||
|
@ -30,7 +32,7 @@ class TestDisabledValidations(TestCase):
|
||||||
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
def test_disabled_form_validator_get_bound_form_returns_none(self):
|
||||||
"""If the view's form attribute is None on then
|
"""If the view's form attribute is None on then
|
||||||
FormValidator(view).get_bound_form(content) should just return None."""
|
FormValidator(view).get_bound_form(content) should just return None."""
|
||||||
class DisabledFormView(object):
|
class DisabledFormView(BaseView):
|
||||||
form = None
|
form = None
|
||||||
|
|
||||||
view = DisabledFormView()
|
view = DisabledFormView()
|
||||||
|
@ -39,11 +41,10 @@ class TestDisabledValidations(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
def test_disabled_model_form_validator_returns_content_unchanged(self):
|
||||||
"""If the view's form and model attributes are None then
|
"""If the view's form is None and does not have a Resource with a model set then
|
||||||
ModelFormValidator(view).validate(content) should just return the content unmodified."""
|
ModelFormValidator(view).validate(content) should just return the content unmodified."""
|
||||||
class DisabledModelFormView(object):
|
class DisabledModelFormView(BaseView):
|
||||||
form = None
|
form = None
|
||||||
model = None
|
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
view = DisabledModelFormView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty':'uiop'}
|
||||||
|
@ -51,13 +52,12 @@ class TestDisabledValidations(TestCase):
|
||||||
|
|
||||||
def test_disabled_model_form_validator_get_bound_form_returns_none(self):
|
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."""
|
"""If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None."""
|
||||||
class DisabledModelFormView(object):
|
class DisabledModelFormView(BaseView):
|
||||||
form = None
|
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
view = DisabledModelFormView()
|
view = DisabledModelFormView()
|
||||||
content = {'qwerty':'uiop'}
|
content = {'qwerty':'uiop'}
|
||||||
self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)#
|
self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)
|
||||||
|
|
||||||
class TestNonFieldErrors(TestCase):
|
class TestNonFieldErrors(TestCase):
|
||||||
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
"""Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)"""
|
||||||
|
@ -84,7 +84,7 @@ class TestNonFieldErrors(TestCase):
|
||||||
except ErrorResponse, exc:
|
except ErrorResponse, exc:
|
||||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||||
else:
|
else:
|
||||||
self.fail('ResourceException was not raised') #pragma: no cover
|
self.fail('ErrorResponse was not raised') #pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
class TestFormValidation(TestCase):
|
class TestFormValidation(TestCase):
|
||||||
|
@ -95,11 +95,11 @@ class TestFormValidation(TestCase):
|
||||||
class MockForm(forms.Form):
|
class MockForm(forms.Form):
|
||||||
qwerty = forms.CharField(required=True)
|
qwerty = forms.CharField(required=True)
|
||||||
|
|
||||||
class MockFormView(object):
|
class MockFormView(BaseView):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
validators = (FormValidator,)
|
validators = (FormValidator,)
|
||||||
|
|
||||||
class MockModelFormView(object):
|
class MockModelFormView(BaseView):
|
||||||
form = MockForm
|
form = MockForm
|
||||||
validators = (ModelFormValidator,)
|
validators = (ModelFormValidator,)
|
||||||
|
|
||||||
|
@ -264,9 +264,12 @@ class TestModelFormValidator(TestCase):
|
||||||
@property
|
@property
|
||||||
def readonly(self):
|
def readonly(self):
|
||||||
return 'read only'
|
return 'read only'
|
||||||
|
|
||||||
class MockView(object):
|
class MockResource(Resource):
|
||||||
model = MockModel
|
model = MockModel
|
||||||
|
|
||||||
|
class MockView(BaseView):
|
||||||
|
resource = MockResource
|
||||||
|
|
||||||
self.validator = ModelFormValidator(MockView)
|
self.validator = ModelFormValidator(MockView)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.test import TestCase
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('djangorestframework.views',
|
urlpatterns = patterns('djangorestframework.utils.staticviews',
|
||||||
url(r'^robots.txt$', 'deny_robots'),
|
url(r'^robots.txt$', 'deny_robots'),
|
||||||
url(r'^favicon.ico$', 'favicon'),
|
url(r'^favicon.ico$', 'favicon'),
|
||||||
url(r'^accounts/login$', 'api_login'),
|
url(r'^accounts/login$', 'api_login'),
|
||||||
|
|
16
djangorestframework/urls.py
Normal file
16
djangorestframework/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django.conf.urls.defaults import patterns
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
urlpatterns = patterns('djangorestframework.utils.staticviews',
|
||||||
|
(r'robots.txt', 'deny_robots'),
|
||||||
|
(r'^accounts/login/$', 'api_login'),
|
||||||
|
(r'^accounts/logout/$', 'api_logout'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only serve favicon in production because otherwise chrome users will pretty much
|
||||||
|
# permanantly have the django-rest-framework favicon whenever they navigate to
|
||||||
|
# 127.0.0.1:8000 or whatever, which gets annoying
|
||||||
|
if not settings.DEBUG:
|
||||||
|
urlpatterns += patterns('djangorestframework.utils.staticviews',
|
||||||
|
(r'favicon.ico', 'favicon'),
|
||||||
|
)
|
65
djangorestframework/utils/staticviews.py
Normal file
65
djangorestframework/utils/staticviews.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
from django.contrib.auth.views import *
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def deny_robots(request):
|
||||||
|
return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain')
|
||||||
|
|
||||||
|
def favicon(request):
|
||||||
|
data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA='
|
||||||
|
return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
|
# BLERGH
|
||||||
|
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS
|
||||||
|
# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to
|
||||||
|
# be making settings changes in order to accomodate django-rest-framework
|
||||||
|
@csrf_protect
|
||||||
|
@never_cache
|
||||||
|
def api_login(request, template_name='api_login.html',
|
||||||
|
redirect_field_name=REDIRECT_FIELD_NAME,
|
||||||
|
authentication_form=AuthenticationForm):
|
||||||
|
"""Displays the login form and handles the login action."""
|
||||||
|
|
||||||
|
redirect_to = request.REQUEST.get(redirect_field_name, '')
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = authentication_form(data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
# Light security check -- make sure redirect_to isn't garbage.
|
||||||
|
if not redirect_to or ' ' in redirect_to:
|
||||||
|
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||||
|
|
||||||
|
# Heavier security check -- redirects to http://example.com should
|
||||||
|
# not be allowed, but things like /view/?param=http://example.com
|
||||||
|
# should be allowed. This regex checks if there is a '//' *before* a
|
||||||
|
# question mark.
|
||||||
|
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
|
||||||
|
redirect_to = settings.LOGIN_REDIRECT_URL
|
||||||
|
|
||||||
|
# Okay, security checks complete. Log the user in.
|
||||||
|
auth_login(request, form.get_user())
|
||||||
|
|
||||||
|
if request.session.test_cookie_worked():
|
||||||
|
request.session.delete_test_cookie()
|
||||||
|
|
||||||
|
return HttpResponseRedirect(redirect_to)
|
||||||
|
|
||||||
|
else:
|
||||||
|
form = authentication_form(request)
|
||||||
|
|
||||||
|
request.session.set_test_cookie()
|
||||||
|
|
||||||
|
#current_site = get_current_site(request)
|
||||||
|
|
||||||
|
return render_to_response(template_name, {
|
||||||
|
'form': form,
|
||||||
|
redirect_field_name: redirect_to,
|
||||||
|
#'site': current_site,
|
||||||
|
#'site_name': current_site.name,
|
||||||
|
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
|
||||||
|
}, context_instance=RequestContext(request))
|
||||||
|
|
||||||
|
|
||||||
|
def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME):
|
||||||
|
return logout(request, next_page, template_name, redirect_field_name)
|
|
@ -159,7 +159,7 @@ class ModelFormValidator(FormValidator):
|
||||||
otherwise if model is set use that class to create a ModelForm, otherwise return None."""
|
otherwise if model is set use that class to create a ModelForm, otherwise return None."""
|
||||||
|
|
||||||
form_cls = getattr(self.view, 'form', None)
|
form_cls = getattr(self.view, 'form', None)
|
||||||
model_cls = getattr(self.view, 'model', None)
|
model_cls = getattr(self.view.resource, 'model', None)
|
||||||
|
|
||||||
if form_cls:
|
if form_cls:
|
||||||
# Use explict Form
|
# Use explict Form
|
||||||
|
@ -189,9 +189,10 @@ class ModelFormValidator(FormValidator):
|
||||||
@property
|
@property
|
||||||
def _model_fields_set(self):
|
def _model_fields_set(self):
|
||||||
"""Return a set containing the names of validated fields on the model."""
|
"""Return a set containing the names of validated fields on the model."""
|
||||||
model = getattr(self.view, 'model', None)
|
resource = self.view.resource
|
||||||
fields = getattr(self.view, 'fields', self.fields)
|
model = getattr(resource, 'model', None)
|
||||||
exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
|
fields = getattr(resource, 'fields', self.fields)
|
||||||
|
exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields)
|
||||||
|
|
||||||
model_fields = set(field.name for field in model._meta.fields)
|
model_fields = set(field.name for field in model._meta.fields)
|
||||||
|
|
||||||
|
@ -203,9 +204,10 @@ class ModelFormValidator(FormValidator):
|
||||||
@property
|
@property
|
||||||
def _property_fields_set(self):
|
def _property_fields_set(self):
|
||||||
"""Returns a set containing the names of validated properties on the model."""
|
"""Returns a set containing the names of validated properties on the model."""
|
||||||
model = getattr(self.view, 'model', None)
|
resource = self.view.resource
|
||||||
fields = getattr(self.view, 'fields', self.fields)
|
model = getattr(resource, 'model', None)
|
||||||
exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
|
fields = getattr(resource, 'fields', self.fields)
|
||||||
|
exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields)
|
||||||
|
|
||||||
property_fields = set(attr for attr in dir(model) if
|
property_fields = set(attr for attr in dir(model) if
|
||||||
isinstance(getattr(model, attr, None), property)
|
isinstance(getattr(model, attr, None), property)
|
||||||
|
|
|
@ -1,66 +1,147 @@
|
||||||
from django.contrib.auth.views import *
|
from django.core.urlresolvers import set_script_prefix
|
||||||
#from django.contrib.sites.models import get_current_site
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.conf import settings
|
|
||||||
from django.http import HttpResponse
|
from djangorestframework.compat import View
|
||||||
import base64
|
from djangorestframework.response import Response, ErrorResponse
|
||||||
|
from djangorestframework.mixins import *
|
||||||
|
from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['BaseView',
|
||||||
|
'ModelView',
|
||||||
|
'InstanceModelView',
|
||||||
|
'ListOrModelView',
|
||||||
|
'ListOrCreateModelView']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
|
||||||
|
"""Handles incoming requests and maps them to REST operations.
|
||||||
|
Performs request deserialization, response serialization, authentication and input validation."""
|
||||||
|
|
||||||
|
# Use the base resource by default
|
||||||
|
resource = resource.Resource
|
||||||
|
|
||||||
|
# List of renderers the resource can serialize the response with, ordered by preference.
|
||||||
|
renderers = ( renderers.JSONRenderer,
|
||||||
|
renderers.DocumentingHTMLRenderer,
|
||||||
|
renderers.DocumentingXHTMLRenderer,
|
||||||
|
renderers.DocumentingPlainTextRenderer,
|
||||||
|
renderers.XMLRenderer )
|
||||||
|
|
||||||
|
# List of parsers the resource can parse the request with.
|
||||||
|
parsers = ( parsers.JSONParser,
|
||||||
|
parsers.FormParser,
|
||||||
|
parsers.MultipartParser )
|
||||||
|
|
||||||
|
# List of validators to validate, cleanup and normalize the request content
|
||||||
|
validators = ( validators.FormValidator, )
|
||||||
|
|
||||||
|
# List of all authenticating methods to attempt.
|
||||||
|
authentication = ( authentication.UserLoggedInAuthenticator,
|
||||||
|
authentication.BasicAuthenticator )
|
||||||
|
|
||||||
|
# List of all permissions that must be checked.
|
||||||
|
permissions = ( permissions.FullAnonAccess, )
|
||||||
|
|
||||||
|
# Optional form for input validation and presentation of HTML formatted responses.
|
||||||
|
form = None
|
||||||
|
|
||||||
|
# Allow name and description for the Resource to be set explicitly,
|
||||||
|
# overiding the default classname/docstring behaviour.
|
||||||
|
# These are used for documentation in the standard html and text renderers.
|
||||||
|
name = None
|
||||||
|
description = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allowed_methods(self):
|
||||||
|
return [method.upper() for method in self.http_method_names if hasattr(self, method)]
|
||||||
|
|
||||||
|
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||||
|
"""Return an HTTP 405 error if an operation is called which does not have a handler method."""
|
||||||
|
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||||
|
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_response(self, data):
|
||||||
|
"""Perform any resource-specific data filtering prior to the standard HTTP
|
||||||
|
content-type serialization.
|
||||||
|
|
||||||
|
Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.
|
||||||
|
|
||||||
|
TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into
|
||||||
|
the RendererMixin and Renderer classes."""
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# Note: session based authentication is explicitly CSRF validated,
|
||||||
|
# all other authentication is CSRF exempt.
|
||||||
|
@csrf_exempt
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.request = request
|
||||||
|
self.args = args
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
|
||||||
|
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
|
||||||
|
set_script_prefix(prefix)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
|
||||||
|
# self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
|
||||||
|
self.perform_form_overloading()
|
||||||
|
|
||||||
|
# Authenticate and check request is has the relevant permissions
|
||||||
|
self.check_permissions()
|
||||||
|
|
||||||
|
# Get the appropriate handler method
|
||||||
|
if self.method.lower() in self.http_method_names:
|
||||||
|
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
|
||||||
|
else:
|
||||||
|
handler = self.http_method_not_allowed
|
||||||
|
|
||||||
|
response_obj = handler(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Allow return value to be either Response, or an object, or None
|
||||||
|
if isinstance(response_obj, Response):
|
||||||
|
response = response_obj
|
||||||
|
elif response_obj is not None:
|
||||||
|
response = Response(status.HTTP_200_OK, response_obj)
|
||||||
|
else:
|
||||||
|
response = Response(status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
|
||||||
|
response.cleaned_content = self.resource.object_to_serializable(response.raw_content)
|
||||||
|
|
||||||
|
except ErrorResponse, exc:
|
||||||
|
response = exc.response
|
||||||
|
|
||||||
|
# Always add these headers.
|
||||||
|
#
|
||||||
|
# TODO - this isn't actually the correct way to set the vary header,
|
||||||
|
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
|
||||||
|
response.headers['Allow'] = ', '.join(self.allowed_methods)
|
||||||
|
response.headers['Vary'] = 'Authenticate, Accept'
|
||||||
|
|
||||||
|
return self.render(response)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelView(BaseView):
|
||||||
|
"""A RESTful view that maps to a model in the database."""
|
||||||
|
validators = (validators.ModelFormValidator,)
|
||||||
|
|
||||||
|
class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView):
|
||||||
|
"""A view which provides default operations for read/update/delete against a model instance."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ListModelResource(ListModelMixin, ModelView):
|
||||||
|
"""A view which provides default operations for list, against a model in the database."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ListOrCreateModelResource(ListModelMixin, CreateModelMixin, ModelView):
|
||||||
|
"""A view which provides default operations for list and create, against a model in the database."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def deny_robots(request):
|
|
||||||
return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain')
|
|
||||||
|
|
||||||
def favicon(request):
|
|
||||||
data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA='
|
|
||||||
return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon')
|
|
||||||
|
|
||||||
# BLERGH
|
|
||||||
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS
|
|
||||||
# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to
|
|
||||||
# be making settings changes in order to accomodate django-rest-framework
|
|
||||||
@csrf_protect
|
|
||||||
@never_cache
|
|
||||||
def api_login(request, template_name='api_login.html',
|
|
||||||
redirect_field_name=REDIRECT_FIELD_NAME,
|
|
||||||
authentication_form=AuthenticationForm):
|
|
||||||
"""Displays the login form and handles the login action."""
|
|
||||||
|
|
||||||
redirect_to = request.REQUEST.get(redirect_field_name, '')
|
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
form = authentication_form(data=request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
# Light security check -- make sure redirect_to isn't garbage.
|
|
||||||
if not redirect_to or ' ' in redirect_to:
|
|
||||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
|
||||||
|
|
||||||
# Heavier security check -- redirects to http://example.com should
|
|
||||||
# not be allowed, but things like /view/?param=http://example.com
|
|
||||||
# should be allowed. This regex checks if there is a '//' *before* a
|
|
||||||
# question mark.
|
|
||||||
elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to):
|
|
||||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
|
||||||
|
|
||||||
# Okay, security checks complete. Log the user in.
|
|
||||||
auth_login(request, form.get_user())
|
|
||||||
|
|
||||||
if request.session.test_cookie_worked():
|
|
||||||
request.session.delete_test_cookie()
|
|
||||||
|
|
||||||
return HttpResponseRedirect(redirect_to)
|
|
||||||
|
|
||||||
else:
|
|
||||||
form = authentication_form(request)
|
|
||||||
|
|
||||||
request.session.set_test_cookie()
|
|
||||||
|
|
||||||
#current_site = get_current_site(request)
|
|
||||||
|
|
||||||
return render_to_response(template_name, {
|
|
||||||
'form': form,
|
|
||||||
redirect_field_name: redirect_to,
|
|
||||||
#'site': current_site,
|
|
||||||
#'site_name': current_site.name,
|
|
||||||
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
|
|
||||||
}, context_instance=RequestContext(request))
|
|
||||||
|
|
||||||
|
|
||||||
def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME):
|
|
||||||
return logout(request, next_page, template_name, redirect_field_name)
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from djangorestframework.modelresource import ModelResource, RootModelResource
|
from djangorestframework.modelresource import InstanceModelResource, ListOrCreateModelResource
|
||||||
from modelresourceexample.models import MyModel
|
from modelresourceexample.models import MyModel
|
||||||
|
|
||||||
FIELDS = ('foo', 'bar', 'baz', 'absolute_url')
|
FIELDS = ('foo', 'bar', 'baz', 'absolute_url')
|
||||||
|
|
||||||
class MyModelRootResource(RootModelResource):
|
class MyModelRootResource(ListOrCreateModelResource):
|
||||||
"""A create/list resource for MyModel.
|
"""A create/list resource for MyModel.
|
||||||
Available for both authenticated and anonymous access for the purposes of the sandbox."""
|
Available for both authenticated and anonymous access for the purposes of the sandbox."""
|
||||||
model = MyModel
|
model = MyModel
|
||||||
fields = FIELDS
|
fields = FIELDS
|
||||||
|
|
||||||
class MyModelResource(ModelResource):
|
class MyModelResource(InstanceModelResource):
|
||||||
"""A read/update/delete resource for MyModel.
|
"""A read/update/delete resource for MyModel.
|
||||||
Available for both authenticated and anonymous access for the purposes of the sandbox."""
|
Available for both authenticated and anonymous access for the purposes of the sandbox."""
|
||||||
model = MyModel
|
model = MyModel
|
||||||
|
|
|
@ -2,11 +2,8 @@ from django.conf.urls.defaults import patterns, include, url
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from sandbox.views import Sandbox
|
from sandbox.views import Sandbox
|
||||||
|
|
||||||
urlpatterns = patterns('djangorestframework.views',
|
urlpatterns = patterns('',
|
||||||
(r'robots.txt', 'deny_robots'),
|
|
||||||
|
|
||||||
(r'^$', Sandbox.as_view()),
|
(r'^$', Sandbox.as_view()),
|
||||||
|
|
||||||
(r'^resource-example/', include('resourceexample.urls')),
|
(r'^resource-example/', include('resourceexample.urls')),
|
||||||
(r'^model-resource-example/', include('modelresourceexample.urls')),
|
(r'^model-resource-example/', include('modelresourceexample.urls')),
|
||||||
(r'^mixin/', include('mixin.urls')),
|
(r'^mixin/', include('mixin.urls')),
|
||||||
|
@ -14,14 +11,6 @@ urlpatterns = patterns('djangorestframework.views',
|
||||||
(r'^pygments/', include('pygments_api.urls')),
|
(r'^pygments/', include('pygments_api.urls')),
|
||||||
(r'^blog-post/', include('blogpost.urls')),
|
(r'^blog-post/', include('blogpost.urls')),
|
||||||
|
|
||||||
(r'^accounts/login/$', 'api_login'),
|
(r'^', include('djangorestframework.urls')),
|
||||||
(r'^accounts/logout/$', 'api_logout'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only serve favicon in production because otherwise chrome users will pretty much
|
|
||||||
# permanantly have the django-rest-framework favicon whenever they navigate to
|
|
||||||
# 127.0.0.1:8000 or whatever, which gets annoying
|
|
||||||
if not settings.DEBUG:
|
|
||||||
urlpatterns += patterns('djangorestframework.views',
|
|
||||||
(r'favicon.ico', 'favicon'),
|
|
||||||
)
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user