mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-31 16:07:38 +03:00 
			
		
		
		
	Lots of good form validation and default actions
This commit is contained in:
		
							parent
							
								
									48c7171aa0
								
							
						
					
					
						commit
						f144b769fe
					
				|  | @ -4,11 +4,12 @@ import json | ||||||
| from utils import dict2xml | from utils import dict2xml | ||||||
| 
 | 
 | ||||||
| class BaseEmitter(object): | class BaseEmitter(object): | ||||||
|     def __init__(self, resource, request, status, headers): |     def __init__(self, resource, request, status, headers, form): | ||||||
|         self.request = request |         self.request = request | ||||||
|         self.resource = resource |         self.resource = resource | ||||||
|         self.status = status |         self.status = status | ||||||
|         self.headers = headers |         self.headers = headers | ||||||
|  |         self.form = form | ||||||
| 
 | 
 | ||||||
|     def emit(self, output): |     def emit(self, output): | ||||||
|         return output |         return output | ||||||
|  | @ -26,9 +27,8 @@ class TemplatedEmitter(BaseEmitter): | ||||||
|             'headers': self.headers, |             'headers': self.headers, | ||||||
|             'resource_name': self.resource.__class__.__name__, |             'resource_name': self.resource.__class__.__name__, | ||||||
|             'resource_doc': self.resource.__doc__, |             'resource_doc': self.resource.__doc__, | ||||||
|             'create_form': self.resource.create_form and self.resource.create_form() or None, |             'create_form': self.form, | ||||||
|             'update_form': self.resource.update_form and self.resource.update_form() or None, |             'update_form': self.form, | ||||||
|             'allowed_methods': self.resource.allowed_methods, |  | ||||||
|             'request': self.request, |             'request': self.request, | ||||||
|             'resource': self.resource, |             'resource': self.resource, | ||||||
|         }) |         }) | ||||||
|  |  | ||||||
|  | @ -17,6 +17,44 @@ class XMLParser(BaseParser): | ||||||
|     pass |     pass | ||||||
| 
 | 
 | ||||||
| class FormParser(BaseParser): | class FormParser(BaseParser): | ||||||
|     def parse(self, input): |     """The default parser for form data. | ||||||
|         return self.request.POST |     Return a dict containing a single value for each non-reserved parameter | ||||||
|  |     """ | ||||||
|  |     def __init__(self, resource, request): | ||||||
|  | 
 | ||||||
|  |         if 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(request, '_post'): | ||||||
|  |                 del request._post | ||||||
|  |                 del request._files | ||||||
|  |              | ||||||
|  |             try: | ||||||
|  |                 request.method = "POST" | ||||||
|  |                 request._load_post_and_files() | ||||||
|  |                 request.method = "PUT" | ||||||
|  |             except AttributeError: | ||||||
|  |                 request.META['REQUEST_METHOD'] = 'POST' | ||||||
|  |                 request._load_post_and_files() | ||||||
|  |                 request.META['REQUEST_METHOD'] = 'PUT' | ||||||
|  | 
 | ||||||
|  |         #  | ||||||
|  |         self.data = {} | ||||||
|  |         for (key, val) in request.POST.items(): | ||||||
|  |             if key not in resource.RESERVED_PARAMS: | ||||||
|  |                 self.data[key] = val | ||||||
|  | 
 | ||||||
|  |     def parse(self, input): | ||||||
|  |         return self.data | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| from django.http import HttpResponse | from django.http import HttpResponse | ||||||
| from django.core.urlresolvers import reverse | from django.core.urlresolvers import reverse | ||||||
| from rest import emitters, parsers, utils | from rest import emitters, parsers | ||||||
| from decimal import Decimal | from decimal import Decimal | ||||||
| 
 | 
 | ||||||
| #  | #  | ||||||
|  | @ -20,44 +20,103 @@ class ResourceException(Exception): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Resource(object): | class Resource(object): | ||||||
|  |     # List of RESTful operations which may be performed on this resource. | ||||||
|  |     allowed_operations = ('read',) | ||||||
| 
 | 
 | ||||||
|     allowed_methods = ('GET',) |     # List of content-types the resource can respond with, ordered by preference | ||||||
| 
 |     emitters = ( ('application/json', emitters.JSONEmitter), | ||||||
|     callmap = { 'GET': 'read', 'POST': 'create',  |  | ||||||
|                 'PUT': 'update', 'DELETE': 'delete' } |  | ||||||
| 
 |  | ||||||
|     emitters = [ ('application/json', emitters.JSONEmitter), |  | ||||||
|                  ('text/html', emitters.HTMLEmitter), |                  ('text/html', emitters.HTMLEmitter), | ||||||
|                  ('application/xhtml+xml', emitters.HTMLEmitter), |                  ('application/xhtml+xml', emitters.HTMLEmitter), | ||||||
|                  ('text/plain', emitters.TextEmitter), |                  ('text/plain', emitters.TextEmitter), | ||||||
|                  ('application/xml', emitters.XMLEmitter), ] |                  ('application/xml', emitters.XMLEmitter), ) | ||||||
| 
 | 
 | ||||||
