from django.core.urlresolvers import set_script_prefix from django.views.decorators.csrf import csrf_exempt from djangorestframework.compat import View from djangorestframework.response import Response, ErrorResponse from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin from djangorestframework import emitters, parsers, authenticators, validators, status # TODO: Figure how out references and named urls need to work nicely # TODO: POST on existing 404 URL, PUT on existing 404 URL # # NEXT: Exceptions on func() -> 500, tracebacks emitted if settings.DEBUG __all__ = ['Resource'] class Resource(RequestMixin, ResponseMixin, AuthMixin, View): """Handles incoming requests and maps them to REST operations, performing authentication, input deserialization, input validation, output serialization.""" # List of RESTful operations which may be performed on this resource. # These are going to get dropped at some point, the allowable methods will be defined simply by # which methods are present on the request (in the same way as Django's generic View) allowed_methods = ('GET',) anon_allowed_methods = () # List of emitters the resource can serialize the response with, ordered by preference. emitters = ( emitters.JSONEmitter, emitters.DocumentingHTMLEmitter, emitters.DocumentingXHTMLEmitter, emitters.DocumentingPlainTextEmitter, emitters.XMLEmitter ) # List of parsers the resource can parse the request with. parsers = ( parsers.JSONParser, parsers.FormParser, parsers.MultipartParser ) # List of validators to validate, cleanup and type-ify the request content validators = ( validators.FormValidator, ) # List of all authenticating methods to attempt. authenticators = ( authenticators.UserLoggedInAuthenticator, authenticators.BasicAuthenticator ) # Optional form for input validation and presentation of HTML formatted responses. form = None # Allow name and description for the Resource to be set explicitly, # overiding the default classname/docstring behaviour. # These are used for documentation in the standard html and text emitters. name = None description = None # Map standard HTTP methods to function calls callmap = { 'GET': 'get', 'POST': 'post', 'PUT': 'put', 'DELETE': 'delete' } def get(self, request, auth, *args, **kwargs): """Must be subclassed to be implemented.""" self.not_implemented('GET') def post(self, request, auth, content, *args, **kwargs): """Must be subclassed to be implemented.""" self.not_implemented('POST') def put(self, request, auth, content, *args, **kwargs): """Must be subclassed to be implemented.""" self.not_implemented('PUT') def delete(self, request, auth, *args, **kwargs): """Must be subclassed to be implemented.""" 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_methods, but which has not been implemented.""" raise ErrorResponse(status.HTTP_500_INTERNAL_SERVER_ERROR, {'detail': '%s operation on this resource has not been implemented' % (operation, )}) def check_method_allowed(self, method, auth): """Ensure the request method is permitted for this resource, raising a ResourceException if it is not.""" if not method in self.callmap.keys(): raise ErrorResponse(status.HTTP_501_NOT_IMPLEMENTED, {'detail': 'Unknown or unsupported method \'%s\'' % method}) if not method in self.allowed_methods: raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED, {'detail': 'Method \'%s\' not allowed on this resource.' % method}) if auth is None and not method in self.anon_allowed_methods: raise ErrorResponse(status.HTTP_403_FORBIDDEN, {'detail': 'You do not have permission to access this resource. ' + 'You may need to login or otherwise authenticate the request.'}) 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. TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into the EmitterMixin and Emitter classes.""" return data # Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt. @csrf_exempt def dispatch(self, request, *args, **kwargs): """This method is the core of Resource, through which all requests are passed. 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 """ self.request = request # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here. prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host()) set_script_prefix(prefix) try: # Authenticate the request, and store any context so that the resource operations can # do more fine grained authentication if required. # # Typically the context will be a user, or None if this is an anonymous request, # but it could potentially be more complex (eg the context of a request key which # has been signed against a particular set of permissions) auth_context = self.auth # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter # self.method, self.content_type, self.CONTENT appropriately. self.perform_form_overloading() # Ensure the requested operation is permitted on this resource self.check_method_allowed(self.method, auth_context) # Get the appropriate create/read/update/delete function func = getattr(self, self.callmap.get(self.method, None)) # Either generate the response data, deserializing and validating any request data # TODO: This is going to change to: func(request, *args, **kwargs) # That'll work out now that we have the lazily evaluated self.CONTENT property. if self.method in ('PUT', 'POST'): response_obj = func(request, auth_context, self.CONTENT, *args, **kwargs) else: response_obj = func(request, auth_context, *args, **kwargs) # Allow return value to be either Response, or an object, or None if isinstance(response_obj, Response): response = response_obj elif response_obj is not None: response = Response(status.HTTP_200_OK, response_obj) else: response = Response(status.HTTP_204_NO_CONTENT) # Pre-serialize filtering (eg filter complex objects into natively serializable types) response.cleaned_content = self.cleanup_response(response.raw_content) except ErrorResponse, exc: response = exc.response except: import traceback traceback.print_exc() # Always add these headers. # # TODO - this isn't actually the correct way to set the vary header, # also it's currently sub-obtimal for HTTP caching - need to sort that out. try: response.headers['Allow'] = ', '.join(self.allowed_methods) response.headers['Vary'] = 'Authenticate, Accept' return self.emit(response) except: import traceback traceback.print_exc()