diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/initial_data.json b/src/initial_data.json deleted file mode 100644 index 62103cf97..000000000 --- a/src/initial_data.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "pk": 1, - "model": "auth.user", - "fields": { - "username": "admin", - "first_name": "", - "last_name": "", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2010-01-01 00:00:00", - "groups": [], - "user_permissions": [], - "password": "sha1$6cbce$e4e808893d586a3301ac3c14da6c84855999f1d8", - "email": "test@example.com", - "date_joined": "2010-01-01 00:00:00" - } - } -] \ No newline at end of file diff --git a/src/manage.py b/src/manage.py deleted file mode 100755 index 5e78ea979..000000000 --- a/src/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) - -if __name__ == "__main__": - execute_manager(settings) diff --git a/src/rest/__init__.py b/src/rest/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/rest/emitters.py b/src/rest/emitters.py deleted file mode 100644 index c1ff5cc22..000000000 --- a/src/rest/emitters.py +++ /dev/null @@ -1,59 +0,0 @@ -from django.template import RequestContext, loader -import json -from utils import dict2xml - -class BaseEmitter(object): - uses_forms = False - - def __init__(self, resource): - self.resource = resource - - def emit(self, output): - return output - -class TemplatedEmitter(BaseEmitter): - template = None - - def emit(self, output): - if output is None: - content = '' - else: - content = json.dumps(output, indent=4, sort_keys=True) - - template = loader.get_template(self.template) - context = RequestContext(self.resource.request, { - 'content': content, - 'resource': self.resource, - }) - - ret = template.render(context) - - # Munge DELETE Response code to allow us to return content - # (Do this *after* we've rendered the template so that we include the normal deletion response code in the output) - if self.resource.resp_status == 204: - self.resource.resp_status = 200 - - return ret - -class JSONEmitter(BaseEmitter): - def emit(self, output): - if output is None: - # Treat None as no message body, rather than serializing - return '' - return json.dumps(output) - -class XMLEmitter(BaseEmitter): - def emit(self, output): - if output is None: - # Treat None as no message body, rather than serializing - return '' - return dict2xml(output) - -class HTMLEmitter(TemplatedEmitter): - template = 'emitter.html' - uses_forms = True - -class TextEmitter(TemplatedEmitter): - template = 'emitter.txt' - - diff --git a/src/rest/modelresource.py b/src/rest/modelresource.py deleted file mode 100644 index 6719a9ede..000000000 --- a/src/rest/modelresource.py +++ /dev/null @@ -1,394 +0,0 @@ -"""TODO: docs -""" -from django.forms import ModelForm -from django.db.models.query import QuerySet -from django.db.models import Model - -from rest.resource import Resource - -import decimal -import inspect -import re - - -class ModelResource(Resource): - """A specialized type of Resource, for RESTful resources that map directly to a Django Model. - Useful things this provides: - - 0. Default input validation based on ModelForms. - 1. Nice serialization of returned Models and QuerySets. - 2. A default set of create/read/update/delete operations.""" - - # The model attribute refers to the Django Model which this Resource maps to. - # (The Model's class, rather than an instance of the Model) - model = None - - # By default the set of returned fields will be the set of: - # - # 0. All the fields on the model, excluding 'id'. - # 1. All the properties on the model. - # 2. The absolute_url of the model, if a get_absolute_url method exists for the model. - # - # If you wish to override this behaviour, - # you should explicitly set the fields attribute on your class. - fields = None - - # By default the form used with be a ModelForm for self.model - # If you wish to override this behaviour or provide a sub-classed ModelForm - # you should explicitly set the form attribute on your class. - form = None - - # By default the set of input fields will be the same as the set of output fields - # If you wish to override this behaviour you should explicitly set the - # form_fields attribute on your class. - form_fields = None - - - def get_bound_form(self, data=None, is_response=False): - """Return a form that may be used in validation and/or rendering an html emitter""" - if self.form: - return super(self.__class__, self).get_bound_form(data, is_response=is_response) - - elif self.model: - class NewModelForm(ModelForm): - class Meta: - model = self.model - fields = self.form_fields if self.form_fields else None - - if data and not is_response: - return NewModelForm(data) - elif data and is_response: - return NewModelForm(instance=data) - else: - return NewModelForm() - - else: - return None - - - def cleanup_request(self, data, form_instance): - """Override cleanup_request to drop read-only fields from the input prior to validation. - This ensures that we don't error out with 'non-existent field' when these fields are supplied, - and allows for a pragmatic approach to resources which include read-only elements. - - I would actually like to be strict and verify the value of correctness of the values in these fields, - although that gets tricky as it involves validating at the point that we get the model instance. - - See here for another example of this approach: - http://fedoraproject.org/wiki/Cloud_APIs_REST_Style_Guide - https://www.redhat.com/archives/rest-practices/2010-April/thread.html#00041""" - read_only_fields = set(self.fields) - set(self.form_instance.fields) - input_fields = set(data.keys()) - - clean_data = {} - for key in input_fields - read_only_fields: - clean_data[key] = data[key] - - return super(ModelResource, self).cleanup_request(clean_data, form_instance) - - - def cleanup_response(self, data): - """A munging of Piston's pre-serialization. Returns a dict""" - - def _any(thing, fields=()): - """ - Dispatch, all types are routed through here. - """ - ret = None - - if isinstance(thing, QuerySet): - ret = _qs(thing, fields=fields) - elif isinstance(thing, (tuple, list)): - ret = _list(thing) - elif isinstance(thing, dict): - ret = _dict(thing) - elif isinstance(thing, int): - ret = thing - elif isinstance(thing, bool): - ret = thing - elif isinstance(thing, type(None)): - ret = thing - elif isinstance(thing, decimal.Decimal): - ret = str(thing) - elif isinstance(thing, Model): - ret = _model(thing, fields=fields) - #elif isinstance(thing, HttpResponse): TRC - # raise HttpStatusCode(thing) - elif inspect.isfunction(thing): - if not inspect.getargspec(thing)[0]: - ret = _any(thing()) - elif hasattr(thing, '__emittable__'): - f = thing.__emittable__ - if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1: - ret = _any(f()) - else: - ret = str(thing) # TRC TODO: Change this back! - - return ret - - def _fk(data, field): - """ - Foreign keys. - """ - return _any(getattr(data, field.name)) - - def _related(data, fields=()): - """ - Foreign keys. - """ - return [ _model(m, fields) for m in data.iterator() ] - - def _m2m(data, field, fields=()): - """ - Many to many (re-route to `_model`.) - """ - return [ _model(m, fields) for m in getattr(data, field.name).iterator() ] - - - def _method_fields(data, fields): - if not data: - return { } - - has = dir(data) - ret = dict() - - for field in fields: - if field in has: - ret[field] = getattr(data, field) - - return ret - - def _model(data, fields=()): - """ - Models. Will respect the `fields` and/or - `exclude` on the handler (see `typemapper`.) - """ - ret = { } - #handler = self.in_typemapper(type(data), self.anonymous) # TRC - handler = None # TRC - get_absolute_url = False - - if handler or fields: - v = lambda f: getattr(data, f.attname) - - if not fields: - """ - Fields was not specified, try to find teh correct - version in the typemapper we were sent. - """ - mapped = self.in_typemapper(type(data), self.anonymous) - get_fields = set(mapped.fields) - exclude_fields = set(mapped.exclude).difference(get_fields) - - if not get_fields: - get_fields = set([ f.attname.replace("_id", "", 1) - for f in data._meta.fields ]) - - # sets can be negated. - for exclude in exclude_fields: - if isinstance(exclude, basestring): - get_fields.discard(exclude) - - elif isinstance(exclude, re._pattern_type): - for field in get_fields.copy(): - if exclude.match(field): - get_fields.discard(field) - - get_absolute_url = True - - else: - get_fields = set(fields) - if 'absolute_url' in get_fields: # MOVED (TRC) - get_absolute_url = True - - met_fields = _method_fields(handler, get_fields) # TRC - - for f in data._meta.local_fields: - if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]): - if not f.rel: - if f.attname in get_fields: - ret[f.attname] = _any(v(f)) - get_fields.remove(f.attname) - else: - if f.attname[:-3] in get_fields: - ret[f.name] = _fk(data, f) - get_fields.remove(f.name) - - for mf in data._meta.many_to_many: - if mf.serialize and mf.attname not in met_fields: - if mf.attname in get_fields: - ret[mf.name] = _m2m(data, mf) - get_fields.remove(mf.name) - - # try to get the remainder of fields - for maybe_field in get_fields: - - if isinstance(maybe_field, (list, tuple)): - model, fields = maybe_field - inst = getattr(data, model, None) - - if inst: - if hasattr(inst, 'all'): - ret[model] = _related(inst, fields) - elif callable(inst): - if len(inspect.getargspec(inst)[0]) == 1: - ret[model] = _any(inst(), fields) - else: - ret[model] = _model(inst, fields) - - elif maybe_field in met_fields: - # Overriding normal field which has a "resource method" - # so you can alter the contents of certain fields without - # using different names. - ret[maybe_field] = _any(met_fields[maybe_field](data)) - - else: - maybe = getattr(data, maybe_field, None) - if maybe: - if callable(maybe): - if len(inspect.getargspec(maybe)[0]) == 1: - ret[maybe_field] = _any(maybe()) - else: - ret[maybe_field] = _any(maybe) - else: - pass # TRC - #handler_f = getattr(handler or self.handler, maybe_field, None) - # - #if handler_f: - # ret[maybe_field] = _any(handler_f(data)) - - else: - # Add absolute_url if it exists - get_absolute_url = True - - # Add all the fields - for f in data._meta.fields: - if f.attname != 'id': - ret[f.attname] = _any(getattr(data, f.attname)) - - # Add all the propertiess - klass = data.__class__ - for attr in dir(klass): - if not attr.startswith('_') and not attr in ('pk','id') and isinstance(getattr(klass, attr, None), property): - #if attr.endswith('_url') or attr.endswith('_uri'): - # ret[attr] = self.make_absolute(_any(getattr(data, attr))) - #else: - ret[attr] = _any(getattr(data, attr)) - #fields = dir(data.__class__) + ret.keys() - #add_ons = [k for k in dir(data) if k not in fields and not k.startswith('_')] - #print add_ons - ###print dir(data.__class__) - #from django.db.models import Model - #model_fields = dir(Model) - - #for attr in dir(data): - ## #if attr.startswith('_'): - ## # continue - # if (attr in fields) and not (attr in model_fields) and not attr.startswith('_'): - # print attr, type(getattr(data, attr, None)), attr in fields, attr in model_fields - - #for k in add_ons: - # ret[k] = _any(getattr(data, k)) - - # TRC - # resouce uri - #if self.in_typemapper(type(data), self.anonymous): - # handler = self.in_typemapper(type(data), self.anonymous) - # if hasattr(handler, 'resource_uri'): - # url_id, fields = handler.resource_uri() - # ret['resource_uri'] = permalink( lambda: (url_id, - # (getattr(data, f) for f in fields) ) )() - - # TRC - #if hasattr(data, 'get_api_url') and 'resource_uri' not in ret: - # try: ret['resource_uri'] = data.get_api_url() - # except: pass - - # absolute uri - if hasattr(data, 'get_absolute_url') and get_absolute_url: - try: ret['absolute_url'] = data.get_absolute_url() - except: pass - - for key, val in ret.items(): - if key.endswith('_url') or key.endswith('_uri'): - ret[key] = self.add_domain(val) - - return ret - - def _qs(data, fields=()): - """ - Querysets. - """ - return [ _any(v, fields) for v in data ] - - def _list(data): - """ - Lists. - """ - return [ _any(v) for v in data ] - - def _dict(data): - """ - Dictionaries. - """ - return dict([ (k, _any(v)) for k, v in data.iteritems() ]) - - # Kickstart the seralizin'. - return _any(data, self.fields) - - - def create(self, data, headers={}, *args, **kwargs): - # TODO: test creation on a non-existing resource url - all_kw_args = dict(data.items() + kwargs.items()) - instance = self.model(**all_kw_args) - instance.save() - headers = {} - if hasattr(instance, 'get_absolute_url'): - headers['Location'] = self.add_domain(instance.get_absolute_url()) - return (201, instance, headers) - - def read(self, headers={}, *args, **kwargs): - try: - instance = self.model.objects.get(**kwargs) - except self.model.DoesNotExist: - return (404, None, {}) - - return (200, instance, {}) - - def update(self, data, headers={}, *args, **kwargs): - # TODO: update on the url of a non-existing resource url doesn't work correctly at the moment - will end up with a new url - try: - instance = self.model.objects.get(**kwargs) - for (key, val) in data.items(): - setattr(instance, key, val) - except self.model.DoesNotExist: - instance = self.model(**data) - instance.save() - - instance.save() - return (200, instance, {}) - - def delete(self, headers={}, *args, **kwargs): - try: - instance = self.model.objects.get(**kwargs) - except self.model.DoesNotExist: - return (404, None, {}) - - instance.delete() - return (204, None, {}) - - - -class QueryModelResource(ModelResource): - allowed_methods = ('read',) - queryset = None - - def get_bound_form(self, data=None, is_response=False): - return None - - def read(self, headers={}, *args, **kwargs): - if self.queryset: - return (200, self.queryset, {}) - queryset = self.model.objects.all() - return (200, queryset, {}) - diff --git a/src/rest/parsers.py b/src/rest/parsers.py deleted file mode 100644 index ac449e495..000000000 --- a/src/rest/parsers.py +++ /dev/null @@ -1,63 +0,0 @@ -import json -from rest.status import ResourceException, Status - -class BaseParser(object): - def __init__(self, resource): - self.resource = resource - - def parse(self, input): - return {} - - -class JSONParser(BaseParser): - def parse(self, input): - try: - return json.loads(input) - except ValueError, exc: - raise ResourceException(Status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)}) - -class XMLParser(BaseParser): - pass - -class FormParser(BaseParser): - """The default parser for form data. - Return a dict containing a single value for each non-reserved parameter - """ - def __init__(self, resource): - - if resource.request.method == 'PUT': - # Fix from piston to force Django to give PUT requests the same - # form processing that POST requests get... - # - # Bug fix: if _load_post_and_files has already been called, for - # example by middleware accessing request.POST, the below code to - # pretend the request is a POST instead of a PUT will be too late - # to make a difference. Also calling _load_post_and_files will result - # in the following exception: - # AttributeError: You cannot set the upload handlers after the upload has been processed. - # The fix is to check for the presence of the _post field which is set - # the first time _load_post_and_files is called (both by wsgi.py and - # modpython.py). If it's set, the request has to be 'reset' to redo - # the query value parsing in POST mode. - if hasattr(resource.request, '_post'): - del request._post - del request._files - - try: - resource.request.method = "POST" - resource.request._load_post_and_files() - resource.request.method = "PUT" - except AttributeError: - resource.request.META['REQUEST_METHOD'] = 'POST' - resource.request._load_post_and_files() - resource.request.META['REQUEST_METHOD'] = 'PUT' - - # - self.data = {} - for (key, val) in resource.request.POST.items(): - if key not in resource.RESERVED_PARAMS: - self.data[key] = val - - def parse(self, input): - return self.data - diff --git a/src/rest/resource.py b/src/rest/resource.py deleted file mode 100644 index b94854f58..000000000 --- a/src/rest/resource.py +++ /dev/null @@ -1,382 +0,0 @@ -from django.contrib.sites.models import Site -from django.core.urlresolvers import reverse -from django.core.handlers.wsgi import STATUS_CODE_TEXT -from django.http import HttpResponse -from rest import emitters, parsers -from rest.status import Status, ResourceException -from decimal import Decimal -import re - -# TODO: Authentication -# TODO: Display user login in top panel: http://stackoverflow.com/questions/806835/django-redirect-to-previous-page-after-login -# TODO: Return basic object, not tuple of status code, content, headers -# TODO: Take request, not headers -# TODO: Standard exception classes -# TODO: Figure how out references and named urls need to work nicely -# TODO: POST on existing 404 URL, PUT on existing 404 URL -# -# NEXT: Generic content form -# NEXT: Remove self.blah munging (Add a ResponseContext object?) -# NEXT: Caching cleverness -# NEXT: Test non-existent fields on ModelResources -# -# FUTURE: Erroring on read-only fields - -# Documentation, Release - - - -class Resource(object): - # List of RESTful operations which may be performed on this resource. - allowed_operations = ('read',) - anon_allowed_operations = () - - # Optional form for input validation and presentation of HTML formatted responses. - form = None - - # List of content-types the resource can respond with, ordered by preference - emitters = ( ('application/json', emitters.JSONEmitter), - ('text/html', emitters.HTMLEmitter), - ('application/xhtml+xml', emitters.HTMLEmitter), - ('text/plain', emitters.TextEmitter), - ('application/xml', emitters.XMLEmitter), ) - - # List of content-types the resource can read from - parsers = { 'application/json': parsers.JSONParser, - 'application/xml': parsers.XMLParser, - 'application/x-www-form-urlencoded': parsers.FormParser, - 'multipart/form-data': parsers.FormParser } - - # Map standard HTTP methods to RESTful operations - CALLMAP = { 'GET': 'read', 'POST': 'create', - 'PUT': 'update', 'DELETE': 'delete' } - - REVERSE_CALLMAP = dict([(val, key) for (key, val) in CALLMAP.items()]) - - # Some reserved parameters to allow us to use standard HTML forms with our resource - METHOD_PARAM = '_method' # Allow POST overloading - ACCEPT_PARAM = '_accept' # Allow override of Accept header in GET requests - CONTENTTYPE_PARAM = '_contenttype' # Allow override of Content-Type header (allows sending arbitrary content with standard forms) - CONTENT_PARAM = '_content' # Allow override of body content (allows sending arbitrary content with standard forms) - CSRF_PARAM = 'csrfmiddlewaretoken' # Django's CSRF token - - RESERVED_PARAMS = set((METHOD_PARAM, ACCEPT_PARAM, CONTENTTYPE_PARAM, CONTENT_PARAM, CSRF_PARAM)) - - - def __new__(cls, request, *args, **kwargs): - """Make the class callable so it can be used as a Django view.""" - self = object.__new__(cls) - self.__init__() - return self._handle_request(request, *args, **kwargs) - - - def __init__(self): - pass - - - def name(self): - """Provide a name for the resource. - By default this is the class name, with 'CamelCaseNames' converted to 'Camel Case Names'.""" - class_name = self.__class__.__name__ - return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).strip() - - - def description(self): - """Provide a description for the resource. - By default this is the class's docstring with leading line spaces stripped.""" - return re.sub(re.compile('^ +', re.MULTILINE), '', self.__doc__) - - - def available_content_types(self): - """Return a list of strings of all the content-types that this resource can emit.""" - return [item[0] for item in self.emitters] - - - def resp_status_text(self): - """Return reason text corrosponding to our HTTP response status code. - Provided for convienience.""" - return STATUS_CODE_TEXT.get(self.resp_status, '') - - - def reverse(self, view, *args, **kwargs): - """Return a fully qualified URI for a given view or resource. - Add the domain using the Sites framework if possible, otherwise fallback to using the current request.""" - return self.add_domain(reverse(view, *args, **kwargs)) - - - def add_domain(self, path): - """Given a path, return an fully qualified URI. - Use the Sites framework if possible, otherwise fallback to using the domain from the current request.""" - - # Note that out-of-the-box the Sites framework uses the reserved domain 'example.com' - # See RFC 2606 - http://www.faqs.org/rfcs/rfc2606.html - try: - site = Site.objects.get_current() - if site.domain and site.domain != 'example.com': - return 'http://%s%s' % (site.domain, path) - except: - pass - - return self.request.build_absolute_uri(path) - - - def read(self, headers={}, *args, **kwargs): - """RESTful read on the resource, which must be subclassed to be implemented. Should be a safe operation.""" - self.not_implemented('read') - - - def create(self, data=None, headers={}, *args, **kwargs): - """RESTful create on the resource, which must be subclassed to be implemented.""" - self.not_implemented('create') - - - def update(self, data=None, headers={}, *args, **kwargs): - """RESTful update on the resource, which must be subclassed to be implemented. Should be an idempotent operation.""" - self.not_implemented('update') - - - def delete(self, headers={}, *args, **kwargs): - """RESTful delete on the resource, which must be subclassed to be implemented. Should be an idempotent operation.""" - self.not_implemented('delete') - - - def not_implemented(self, operation): - """Return an HTTP 500 server error if an operation is called which has been allowed by - allowed_operations, but which has not been implemented.""" - raise ResourceException(Status.HTTP_500_INTERNAL_SERVER_ERROR, - {'detail': '%s operation on this resource has not been implemented' % (operation, )}) - - - def determine_method(self, request): - """Determine the HTTP method that this request should be treated as. - Allow for PUT and DELETE tunneling via the _method parameter.""" - method = request.method.upper() - - if method == 'POST' and self.METHOD_PARAM and request.POST.has_key(self.METHOD_PARAM): - method = request.POST[self.METHOD_PARAM].upper() - - return method - - - def authenticate(self): - """TODO""" - # user = ... - # if DEBUG and request is from localhost - # if anon_user and not anon_allowed_operations raise PermissionDenied - # return - - - def check_method_allowed(self, method): - """Ensure the request method is acceptable for this resource.""" - if not method in self.CALLMAP.keys(): - raise ResourceException(Status.HTTP_501_NOT_IMPLEMENTED, - {'detail': 'Unknown or unsupported method \'%s\'' % method}) - - if not self.CALLMAP[method] in self.allowed_operations: - raise ResourceException(Status.HTTP_405_METHOD_NOT_ALLOWED, - {'detail': 'Method \'%s\' not allowed on this resource.' % method}) - - - def get_bound_form(self, data=None, is_response=False): - """Optionally return a Django Form instance, which may be used for validation - and/or rendered by an HTML/XHTML emitter. - - If data is not None the form will be bound to data. is_response indicates if data should be - treated as the input data (bind to client input) or the response data (bind to an existing object).""" - if self.form: - if data: - return self.form(data) - else: - return self.form() - return None - - - def cleanup_request(self, data, form_instance): - """Perform any resource-specific data deserialization and/or validation - after the initial HTTP content-type deserialization has taken place. - - Returns a tuple containing the cleaned up data, and optionally a form bound to that data. - - By default this uses form validation to filter the basic input into the required types.""" - if form_instance is None: - return data - - # Default form validation does not check for additional invalid fields - non_existent_fields = [] - for key in set(data.keys()) - set(form_instance.fields.keys()): - non_existent_fields.append(key) - - if not form_instance.is_valid() or non_existent_fields: - if not form_instance.errors and not non_existent_fields: - # If no data was supplied the errors property will be None - details = 'No content was supplied' - - else: - # Add standard field errors - details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems()) - - # Add any non-field errors - if form_instance.non_field_errors(): - details['errors'] = self.form.non_field_errors() - - # Add any non-existent field errors - for key in non_existent_fields: - details[key] = ['This field does not exist'] - - # Bail. Note that we will still serialize this response with the appropriate content type - raise ResourceException(Status.HTTP_400_BAD_REQUEST, {'detail': details}) - - return form_instance.cleaned_data - - - def cleanup_response(self, data): - """Perform any resource-specific data filtering prior to the standard HTTP - content-type serialization. - - Eg filter complex objects that cannot be serialized by json/xml/etc into basic objects that can.""" - return data - - - def determine_parser(self, request): - """Return the appropriate parser for the input, given the client's 'Content-Type' header, - and the content types that this Resource knows how to parse.""" - content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded') - split = content_type.split(';', 1) - if len(split) > 1: - content_type = split[0] - content_type = content_type.strip() - - try: - return self.parsers[content_type] - except KeyError: - raise ResourceException(Status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - {'detail': 'Unsupported media type \'%s\'' % content_type}) - - - def determine_emitter(self, request): - """Return the appropriate emitter for the output, given the client's 'Accept' header, - and the content types that this Resource knows how to serve. - - See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html""" - default = self.emitters[0] - - if self.ACCEPT_PARAM and request.GET.get(self.ACCEPT_PARAM, None): - # Use _accept parameter override - accept_list = [(request.GET.get(self.ACCEPT_PARAM),)] - elif request.META.has_key('HTTP_ACCEPT'): - # Use standard HTTP Accept negotiation - accept_list = [item.split(';') for item in request.META["HTTP_ACCEPT"].split(',')] - else: - # No accept header specified - return default - - # Parse the accept header into a dict of {Priority: List of Mimetypes} - accept_dict = {} - for item in accept_list: - mimetype = item[0].strip() - qvalue = Decimal('1.0') - - if len(item) > 1: - # Parse items that have a qvalue eg text/html;q=0.9 - try: - (q, num) = item[1].split('=') - if q == 'q': - qvalue = Decimal(num) - except: - # Skip malformed entries - continue - - if accept_dict.has_key(qvalue): - accept_dict[qvalue].append(mimetype) - else: - accept_dict[qvalue] = [mimetype] - - # Go through all accepted mimetypes in priority order and return our first match - qvalues = accept_dict.keys() - qvalues.sort(reverse=True) - - for qvalue in qvalues: - for (mimetype, emitter) in self.emitters: - for accept_mimetype in accept_dict[qvalue]: - if ((accept_mimetype == '*/*') or - (accept_mimetype.endswith('/*') and mimetype.startswith(accept_mimetype[:-1])) or - (accept_mimetype == mimetype)): - return (mimetype, emitter) - - raise ResourceException(Status.HTTP_406_NOT_ACCEPTABLE, - {'detail': 'Could not statisfy the client\'s accepted content type', - 'accepted_types': [item[0] for item in self.emitters]}) - - - def _handle_request(self, request, *args, **kwargs): - """ - Broadly this consists of the following procedure: - - 0. ensure the operation is permitted - 1. deserialize request content into request data, using standard HTTP content types (PUT/POST only) - 2. cleanup and validate request data (PUT/POST only) - 3. call the core method to get the response data - 4. cleanup the response data - 5. serialize response data into response content, using standard HTTP content negotiation - """ - emitter = None - method = self.determine_method(request) - - # We make these attributes to allow for a certain amount of munging, - # eg The HTML emitter needs to render this information - self.request = request - self.form_instance = None - self.resp_status = None - self.resp_headers = {} - - try: - # Before we attempt anything else determine what format to emit our response data with. - mimetype, emitter = self.determine_emitter(request) - - # Ensure the requested operation is permitted on this resource - self.check_method_allowed(method) - - # Get the appropriate create/read/update/delete function - func = getattr(self, self.CALLMAP.get(method, '')) - - # Either generate the response data, deserializing and validating any request data - if method in ('PUT', 'POST'): - parser = self.determine_parser(request) - data = parser(self).parse(request.raw_post_data) - self.form_instance = self.get_bound_form(data) - data = self.cleanup_request(data, self.form_instance) - (self.resp_status, ret, self.resp_headers) = func(data, request.META, *args, **kwargs) - - else: - (self.resp_status, ret, self.resp_headers) = func(request.META, *args, **kwargs) - if emitter.uses_forms: - self.form_instance = self.get_bound_form(ret, is_response=True) - - - except ResourceException, exc: - # On exceptions we still serialize the response appropriately - (self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers) - - # Fall back to the default emitter if we failed to perform content negotiation - if emitter is None: - mimetype, emitter = self.emitters[0] - - # Provide an empty bound form if we do not have an existing form and if one is required - if self.form_instance is None and emitter.uses_forms: - self.form_instance = self.get_bound_form() - - - # Always add the allow header - self.resp_headers['Allow'] = ', '.join([self.REVERSE_CALLMAP[operation] for operation in self.allowed_operations]) - - # Serialize the response content - ret = self.cleanup_response(ret) - content = emitter(self).emit(ret) - - # Build the HTTP Response - resp = HttpResponse(content, mimetype=mimetype, status=self.resp_status) - for (key, val) in self.resp_headers.items(): - resp[key] = val - - return resp - diff --git a/src/rest/status.py b/src/rest/status.py deleted file mode 100644 index d1b49d69c..000000000 --- a/src/rest/status.py +++ /dev/null @@ -1,50 +0,0 @@ - -class Status(object): - """Descriptive HTTP status codes, for code readability.""" - HTTP_200_OK = 200 - HTTP_201_CREATED = 201 - HTTP_202_ACCEPTED = 202 - HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 - HTTP_204_NO_CONTENT = 204 - HTTP_205_RESET_CONTENT = 205 - HTTP_206_PARTIAL_CONTENT = 206 - HTTP_400_BAD_REQUEST = 400 - HTTP_401_UNAUTHORIZED = 401 - HTTP_402_PAYMENT_REQUIRED = 402 - HTTP_403_FORBIDDEN = 403 - HTTP_404_NOT_FOUND = 404 - HTTP_405_METHOD_NOT_ALLOWED = 405 - HTTP_406_NOT_ACCEPTABLE = 406 - HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 - HTTP_408_REQUEST_TIMEOUT = 408 - HTTP_409_CONFLICT = 409 - HTTP_410_GONE = 410 - HTTP_411_LENGTH_REQUIRED = 411 - HTTP_412_PRECONDITION_FAILED = 412 - HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 - HTTP_414_REQUEST_URI_TOO_LONG = 414 - HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 - HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 - HTTP_417_EXPECTATION_FAILED = 417 - HTTP_100_CONTINUE = 100 - HTTP_101_SWITCHING_PROTOCOLS = 101 - HTTP_300_MULTIPLE_CHOICES = 300 - HTTP_301_MOVED_PERMANENTLY = 301 - HTTP_302_FOUND = 302 - HTTP_303_SEE_OTHER = 303 - HTTP_304_NOT_MODIFIED = 304 - HTTP_305_USE_PROXY = 305 - HTTP_306_RESERVED = 306 - HTTP_307_TEMPORARY_REDIRECT = 307 - HTTP_500_INTERNAL_SERVER_ERROR = 500 - HTTP_501_NOT_IMPLEMENTED = 501 - HTTP_502_BAD_GATEWAY = 502 - HTTP_503_SERVICE_UNAVAILABLE = 503 - HTTP_504_GATEWAY_TIMEOUT = 504 - HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 - -class ResourceException(Exception): - def __init__(self, status, content='', headers={}): - self.status = status - self.content = content - self.headers = headers diff --git a/src/rest/templates/emitter.html b/src/rest/templates/emitter.html deleted file mode 100644 index 17d53b816..000000000 --- a/src/rest/templates/emitter.html +++ /dev/null @@ -1,93 +0,0 @@ -{% load urlize_quoted_links %}{% load add_query_param %} - - -
- -{{ 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 %} -{{ content|urlize_quoted_links }}{% endautoescape %} - -{% if 'read' in resource.allowed_operations %} -
(?:%s).*?[a-zA-Z].*?
\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL) -trailing_empty_content_re = re.compile(r'(?:(?: |\s|
)*?