|  |     # List of content-types the resource can read from | ||||||
|     parsers = { 'application/json': parsers.JSONParser, |     parsers = { 'application/json': parsers.JSONParser, | ||||||
|                 'application/xml': parsers.XMLParser, |                 'application/xml': parsers.XMLParser, | ||||||
|                 'application/x-www-form-urlencoded': parsers.FormParser, |                 'application/x-www-form-urlencoded': parsers.FormParser, | ||||||
|                 'multipart/form-data': parsers.FormParser } |                 'multipart/form-data': parsers.FormParser } | ||||||
| 
 | 
 | ||||||
|     create_form = None |     # Optional form for input validation and presentation of HTML formatted responses.  | ||||||
|     update_form = None |     form = None | ||||||
| 
 | 
 | ||||||
|  |     # 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' |     METHOD_PARAM = '_method' | ||||||
|     ACCEPT_PARAM = '_accept' |     ACCEPT_PARAM = '_accept' | ||||||
|  |     CSRF_PARAM = 'csrfmiddlewaretoken' | ||||||
|  |     RESERVED_PARAMS = set((METHOD_PARAM, ACCEPT_PARAM, CSRF_PARAM)) | ||||||
|  | 
 | ||||||
|  |     USE_SITEMAP_FOR_ABSOLUTE_URLS = False | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def __new__(cls, request, *args, **kwargs): |     def __new__(cls, request, *args, **kwargs): | ||||||
|  |         """Make the class callable so it can be used as a Django view.""" | ||||||
|         self = object.__new__(cls) |         self = object.__new__(cls) | ||||||
|         self.__init__() |         self.__init__() | ||||||
|         self._request = request |         self._request = request | ||||||
|  |         try: | ||||||
|             return self._handle_request(request, *args, **kwargs) |             return self._handle_request(request, *args, **kwargs) | ||||||
|  |         except: | ||||||
|  |             import traceback | ||||||
|  |             traceback.print_exc() | ||||||
|  |             raise | ||||||
|  |          | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def _determine_method(self, request): |     def reverse(self, view, *args, **kwargs): | ||||||
|         """Determine the HTTP method that this request should be treated as, |         """Return a fully qualified URI for a given view or resource, using the current request as the base URI. | ||||||
|         allowing for PUT and DELETE tunneling via the _method parameter.""" |         TODO: Add SITEMAP option. | ||||||
|  |          | ||||||
|  |         Provided for convienience.""" | ||||||
|  |         return self._request.build_absolute_uri(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. | ||||||
|  | 
 | ||||||
|  |         Provided for convienience.""" | ||||||
|  |         return self._request.build_absolute_uri(uri) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     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_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 |         method = request.method | ||||||
|          |          | ||||||
|         if method == 'POST' and request.POST.has_key(self.METHOD_PARAM): |         if method == 'POST' and request.POST.has_key(self.METHOD_PARAM): | ||||||
|  | @ -66,17 +125,47 @@ class Resource(object): | ||||||
|         return method |         return method | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def _check_method_allowed(self, method): |     def check_method_allowed(self, method): | ||||||
|         if not method in self.allowed_methods: |         """Ensure the request method is acceptable fot this resource.""" | ||||||
|             raise ResourceException(STATUS_405_METHOD_NOT_ALLOWED, |         if not method in self.CALLMAP.keys(): | ||||||
|                                     {'detail': 'Method \'%s\' not allowed on this resource.' % method}) |  | ||||||
|          |  | ||||||
|         if not method in self.callmap.keys(): |  | ||||||
|             raise ResourceException(STATUS_501_NOT_IMPLEMENTED, |             raise ResourceException(STATUS_501_NOT_IMPLEMENTED, | ||||||
|                                     {'detail': 'Unknown or unsupported method \'%s\'' % method}) |                                     {'detail': 'Unknown or unsupported method \'%s\'' % method}) | ||||||
|              |              | ||||||
|  |         if not self.CALLMAP[method] in self.allowed_operations: | ||||||
|  |             raise ResourceException(STATUS_405_METHOD_NOT_ALLOWED, | ||||||
|  |                                     {'detail': 'Method \'%s\' not allowed on this resource.' % method}) | ||||||
| 
 | 
 | ||||||
|     def _determine_parser(self, request): | 
 | ||||||
|  | 
 | ||||||
|  |     def determine_form(self, data=None): | ||||||
|  |         """Optionally return a Django Form instance, which may be used for validation | ||||||
|  |         and/or rendered by an HTML/XHTML emitter. | ||||||
|  |          | ||||||
|  |         The data argument will be non Null if the form is required to be bound to some deserialized | ||||||
|  |         input data, or Null if the form is required to be unbound.  | ||||||
|  |         """ | ||||||
|  |         if self.form: | ||||||
|  |             return self.form(data) | ||||||
|  |         return None | ||||||
|  |    | ||||||
|  |    | ||||||
|  |     def cleanup_request(self, data, form=None): | ||||||
|  |         """Perform any resource-specific data deserialization and/or validation | ||||||
|  |         after the initial HTTP content-type deserialization has taken place. | ||||||
|  |          | ||||||
|  |         Optionally this may use a Django Form which will have been bound to the data, | ||||||
|  |         rather than using the data directly. | ||||||
|  |         """ | ||||||
|  |         return data | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def cleanup_response(self, data): | ||||||
|  |         """Perform any resource-specific data filtering prior to the standard HTTP | ||||||
|  |         content-type serialization.""" | ||||||
|  |         return data | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def determine_parser(self, request): | ||||||
|         """Return the appropriate parser for the input, given the client's 'Content-Type' header, |         """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.""" |         and the content types that this Resource knows how to parse.""" | ||||||
|         content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded') |         content_type = request.META.get('CONTENT_TYPE', 'application/x-www-form-urlencoded') | ||||||
|  | @ -91,13 +180,12 @@ class Resource(object): | ||||||
|             raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE, |             raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE, | ||||||
|                                     {'detail': 'Unsupported content type \'%s\'' % content_type}) |                                     {'detail': 'Unsupported content type \'%s\'' % content_type}) | ||||||
| 
 | 
 | ||||||
