mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 08:14:16 +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.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
@ -25,10 +25,10 @@ class BaseAuthenticator(object):
|
|||
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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
checking at the point the response is being generated.
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
""""""
|
||||
from djangorestframework.utils.mediatypes import MediaType
|
||||
from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX
|
||||
from djangorestframework.response import ErrorResponse
|
||||
|
@ -12,6 +13,14 @@ from decimal import Decimal
|
|||
import re
|
||||
|
||||
|
||||
__all__ = ['RequestMixin',
|
||||
'ResponseMixin',
|
||||
'AuthMixin',
|
||||
'ReadModelMixin',
|
||||
'CreateModelMixin',
|
||||
'UpdateModelMixin',
|
||||
'DeleteModelMixin',
|
||||
'ListModelMixin']
|
||||
|
||||
########## Request Mixin ##########
|
||||
|
||||
|
@ -250,7 +259,7 @@ class RequestMixin(object):
|
|||
########## ResponseMixin ##########
|
||||
|
||||
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.
|
||||
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
|
||||
REWRITE_IE_ACCEPT_HEADER = True
|
||||
|
||||
#request = None
|
||||
#response = None
|
||||
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):
|
||||
"""Takes a :class:`Response` object and returns a Django :class:`HttpResponse`."""
|
||||
|
@ -318,7 +303,7 @@ class ResponseMixin(object):
|
|||
|
||||
def _determine_renderer(self, request):
|
||||
"""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"""
|
||||
|
||||
|
@ -415,17 +400,6 @@ class AuthMixin(object):
|
|||
return auth
|
||||
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):
|
||||
if not self.permissions:
|
||||
return
|
||||
|
@ -443,14 +417,15 @@ class AuthMixin(object):
|
|||
class ReadModelMixin(object):
|
||||
"""Behaviour to read a model instance on GET requests"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
model = self.resource.model
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
instance = self.model.objects.get(pk=args[-1], **kwargs)
|
||||
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
instance = model.objects.get(**kwargs)
|
||||
except model.DoesNotExist:
|
||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return instance
|
||||
|
@ -459,17 +434,18 @@ class ReadModelMixin(object):
|
|||
class CreateModelMixin(object):
|
||||
"""Behaviour to create a model instance on POST requests"""
|
||||
def post(self, request, *args, **kwargs):
|
||||
model = self.resource.model
|
||||
# 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):
|
||||
kwargs[related_name + '_id'] = kwargs[related_name]
|
||||
del kwargs[related_name]
|
||||
|
||||
all_kw_args = dict(self.CONTENT.items() + kwargs.items())
|
||||
if args:
|
||||
instance = self.model(pk=args[-1], **all_kw_args)
|
||||
instance = model(pk=args[-1], **all_kw_args)
|
||||
else:
|
||||
instance = self.model(**all_kw_args)
|
||||
instance = model(**all_kw_args)
|
||||
instance.save()
|
||||
headers = {}
|
||||
if hasattr(instance, 'get_absolute_url'):
|
||||
|
@ -480,19 +456,20 @@ class CreateModelMixin(object):
|
|||
class UpdateModelMixin(object):
|
||||
"""Behaviour to update a model instance on PUT requests"""
|
||||
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
|
||||
try:
|
||||
if args:
|
||||
# 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:
|
||||
# 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():
|
||||
setattr(instance, key, val)
|
||||
except self.model.DoesNotExist:
|
||||
instance = self.model(**self.CONTENT)
|
||||
except model.DoesNotExist:
|
||||
instance = model(**self.CONTENT)
|
||||
instance.save()
|
||||
|
||||
instance.save()
|
||||
|
@ -502,14 +479,15 @@ class UpdateModelMixin(object):
|
|||
class DeleteModelMixin(object):
|
||||
"""Behaviour to delete a model instance on DELETE requests"""
|
||||
def delete(self, request, *args, **kwargs):
|
||||
model = self.resource.model
|
||||
try:
|
||||
if args:
|
||||
# If we have any none kwargs then assume the last represents the primrary key
|
||||
instance = self.model.objects.get(pk=args[-1], **kwargs)
|
||||
instance = model.objects.get(pk=args[-1], **kwargs)
|
||||
else:
|
||||
# Otherwise assume the kwargs uniquely identify the model
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
instance = model.objects.get(**kwargs)
|
||||
except model.DoesNotExist:
|
||||
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
|
||||
|
||||
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):
|
||||
return True
|
||||
|
||||
|
||||
class FullAnonAccess(BasePermission):
|
||||
""""""
|
||||
def has_permission(self, auth):
|
||||
return True
|
||||
|
||||
class IsAuthenticated(BasePermission):
|
||||
""""""
|
||||
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,
|
||||
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.
|
||||
|
|
|
@ -1,130 +1,251 @@
|
|||
from django.core.urlresolvers import set_script_prefix
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.db.models import Model
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.fields.related import RelatedField
|
||||
|
||||
from djangorestframework.compat import View
|
||||
from djangorestframework.response import Response, ErrorResponse
|
||||
from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
|
||||
from djangorestframework import renderers, parsers, authentication, permissions, validators, status
|
||||
import decimal
|
||||
import inspect
|
||||
import re
|
||||
|
||||
|
||||
# TODO: Figure how out references and named urls need to work nicely
|
||||
# TODO: POST on existing 404 URL, PUT on existing 404 URL
|
||||
#
|
||||
# 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 )
|
||||
class Resource(object):
|
||||
"""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."""
|
||||
|
||||
# List of all permissions required to access the resource
|
||||
permissions = ()
|
||||
# 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
|
||||
|
||||
# Optional form for input validation and presentation of HTML formatted responses.
|
||||
form = None
|
||||
@classmethod
|
||||
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,
|
||||
# overiding the default classname/docstring behaviour.
|
||||
# These are used for documentation in the standard html and text renderers.
|
||||
name = None
|
||||
description = None
|
||||
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!
|
||||
|
||||
@property
|
||||
def allowed_methods(self):
|
||||
return [method.upper() for method in self.http_method_names if hasattr(self, method)]
|
||||
return ret
|
||||
|
||||
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.
|
||||
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() ]
|
||||
|
||||
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,
|
||||
# all other authentication is CSRF exempt.
|
||||
@csrf_exempt
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
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.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()
|
||||
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 fields:
|
||||
v = lambda f: getattr(data, f.attname)
|
||||
|
||||
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):
|
||||
"""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={}):
|
||||
self.response = Response(status, content=content, headers=headers)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<div id="content" class="colM">
|
||||
|
||||
<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 %}
|
||||
<div class="form-row">
|
||||
<label for="id_username">Username:</label> {{ form.username }}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import BaseView
|
||||
|
||||
|
||||
# See: http://www.useragentstring.com/
|
||||
|
@ -19,15 +19,15 @@ class UserAgentMungingTest(TestCase):
|
|||
|
||||
def setUp(self):
|
||||
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
permissions = ()
|
||||
|
||||
def get(self, request):
|
||||
return {'a':1, 'b':2, 'c':3}
|
||||
|
||||
self.req = RequestFactory()
|
||||
self.MockResource = MockResource
|
||||
self.view = MockResource.as_view()
|
||||
self.MockView = MockView
|
||||
self.view = MockView.as_view()
|
||||
|
||||
def test_munge_msie_accept_header(self):
|
||||
"""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):
|
||||
"""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."""
|
||||
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,
|
||||
MSIE_8_USER_AGENT,
|
||||
|
|
|
@ -6,19 +6,19 @@ from django.test import Client, TestCase
|
|||
from django.utils import simplejson as json
|
||||
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import BaseView
|
||||
from djangorestframework import permissions
|
||||
|
||||
import base64
|
||||
|
||||
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
permissions = ( permissions.IsAuthenticated, )
|
||||
def post(self, request):
|
||||
return {'a':1, 'b':2, 'c':3}
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^$', MockResource.as_view()),
|
||||
(r'^$', MockView.as_view()),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
from django.test import TestCase
|
||||
from djangorestframework.utils.breadcrumbs import get_breadcrumbs
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import BaseView
|
||||
|
||||
class Root(Resource):
|
||||
class Root(BaseView):
|
||||
pass
|
||||
|
||||
class ResourceRoot(Resource):
|
||||
class ResourceRoot(BaseView):
|
||||
pass
|
||||
|
||||
class ResourceInstance(Resource):
|
||||
class ResourceInstance(BaseView):
|
||||
pass
|
||||
|
||||
class NestedResourceRoot(Resource):
|
||||
class NestedResourceRoot(BaseView):
|
||||
pass
|
||||
|
||||
class NestedResourceInstance(Resource):
|
||||
class NestedResourceInstance(BaseView):
|
||||
pass
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.test import TestCase
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import BaseView
|
||||
from djangorestframework.compat import apply_markdown
|
||||
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>"""
|
||||
|
||||
|
||||
class TestResourceNamesAndDescriptions(TestCase):
|
||||
class TestViewNamesAndDescriptions(TestCase):
|
||||
def test_resource_name_uses_classname_by_default(self):
|
||||
"""Ensure Resource names are based on the classname by default."""
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
pass
|
||||
self.assertEquals(get_name(MockResource()), 'Mock Resource')
|
||||
self.assertEquals(get_name(MockView()), 'Mock View')
|
||||
|
||||
def test_resource_name_can_be_set_explicitly(self):
|
||||
"""Ensure Resource names can be set using the 'name' class attribute."""
|
||||
example = 'Some Other Name'
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
name = example
|
||||
self.assertEquals(get_name(MockResource()), example)
|
||||
self.assertEquals(get_name(MockView()), example)
|
||||
|
||||
def test_resource_description_uses_docstring_by_default(self):
|
||||
"""Ensure Resource names are based on the docstring by default."""
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
"""an example docstring
|
||||
====================
|
||||
|
||||
|
@ -64,28 +64,28 @@ class TestResourceNamesAndDescriptions(TestCase):
|
|||
|
||||
# hash style header #"""
|
||||
|
||||
self.assertEquals(get_description(MockResource()), DESCRIPTION)
|
||||
self.assertEquals(get_description(MockView()), DESCRIPTION)
|
||||
|
||||
def test_resource_description_can_be_set_explicitly(self):
|
||||
"""Ensure Resource descriptions can be set using the 'description' class attribute."""
|
||||
example = 'Some other description'
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
"""docstring"""
|
||||
description = example
|
||||
self.assertEquals(get_description(MockResource()), example)
|
||||
self.assertEquals(get_description(MockView()), example)
|
||||
|
||||
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."""
|
||||
example = 'Some other description'
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
description = example
|
||||
self.assertEquals(get_description(MockResource()), example)
|
||||
self.assertEquals(get_description(MockView()), example)
|
||||
|
||||
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"""
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
pass
|
||||
self.assertEquals(get_description(MockResource()), '')
|
||||
self.assertEquals(get_description(MockView()), '')
|
||||
|
||||
def test_markdown(self):
|
||||
"""Ensure markdown to HTML works as expected"""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.test import TestCase
|
||||
from django import forms
|
||||
from djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import BaseView
|
||||
import StringIO
|
||||
|
||||
class UploadFilesTests(TestCase):
|
||||
|
@ -15,7 +15,7 @@ class UploadFilesTests(TestCase):
|
|||
class FileForm(forms.Form):
|
||||
file = forms.FileField
|
||||
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
permissions = ()
|
||||
form = FileForm
|
||||
|
||||
|
@ -26,7 +26,7 @@ class UploadFilesTests(TestCase):
|
|||
file = StringIO.StringIO('stuff')
|
||||
file.name = 'stuff.txt'
|
||||
request = self.factory.post('/', {'file': file})
|
||||
view = MockResource.as_view()
|
||||
view = MockView.as_view()
|
||||
response = view(request)
|
||||
self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}')
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
..
|
||||
>>> from djangorestframework.parsers import FormParser
|
||||
>>> from djangorestframework.compat import RequestFactory
|
||||
>>> from djangorestframework.resource import Resource
|
||||
>>> from djangorestframework.views import BaseView
|
||||
>>> from StringIO import StringIO
|
||||
>>> from urllib import urlencode
|
||||
>>> req = RequestFactory().get('/')
|
||||
>>> some_resource = Resource()
|
||||
>>> some_resource.request = req # Make as if this request had been dispatched
|
||||
>>> some_view = BaseView()
|
||||
>>> some_view.request = req # Make as if this request had been dispatched
|
||||
|
||||
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 :
|
||||
|
||||
>>> FormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'}
|
||||
>>> FormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'}
|
||||
True
|
||||
|
||||
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.
|
||||
|
||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']}
|
||||
True
|
||||
|
||||
.. 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.
|
||||
|
||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1'}
|
||||
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1'}
|
||||
True
|
||||
|
||||
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):
|
||||
... return key == 'key2'
|
||||
...
|
||||
>>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []}
|
||||
>>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []}
|
||||
True
|
||||
|
||||
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 djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.parsers import MultipartParser
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import BaseView
|
||||
from djangorestframework.utils.mediatypes import MediaType
|
||||
from StringIO import StringIO
|
||||
|
||||
|
@ -122,9 +122,9 @@ class TestMultipartParser(TestCase):
|
|||
def test_multipartparser(self):
|
||||
"""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)
|
||||
resource = Resource()
|
||||
resource.request = post_req
|
||||
parsed = MultipartParser(resource).parse(StringIO(self.body))
|
||||
view = BaseView()
|
||||
view.request = post_req
|
||||
parsed = MultipartParser(view).parse(StringIO(self.body))
|
||||
self.assertEqual(parsed['key1'], 'val1')
|
||||
self.assertEqual(parsed.FILES['file1'].read(), 'blablabla')
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ from django.core.urlresolvers import reverse
|
|||
from django.test import TestCase
|
||||
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"""
|
||||
permissions = ()
|
||||
|
||||
|
@ -14,8 +14,8 @@ class MockResource(Resource):
|
|||
return reverse('another')
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', MockResource.as_view()),
|
||||
url(r'^another$', MockResource.as_view(), name='another'),
|
||||
url(r'^$', MockView.as_view()),
|
||||
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 djangorestframework.compat import RequestFactory
|
||||
from djangorestframework.resource import Resource
|
||||
from djangorestframework.views import BaseView
|
||||
from djangorestframework.permissions import Throttling
|
||||
|
||||
|
||||
class MockResource(Resource):
|
||||
class MockView(BaseView):
|
||||
permissions = ( Throttling, )
|
||||
throttle = (3, 1) # 3 requests per second
|
||||
|
||||
|
@ -15,7 +15,7 @@ class MockResource(Resource):
|
|||
return 'foo'
|
||||
|
||||
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.validators import BaseValidator, FormValidator, ModelFormValidator
|
||||
from djangorestframework.response import ErrorResponse
|
||||
from djangorestframework.views import BaseView
|
||||
from djangorestframework.resource import Resource
|
||||
|
||||
|
||||
class TestValidatorMixinInterfaces(TestCase):
|
||||
|
@ -20,7 +22,7 @@ class TestDisabledValidations(TestCase):
|
|||
def test_disabled_form_validator_returns_content_unchanged(self):
|
||||
"""If the view's form attribute is None then FormValidator(view).validate(content)
|
||||
should just return the content unmodified."""
|
||||
class DisabledFormView(object):
|
||||
class DisabledFormView(BaseView):
|
||||
form = None
|
||||
|
||||
view = DisabledFormView()
|
||||
|
@ -30,7 +32,7 @@ class TestDisabledValidations(TestCase):
|
|||
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 DisabledFormView(object):
|
||||
class DisabledFormView(BaseView):
|
||||
form = None
|
||||
|
||||
view = DisabledFormView()
|
||||
|
@ -39,11 +41,10 @@ class TestDisabledValidations(TestCase):
|
|||
|
||||
|
||||
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."""
|
||||
class DisabledModelFormView(object):
|
||||
class DisabledModelFormView(BaseView):
|
||||
form = None
|
||||
model = None
|
||||
|
||||
view = DisabledModelFormView()
|
||||
content = {'qwerty':'uiop'}
|
||||
|
@ -51,13 +52,12 @@ class TestDisabledValidations(TestCase):
|
|||
|
||||
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(object):
|
||||
form = None
|
||||
class DisabledModelFormView(BaseView):
|
||||
model = None
|
||||
|
||||
view = DisabledModelFormView()
|
||||
content = {'qwerty':'uiop'}
|
||||
self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)#
|
||||
self.assertEqual(ModelFormValidator(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)"""
|
||||
|
@ -84,7 +84,7 @@ class TestNonFieldErrors(TestCase):
|
|||
except ErrorResponse, exc:
|
||||
self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]})
|
||||
else:
|
||||
self.fail('ResourceException was not raised') #pragma: no cover
|
||||
self.fail('ErrorResponse was not raised') #pragma: no cover
|
||||
|
||||
|
||||
class TestFormValidation(TestCase):
|
||||
|
@ -95,11 +95,11 @@ class TestFormValidation(TestCase):
|
|||
class MockForm(forms.Form):
|
||||
qwerty = forms.CharField(required=True)
|
||||
|
||||
class MockFormView(object):
|
||||
class MockFormView(BaseView):
|
||||
form = MockForm
|
||||
validators = (FormValidator,)
|
||||
|
||||
class MockModelFormView(object):
|
||||
class MockModelFormView(BaseView):
|
||||
form = MockForm
|
||||
validators = (ModelFormValidator,)
|
||||
|
||||
|
@ -264,9 +264,12 @@ class TestModelFormValidator(TestCase):
|
|||
@property
|
||||
def readonly(self):
|
||||
return 'read only'
|
||||
|
||||
class MockView(object):
|
||||
|
||||
class MockResource(Resource):
|
||||
model = MockModel
|
||||
|
||||
class MockView(BaseView):
|
||||
resource = MockResource
|
||||
|
||||
self.validator = ModelFormValidator(MockView)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.test import TestCase
|
|||
from django.test import Client
|
||||
|
||||
|
||||
urlpatterns = patterns('djangorestframework.views',
|
||||
urlpatterns = patterns('djangorestframework.utils.staticviews',
|
||||
url(r'^robots.txt$', 'deny_robots'),
|
||||
url(r'^favicon.ico$', 'favicon'),
|
||||
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."""
|
||||
|
||||
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:
|
||||
# Use explict Form
|
||||
|
@ -189,9 +189,10 @@ class ModelFormValidator(FormValidator):
|
|||
@property
|
||||
def _model_fields_set(self):
|
||||
"""Return a set containing the names of validated fields on the model."""
|
||||
model = getattr(self.view, 'model', None)
|
||||
fields = getattr(self.view, 'fields', self.fields)
|
||||
exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
|
||||
resource = self.view.resource
|
||||
model = getattr(resource, 'model', None)
|
||||
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)
|
||||
|
||||
|
@ -203,9 +204,10 @@ class ModelFormValidator(FormValidator):
|
|||
@property
|
||||
def _property_fields_set(self):
|
||||
"""Returns a set containing the names of validated properties on the model."""
|
||||
model = getattr(self.view, 'model', None)
|
||||
fields = getattr(self.view, 'fields', self.fields)
|
||||
exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields)
|
||||
resource = self.view.resource
|
||||
model = getattr(resource, 'model', None)
|
||||
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
|
||||
isinstance(getattr(model, attr, None), property)
|
||||
|
|
|
@ -1,66 +1,147 @@
|
|||
from django.contrib.auth.views import *
|
||||
#from django.contrib.sites.models import get_current_site
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
import base64
|
||||
from django.core.urlresolvers import set_script_prefix
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from djangorestframework.compat import View
|
||||
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
|
||||
|
||||
FIELDS = ('foo', 'bar', 'baz', 'absolute_url')
|
||||
|
||||
class MyModelRootResource(RootModelResource):
|
||||
class MyModelRootResource(ListOrCreateModelResource):
|
||||
"""A create/list resource for MyModel.
|
||||
Available for both authenticated and anonymous access for the purposes of the sandbox."""
|
||||
model = MyModel
|
||||
fields = FIELDS
|
||||
|
||||
class MyModelResource(ModelResource):
|
||||
class MyModelResource(InstanceModelResource):
|
||||
"""A read/update/delete resource for MyModel.
|
||||
Available for both authenticated and anonymous access for the purposes of the sandbox."""
|
||||
model = MyModel
|
||||
|
|
|
@ -2,11 +2,8 @@ from django.conf.urls.defaults import patterns, include, url
|
|||
from django.conf import settings
|
||||
from sandbox.views import Sandbox
|
||||
|
||||
urlpatterns = patterns('djangorestframework.views',
|
||||
(r'robots.txt', 'deny_robots'),
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^$', Sandbox.as_view()),
|
||||
|
||||
(r'^resource-example/', include('resourceexample.urls')),
|
||||
(r'^model-resource-example/', include('modelresourceexample.urls')),
|
||||
(r'^mixin/', include('mixin.urls')),
|
||||
|
@ -14,14 +11,6 @@ urlpatterns = patterns('djangorestframework.views',
|
|||
(r'^pygments/', include('pygments_api.urls')),
|
||||
(r'^blog-post/', include('blogpost.urls')),
|
||||
|
||||
(r'^accounts/login/$', 'api_login'),
|
||||
(r'^accounts/logout/$', 'api_logout'),
|
||||
(r'^', include('djangorestframework.urls')),
|
||||
)
|
||||
|
||||
# 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