mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-10 19:56:59 +03:00
Added formats, various form improvements, more refactoring/cleanup
This commit is contained in:
parent
764fbe335f
commit
b0ce3f92c6
|
@ -3,6 +3,8 @@ import json
|
||||||
from utils import dict2xml
|
from utils import dict2xml
|
||||||
|
|
||||||
class BaseEmitter(object):
|
class BaseEmitter(object):
|
||||||
|
uses_forms = False
|
||||||
|
|
||||||
def __init__(self, resource):
|
def __init__(self, resource):
|
||||||
self.resource = resource
|
self.resource = resource
|
||||||
|
|
||||||
|
@ -24,11 +26,14 @@ class TemplatedEmitter(BaseEmitter):
|
||||||
'resource': self.resource,
|
'resource': self.resource,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ret = template.render(context)
|
||||||
|
|
||||||
# Munge DELETE Response code to allow us to return content
|
# 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:
|
if self.resource.resp_status == 204:
|
||||||
self.resource.resp_status = 200
|
self.resource.resp_status = 200
|
||||||
|
|
||||||
return template.render(context)
|
return ret
|
||||||
|
|
||||||
class JSONEmitter(BaseEmitter):
|
class JSONEmitter(BaseEmitter):
|
||||||
def emit(self, output):
|
def emit(self, output):
|
||||||
|
@ -46,6 +51,7 @@ class XMLEmitter(BaseEmitter):
|
||||||
|
|
||||||
class HTMLEmitter(TemplatedEmitter):
|
class HTMLEmitter(TemplatedEmitter):
|
||||||
template = 'emitter.html'
|
template = 'emitter.html'
|
||||||
|
uses_forms = True
|
||||||
|
|
||||||
class TextEmitter(TemplatedEmitter):
|
class TextEmitter(TemplatedEmitter):
|
||||||
template = 'emitter.txt'
|
template = 'emitter.txt'
|
||||||
|
|
346
src/rest/modelresource.py
Normal file
346
src/rest/modelresource.py
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
"""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):
|
||||||
|
model = None
|
||||||
|
fields = None
|
||||||
|
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 #self.fields
|
||||||
|
|
||||||
|
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_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,10 +1,25 @@
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.contrib.sites.models import Site
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
from django.core.handlers.wsgi import STATUS_CODE_TEXT
|
||||||
from rest import emitters, parsers
|
from rest import emitters, parsers
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# TODO: Take request, not headers
|
||||||
|
# TODO: Remove self.blah munging (Add a ResponseContext object)
|
||||||
|
# TODO: Erroring on non-existent fields
|
||||||
|
# TODO: Standard exception classes and module for status codes
|
||||||
|
# TODO: Figure how out references and named urls need to work nicely
|
||||||
|
# TODO: POST on existing 404 URL, PUT on existing 404 URL
|
||||||
|
# TODO: Authentication
|
||||||
|
#
|
||||||
|
# FUTURE: Erroring on read-only fields
|
||||||
|
|
||||||
|
# Documentation, Release
|
||||||
|
|
||||||
#
|
#
|
||||||
STATUS_400_BAD_REQUEST = 400
|
STATUS_400_BAD_REQUEST = 400
|
||||||
STATUS_405_METHOD_NOT_ALLOWED = 405
|
STATUS_405_METHOD_NOT_ALLOWED = 405
|
||||||
|
@ -45,15 +60,17 @@ class Resource(object):
|
||||||
# Map standard HTTP methods to RESTful operations
|
# Map standard HTTP methods to RESTful operations
|
||||||
CALLMAP = { 'GET': 'read', 'POST': 'create',
|
CALLMAP = { 'GET': 'read', 'POST': 'create',
|
||||||
'PUT': 'update', 'DELETE': 'delete' }
|
'PUT': 'update', 'DELETE': 'delete' }
|
||||||
|
|
||||||
REVERSE_CALLMAP = dict([(val, key) for (key, val) in CALLMAP.items()])
|
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.
|
# Some reserved parameters to allow us to use standard HTML forms with our resource
|
||||||
METHOD_PARAM = '_method'
|
METHOD_PARAM = '_method' # Allow POST overloading
|
||||||
ACCEPT_PARAM = '_accept'
|
ACCEPT_PARAM = '_accept' # Allow override of Accept header in GET requests
|
||||||
CSRF_PARAM = 'csrfmiddlewaretoken'
|
CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header (allows sending arbitrary content with standard forms)
|
||||||
RESERVED_PARAMS = set((METHOD_PARAM, ACCEPT_PARAM, CSRF_PARAM))
|
CONTENT_PARAM = '_content' # Allow override of body content (allows sending arbitrary content with standard forms)
|
||||||
|
CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token
|
||||||
|
|
||||||
USE_SITEMAP_FOR_ABSOLUTE_URLS = False
|
RESERVED_PARAMS = set((METHOD_PARAM, ACCEPT_PARAM, CONTENTTYPE_PARAM, CONTENT_PARAM, CSRF_PARAM))
|
||||||
|
|
||||||
|
|
||||||
def __new__(cls, request, *args, **kwargs):
|
def __new__(cls, request, *args, **kwargs):
|
||||||
|
@ -69,19 +86,22 @@ class Resource(object):
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Provide a name for the resource.
|
"""Provide a name for the resource.
|
||||||
By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names',
|
By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'."""
|
||||||
although this behaviour may be overridden."""
|
|
||||||
class_name = self.__class__.__name__
|
class_name = self.__class__.__name__
|
||||||
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip()
|
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip()
|
||||||
|
|
||||||
|
|
||||||
def description(self):
|
def description(self):
|
||||||
"""Provide a description for the resource.
|
"""Provide a description for the resource.
|
||||||
By default this is the class's docstring,
|
By default this is the class's docstring with leading line spaces stripped."""
|
||||||
although this behaviour may be overridden."""
|
return re.sub(re.compile('^ +', re.MULTILINE), '', self.__doc__)
|
||||||
return "%s" % 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):
|
def resp_status_text(self):
|
||||||
"""Return reason text corrosponding to our HTTP response status code.
|
"""Return reason text corrosponding to our HTTP response status code.
|
||||||
Provided for convienience."""
|
Provided for convienience."""
|
||||||
|
@ -89,19 +109,22 @@ class Resource(object):
|
||||||
|
|
||||||
|
|
||||||
def reverse(self, view, *args, **kwargs):
|
def reverse(self, view, *args, **kwargs):
|
||||||
"""Return a fully qualified URI for a given view or resource, using the current request as the base URI.
|
"""Return a fully qualified URI for a given view or resource.
|
||||||
TODO: Add SITEMAP option.
|
Use the Sites framework if possible, otherwise fallback to using the current request."""
|
||||||
|
return self.add_domain(reverse(view, *args, **kwargs))
|
||||||
Provided for convienience."""
|
|
||||||
return self.request.build_absolute_uri(reverse(view, *args, **kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
def make_absolute(self, uri):
|
def add_domain(self, path):
|
||||||
"""Given a relative URI, return an absolute URI using the current request as the base URI.
|
"""Given a path, return an fully qualified URI.
|
||||||
TODO: Add SITEMAP option.
|
Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
|
||||||
|
try:
|
||||||
|
site = Site.objects.get_current()
|
||||||
|
if site.domain and site.domain != 'example.com':
|
||||||
|
return 'http://%s%s' % (site.domain, path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
Provided for convienience."""
|
return self.request.build_absolute_uri(path)
|
||||||
return self.request.build_absolute_uri(uri)
|
|
||||||
|
|
||||||
|
|
||||||
def read(self, headers={}, *args, **kwargs):
|
def read(self, headers={}, *args, **kwargs):
|
||||||
|
@ -134,17 +157,18 @@ class Resource(object):
|
||||||
def determine_method(self, request):
|
def determine_method(self, request):
|
||||||
"""Determine the HTTP method that this request should be treated as.
|
"""Determine the HTTP method that this request should be treated as.
|
||||||
Allow for PUT and DELETE tunneling via the _method parameter."""
|
Allow for PUT and DELETE tunneling via the _method parameter."""
|
||||||
method = request.method
|
method = request.method.upper()
|
||||||
|
|
||||||
if method == 'POST' and request.POST.has_key(self.METHOD_PARAM):
|
if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM):
|
||||||
method = request.POST[self.METHOD_PARAM].upper()
|
method = request.POST[self.METHOD_PARAM].upper()
|
||||||
|
|
||||||
return method
|
return method
|
||||||
|
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
"""..."""
|
"""TODO"""
|
||||||
# user = ...
|
# user = ...
|
||||||
|
# if DEBUG and request is from localhost
|
||||||
# if anon_user and not anon_allowed_operations raise PermissionDenied
|
# if anon_user and not anon_allowed_operations raise PermissionDenied
|
||||||
# return
|
# return
|
||||||
|
|
||||||
|
@ -174,17 +198,15 @@ class Resource(object):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def cleanup_request(self, data):
|
def cleanup_request(self, data, form_instance):
|
||||||
"""Perform any resource-specific data deserialization and/or validation
|
"""Perform any resource-specific data deserialization and/or validation
|
||||||
after the initial HTTP content-type deserialization has taken place.
|
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.
|
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."""
|
By default this uses form validation to filter the basic input into the required types."""
|
||||||
if self.form is None:
|
if form_instance is None:
|
||||||
return (data, None)
|
return data
|
||||||
|
|
||||||
form_instance = self.get_bound_form(data)
|
|
||||||
|
|
||||||
if not form_instance.is_valid():
|
if not form_instance.is_valid():
|
||||||
if not form_instance.errors:
|
if not form_instance.errors:
|
||||||
|
@ -196,7 +218,7 @@ class Resource(object):
|
||||||
|
|
||||||
raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details})
|
raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details})
|
||||||
|
|
||||||
return (form_instance.cleaned_data, form_instance)
|
return form_instance.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
def cleanup_response(self, data):
|
def cleanup_response(self, data):
|
||||||
|
@ -230,11 +252,17 @@ class Resource(object):
|
||||||
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
|
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
|
||||||
default = self.emitters[0]
|
default = self.emitters[0]
|
||||||
|
|
||||||
if not request.META.has_key('HTTP_ACCEPT'):
|
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
|
return default
|
||||||
|
|
||||||
# Parse the accept header into a dict of {Priority: List of Mimetypes}
|
# Parse the accept header into a dict of {Priority: List of Mimetypes}
|
||||||
accept_list = [item.split(';') for item in request.META["HTTP_ACCEPT"].split(',')]
|
|
||||||
accept_dict = {}
|
accept_dict = {}
|
||||||
for item in accept_list:
|
for item in accept_list:
|
||||||
mimetype = item[0].strip()
|
mimetype = item[0].strip()
|
||||||
|
@ -308,19 +336,21 @@ class Resource(object):
|
||||||
if method in ('PUT', 'POST'):
|
if method in ('PUT', 'POST'):
|
||||||
parser = self.determine_parser(request)
|
parser = self.determine_parser(request)
|
||||||
data = parser(self).parse(request.raw_post_data)
|
data = parser(self).parse(request.raw_post_data)
|
||||||
(data, self.form_instance) = self.cleanup_request(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)
|
(self.resp_status, ret, self.resp_headers) = func(data, request.META, *args, **kwargs)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
(self.resp_status, ret, self.resp_headers) = func(request.META, *args, **kwargs)
|
(self.resp_status, ret, self.resp_headers) = func(request.META, *args, **kwargs)
|
||||||
self.form_instance = self.get_bound_form(ret, is_response=True)
|
if emitter.uses_forms:
|
||||||
|
self.form_instance = self.get_bound_form(ret, is_response=True)
|
||||||
|
|
||||||
|
|
||||||
except ResourceException, exc:
|
except ResourceException, exc:
|
||||||
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
|
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
|
||||||
if emitter is None:
|
if emitter is None:
|
||||||
mimetype, emitter = self.emitters[0]
|
mimetype, emitter = self.emitters[0]
|
||||||
if self.form_instance is None:
|
if self.form_instance is None and emitter.uses_forms:
|
||||||
self.form_instance = self.get_bound_form()
|
self.form_instance = self.get_bound_form()
|
||||||
|
|
||||||
|
|
||||||
|
@ -338,341 +368,3 @@ class Resource(object):
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from django.forms import ModelForm
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from django.db.models import Model
|
|
||||||
import decimal
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
class ModelResource(Resource):
|
|
||||||
model = None
|
|
||||||
fields = None
|
|
||||||
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 #self.fields
|
|
||||||
|
|
||||||
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_response(self, data):
|
|
||||||
"""
|
|
||||||
Recursively serialize a lot of types, and
|
|
||||||
in cases where it doesn't recognize the type,
|
|
||||||
it will fall back to Django's `smart_unicode`.
|
|
||||||
|
|
||||||
Returns `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'] = self.make_absolute(data.get_absolute_url())
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
for key, val in ret.items():
|
|
||||||
if key.endswith('_url') or key.endswith('_uri'):
|
|
||||||
ret[key] = self.make_absolute(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):
|
|
||||||
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.make_absolute(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):
|
|
||||||
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):
|
|
||||||
instance = self.model.objects.get(**kwargs)
|
|
||||||
instance.delete()
|
|
||||||
return (204, None, {})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class QueryModelResource(ModelResource):
|
|
||||||
allowed_methods = ('read',)
|
|
||||||
|
|
||||||
def get_bound_form(self, data=None, is_response=False):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def read(self, headers={}, *args, **kwargs):
|
|
||||||
query = self.model.objects.all()
|
|
||||||
return (200, query, {})
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load urlize_quoted_links %}<?xml version="1.0" encoding="UTF-8"?>
|
{% 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"
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
@ -6,12 +6,19 @@
|
||||||
<style>
|
<style>
|
||||||
pre {border: 1px solid black; padding: 1em; background: #ffd}
|
pre {border: 1px solid black; padding: 1em; background: #ffd}
|
||||||
div.action {padding: 0.5em 1em; margin-bottom: 0.5em; background: #ddf}
|
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>
|
</style>
|
||||||
<title>API - {{ resource.name }}</title>
|
<title>API - {{ resource.name }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{{ resource.name }}</h1>
|
<h1>{{ resource.name }}</h1>
|
||||||
<p>{{ resource.description }}</p>
|
<p>{{ resource.description|linebreaksbr }}</p>
|
||||||
<pre><b>{{ resource.resp_status }} {{ resource.resp_status_text }}</b>{% autoescape off %}
|
<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 }}
|
{% for key, val in resource.resp_headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -20,14 +27,32 @@
|
||||||
{% if 'read' in resource.allowed_operations %}
|
{% if 'read' in resource.allowed_operations %}
|
||||||
<div class='action'>
|
<div class='action'>
|
||||||
<a href='{{ resource.request.path }}'>Read</a>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if 'create' in resource.allowed_operations %}
|
{% if 'create' in resource.allowed_operations %}
|
||||||
<div class='action'>
|
<div class='action'>
|
||||||
<form action="{{ resource.request.path }}" method="POST">
|
<form action="{{ resource.request.path }}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ resource.form_instance.as_p }}
|
{% 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" />
|
<input type="submit" value="Create" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,10 +60,20 @@
|
||||||
|
|
||||||
{% if 'update' in resource.allowed_operations %}
|
{% if 'update' in resource.allowed_operations %}
|
||||||
<div class='action'>
|
<div class='action'>
|
||||||
<form action="{{ resource.request.path }}" method="POST">
|
<form action="{{ resource.request.path }}" method="post">
|
||||||
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
|
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" />
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ resource.form_instance.as_p }}
|
{% 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" />
|
<input type="submit" value="Update" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +81,7 @@
|
||||||
|
|
||||||
{% if 'delete' in resource.allowed_operations %}
|
{% if 'delete' in resource.allowed_operations %}
|
||||||
<div class='action'>
|
<div class='action'>
|
||||||
<form action="{{ resource.request.path }}" method="POST">
|
<form action="{{ resource.request.path }}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
|
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
|
||||||
<input type="submit" value="Delete" />
|
<input type="submit" value="Delete" />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{ resource.name }}
|
{{ resource.name }}
|
||||||
|
|
||||||
{{ resource.description }}
|
{{ resource.description }}
|
||||||
|
|
||||||
{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}
|
{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}
|
||||||
|
|
17
src/rest/templatetags/add_query_param.py
Normal file
17
src/rest/templatetags/add_query_param.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from django.template import Library
|
||||||
|
from urlparse import urlparse, urlunparse
|
||||||
|
from urllib import quote
|
||||||
|
register = Library()
|
||||||
|
|
||||||
|
def add_query_param(url, param):
|
||||||
|
(key, val) = param.split('=')
|
||||||
|
param = '%s=%s' % (key, quote(val))
|
||||||
|
(scheme, netloc, path, params, query, fragment) = urlparse(url)
|
||||||
|
if query:
|
||||||
|
query += "&" + param
|
||||||
|
else:
|
||||||
|
query = param
|
||||||
|
return urlunparse((scheme, netloc, path, params, query, fragment))
|
||||||
|
|
||||||
|
|
||||||
|
register.filter('add_query_param', add_query_param)
|
|
@ -140,9 +140,9 @@ class XMLEmitter():
|
||||||
def _to_xml(self, xml, data):
|
def _to_xml(self, xml, data):
|
||||||
if isinstance(data, (list, tuple)):
|
if isinstance(data, (list, tuple)):
|
||||||
for item in data:
|
for item in data:
|
||||||
xml.startElement("resource", {})
|
xml.startElement("list-item", {})
|
||||||
self._to_xml(xml, item)
|
self._to_xml(xml, item)
|
||||||
xml.endElement("resource")
|
xml.endElement("list-item")
|
||||||
|
|
||||||
elif isinstance(data, dict):
|
elif isinstance(data, dict):
|
||||||
for key, value in data.iteritems():
|
for key, value in data.iteritems():
|
||||||
|
@ -158,11 +158,11 @@ class XMLEmitter():
|
||||||
|
|
||||||
xml = SimplerXMLGenerator(stream, "utf-8")
|
xml = SimplerXMLGenerator(stream, "utf-8")
|
||||||
xml.startDocument()
|
xml.startDocument()
|
||||||
xml.startElement("content", {})
|
xml.startElement("root", {})
|
||||||
|
|
||||||
self._to_xml(xml, data)
|
self._to_xml(xml, data)
|
||||||
|
|
||||||
xml.endElement("content")
|
xml.endElement("root")
|
||||||
xml.endDocument()
|
xml.endDocument()
|
||||||
return stream.getvalue()
|
return stream.getvalue()
|
||||||
|
|
||||||
|
|
|
@ -40,8 +40,8 @@ RATING_CHOICES = ((0, 'Awful'),
|
||||||
|
|
||||||
class BlogPost(models.Model):
|
class BlogPost(models.Model):
|
||||||
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
|
key = models.CharField(primary_key=True, max_length=64, default=uuid_str, editable=False)
|
||||||
title = models.CharField(max_length=128, help_text='The article title (Required)')
|
title = models.CharField(max_length=128)
|
||||||
content = models.TextField(help_text='The article body (Required)')
|
content = models.TextField()
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
slug = models.SlugField(editable=False, default='')
|
slug = models.SlugField(editable=False, default='')
|
||||||
|
|
||||||
|
@ -74,11 +74,14 @@ class BlogPost(models.Model):
|
||||||
|
|
||||||
class Comment(models.Model):
|
class Comment(models.Model):
|
||||||
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
|
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
|
||||||
username = models.CharField(max_length=128, help_text='Please enter a username (Required)')
|
username = models.CharField(max_length=128)
|
||||||
comment = models.TextField(help_text='Enter your comment here (Required)')
|
comment = models.TextField()
|
||||||
rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='Please rate the blog post (Optional)')
|
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)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('created',)
|
||||||
|
|
||||||
@models.permalink
|
@models.permalink
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return ('testapp.views.CommentInstance', (self.blogpost.key, self.id))
|
return ('testapp.views.CommentInstance', (self.blogpost.key, self.id))
|
||||||
|
@ -86,5 +89,6 @@ class Comment(models.Model):
|
||||||
@property
|
@property
|
||||||
@models.permalink
|
@models.permalink
|
||||||
def blogpost_url(self):
|
def blogpost_url(self):
|
||||||
|
"""Link to the blog post resource which this comment corresponds to."""
|
||||||
return ('testapp.views.BlogPostInstance', (self.blogpost.key,))
|
return ('testapp.views.BlogPostInstance', (self.blogpost.key,))
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from rest.resource import Resource, ModelResource, QueryModelResource
|
from rest.resource import Resource
|
||||||
|
from rest.modelresource import ModelResource, QueryModelResource
|
||||||
from testapp.models import BlogPost, Comment
|
from testapp.models import BlogPost, Comment
|
||||||
|
|
||||||
##### Root Resource #####
|
##### Root Resource #####
|
||||||
|
|
Loading…
Reference in New Issue
Block a user