aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2011-01-14 18:06:40 +0000
committerTom Christie2011-01-14 18:06:40 +0000
commitb0ce3f92c68b13d7f437a21bedcc95727e271860 (patch)
tree03e1feada051ea4501189fe5d7ac66fabe8e834a
parent764fbe335fbd8dab2b9a097a008bd80bf6582f89 (diff)
downloaddjango-rest-framework-b0ce3f92c68b13d7f437a21bedcc95727e271860.tar.bz2
Added formats, various form improvements, more refactoring/cleanup
-rw-r--r--src/rest/emitters.py8
-rw-r--r--src/rest/modelresource.py346
-rw-r--r--src/rest/resource.py442
-rw-r--r--src/rest/templates/emitter.html49
-rw-r--r--src/rest/templates/emitter.txt1
-rw-r--r--src/rest/templatetags/add_query_param.py17
-rw-r--r--src/rest/utils.py8
-rw-r--r--src/testapp/models.py14
-rw-r--r--src/testapp/views.py3
9 files changed, 495 insertions, 393 deletions
diff --git a/src/rest/emitters.py b/src/rest/emitters.py
index b911f31c..c1ff5cc2 100644
--- a/src/rest/emitters.py
+++ b/src/rest/emitters.py
@@ -3,6 +3,8 @@ import json
from utils import dict2xml
class BaseEmitter(object):
+ uses_forms = False
+
def __init__(self, resource):
self.resource = resource
@@ -24,11 +26,14 @@ class TemplatedEmitter(BaseEmitter):
'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 template.render(context)
+ return ret
class JSONEmitter(BaseEmitter):
def emit(self, output):
@@ -46,6 +51,7 @@ class XMLEmitter(BaseEmitter):
class HTMLEmitter(TemplatedEmitter):
template = 'emitter.html'
+ uses_forms = True
class TextEmitter(TemplatedEmitter):
template = 'emitter.txt'
diff --git a/src/rest/modelresource.py b/src/rest/modelresource.py
new file mode 100644
index 00000000..39358d9b
--- /dev/null
+++ b/src/rest/modelresource.py
@@ -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, {})
+
diff --git a/src/rest/resource.py b/src/rest/resource.py
index 6ea19246..e66cb357 100644
--- a/src/rest/resource.py
+++ b/src/rest/resource.py
@@ -1,10 +1,25 @@
from django.http import HttpResponse
+from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from rest import emitters, parsers
from decimal import Decimal
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_405_METHOD_NOT_ALLOWED = 405
@@ -45,15 +60,17 @@ class Resource(object):
# 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'
- ACCEPT_PARAM = '_accept'
- CSRF_PARAM = 'csrfmiddlewaretoken'
- RESERVED_PARAMS = set((METHOD_PARAM, ACCEPT_PARAM, CSRF_PARAM))
+ # 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
- 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):
@@ -69,19 +86,22 @@ class Resource(object):
def name(self):
"""Provide a name for the resource.
- By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names',
- although this behaviour may be overridden."""
+ 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,
- although this behaviour may be overridden."""
- return "%s" % self.__doc__
+ 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."""
@@ -89,19 +109,22 @@ class Resource(object):
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.
- TODO: Add SITEMAP option.
-
- Provided for convienience."""
- return self.request.build_absolute_uri(reverse(view, *args, **kwargs))
+ """Return a fully qualified URI for a given view or resource.
+ Use the Sites framework if possible, otherwise fallback to using the current request."""
+ return self.add_domain(reverse(view, *args, **kwargs))
- def make_absolute(self, uri):
- """Given a relative URI, return an absolute URI using the current request as the base URI.
- TODO: Add SITEMAP option.
+ 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."""
+ 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(uri)
+ return self.request.build_absolute_uri(path)
def read(self, headers={}, *args, **kwargs):
@@ -134,17 +157,18 @@ class Resource(object):
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
-
- if method == 'POST' and request.POST.has_key(self.METHOD_PARAM):
+ 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
@@ -174,17 +198,15 @@ class Resource(object):
return None
- def cleanup_request(self, data):
+ 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 self.form is None:
- return (data, None)
-
- form_instance = self.get_bound_form(data)
+ if form_instance is None:
+ return data
if not form_instance.is_valid():
if not form_instance.errors:
@@ -196,7 +218,7 @@ class Resource(object):
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):
@@ -230,11 +252,17 @@ class Resource(object):
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html"""
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
# 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 = {}
for item in accept_list:
mimetype = item[0].strip()
@@ -308,19 +336,21 @@ class Resource(object):
if method in ('PUT', 'POST'):
parser = self.determine_parser(request)
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)
else:
(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:
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
if emitter is None:
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()
@@ -338,341 +368,3 @@ class Resource(object):
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, {})
diff --git a/src/rest/templates/emitter.html b/src/rest/templates/emitter.html
index 056c52c5..17d53b81 100644
--- a/src/rest/templates/emitter.html
+++ b/src/rest/templates/emitter.html
@@ -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"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
@@ -6,12 +6,19 @@
<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 }}</p>
+ <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 %}
@@ -20,14 +27,32 @@
{% 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">
+ <form action="{{ resource.request.path }}" method="post">
{% 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" />
</form>
</div>
@@ -35,10 +60,20 @@
{% if 'update' in resource.allowed_operations %}
<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" />
{% 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" />
</form>
</div>
@@ -46,7 +81,7 @@
{% if 'delete' in resource.allowed_operations %}
<div class='action'>
- <form action="{{ resource.request.path }}" method="POST">
+ <form action="{{ resource.request.path }}" method="post">
{% csrf_token %}
<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="DELETE" />
<input type="submit" value="Delete" />
diff --git a/src/rest/templates/emitter.txt b/src/rest/templates/emitter.txt
index 78c619df..925529bd 100644
--- a/src/rest/templates/emitter.txt
+++ b/src/rest/templates/emitter.txt
@@ -1,4 +1,5 @@
{{ resource.name }}
+
{{ resource.description }}
{% autoescape off %}HTTP/1.0 {{ resource.resp_status }} {{ resource.resp_status_text }}
diff --git a/src/rest/templatetags/add_query_param.py b/src/rest/templatetags/add_query_param.py
new file mode 100644
index 00000000..91c1a312
--- /dev/null
+++ b/src/rest/templatetags/add_query_param.py
@@ -0,0 +1,17 @@
+from django.template import Library
+from urlparse import urlparse, urlunparse
+from urllib import quote
+register = Library()
+
+def add_query_param(url, param):
+ (key, val) = param.split('=')
+ param = '%s=%s' % (key, quote(val))
+ (scheme, netloc, path, params, query, fragment) = urlparse(url)
+ if query:
+ query += "&" + param
+ else:
+ query = param
+ return urlunparse((scheme, netloc, path, params, query, fragment))
+
+
+register.filter('add_query_param', add_query_param)
diff --git a/src/rest/utils.py b/src/rest/utils.py
index b80ed9a7..98d8e1ae 100644
--- a/src/rest/utils.py
+++ b/src/rest/utils.py
@@ -140,9 +140,9 @@ class XMLEmitter():
def _to_xml(self, xml, data):
if isinstance(data, (list, tuple)):
for item in data:
- xml.startElement("resource", {})
+ xml.startElement("list-item", {})
self._to_xml(xml, item)
- xml.endElement("resource")
+ xml.endElement("list-item")
elif isinstance(data, dict):
for key, value in data.iteritems():
@@ -158,11 +158,11 @@ class XMLEmitter():
xml = SimplerXMLGenerator(stream, "utf-8")
xml.startDocument()
- xml.startElement("content", {})
+ xml.startElement("root", {})
self._to_xml(xml, data)
- xml.endElement("content")
+ xml.endElement("root")
xml.endDocument()
return stream.getvalue()
diff --git a/src/testapp/models.py b/src/testapp/models.py
index 32d9a612..909788a3 100644
--- a/src/testapp/models.py
+++ b/src/testapp/models.py
@@ -40,8 +40,8 @@ RATING_CHOICES = ((0, 'Awful'),
class BlogPost(models.Model):
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)')
- content = models.TextField(help_text='The article body (Required)')
+ title = models.CharField(max_length=128)
+ content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
slug = models.SlugField(editable=False, default='')
@@ -74,11 +74,14 @@ class BlogPost(models.Model):
class Comment(models.Model):
blogpost = models.ForeignKey(BlogPost, editable=False, related_name='comments')
- username = models.CharField(max_length=128, help_text='Please enter a username (Required)')
- comment = models.TextField(help_text='Enter your comment here (Required)')
- rating = models.IntegerField(blank=True, null=True, choices=RATING_CHOICES, help_text='Please rate the blog post (Optional)')
+ 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))
@@ -86,5 +89,6 @@ class Comment(models.Model):
@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,))
diff --git a/src/testapp/views.py b/src/testapp/views.py
index dee0b19b..82539435 100644
--- a/src/testapp/views.py
+++ b/src/testapp/views.py
@@ -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
##### Root Resource #####