Sphinx docs, examples, lots of refactoring

This commit is contained in:
tom christie tom@tomchristie.com 2011-01-23 23:08:44 +00:00
parent 4100242fa2
commit e95198a1c0
26 changed files with 0 additions and 1883 deletions

View File

View File

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

View File

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

View File

View File

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

View File

@ -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, {})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
HTML:
{{ content }}

Binary file not shown.

View File

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

View File

@ -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 = ['(', '<', '&lt;', '"', "'"]
TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '&gt;', '"', "'"]
# List of possible strings used for bullets in bulleted lists.
DOTS = ['&middot;', '*', '\xe2\x80\xa2', '&#149;', '&bull;', '&#8226;']
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>(?:&nbsp;|\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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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