From d373b3a067796b8e181be9368fa24e89c572c45e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 May 2011 09:21:17 +0100 Subject: [PATCH] Decouple views and resources --- djangorestframework/authentication.py | 6 +- djangorestframework/mixins.py | 78 ++-- djangorestframework/modelresource.py | 356 ------------------- djangorestframework/permissions.py | 6 + djangorestframework/renderers.py | 2 +- djangorestframework/resource.py | 353 ++++++++++++------ djangorestframework/response.py | 2 +- djangorestframework/templates/api_login.html | 2 +- djangorestframework/tests/accept.py | 10 +- djangorestframework/tests/authentication.py | 6 +- djangorestframework/tests/breadcrumbs.py | 12 +- djangorestframework/tests/description.py | 28 +- djangorestframework/tests/files.py | 6 +- djangorestframework/tests/parsers.py | 22 +- djangorestframework/tests/reverse.py | 8 +- djangorestframework/tests/throttling.py | 6 +- djangorestframework/tests/validators.py | 29 +- djangorestframework/tests/views.py | 2 +- djangorestframework/urls.py | 16 + djangorestframework/utils/staticviews.py | 65 ++++ djangorestframework/validators.py | 16 +- djangorestframework/views.py | 207 +++++++---- examples/modelresourceexample/views.py | 6 +- examples/urls.py | 15 +- 24 files changed, 582 insertions(+), 677 deletions(-) delete mode 100644 djangorestframework/modelresource.py create mode 100644 djangorestframework/urls.py create mode 100644 djangorestframework/utils/staticviews.py diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index 894b34fca..9dd5c958d 100644 --- a/djangorestframework/authentication.py +++ b/djangorestframework/authentication.py @@ -1,6 +1,6 @@ """The :mod:`authentication` modules provides for pluggable authentication behaviour. -Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.Resource` or Django :class:`View` class. +Authentication behaviour is provided by adding the mixin class :class:`AuthenticatorMixin` to a :class:`.BaseView` or Django :class:`View` class. The set of authentication which are use is then specified by setting the :attr:`authentication` attribute on the class, and listing a set of authentication classes. """ @@ -25,10 +25,10 @@ class BaseAuthenticator(object): be some more complicated token, for example authentication tokens which are signed against a particular set of permissions for a given user, over a given timeframe. - The default permission checking on Resource will use the allowed_methods attribute + The default permission checking on View will use the allowed_methods attribute for permissions if the authentication context is not None, and use anon_allowed_methods otherwise. - The authentication context is available to the method calls eg Resource.get(request) + The authentication context is available to the method calls eg View.get(request) by accessing self.auth in order to allow them to apply any more fine grained permission checking at the point the response is being generated. diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 6bd83bfac..467ce0e0d 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -1,3 +1,4 @@ +"""""" from djangorestframework.utils.mediatypes import MediaType from djangorestframework.utils import as_tuple, MSIE_USER_AGENT_REGEX from djangorestframework.response import ErrorResponse @@ -12,6 +13,14 @@ from decimal import Decimal import re +__all__ = ['RequestMixin', + 'ResponseMixin', + 'AuthMixin', + 'ReadModelMixin', + 'CreateModelMixin', + 'UpdateModelMixin', + 'DeleteModelMixin', + 'ListModelMixin'] ########## Request Mixin ########## @@ -250,7 +259,7 @@ class RequestMixin(object): ########## ResponseMixin ########## class ResponseMixin(object): - """Adds behaviour for pluggable Renderers to a :class:`.Resource` or Django :class:`View`. class. + """Adds behaviour for pluggable Renderers to a :class:`.BaseView` or Django :class:`View`. class. Default behaviour is to use standard HTTP Accept header content negotiation. Also supports overidding the content type by specifying an _accept= parameter in the URL. @@ -259,32 +268,8 @@ class ResponseMixin(object): ACCEPT_QUERY_PARAM = '_accept' # Allow override of Accept header in URL query params REWRITE_IE_ACCEPT_HEADER = True - #request = None - #response = None renderers = () - #def render_to_response(self, obj): - # if isinstance(obj, Response): - # response = obj - # elif response_obj is not None: - # response = Response(status.HTTP_200_OK, obj) - # else: - # response = Response(status.HTTP_204_NO_CONTENT) - - # response.cleaned_content = self._filter(response.raw_content) - - # self._render(response) - - - #def filter(self, content): - # """ - # Filter the response content. - # """ - # for filterer_cls in self.filterers: - # filterer = filterer_cls(self) - # content = filterer.filter(content) - # return content - def render(self, response): """Takes a :class:`Response` object and returns a Django :class:`HttpResponse`.""" @@ -318,7 +303,7 @@ class ResponseMixin(object): def _determine_renderer(self, request): """Return the appropriate renderer 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 mixin knows how to serve. See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html""" @@ -415,17 +400,6 @@ class AuthMixin(object): return auth return None - # TODO? - #@property - #def user(self): - # if not has_attr(self, '_user'): - # auth = self.auth - # if isinstance(auth, User...): - # self._user = auth - # else: - # self._user = getattr(auth, 'user', None) - # return self._user - def check_permissions(self): if not self.permissions: return @@ -443,14 +417,15 @@ class AuthMixin(object): class ReadModelMixin(object): """Behaviour to read a model instance on GET requests""" def get(self, request, *args, **kwargs): + model = self.resource.model try: if args: # If we have any none kwargs then assume the last represents the primrary key - instance = self.model.objects.get(pk=args[-1], **kwargs) + instance = model.objects.get(pk=args[-1], **kwargs) else: # Otherwise assume the kwargs uniquely identify the model - instance = self.model.objects.get(**kwargs) - except self.model.DoesNotExist: + instance = model.objects.get(**kwargs) + except model.DoesNotExist: raise ErrorResponse(status.HTTP_404_NOT_FOUND) return instance @@ -459,17 +434,18 @@ class ReadModelMixin(object): class CreateModelMixin(object): """Behaviour to create a model instance on POST requests""" def post(self, request, *args, **kwargs): + model = self.resource.model # translated 'related_field' kwargs into 'related_field_id' - for related_name in [field.name for field in self.model._meta.fields if isinstance(field, RelatedField)]: + for related_name in [field.name for field in model._meta.fields if isinstance(field, RelatedField)]: if kwargs.has_key(related_name): kwargs[related_name + '_id'] = kwargs[related_name] del kwargs[related_name] all_kw_args = dict(self.CONTENT.items() + kwargs.items()) if args: - instance = self.model(pk=args[-1], **all_kw_args) + instance = model(pk=args[-1], **all_kw_args) else: - instance = self.model(**all_kw_args) + instance = model(**all_kw_args) instance.save() headers = {} if hasattr(instance, 'get_absolute_url'): @@ -480,19 +456,20 @@ class CreateModelMixin(object): class UpdateModelMixin(object): """Behaviour to update a model instance on PUT requests""" def put(self, request, *args, **kwargs): + model = self.resource.model # 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: if args: # If we have any none kwargs then assume the last represents the primrary key - instance = self.model.objects.get(pk=args[-1], **kwargs) + instance = model.objects.get(pk=args[-1], **kwargs) else: # Otherwise assume the kwargs uniquely identify the model - instance = self.model.objects.get(**kwargs) + instance = model.objects.get(**kwargs) for (key, val) in self.CONTENT.items(): setattr(instance, key, val) - except self.model.DoesNotExist: - instance = self.model(**self.CONTENT) + except model.DoesNotExist: + instance = model(**self.CONTENT) instance.save() instance.save() @@ -502,14 +479,15 @@ class UpdateModelMixin(object): class DeleteModelMixin(object): """Behaviour to delete a model instance on DELETE requests""" def delete(self, request, *args, **kwargs): + model = self.resource.model try: if args: # If we have any none kwargs then assume the last represents the primrary key - instance = self.model.objects.get(pk=args[-1], **kwargs) + instance = model.objects.get(pk=args[-1], **kwargs) else: # Otherwise assume the kwargs uniquely identify the model - instance = self.model.objects.get(**kwargs) - except self.model.DoesNotExist: + instance = model.objects.get(**kwargs) + except model.DoesNotExist: raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {}) instance.delete() diff --git a/djangorestframework/modelresource.py b/djangorestframework/modelresource.py deleted file mode 100644 index c286e5866..000000000 --- a/djangorestframework/modelresource.py +++ /dev/null @@ -1,356 +0,0 @@ -from django.forms import ModelForm -from django.db.models import Model -from django.db.models.query import QuerySet -from django.db.models.fields.related import RelatedField - -from djangorestframework.response import Response, ErrorResponse -from djangorestframework.resource import Resource -from djangorestframework import status, validators - -import decimal -import inspect -import re - - -class ModelResource(Resource): - """A specialized type of Resource, for 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.""" - - # List of validators to validate, cleanup and type-ify the request content - validators = (validators.ModelFormValidator,) - - # 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_form(self, content=None): - # """Return a form that may be used in validation and/or rendering an html renderer""" - # if self.form: - # return super(self.__class__, self).get_form(content) - # - # elif self.model: - # - # class NewModelForm(ModelForm): - # class Meta: - # model = self.model - # fields = self.form_fields if self.form_fields else None - # - # if content and isinstance(content, Model): - # return NewModelForm(instance=content) - # elif content: - # return NewModelForm(content) - # - # return NewModelForm() - # - # 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, '__rendertable__'): - f = thing.__rendertable__ - if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1: - ret = _any(f()) - else: - ret = unicode(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) - - - - -class InstanceModelResource(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelResource): - """A view which provides default operations for read/update/delete against a model instance.""" - pass - -class ListOrCreateModelResource(CreateModelMixin, ListModelMixin, ModelResource): - """A Resource which provides default operations for list and create.""" - pass - -class ListModelResource(ListModelMixin, ModelResource): - """Resource with default operations for list.""" - pass \ No newline at end of file diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py index 98d4b0be3..d98651e0c 100644 --- a/djangorestframework/permissions.py +++ b/djangorestframework/permissions.py @@ -11,6 +11,12 @@ class BasePermission(object): def has_permission(self, auth): return True + +class FullAnonAccess(BasePermission): + """""" + def has_permission(self, auth): + return True + class IsAuthenticated(BasePermission): """""" def has_permission(self, auth): diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py index 2a07894eb..9e4e20533 100644 --- a/djangorestframework/renderers.py +++ b/djangorestframework/renderers.py @@ -1,4 +1,4 @@ -"""Renderers are used to serialize a Resource's output into specific media types. +"""Renderers are used to serialize a View's output into specific media types. django-rest-framework also provides HTML and PlainText renderers that help self-document the API, by serializing the output along with documentation regarding the Resource, output status and headers, and providing forms and links depending on the allowed methods, renderers and parsers on the Resource. diff --git a/djangorestframework/resource.py b/djangorestframework/resource.py index e06873ae7..044424987 100644 --- a/djangorestframework/resource.py +++ b/djangorestframework/resource.py @@ -1,130 +1,251 @@ -from django.core.urlresolvers import set_script_prefix -from django.views.decorators.csrf import csrf_exempt +from django.db.models import Model +from django.db.models.query import QuerySet +from django.db.models.fields.related import RelatedField -from djangorestframework.compat import View -from djangorestframework.response import Response, ErrorResponse -from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin -from djangorestframework import renderers, parsers, authentication, permissions, validators, status +import decimal +import inspect +import re -# 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 renderted if settings.DEBUG - -__all__ = ['Resource'] - - -class Resource(RequestMixin, ResponseMixin, AuthMixin, View): - """Handles incoming requests and maps them to REST operations. - Performs request deserialization, response serialization, authentication and input validation.""" - - # List of renderers the resource can serialize the response with, ordered by preference. - renderers = ( renderers.JSONRenderer, - renderers.DocumentingHTMLRenderer, - renderers.DocumentingXHTMLRenderer, - renderers.DocumentingPlainTextRenderer, - renderers.XMLRenderer ) - - # List of parsers the resource can parse the request with. - parsers = ( parsers.JSONParser, - parsers.FormParser, - parsers.MultipartParser ) - - # List of validators to validate, cleanup and normalize the request content - validators = ( validators.FormValidator, ) - - # List of all authenticating methods to attempt. - authentication = ( authentication.UserLoggedInAuthenticator, - authentication.BasicAuthenticator ) +class Resource(object): + """A Resource determines how an object maps to a serializable entity. + Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.""" - # List of all permissions required to access the resource - permissions = () + # 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 - # Optional form for input validation and presentation of HTML formatted responses. - form = None + @classmethod + def object_to_serializable(self, data): + """A (horrible) munging of Piston's pre-serialization. Returns a dict""" - # 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 renderers. - name = None - description = None + 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, '__rendertable__'): + f = thing.__rendertable__ + if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1: + ret = _any(f()) + else: + ret = unicode(thing) # TRC TODO: Change this back! - @property - def allowed_methods(self): - return [method.upper() for method in self.http_method_names if hasattr(self, method)] + return ret - def http_method_not_allowed(self, request, *args, **kwargs): - """Return an HTTP 405 error if an operation is called which does not have a handler method.""" - raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED, - {'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) - - - 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. + 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() ] - TODO: This is going to be removed. I think that the 'fields' behaviour is going to move into - the RendererMixin and Renderer classes.""" - return data + 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 - # Note: session based authentication is explicitly CSRF validated, - # all other authentication is CSRF exempt. - @csrf_exempt - def dispatch(self, request, *args, **kwargs): - try: - self.request = request - self.args = args - self.kwargs = kwargs - - # 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: - # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter - # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately. - self.perform_form_overloading() - - # Authenticate and check request is has the relevant permissions - self.check_permissions() - - # Get the appropriate handler method - if self.method.lower() in self.http_method_names: - handler = getattr(self, self.method.lower(), self.http_method_not_allowed) - else: - handler = self.http_method_not_allowed - - response_obj = handler(request, *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 - - # 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. - response.headers['Allow'] = ', '.join(self.allowed_methods) - response.headers['Vary'] = 'Authenticate, Accept' - - return self.render(response) - except: - import traceback - traceback.print_exc() + 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 fields: + v = lambda f: getattr(data, f.attname) + 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) diff --git a/djangorestframework/response.py b/djangorestframework/response.py index 545a58343..9b3c5851b 100644 --- a/djangorestframework/response.py +++ b/djangorestframework/response.py @@ -21,6 +21,6 @@ class Response(object): class ErrorResponse(BaseException): - """An exception representing an HttpResponse that should be returned immediatley.""" + """An exception representing an HttpResponse that should be returned immediately.""" def __init__(self, status, content=None, headers={}): self.response = Response(status, content=content, headers=headers) diff --git a/djangorestframework/templates/api_login.html b/djangorestframework/templates/api_login.html index ef383a0b5..9d06e8510 100644 --- a/djangorestframework/templates/api_login.html +++ b/djangorestframework/templates/api_login.html @@ -18,7 +18,7 @@
-
+ {% csrf_token %}
{{ form.username }} diff --git a/djangorestframework/tests/accept.py b/djangorestframework/tests/accept.py index b12dc7574..c5a3f69e8 100644 --- a/djangorestframework/tests/accept.py +++ b/djangorestframework/tests/accept.py @@ -1,6 +1,6 @@ from django.test import TestCase from djangorestframework.compat import RequestFactory -from djangorestframework.resource import Resource +from djangorestframework.views import BaseView # See: http://www.useragentstring.com/ @@ -19,15 +19,15 @@ class UserAgentMungingTest(TestCase): def setUp(self): - class MockResource(Resource): + class MockView(BaseView): permissions = () def get(self, request): return {'a':1, 'b':2, 'c':3} self.req = RequestFactory() - self.MockResource = MockResource - self.view = MockResource.as_view() + self.MockView = MockView + self.view = MockView.as_view() def test_munge_msie_accept_header(self): """Send MSIE user agent strings and ensure that we get an HTML response, @@ -42,7 +42,7 @@ class UserAgentMungingTest(TestCase): def test_dont_rewrite_msie_accept_header(self): """Turn off REWRITE_IE_ACCEPT_HEADER, send MSIE user agent strings and ensure that we get a JSON response if we set a */* accept header.""" - view = self.MockResource.as_view(REWRITE_IE_ACCEPT_HEADER=False) + view = self.MockView.as_view(REWRITE_IE_ACCEPT_HEADER=False) for user_agent in (MSIE_9_USER_AGENT, MSIE_8_USER_AGENT, diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py index 248bd87a6..04ac471ad 100644 --- a/djangorestframework/tests/authentication.py +++ b/djangorestframework/tests/authentication.py @@ -6,19 +6,19 @@ from django.test import Client, TestCase from django.utils import simplejson as json from djangorestframework.compat import RequestFactory -from djangorestframework.resource import Resource +from djangorestframework.views import BaseView from djangorestframework import permissions import base64 -class MockResource(Resource): +class MockView(BaseView): permissions = ( permissions.IsAuthenticated, ) def post(self, request): return {'a':1, 'b':2, 'c':3} urlpatterns = patterns('', - (r'^$', MockResource.as_view()), + (r'^$', MockView.as_view()), ) diff --git a/djangorestframework/tests/breadcrumbs.py b/djangorestframework/tests/breadcrumbs.py index 2f9a7e9d2..1fd756347 100644 --- a/djangorestframework/tests/breadcrumbs.py +++ b/djangorestframework/tests/breadcrumbs.py @@ -1,21 +1,21 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from djangorestframework.utils.breadcrumbs import get_breadcrumbs -from djangorestframework.resource import Resource +from djangorestframework.views import BaseView -class Root(Resource): +class Root(BaseView): pass -class ResourceRoot(Resource): +class ResourceRoot(BaseView): pass -class ResourceInstance(Resource): +class ResourceInstance(BaseView): pass -class NestedResourceRoot(Resource): +class NestedResourceRoot(BaseView): pass -class NestedResourceInstance(Resource): +class NestedResourceInstance(BaseView): pass urlpatterns = patterns('', diff --git a/djangorestframework/tests/description.py b/djangorestframework/tests/description.py index d34e2d110..d5a1102f7 100644 --- a/djangorestframework/tests/description.py +++ b/djangorestframework/tests/description.py @@ -1,5 +1,5 @@ from django.test import TestCase -from djangorestframework.resource import Resource +from djangorestframework.views import BaseView from djangorestframework.compat import apply_markdown from djangorestframework.utils.description import get_name, get_description @@ -32,23 +32,23 @@ MARKED_DOWN = """

