mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-30 23:47:53 +03:00 
			
		
		
		
	Decouple views and resources
This commit is contained in:
		
							parent
							
								
									8756664e06
								
							
						
					
					
						commit
						d373b3a067
					
				|  | @ -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. | ||||
|          | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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.  | ||||
|  |  | |||
|  | @ -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 | ||||
| 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.""" | ||||
|      | ||||
| __all__ = ['Resource'] | ||||
|     # 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 | ||||
|      | ||||
| 
 | ||||
| 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 ) | ||||
|      | ||||
|     # List of all permissions required to access the resource | ||||
|     permissions = () | ||||
| 
 | ||||
|     # 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): | ||||
|         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. | ||||
|     # By default the set of returned fields will be the set of: | ||||
|     # | ||||
|             # 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' | ||||
|     # 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 | ||||
| 
 | ||||
|             return self.render(response) | ||||
|         except: | ||||
|             import traceback | ||||
|             traceback.print_exc() | ||||
|     @classmethod | ||||
|     def object_to_serializable(self, data): | ||||
|         """A (horrible) 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 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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
| <div id="content" class="colM"> | ||||
|          | ||||
| <div id="content-main"> | ||||
| <form method="post" action="{% url djangorestframework.views.api_login %}" id="login-form"> | ||||
| <form method="post" action="{% url djangorestframework.utils.staticviews.api_login %}" id="login-form"> | ||||
| {% csrf_token %} | ||||
|   <div class="form-row"> | ||||
|     <label for="id_username">Username:</label> {{ form.username }} | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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()), | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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('', | ||||
|  |  | |||
|  | @ -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 = """<h2>an example docstring</h2> | |||
| <h2 id="hash_style_header">hash style header</h2>""" | ||||
| 
 | ||||
| 
 | ||||
| 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""" | ||||
|  |  | |||
|  | @ -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"}') | ||||
| 
 | ||||
|  |  | |||
|  | @ -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') | ||||
| 
 | ||||
|  |  | |||
|  | @ -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'), | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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()), | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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,) | ||||
|           | ||||
|  | @ -265,9 +265,12 @@ class TestModelFormValidator(TestCase): | |||
|             def readonly(self): | ||||
|                 return 'read only' | ||||
|          | ||||
|         class MockView(object): | ||||
|         class MockResource(Resource): | ||||
|             model = MockModel | ||||
|   | ||||
|         class MockView(BaseView): | ||||
|             resource = MockResource | ||||
|         | ||||
|         self.validator = ModelFormValidator(MockView) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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'), | ||||
|  |  | |||
							
								
								
									
										16
									
								
								djangorestframework/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								djangorestframework/urls.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -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'), | ||||
|     ) | ||||
							
								
								
									
										65
									
								
								djangorestframework/utils/staticviews.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								djangorestframework/utils/staticviews.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
| def deny_robots(request): | ||||
|     return HttpResponse('User-agent: *\nDisallow: /', mimetype='text/plain') | ||||
| 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 | ||||
| 
 | ||||
| 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.""" | ||||
| __all__ = ['BaseView', | ||||
|            'ModelView', | ||||
|            'InstanceModelView', | ||||
|            'ListOrModelView', | ||||
|            'ListOrCreateModelView'] | ||||
| 
 | ||||
|     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 | ||||
| class BaseView(RequestMixin, ResponseMixin, AuthMixin, View): | ||||
|     """Handles incoming requests and maps them to REST operations. | ||||
|     Performs request deserialization, response serialization, authentication and input validation.""" | ||||
| 
 | ||||
|             # Okay, security checks complete. Log the user in. | ||||
|             auth_login(request, form.get_user()) | ||||
|     # Use the base resource by default | ||||
|     resource = resource.Resource | ||||
| 
 | ||||
|             if request.session.test_cookie_worked(): | ||||
|                 request.session.delete_test_cookie() | ||||
|     # List of renderers the resource can serialize the response with, ordered by preference. | ||||
|     renderers = ( renderers.JSONRenderer, | ||||
|                   renderers.DocumentingHTMLRenderer, | ||||
|                   renderers.DocumentingXHTMLRenderer, | ||||
|                   renderers.DocumentingPlainTextRenderer, | ||||
|                   renderers.XMLRenderer ) | ||||
| 
 | ||||
|             return HttpResponseRedirect(redirect_to) | ||||
|     # 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: | ||||
|         form = authentication_form(request) | ||||
|                 handler = self.http_method_not_allowed | ||||
| 
 | ||||
|     request.session.set_test_cookie() | ||||
|             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 | ||||
| 
 | ||||
|     #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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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'), | ||||
|     ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user