From 42f2f9b40d1295e18a5b720b0d1f6ad85e928d8a Mon Sep 17 00:00:00 2001
From: tom christie tom@tomchristie.com
Date: Sun, 30 Jan 2011 18:30:39 +0000
Subject: Rename to django-rest-framework, get simpleexample working
---
README.txt | 20 +-
djangorestframework/__init__.py | 0
djangorestframework/authenticators.py | 44 ++
djangorestframework/emitters.py | 216 ++++++++++
djangorestframework/modelresource.py | 420 +++++++++++++++++++
djangorestframework/parsers.py | 89 ++++
djangorestframework/resource.py | 451 +++++++++++++++++++++
djangorestframework/response.py | 125 ++++++
djangorestframework/templates/emitter.html | 108 +++++
djangorestframework/templates/emitter.txt | 8 +
djangorestframework/templatetags/__init__.py | 0
.../templatetags/add_query_param.py | 17 +
.../templatetags/urlize_quoted_links.py | 100 +++++
djangorestframework/utils.py | 179 ++++++++
docs/conf.py | 2 +-
docs/index.rst | 28 +-
docs/requirements.txt | 8 +
examples/blogpost/views.py | 6 +-
examples/objectstore/views.py | 4 +-
examples/pygments_api/views.py | 6 +-
examples/requirements.txt | 6 +
examples/settings.py | 9 +-
examples/simpleexample/__init__.py | 0
examples/simpleexample/models.py | 23 ++
examples/simpleexample/urls.py | 6 +
examples/simpleexample/views.py | 18 +
examples/urls.py | 27 +-
flywheel/__init__.py | 0
flywheel/authenticators.py | 44 --
flywheel/emitters.py | 216 ----------
flywheel/modelresource.py | 402 ------------------
flywheel/parsers.py | 89 ----
flywheel/resource.py | 451 ---------------------
flywheel/response.py | 125 ------
flywheel/templates/emitter.html | 108 -----
flywheel/templates/emitter.txt | 8 -
flywheel/templatetags/__init__.py | 0
flywheel/templatetags/add_query_param.py | 17 -
flywheel/templatetags/urlize_quoted_links.py | 100 -----
flywheel/utils.py | 179 --------
requirements.txt | 2 +
41 files changed, 1874 insertions(+), 1787 deletions(-)
create mode 100644 djangorestframework/__init__.py
create mode 100644 djangorestframework/authenticators.py
create mode 100644 djangorestframework/emitters.py
create mode 100644 djangorestframework/modelresource.py
create mode 100644 djangorestframework/parsers.py
create mode 100644 djangorestframework/resource.py
create mode 100644 djangorestframework/response.py
create mode 100644 djangorestframework/templates/emitter.html
create mode 100644 djangorestframework/templates/emitter.txt
create mode 100644 djangorestframework/templatetags/__init__.py
create mode 100644 djangorestframework/templatetags/add_query_param.py
create mode 100644 djangorestframework/templatetags/urlize_quoted_links.py
create mode 100644 djangorestframework/utils.py
create mode 100644 docs/requirements.txt
create mode 100644 examples/requirements.txt
create mode 100644 examples/simpleexample/__init__.py
create mode 100644 examples/simpleexample/models.py
create mode 100644 examples/simpleexample/urls.py
create mode 100644 examples/simpleexample/views.py
delete mode 100644 flywheel/__init__.py
delete mode 100644 flywheel/authenticators.py
delete mode 100644 flywheel/emitters.py
delete mode 100644 flywheel/modelresource.py
delete mode 100644 flywheel/parsers.py
delete mode 100644 flywheel/resource.py
delete mode 100644 flywheel/response.py
delete mode 100644 flywheel/templates/emitter.html
delete mode 100644 flywheel/templates/emitter.txt
delete mode 100644 flywheel/templatetags/__init__.py
delete mode 100644 flywheel/templatetags/add_query_param.py
delete mode 100644 flywheel/templatetags/urlize_quoted_links.py
delete mode 100644 flywheel/utils.py
diff --git a/README.txt b/README.txt
index 44e4f96d..784713b6 100644
--- a/README.txt
+++ b/README.txt
@@ -1,17 +1,21 @@
-# To install django-rest-framework...
-#
-# Requirements:
-# python2.6
-# virtualenv
+# To install django-rest-framework in a virtualenv environment...
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
-python ./src/manage.py test
+pip install -r requirements.txt
# To build the documentation...
-sphinx-build -c docs -b html -d cache docs html
+pip install -r docs/requirements.txt
+sphinx-build -c docs -b html -d docs-build docs html
+
+# To run the examples...
+
+pip install -r examples/requirements.txt
+cd examples
+export PYTHONPATH=..
+python manage.py syncdb
+python manage.py runserver
diff --git a/djangorestframework/__init__.py b/djangorestframework/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangorestframework/authenticators.py b/djangorestframework/authenticators.py
new file mode 100644
index 00000000..8de182de
--- /dev/null
+++ b/djangorestframework/authenticators.py
@@ -0,0 +1,44 @@
+from django.contrib.auth import authenticate
+import base64
+
+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 authenticate(self, request):
+ """Authenticate the request and return the authentication context or None.
+
+ 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.
+
+ The authentication context is passed to the method calls eg Resource.get(request, auth) in order to
+ allow them to apply any more fine grained permission checking at the point the response is being generated.
+
+ This function must be overridden to be implemented."""
+ return None
+
+
+class BasicAuthenticator(BaseAuthenticator):
+ """Use HTTP Basic authentication"""
+ def authenticate(self, request):
+ if 'HTTP_AUTHORIZATION' in request.META:
+ auth = request.META['HTTP_AUTHORIZATION'].split()
+ if len(auth) == 2 and auth[0].lower() == "basic":
+ uname, passwd = base64.b64decode(auth[1]).split(':')
+ user = authenticate(username=uname, password=passwd)
+ if user is not None and user.is_active:
+ return user
+ return None
+
+
+class UserLoggedInAuthenticator(BaseAuthenticator):
+ """Use Djagno's built-in request session for authentication."""
+ def authenticate(self, request):
+ if request.user and request.user.is_active:
+ return request.user
+ return None
+
diff --git a/djangorestframework/emitters.py b/djangorestframework/emitters.py
new file mode 100644
index 00000000..a69407f1
--- /dev/null
+++ b/djangorestframework/emitters.py
@@ -0,0 +1,216 @@
+"""Emitters are used to serialize a Resource's output into specific media types.
+django-rest-framework also provides HTML and PlainText emitters 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, emitters and parsers on the Resource.
+"""
+from django.conf import settings
+from django.template import RequestContext, loader
+from django import forms
+
+from djangorestframework.response import NoContent
+from djangorestframework.utils import dict2xml, url_resolves
+
+from urllib import quote_plus
+import string
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+
+
+# TODO: Rename verbose to something more appropriate
+# TODO: NoContent could be handled more cleanly. It'd be nice if it was handled by default,
+# and only have an emitter output anything if it explicitly provides support for that.
+
+class BaseEmitter(object):
+ """All emitters must extend this class, set the media_type attribute, and
+ override the emit() function."""
+ media_type = None
+
+ def __init__(self, resource):
+ self.resource = resource
+
+ def emit(self, output=NoContent, verbose=False):
+ """By default emit simply returns the ouput as-is.
+ Override this method to provide for other behaviour."""
+ if output is NoContent:
+ return ''
+
+ return output
+
+
+class TemplateEmitter(BaseEmitter):
+ """Provided for convienience.
+ Emit the output by simply rendering it with the given template."""
+ media_type = None
+ template = None
+
+ def emit(self, output=NoContent, verbose=False):
+ if output is NoContent:
+ return ''
+
+ context = RequestContext(self.resource.request, output)
+ return self.template.render(context)
+
+
+class DocumentingTemplateEmitter(BaseEmitter):
+ """Base class for emitters used to self-document the API.
+ Implementing classes should extend this class and set the template attribute."""
+ template = None
+
+ def _get_content(self, resource, output):
+ """Get the content as if it had been emitted by a non-documenting emitter.
+
+ (Typically this will be the content as it would have been if the Resource had been
+ requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)"""
+
+ # Find the first valid emitter and emit the content. (Don't use another documenting emitter.)
+ emitters = [emitter for emitter in resource.emitters if not isinstance(emitter, DocumentingTemplateEmitter)]
+ if not emitters:
+ return '[No emitters were found]'
+
+ content = emitters[0](resource).emit(output, verbose=True)
+ if not all(char in string.printable for char in content):
+ return '[%d bytes of binary content]'
+
+ return content
+
+
+ def _get_form_instance(self, resource):
+ """Get a form, possibly bound to either the input or output data.
+ In the absence on of the Resource having an associated form then
+ provide a form that can be used to submit arbitrary content."""
+ # Get the form instance if we have one bound to the input
+ form_instance = resource.form_instance
+
+ # Otherwise if this isn't an error response
+ # then attempt to get a form bound to the response object
+ if not form_instance and resource.response.has_content_body:
+ try:
+ form_instance = resource.get_form(resource.response.raw_content)
+ if form_instance:
+ form_instance.is_valid()
+ except:
+ form_instance = None
+
+ # If we still don't have a form instance then try to get an unbound form
+ if not form_instance:
+ try:
+ form_instance = self.resource.get_form()
+ except:
+ pass
+
+ # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
+ if not form_instance:
+ form_instance = self._get_generic_content_form(resource)
+
+ return form_instance
+
+
+ def _get_generic_content_form(self, resource):
+ """Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
+ (Which are typically application/x-www-form-urlencoded)"""
+
+ # NB. http://jacobian.org/writing/dynamic-form-generation/
+ class GenericContentForm(forms.Form):
+ def __init__(self, resource):
+ """We don't know the names of the fields we want to set until the point the form is instantiated,
+ as they are determined by the Resource the form is being created against.
+ Add the fields dynamically."""
+ super(GenericContentForm, self).__init__()
+
+ contenttype_choices = [(media_type, media_type) for media_type in resource.parsed_media_types]
+ initial_contenttype = resource.default_parser.media_type
+
+ self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
+ choices=contenttype_choices,
+ initial=initial_contenttype)
+ self.fields[resource.CONTENT_PARAM] = forms.CharField(label='Content',
+ widget=forms.Textarea)
+
+ # If either of these reserved parameters are turned off then content tunneling is not possible
+ if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None:
+ return None
+
+ # Okey doke, let's do it
+ return GenericContentForm(resource)
+
+
+ def emit(self, output=NoContent):
+ content = self._get_content(self.resource, output)
+ form_instance = self._get_form_instance(self.resource)
+
+ if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
+ login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path))
+ logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path))
+ else:
+ login_url = None
+ logout_url = None
+
+ template = loader.get_template(self.template)
+ context = RequestContext(self.resource.request, {
+ 'content': content,
+ 'resource': self.resource,
+ 'request': self.resource.request,
+ 'response': self.resource.response,
+ 'form': form_instance,
+ 'login_url': login_url,
+ 'logout_url': logout_url,
+ })
+
+ ret = template.render(context)
+
+ # Munge DELETE Response code to allow us to return content
+ # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
+ if self.resource.response.status == 204:
+ self.resource.response.status = 200
+
+ return ret
+
+
+class JSONEmitter(BaseEmitter):
+ """Emitter which serializes to JSON"""
+ media_type = 'application/json'
+
+ def emit(self, output=NoContent, verbose=False):
+ if output is NoContent:
+ return ''
+ if verbose:
+ return json.dumps(output, indent=4, sort_keys=True)
+ return json.dumps(output)
+
+
+class XMLEmitter(BaseEmitter):
+ """Emitter which serializes to XML."""
+ media_type = 'application/xml'
+
+ def emit(self, output=NoContent, verbose=False):
+ if output is NoContent:
+ return ''
+ return dict2xml(output)
+
+
+class DocumentingHTMLEmitter(DocumentingTemplateEmitter):
+ """Emitter which provides a browsable HTML interface for an API.
+ See the examples listed in the django-rest-framework documentation to see this in actions."""
+ media_type = 'text/html'
+ template = 'emitter.html'
+
+
+class DocumentingXHTMLEmitter(DocumentingTemplateEmitter):
+ """Identical to DocumentingHTMLEmitter, except with an xhtml media type.
+ We need this to be listed in preference to xml in order to return HTML to WebKit based browsers,
+ given their Accept headers."""
+ media_type = 'application/xhtml+xml'
+ template = 'emitter.html'
+
+
+class DocumentingPlainTextEmitter(DocumentingTemplateEmitter):
+ """Emitter that serializes the output with the default emitter, but also provides plain-text
+ doumentation of the returned status and headers, and of the resource's name and description.
+ Useful for browsing an API with command line tools."""
+ media_type = 'text/plain'
+ template = 'emitter.txt'
+
+
diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py
new file mode 100644
index 00000000..a9605d4a
--- /dev/null
+++ b/djangorestframework/modelresource.py
@@ -0,0 +1,420 @@
+from django.forms import ModelForm
+from django.db.models.query import QuerySet
+from django.db.models import Model
+
+from djangorestframework.response import status, Response, ResponseException
+from djangorestframework.resource import Resource
+
+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."""
+
+ # 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 emitter"""
+ 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, '__emittable__'):
+ f = thing.__emittable__
+ if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
+ ret = _any(f())
+ else:
+ ret = str(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)
+
+
+ def post(self, request, auth, content, *args, **kwargs):
+ # TODO: test creation on a non-existing resource url
+ all_kw_args = dict(content.items() + kwargs.items())
+ if args:
+ instance = self.model(pk=args[-1], **all_kw_args)
+ else:
+ instance = self.model(**all_kw_args)
+ instance.save()
+ headers = {}
+ if hasattr(instance, 'get_absolute_url'):
+ headers['Location'] = self.add_domain(instance.get_absolute_url())
+ return Response(status.HTTP_201_CREATED, instance, headers)
+
+ def get(self, request, auth, *args, **kwargs):
+ 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)
+ else:
+ # Otherwise assume the kwargs uniquely identify the model
+ instance = self.model.objects.get(**kwargs)
+ except self.model.DoesNotExist:
+ raise ResponseException(status.HTTP_404_NOT_FOUND)
+
+ return instance
+
+ def put(self, request, auth, content, *args, **kwargs):
+ # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
+ try:
+ 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)
+ else:
+ # Otherwise assume the kwargs uniquely identify the model
+ instance = self.model.objects.get(**kwargs)
+ for (key, val) in content.items():
+ setattr(instance, key, val)
+ except self.model.DoesNotExist:
+ instance = self.model(**content)
+ instance.save()
+
+ instance.save()
+ return instance
+
+ def delete(self, request, auth, *args, **kwargs):
+ 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)
+ else:
+ # Otherwise assume the kwargs uniquely identify the model
+ instance = self.model.objects.get(**kwargs)
+ except self.model.DoesNotExist:
+ raise ResponseException(status.HTTP_404_NOT_FOUND, None, {})
+
+ instance.delete()
+ return
+
+
+class RootModelResource(ModelResource):
+ """A Resource which provides default operations for list and create."""
+ allowed_methods = ('GET', 'POST')
+ queryset = None
+
+ def get(self, request, auth, *args, **kwargs):
+ queryset = self.queryset if self.queryset else self.model.objects.all()
+ return queryset
+
+
+class QueryModelResource(ModelResource):
+ """Resource with default operations for list.
+ TODO: provide filter/order/num_results/paging, and a create operation to create queries."""
+ allowed_methods = ('GET',)
+ queryset = None
+
+ def get_form(self, data=None):
+ return None
+
+ def get(self, request, auth, *args, **kwargs):
+ queryset = self.queryset if self.queryset else self.model.objects.all()
+ return queryset
+
diff --git a/djangorestframework/parsers.py b/djangorestframework/parsers.py
new file mode 100644
index 00000000..a656e2eb
--- /dev/null
+++ b/djangorestframework/parsers.py
@@ -0,0 +1,89 @@
+from djangorestframework.response import status, ResponseException
+
+try:
+ import json
+except ImportError:
+ import simplejson as json
+
+# TODO: Make all parsers only list a single media_type, rather than a list
+
+class BaseParser(object):
+ """All parsers should extend BaseParser, specifing a media_type attribute,
+ and overriding the parse() method."""
+
+ media_type = None
+
+ def __init__(self, resource):
+ """Initialise the parser with the Resource instance as state,
+ in case the parser needs to access any metadata on the Resource object."""
+ self.resource = resource
+
+ def parse(self, input):
+ """Given some serialized input, return the deserialized output.
+ The input will be the raw request content body. The return value may be of
+ any type, but for many parsers/inputs it might typically be a dict."""
+ return input
+
+
+class JSONParser(BaseParser):
+ media_type = 'application/json'
+
+ def parse(self, input):
+ try:
+ return json.loads(input)
+ except ValueError, exc:
+ raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
+
+
+class XMLParser(BaseParser):
+ media_type = 'application/xml'
+
+
+class FormParser(BaseParser):
+ """The default parser for form data.
+ Return a dict containing a single value for each non-reserved parameter.
+ """
+
+ media_type = 'application/x-www-form-urlencoded'
+
+ def parse(self, input):
+ # The FormParser doesn't parse the input as other parsers would, since Django's already done the
+ # form parsing for us. We build the content object from the request directly.
+ request = self.resource.request
+
+ if request.method == 'PUT':
+ # Fix from piston to force Django to give PUT requests the same
+ # form processing that POST requests get...
+ #
+ # Bug fix: if _load_post_and_files has already been called, for
+ # example by middleware accessing request.POST, the below code to
+ # pretend the request is a POST instead of a PUT will be too late
+ # to make a difference. Also calling _load_post_and_files will result
+ # in the following exception:
+ # AttributeError: You cannot set the upload handlers after the upload has been processed.
+ # The fix is to check for the presence of the _post field which is set
+ # the first time _load_post_and_files is called (both by wsgi.py and
+ # modpython.py). If it's set, the request has to be 'reset' to redo
+ # the query value parsing in POST mode.
+ if hasattr(request, '_post'):
+ del request._post
+ del request._files
+
+ try:
+ request.method = "POST"
+ request._load_post_and_files()
+ request.method = "PUT"
+ except AttributeError:
+ request.META['REQUEST_METHOD'] = 'POST'
+ request._load_post_and_files()
+ request.META['REQUEST_METHOD'] = 'PUT'
+
+ # Strip any parameters that we are treating as reserved
+ data = {}
+ for (key, val) in request.POST.items():
+ if key not in self.resource.RESERVED_FORM_PARAMS:
+ data[key] = val
+
+ return data
+
+
diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py
new file mode 100644
index 00000000..d06d51b0
--- /dev/null
+++ b/djangorestframework/resource.py
@@ -0,0 +1,451 @@
+from django.contrib.sites.models import Site
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+
+from djangorestframework import emitters, parsers, authenticators
+from djangorestframework.response import status, Response, ResponseException
+
+from decimal import Decimal
+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 emitted if settings.DEBUG
+#
+
+__all__ = ['Resource']
+
+
+
+class Resource(object):
+ """Handles incoming requests and maps them to REST operations,
+ performing authentication, input deserialization, input validation, output serialization."""
+
+ # List of RESTful operations which may be performed on this resource.
+ allowed_methods = ('GET',)
+ anon_allowed_methods = ()
+
+ # List of emitters the resource can serialize the response with, ordered by preference
+ emitters = ( emitters.JSONEmitter,
+ emitters.DocumentingHTMLEmitter,
+ emitters.DocumentingXHTMLEmitter,
+ emitters.DocumentingPlainTextEmitter,
+ emitters.XMLEmitter )
+
+ # List of content-types the resource can read from
+ parsers = ( parsers.JSONParser,
+ parsers.XMLParser,
+ parsers.FormParser )
+
+ # List of all authenticating methods to attempt
+ authenticators = ( authenticators.UserLoggedInAuthenticator,
+ authenticators.BasicAuthenticator )
+
+ # Optional form for input validation and presentation of HTML formatted responses.
+ form = None
+
+ # Map standard HTTP methods to function calls
+ callmap = { 'GET': 'get', 'POST': 'post',
+ 'PUT': 'put', 'DELETE': 'delete' }
+
+ # Some reserved parameters to allow us to use standard HTML forms with our resource
+ # Override any/all of these with None to disable them, or override them with another value to rename them.
+ ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
+ METHOD_PARAM = '_method' # Allow POST overloading in form params
+ CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header in form params (allows sending arbitrary content with standard forms)
+ CONTENT_PARAM = '_content' # Allow override of body content in form params (allows sending arbitrary content with standard forms)
+ CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token used in form params
+
+
+ def __new__(cls, *args, **kwargs):
+ """Make the class callable so it can be used as a Django view."""
+ self = object.__new__(cls)
+ if args:
+ request = args[0]
+ self.__init__(request)
+ return self._handle_request(request, *args[1:], **kwargs)
+ else:
+ self.__init__()
+ return self
+
+
+ def __init__(self, request=None):
+ """"""
+ # Setup the resource context
+ self.request = request
+ self.response = None
+ self.form_instance = None
+
+ # These sets are determined now so that overridding classes can modify the various parameter names,
+ # or set them to None to disable them.
+ self.RESERVED_FORM_PARAMS = set((self.METHOD_PARAM, self.CONTENTTYPE_PARAM, self.CONTENT_PARAM, self.CSRF_PARAM))
+ self.RESERVED_QUERY_PARAMS = set((self.ACCEPT_QUERY_PARAM))
+ self.RESERVED_FORM_PARAMS.discard(None)
+ self.RESERVED_QUERY_PARAMS.discard(None)
+
+
+ @property
+ def name(self):
+ """Provide a name for the resource.
+ By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
+ class_name = self.__class__.__name__
+ return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip()
+
+ @property
+ def description(self):
+ """Provide a description for the resource.
+ By default this is the class's docstring with leading line spaces stripped."""
+ return re.sub(re.compile('^ +', re.MULTILINE), '', self.__doc__)
+
+ @property
+ def emitted_media_types(self):
+ """Return an list of all the media types that this resource can emit."""
+ return [emitter.media_type for emitter in self.emitters]
+
+ @property
+ def default_emitter(self):
+ """Return the resource's most prefered emitter.
+ (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
+ return self.emitters[0]
+
+ @property
+ def parsed_media_types(self):
+ """Return an list of all the media types that this resource can emit."""
+ return [parser.media_type for parser in self.parsers]
+
+ @property
+ def default_parser(self):
+ """Return the resource's most prefered emitter.
+ (This has no behavioural effect, but is may be used by documenting emitters)"""
+ return self.parsers[0]
+
+
+ def get(self, request, auth, *args, **kwargs):
+ """Must be subclassed to be implemented."""
+ self.not_implemented('GET')
+
+
+ def post(self, request, auth, content, *args, **kwargs):
+ """Must be subclassed to be implemented."""
+ self.not_implemented('POST')
+
+
+ def put(self, request, auth, content, *args, **kwargs):
+ """Must be subclassed to be implemented."""
+ self.not_implemented('PUT')
+
+
+ def delete(self, request, auth, *args, **kwargs):
+ """Must be subclassed to be implemented."""
+ self.not_implemented('DELETE')
+
+
+ def reverse(self, view, *args, **kwargs):
+ """Return a fully qualified URI for a given view or resource.
+ Add the domain using the Sites framework if possible, otherwise fallback to using the current request."""
+ return self.add_domain(reverse(view, args=args, kwargs=kwargs))
+
+
+ def not_implemented(self, operation):
+ """Return an HTTP 500 server error if an operation is called which has been allowed by
+ allowed_methods, but which has not been implemented."""
+ raise ResponseException(status.HTTP_500_INTERNAL_SERVER_ERROR,
+ {'detail': '%s operation on this resource has not been implemented' % (operation, )})
+
+
+ def add_domain(self, path):
+ """Given a path, return an fully qualified URI.
+ Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
+
+ # Note that out-of-the-box the Sites framework uses the reserved domain 'example.com'
+ # See RFC 2606 - http://www.faqs.org/rfcs/rfc2606.html
+ try:
+ site = Site.objects.get_current()
+ if site.domain and site.domain != 'example.com':
+ return 'http://%s%s' % (site.domain, path)
+ except:
+ pass
+
+ return self.request.build_absolute_uri(path)
+
+
+ def determine_method(self, request):
+ """Determine the HTTP method that this request should be treated as.
+ Allows PUT and DELETE tunneling via the _method parameter if METHOD_PARAM is set."""
+ method = request.method.upper()
+
+ if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM):
+ method = request.POST[self.METHOD_PARAM].upper()
+
+ return method
+
+
+ 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 self.authenticators:
+ auth_context = authenticator(self).authenticate(request)
+ if auth_context:
+ return auth_context
+
+ return None
+
+
+ def check_method_allowed(self, method, auth):
+ """Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
+
+ if not method in self.callmap.keys():
+ raise ResponseException(status.HTTP_501_NOT_IMPLEMENTED,
+ {'detail': 'Unknown or unsupported method \'%s\'' % method})
+
+ if not method in self.allowed_methods:
+ raise ResponseException(status.HTTP_405_METHOD_NOT_ALLOWED,
+ {'detail': 'Method \'%s\' not allowed on this resource.' % method})
+
+ if auth is None and not method in self.anon_allowed_methods:
+ raise ResponseException(status.HTTP_403_FORBIDDEN,
+ {'detail': 'You do not have permission to access this resource. ' +
+ 'You may need to login or otherwise authenticate the request.'})
+
+ def get_form(self, data=None):
+ """Optionally return a Django Form instance, which may be used for validation
+ and/or rendered by an HTML/XHTML emitter.
+
+ If data is not None the form will be bound to data."""
+
+ if self.form:
+ if data:
+ return self.form(data)
+ else:
+ return self.form()
+ return None
+
+
+ def cleanup_request(self, data, form_instance):
+ """Perform any resource-specific data deserialization and/or validation
+ after the initial HTTP content-type deserialization has taken place.
+
+ Returns a tuple containing the cleaned up data, and optionally a form bound to that data.
+
+ By default this uses form validation to filter the basic input into the required types."""
+
+ if form_instance is None:
+ return data
+
+ # Default form validation does not check for additional invalid fields
+ non_existent_fields = []
+ for key in set(data.keys()) - set(form_instance.fields.keys()):
+ non_existent_fields.append(key)
+
+ if not form_instance.is_valid() or non_existent_fields:
+ if not form_instance.errors and not non_existent_fields:
+ # If no data was supplied the errors property will be None
+ details = 'No content was supplied'
+
+ else:
+ # Add standard field errors
+ details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems() if key != '__all__')
+
+ # Add any non-field errors
+ if form_instance.non_field_errors():
+ details['errors'] = form_instance.non_field_errors()
+
+ # Add any non-existent field errors
+ for key in non_existent_fields:
+ details[key] = ['This field does not exist']
+
+ # Bail. Note that we will still serialize this response with the appropriate content type
+ raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': details})
+
+ return form_instance.cleaned_data
+
+
+ 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."""
+ return data
+
+
+ def determine_parser(self, request):
+ """Return the appropriate parser for the input, given the client's 'Content-Type' header,
+ and the content types that this Resource knows how to parse."""
+ content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
+ raw_content = request.raw_post_data
+
+ split = content_type.split(';', 1)
+ if len(split) > 1:
+ content_type = split[0]
+ content_type = content_type.strip()
+
+ # If CONTENTTYPE_PARAM is turned on, and this is a standard POST form then allow the content type to be overridden
+ if (content_type == 'application/x-www-form-urlencoded' and
+ request.method == 'POST' and
+ self.CONTENTTYPE_PARAM and
+ self.CONTENT_PARAM and
+ request.POST.get(self.CONTENTTYPE_PARAM, None) and
+ request.POST.get(self.CONTENT_PARAM, None)):
+ raw_content = request.POST[self.CONTENT_PARAM]
+ content_type = request.POST[self.CONTENTTYPE_PARAM]
+
+ # Create a list of list of (media_type, Parser) tuples
+ media_type_to_parser = dict([(parser.media_type, parser) for parser in self.parsers])
+
+ try:
+ return (media_type_to_parser[content_type], raw_content)
+ except KeyError:
+ raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
+ {'detail': 'Unsupported media type \'%s\'' % content_type})
+
+
+ def determine_emitter(self, request):
+ """Return the appropriate emitter for the output, given the client's 'Accept' header,
+ and the content types that this Resource knows how to serve.
+
+ See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
+
+ if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
+ # Use _accept parameter override
+ accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
+ elif request.META.has_key('HTTP_ACCEPT'):
+ # Use standard HTTP Accept negotiation
+ accept_list = request.META["HTTP_ACCEPT"].split(',')
+ else:
+ # No accept header specified
+ return self.default_emitter
+
+ # Parse the accept header into a dict of {qvalue: set of media types}
+ # We ignore mietype parameters
+ accept_dict = {}
+ for token in accept_list:
+ components = token.split(';')
+ mimetype = components[0].strip()
+ qvalue = Decimal('1.0')
+
+ if len(components) > 1:
+ # Parse items that have a qvalue eg text/html;q=0.9
+ try:
+ (q, num) = components[-1].split('=')
+ if q == 'q':
+ qvalue = Decimal(num)
+ except:
+ # Skip malformed entries
+ continue
+
+ if accept_dict.has_key(qvalue):
+ accept_dict[qvalue].add(mimetype)
+ else:
+ accept_dict[qvalue] = set((mimetype,))
+
+ # Convert to a list of sets ordered by qvalue (highest first)
+ accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
+
+ for accept_set in accept_sets:
+ # Return any exact match
+ for emitter in self.emitters:
+ if emitter.media_type in accept_set:
+ return emitter
+
+ # Return any subtype match
+ for emitter in self.emitters:
+ if emitter.media_type.split('/')[0] + '/*' in accept_set:
+ return emitter
+
+ # Return default
+ if '*/*' in accept_set:
+ return self.default_emitter
+
+
+ raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
+ {'detail': 'Could not statisfy the client\'s Accept header',
+ 'available_types': self.emitted_media_types})
+
+
+ def _handle_request(self, request, *args, **kwargs):
+ """This method is the core of Resource, through which all requests are passed.
+
+ Broadly this consists of the following procedure:
+
+ 0. ensure the operation is permitted
+ 1. deserialize request content into request data, using standard HTTP content types (PUT/POST only)
+ 2. cleanup and validate request data (PUT/POST only)
+ 3. call the core method to get the response data
+ 4. cleanup the response data
+ 5. serialize response data into response content, using standard HTTP content negotiation
+ """
+ emitter = None
+ method = self.determine_method(request)
+
+ try:
+ # Before we attempt anything else determine what format to emit our response data with.
+ emitter = self.determine_emitter(request)
+
+ # Authenticate the request, and store any context so that the resource operations can
+ # do more fine grained authentication if required.
+ #
+ # Typically the context will be a user, or None if this is an anonymous request,
+ # but it could potentially be more complex (eg the context of a request key which
+ # has been signed against a particular set of permissions)
+ auth_context = self.authenticate(request)
+
+ # Ensure the requested operation is permitted on this resource
+ self.check_method_allowed(method, auth_context)
+
+ # Get the appropriate create/read/update/delete function
+ func = getattr(self, self.callmap.get(method, None))
+
+ # Either generate the response data, deserializing and validating any request data
+ # TODO: Add support for message bodys on other HTTP methods, as it is valid.
+ if method in ('PUT', 'POST'):
+ (parser, raw_content) = self.determine_parser(request)
+ data = parser(self).parse(raw_content)
+ self.form_instance = self.get_form(data)
+ data = self.cleanup_request(data, self.form_instance)
+ response = func(request, auth_context, data, *args, **kwargs)
+
+ else:
+ response = func(request, auth_context, *args, **kwargs)
+
+ # Allow return value to be either Response, or an object, or None
+ if isinstance(response, Response):
+ self.response = response
+ elif response is not None:
+ self.response = Response(status.HTTP_200_OK, response)
+ else:
+ self.response = Response(status.HTTP_204_NO_CONTENT)
+
+ # Pre-serialize filtering (eg filter complex objects into natively serializable types)
+ self.response.cleaned_content = self.cleanup_response(self.response.raw_content)
+
+
+ except ResponseException, exc:
+ self.response = exc.response
+
+ # Fall back to the default emitter if we failed to perform content negotiation
+ if emitter is None:
+ emitter = self.default_emitter
+
+
+ # Always add these headers
+ self.response.headers['Allow'] = ', '.join(self.allowed_methods)
+ self.response.headers['Vary'] = 'Authenticate, Allow'
+
+ # Serialize the response content
+ if self.response.has_content_body:
+ content = emitter(self).emit(output=self.response.cleaned_content)
+ else:
+ content = emitter(self).emit()
+
+ # Build the HTTP Response
+ # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
+ resp = HttpResponse(content, mimetype=emitter.media_type, status=self.response.status)
+ for (key, val) in self.response.headers.items():
+ resp[key] = val
+
+ return resp
+
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
new file mode 100644
index 00000000..4f23bb0a
--- /dev/null
+++ b/djangorestframework/response.py
@@ -0,0 +1,125 @@
+from django.core.handlers.wsgi import STATUS_CODE_TEXT
+
+__all__ =['status', 'NoContent', 'Response', ]
+
+
+class Status(object):
+ """Descriptive HTTP status codes, for code readability.
+ See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html"""
+
+ # Verbose format (I prefer this as it's more explicit)
+ HTTP_100_CONTINUE = 100
+ HTTP_101_SWITCHING_PROTOCOLS = 101
+ HTTP_200_OK = 200
+ HTTP_201_CREATED = 201
+ HTTP_202_ACCEPTED = 202
+ HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
+ HTTP_204_NO_CONTENT = 204
+ HTTP_205_RESET_CONTENT = 205
+ HTTP_206_PARTIAL_CONTENT = 206
+ HTTP_300_MULTIPLE_CHOICES = 300
+ HTTP_301_MOVED_PERMANENTLY = 301
+ HTTP_302_FOUND = 302
+ HTTP_303_SEE_OTHER = 303
+ HTTP_304_NOT_MODIFIED = 304
+ HTTP_305_USE_PROXY = 305
+ HTTP_306_RESERVED = 306
+ HTTP_307_TEMPORARY_REDIRECT = 307
+ HTTP_400_BAD_REQUEST = 400
+ HTTP_401_UNAUTHORIZED = 401
+ HTTP_402_PAYMENT_REQUIRED = 402
+ HTTP_403_FORBIDDEN = 403
+ HTTP_404_NOT_FOUND = 404
+ HTTP_405_METHOD_NOT_ALLOWED = 405
+ HTTP_406_NOT_ACCEPTABLE = 406
+ HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
+ HTTP_408_REQUEST_TIMEOUT = 408
+ HTTP_409_CONFLICT = 409
+ HTTP_410_GONE = 410
+ HTTP_411_LENGTH_REQUIRED = 411
+ HTTP_412_PRECONDITION_FAILED = 412
+ HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
+ HTTP_414_REQUEST_URI_TOO_LONG = 414
+ HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
+ HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
+ HTTP_417_EXPECTATION_FAILED = 417
+ HTTP_500_INTERNAL_SERVER_ERROR = 500
+ HTTP_501_NOT_IMPLEMENTED = 501
+ HTTP_502_BAD_GATEWAY = 502
+ HTTP_503_SERVICE_UNAVAILABLE = 503
+ HTTP_504_GATEWAY_TIMEOUT = 504
+ HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
+
+ # Short format
+ CONTINUE = 100
+ SWITCHING_PROTOCOLS = 101
+ OK = 200
+ CREATED = 201
+ ACCEPTED = 202
+ NON_AUTHORITATIVE_INFORMATION = 203
+ NO_CONTENT = 204
+ RESET_CONTENT = 205
+ PARTIAL_CONTENT = 206
+ MULTIPLE_CHOICES = 300
+ MOVED_PERMANENTLY = 301
+ FOUND = 302
+ SEE_OTHER = 303
+ NOT_MODIFIED = 304
+ USE_PROXY = 305
+ RESERVED = 306
+ TEMPORARY_REDIRECT = 307
+ BAD_REQUEST = 400
+ UNAUTHORIZED = 401
+ PAYMENT_REQUIRED = 402
+ FORBIDDEN = 403
+ NOT_FOUND = 404
+ METHOD_NOT_ALLOWED = 405
+ NOT_ACCEPTABLE = 406
+ PROXY_AUTHENTICATION_REQUIRED = 407
+ REQUEST_TIMEOUT = 408
+ CONFLICT = 409
+ GONE = 410
+ LENGTH_REQUIRED = 411
+ PRECONDITION_FAILED = 412
+ REQUEST_ENTITY_TOO_LARGE = 413
+ REQUEST_URI_TOO_LONG = 414
+ UNSUPPORTED_MEDIA_TYPE = 415
+ REQUESTED_RANGE_NOT_SATISFIABLE = 416
+ EXPECTATION_FAILED = 417
+ INTERNAL_SERVER_ERROR = 500
+ NOT_IMPLEMENTED = 501
+ BAD_GATEWAY = 502
+ SERVICE_UNAVAILABLE = 503
+ GATEWAY_TIMEOUT = 504
+ HTTP_VERSION_NOT_SUPPORTED = 505
+
+
+
+# This is simply stylistic, I think 'status.HTTP_200_OK' reads nicely.
+status = Status()
+
+
+class NoContent(object):
+ """Used to indicate no body in http response.
+ (We cannot just use None, as that is a valid, serializable response object.)"""
+ pass
+
+
+class Response(object):
+ def __init__(self, status, content=NoContent, headers={}):
+ self.status = status
+ self.has_content_body = not content is NoContent
+ self.raw_content = content # content prior to filtering
+ self.cleaned_content = content # content after filtering
+ self.headers = headers
+
+ @property
+ def status_text(self):
+ """Return reason text corrosponding to our HTTP response status code.
+ Provided for convienience."""
+ return STATUS_CODE_TEXT.get(self.status, '')
+
+
+class ResponseException(BaseException):
+ def __init__(self, status, content=NoContent, headers={}):
+ self.response = Response(status, content=content, headers=headers)
diff --git a/djangorestframework/templates/emitter.html b/djangorestframework/templates/emitter.html
new file mode 100644
index 00000000..d21350cd
--- /dev/null
+++ b/djangorestframework/templates/emitter.html
@@ -0,0 +1,108 @@
+{% load urlize_quoted_links %}{% load add_query_param %}
+
+
+
+
+ API - {{ resource.name }}
+
+
+
+
+
{{ resource.name }}
+
{{ resource.description|linebreaksbr }}
+
{{ response.status }} {{ response.status_text }}{% autoescape off %}
+{% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
+{% endfor %}
+{{ content|urlize_quoted_links }}{% endautoescape %}
+
+ {% if 'GET' in resource.allowed_methods %}
+
+
GET
+
+ {% for media_type in resource.emitted_media_types %}
+ {% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %}
+ - [{{ media_type }}]
+ {% endwith %}
+ {% endfor %}
+
+
+
+ {% endif %}
+
+ {% comment %} *** Only display the POST/PUT/DELETE forms if we have a bound form, and if method ***
+ *** tunneling via POST forms is enabled. ***
+ *** (We could display only the POST form if method tunneling is disabled, but I think ***
+ *** the user experience would be confusing, so we simply turn all forms off. *** {% endcomment %}
+
+ {% if resource.METHOD_PARAM and form %}
+ {% if 'POST' in resource.allowed_methods %}
+
+ {% endif %}
+
+ {% if 'PUT' in resource.allowed_methods %}
+
+ {% endif %}
+
+ {% if 'DELETE' in resource.allowed_methods %}
+
+
+
+ {% endif %}
+ {% endif %}
+
+
+
\ No newline at end of file
diff --git a/djangorestframework/templates/emitter.txt b/djangorestframework/templates/emitter.txt
new file mode 100644
index 00000000..1cc7d1d7
--- /dev/null
+++ b/djangorestframework/templates/emitter.txt
@@ -0,0 +1,8 @@
+{{ resource.name }}
+
+{{ resource.description }}
+
+{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
+{% for key, val in response.headers.items %}{{ key }}: {{ val }}
+{% endfor %}
+{{ content }}{% endautoescape %}
diff --git a/djangorestframework/templatetags/__init__.py b/djangorestframework/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/djangorestframework/templatetags/add_query_param.py b/djangorestframework/templatetags/add_query_param.py
new file mode 100644
index 00000000..91c1a312
--- /dev/null
+++ b/djangorestframework/templatetags/add_query_param.py
@@ -0,0 +1,17 @@
+from django.template import Library
+from urlparse import urlparse, urlunparse
+from urllib import quote
+register = Library()
+
+def add_query_param(url, param):
+ (key, val) = param.split('=')
+ param = '%s=%s' % (key, quote(val))
+ (scheme, netloc, path, params, query, fragment) = urlparse(url)
+ if query:
+ query += "&" + param
+ else:
+ query = param
+ return urlunparse((scheme, netloc, path, params, query, fragment))
+
+
+register.filter('add_query_param', add_query_param)
diff --git a/djangorestframework/templatetags/urlize_quoted_links.py b/djangorestframework/templatetags/urlize_quoted_links.py
new file mode 100644
index 00000000..eea424a4
--- /dev/null
+++ b/djangorestframework/templatetags/urlize_quoted_links.py
@@ -0,0 +1,100 @@
+"""Adds the custom filter 'urlize_quoted_links'
+
+This is identical to the built-in filter 'urlize' with the exception that
+single and double quotes are permitted as leading or trailing punctuation.
+"""
+
+# Almost all of this code is copied verbatim from django.utils.html
+# LEADING_PUNCTUATION and TRAILING_PUNCTUATION have been modified
+import re
+import string
+
+from django.utils.safestring import SafeData, mark_safe
+from django.utils.encoding import force_unicode
+from django.utils.http import urlquote
+from django.utils.html import escape
+from django import template
+
+# Configuration for urlize() function.
+LEADING_PUNCTUATION = ['(', '<', '<', '"', "'"]
+TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '>', '"', "'"]
+
+# List of possible strings used for bullets in bulleted lists.
+DOTS = ['·', '*', '\xe2\x80\xa2', '', '•', '•']
+
+unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
+word_split_re = re.compile(r'(\s+)')
+punctuation_re = re.compile('^(?P(?:%s)*)(?P.*?)(?P(?:%s)*)$' % \
+ ('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]),
+ '|'.join([re.escape(x) for x in TRAILING_PUNCTUATION])))
+simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
+link_target_attribute_re = re.compile(r'(]*?)target=[^\s>]+')
+html_gunk_re = re.compile(r'(?:
|<\/i>|<\/b>|<\/em>|<\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
+hard_coded_bullets_re = re.compile(r'((?:(?:%s).*?[a-zA-Z].*?
\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
+trailing_empty_content_re = re.compile(r'(?:(?: |\s|
)*?
\s*)+\Z')
+
+def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
+ """
+ Converts any URLs in text into clickable links.
+
+ Works on http://, https://, www. links and links ending in .org, .net or
+ .com. Links can have trailing punctuation (periods, commas, close-parens)
+ and leading punctuation (opening parens) and it'll still do the right
+ thing.
+
+ If trim_url_limit is not None, the URLs in link text longer than this limit
+ will truncated to trim_url_limit-3 characters and appended with an elipsis.
+
+ If nofollow is True, the URLs in link text will get a rel="nofollow"
+ attribute.
+
+ If autoescape is True, the link text and URLs will get autoescaped.
+ """
+ trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+ safe_input = isinstance(text, SafeData)
+ words = word_split_re.split(force_unicode(text))
+ nofollow_attr = nofollow and ' rel="nofollow"' or ''
+ for i, word in enumerate(words):
+ match = None
+ if '.' in word or '@' in word or ':' in word:
+ match = punctuation_re.match(word)
+ if match:
+ lead, middle, trail = match.groups()
+ # Make URL we want to point to.
+ url = None
+ if middle.startswith('http://') or middle.startswith('https://'):
+ url = urlquote(middle, safe='/&=:;#?+*')
+ elif middle.startswith('www.') or ('@' not in middle and \
+ middle and middle[0] in string.ascii_letters + string.digits and \
+ (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
+ url = urlquote('http://%s' % middle, safe='/&=:;#?+*')
+ elif '@' in middle and not ':' in middle and simple_email_re.match(middle):
+ url = 'mailto:%s' % middle
+ nofollow_attr = ''
+ # Make link.
+ if url:
+ trimmed = trim_url(middle)
+ if autoescape and not safe_input:
+ lead, trail = escape(lead), escape(trail)
+ url, trimmed = escape(url), escape(trimmed)
+ middle = '%s' % (url, nofollow_attr, trimmed)
+ words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
+ else:
+ if safe_input:
+ words[i] = mark_safe(word)
+ elif autoescape:
+ words[i] = escape(word)
+ elif safe_input:
+ words[i] = mark_safe(word)
+ elif autoescape:
+ words[i] = escape(word)
+ return u''.join(words)
+
+
+#urlize_quoted_links.needs_autoescape = True
+urlize_quoted_links.is_safe = True
+
+# Register urlize_quoted_links as a custom filter
+# http://docs.djangoproject.com/en/dev/howto/custom-template-tags/
+register = template.Library()
+register.filter(urlize_quoted_links)
diff --git a/djangorestframework/utils.py b/djangorestframework/utils.py
new file mode 100644
index 00000000..f9bbc0fe
--- /dev/null
+++ b/djangorestframework/utils.py
@@ -0,0 +1,179 @@
+import re
+import xml.etree.ElementTree as ET
+from django.utils.encoding import smart_unicode
+from django.utils.xmlutils import SimplerXMLGenerator
+from django.core.urlresolvers import resolve
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+
+def url_resolves(url):
+ """Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
+ try:
+ resolve(url)
+ except:
+ return False
+ return True
+
+# From piston
+def coerce_put_post(request):
+ """
+ Django doesn't particularly understand REST.
+ In case we send data over PUT, Django won't
+ actually look at the data and load it. We need
+ to twist its arm here.
+
+ The try/except abominiation here is due to a bug
+ in mod_python. This should fix it.
+ """
+ if request.method != 'PUT':
+ return
+
+ # Bug fix: if _load_post_and_files has already been called, for
+ # example by middleware accessing request.POST, the below code to
+ # pretend the request is a POST instead of a PUT will be too late
+ # to make a difference. Also calling _load_post_and_files will result
+ # in the following exception:
+ # AttributeError: You cannot set the upload handlers after the upload has been processed.
+ # The fix is to check for the presence of the _post field which is set
+ # the first time _load_post_and_files is called (both by wsgi.py and
+ # modpython.py). If it's set, the request has to be 'reset' to redo
+ # the query value parsing in POST mode.
+ if hasattr(request, '_post'):
+ del request._post
+ del request._files
+
+ try:
+ request.method = "POST"
+ request._load_post_and_files()
+ request.method = "PUT"
+ except AttributeError:
+ request.META['REQUEST_METHOD'] = 'POST'
+ request._load_post_and_files()
+ request.META['REQUEST_METHOD'] = 'PUT'
+
+ request.PUT = request.POST
+
+# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
+#class object_dict(dict):
+# """object view of dict, you can
+# >>> a = object_dict()
+# >>> a.fish = 'fish'
+# >>> a['fish']
+# 'fish'
+# >>> a['water'] = 'water'
+# >>> a.water
+# 'water'
+# >>> a.test = {'value': 1}
+# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
+# >>> a.test, a.test2.name, a.test2.value
+# (1, 'test2', 2)
+# """
+# def __init__(self, initd=None):
+# if initd is None:
+# initd = {}
+# dict.__init__(self, initd)
+#
+# def __getattr__(self, item):
+# d = self.__getitem__(item)
+# # if value is the only key in object, you can omit it
+# if isinstance(d, dict) and 'value' in d and len(d) == 1:
+# return d['value']
+# else:
+# return d
+#
+# def __setattr__(self, item, value):
+# self.__setitem__(item, value)
+
+
+# From xml2dict
+class XML2Dict(object):
+
+ def __init__(self):
+ pass
+
+ def _parse_node(self, node):
+ node_tree = {}
+ # Save attrs and text, hope there will not be a child with same name
+ if node.text:
+ node_tree = node.text
+ for (k,v) in node.attrib.items():
+ k,v = self._namespace_split(k, v)
+ node_tree[k] = v
+ #Save childrens
+ for child in node.getchildren():
+ tag, tree = self._namespace_split(child.tag, self._parse_node(child))
+ if tag not in node_tree: # the first time, so store it in dict
+ node_tree[tag] = tree
+ continue
+ old = node_tree[tag]
+ if not isinstance(old, list):
+ node_tree.pop(tag)
+ node_tree[tag] = [old] # multi times, so change old dict to a list
+ node_tree[tag].append(tree) # add the new one
+
+ return node_tree
+
+
+ def _namespace_split(self, tag, value):
+ """
+ Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
+ ns = http://cs.sfsu.edu/csc867/myscheduler
+ name = patients
+ """
+ result = re.compile("\{(.*)\}(.*)").search(tag)
+ if result:
+ value.namespace, tag = result.groups()
+ return (tag, value)
+
+ def parse(self, file):
+ """parse a xml file to a dict"""
+ f = open(file, 'r')
+ return self.fromstring(f.read())
+
+ def fromstring(self, s):
+ """parse a string"""
+ t = ET.fromstring(s)
+ unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
+ return root_tree
+
+
+def xml2dict(input):
+ return XML2Dict().fromstring(input)
+
+
+# Piston:
+class XMLEmitter():
+ def _to_xml(self, xml, data):
+ if isinstance(data, (list, tuple)):
+ for item in data:
+ xml.startElement("list-item", {})
+ self._to_xml(xml, item)
+ xml.endElement("list-item")
+
+ elif isinstance(data, dict):
+ for key, value in data.iteritems():
+ xml.startElement(key, {})
+ self._to_xml(xml, value)
+ xml.endElement(key)
+
+ else:
+ xml.characters(smart_unicode(data))
+
+ def dict2xml(self, data):
+ stream = StringIO.StringIO()
+
+ xml = SimplerXMLGenerator(stream, "utf-8")
+ xml.startDocument()
+ xml.startElement("root", {})
+
+ self._to_xml(xml, data)
+
+ xml.endElement("root")
+ xml.endDocument()
+ return stream.getvalue()
+
+def dict2xml(input):
+ return XMLEmitter().dict2xml(input)
diff --git a/docs/conf.py b/docs/conf.py
index 71cacb3d..3689a636 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -14,7 +14,7 @@
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
-sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'flywheel'))
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'djangorestframework'))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'examples'))
import settings
from django.core.management import setup_environ
diff --git a/docs/index.rst b/docs/index.rst
index 7a871a5c..7da3f017 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -26,10 +26,10 @@ Requirements
Installation & Setup
--------------------
-The django-rest-framework project is hosted as a `mercurial repository on bitbucket `_.
+The django-rest-framework project is hosted as a `mercurial repository on bitbucket `_.
To get a local copy of the repository use mercurial::
- hg clone https://tomchristie@bitbucket.org/tomchristie/flywheel
+ hg clone https://tomchristie@bitbucket.org/tomchristie/django-rest-framework
To add django-rest-framework to a django project:
@@ -43,27 +43,15 @@ Getting Started
Often you'll want parts of your API to directly map to existing Models.
At it's simplest this looks something like this...
-``views.py``::
+``urls.py``
- from djangorestframework.modelresource import ModelResource, ModelRootResource
- from models import MyModel
+.. include:: ../examples/simpleexample/urls.py
+ :literal:
- class MyModelRootResource(ModelRootResource):
- """A create/list resource for MyModel."""
- allowed_methods = ('GET', 'POST')
- model = MyModel
+``views.py``
- class MyModelResource(ModelResource):
- """A read/update/delete resource for MyModel."""
- allowed_methods = ('GET', 'PUT', 'DELETE')
- model = MyModel
-
-``urls.py``::
-
- urlpatterns += patterns('myapp.views',
- url(r'^mymodel/$', 'MyModelRootResource'),
- url(r'^mymodel/([^/]+)/$', 'MyModelResource'),
- )
+.. include:: ../examples/simpleexample/views.py
+ :literal:
Examples
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..77cdf485
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,8 @@
+# Documentation requires Django & Sphinx, and their dependencies...
+
+Django==1.2.4
+Jinja2==2.5.5
+Pygments==1.4
+Sphinx==1.0.7
+docutils==0.7
+wsgiref==0.1.2
diff --git a/examples/blogpost/views.py b/examples/blogpost/views.py
index c5be2544..bfb53b5d 100644
--- a/examples/blogpost/views.py
+++ b/examples/blogpost/views.py
@@ -1,6 +1,6 @@
-from flywheel.response import Response, status
-from flywheel.resource import Resource
-from flywheel.modelresource import ModelResource, RootModelResource
+from djangorestframework.response import Response, status
+from djangorestframework.resource import Resource
+from djangorestframework.modelresource import ModelResource, RootModelResource
from blogpost.models import BlogPost, Comment
BLOG_POST_FIELDS = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
diff --git a/examples/objectstore/views.py b/examples/objectstore/views.py
index 4681fe70..90bdb9e3 100644
--- a/examples/objectstore/views.py
+++ b/examples/objectstore/views.py
@@ -1,7 +1,7 @@
from django.conf import settings
-from flywheel.resource import Resource
-from flywheel.response import Response, status
+from djangorestframework.resource import Resource
+from djangorestframework.response import Response, status
import pickle
import os
diff --git a/examples/pygments_api/views.py b/examples/pygments_api/views.py
index d9082ada..e22705d9 100644
--- a/examples/pygments_api/views.py
+++ b/examples/pygments_api/views.py
@@ -1,8 +1,8 @@
from django.conf import settings
-from flywheel.resource import Resource
-from flywheel.response import Response, status
-from flywheel.emitters import BaseEmitter
+from djangorestframework.resource import Resource
+from djangorestframework.response import Response, status
+from djangorestframework.emitters import BaseEmitter
from pygments.formatters import HtmlFormatter
from pygments.lexers import get_lexer_by_name
diff --git a/examples/requirements.txt b/examples/requirements.txt
new file mode 100644
index 00000000..4ae9e3c7
--- /dev/null
+++ b/examples/requirements.txt
@@ -0,0 +1,6 @@
+# For the examples we need Django, pygments and httplib2...
+
+Django==1.2.4
+wsgiref==0.1.2
+Pygments==1.4
+httplib2==0.6.0
diff --git a/examples/settings.py b/examples/settings.py
index 0ae3bf56..4aa5dd00 100644
--- a/examples/settings.py
+++ b/examples/settings.py
@@ -93,9 +93,10 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
- 'django.contrib.admin',
- 'flywheel',
- 'blogpost',
+ #'django.contrib.admin',
+ 'djangorestframework',
+ 'simpleexample',
'objectstore',
- 'pygments_api'
+ 'pygments_api',
+ 'blogpost',
)
diff --git a/examples/simpleexample/__init__.py b/examples/simpleexample/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/simpleexample/models.py b/examples/simpleexample/models.py
new file mode 100644
index 00000000..13867f61
--- /dev/null
+++ b/examples/simpleexample/models.py
@@ -0,0 +1,23 @@
+from django.db import models
+
+MAX_INSTANCES = 20
+
+class MyModel(models.Model):
+ foo = models.BooleanField()
+ bar = models.IntegerField(help_text='Must be an integer.')
+ baz = models.CharField(max_length=32, help_text='Free text. Max length 32 chars.')
+ created = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ ordering = ('created',)
+
+ def save(self, *args, **kwargs):
+ """For the purposes of the sandbox, limit the maximum number of stored models."""
+ while MyModel.objects.all().count() > MAX_INSTANCES:
+ MyModel.objects.all()[0].delete()
+ super(MyModel, self).save(*args, **kwargs)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('simpleexample.views.MyModelResource', (self.pk,))
+
diff --git a/examples/simpleexample/urls.py b/examples/simpleexample/urls.py
new file mode 100644
index 00000000..d853ba5a
--- /dev/null
+++ b/examples/simpleexample/urls.py
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import patterns, url
+
+urlpatterns = patterns('simpleexample.views',
+ url(r'^$', 'MyModelRootResource'),
+ url(r'^([0-9]+)/$', 'MyModelResource'),
+)
diff --git a/examples/simpleexample/views.py b/examples/simpleexample/views.py
new file mode 100644
index 00000000..1f113ac2
--- /dev/null
+++ b/examples/simpleexample/views.py
@@ -0,0 +1,18 @@
+from djangorestframework.modelresource import ModelResource, RootModelResource
+from simpleexample.models import MyModel
+
+FIELDS = ('foo', 'bar', 'baz', 'absolute_url')
+
+class MyModelRootResource(RootModelResource):
+ """A create/list resource for MyModel.
+ Available for both authenticated and anonymous access for the purposes of the sandbox."""
+ model = MyModel
+ allowed_methods = anon_allowed_methods = ('GET', 'POST')
+ fields = FIELDS
+
+class MyModelResource(ModelResource):
+ """A read/update/delete resource for MyModel.
+ Available for both authenticated and anonymous access for the purposes of the sandbox."""
+ model = MyModel
+ allowed_methods = anon_allowed_methods = ('GET', 'PUT', 'DELETE')
+ fields = FIELDS
diff --git a/examples/urls.py b/examples/urls.py
index b1dec13d..2b8e6fcd 100644
--- a/examples/urls.py
+++ b/examples/urls.py
@@ -1,14 +1,27 @@
from django.conf.urls.defaults import patterns, include
-from django.contrib import admin
+#from django.contrib import admin
+from djangorestframework.resource import Resource
+
+#admin.autodiscover()
+
+class RootResource(Resource):
+ allowed_methods = anon_allowed_methods = ('GET',)
+
+ def get(self, request, auth):
+ return {'simple example': self.reverse('simpleexample.views.MyModelRootResource'),
+ 'pygments example': self.reverse('pygments_api.views.PygmentsRoot'),
+ 'object store example': self.reverse('objectstore.views.ObjectStoreRoot'),
+ 'blog post example': self.reverse('blogpost.views.BlogPostRoot'),}
-admin.autodiscover()
urlpatterns = patterns('',
- (r'^pygments-example/', include('pygments_api.urls')),
- (r'^blog-post-example/', include('blogpost.urls')),
- (r'^object-store-example/', include('objectstore.urls')),
+ (r'^$', RootResource),
+ (r'^simple-example/', include('simpleexample.urls')),
+ (r'^object-store/', include('objectstore.urls')),
+ (r'^pygments/', include('pygments_api.urls')),
+ (r'^blog-post/', include('blogpost.urls')),
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
(r'^accounts/logout/$', 'django.contrib.auth.views.logout'),
- (r'^admin/doc/', include('django.contrib.admindocs.urls')),
- (r'^admin/', include(admin.site.urls)),
+ #(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+ #(r'^admin/', include(admin.site.urls)),
)
diff --git a/flywheel/__init__.py b/flywheel/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/flywheel/authenticators.py b/flywheel/authenticators.py
deleted file mode 100644
index 8de182de..00000000
--- a/flywheel/authenticators.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from django.contrib.auth import authenticate
-import base64
-
-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 authenticate(self, request):
- """Authenticate the request and return the authentication context or None.
-
- 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.
-
- The authentication context is passed to the method calls eg Resource.get(request, auth) in order to
- allow them to apply any more fine grained permission checking at the point the response is being generated.
-
- This function must be overridden to be implemented."""
- return None
-
-
-class BasicAuthenticator(BaseAuthenticator):
- """Use HTTP Basic authentication"""
- def authenticate(self, request):
- if 'HTTP_AUTHORIZATION' in request.META:
- auth = request.META['HTTP_AUTHORIZATION'].split()
- if len(auth) == 2 and auth[0].lower() == "basic":
- uname, passwd = base64.b64decode(auth[1]).split(':')
- user = authenticate(username=uname, password=passwd)
- if user is not None and user.is_active:
- return user
- return None
-
-
-class UserLoggedInAuthenticator(BaseAuthenticator):
- """Use Djagno's built-in request session for authentication."""
- def authenticate(self, request):
- if request.user and request.user.is_active:
- return request.user
- return None
-
diff --git a/flywheel/emitters.py b/flywheel/emitters.py
deleted file mode 100644
index f548e1d9..00000000
--- a/flywheel/emitters.py
+++ /dev/null
@@ -1,216 +0,0 @@
-"""Emitters are used to serialize a Resource's output into specific media types.
-FlyWheel also provides HTML and PlainText emitters 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, emitters and parsers on the Resource.
-"""
-from django.conf import settings
-from django.template import RequestContext, loader
-from django import forms
-
-from flywheel.response import NoContent
-from flywheel.utils import dict2xml, url_resolves
-
-from urllib import quote_plus
-import string
-try:
- import json
-except ImportError:
- import simplejson as json
-
-
-
-# TODO: Rename verbose to something more appropriate
-# TODO: NoContent could be handled more cleanly. It'd be nice if it was handled by default,
-# and only have an emitter output anything if it explicitly provides support for that.
-
-class BaseEmitter(object):
- """All emitters must extend this class, set the media_type attribute, and
- override the emit() function."""
- media_type = None
-
- def __init__(self, resource):
- self.resource = resource
-
- def emit(self, output=NoContent, verbose=False):
- """By default emit simply returns the ouput as-is.
- Override this method to provide for other behaviour."""
- if output is NoContent:
- return ''
-
- return output
-
-
-class TemplateEmitter(BaseEmitter):
- """Provided for convienience.
- Emit the output by simply rendering it with the given template."""
- media_type = None
- template = None
-
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
- return ''
-
- context = RequestContext(self.resource.request, output)
- return self.template.render(context)
-
-
-class DocumentingTemplateEmitter(BaseEmitter):
- """Base class for emitters used to self-document the API.
- Implementing classes should extend this class and set the template attribute."""
- template = None
-
- def _get_content(self, resource, output):
- """Get the content as if it had been emitted by a non-documenting emitter.
-
- (Typically this will be the content as it would have been if the Resource had been
- requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)"""
-
- # Find the first valid emitter and emit the content. (Don't use another documenting emitter.)
- emitters = [emitter for emitter in resource.emitters if not isinstance(emitter, DocumentingTemplateEmitter)]
- if not emitters:
- return '[No emitters were found]'
-
- content = emitters[0](resource).emit(output, verbose=True)
- if not all(char in string.printable for char in content):
- return '[%d bytes of binary content]'
-
- return content
-
-
- def _get_form_instance(self, resource):
- """Get a form, possibly bound to either the input or output data.
- In the absence on of the Resource having an associated form then
- provide a form that can be used to submit arbitrary content."""
- # Get the form instance if we have one bound to the input
- form_instance = resource.form_instance
-
- # Otherwise if this isn't an error response
- # then attempt to get a form bound to the response object
- if not form_instance and resource.response.has_content_body:
- try:
- form_instance = resource.get_form(resource.response.raw_content)
- if form_instance:
- form_instance.is_valid()
- except:
- form_instance = None
-
- # If we still don't have a form instance then try to get an unbound form
- if not form_instance:
- try:
- form_instance = self.resource.get_form()
- except:
- pass
-
- # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
- if not form_instance:
- form_instance = self._get_generic_content_form(resource)
-
- return form_instance
-
-
- def _get_generic_content_form(self, resource):
- """Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
- (Which are typically application/x-www-form-urlencoded)"""
-
- # NB. http://jacobian.org/writing/dynamic-form-generation/
- class GenericContentForm(forms.Form):
- def __init__(self, resource):
- """We don't know the names of the fields we want to set until the point the form is instantiated,
- as they are determined by the Resource the form is being created against.
- Add the fields dynamically."""
- super(GenericContentForm, self).__init__()
-
- contenttype_choices = [(media_type, media_type) for media_type in resource.parsed_media_types]
- initial_contenttype = resource.default_parser.media_type
-
- self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
- choices=contenttype_choices,
- initial=initial_contenttype)
- self.fields[resource.CONTENT_PARAM] = forms.CharField(label='Content',
- widget=forms.Textarea)
-
- # If either of these reserved parameters are turned off then content tunneling is not possible
- if self.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None:
- return None
-
- # Okey doke, let's do it
- return GenericContentForm(resource)
-
-
- def emit(self, output=NoContent):
- content = self._get_content(self.resource, output)
- form_instance = self._get_form_instance(self.resource)
-
- if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL):
- login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path))
- logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path))
- else:
- login_url = None
- logout_url = None
-
- template = loader.get_template(self.template)
- context = RequestContext(self.resource.request, {
- 'content': content,
- 'resource': self.resource,
- 'request': self.resource.request,
- 'response': self.resource.response,
- 'form': form_instance,
- 'login_url': login_url,
- 'logout_url': logout_url,
- })
-
- ret = template.render(context)
-
- # Munge DELETE Response code to allow us to return content
- # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output)
- if self.resource.response.status == 204:
- self.resource.response.status = 200
-
- return ret
-
-
-class JSONEmitter(BaseEmitter):
- """Emitter which serializes to JSON"""
- media_type = 'application/json'
-
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
- return ''
- if verbose:
- return json.dumps(output, indent=4, sort_keys=True)
- return json.dumps(output)
-
-
-class XMLEmitter(BaseEmitter):
- """Emitter which serializes to XML."""
- media_type = 'application/xml'
-
- def emit(self, output=NoContent, verbose=False):
- if output is NoContent:
- return ''
- return dict2xml(output)
-
-
-class DocumentingHTMLEmitter(DocumentingTemplateEmitter):
- """Emitter which provides a browsable HTML interface for an API.
- See the examples listed in the FlyWheel documentation to see this in actions."""
- media_type = 'text/html'
- template = 'emitter.html'
-
-
-class DocumentingXHTMLEmitter(DocumentingTemplateEmitter):
- """Identical to DocumentingHTMLEmitter, except with an xhtml media type.
- We need this to be listed in preference to xml in order to return HTML to WebKit based browsers,
- given their Accept headers."""
- media_type = 'application/xhtml+xml'
- template = 'emitter.html'
-
-
-class DocumentingPlainTextEmitter(DocumentingTemplateEmitter):
- """Emitter that serializes the output with the default emitter, but also provides plain-text
- doumentation of the returned status and headers, and of the resource's name and description.
- Useful for browsing an API with command line tools."""
- media_type = 'text/plain'
- template = 'emitter.txt'
-
-
diff --git a/flywheel/modelresource.py b/flywheel/modelresource.py
deleted file mode 100644
index d68ec79e..00000000
--- a/flywheel/modelresource.py
+++ /dev/null
@@ -1,402 +0,0 @@
-from django.forms import ModelForm
-from django.db.models.query import QuerySet
-from django.db.models import Model
-
-from flywheel.response import status, Response, ResponseException
-from flywheel.resource import Resource
-
-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."""
-
- # 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 emitter"""
- 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, '__emittable__'):
- f = thing.__emittable__
- if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
- ret = _any(f())
- else:
- ret = str(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)
-
-
- def post(self, request, content, *args, **kwargs):
- # TODO: test creation on a non-existing resource url
- all_kw_args = dict(content.items() + kwargs.items())
- instance = self.model(**all_kw_args)
- instance.save()
- headers = {}
- if hasattr(instance, 'get_absolute_url'):
- headers['Location'] = self.add_domain(instance.get_absolute_url())
- return Response(status.HTTP_201_CREATED, instance, headers)
-
- def get(self, request, *args, **kwargs):
- try:
- instance = self.model.objects.get(**kwargs)
- except self.model.DoesNotExist:
- raise ResponseException(status.HTTP_404_NOT_FOUND)
-
- return instance
-
- def put(self, request, content, *args, **kwargs):
- # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url
- try:
- instance = self.model.objects.get(**kwargs)
- for (key, val) in content.items():
- setattr(instance, key, val)
- except self.model.DoesNotExist:
- instance = self.model(**content)
- instance.save()
-
- instance.save()
- return instance
-
- def delete(self, request, *args, **kwargs):
- try:
- instance = self.model.objects.get(**kwargs)
- except self.model.DoesNotExist:
- raise ResponseException(status.HTTP_404_NOT_FOUND, None, {})
-
- instance.delete()
- return
-
-
-class RootModelResource(ModelResource):
- """A Resource which provides default operations for list and create."""
- allowed_methods = ('GET', 'POST')
- queryset = None
-
- def get(self, request, *args, **kwargs):
- queryset = self.queryset if self.queryset else self.model.objects.all()
- return queryset
-
-
-class QueryModelResource(ModelResource):
- """Resource with default operations for list.
- TODO: provide filter/order/num_results/paging, and a create operation to create queries."""
- allowed_methods = ('GET',)
- queryset = None
-
- def get_form(self, data=None):
- return None
-
- def get(self, request, *args, **kwargs):
- queryset = self.queryset if self.queryset else self.model.objects.all()
- return queryset
-
diff --git a/flywheel/parsers.py b/flywheel/parsers.py
deleted file mode 100644
index 98232a96..00000000
--- a/flywheel/parsers.py
+++ /dev/null
@@ -1,89 +0,0 @@
-from flywheel.response import status, ResponseException
-
-try:
- import json
-except ImportError:
- import simplejson as json
-
-# TODO: Make all parsers only list a single media_type, rather than a list
-
-class BaseParser(object):
- """All parsers should extend BaseParser, specifing a media_type attribute,
- and overriding the parse() method."""
-
- media_type = None
-
- def __init__(self, resource):
- """Initialise the parser with the Resource instance as state,
- in case the parser needs to access any metadata on the Resource object."""
- self.resource = resource
-
- def parse(self, input):
- """Given some serialized input, return the deserialized output.
- The input will be the raw request content body. The return value may be of
- any type, but for many parsers/inputs it might typically be a dict."""
- return input
-
-
-class JSONParser(BaseParser):
- media_type = 'application/json'
-
- def parse(self, input):
- try:
- return json.loads(input)
- except ValueError, exc:
- raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
-
-
-class XMLParser(BaseParser):
- media_type = 'application/xml'
-
-
-class FormParser(BaseParser):
- """The default parser for form data.
- Return a dict containing a single value for each non-reserved parameter.
- """
-
- media_type = 'application/x-www-form-urlencoded'
-
- def parse(self, input):
- # The FormParser doesn't parse the input as other parsers would, since Django's already done the
- # form parsing for us. We build the content object from the request directly.
- request = self.resource.request
-
- if request.method == 'PUT':
- # Fix from piston to force Django to give PUT requests the same
- # form processing that POST requests get...
- #
- # Bug fix: if _load_post_and_files has already been called, for
- # example by middleware accessing request.POST, the below code to
- # pretend the request is a POST instead of a PUT will be too late
- # to make a difference. Also calling _load_post_and_files will result
- # in the following exception:
- # AttributeError: You cannot set the upload handlers after the upload has been processed.
- # The fix is to check for the presence of the _post field which is set
- # the first time _load_post_and_files is called (both by wsgi.py and
- # modpython.py). If it's set, the request has to be 'reset' to redo
- # the query value parsing in POST mode.
- if hasattr(request, '_post'):
- del request._post
- del request._files
-
- try:
- request.method = "POST"
- request._load_post_and_files()
- request.method = "PUT"
- except AttributeError:
- request.META['REQUEST_METHOD'] = 'POST'
- request._load_post_and_files()
- request.META['REQUEST_METHOD'] = 'PUT'
-
- # Strip any parameters that we are treating as reserved
- data = {}
- for (key, val) in request.POST.items():
- if key not in self.resource.RESERVED_FORM_PARAMS:
- data[key] = val
-
- return data
-
-
diff --git a/flywheel/resource.py b/flywheel/resource.py
deleted file mode 100644
index 2a8554f3..00000000
--- a/flywheel/resource.py
+++ /dev/null
@@ -1,451 +0,0 @@
-from django.contrib.sites.models import Site
-from django.core.urlresolvers import reverse
-from django.http import HttpResponse
-
-from flywheel import emitters, parsers, authenticators
-from flywheel.response import status, Response, ResponseException
-
-from decimal import Decimal
-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 emitted if settings.DEBUG
-#
-
-__all__ = ['Resource']
-
-
-
-class Resource(object):
- """Handles incoming requests and maps them to REST operations,
- performing authentication, input deserialization, input validation, output serialization."""
-
- # List of RESTful operations which may be performed on this resource.
- allowed_methods = ('GET',)
- anon_allowed_methods = ()
-
- # List of emitters the resource can serialize the response with, ordered by preference
- emitters = ( emitters.JSONEmitter,
- emitters.DocumentingHTMLEmitter,
- emitters.DocumentingXHTMLEmitter,
- emitters.DocumentingPlainTextEmitter,
- emitters.XMLEmitter )
-
- # List of content-types the resource can read from
- parsers = ( parsers.JSONParser,
- parsers.XMLParser,
- parsers.FormParser )
-
- # List of all authenticating methods to attempt
- authenticators = ( authenticators.UserLoggedInAuthenticator,
- authenticators.BasicAuthenticator )
-
- # Optional form for input validation and presentation of HTML formatted responses.
- form = None
-
- # Map standard HTTP methods to function calls
- callmap = { 'GET': 'get', 'POST': 'post',
- 'PUT': 'put', 'DELETE': 'delete' }
-
- # Some reserved parameters to allow us to use standard HTML forms with our resource
- # Override any/all of these with None to disable them, or override them with another value to rename them.
- ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params
- METHOD_PARAM = '_method' # Allow POST overloading in form params
- CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header in form params (allows sending arbitrary content with standard forms)
- CONTENT_PARAM = '_content' # Allow override of body content in form params (allows sending arbitrary content with standard forms)
- CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token used in form params
-
-
- def __new__(cls, *args, **kwargs):
- """Make the class callable so it can be used as a Django view."""
- self = object.__new__(cls)
- if args:
- request = args[0]
- self.__init__(request)
- return self._handle_request(request, *args[1:], **kwargs)
- else:
- self.__init__()
- return self
-
-
- def __init__(self, request=None):
- """"""
- # Setup the resource context
- self.request = request
- self.response = None
- self.form_instance = None
-
- # These sets are determined now so that overridding classes can modify the various parameter names,
- # or set them to None to disable them.
- self.RESERVED_FORM_PARAMS = set((self.METHOD_PARAM, self.CONTENTTYPE_PARAM, self.CONTENT_PARAM, self.CSRF_PARAM))
- self.RESERVED_QUERY_PARAMS = set((self.ACCEPT_QUERY_PARAM))
- self.RESERVED_FORM_PARAMS.discard(None)
- self.RESERVED_QUERY_PARAMS.discard(None)
-
-
- @property
- def name(self):
- """Provide a name for the resource.
- By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
- class_name = self.__class__.__name__
- return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip()
-
- @property
- def description(self):
- """Provide a description for the resource.
- By default this is the class's docstring with leading line spaces stripped."""
- return re.sub(re.compile('^ +', re.MULTILINE), '', self.__doc__)
-
- @property
- def emitted_media_types(self):
- """Return an list of all the media types that this resource can emit."""
- return [emitter.media_type for emitter in self.emitters]
-
- @property
- def default_emitter(self):
- """Return the resource's most prefered emitter.
- (This emitter is used if the client does not send and Accept: header, or sends Accept: */*)"""
- return self.emitters[0]
-
- @property
- def parsed_media_types(self):
- """Return an list of all the media types that this resource can emit."""
- return [parser.media_type for parser in self.parsers]
-
- @property
- def default_parser(self):
- """Return the resource's most prefered emitter.
- (This has no behavioural effect, but is may be used by documenting emitters)"""
- return self.parsers[0]
-
-
- def get(self, request, auth, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('GET')
-
-
- def post(self, request, auth, content, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('POST')
-
-
- def put(self, request, auth, content, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('PUT')
-
-
- def delete(self, request, auth, *args, **kwargs):
- """Must be subclassed to be implemented."""
- self.not_implemented('DELETE')
-
-
- def reverse(self, view, *args, **kwargs):
- """Return a fully qualified URI for a given view or resource.
- Add the domain using the Sites framework if possible, otherwise fallback to using the current request."""
- return self.add_domain(reverse(view, args=args, kwargs=kwargs))
-
-
- def not_implemented(self, operation):
- """Return an HTTP 500 server error if an operation is called which has been allowed by
- allowed_methods, but which has not been implemented."""
- raise ResponseException(status.HTTP_500_INTERNAL_SERVER_ERROR,
- {'detail': '%s operation on this resource has not been implemented' % (operation, )})
-
-
- def add_domain(self, path):
- """Given a path, return an fully qualified URI.
- Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
-
- # Note that out-of-the-box the Sites framework uses the reserved domain 'example.com'
- # See RFC 2606 - http://www.faqs.org/rfcs/rfc2606.html
- try:
- site = Site.objects.get_current()
- if site.domain and site.domain != 'example.com':
- return 'http://%s%s' % (site.domain, path)
- except:
- pass
-
- return self.request.build_absolute_uri(path)
-
-
- def determine_method(self, request):
- """Determine the HTTP method that this request should be treated as.
- Allows PUT and DELETE tunneling via the _method parameter if METHOD_PARAM is set."""
- method = request.method.upper()
-
- if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM):
- method = request.POST[self.METHOD_PARAM].upper()
-
- return method
-
-
- 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 self.authenticators:
- auth_context = authenticator(self).authenticate(request)
- if auth_context:
- return auth_context
-
- return None
-
-
- def check_method_allowed(self, method, auth):
- """Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
-
- if not method in self.callmap.keys():
- raise ResponseException(status.HTTP_501_NOT_IMPLEMENTED,
- {'detail': 'Unknown or unsupported method \'%s\'' % method})
-
- if not method in self.allowed_methods:
- raise ResponseException(status.HTTP_405_METHOD_NOT_ALLOWED,
- {'detail': 'Method \'%s\' not allowed on this resource.' % method})
-
- if auth is None and not method in self.anon_allowed_methods:
- raise ResponseException(status.HTTP_403_FORBIDDEN,
- {'detail': 'You do not have permission to access this resource. ' +
- 'You may need to login or otherwise authenticate the request.'})
-
- def get_form(self, data=None):
- """Optionally return a Django Form instance, which may be used for validation
- and/or rendered by an HTML/XHTML emitter.
-
- If data is not None the form will be bound to data."""
-
- if self.form:
- if data:
- return self.form(data)
- else:
- return self.form()
- return None
-
-
- def cleanup_request(self, data, form_instance):
- """Perform any resource-specific data deserialization and/or validation
- after the initial HTTP content-type deserialization has taken place.
-
- Returns a tuple containing the cleaned up data, and optionally a form bound to that data.
-
- By default this uses form validation to filter the basic input into the required types."""
-
- if form_instance is None:
- return data
-
- # Default form validation does not check for additional invalid fields
- non_existent_fields = []
- for key in set(data.keys()) - set(form_instance.fields.keys()):
- non_existent_fields.append(key)
-
- if not form_instance.is_valid() or non_existent_fields:
- if not form_instance.errors and not non_existent_fields:
- # If no data was supplied the errors property will be None
- details = 'No content was supplied'
-
- else:
- # Add standard field errors
- details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems() if key != '__all__')
-
- # Add any non-field errors
- if form_instance.non_field_errors():
- details['errors'] = form_instance.non_field_errors()
-
- # Add any non-existent field errors
- for key in non_existent_fields:
- details[key] = ['This field does not exist']
-
- # Bail. Note that we will still serialize this response with the appropriate content type
- raise ResponseException(status.HTTP_400_BAD_REQUEST, {'detail': details})
-
- return form_instance.cleaned_data
-
-
- 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."""
- return data
-
-
- def determine_parser(self, request):
- """Return the appropriate parser for the input, given the client's 'Content-Type' header,
- and the content types that this Resource knows how to parse."""
- content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded')
- raw_content = request.raw_post_data
-
- split = content_type.split(';', 1)
- if len(split) > 1:
- content_type = split[0]
- content_type = content_type.strip()
-
- # If CONTENTTYPE_PARAM is turned on, and this is a standard POST form then allow the content type to be overridden
- if (content_type == 'application/x-www-form-urlencoded' and
- request.method == 'POST' and
- self.CONTENTTYPE_PARAM and
- self.CONTENT_PARAM and
- request.POST.get(self.CONTENTTYPE_PARAM, None) and
- request.POST.get(self.CONTENT_PARAM, None)):
- raw_content = request.POST[self.CONTENT_PARAM]
- content_type = request.POST[self.CONTENTTYPE_PARAM]
-
- # Create a list of list of (media_type, Parser) tuples
- media_type_to_parser = dict([(parser.media_type, parser) for parser in self.parsers])
-
- try:
- return (media_type_to_parser[content_type], raw_content)
- except KeyError:
- raise ResponseException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
- {'detail': 'Unsupported media type \'%s\'' % content_type})
-
-
- def determine_emitter(self, request):
- """Return the appropriate emitter for the output, given the client's 'Accept' header,
- and the content types that this Resource knows how to serve.
-
- See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
-
- if self.ACCEPT_QUERY_PARAM and request.GET.get(self.ACCEPT_QUERY_PARAM, None):
- # Use _accept parameter override
- accept_list = [request.GET.get(self.ACCEPT_QUERY_PARAM)]
- elif request.META.has_key('HTTP_ACCEPT'):
- # Use standard HTTP Accept negotiation
- accept_list = request.META["HTTP_ACCEPT"].split(',')
- else:
- # No accept header specified
- return self.default_emitter
-
- # Parse the accept header into a dict of {qvalue: set of media types}
- # We ignore mietype parameters
- accept_dict = {}
- for token in accept_list:
- components = token.split(';')
- mimetype = components[0].strip()
- qvalue = Decimal('1.0')
-
- if len(components) > 1:
- # Parse items that have a qvalue eg text/html;q=0.9
- try:
- (q, num) = components[-1].split('=')
- if q == 'q':
- qvalue = Decimal(num)
- except:
- # Skip malformed entries
- continue
-
- if accept_dict.has_key(qvalue):
- accept_dict[qvalue].add(mimetype)
- else:
- accept_dict[qvalue] = set((mimetype,))
-
- # Convert to a list of sets ordered by qvalue (highest first)
- accept_sets = [accept_dict[qvalue] for qvalue in sorted(accept_dict.keys(), reverse=True)]
-
- for accept_set in accept_sets:
- # Return any exact match
- for emitter in self.emitters:
- if emitter.media_type in accept_set:
- return emitter
-
- # Return any subtype match
- for emitter in self.emitters:
- if emitter.media_type.split('/')[0] + '/*' in accept_set:
- return emitter
-
- # Return default
- if '*/*' in accept_set:
- return self.default_emitter
-
-
- raise ResponseException(status.HTTP_406_NOT_ACCEPTABLE,
- {'detail': 'Could not statisfy the client\'s Accept header',
- 'available_types': self.emitted_media_types})
-
-
- def _handle_request(self, request, *args, **kwargs):
- """This method is the core of Resource, through which all requests are passed.
-
- Broadly this consists of the following procedure:
-
- 0. ensure the operation is permitted
- 1. deserialize request content into request data, using standard HTTP content types (PUT/POST only)
- 2. cleanup and validate request data (PUT/POST only)
- 3. call the core method to get the response data
- 4. cleanup the response data
- 5. serialize response data into response content, using standard HTTP content negotiation
- """
- emitter = None
- method = self.determine_method(request)
-
- try:
- # Before we attempt anything else determine what format to emit our response data with.
- emitter = self.determine_emitter(request)
-
- # Authenticate the request, and store any context so that the resource operations can
- # do more fine grained authentication if required.
- #
- # Typically the context will be a user, or None if this is an anonymous request,
- # but it could potentially be more complex (eg the context of a request key which
- # has been signed against a particular set of permissions)
- auth_context = self.authenticate(request)
-
- # Ensure the requested operation is permitted on this resource
- self.check_method_allowed(method, auth_context)
-
- # Get the appropriate create/read/update/delete function
- func = getattr(self, self.callmap.get(method, None))
-
- # Either generate the response data, deserializing and validating any request data
- # TODO: Add support for message bodys on other HTTP methods, as it is valid.
- if method in ('PUT', 'POST'):
- (parser, raw_content) = self.determine_parser(request)
- data = parser(self).parse(raw_content)
- self.form_instance = self.get_form(data)
- data = self.cleanup_request(data, self.form_instance)
- response = func(request, auth_context, data, *args, **kwargs)
-
- else:
- response = func(request, auth_context, *args, **kwargs)
-
- # Allow return value to be either Response, or an object, or None
- if isinstance(response, Response):
- self.response = response
- elif response is not None:
- self.response = Response(status.HTTP_200_OK, response)
- else:
- self.response = Response(status.HTTP_204_NO_CONTENT)
-
- # Pre-serialize filtering (eg filter complex objects into natively serializable types)
- self.response.cleaned_content = self.cleanup_response(self.response.raw_content)
-
-
- except ResponseException, exc:
- self.response = exc.response
-
- # Fall back to the default emitter if we failed to perform content negotiation
- if emitter is None:
- emitter = self.default_emitter
-
-
- # Always add these headers
- self.response.headers['Allow'] = ', '.join(self.allowed_methods)
- self.response.headers['Vary'] = 'Authenticate, Allow'
-
- # Serialize the response content
- if self.response.has_content_body:
- content = emitter(self).emit(output=self.response.cleaned_content)
- else:
- content = emitter(self).emit()
-
- # Build the HTTP Response
- # TODO: Check if emitter.mimetype is underspecified, or if a content-type header has been set
- resp = HttpResponse(content, mimetype=emitter.media_type, status=self.response.status)
- for (key, val) in self.response.headers.items():
- resp[key] = val
-
- return resp
-
diff --git a/flywheel/response.py b/flywheel/response.py
deleted file mode 100644
index 4f23bb0a..00000000
--- a/flywheel/response.py
+++ /dev/null
@@ -1,125 +0,0 @@
-from django.core.handlers.wsgi import STATUS_CODE_TEXT
-
-__all__ =['status', 'NoContent', 'Response', ]
-
-
-class Status(object):
- """Descriptive HTTP status codes, for code readability.
- See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html"""
-
- # Verbose format (I prefer this as it's more explicit)
- HTTP_100_CONTINUE = 100
- HTTP_101_SWITCHING_PROTOCOLS = 101
- HTTP_200_OK = 200
- HTTP_201_CREATED = 201
- HTTP_202_ACCEPTED = 202
- HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
- HTTP_204_NO_CONTENT = 204
- HTTP_205_RESET_CONTENT = 205
- HTTP_206_PARTIAL_CONTENT = 206
- HTTP_300_MULTIPLE_CHOICES = 300
- HTTP_301_MOVED_PERMANENTLY = 301
- HTTP_302_FOUND = 302
- HTTP_303_SEE_OTHER = 303
- HTTP_304_NOT_MODIFIED = 304
- HTTP_305_USE_PROXY = 305
- HTTP_306_RESERVED = 306
- HTTP_307_TEMPORARY_REDIRECT = 307
- HTTP_400_BAD_REQUEST = 400
- HTTP_401_UNAUTHORIZED = 401
- HTTP_402_PAYMENT_REQUIRED = 402
- HTTP_403_FORBIDDEN = 403
- HTTP_404_NOT_FOUND = 404
- HTTP_405_METHOD_NOT_ALLOWED = 405
- HTTP_406_NOT_ACCEPTABLE = 406
- HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
- HTTP_408_REQUEST_TIMEOUT = 408
- HTTP_409_CONFLICT = 409
- HTTP_410_GONE = 410
- HTTP_411_LENGTH_REQUIRED = 411
- HTTP_412_PRECONDITION_FAILED = 412
- HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
- HTTP_414_REQUEST_URI_TOO_LONG = 414
- HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
- HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
- HTTP_417_EXPECTATION_FAILED = 417
- HTTP_500_INTERNAL_SERVER_ERROR = 500
- HTTP_501_NOT_IMPLEMENTED = 501
- HTTP_502_BAD_GATEWAY = 502
- HTTP_503_SERVICE_UNAVAILABLE = 503
- HTTP_504_GATEWAY_TIMEOUT = 504
- HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
-
- # Short format
- CONTINUE = 100
- SWITCHING_PROTOCOLS = 101
- OK = 200
- CREATED = 201
- ACCEPTED = 202
- NON_AUTHORITATIVE_INFORMATION = 203
- NO_CONTENT = 204
- RESET_CONTENT = 205
- PARTIAL_CONTENT = 206
- MULTIPLE_CHOICES = 300
- MOVED_PERMANENTLY = 301
- FOUND = 302
- SEE_OTHER = 303
- NOT_MODIFIED = 304
- USE_PROXY = 305
- RESERVED = 306
- TEMPORARY_REDIRECT = 307
- BAD_REQUEST = 400
- UNAUTHORIZED = 401
- PAYMENT_REQUIRED = 402
- FORBIDDEN = 403
- NOT_FOUND = 404
- METHOD_NOT_ALLOWED = 405
- NOT_ACCEPTABLE = 406
- PROXY_AUTHENTICATION_REQUIRED = 407
- REQUEST_TIMEOUT = 408
- CONFLICT = 409
- GONE = 410
- LENGTH_REQUIRED = 411
- PRECONDITION_FAILED = 412
- REQUEST_ENTITY_TOO_LARGE = 413
- REQUEST_URI_TOO_LONG = 414
- UNSUPPORTED_MEDIA_TYPE = 415
- REQUESTED_RANGE_NOT_SATISFIABLE = 416
- EXPECTATION_FAILED = 417
- INTERNAL_SERVER_ERROR = 500
- NOT_IMPLEMENTED = 501
- BAD_GATEWAY = 502
- SERVICE_UNAVAILABLE = 503
- GATEWAY_TIMEOUT = 504
- HTTP_VERSION_NOT_SUPPORTED = 505
-
-
-
-# This is simply stylistic, I think 'status.HTTP_200_OK' reads nicely.
-status = Status()
-
-
-class NoContent(object):
- """Used to indicate no body in http response.
- (We cannot just use None, as that is a valid, serializable response object.)"""
- pass
-
-
-class Response(object):
- def __init__(self, status, content=NoContent, headers={}):
- self.status = status
- self.has_content_body = not content is NoContent
- self.raw_content = content # content prior to filtering
- self.cleaned_content = content # content after filtering
- self.headers = headers
-
- @property
- def status_text(self):
- """Return reason text corrosponding to our HTTP response status code.
- Provided for convienience."""
- return STATUS_CODE_TEXT.get(self.status, '')
-
-
-class ResponseException(BaseException):
- def __init__(self, status, content=NoContent, headers={}):
- self.response = Response(status, content=content, headers=headers)
diff --git a/flywheel/templates/emitter.html b/flywheel/templates/emitter.html
deleted file mode 100644
index d21350cd..00000000
--- a/flywheel/templates/emitter.html
+++ /dev/null
@@ -1,108 +0,0 @@
-{% load urlize_quoted_links %}{% load add_query_param %}
-
-
-
-
- API - {{ resource.name }}
-
-
-
-
-
{{ resource.name }}
-
{{ resource.description|linebreaksbr }}
-
{{ response.status }} {{ response.status_text }}{% autoescape off %}
-{% for key, val in response.headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
-{% endfor %}
-{{ content|urlize_quoted_links }}{% endautoescape %}
-
- {% if 'GET' in resource.allowed_methods %}
-
-
GET
-
- {% for media_type in resource.emitted_media_types %}
- {% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %}
- - [{{ media_type }}]
- {% endwith %}
- {% endfor %}
-
-
-
- {% endif %}
-
- {% comment %} *** Only display the POST/PUT/DELETE forms if we have a bound form, and if method ***
- *** tunneling via POST forms is enabled. ***
- *** (We could display only the POST form if method tunneling is disabled, but I think ***
- *** the user experience would be confusing, so we simply turn all forms off. *** {% endcomment %}
-
- {% if resource.METHOD_PARAM and form %}
- {% if 'POST' in resource.allowed_methods %}
-
- {% endif %}
-
- {% if 'PUT' in resource.allowed_methods %}
-
- {% endif %}
-
- {% if 'DELETE' in resource.allowed_methods %}
-
-
-
- {% endif %}
- {% endif %}
-
-
-
\ No newline at end of file
diff --git a/flywheel/templates/emitter.txt b/flywheel/templates/emitter.txt
deleted file mode 100644
index 1cc7d1d7..00000000
--- a/flywheel/templates/emitter.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-{{ resource.name }}
-
-{{ resource.description }}
-
-{% autoescape off %}HTTP/1.0 {{ response.status }} {{ response.status_text }}
-{% for key, val in response.headers.items %}{{ key }}: {{ val }}
-{% endfor %}
-{{ content }}{% endautoescape %}
diff --git a/flywheel/templatetags/__init__.py b/flywheel/templatetags/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/flywheel/templatetags/add_query_param.py b/flywheel/templatetags/add_query_param.py
deleted file mode 100644
index 91c1a312..00000000
--- a/flywheel/templatetags/add_query_param.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from django.template import Library
-from urlparse import urlparse, urlunparse
-from urllib import quote
-register = Library()
-
-def add_query_param(url, param):
- (key, val) = param.split('=')
- param = '%s=%s' % (key, quote(val))
- (scheme, netloc, path, params, query, fragment) = urlparse(url)
- if query:
- query += "&" + param
- else:
- query = param
- return urlunparse((scheme, netloc, path, params, query, fragment))
-
-
-register.filter('add_query_param', add_query_param)
diff --git a/flywheel/templatetags/urlize_quoted_links.py b/flywheel/templatetags/urlize_quoted_links.py
deleted file mode 100644
index 60088cf6..00000000
--- a/flywheel/templatetags/urlize_quoted_links.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""Adds the custom filter 'urlize_quoted_links'
-
-This is identical to the built-in filter 'urlize' with the exception that
-single and double quotes are permitted as leading or trailing punctuation.
-"""
-
-# Almost all of this code is copied verbatim from django.utils.html
-# LEADING_PUNCTUATION and TRAILING_PUNCTUATION have been modified
-import re
-import string
-
-from django.utils.safestring import SafeData, mark_safe
-from django.utils.encoding import force_unicode
-from django.utils.http import urlquote
-from django.utils.html import escape
-from django import template
-
-# Configuration for urlize() function.
-LEADING_PUNCTUATION = ['(', '<', '<', '"', "'"]
-TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '>', '"', "'"]
-
-# List of possible strings used for bullets in bulleted lists.
-DOTS = ['·', '*', '\xe2\x80\xa2', '', '•', '•']
-
-unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
-word_split_re = re.compile(r'(\s+)')
-punctuation_re = re.compile('^(?P(?:%s)*)(?P.*?)(?P(?:%s)*)$' % \
- ('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]),
- '|'.join([re.escape(x) for x in TRAILING_PUNCTUATION])))
-simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
-link_target_attribute_re = re.compile(r'(]*?)target=[^\s>]+')
-html_gunk_re = re.compile(r'(?:
|<\/i>|<\/b>|<\/em>|<\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
-hard_coded_bullets_re = re.compile(r'((?:(?:%s).*?[a-zA-Z].*?
\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
-trailing_empty_content_re = re.compile(r'(?:(?: |\s|
)*?
\s*)+\Z')
-
-def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
- """
- Converts any URLs in text into clickable links.
-
- Works on http://, https://, www. links and links ending in .org, .net or
- .com. Links can have trailing punctuation (periods, commas, close-parens)
- and leading punctuation (opening parens) and it'll still do the right
- thing.
-
- If trim_url_limit is not None, the URLs in link text longer than this limit
- will truncated to trim_url_limit-3 characters and appended with an elipsis.
-
- If nofollow is True, the URLs in link text will get a rel="nofollow"
- attribute.
-
- If autoescape is True, the link text and URLs will get autoescaped.
- """
- trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
- safe_input = isinstance(text, SafeData)
- words = word_split_re.split(force_unicode(text))
- nofollow_attr = nofollow and ' rel="nofollow"' or ''
- for i, word in enumerate(words):
- match = None
- if '.' in word or '@' in word or ':' in word:
- match = punctuation_re.match(word)
- if match:
- lead, middle, trail = match.groups()
- # Make URL we want to point to.
- url = None
- if middle.startswith('http://') or middle.startswith('https://'):
- url = urlquote(middle, safe='/&=:;#?+*')
- elif middle.startswith('www.') or ('@' not in middle and \
- middle and middle[0] in string.ascii_letters + string.digits and \
- (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
- url = urlquote('http://%s' % middle, safe='/&=:;#?+*')
- elif '@' in middle and not ':' in middle and simple_email_re.match(middle):
- url = 'mailto:%s' % middle
- nofollow_attr = ''
- # Make link.
- if url:
- trimmed = trim_url(middle)
- if autoescape and not safe_input:
- lead, trail = escape(lead), escape(trail)
- url, trimmed = escape(url), escape(trimmed)
- middle = '%s' % (url, nofollow_attr, trimmed)
- words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
- else:
- if safe_input:
- words[i] = mark_safe(word)
- elif autoescape:
- words[i] = escape(word)
- elif safe_input:
- words[i] = mark_safe(word)
- elif autoescape:
- words[i] = escape(word)
- return u''.join(words)
-
-
-#urlize_quoted_links.needs_autoescape = True
-urlize_quoted_links.is_safe = True
-
-# Register urlize_quoted_links as a custom filter
-# http://docs.djangoproject.com/en/dev/howto/custom-template-tags/
-register = template.Library()
-register.filter(urlize_quoted_links)
\ No newline at end of file
diff --git a/flywheel/utils.py b/flywheel/utils.py
deleted file mode 100644
index f9bbc0fe..00000000
--- a/flywheel/utils.py
+++ /dev/null
@@ -1,179 +0,0 @@
-import re
-import xml.etree.ElementTree as ET
-from django.utils.encoding import smart_unicode
-from django.utils.xmlutils import SimplerXMLGenerator
-from django.core.urlresolvers import resolve
-try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
-
-
-def url_resolves(url):
- """Return True if the given URL is mapped to a view in the urlconf, False otherwise."""
- try:
- resolve(url)
- except:
- return False
- return True
-
-# From piston
-def coerce_put_post(request):
- """
- Django doesn't particularly understand REST.
- In case we send data over PUT, Django won't
- actually look at the data and load it. We need
- to twist its arm here.
-
- The try/except abominiation here is due to a bug
- in mod_python. This should fix it.
- """
- if request.method != 'PUT':
- return
-
- # Bug fix: if _load_post_and_files has already been called, for
- # example by middleware accessing request.POST, the below code to
- # pretend the request is a POST instead of a PUT will be too late
- # to make a difference. Also calling _load_post_and_files will result
- # in the following exception:
- # AttributeError: You cannot set the upload handlers after the upload has been processed.
- # The fix is to check for the presence of the _post field which is set
- # the first time _load_post_and_files is called (both by wsgi.py and
- # modpython.py). If it's set, the request has to be 'reset' to redo
- # the query value parsing in POST mode.
- if hasattr(request, '_post'):
- del request._post
- del request._files
-
- try:
- request.method = "POST"
- request._load_post_and_files()
- request.method = "PUT"
- except AttributeError:
- request.META['REQUEST_METHOD'] = 'POST'
- request._load_post_and_files()
- request.META['REQUEST_METHOD'] = 'PUT'
-
- request.PUT = request.POST
-
-# From http://www.koders.com/python/fidB6E125C586A6F49EAC38992CF3AFDAAE35651975.aspx?s=mdef:xml
-#class object_dict(dict):
-# """object view of dict, you can
-# >>> a = object_dict()
-# >>> a.fish = 'fish'
-# >>> a['fish']
-# 'fish'
-# >>> a['water'] = 'water'
-# >>> a.water
-# 'water'
-# >>> a.test = {'value': 1}
-# >>> a.test2 = object_dict({'name': 'test2', 'value': 2})
-# >>> a.test, a.test2.name, a.test2.value
-# (1, 'test2', 2)
-# """
-# def __init__(self, initd=None):
-# if initd is None:
-# initd = {}
-# dict.__init__(self, initd)
-#
-# def __getattr__(self, item):
-# d = self.__getitem__(item)
-# # if value is the only key in object, you can omit it
-# if isinstance(d, dict) and 'value' in d and len(d) == 1:
-# return d['value']
-# else:
-# return d
-#
-# def __setattr__(self, item, value):
-# self.__setitem__(item, value)
-
-
-# From xml2dict
-class XML2Dict(object):
-
- def __init__(self):
- pass
-
- def _parse_node(self, node):
- node_tree = {}
- # Save attrs and text, hope there will not be a child with same name
- if node.text:
- node_tree = node.text
- for (k,v) in node.attrib.items():
- k,v = self._namespace_split(k, v)
- node_tree[k] = v
- #Save childrens
- for child in node.getchildren():
- tag, tree = self._namespace_split(child.tag, self._parse_node(child))
- if tag not in node_tree: # the first time, so store it in dict
- node_tree[tag] = tree
- continue
- old = node_tree[tag]
- if not isinstance(old, list):
- node_tree.pop(tag)
- node_tree[tag] = [old] # multi times, so change old dict to a list
- node_tree[tag].append(tree) # add the new one
-
- return node_tree
-
-
- def _namespace_split(self, tag, value):
- """
- Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients'
- ns = http://cs.sfsu.edu/csc867/myscheduler
- name = patients
- """
- result = re.compile("\{(.*)\}(.*)").search(tag)
- if result:
- value.namespace, tag = result.groups()
- return (tag, value)
-
- def parse(self, file):
- """parse a xml file to a dict"""
- f = open(file, 'r')
- return self.fromstring(f.read())
-
- def fromstring(self, s):
- """parse a string"""
- t = ET.fromstring(s)
- unused_root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t))
- return root_tree
-
-
-def xml2dict(input):
- return XML2Dict().fromstring(input)
-
-
-# Piston:
-class XMLEmitter():
- def _to_xml(self, xml, data):
- if isinstance(data, (list, tuple)):
- for item in data:
- xml.startElement("list-item", {})
- self._to_xml(xml, item)
- xml.endElement("list-item")
-
- elif isinstance(data, dict):
- for key, value in data.iteritems():
- xml.startElement(key, {})
- self._to_xml(xml, value)
- xml.endElement(key)
-
- else:
- xml.characters(smart_unicode(data))
-
- def dict2xml(self, data):
- stream = StringIO.StringIO()
-
- xml = SimplerXMLGenerator(stream, "utf-8")
- xml.startDocument()
- xml.startElement("root", {})
-
- self._to_xml(xml, data)
-
- xml.endElement("root")
- xml.endDocument()
- return stream.getvalue()
-
-def dict2xml(input):
- return XMLEmitter().dict2xml(input)
diff --git a/requirements.txt b/requirements.txt
index 8144d828..84f0c4eb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,5 @@
+# Django and pip are required if installing into a virtualenv environment...
+
Django==1.2.4
distribute==0.6.14
wsgiref==0.1.2
--
cgit v1.2.3