|     def _determine_emitter(self, request): | 
 | ||||||
|  |     def determine_emitter(self, request): | ||||||
|         """Return the appropriate emitter for the output, given the client's 'Accept' header, |         """Return the appropriate emitter for the output, given the client's 'Accept' header, | ||||||
|         and the content types that this Resource knows how to serve. |         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 |         See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html""" | ||||||
|         """ |  | ||||||
| 
 |  | ||||||
|         default = self.emitters[0] |         default = self.emitters[0] | ||||||
| 
 | 
 | ||||||
|         if not request.META.has_key('HTTP_ACCEPT'): |         if not request.META.has_key('HTTP_ACCEPT'): | ||||||
|  | @ -142,60 +230,60 @@ class Resource(object): | ||||||
|                                  'accepted_types': [item[0] for item in self.emitters]}) |                                  'accepted_types': [item[0] for item in self.emitters]}) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def _validate_data(self, method, data): |  | ||||||
|         """If there is an appropriate form to deal with this operation, |  | ||||||
|         then validate the data and return the resulting dictionary. |  | ||||||
|         """ |  | ||||||
|         if method == 'PUT' and self.update_form: |  | ||||||
|             form = self.update_form(data) |  | ||||||
|         elif method == 'POST' and self.create_form: |  | ||||||
|             form = self.create_form(data) |  | ||||||
|         else: |  | ||||||
|             return data |  | ||||||
| 
 |  | ||||||
|         if not form.is_valid(): |  | ||||||
|             raise ResourceException(STATUS_400_BAD_REQUEST, |  | ||||||
|                                     {'detail': dict((k, map(unicode, v)) |  | ||||||
|                                                     for (k,v) in form.errors.iteritems())}) |  | ||||||
| 
 |  | ||||||
|         return form.cleaned_data |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     def _handle_request(self, request, *args, **kwargs): |     def _handle_request(self, request, *args, **kwargs): | ||||||
|  |         """ | ||||||
|          |          | ||||||
|         # Hack to ensure PUT requests get the same form treatment as POST requests |         Broadly this consists of the following procedure: | ||||||
|         utils.coerce_put_post(request) |  | ||||||
| 
 |  | ||||||
|         # Get the request method, allowing for PUT and DELETE tunneling |  | ||||||
|         method = self._determine_method(request) |  | ||||||
| 
 | 
 | ||||||
|  |         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 | ||||||
|  |         """ | ||||||
|  |         method = self.determine_method(request) | ||||||
|  |         emitter = None | ||||||
|  |         form = None | ||||||
|         try: |         try: | ||||||
|             self._check_method_allowed(method) |             # Before we attempt anything else determine what format to emit our response data with. | ||||||
|  |             mimetype, emitter = self.determine_emitter(request) | ||||||
| 
 | 
 | ||||||
