diff --git a/src/rest/emitters.py b/src/rest/emitters.py index b911f31c1..c1ff5cc22 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 000000000..39358d9b2 --- /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 6ea19246a..e66cb3573 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 056c52c53..17d53b816 100644 --- a/src/rest/templates/emitter.html +++ b/src/rest/templates/emitter.html @@ -1,4 +1,4 @@ -{% load urlize_quoted_links %} +{% load urlize_quoted_links %}{% load add_query_param %} @@ -6,12 +6,19 @@ API - {{ resource.name }}

{{ resource.name }}

-

{{ resource.description }}

+

{{ resource.description|linebreaksbr }}

{{ resource.resp_status }} {{ resource.resp_status_text }}{% autoescape off %}
 {% for key, val in resource.resp_headers.items %}{{ key }}: {{ val|urlize_quoted_links }}
 {% endfor %}
@@ -20,14 +27,32 @@
 {% if 'read' in resource.allowed_operations %}
 	
Read + +
{% endif %} {% if 'create' in resource.allowed_operations %}
-
+ {% csrf_token %} - {{ resource.form_instance.as_p }} + {% with resource.form_instance as form %} + {% for field in form %} +
+ {{ field.label_tag }}: + {{ field }} + {{ field.help_text }} + {{ field.errors }} +
+ {% endfor %} + {% endwith %} +
@@ -35,10 +60,20 @@ {% if 'update' in resource.allowed_operations %}
-
+ {% csrf_token %} - {{ resource.form_instance.as_p }} + {% with resource.form_instance as form %} + {% for field in form %} +
+ {{ field.label_tag }}: + {{ field }} + {{ field.help_text }} + {{ field.errors }} +
+ {% endfor %} + {% endwith %} +
@@ -46,7 +81,7 @@ {% if 'delete' in resource.allowed_operations %}
-
+ {% csrf_token %} diff --git a/src/rest/templates/emitter.txt b/src/rest/templates/emitter.txt index 78c619df4..925529bd6 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 000000000..91c1a312b --- /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 b80ed9a70..98d8e1ae9 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 32d9a612e..909788a3b 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 dee0b19be..825394356 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 #####