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 @@