|             # Parse the HTTP Request content |             # Ensure the requested operation is permitted on this resource | ||||||
|             func = getattr(self, self.callmap.get(method, '')) |             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'): |             if method in ('PUT', 'POST'): | ||||||
|                 parser = self._determine_parser(request) |                 parser = self.determine_parser(request) | ||||||
|                 data = parser(self, request).parse(request.raw_post_data) |                 data = parser(self, request).parse(request.raw_post_data) | ||||||
|                 data = self._validate_data(method, data) |                 form = self.determine_form(data) | ||||||
|  |                 data = self.cleanup_request(data, form) | ||||||
|                 (status, ret, headers) = func(data, request.META, *args, **kwargs) |                 (status, ret, headers) = func(data, request.META, *args, **kwargs) | ||||||
| 
 | 
 | ||||||
|             else: |             else: | ||||||
|                 (status, ret, headers) = func(request.META, *args, **kwargs) |                 (status, ret, headers) = func(request.META, *args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|         except ResourceException, exc: |         except ResourceException, exc: | ||||||
|             (status, ret, headers) = (exc.status, exc.content, exc.headers) |             (status, ret, headers) = (exc.status, exc.content, exc.headers) | ||||||
| 
 | 
 | ||||||
|         headers['Allow'] = ', '.join(self.allowed_methods) |         # Use a default emitter if request failed without being able to determine an acceptable emitter  | ||||||
|          |         if emitter is None: | ||||||
|         # Serialize the HTTP Response content |  | ||||||
|         try:         |  | ||||||
|             mimetype, emitter = self._determine_emitter(request) |  | ||||||
|         except ResourceException, exc: |  | ||||||
|             (status, ret, headers) = (exc.status, exc.content, exc.headers) |  | ||||||
|             mimetype, emitter = self.emitters[0] |             mimetype, emitter = self.emitters[0] | ||||||
|          |          | ||||||
|         content = emitter(self, request, status, headers).emit(ret) |         # Use a form unbound to any data if one has not yet been created | ||||||
|  |         if form is None: | ||||||
|  |             form = self.determine_form() | ||||||
|  |          | ||||||
|  |         # Always add the allow header | ||||||
|  |         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, request, status, headers, form).emit(ret) | ||||||
| 
 | 
 | ||||||
|         # Build the HTTP Response |         # Build the HTTP Response | ||||||
|         resp = HttpResponse(content, mimetype=mimetype, status=status) |         resp = HttpResponse(content, mimetype=mimetype, status=status) | ||||||
|  | @ -204,24 +292,293 @@ class Resource(object): | ||||||
| 
 | 
 | ||||||
|         return resp |         return resp | ||||||
| 
 | 
 | ||||||
|     def _not_implemented(self, operation): | 
 | ||||||
|         resource_name = self.__class__.__name__ | 
 | ||||||
|         raise ResourceException(STATUS_500_INTERNAL_SERVER_ERROR, | 
 | ||||||
|                                 {'detail': '%s operation on this resource has not been implemented' % (operation, )}) | from django.forms import ModelForm | ||||||
|  | from django.db.models.query import QuerySet | ||||||
|  | from django.db.models import Model | ||||||
|  | import decimal | ||||||
|  | import inspect | ||||||
|  | import re | ||||||
|  | 
 | ||||||
|  | class ModelResource(Resource): | ||||||
|  |     model = None | ||||||
|  |     fields = None | ||||||
|  |     form_fields = None | ||||||
|  | 
 | ||||||
|  |     def determine_form(self, data=None): | ||||||
|  |         """Return a form that may be used in validation and/or rendering an html emitter""" | ||||||
|  |         if self.form: | ||||||
|  |             return self.form | ||||||
|  | 
 | ||||||
|  |         elif self.model: | ||||||
|  |             class NewModelForm(ModelForm): | ||||||
|  |                 class Meta: | ||||||
|  |                     model = self.model | ||||||
|  |                     fields = self.form_fields if self.form_fields else self.fields | ||||||
|  |                      | ||||||
|  |             if data is None: | ||||||
|  |                 return NewModelForm() | ||||||
|  |             else: | ||||||
|  |                 return NewModelForm(data) | ||||||
|  |          | ||||||
|  |         else: | ||||||
|  |             return None | ||||||
|  |      | ||||||
|  |     def cleanup_request(self, data, form=None): | ||||||
|  |         """Filter data into form-cleaned data, performing validation and type coercion.""" | ||||||
|  |         if form is None: | ||||||
|  |             return data | ||||||
|  | 
 | ||||||
|  |         if not form.is_valid(): | ||||||
|  |             details = dict((key, map(unicode, val)) for (key, val) in form.errors.iteritems()) | ||||||
|  |             raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details}) | ||||||
|  | 
 | ||||||
|  |         return form.cleaned_data        | ||||||
|  | 
 | ||||||
