mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 11:04:02 +03:00
Sphinx docs, examples, lots of refactoring
This commit is contained in:
parent
4100242fa2
commit
e95198a1c0
|
@ -1,20 +0,0 @@
|
|||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "auth.user",
|
||||
"fields": {
|
||||
"username": "admin",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"is_active": true,
|
||||
"is_superuser": true,
|
||||
"is_staff": true,
|
||||
"last_login": "2010-01-01 00:00:00",
|
||||
"groups": [],
|
||||
"user_permissions": [],
|
||||
"password": "sha1$6cbce$e4e808893d586a3301ac3c14da6c84855999f1d8",
|
||||
"email": "test@example.com",
|
||||
"date_joined": "2010-01-01 00:00:00"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -1,11 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
from django.core.management import execute_manager
|
||||
try:
|
||||
import settings # Assumed to be in the same directory.
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
execute_manager(settings)
|
|
@ -1,59 +0,0 @@
|
|||
from django.template import RequestContext, loader
|
||||
import json
|
||||
from utils import dict2xml
|
||||
|
||||
class BaseEmitter(object):
|
||||
uses_forms = False
|
||||
|
||||
def __init__(self, resource):
|
||||
self.resource = resource
|
||||
|
||||
def emit(self, output):
|
||||
return output
|
||||
|
||||
class TemplatedEmitter(BaseEmitter):
|
||||
template = None
|
||||
|
||||
def emit(self, output):
|
||||
if output is None:
|
||||
content = ''
|
||||
else:
|
||||
content = json.dumps(output, indent=4, sort_keys=True)
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = RequestContext(self.resource.request, {
|
||||
'content': content,
|
||||
'resource': self.resource,
|
||||
})
|
||||
|
||||
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.resp_status == 204:
|
||||
self.resource.resp_status = 200
|
||||
|
||||
return ret
|
||||
|
||||
class JSONEmitter(BaseEmitter):
|
||||
def emit(self, output):
|
||||
if output is None:
|
||||
# Treat None as no message body, rather than serializing
|
||||
return ''
|
||||
return json.dumps(output)
|
||||
|
||||
class XMLEmitter(BaseEmitter):
|
||||
def emit(self, output):
|
||||
if output is None:
|
||||
# Treat None as no message body, rather than serializing
|
||||
return ''
|
||||
return dict2xml(output)
|
||||
|
||||
class HTMLEmitter(TemplatedEmitter):
|
||||
template = 'emitter.html'
|
||||
uses_forms = True
|
||||
|
||||
class TextEmitter(TemplatedEmitter):
|
||||
template = 'emitter.txt'
|
||||
|
||||
|
|
@ -1,394 +0,0 @@
|
|||
"""TODO: docs
|
||||
"""
|
||||
from django.forms import ModelForm
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models import Model
|
||||
|
||||
from rest.resource import Resource
|
||||
|
||||
import decimal
|
||||
import inspect
|
||||
import re
|
||||
|
||||
|
||||
class ModelResource(Resource):
|
||||
"""A specialized type of Resource, for RESTful 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_bound_form(self, data=None, is_response=False):
|
||||
"""Return a form that may be used in validation and/or rendering an html emitter"""
|
||||
if self.form:
|
||||
return super(self.__class__, self).get_bound_form(data, is_response=is_response)
|
||||
|
||||
elif self.model:
|
||||
class NewModelForm(ModelForm):
|
||||
class Meta:
|
||||
model = self.model
|
||||
fields = self.form_fields if self.form_fields else None
|
||||
|
||||
if data and not is_response:
|
||||
return NewModelForm(data)
|
||||
elif data and is_response:
|
||||
return NewModelForm(instance=data)
|
||||
else:
|
||||
return NewModelForm()
|
||||
|
||||
else:
|
||||
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 create(self, data, headers={}, *args, **kwargs):
|
||||
# TODO: test creation on a non-existing resource url
|
||||
all_kw_args = dict(data.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 (201, instance, headers)
|
||||
|
||||
def read(self, headers={}, *args, **kwargs):
|
||||
try:
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
return (404, None, {})
|
||||
|
||||
return (200, instance, {})
|
||||
|
||||
def update(self, data, headers={}, *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 data.items():
|
||||
setattr(instance, key, val)
|
||||
except self.model.DoesNotExist:
|
||||
instance = self.model(**data)
|
||||
instance.save()
|
||||
|
||||
instance.save()
|
||||
return (200, instance, {})
|
||||
|
||||
def delete(self, headers={}, *args, **kwargs):
|
||||
try:
|
||||
instance = self.model.objects.get(**kwargs)
|
||||
except self.model.DoesNotExist:
|
||||
return (404, None, {})
|
||||
|
||||
instance.delete()
|
||||
return (204, None, {})
|
||||
|
||||
|
||||
|
||||
class QueryModelResource(ModelResource):
|
||||
allowed_methods = ('read',)
|
||||
queryset = None
|
||||
|
||||
def get_bound_form(self, data=None, is_response=False):
|
||||
return None
|
||||
|
||||
def read(self, headers={}, *args, **kwargs):
|
||||
if self.queryset:
|
||||
return (200, self.queryset, {})
|
||||
queryset = self.model.objects.all()
|
||||
return (200, queryset, {})
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import json
|
||||
from rest.status import ResourceException, Status
|
||||
|
||||
class BaseParser(object):
|
||||
def __init__(self, resource):
|
||||
self.resource = resource
|
||||
|
||||
def parse(self, input):
|
||||
return {}
|
||||
|
||||
|
||||
class JSONParser(BaseParser):
|
||||
def parse(self, input):
|
||||
try:
|
||||
return json.loads(input)
|
||||
except ValueError, exc:
|
||||
raise ResourceException(Status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
|
||||
|
||||
class XMLParser(BaseParser):
|
||||
pass
|
||||
|
||||
class FormParser(BaseParser):
|
||||
"""The default parser for form data.
|
||||
Return a dict containing a single value for each non-reserved parameter
|
||||
"""
|
||||
def __init__(self, resource):
|
||||
|
||||
if resource.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(resource.request, '_post'):
|
||||
del request._post
|
||||
del request._files
|
||||
|
||||
try:
|
||||
resource.request.method = "POST"
|
||||
resource.request._load_post_and_files()
|
||||
resource.request.method = "PUT"
|
||||
except AttributeError:
|
||||
resource.request.META['REQUEST_METHOD'] = 'POST'
|
||||
resource.request._load_post_and_files()
|
||||
resource.request.META['REQUEST_METHOD'] = 'PUT'
|
||||
|
||||
#
|
||||
self.data = {}
|
||||
for (key, val) in resource.request.POST.items():
|
||||
if key not in resource.RESERVED_PARAMS:
|
||||
self.data[key] = val
|
||||
|
||||
def parse(self, input):
|
||||
return self.data
|
||||
|
|
@ -1,382 +0,0 @@
|
|||
from django.contrib.sites.models import Site
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||
from django.http import HttpResponse
|
||||
from rest import emitters, parsers
|
||||
from rest.status import Status, ResourceException
|
||||
from decimal import Decimal
|
||||
import re
|
||||
|
||||
# TODO: Authentication
|
||||
# TODO: Display user login in top panel: http://stackoverflow.com/questions/806835/django-redirect-to-previous-page-after-login
|
||||
# TODO: Return basic object, not tuple of status code, content, headers
|
||||
# TODO: Take request, not headers
|
||||
# TODO: Standard exception classes
|
||||
# TODO: Figure how out references and named urls need to work nicely
|
||||
# TODO: POST on existing 404 URL, PUT on existing 404 URL
|
||||
#
|
||||
# NEXT: Generic content form
|
||||
# NEXT: Remove self.blah munging (Add a ResponseContext object?)
|
||||
# NEXT: Caching cleverness
|
||||
# NEXT: Test non-existent fields on ModelResources
|
||||
#
|
||||
# FUTURE: Erroring on read-only fields
|
||||
|
||||
# Documentation, Release
|
||||
|
||||
|
||||
|
||||
class Resource(object):
|
||||
# List of RESTful operations which may be performed on this resource.
|
||||
allowed_operations = ('read',)
|
||||
anon_allowed_operations = ()
|
||||
|
||||
# Optional form for input validation and presentation of HTML formatted responses.
|
||||
form = None
|
||||
|
||||
# List of content-types the resource can respond with, ordered by preference
|
||||
emitters = ( ('application/json', emitters.JSONEmitter),
|
||||
('text/html', emitters.HTMLEmitter),
|
||||
('application/xhtml+xml', emitters.HTMLEmitter),
|
||||
('text/plain', emitters.TextEmitter),
|
||||
('application/xml', emitters.XMLEmitter), )
|
||||
|
||||
# List of content-types the resource can read from
|
||||
parsers = { 'application/json': parsers.JSONParser,
|
||||
'application/xml': parsers.XMLParser,
|
||||
'application/x-www-form-urlencoded': parsers.FormParser,
|
||||
'multipart/form-data': parsers.FormParser }
|
||||
|
||||
# Map standard HTTP methods to RESTful operations
|
||||
CALLMAP = { 'GET': 'read', 'POST': 'create',
|
||||
'PUT': 'update', 'DELETE': 'delete' }
|
||||
|
||||
REVERSE_CALLMAP = dict([(val, key) for (key, val) in CALLMAP.items()])
|
||||
|
||||
# Some reserved parameters to allow us to use standard HTML forms with our resource
|
||||
METHOD_PARAM = '_method' # Allow POST overloading
|
||||
ACCEPT_PARAM = '_accept' # Allow override of Accept header in GET requests
|
||||
CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header (allows sending arbitrary content with standard forms)
|
||||
CONTENT_PARAM = '_content' # Allow override of body content (allows sending arbitrary content with standard forms)
|
||||
CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token
|
||||
|
||||
RESERVED_PARAMS = set((METHOD_PARAM, ACCEPT_PARAM, CONTENTTYPE_PARAM, CONTENT_PARAM, CSRF_PARAM))
|
||||
|
||||
|
||||
def __new__(cls, request, *args, **kwargs):
|
||||
"""Make the class callable so it can be used as a Django view."""
|
||||
self = object.__new__(cls)
|
||||
self.__init__()
|
||||
return self._handle_request(request, *args, **kwargs)
|
||||
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
def available_content_types(self):
|
||||
"""Return a list of strings of all the content-types that this resource can emit."""
|
||||
return [item[0] for item in self.emitters]
|
||||
|
||||
|
||||
def resp_status_text(self):
|
||||
"""Return reason text corrosponding to our HTTP response status code.
|
||||
Provided for convienience."""
|
||||
return STATUS_CODE_TEXT.get(self.resp_status, '')
|
||||
|
||||
|
||||
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, **kwargs))
|
||||
|
||||
|
||||
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 read(self, headers={}, *args, **kwargs):
|
||||
"""RESTful read on the resource, which must be subclassed to be implemented. Should be a safe operation."""
|
||||
self.not_implemented('read')
|
||||
|
||||
|
||||
def create(self, data=None, headers={}, *args, **kwargs):
|
||||
"""RESTful create on the resource, which must be subclassed to be implemented."""
|
||||
self.not_implemented('create')
|
||||
|
||||
|
||||
def update(self, data=None, headers={}, *args, **kwargs):
|
||||
"""RESTful update on the resource, which must be subclassed to be implemented. Should be an idempotent operation."""
|
||||
self.not_implemented('update')
|
||||
|
||||
|
||||
def delete(self, headers={}, *args, **kwargs):
|
||||
"""RESTful delete on the resource, which must be subclassed to be implemented. Should be an idempotent operation."""
|
||||
self.not_implemented('delete')
|
||||
|
||||
|
||||
def not_implemented(self, operation):
|
||||
"""Return an HTTP 500 server error if an operation is called which has been allowed by
|
||||
allowed_operations, but which has not been implemented."""
|
||||
raise ResourceException(Status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
|
||||
|
||||
|
||||
def determine_method(self, request):
|
||||
"""Determine the HTTP method that this request should be treated as.
|
||||
Allow for PUT and DELETE tunneling via the _method parameter."""
|
||||
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):
|
||||
"""TODO"""
|
||||
# user = ...
|
||||
# if DEBUG and request is from localhost
|
||||
# if anon_user and not anon_allowed_operations raise PermissionDenied
|
||||
# return
|
||||
|
||||
|
||||
def check_method_allowed(self, method):
|
||||
"""Ensure the request method is acceptable for this resource."""
|
||||
if not method in self.CALLMAP.keys():
|
||||
raise ResourceException(Status.HTTP_501_NOT_IMPLEMENTED,
|
||||
{'detail': 'Unknown or unsupported method \'%s\'' % method})
|
||||
|
||||
if not self.CALLMAP[method] in self.allowed_operations:
|
||||
raise ResourceException(Status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
|
||||
|
||||
|
||||
def get_bound_form(self, data=None, is_response=False):
|
||||
"""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. is_response indicates if data should be
|
||||
treated as the input data (bind to client input) or the response data (bind to an existing object)."""
|
||||
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())
|
||||
|
||||
# Add any non-field errors
|
||||
if form_instance.non_field_errors():
|
||||
details['errors'] = self.form.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 ResourceException(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')
|
||||
split = content_type.split(';', 1)
|
||||
if len(split) > 1:
|
||||
content_type = split[0]
|
||||
content_type = content_type.strip()
|
||||
|
||||
try:
|
||||
return self.parsers[content_type]
|
||||
except KeyError:
|
||||
raise ResourceException(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"""
|
||||
default = self.emitters[0]
|
||||
|
||||
if self.ACCEPT_PARAM and request.GET.get(self.ACCEPT_PARAM, None):
|
||||
# Use _accept parameter override
|
||||
accept_list = [(request.GET.get(self.ACCEPT_PARAM),)]
|
||||
elif request.META.has_key('HTTP_ACCEPT'):
|
||||
# Use standard HTTP Accept negotiation
|
||||
accept_list = [item.split(';') for item in request.META["HTTP_ACCEPT"].split(',')]
|
||||
else:
|
||||
# No accept header specified
|
||||
return default
|
||||
|
||||
# Parse the accept header into a dict of {Priority: List of Mimetypes}
|
||||
accept_dict = {}
|
||||
for item in accept_list:
|
||||
mimetype = item[0].strip()
|
||||
qvalue = Decimal('1.0')
|
||||
|
||||
if len(item) > 1:
|
||||
# Parse items that have a qvalue eg text/html;q=0.9
|
||||
try:
|
||||
(q, num) = item[1].split('=')
|
||||
if q == 'q':
|
||||
qvalue = Decimal(num)
|
||||
except:
|
||||
# Skip malformed entries
|
||||
continue
|
||||
|
||||
if accept_dict.has_key(qvalue):
|
||||
accept_dict[qvalue].append(mimetype)
|
||||
else:
|
||||
accept_dict[qvalue] = [mimetype]
|
||||
|
||||
# Go through all accepted mimetypes in priority order and return our first match
|
||||
qvalues = accept_dict.keys()
|
||||
qvalues.sort(reverse=True)
|
||||
|
||||
for qvalue in qvalues:
|
||||
for (mimetype, emitter) in self.emitters:
|
||||
for accept_mimetype in accept_dict[qvalue]:
|
||||
if ((accept_mimetype == '*/*') or
|
||||
(accept_mimetype.endswith('/*') and mimetype.startswith(accept_mimetype[:-1])) or
|
||||
(accept_mimetype == mimetype)):
|
||||
return (mimetype, emitter)
|
||||
|
||||
raise ResourceException(Status.HTTP_406_NOT_ACCEPTABLE,
|
||||
{'detail': 'Could not statisfy the client\'s accepted content type',
|
||||
'accepted_types': [item[0] for item in self.emitters]})
|
||||
|
||||
|
||||
def _handle_request(self, request, *args, **kwargs):
|
||||
"""
|
||||
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)
|
||||
|
||||
# We make these attributes to allow for a certain amount of munging,
|
||||
# eg The HTML emitter needs to render this information
|
||||
self.request = request
|
||||
self.form_instance = None
|
||||
self.resp_status = None
|
||||
self.resp_headers = {}
|
||||
|
||||
try:
|
||||
# Before we attempt anything else determine what format to emit our response data with.
|
||||
mimetype, emitter = self.determine_emitter(request)
|
||||
|
||||
# Ensure the requested operation is permitted on this resource
|
||||
self.check_method_allowed(method)
|
||||
|
||||
# Get the appropriate create/read/update/delete function
|
||||
func = getattr(self, self.CALLMAP.get(method, ''))
|
||||
|
||||
# Either generate the response data, deserializing and validating any request data
|
||||
if method in ('PUT', 'POST'):
|
||||
parser = self.determine_parser(request)
|
||||
data = parser(self).parse(request.raw_post_data)
|
||||
self.form_instance = self.get_bound_form(data)
|
||||
data = self.cleanup_request(data, self.form_instance)
|
||||
(self.resp_status, ret, self.resp_headers) = func(data, request.META, *args, **kwargs)
|
||||
|
||||
else:
|
||||
(self.resp_status, ret, self.resp_headers) = func(request.META, *args, **kwargs)
|
||||
if emitter.uses_forms:
|
||||
self.form_instance = self.get_bound_form(ret, is_response=True)
|
||||
|
||||
|
||||
except ResourceException, exc:
|
||||
# On exceptions we still serialize the response appropriately
|
||||
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
|
||||
|
||||
# Fall back to the default emitter if we failed to perform content negotiation
|
||||
if emitter is None:
|
||||
mimetype, emitter = self.emitters[0]
|
||||
|
||||
# Provide an empty bound form if we do not have an existing form and if one is required
|
||||
if self.form_instance is None and emitter.uses_forms:
|
||||
self.form_instance = self.get_bound_form()
|
||||
|
||||
|
||||
# Always add the allow header
|
||||
self.resp_headers['Allow'] = ', '.join([self.REVERSE_CALLMAP[operation] for operation in self.allowed_operations])
|
||||
|
||||
# Serialize the response content
|
||||
ret = self.cleanup_response(ret)
|
||||
content = emitter(self).emit(ret)
|
||||
|
||||
# Build the HTTP Response
|
||||
resp = HttpResponse(content, mimetype=mimetype, status=self.resp_status)
|
||||
for (key, val) in self.resp_headers.items():
|
||||
resp[key] = val
|
||||
|
||||
return resp
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
|
||||
class Status(object):
|
||||
"""Descriptive HTTP status codes, for code readability."""
|
||||
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_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_100_CONTINUE = 100
|
||||
HTTP_101_SWITCHING_PROTOCOLS = 101
|
||||
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_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
|
||||
|
||||
class ResourceException(Exception):
|
||||
def __init__(self, status, content='', headers={}):
|
||||
self.status = status
|
||||
self.content = content
|
||||
self.headers = headers
|
|
@ -1,93 +0,0 @@
|
|||
{% load urlize_quoted_links %}{% load add_query_param %}<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<style>
|
||||
pre {border: 1px solid black; padding: 1em; background: #ffd}
|
||||
div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
|
||||
ul.accepttypes {float: right; list-style-type: none; margin: 0; padding: 0}
|
||||
ul.accepttypes li {display: inline;}
|
||||
form div {margin: 0.5em 0}
|
||||
form div * {vertical-align: top}
|
||||
form ul.errorlist {display: inline; margin: 0; padding: 0}
|
||||
form ul.errorlist li {display: inline; color: red;}
|
||||
.clearing {display: block; margin: 0; padding: 0; clear: both;}
|
||||
</style>
|
||||
<title>API - {{ resource.name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ resource.name }}</h1>
|
||||
<p>{{ resource.description|linebreaksbr }}</p>
|
||||
<pre><b>{{ resource.resp_status }} {{ resource.resp_status_text }}</b>{% autoescape off %}
|
||||
{% for key, val in resource.resp_headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
||||
{% endfor %}
|
||||
{{ content|urlize_quoted_links }} </pre>{% endautoescape %}
|
||||
|
||||
{% if 'read' in resource.allowed_operations %}
|
||||
<div class='action'>
|
||||
<a href='{{ resource.request.path }}'>Read</a>
|
||||
<ul class="accepttypes">
|
||||
{% for content_type in resource.available_content_types %}
|
||||
{% with resource.ACCEPT_PARAM|add:"="|add:content_type as param %}
|
||||
<li>[<a href='{{ resource.request.path|add_query_param:param }}'>{{ content_type }}</a>]</li>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="clearing"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if 'create' in resource.allowed_operations %}
|
||||
<div class='action'>
|
||||
<form action="{{ resource.request.path }}" method="post">
|
||||
{% csrf_token %}
|
||||
{% with resource.form_instance as form %}
|
||||
{% for field in form %}
|
||||
<div>
|
||||
{{ field.label_tag }}:
|
||||
{{ field }}
|
||||
{{ field.help_text }}
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<div class="clearing"></div>
|
||||
<input type="submit" value="Create" />
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if 'update' in resource.allowed_operations %}
|
||||
<div class='action'>
|
||||
<form action="{{ resource.request.path }}" method="post">
|
||||
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
|
||||
{% csrf_token %}
|
||||
{% with resource.form_instance as form %}
|
||||
{% for field in form %}
|
||||
<div>
|
||||
{{ field.label_tag }}:
|
||||
{{ field }}
|
||||
{{ field.help_text }}
|
||||
{{ field.errors }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<div class="clearing"></div>
|
||||
<input type="submit" value="Update" />
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if 'delete' in resource.allowed_operations %}
|
||||
<div class='action'>
|
||||
<form action="{{ resource.request.path }}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
|
||||
<input type="submit" value="Delete" />
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,8 +0,0 @@
|
|||
{{ resource.name }}
|
||||
|
||||
{{ resource.description }}
|
||||
|
||||
{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}
|
||||
{% for key, val in resource.resp_headers.items %}{{ key }}: {{ val }}
|
||||
{% endfor %}
|
||||
{{ content }}{% endautoescape %}
|
|
@ -1,3 +0,0 @@
|
|||
HTML:
|
||||
|
||||
{{ content }}
|
Binary file not shown.
|
@ -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)
|
|
@ -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<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%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'(<a [^>]*?)target=[^\s>]+')
|
||||
html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
|
||||
hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
|
||||
trailing_empty_content_re = re.compile(r'(?:<p>(?: |\s|<br \/>)*?</p>\s*)+\Z')
|
||||
|
||||
def urlize_quoted_links(text, trim_url_limit=None, nofollow=False, 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 = '<a href="%s"%s>%s</a>' % (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)
|
Binary file not shown.
|
@ -1,170 +0,0 @@
|
|||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from django.utils.encoding import smart_unicode
|
||||
from django.utils.xmlutils import SimplerXMLGenerator
|
||||
try:
|
||||
import cStringIO as StringIO
|
||||
except ImportError:
|
||||
import StringIO
|
||||
|
||||
# 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:
|
||||
print tag
|
||||
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)
|
|
@ -1,98 +0,0 @@
|
|||
# Django settings for src project.
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@domain.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': 'sqlite3.db', # Or path to database file if using sqlite3.
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
# On Unix systems, a value of None will cause Django to use the same
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'Europe/London'
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'en-uk'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = True
|
||||
|
||||
# If you set this to False, Django will not format dates, numbers and
|
||||
# calendars according to the current locale
|
||||
USE_L10N = True
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash if there is a path component (optional in other cases).
|
||||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://foo.com/media/", "/media/".
|
||||
ADMIN_MEDIA_PREFIX = '/media/'
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu'
|
||||
|
||||
# List of callables that know how to import templates from various sources.
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
# 'django.template.loaders.eggs.Loader',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'urls'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
# Uncomment the next line to enable the admin:
|
||||
'django.contrib.admin',
|
||||
# Uncomment the next line to enable admin documentation:
|
||||
# 'django.contrib.admindocs',
|
||||
'testapp',
|
||||
'rest',
|
||||
)
|
|
@ -1,7 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
class ExampleForm(forms.Form):
|
||||
title = forms.CharField(max_length=100)
|
||||
message = forms.CharField()
|
||||
sender = forms.EmailField()
|
||||
valid = forms.BooleanField(required=False)
|
|
@ -1,94 +0,0 @@
|
|||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
import uuid
|
||||
|
||||
def uuid_str():
|
||||
return str(uuid.uuid1())
|
||||
|
||||
#class ExampleModel(models.Model):
|
||||
# num = models.IntegerField(default=2, choices=((1,'one'), (2, 'two')))
|
||||
# hidden_num = models.IntegerField(verbose_name='Something', help_text='HELP')
|
||||
# text = models.TextField(blank=False)
|
||||
# another = models.CharField(max_length=10)
|
||||
|
||||
|
||||
#class ExampleContainer(models.Model):
|
||||
# """Container. Has a key, a name, and some internal data, and contains a set of items."""
|
||||
# key = models.CharField(primary_key=True, default=uuid_str, max_length=36, editable=False)
|
||||
# name = models.CharField(max_length=256)
|
||||
# internal = models.IntegerField(default=0)
|
||||
|
||||
# @models.permalink
|
||||
# def get_absolute_url(self):
|
||||
# return ('testapp.views.ContainerInstance', [self.key])
|
||||
|
||||
|
||||
#class ExampleItem(models.Model):
|
||||
# """Item. Belongs to a container and has an index number and a note.
|
||||
# Items are uniquely identified by their container and index number."""
|
||||
# container = models.ForeignKey(ExampleContainer, related_name='items')
|
||||
# index = models.IntegerField()
|
||||
# note = models.CharField(max_length=1024)
|
||||
# unique_together = (container, index)
|
||||
|
||||
|
||||
RATING_CHOICES = ((0, 'Awful'),
|
||||
(1, 'Poor'),
|
||||
(2, 'OK'),
|
||||
(3, 'Good'),
|
||||
(4, 'Excellent'))
|
||||
|
||||
class BlogPost(models.Model):
|
||||
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
|
||||
title = models.CharField(max_length=128)
|
||||
content = models.TextField()
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
slug = models.SlugField(editable=False, default='')
|
||||
|
||||
class Meta:
|
||||
ordering = ('created',)
|
||||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('testapp.views.BlogPostInstance', (self.key,))
|
||||
|
||||
@property
|
||||
@models.permalink
|
||||
def comments_url(self):
|
||||
"""Link to a resource which lists all comments for this blog post."""
|
||||
return ('testapp.views.CommentList', (self.key,))
|
||||
|
||||
@property
|
||||
@models.permalink
|
||||
def comment_url(self):
|
||||
"""Link to a resource which can create a comment for this blog post."""
|
||||
return ('testapp.views.CommentCreator', (self.key,))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.slug = slugify(self.title)
|
||||
super(self.__class__, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
|
||||
username = models.CharField(max_length=128)
|
||||
comment = models.TextField()
|
||||
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='How did you rate this post?')
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('created',)
|
||||
|
||||
@models.permalink
|
||||
def get_absolute_url(self):
|
||||
return ('testapp.views.CommentInstance', (self.blogpost.key, self.id))
|
||||
|
||||
@property
|
||||
@models.permalink
|
||||
def blogpost_url(self):
|
||||
"""Link to the blog post resource which this comment corresponds to."""
|
||||
return ('testapp.views.BlogPostInstance', (self.blogpost.key,))
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
"""Test a range of REST API usage of the example application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from testapp import views
|
||||
#import json
|
||||
#from rest.utils import xml2dict, dict2xml
|
||||
|
||||
|
||||
class AcceptHeaderTests(TestCase):
|
||||
"""Test correct behaviour of the Accept header as specified by RFC 2616:
|
||||
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1"""
|
||||
|
||||
def assert_accept_mimetype(self, mimetype, expect=None):
|
||||
"""Assert that a request with given mimetype in the accept header,
|
||||
gives a response with the appropriate content-type."""
|
||||
if expect is None:
|
||||
expect = mimetype
|
||||
|
||||
resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT=mimetype)
|
||||
|
||||
self.assertEquals(resp['content-type'], expect)
|
||||
|
||||
|
||||
def test_accept_json(self):
|
||||
"""Ensure server responds with Content-Type of JSON when requested."""
|
||||
self.assert_accept_mimetype('application/json')
|
||||
|
||||
def test_accept_xml(self):
|
||||
"""Ensure server responds with Content-Type of XML when requested."""
|
||||
self.assert_accept_mimetype('application/xml')
|
||||
|
||||
def test_accept_json_when_prefered_to_xml(self):
|
||||
"""Ensure server responds with Content-Type of JSON when it is the client's prefered choice."""
|
||||
self.assert_accept_mimetype('application/json,q=0.9;application/xml,q=0.1', expect='application/json')
|
||||
|
||||
def test_accept_xml_when_prefered_to_json(self):
|
||||
"""Ensure server responds with Content-Type of XML when it is the client's prefered choice."""
|
||||
self.assert_accept_mimetype('application/xml,q=0.9;application/json,q=0.1', expect='application/xml')
|
||||
|
||||
def test_default_json_prefered(self):
|
||||
"""Ensure server responds with JSON in preference to XML."""
|
||||
self.assert_accept_mimetype('application/json;application/xml', expect='application/json')
|
||||
|
||||
def test_accept_generic_subtype_format(self):
|
||||
"""Ensure server responds with an appropriate type, when the subtype is left generic."""
|
||||
self.assert_accept_mimetype('text/*', expect='text/html')
|
||||
|
||||
def test_accept_generic_type_format(self):
|
||||
"""Ensure server responds with an appropriate type, when the type and subtype are left generic."""
|
||||
self.assert_accept_mimetype('*/*', expect='application/json')
|
||||
|
||||
def test_invalid_accept_header_returns_406(self):
|
||||
"""Ensure server returns a 406 (not acceptable) response if we set the Accept header to junk."""
|
||||
resp = self.client.get(reverse(views.RootResource), HTTP_ACCEPT='invalid/invalid')
|
||||
self.assertNotEquals(resp['content-type'], 'invalid/invalid')
|
||||
self.assertEquals(resp.status_code, 406)
|
||||
|
||||
def test_prefer_specific_over_generic(self): # This test is broken right now
|
||||
"""More specific accept types have precedence over less specific types."""
|
||||
self.assert_accept_mimetype('application/xml;*/*', expect='application/xml')
|
||||
|
||||
|
||||
class AllowedMethodsTests(TestCase):
|
||||
"""Basic tests to check that only allowed operations may be performed on a Resource"""
|
||||
|
||||
def test_reading_a_read_only_resource_is_allowed(self):
|
||||
"""GET requests on a read only resource should default to a 200 (OK) response"""
|
||||
resp = self.client.get(reverse(views.RootResource))
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
def test_writing_to_read_only_resource_is_not_allowed(self):
|
||||
"""PUT requests on a read only resource should default to a 405 (method not allowed) response"""
|
||||
resp = self.client.put(reverse(views.RootResource), {})
|
||||
self.assertEquals(resp.status_code, 405)
|
||||
#
|
||||
# def test_reading_write_only_not_allowed(self):
|
||||
# resp = self.client.get(reverse(views.WriteOnlyResource))
|
||||
# self.assertEquals(resp.status_code, 405)
|
||||
#
|
||||
# def test_writing_write_only_allowed(self):
|
||||
# resp = self.client.put(reverse(views.WriteOnlyResource), {})
|
||||
# self.assertEquals(resp.status_code, 200)
|
||||
#
|
||||
#
|
||||
#class EncodeDecodeTests(TestCase):
|
||||
# def setUp(self):
|
||||
# super(self.__class__, self).setUp()
|
||||
# self.input = {'a': 1, 'b': 'example'}
|
||||
#
|
||||
# def test_encode_form_decode_json(self):
|
||||
# content = self.input
|
||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content)
|
||||
# output = json.loads(resp.content)
|
||||
# self.assertEquals(self.input, output)
|
||||
#
|
||||
# def test_encode_json_decode_json(self):
|
||||
# content = json.dumps(self.input)
|
||||
# resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json')
|
||||
# output = json.loads(resp.content)
|
||||
# self.assertEquals(self.input, output)
|
||||
#
|
||||
# #def test_encode_xml_decode_json(self):
|
||||
# # content = dict2xml(self.input)
|
||||
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json')
|
||||
# # output = json.loads(resp.content)
|
||||
# # self.assertEquals(self.input, output)
|
||||
#
|
||||
# #def test_encode_form_decode_xml(self):
|
||||
# # content = self.input
|
||||
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml')
|
||||
# # output = xml2dict(resp.content)
|
||||
# # self.assertEquals(self.input, output)
|
||||
#
|
||||
# #def test_encode_json_decode_xml(self):
|
||||
# # content = json.dumps(self.input)
|
||||
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
||||
# # output = xml2dict(resp.content)
|
||||
# # self.assertEquals(self.input, output)
|
||||
#
|
||||
# #def test_encode_xml_decode_xml(self):
|
||||
# # content = dict2xml(self.input)
|
||||
# # resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml')
|
||||
# # output = xml2dict(resp.content)
|
||||
# # self.assertEquals(self.input, output)
|
||||
#
|
||||
#class ModelTests(TestCase):
|
||||
# def test_create_container(self):
|
||||
# content = json.dumps({'name': 'example'})
|
||||
# resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json')
|
||||
# output = json.loads(resp.content)
|
||||
# self.assertEquals(resp.status_code, 201)
|
||||
# self.assertEquals(output['name'], 'example')
|
||||
# self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key')))
|
||||
#
|
||||
#class CreatedModelTests(TestCase):
|
||||
# def setUp(self):
|
||||
# content = json.dumps({'name': 'example'})
|
||||
# resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json')
|
||||
# self.container = json.loads(resp.content)
|
||||
#
|
||||
# def test_read_container(self):
|
||||
# resp = self.client.get(self.container["absolute_uri"])
|
||||
# self.assertEquals(resp.status_code, 200)
|
||||
# container = json.loads(resp.content)
|
||||
# self.assertEquals(container, self.container)
|
||||
#
|
||||
# def test_delete_container(self):
|
||||
# resp = self.client.delete(self.container["absolute_uri"])
|
||||
# self.assertEquals(resp.status_code, 204)
|
||||
# self.assertEquals(resp.content, '')
|
||||
#
|
||||
# def test_update_container(self):
|
||||
# self.container['name'] = 'new'
|
||||
# content = json.dumps(self.container)
|
||||
# resp = self.client.put(self.container["absolute_uri"], content, 'application/json')
|
||||
# self.assertEquals(resp.status_code, 200)
|
||||
# container = json.loads(resp.content)
|
||||
# self.assertEquals(container, self.container)
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
from django.conf.urls.defaults import patterns
|
||||
|
||||
urlpatterns = patterns('testapp.views',
|
||||
(r'^$', 'RootResource'),
|
||||
#(r'^read-only$', 'ReadOnlyResource'),
|
||||
#(r'^write-only$', 'WriteOnlyResource'),
|
||||
#(r'^read-write$', 'ReadWriteResource'),
|
||||
#(r'^model$', 'ModelFormResource'),
|
||||
#(r'^container$', 'ContainerFactory'),
|
||||
#(r'^container/((?P<key>[^/]+))$', 'ContainerInstance'),
|
||||
|
||||
(r'^blog-posts/$', 'BlogPostList'),
|
||||
(r'^blog-post/$', 'BlogPostCreator'),
|
||||
(r'^blog-post/(?P<key>[^/]+)/$', 'BlogPostInstance'),
|
||||
|
||||
(r'^blog-post/(?P<blogpost_id>[^/]+)/comments/$', 'CommentList'),
|
||||
(r'^blog-post/(?P<blogpost_id>[^/]+)/comment/$', 'CommentCreator'),
|
||||
(r'^blog-post/(?P<blogpost>[^/]+)/comments/(?P<id>[^/]+)/$', 'CommentInstance'),
|
||||
)
|
|
@ -1,118 +0,0 @@
|
|||
from rest.resource import Resource
|
||||
from rest.modelresource import ModelResource, QueryModelResource
|
||||
from testapp.models import BlogPost, Comment
|
||||
|
||||
##### Root Resource #####
|
||||
|
||||
class RootResource(Resource):
|
||||
"""This is the top level resource for the API.
|
||||
All the sub-resources are discoverable from here."""
|
||||
allowed_operations = ('read',)
|
||||
|
||||
def read(self, headers={}, *args, **kwargs):
|
||||
return (200, {'blog-posts': self.reverse(BlogPostList),
|
||||
'blog-post': self.reverse(BlogPostCreator)}, {})
|
||||
|
||||
|
||||
##### Blog Post Resources #####
|
||||
|
||||
BLOG_POST_FIELDS = ('created', 'title', 'slug', 'content', 'absolute_url', 'comment_url', 'comments_url')
|
||||
|
||||
class BlogPostList(QueryModelResource):
|
||||
"""A resource which lists all existing blog posts."""
|
||||
allowed_operations = ('read', )
|
||||
model = BlogPost
|
||||
fields = BLOG_POST_FIELDS
|
||||
|
||||
class BlogPostCreator(ModelResource):
|
||||
"""A resource with which blog posts may be created."""
|
||||
allowed_operations = ('create',)
|
||||
model = BlogPost
|
||||
fields = BLOG_POST_FIELDS
|
||||
|
||||
class BlogPostInstance(ModelResource):
|
||||
"""A resource which represents a single blog post."""
|
||||
allowed_operations = ('read', 'update', 'delete')
|
||||
model = BlogPost
|
||||
fields = BLOG_POST_FIELDS
|
||||
|
||||
|
||||
##### Comment Resources #####
|
||||
|
||||
COMMENT_FIELDS = ('username', 'comment', 'created', 'rating', 'absolute_url', 'blogpost_url')
|
||||
|
||||
class CommentList(QueryModelResource):
|
||||
"""A resource which lists all existing comments for a given blog post."""
|
||||
allowed_operations = ('read', )
|
||||
model = Comment
|
||||
fields = COMMENT_FIELDS
|
||||
|
||||
class CommentCreator(ModelResource):
|
||||
"""A resource with which blog comments may be created for a given blog post."""
|
||||
allowed_operations = ('create',)
|
||||
model = Comment
|
||||
fields = COMMENT_FIELDS
|
||||
|
||||
class CommentInstance(ModelResource):
|
||||
"""A resource which represents a single comment."""
|
||||
allowed_operations = ('read', 'update', 'delete')
|
||||
model = Comment
|
||||
fields = COMMENT_FIELDS
|
||||
|
||||
|
||||
|
||||
#
|
||||
#'read-only-api': self.reverse(ReadOnlyResource),
|
||||
# 'write-only-api': self.reverse(WriteOnlyResource),
|
||||
# 'read-write-api': self.reverse(ReadWriteResource),
|
||||
# 'model-api': self.reverse(ModelFormResource),
|
||||
# 'create-container': self.reverse(ContainerFactory),
|
||||
#
|
||||
#class ReadOnlyResource(Resource):
|
||||
# """This is my docstring
|
||||
# """
|
||||
# allowed_operations = ('read',)
|
||||
#
|
||||
# def read(self, headers={}, *args, **kwargs):
|
||||
# return (200, {'ExampleString': 'Example',
|
||||
# 'ExampleInt': 1,
|
||||
# 'ExampleDecimal': 1.0}, {})
|
||||
#
|
||||
#
|
||||
#class WriteOnlyResource(Resource):
|
||||
# """This is my docstring
|
||||
# """
|
||||
# allowed_operations = ('update',)
|
||||
#
|
||||
# def update(self, data, headers={}, *args, **kwargs):
|
||||
# return (200, data, {})
|
||||
#
|
||||
#
|
||||
#class ReadWriteResource(Resource):
|
||||
# allowed_operations = ('read', 'update', 'delete')
|
||||
# create_form = ExampleForm
|
||||
# update_form = ExampleForm
|
||||
#
|
||||
#
|
||||
#class ModelFormResource(ModelResource):
|
||||
# allowed_operations = ('read', 'update', 'delete')
|
||||
# model = ExampleModel
|
||||
#
|
||||
## Nice things: form validation is applied to any input type
|
||||
## html forms for output
|
||||
## output always serialized nicely
|
||||
#class ContainerFactory(ModelResource):
|
||||
# allowed_operations = ('create',)
|
||||
# model = ExampleContainer
|
||||
# fields = ('absolute_uri', 'name', 'key')
|
||||
# form_fields = ('name',)
|
||||
#
|
||||
#
|
||||
#class ContainerInstance(ModelResource):
|
||||
# allowed_operations = ('read', 'update', 'delete')
|
||||
# model = ExampleContainer
|
||||
# fields = ('absolute_uri', 'name', 'key')
|
||||
# form_fields = ('name',)
|
||||
|
||||
#######################
|
||||
|
15
src/urls.py
15
src/urls.py
|
@ -1,15 +0,0 @@
|
|||
from django.conf.urls.defaults import patterns, include
|
||||
from django.contrib import admin
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# Example:
|
||||
(r'^', include('testapp.urls')),
|
||||
|
||||
# Uncomment the admin/doc line below to enable admin documentation:
|
||||
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
|
||||
# Uncomment the next line to enable the admin:
|
||||
(r'^admin/', include(admin.site.urls)),
|
||||
)
|
Loading…
Reference in New Issue
Block a user