an example docstring

hash style header

""" -class TestResourceNamesAndDescriptions(TestCase): +class TestViewNamesAndDescriptions(TestCase): def test_resource_name_uses_classname_by_default(self): """Ensure Resource names are based on the classname by default.""" - class MockResource(Resource): + class MockView(BaseView): pass - self.assertEquals(get_name(MockResource()), 'Mock Resource') + self.assertEquals(get_name(MockView()), 'Mock View') def test_resource_name_can_be_set_explicitly(self): """Ensure Resource names can be set using the 'name' class attribute.""" example = 'Some Other Name' - class MockResource(Resource): + class MockView(BaseView): name = example - self.assertEquals(get_name(MockResource()), example) + self.assertEquals(get_name(MockView()), example) def test_resource_description_uses_docstring_by_default(self): """Ensure Resource names are based on the docstring by default.""" - class MockResource(Resource): + class MockView(BaseView): """an example docstring ==================== @@ -64,28 +64,28 @@ class TestResourceNamesAndDescriptions(TestCase): # hash style header #""" - self.assertEquals(get_description(MockResource()), DESCRIPTION) + self.assertEquals(get_description(MockView()), DESCRIPTION) def test_resource_description_can_be_set_explicitly(self): """Ensure Resource descriptions can be set using the 'description' class attribute.""" example = 'Some other description' - class MockResource(Resource): + class MockView(BaseView): """docstring""" description = example - self.assertEquals(get_description(MockResource()), example) + self.assertEquals(get_description(MockView()), example) def test_resource_description_does_not_require_docstring(self): """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'description' class attribute.""" example = 'Some other description' - class MockResource(Resource): + class MockView(BaseView): description = example - self.assertEquals(get_description(MockResource()), example) + self.assertEquals(get_description(MockView()), example) def test_resource_description_can_be_empty(self): """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string""" - class MockResource(Resource): + class MockView(BaseView): pass - self.assertEquals(get_description(MockResource()), '') + self.assertEquals(get_description(MockView()), '') def test_markdown(self): """Ensure markdown to HTML works as expected""" diff --git a/djangorestframework/tests/files.py b/djangorestframework/tests/files.py index 4dc3aa401..f0321cb32 100644 --- a/djangorestframework/tests/files.py +++ b/djangorestframework/tests/files.py @@ -1,7 +1,7 @@ from django.test import TestCase from django import forms from djangorestframework.compat import RequestFactory -from djangorestframework.resource import Resource +from djangorestframework.views import BaseView import StringIO class UploadFilesTests(TestCase): @@ -15,7 +15,7 @@ class UploadFilesTests(TestCase): class FileForm(forms.Form): file = forms.FileField - class MockResource(Resource): + class MockView(BaseView): permissions = () form = FileForm @@ -26,7 +26,7 @@ class UploadFilesTests(TestCase): file = StringIO.StringIO('stuff') file.name = 'stuff.txt' request = self.factory.post('/', {'file': file}) - view = MockResource.as_view() + view = MockView.as_view() response = view(request) self.assertEquals(response.content, '{"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}') diff --git a/djangorestframework/tests/parsers.py b/djangorestframework/tests/parsers.py index 00ebc812b..049ac741e 100644 --- a/djangorestframework/tests/parsers.py +++ b/djangorestframework/tests/parsers.py @@ -2,12 +2,12 @@ .. >>> from djangorestframework.parsers import FormParser >>> from djangorestframework.compat import RequestFactory - >>> from djangorestframework.resource import Resource + >>> from djangorestframework.views import BaseView >>> from StringIO import StringIO >>> from urllib import urlencode >>> req = RequestFactory().get('/') - >>> some_resource = Resource() - >>> some_resource.request = req # Make as if this request had been dispatched + >>> some_view = BaseView() + >>> some_view.request = req # Make as if this request had been dispatched FormParser ============ @@ -24,7 +24,7 @@ Here is some example data, which would eventually be sent along with a post requ Default behaviour for :class:`parsers.FormParser`, is to return a single value for each parameter : - >>> FormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'} + >>> FormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': 'blo1'} True However, you can customize this behaviour by subclassing :class:`parsers.FormParser`, and overriding :meth:`parsers.FormParser.is_a_list` : @@ -36,7 +36,7 @@ However, you can customize this behaviour by subclassing :class:`parsers.FormPar This new parser only flattens the lists of parameters that contain a single value. - >>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} + >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'bla1', 'key2': ['blo1', 'blo2']} True .. note:: The same functionality is available for :class:`parsers.MultipartParser`. @@ -61,7 +61,7 @@ The browsers usually strip the parameter completely. A hack to avoid this, and t :class:`parsers.FormParser` strips the values ``_empty`` from all the lists. - >>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1'} + >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1'} True Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a list, so the parser just stripped it. @@ -71,7 +71,7 @@ Oh ... but wait a second, the parameter ``key2`` isn't even supposed to be a lis ... def is_a_list(self, key, val_list): ... return key == 'key2' ... - >>> MyFormParser(some_resource).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []} + >>> MyFormParser(some_view).parse(StringIO(inpt)) == {'key1': 'blo1', 'key2': []} True Better like that. Note that you can configure something else than ``_empty`` for the empty value by setting :attr:`parsers.FormParser.EMPTY_VALUE`. @@ -81,7 +81,7 @@ from tempfile import TemporaryFile from django.test import TestCase from djangorestframework.compat import RequestFactory from djangorestframework.parsers import MultipartParser -from djangorestframework.resource import Resource +from djangorestframework.views import BaseView from djangorestframework.utils.mediatypes import MediaType from StringIO import StringIO @@ -122,9 +122,9 @@ class TestMultipartParser(TestCase): def test_multipartparser(self): """Ensure that MultipartParser can parse multipart/form-data that contains a mix of several files and parameters.""" post_req = RequestFactory().post('/', self.body, content_type=self.content_type) - resource = Resource() - resource.request = post_req - parsed = MultipartParser(resource).parse(StringIO(self.body)) + view = BaseView() + view.request = post_req + parsed = MultipartParser(view).parse(StringIO(self.body)) self.assertEqual(parsed['key1'], 'val1') self.assertEqual(parsed.FILES['file1'].read(), 'blablabla') diff --git a/djangorestframework/tests/reverse.py b/djangorestframework/tests/reverse.py index 49939d0e9..7026d4a47 100644 --- a/djangorestframework/tests/reverse.py +++ b/djangorestframework/tests/reverse.py @@ -3,10 +3,10 @@ from django.core.urlresolvers import reverse from django.test import TestCase from django.utils import simplejson as json -from djangorestframework.resource import Resource +from djangorestframework.views import BaseView -class MockResource(Resource): +class MockView(BaseView): """Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified""" permissions = () @@ -14,8 +14,8 @@ class MockResource(Resource): return reverse('another') urlpatterns = patterns('', - url(r'^$', MockResource.as_view()), - url(r'^another$', MockResource.as_view(), name='another'), + url(r'^$', MockView.as_view()), + url(r'^another$', MockView.as_view(), name='another'), ) diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py index 46383271f..94d014288 100644 --- a/djangorestframework/tests/throttling.py +++ b/djangorestframework/tests/throttling.py @@ -3,11 +3,11 @@ from django.test import TestCase from django.utils import simplejson as json from djangorestframework.compat import RequestFactory -from djangorestframework.resource import Resource +from djangorestframework.views import BaseView from djangorestframework.permissions import Throttling -class MockResource(Resource): +class MockView(BaseView): permissions = ( Throttling, ) throttle = (3, 1) # 3 requests per second @@ -15,7 +15,7 @@ class MockResource(Resource): return 'foo' urlpatterns = patterns('', - (r'^$', MockResource.as_view()), + (r'^$', MockView.as_view()), ) diff --git a/djangorestframework/tests/validators.py b/djangorestframework/tests/validators.py index b6563db62..52a675d23 100644 --- a/djangorestframework/tests/validators.py +++ b/djangorestframework/tests/validators.py @@ -4,6 +4,8 @@ from django.test import TestCase from djangorestframework.compat import RequestFactory from djangorestframework.validators import BaseValidator, FormValidator, ModelFormValidator from djangorestframework.response import ErrorResponse +from djangorestframework.views import BaseView +from djangorestframework.resource import Resource class TestValidatorMixinInterfaces(TestCase): @@ -20,7 +22,7 @@ class TestDisabledValidations(TestCase): def test_disabled_form_validator_returns_content_unchanged(self): """If the view's form attribute is None then FormValidator(view).validate(content) should just return the content unmodified.""" - class DisabledFormView(object): + class DisabledFormView(BaseView): form = None view = DisabledFormView() @@ -30,7 +32,7 @@ class TestDisabledValidations(TestCase): def test_disabled_form_validator_get_bound_form_returns_none(self): """If the view's form attribute is None on then FormValidator(view).get_bound_form(content) should just return None.""" - class DisabledFormView(object): + class DisabledFormView(BaseView): form = None view = DisabledFormView() @@ -39,11 +41,10 @@ class TestDisabledValidations(TestCase): def test_disabled_model_form_validator_returns_content_unchanged(self): - """If the view's form and model attributes are None then + """If the view's form is None and does not have a Resource with a model set then ModelFormValidator(view).validate(content) should just return the content unmodified.""" - class DisabledModelFormView(object): + class DisabledModelFormView(BaseView): form = None - model = None view = DisabledModelFormView() content = {'qwerty':'uiop'} @@ -51,13 +52,12 @@ class TestDisabledValidations(TestCase): def test_disabled_model_form_validator_get_bound_form_returns_none(self): """If the form attribute is None on FormValidatorMixin then get_bound_form(content) should just return None.""" - class DisabledModelFormView(object): - form = None + class DisabledModelFormView(BaseView): model = None view = DisabledModelFormView() content = {'qwerty':'uiop'} - self.assertEqual(ModelFormValidator(view).get_bound_form(content), None)# + self.assertEqual(ModelFormValidator(view).get_bound_form(content), None) class TestNonFieldErrors(TestCase): """Tests against form validation errors caused by non-field errors. (eg as might be caused by some custom form validation)""" @@ -84,7 +84,7 @@ class TestNonFieldErrors(TestCase): except ErrorResponse, exc: self.assertEqual(exc.response.raw_content, {'errors': [MockForm.ERROR_TEXT]}) else: - self.fail('ResourceException was not raised') #pragma: no cover + self.fail('ErrorResponse was not raised') #pragma: no cover class TestFormValidation(TestCase): @@ -95,11 +95,11 @@ class TestFormValidation(TestCase): class MockForm(forms.Form): qwerty = forms.CharField(required=True) - class MockFormView(object): + class MockFormView(BaseView): form = MockForm validators = (FormValidator,) - class MockModelFormView(object): + class MockModelFormView(BaseView): form = MockForm validators = (ModelFormValidator,) @@ -264,9 +264,12 @@ class TestModelFormValidator(TestCase): @property def readonly(self): return 'read only' - - class MockView(object): + + class MockResource(Resource): model = MockModel + + class MockView(BaseView): + resource = MockResource self.validator = ModelFormValidator(MockView) diff --git a/djangorestframework/tests/views.py b/djangorestframework/tests/views.py index 9e2e893fe..598712d2e 100644 --- a/djangorestframework/tests/views.py +++ b/djangorestframework/tests/views.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.test import Client -urlpatterns = patterns('djangorestframework.views', +urlpatterns = patterns('djangorestframework.utils.staticviews', url(r'^robots.txt$', 'deny_robots'), url(r'^favicon.ico$', 'favicon'), url(r'^accounts/login$', 'api_login'), diff --git a/djangorestframework/urls.py b/djangorestframework/urls.py new file mode 100644 index 000000000..271b02159 --- /dev/null +++ b/djangorestframework/urls.py @@ -0,0 +1,16 @@ +from django.conf.urls.defaults import patterns +from django.conf import settings + +urlpatterns = patterns('djangorestframework.utils.staticviews', + (r'robots.txt', 'deny_robots'), + (r'^accounts/login/$', 'api_login'), + (r'^accounts/logout/$', 'api_logout'), +) + +# Only serve favicon in production because otherwise chrome users will pretty much +# permanantly have the django-rest-framework favicon whenever they navigate to +# 127.0.0.1:8000 or whatever, which gets annoying +if not settings.DEBUG: + urlpatterns += patterns('djangorestframework.utils.staticviews', + (r'favicon.ico', 'favicon'), + ) \ No newline at end of file diff --git a/djangorestframework/utils/staticviews.py b/djangorestframework/utils/staticviews.py new file mode 100644 index 000000000..de2cb5d8b --- /dev/null +++ b/djangorestframework/utils/staticviews.py @@ -0,0 +1,65 @@ +from django.contrib.auth.views import * +from django.conf import settings +from django.http import HttpResponse +import base64 + +def deny_robots(request): + return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain') + +def favicon(request): + data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA=' + return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon') + +# BLERGH +# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS +# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to +# be making settings changes in order to accomodate django-rest-framework +@csrf_protect +@never_cache +def api_login(request, template_name='api_login.html', + redirect_field_name=REDIRECT_FIELD_NAME, + authentication_form=AuthenticationForm): + """Displays the login form and handles the login action.""" + + redirect_to = request.REQUEST.get(redirect_field_name, '') + + if request.method == "POST": + form = authentication_form(data=request.POST) + if form.is_valid(): + # Light security check -- make sure redirect_to isn't garbage. + if not redirect_to or ' ' in redirect_to: + redirect_to = settings.LOGIN_REDIRECT_URL + + # Heavier security check -- redirects to http://example.com should + # not be allowed, but things like /view/?param=http://example.com + # should be allowed. This regex checks if there is a '//' *before* a + # question mark. + elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to): + redirect_to = settings.LOGIN_REDIRECT_URL + + # Okay, security checks complete. Log the user in. + auth_login(request, form.get_user()) + + if request.session.test_cookie_worked(): + request.session.delete_test_cookie() + + return HttpResponseRedirect(redirect_to) + + else: + form = authentication_form(request) + + request.session.set_test_cookie() + + #current_site = get_current_site(request) + + return render_to_response(template_name, { + 'form': form, + redirect_field_name: redirect_to, + #'site': current_site, + #'site_name': current_site.name, + 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, + }, context_instance=RequestContext(request)) + + +def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME): + return logout(request, next_page, template_name, redirect_field_name) diff --git a/djangorestframework/validators.py b/djangorestframework/validators.py index c612de551..d806a6146 100644 --- a/djangorestframework/validators.py +++ b/djangorestframework/validators.py @@ -159,7 +159,7 @@ class ModelFormValidator(FormValidator): otherwise if model is set use that class to create a ModelForm, otherwise return None.""" form_cls = getattr(self.view, 'form', None) - model_cls = getattr(self.view, 'model', None) + model_cls = getattr(self.view.resource, 'model', None) if form_cls: # Use explict Form @@ -189,9 +189,10 @@ class ModelFormValidator(FormValidator): @property def _model_fields_set(self): """Return a set containing the names of validated fields on the model.""" - model = getattr(self.view, 'model', None) - fields = getattr(self.view, 'fields', self.fields) - exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields) + resource = self.view.resource + model = getattr(resource, 'model', None) + fields = getattr(resource, 'fields', self.fields) + exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields) model_fields = set(field.name for field in model._meta.fields) @@ -203,9 +204,10 @@ class ModelFormValidator(FormValidator): @property def _property_fields_set(self): """Returns a set containing the names of validated properties on the model.""" - model = getattr(self.view, 'model', None) - fields = getattr(self.view, 'fields', self.fields) - exclude_fields = getattr(self.view, 'exclude_fields', self.exclude_fields) + resource = self.view.resource + model = getattr(resource, 'model', None) + fields = getattr(resource, 'fields', self.fields) + exclude_fields = getattr(resource, 'exclude_fields', self.exclude_fields) property_fields = set(attr for attr in dir(model) if isinstance(getattr(model, attr, None), property) diff --git a/djangorestframework/views.py b/djangorestframework/views.py index d50e126fc..dd30a0924 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -1,66 +1,147 @@ -from django.contrib.auth.views import * -#from django.contrib.sites.models import get_current_site -from django.conf import settings -from django.http import HttpResponse -import base64 +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 * +from djangorestframework import resource, renderers, parsers, authentication, permissions, validators, status + + +__all__ = ['BaseView', + 'ModelView', + 'InstanceModelView', + 'ListOrModelView', + 'ListOrCreateModelView'] + + + +class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): + """Handles incoming requests and maps them to REST operations. + Performs request deserialization, response serialization, authentication and input validation.""" + + # Use the base resource by default + resource = resource.Resource + + # List of renderers the resource can serialize the response with, ordered by preference. + renderers = ( renderers.JSONRenderer, + renderers.DocumentingHTMLRenderer, + renderers.DocumentingXHTMLRenderer, + renderers.DocumentingPlainTextRenderer, + renderers.XMLRenderer ) + + # List of parsers the resource can parse the request with. + parsers = ( parsers.JSONParser, + parsers.FormParser, + parsers.MultipartParser ) + + # List of validators to validate, cleanup and normalize the request content + validators = ( validators.FormValidator, ) + + # List of all authenticating methods to attempt. + authentication = ( authentication.UserLoggedInAuthenticator, + authentication.BasicAuthenticator ) + + # List of all permissions that must be checked. + permissions = ( permissions.FullAnonAccess, ) + + # 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 renderers. + name = None + description = None + + @property + def allowed_methods(self): + return [method.upper() for method in self.http_method_names if hasattr(self, method)] + + def http_method_not_allowed(self, request, *args, **kwargs): + """Return an HTTP 405 error if an operation is called which does not have a handler method.""" + raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED, + {'detail': 'Method \'%s\' not allowed on this resource.' % self.method}) + + + 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 RendererMixin and Renderer classes.""" + return data + + + # Note: session based authentication is explicitly CSRF validated, + # all other authentication is CSRF exempt. + @csrf_exempt + def dispatch(self, request, *args, **kwargs): + self.request = request + self.args = args + self.kwargs = kwargs + + # 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: + # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter + # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately. + self.perform_form_overloading() + + # Authenticate and check request is has the relevant permissions + self.check_permissions() + + # Get the appropriate handler method + if self.method.lower() in self.http_method_names: + handler = getattr(self, self.method.lower(), self.http_method_not_allowed) + else: + handler = self.http_method_not_allowed + + response_obj = handler(request, *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.resource.object_to_serializable(response.raw_content) + + except ErrorResponse, exc: + response = exc.response + + # 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. + response.headers['Allow'] = ', '.join(self.allowed_methods) + response.headers['Vary'] = 'Authenticate, Accept' + + return self.render(response) + + +class ModelView(BaseView): + """A RESTful view that maps to a model in the database.""" + validators = (validators.ModelFormValidator,) + +class InstanceModelView(ReadModelMixin, UpdateModelMixin, DeleteModelMixin, ModelView): + """A view which provides default operations for read/update/delete against a model instance.""" + pass + +class ListModelResource(ListModelMixin, ModelView): + """A view which provides default operations for list, against a model in the database.""" + pass + +class ListOrCreateModelResource(ListModelMixin, CreateModelMixin, ModelView): + """A view which provides default operations for list and create, against a model in the database.""" + pass + -def deny_robots(request): - return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain') - -def favicon(request): - data = 'AAABAAEAEREAAAEAIADwBAAAFgAAACgAAAARAAAAIgAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADLy8tLy8vL3svLy1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy8vLBsvLywkAAAAATkZFS1xUVPqhn57/y8vL0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmVlQ/GxcXiy8vL88vLy4FdVlXzTkZF/2RdXP/Ly8vty8vLtMvLy5DLy8vty8vLxgAAAAAAAAAAAAAAAAAAAABORkUJTkZF4lNMS/+Lh4f/cWtq/05GRf9ORkX/Vk9O/3JtbP+Ef3//Vk9O/2ljYv/Ly8v5y8vLCQAAAAAAAAAAAAAAAE5GRQlORkX2TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/UElI/8PDw5cAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRZZORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vLvQAAAAAAAAAAAAAAAAAAAADLy8tIaWNi805GRf9ORkX/YVpZ/396eV7Ly8t7qaen9lZOTu5ORkX/TkZF/25oZ//Ly8v/y8vLycvLy0gAAAAATkZFSGNcXPpORkX/TkZF/05GRf+ysLDzTkZFe1NLSv6Oior/raur805GRf9ORkX/TkZF/2hiYf+npaX/y8vL5wAAAABORkXnTkZF/05GRf9ORkX/VU1M/8vLy/9PR0b1TkZF/1VNTP/Ly8uQT0dG+E5GRf9ORkX/TkZF/1hRUP3Ly8tmAAAAAE5GRWBORkXkTkZF/05GRf9ORkX/t7a2/355eOpORkX/TkZFkISAf1BORkX/TkZF/05GRf9XT075TkZFZgAAAAAAAAAAAAAAAAAAAABORkXDTkZF/05GRf9lX17/ubi4/8vLy/+2tbT/Yltb/05GRf9ORkX/a2Vk/8vLy5MAAAAAAAAAAAAAAAAAAAAAAAAAAFNLSqNORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf+Cfn3/y8vL+cvLyw8AAAAAAAAAAAAAAABORkUSTkZF+U5GRf9ORkX/TkZF/05GRf9ORkX/TkZF/05GRf9ORkX/TkZF/1BJSP/CwsLmy8vLDwAAAAAAAAAAAAAAAE5GRRJORkXtTkZF9FFJSJ1ORkXJTkZF/05GRf9ORkX/ZF5d9k5GRZ9ORkXtTkZF5HFsaxUAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRQxORkUJAAAAAAAAAABORkXhTkZF/2JbWv7Ly8tgAAAAAAAAAABORkUGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5GRWBORkX2TkZFYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//+AAP9/gAD+P4AA4AOAAMADgADAA4AAwAOAAMMBgACCAIAAAAGAAIBDgADAA4AAwAOAAMADgADAB4AA/H+AAP7/gAA=' - return HttpResponse(base64.b64decode(data), mimetype='image/vnd.microsoft.icon') - -# BLERGH -# Replicate django.contrib.auth.views.login simply so we don't have get users to update TEMPLATE_CONTEXT_PROCESSORS -# to add ADMIN_MEDIA_PREFIX to the RequestContext. I don't like this but really really want users to not have to -# be making settings changes in order to accomodate django-rest-framework -@csrf_protect -@never_cache -def api_login(request, template_name='api_login.html', - redirect_field_name=REDIRECT_FIELD_NAME, - authentication_form=AuthenticationForm): - """Displays the login form and handles the login action.""" - - redirect_to = request.REQUEST.get(redirect_field_name, '') - - if request.method == "POST": - form = authentication_form(data=request.POST) - if form.is_valid(): - # Light security check -- make sure redirect_to isn't garbage. - if not redirect_to or ' ' in redirect_to: - redirect_to = settings.LOGIN_REDIRECT_URL - - # Heavier security check -- redirects to http://example.com should - # not be allowed, but things like /view/?param=http://example.com - # should be allowed. This regex checks if there is a '//' *before* a - # question mark. - elif '//' in redirect_to and re.match(r'[^\?]*//', redirect_to): - redirect_to = settings.LOGIN_REDIRECT_URL - - # Okay, security checks complete. Log the user in. - auth_login(request, form.get_user()) - - if request.session.test_cookie_worked(): - request.session.delete_test_cookie() - - return HttpResponseRedirect(redirect_to) - - else: - form = authentication_form(request) - - request.session.set_test_cookie() - - #current_site = get_current_site(request) - - return render_to_response(template_name, { - 'form': form, - redirect_field_name: redirect_to, - #'site': current_site, - #'site_name': current_site.name, - 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, - }, context_instance=RequestContext(request)) -def api_logout(request, next_page=None, template_name='api_login.html', redirect_field_name=REDIRECT_FIELD_NAME): - return logout(request, next_page, template_name, redirect_field_name) diff --git a/examples/modelresourceexample/views.py b/examples/modelresourceexample/views.py index 07f50b653..5495a2931 100644 --- a/examples/modelresourceexample/views.py +++ b/examples/modelresourceexample/views.py @@ -1,15 +1,15 @@ -from djangorestframework.modelresource import ModelResource, RootModelResource +from djangorestframework.modelresource import InstanceModelResource, ListOrCreateModelResource from modelresourceexample.models import MyModel FIELDS = ('foo', 'bar', 'baz', 'absolute_url') -class MyModelRootResource(RootModelResource): +class MyModelRootResource(ListOrCreateModelResource): """A create/list resource for MyModel. Available for both authenticated and anonymous access for the purposes of the sandbox.""" model = MyModel fields = FIELDS -class MyModelResource(ModelResource): +class MyModelResource(InstanceModelResource): """A read/update/delete resource for MyModel. Available for both authenticated and anonymous access for the purposes of the sandbox.""" model = MyModel diff --git a/examples/urls.py b/examples/urls.py index 7cb5e7ce4..cf4d4042c 100644 --- a/examples/urls.py +++ b/examples/urls.py @@ -2,11 +2,8 @@ from django.conf.urls.defaults import patterns, include, url from django.conf import settings from sandbox.views import Sandbox -urlpatterns = patterns('djangorestframework.views', - (r'robots.txt', 'deny_robots'), - +urlpatterns = patterns('', (r'^$', Sandbox.as_view()), - (r'^resource-example/', include('resourceexample.urls')), (r'^model-resource-example/', include('modelresourceexample.urls')), (r'^mixin/', include('mixin.urls')), @@ -14,14 +11,6 @@ urlpatterns = patterns('djangorestframework.views', (r'^pygments/', include('pygments_api.urls')), (r'^blog-post/', include('blogpost.urls')), - (r'^accounts/login/$', 'api_login'), - (r'^accounts/logout/$', 'api_logout'), + (r'^', include('djangorestframework.urls')), ) -# Only serve favicon in production because otherwise chrome users will pretty much -# permanantly have the django-rest-framework favicon whenever they navigate to -# 127.0.0.1:8000 or whatever, which gets annoying -if not settings.DEBUG: - urlpatterns += patterns('djangorestframework.views', - (r'favicon.ico', 'favicon'), - )