|  |     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, 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_uri = 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) | ||||||
|  |                                      | ||||||
|  |                 else: | ||||||
|  |                     get_fields = set(fields) | ||||||
|  | 
 | ||||||
|  |                 if 'absolute_uri' in get_fields:   # MOVED (TRC) | ||||||
|  |                     get_absolute_uri = 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: | ||||||
|  |                 for f in data._meta.fields: | ||||||
|  |                     ret[f.attname] = _any(getattr(data, f.attname)) | ||||||
|  |                  | ||||||
|  |                 fields = dir(data.__class__) + ret.keys() | ||||||
|  |                 add_ons = [k for k in dir(data) if k not in 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_uri: | ||||||
|  |                 try: ret['absolute_uri'] = self.make_absolute(data.get_absolute_url()) | ||||||
|  |                 except: pass | ||||||
|  | 
 | ||||||
|  |             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={}): | ||||||
|  |         instance = self.model(**data) | ||||||
|  |         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): |     def read(self, headers={}, *args, **kwargs): | ||||||
|         self._not_implemented('read') |         instance = self.model.objects.get(**kwargs) | ||||||
|  |         return (200, instance, {}) | ||||||
| 
 | 
 | ||||||
|     def create(self, data=None, headers={}, *args, **kwargs): |     def update(self, data, headers={}, *args, **kwargs): | ||||||
|         self._not_implemented('create') |         instance = self.model.objects.get(**kwargs)     | ||||||
|      |         for (key, val) in data.items(): | ||||||
|     def update(self, data=None, headers={}, *args, **kwargs): |             setattr(instance, key, val) | ||||||
|         self._not_implemented('update') |         instance.save() | ||||||
|  |         return (200, instance, {}) | ||||||
| 
 | 
 | ||||||
|     def delete(self, headers={}, *args, **kwargs): |     def delete(self, headers={}, *args, **kwargs): | ||||||
|         self._not_implemented('delete') |         instance = self.model.objects.get(**kwargs) | ||||||
| 
 |         instance.delete() | ||||||
|     def reverse(self, view, *args, **kwargs): |         return (204, '', {}) | ||||||
|         """Return a fully qualified URI for a view, using the current request as the base URI. |  | ||||||
|         """ |  | ||||||
|         return self._request.build_absolute_uri(reverse(view, *args, **kwargs)) |  | ||||||
|  | @ -12,17 +12,17 @@ | ||||||
|     <h1>{{ resource_name }}</h1> |     <h1>{{ resource_name }}</h1> | ||||||
|     <p>{{ resource_doc }}</p> |     <p>{{ resource_doc }}</p> | ||||||
|     <pre>{% autoescape off %}<b>{{ status }} {{ reason }}</b> |     <pre>{% autoescape off %}<b>{{ status }} {{ reason }}</b> | ||||||
| {% for key, val in headers.items %}<b>{{ key }}:</b> {{ val }} | {% for key, val in headers.items %}<b>{{ key }}:</b> {{ val|urlize_quoted_links }} | ||||||
| {% endfor %} | {% endfor %} | ||||||
| {{ content|urlize_quoted_links }}{% endautoescape %}    </pre> | {{ content|urlize_quoted_links }}{% endautoescape %}    </pre> | ||||||
| 
 | 
 | ||||||
| {% if 'GET' in allowed_methods %} | {% if 'read' in resource.allowed_operations %} | ||||||
| 	<div class='action'> | 	<div class='action'> | ||||||
| 		<a href='{{ request.path }}'>Read</a> | 		<a href='{{ request.path }}'>Read</a> | ||||||
| 	</div> | 	</div> | ||||||
| {% endif %} | {% endif %} | ||||||
| 
 | 
 | ||||||
| {% if 'POST' in resource.allowed_methods %} | {% if 'create' in resource.allowed_operations %} | ||||||
| 	<div class='action'> | 	<div class='action'> | ||||||
| 		<form action="{{ request.path }}" method="POST"> | 		<form action="{{ request.path }}" method="POST"> | ||||||
| 		    {% csrf_token %} | 		    {% csrf_token %} | ||||||
|  | @ -32,7 +32,7 @@ | ||||||
| 	</div> | 	</div> | ||||||
| {% endif %} | {% endif %} | ||||||
| 
 | 
 | ||||||
| {% if 'PUT' in resource.allowed_methods %} | {% if 'update' in resource.allowed_operations %} | ||||||
| 	<div class='action'> | 	<div class='action'> | ||||||
| 		<form action="{{ request.path }}" method="POST"> | 		<form action="{{ request.path }}" method="POST"> | ||||||
| 			<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" /> | 			<input type="hidden" name="{{ resource.METHOD_PARAM}}" value="PUT" /> | ||||||
|  | @ -43,7 +43,7 @@ | ||||||
| 	</div> | 	</div> | ||||||
| {% endif %} | {% endif %} | ||||||
| 
 | 
 | ||||||
