Decouple views and resources

This commit is contained in:
Tom Christie 2011-05-04 09:21:17 +01:00
parent 8756664e06
commit d373b3a067
24 changed files with 582 additions and 677 deletions

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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.

View File

@ -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."""
# 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:
# #
# NEXT: Exceptions on func() -> 500, tracebacks renderted if settings.DEBUG # 0. All the fields on the model, excluding 'id'.
# 1. All the properties on the model.
__all__ = ['Resource'] # 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
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
permissions = ()
# 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):
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, # If you wish to override this behaviour,
# also it's currently sub-obtimal for HTTP caching - need to sort that out. # you should explicitly set the fields attribute on your class.
response.headers['Allow'] = ', '.join(self.allowed_methods) fields = None
response.headers['Vary'] = 'Authenticate, Accept'
return self.render(response) @classmethod
except: def object_to_serializable(self, data):
import traceback """A (horrible) munging of Piston's pre-serialization. Returns a dict"""
traceback.print_exc()
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 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)

View File

@ -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)

View File

@ -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 }}

View File

@ -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,

View File

@ -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()),
) )

View File

@ -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('',

View File

@ -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"""

View File

@ -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"}')

View File

@ -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')

View File

@ -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'),
) )

View File

@ -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()),
) )

View File

@ -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,)
@ -265,9 +265,12 @@ class TestModelFormValidator(TestCase):
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)

View File

@ -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'),

View 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'),
)

View 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)

View File

@ -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)

View File

@ -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
import base64
def deny_robots(request): from djangorestframework.compat import View
return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain') from djangorestframework.response import Response, ErrorResponse
from djangorestframework.mixins import *
from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status
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 __all__ = ['BaseView',
# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS 'ModelView',
# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to 'InstanceModelView',
# be making settings changes in order to accomodate django-rest-framework 'ListOrModelView',
@csrf_protect 'ListOrCreateModelView']
@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 class BaseView(RequestMixin, ResponseMixin, AuthMixin, View):
# not be allowed, but things like /view/?param=http://example.com """Handles incoming requests and maps them to REST operations.
# should be allowed. This regex checks if there is a '//' *before* a Performs request deserialization, response serialization, authentication and input validation."""
# 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. # Use the base resource by default
auth_login(request, form.get_user()) resource = resource.Resource
if request.session.test_cookie_worked(): # List of renderers the resource can serialize the response with, ordered by preference.
request.session.delete_test_cookie() renderers = ( renderers.JSONRenderer,
renderers.DocumentingHTMLRenderer,
renderers.DocumentingXHTMLRenderer,
renderers.DocumentingPlainTextRenderer,
renderers.XMLRenderer )
return HttpResponseRedirect(redirect_to) # 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: else:
form = authentication_form(request) handler = self.http_method_not_allowed
request.session.set_test_cookie() 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
#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)

View File

@ -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

View File

@ -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'),
)