Skip to content

Commit

Permalink
Yowzers. Final big bunch of refactoring for 0.1 release. Now support …
Browse files Browse the repository at this point in the history
…Django 1.3's views, admin style api is all polished off, loads of tests, new test project for running the test. All sorts of goodness. Getting ready to push this out now.
  • Loading branch information
tom christie tom@tomchristie.com committed Feb 19, 2011
1 parent b749b95 commit 805aa03
Show file tree
Hide file tree
Showing 58 changed files with 1,716 additions and 717 deletions.
8 changes: 8 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Project Owner...

Tom Christie <tomchristie> - tom@tomchristie.com

Thanks to...

Jesper Noehr <jespern> & the django-piston contributors for providing the starting point for this project.
Paul Bagwell <pbgwl> - Suggestions & bugfixes.
4 changes: 0 additions & 4 deletions CREDITS.txt

This file was deleted.

15 changes: 10 additions & 5 deletions README.txt → README
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ hg clone https://tomchristie@bitbucket.org/tomchristie/django-rest-framework
cd django-rest-framework/
virtualenv --no-site-packages --distribute --python=python2.6 env
source ./env/bin/activate
pip install -r requirements.txt
pip install -r requirements.txt # django, pip

# To build the documentation...
# To run the tests...

pip install -r docs/requirements.txt
sphinx-build -c docs -b html -d docs/build docs html
cd testproject
export PYTHONPATH=..
python manage.py test djangorestframework

# To run the examples...

pip install -r examples/requirements.txt
pip install -r examples/requirements.txt # pygments, httplib2, markdown
cd examples
export PYTHONPATH=..
python manage.py syncdb
python manage.py runserver

# To build the documentation...

pip install -r docs/requirements.txt # sphinx
sphinx-build -c docs -b html -d docs/build docs html
38 changes: 32 additions & 6 deletions djangorestframework/authenticators.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
from django.contrib.auth import authenticate
from django.middleware.csrf import CsrfViewMiddleware
from djangorestframework.utils import as_tuple
import base64


class AuthenticatorMixin(object):
authenticators = None

def authenticate(self, request):
"""Attempt to authenticate the request, returning an authentication context or None.
An authentication context may be any object, although in many cases it will be a User instance."""

# Attempt authentication against each authenticator in turn,
# and return None if no authenticators succeed in authenticating the request.
for authenticator in as_tuple(self.authenticators):
auth_context = authenticator(self).authenticate(request)
if auth_context:
return auth_context

return None


class BaseAuthenticator(object):
"""All authenticators should extend BaseAuthenticator."""

def __init__(self, resource):
"""Initialise the authenticator with the Resource instance as state,
in case the authenticator needs to access any metadata on the Resource object."""
self.resource = resource
def __init__(self, mixin):
"""Initialise the authenticator with the mixin instance as state,
in case the authenticator needs to access any metadata on the mixin object."""
self.mixin = mixin

def authenticate(self, request):
"""Authenticate the request and return the authentication context or None.
An authentication context might be something as simple as a User object, or it might
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
for permissions if the authentication context is not None, and use anon_allowed_methods otherwise.
Expand All @@ -38,7 +62,9 @@ def authenticate(self, request):
class UserLoggedInAuthenticator(BaseAuthenticator):
"""Use Djagno's built-in request session for authentication."""
def authenticate(self, request):
if getattr(request, 'user', None) and request.user.is_active:
return request.user
if getattr(request, 'user', None) and request.user.is_active:
resp = CsrfViewMiddleware().process_view(request, None, (), {})
if resp is None: # csrf passed
return request.user
return None

31 changes: 31 additions & 0 deletions djangorestframework/breadcrumbs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.core.urlresolvers import resolve
from djangorestframework.description import get_name

def get_breadcrumbs(url):
"""Given a url returns a list of breadcrumbs, which are each a tuple of (name, url)."""

def breadcrumbs_recursive(url, breadcrumbs_list):
"""Add tuples of (name, url) to the breadcrumbs list, progressively chomping off parts of the url."""

# This is just like compsci 101 all over again...
try:
(view, unused_args, unused_kwargs) = resolve(url)
except:
pass
else:
if callable(view):
breadcrumbs_list.insert(0, (get_name(view), url))

if url == '':
# All done
return breadcrumbs_list

elif url.endswith('/'):
# Drop trailing slash off the end and continue to try to resolve more breadcrumbs
return breadcrumbs_recursive(url.rstrip('/'), breadcrumbs_list)