| {% if 'DELETE' in resource.allowed_methods %} | {% if 'delete' in resource.allowed_operations %} | ||||||
| 	<div class='action'> | 	<div class='action'> | ||||||
| 		<form action="{{ request.path }}" method="POST"> | 		<form action="{{ request.path }}" method="POST"> | ||||||
| 		    {% csrf_token %} | 		    {% csrf_token %} | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -1,3 +1,31 @@ | ||||||
| from django.db import models | from django.db import models | ||||||
|  | import uuid | ||||||
| 
 | 
 | ||||||
| # Create your models here. | def uuid_str(): | ||||||
|  |     return str(uuid.uuid1()) | ||||||
|  | 
 | ||||||
|  | class ExampleModel(models.Model): | ||||||
|  |     num = models.IntegerField(default=2, choices=((1,'one'), (2, 'two'))) | ||||||
|  |     hidden_num = models.IntegerField(verbose_name='Something', help_text='HELP') | ||||||
|  |     text = models.TextField(blank=False) | ||||||
|  |     another = models.CharField(max_length=10) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ExampleContainer(models.Model): | ||||||
|  |     """Container.  Has a key, a name, and some internal data, and contains a set of items.""" | ||||||
|  |     key = models.CharField(primary_key=True, default=uuid_str, max_length=36, editable=False) | ||||||
|  |     name = models.CharField(max_length=256) | ||||||
|  |     internal = models.IntegerField(default=0) | ||||||
|  | 
 | ||||||
|  |     @models.permalink | ||||||
|  |     def get_absolute_url(self): | ||||||
|  |         return ('testapp.views.ContainerInstance', [self.key]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ExampleItem(models.Model): | ||||||
|  |     """Item.  Belongs to a container and has an index number and a note. | ||||||
|  |     Items are uniquely identified by their container and index number.""" | ||||||
|  |     container = models.ForeignKey(ExampleContainer, related_name='items') | ||||||
|  |     index = models.IntegerField() | ||||||
|  |     note = models.CharField(max_length=1024) | ||||||
|  |     unique_together = (container, index) | ||||||
|  | @ -9,7 +9,8 @@ from django.test import TestCase | ||||||
| from django.core.urlresolvers import reverse | from django.core.urlresolvers import reverse | ||||||
| from testapp import views | from testapp import views | ||||||
| import json | import json | ||||||
| from rest.utils import xml2dict, dict2xml | #from rest.utils import xml2dict, dict2xml | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class AcceptHeaderTests(TestCase): | class AcceptHeaderTests(TestCase): | ||||||
|     def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True): |     def assert_accept_mimetype(self, mimetype, expect=None, expect_match=True): | ||||||
|  | @ -46,6 +47,10 @@ class AcceptHeaderTests(TestCase): | ||||||
|         resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT='invalid/invalid') |         resp = self.client.get(reverse(views.ReadOnlyResource), HTTP_ACCEPT='invalid/invalid') | ||||||
|         self.assertEquals(resp.status_code, 406) |         self.assertEquals(resp.status_code, 406) | ||||||
|      |      | ||||||
|  |     def test_prefer_specific(self): | ||||||
|  |         self.fail("Test not implemented") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class AllowedMethodsTests(TestCase): | class AllowedMethodsTests(TestCase): | ||||||
|     def test_reading_read_only_allowed(self): |     def test_reading_read_only_allowed(self): | ||||||
|         resp = self.client.get(reverse(views.ReadOnlyResource)) |         resp = self.client.get(reverse(views.ReadOnlyResource)) | ||||||
|  | @ -63,6 +68,7 @@ class AllowedMethodsTests(TestCase): | ||||||
|         resp = self.client.put(reverse(views.WriteOnlyResource), {}) |         resp = self.client.put(reverse(views.WriteOnlyResource), {}) | ||||||
|         self.assertEquals(resp.status_code, 200) |         self.assertEquals(resp.status_code, 200) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class EncodeDecodeTests(TestCase): | class EncodeDecodeTests(TestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super(self.__class__, self).setUp() |         super(self.__class__, self).setUp() | ||||||
|  | @ -70,36 +76,71 @@ class EncodeDecodeTests(TestCase): | ||||||
| 
 | 
 | ||||||
|     def test_encode_form_decode_json(self): |     def test_encode_form_decode_json(self): | ||||||
|         content = self.input |         content = self.input | ||||||
|         resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/json') |         resp = self.client.put(reverse(views.WriteOnlyResource), content) | ||||||
|         output = json.loads(resp.content) |         output = json.loads(resp.content) | ||||||
|         self.assertEquals(self.input, output) |         self.assertEquals(self.input, output) | ||||||
| 
 | 
 | ||||||
|     def test_encode_json_decode_json(self): |     def test_encode_json_decode_json(self): | ||||||
|         content = json.dumps(self.input) |         content = json.dumps(self.input) | ||||||
|         resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json') |         resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json') | ||||||
|         output = json.loads(resp.content) |         output = json.loads(resp.content) | ||||||
|         self.assertEquals(self.input, output) |         self.assertEquals(self.input, output) | ||||||
| 
 | 
 | ||||||
|     def test_encode_xml_decode_json(self): |     #def test_encode_xml_decode_json(self): | ||||||
|         content = dict2xml(self.input) |     #    content = dict2xml(self.input) | ||||||
|         resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json') |     #    resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/json') | ||||||
|  |     #    output = json.loads(resp.content) | ||||||
|  |     #    self.assertEquals(self.input, output) | ||||||
|  | 
 | ||||||
|  |     #def test_encode_form_decode_xml(self): | ||||||
|  |     #    content = self.input | ||||||
|  |     #    resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml') | ||||||
|  |     #    output = xml2dict(resp.content) | ||||||
|  |     #    self.assertEquals(self.input, output) | ||||||
|  | 
 | ||||||
|  |     #def test_encode_json_decode_xml(self): | ||||||
|  |     #    content = json.dumps(self.input) | ||||||
|  |     #    resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') | ||||||
|  |     #    output = xml2dict(resp.content) | ||||||
|  |     #    self.assertEquals(self.input, output) | ||||||
|  | 
 | ||||||
|  |     #def test_encode_xml_decode_xml(self): | ||||||
|  |     #    content = dict2xml(self.input) | ||||||
|  |     #    resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') | ||||||
|  |     #    output = xml2dict(resp.content) | ||||||
|  |     #    self.assertEquals(self.input, output) | ||||||
|  | 
 | ||||||
|  | class ModelTests(TestCase): | ||||||
|  |     def test_create_container(self): | ||||||
|  |         content = json.dumps({'name': 'example'}) | ||||||
|  |         resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json') | ||||||
|         output = json.loads(resp.content) |         output = json.loads(resp.content) | ||||||
|         self.assertEquals(self.input, output) |         self.assertEquals(resp.status_code, 201) | ||||||
|  |         self.assertEquals(output['name'], 'example') | ||||||
|  |         self.assertEquals(set(output.keys()), set(('absolute_uri', 'name', 'key'))) | ||||||
| 
 | 
 | ||||||
|     def test_encode_form_decode_xml(self): | class CreatedModelTests(TestCase): | ||||||
|         content = self.input |     def setUp(self): | ||||||
|         resp = self.client.put(reverse(views.WriteOnlyResource), content, HTTP_ACCEPT='application/xml') |         content = json.dumps({'name': 'example'}) | ||||||
|         output = xml2dict(resp.content) |         resp = self.client.post(reverse(views.ContainerFactory), content, 'application/json', HTTP_ACCEPT='application/json') | ||||||
|         self.assertEquals(self.input, output) |         self.container = json.loads(resp.content) | ||||||
| 
 | 
 | ||||||
|     def test_encode_json_decode_xml(self): |     def test_read_container(self): | ||||||
|         content = json.dumps(self.input) |         resp = self.client.get(self.container["absolute_uri"]) | ||||||
|         resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') |         self.assertEquals(resp.status_code, 200) | ||||||
|         output = xml2dict(resp.content) |         container = json.loads(resp.content) | ||||||
|         self.assertEquals(self.input, output) |         self.assertEquals(container, self.container) | ||||||
|  | 
 | ||||||
|  |     def test_delete_container(self): | ||||||
|  |         resp = self.client.delete(self.container["absolute_uri"]) | ||||||
|  |         self.assertEquals(resp.status_code, 204) | ||||||
|  |         self.assertEquals(resp.content, '') | ||||||
|  | 
 | ||||||