# Drop trailing non-slash off the end and continue to try to resolve more breadcrumbs
return breadcrumbs_recursive(url[:url.rfind('/') + 1], breadcrumbs_list)

return breadcrumbs_recursive(url, [])

128 changes: 128 additions & 0 deletions djangorestframework/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Compatability module to provide support for backwards compatability with older versions of django/python"""

# django.test.client.RequestFactory (Django >= 1.3)
try:
from django.test.client import RequestFactory

except ImportError:
from django.test import Client
from django.core.handlers.wsgi import WSGIRequest

# From: http://djangosnippets.org/snippets/963/
# Lovely stuff
class RequestFactory(Client):
"""
Class that lets you create mock Request objects for use in testing.
Usage:
rf = RequestFactory()
get_request = rf.get('/hello/')
post_request = rf.post('/submit/', {'foo': 'bar'})
This class re-uses the django.test.client.Client interface, docs here:
http://www.djangoproject.com/documentation/testing/#the-test-client
Once you have a request object you can pass it to any view function,
just as if that view had been hooked up using a URLconf.
"""
def request(self, **request):
"""
Similar to parent class, but returns the request object as soon as it
has created it.
"""
environ = {
'HTTP_COOKIE': self.cookies,
'PATH_INFO': '/',
'QUERY_STRING': '',
'REQUEST_METHOD': 'GET',
'SCRIPT_NAME': '',
'SERVER_NAME': 'testserver',
'SERVER_PORT': 80,
'SERVER_PROTOCOL': 'HTTP/1.1',
}
environ.update(self.defaults)
environ.update(request)
return WSGIRequest(environ)

# django.views.generic.View (Django >= 1.3)
try:
from django.views.generic import View
except:
from django import http
from django.utils.functional import update_wrapper
# from django.utils.log import getLogger
# from django.utils.decorators import classonlymethod

# logger = getLogger('django.request') - We'll just drop support for logger if running Django <= 1.2
# Might be nice to fix this up sometime to allow djangorestframework.compat.View to match 1.3's View more closely

class View(object):
"""
Intentionally simple parent class for all views. Only implements
dispatch-by-method and simple sanity checking.
"""

http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']

def __init__(self, **kwargs):
"""
Constructor. Called in the URLconf; can contain helpful extra
keyword arguments, and other things.
"""
# Go through keyword arguments, and either save their values to our
# instance, or raise an error.
for key, value in kwargs.iteritems():
setattr(self, key, value)

# @classonlymethod - We'll just us classmethod instead if running Django <= 1.2
@classmethod
def as_view(cls, **initkwargs):
"""
Main entry point for a request-response process.
"""
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError(u"You tried to pass in the %s method name as a "
u"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError(u"%s() received an invalid keyword %r" % (
cls.__name__, key))

def view(request, *args, **kwargs):
self = cls(**initkwargs)
return self.dispatch(request, *args, **kwargs)

# take name and docstring from class
update_wrapper(view, cls, updated=())

# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view

def dispatch(self, request, *args, **kwargs):
# Try to dispatch to the right method; if a method doesn't exist,
# defer to the error handler. Also defer to the error handler if the
# request method isn't on the approved list.
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
self.request = request
self.args = args
self.kwargs = kwargs
return handler(request, *args, **kwargs)

def http_method_not_allowed(self, request, *args, **kwargs):
allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
#logger.warning('Method Not Allowed (%s): %s' % (request.method, request.path),
# extra={
# 'status_code': 405,
# 'request': self.request
# }
#)
return http.HttpResponseNotAllowed(allowed_methods)
37 changes: 37 additions & 0 deletions djangorestframework/description.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Get a descriptive name and description for a view,
based on class name and docstring, and override-able by 'name' and 'description' attributes"""
import re

def get_name(view):
"""Return a name for the view.
If view has a name attribute, use that, otherwise use the view's class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
if getattr(view, 'name', None) is not None:
return view.name

if getattr(view, '__name__', None) is not None:
name = view.__name__
elif getattr(view, '__class__', None) is not None: # TODO: should be able to get rid of this case once refactoring to 1.3 class views is complete
name = view.__class__.__name__
else:
return ''

return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', name).strip()

def get_description(view):
"""Provide a description for the view.
By default this is the view's docstring with nice unindention applied."""
if getattr(view, 'description', None) is not None:
return getattr(view, 'description')

if getattr(view, '__doc__', None) is not None:
whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in view.__doc__.splitlines()[1:] if line.lstrip()]

if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
return re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', view.__doc__)

return view.__doc__

return ''
Loading

0 comments on commit 805aa03

Please sign in to comment.