|  |     def test_update_container(self): | ||||||
|  |         self.container['name'] = 'new' | ||||||
|  |         content = json.dumps(self.container) | ||||||
|  |         resp = self.client.put(self.container["absolute_uri"], content, 'application/json') | ||||||
|  |         self.assertEquals(resp.status_code, 200) | ||||||
|  |         container = json.loads(resp.content) | ||||||
|  |         self.assertEquals(container, self.container) | ||||||
|          |          | ||||||
|     def test_encode_xml_decode_xml(self): |  | ||||||
|         content = dict2xml(self.input) |  | ||||||
|         resp = self.client.put(reverse(views.WriteOnlyResource), content, 'application/json', HTTP_ACCEPT='application/xml') |  | ||||||
|         output = xml2dict(resp.content) |  | ||||||
|         self.assertEquals(self.input, output) |  | ||||||
|  | @ -5,4 +5,7 @@ urlpatterns = patterns('testapp.views', | ||||||
|     (r'^read-only$', 'ReadOnlyResource'), |     (r'^read-only$', 'ReadOnlyResource'), | ||||||
|     (r'^write-only$', 'WriteOnlyResource'), |     (r'^write-only$', 'WriteOnlyResource'), | ||||||
|     (r'^read-write$', 'ReadWriteResource'), |     (r'^read-write$', 'ReadWriteResource'), | ||||||
|  |     (r'^model$', 'ModelFormResource'), | ||||||
|  |     (r'^container$', 'ContainerFactory'), | ||||||
|  |     (r'^container/((?P<key>[^/]+))$', 'ContainerInstance'), | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -1,21 +1,24 @@ | ||||||
| from rest.resource import Resource | from rest.resource import Resource, ModelResource | ||||||
| from testapp.forms import ExampleForm | from testapp.forms import ExampleForm | ||||||
|  | from testapp.models import ExampleModel, ExampleContainer | ||||||
|   |   | ||||||
| class RootResource(Resource): | class RootResource(Resource): | ||||||
|     """This is my docstring |     """This is my docstring | ||||||
|     """ |     """ | ||||||
|     allowed_methods = ('GET',) |     allowed_operations = ('read',) | ||||||
| 
 | 
 | ||||||
|     def read(self, headers={}, *args, **kwargs): |     def read(self, headers={}, *args, **kwargs): | ||||||
|         return (200, {'read-only-api': self.reverse(ReadOnlyResource), |         return (200, {'read-only-api': self.reverse(ReadOnlyResource), | ||||||
|                       'write-only-api': self.reverse(WriteOnlyResource), |                       'write-only-api': self.reverse(WriteOnlyResource), | ||||||
|                       'read-write-api': self.reverse(ReadWriteResource)}, {}) |                       'read-write-api': self.reverse(ReadWriteResource), | ||||||
|  |                       'model-api': self.reverse(ModelFormResource), | ||||||
|  |                       'create-container': self.reverse(ContainerFactory)}, {}) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ReadOnlyResource(Resource): | class ReadOnlyResource(Resource): | ||||||
|     """This is my docstring |     """This is my docstring | ||||||
|     """ |     """ | ||||||
|     allowed_methods = ('GET',) |     allowed_operations = ('read',) | ||||||
| 
 | 
 | ||||||
|     def read(self, headers={}, *args, **kwargs): |     def read(self, headers={}, *args, **kwargs): | ||||||
|         return (200, {'ExampleString': 'Example', |         return (200, {'ExampleString': 'Example', | ||||||
|  | @ -26,13 +29,35 @@ class ReadOnlyResource(Resource): | ||||||
| class WriteOnlyResource(Resource): | class WriteOnlyResource(Resource): | ||||||
|     """This is my docstring |     """This is my docstring | ||||||
|     """ |     """ | ||||||
|     allowed_methods = ('PUT',) |     allowed_operations = ('update',) | ||||||
| 
 | 
 | ||||||
|     def update(self, data, headers={}, *args, **kwargs): |     def update(self, data, headers={}, *args, **kwargs): | ||||||
|         return (200, data, {}) |         return (200, data, {}) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ReadWriteResource(Resource): | class ReadWriteResource(Resource): | ||||||
|     allowed_methods = ('GET', 'PUT', 'DELETE') |     allowed_operations = ('read', 'update', 'delete') | ||||||
|     create_form = ExampleForm |     create_form = ExampleForm | ||||||
|     update_form = ExampleForm |     update_form = ExampleForm | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ModelFormResource(ModelResource): | ||||||
|  |     allowed_operations = ('read', 'update', 'delete') | ||||||
|  |     model = ExampleModel | ||||||
|  | 
 | ||||||
|  | # Nice things: form validation is applied to any input type | ||||||
|  | #              html forms for output | ||||||
|  | #              output always serialized nicely | ||||||
|  | class ContainerFactory(ModelResource): | ||||||
|  |     allowed_operations = ('create',) | ||||||
|  |     model = ExampleContainer | ||||||
|  |     fields = ('absolute_uri', 'name', 'key') | ||||||
|  |     form_fields = ('name',) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ContainerInstance(ModelResource): | ||||||
|  |     allowed_operations = ('read', 'update', 'delete') | ||||||
|  |     model = ExampleContainer | ||||||
|  |     fields = ('absolute_uri', 'name', 'key') | ||||||
|  |     form_fields = ('name',) | ||||||
|  | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user