diff options
| author | Tom Christie | 2012-10-30 14:32:31 +0000 |
|---|---|---|
| committer | Tom Christie | 2012-10-30 14:32:31 +0000 |
| commit | 9b30dab4f772f67a626e176dc4fae0a3ef9c2c81 (patch) | |
| tree | ca138abf4792f58ffa28684f784f201ee1eef6d7 /rest_framework/views.py | |
| parent | 7e5b1501b5cede61a9391fb1a751d2ebcdb37031 (diff) | |
| parent | 4e7805cb24d73e7f706318b5e5a27e3f9ba39d14 (diff) | |
| download | django-rest-framework-9b30dab4f772f67a626e176dc4fae0a3ef9c2c81.tar.bz2 | |
Merge branch 'restframework2' into rest-framework-2-merge2.0.0
Conflicts:
.gitignore
.travis.yml
AUTHORS
README.rst
djangorestframework/mixins.py
djangorestframework/renderers.py
djangorestframework/resources.py
djangorestframework/serializer.py
djangorestframework/templates/djangorestframework/base.html
djangorestframework/templates/djangorestframework/login.html
djangorestframework/templatetags/add_query_param.py
djangorestframework/tests/accept.py
djangorestframework/tests/authentication.py
djangorestframework/tests/content.py
djangorestframework/tests/reverse.py
djangorestframework/tests/serializer.py
djangorestframework/views.py
docs/examples.rst
docs/examples/blogpost.rst
docs/examples/modelviews.rst
docs/examples/objectstore.rst
docs/examples/permissions.rst
docs/examples/pygments.rst
docs/examples/views.rst
docs/howto/alternativeframeworks.rst
docs/howto/mixin.rst
docs/howto/reverse.rst
docs/howto/usingurllib2.rst
docs/index.rst
docs/topics/release-notes.md
examples/sandbox/views.py
rest_framework/__init__.py
rest_framework/compat.py
rest_framework/utils/breadcrumbs.py
setup.py
Diffstat (limited to 'rest_framework/views.py')
| -rw-r--r-- | rest_framework/views.py | 370 |
1 files changed, 370 insertions, 0 deletions
diff --git a/rest_framework/views.py b/rest_framework/views.py new file mode 100644 index 00000000..71e1fe6c --- /dev/null +++ b/rest_framework/views.py @@ -0,0 +1,370 @@ +""" +Provides an APIView class that is used as the base of all class-based views. +""" + +import re +from django.core.exceptions import PermissionDenied +from django.http import Http404 +from django.utils.html import escape +from django.utils.safestring import mark_safe +from django.views.decorators.csrf import csrf_exempt +from rest_framework import status, exceptions +from rest_framework.compat import View, apply_markdown +from rest_framework.response import Response +from rest_framework.request import Request +from rest_framework.settings import api_settings + + +def _remove_trailing_string(content, trailing): + """ + Strip trailing component `trailing` from `content` if it exists. + Used when generating names from view classes. + """ + if content.endswith(trailing) and content != trailing: + return content[:-len(trailing)] + return content + + +def _remove_leading_indent(content): + """ + Remove leading indent from a block of text. + Used when generating descriptions from docstrings. + """ + whitespace_counts = [len(line) - len(line.lstrip(' ')) + for line in content.splitlines()[1:] if line.lstrip()] + + # unindent the content if needed + if whitespace_counts: + whitespace_pattern = '^' + (' ' * min(whitespace_counts)) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) + content = content.strip('\n') + return content + + +def _camelcase_to_spaces(content): + """ + Translate 'CamelCaseNames' to 'Camel Case Names'. + Used when generating names from view classes. + """ + camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' + content = re.sub(camelcase_boundry, ' \\1', content).strip() + return ' '.join(content.split('_')).title() + + +class APIView(View): + settings = api_settings + + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + parser_classes = api_settings.DEFAULT_PARSER_CLASSES + authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES + throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES + permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS + + @classmethod + def as_view(cls, **initkwargs): + """ + Override the default :meth:`as_view` to store an instance of the view + as an attribute on the callable function. This allows us to discover + information about the view when we do URL reverse lookups. + """ + # TODO: deprecate? + view = super(APIView, cls).as_view(**initkwargs) + view.cls_instance = cls(**initkwargs) + return view + + @property + def allowed_methods(self): + """ + Return the list of allowed HTTP methods, uppercased. + """ + return [method.upper() for method in self.http_method_names + if hasattr(self, method)] + + @property + def default_response_headers(self): + # TODO: deprecate? + # TODO: Only vary by accept if multiple renderers + return { + 'Allow': ', '.join(self.allowed_methods), + 'Vary': 'Accept' + } + + def get_name(self): + """ + Return the resource or view class name for use as this view's name. + Override to customize. + """ + # TODO: deprecate? + name = self.__class__.__name__ + name = _remove_trailing_string(name, 'View') + return _camelcase_to_spaces(name) + + def get_description(self, html=False): + """ + Return the resource or view docstring for use as this view's description. + Override to customize. + """ + # TODO: deprecate? + description = self.__doc__ or '' + description = _remove_leading_indent(description) + if html: + return self.markup_description(description) + return description + + def markup_description(self, description): + """ + Apply HTML markup to the description of this view. + """ + # TODO: deprecate? + if apply_markdown: + description = apply_markdown(description) + else: + description = escape(description).replace('\n', '<br />') + return mark_safe(description) + + def metadata(self, request): + return { + 'name': self.get_name(), + 'description': self.get_description(), + 'renders': [renderer.media_type for renderer in self.renderer_classes], + 'parses': [parser.media_type for parser in self.parser_classes], + } + # TODO: Add 'fields', from serializer info, if it exists. + # serializer = self.get_serializer() + # if serializer is not None: + # field_name_types = {} + # for name, field in form.fields.iteritems(): + # field_name_types[name] = field.__class__.__name__ + # content['fields'] = field_name_types + + def http_method_not_allowed(self, request, *args, **kwargs): + """ + Called if `request.method` does not corrospond to a handler method. + """ + raise exceptions.MethodNotAllowed(request.method) + + def permission_denied(self, request): + """ + If request is not permitted, determine what kind of exception to raise. + """ + raise exceptions.PermissionDenied() + + def throttled(self, request, wait): + """ + If request is throttled, determine what kind of exception to raise. + """ + raise exceptions.Throttled(wait) + + def get_parser_context(self, http_request): + """ + Returns a dict that is passed through to Parser.parse(), + as the `parser_context` keyword argument. + """ + # Note: Additionally `request` will also be added to the context + # by the Request object. + return { + 'view': self, + 'args': getattr(self, 'args', ()), + 'kwargs': getattr(self, 'kwargs', {}) + } + + def get_renderer_context(self): + """ + Returns a dict that is passed through to Renderer.render(), + as the `renderer_context` keyword argument. + """ + # Note: Additionally 'response' will also be added to the context, + # by the Response object. + return { + 'view': self, + 'args': getattr(self, 'args', ()), + 'kwargs': getattr(self, 'kwargs', {}), + 'request': getattr(self, 'request', None) + } + + # API policy instantiation methods + + def get_format_suffix(self, **kwargs): + """ + Determine if the request includes a '.json' style format suffix + """ + if self.settings.FORMAT_SUFFIX_KWARG: + return kwargs.get(self.settings.FORMAT_SUFFIX_KWARG) + + def get_renderers(self): + """ + Instantiates and returns the list of renderers that this view can use. + """ + return [renderer() for renderer in self.renderer_classes] + + def get_parsers(self): + """ + Instantiates and returns the list of renderers that this view can use. + """ + return [parser() for parser in self.parser_classes] + + def get_authenticators(self): + """ + Instantiates and returns the list of renderers that this view can use. + """ + return [auth() for auth in self.authentication_classes] + + def get_permissions(self): + """ + Instantiates and returns the list of permissions that this view requires. + """ + return [permission() for permission in self.permission_classes] + + def get_throttles(self): + """ + Instantiates and returns the list of throttles that this view uses. + """ + return [throttle() for throttle in self.throttle_classes] + + def get_content_negotiator(self): + """ + Instantiate and return the content negotiation class to use. + """ + if not getattr(self, '_negotiator', None): + self._negotiator = self.content_negotiation_class() + return self._negotiator + + # API policy implementation methods + + def perform_content_negotiation(self, request, force=False): + """ + Determine which renderer and media type to use render the response. + """ + renderers = self.get_renderers() + conneg = self.get_content_negotiator() + + try: + return conneg.select_renderer(request, renderers, self.format_kwarg) + except: + if force: + return (renderers[0], renderers[0].media_type) + raise + + def has_permission(self, request, obj=None): + """ + Return `True` if the request should be permitted. + """ + for permission in self.get_permissions(): + if not permission.has_permission(request, self, obj): + return False + return True + + def check_throttles(self, request): + """ + Check if request should be throttled. + """ + for throttle in self.get_throttles(): + if not throttle.allow_request(request, self): + self.throttled(request, throttle.wait()) + + # Dispatch methods + + def initialize_request(self, request, *args, **kargs): + """ + Returns the initial request object. + """ + parser_context = self.get_parser_context(request) + + return Request(request, + parsers=self.get_parsers(), + authenticators=self.get_authenticators(), + negotiator=self.get_content_negotiator(), + parser_context=parser_context) + + def initial(self, request, *args, **kwargs): + """ + Runs anything that needs to occur prior to calling the method handler. + """ + self.format_kwarg = self.get_format_suffix(**kwargs) + + # Ensure that the incoming request is permitted + if not self.has_permission(request): + self.permission_denied(request) + self.check_throttles(request) + + # Perform content negotiation and store the accepted info on the request + neg = self.perform_content_negotiation(request) + request.accepted_renderer, request.accepted_media_type = neg + + def finalize_response(self, request, response, *args, **kwargs): + """ + Returns the final response object. + """ + if isinstance(response, Response): + if not getattr(request, 'accepted_renderer', None): + neg = self.perform_content_negotiation(request, force=True) + request.accepted_renderer, request.accepted_media_type = neg + + response.accepted_renderer = request.accepted_renderer + response.accepted_media_type = request.accepted_media_type + response.renderer_context = self.get_renderer_context() + + for key, value in self.headers.items(): + response[key] = value + + return response + + def handle_exception(self, exc): + """ + Handle any exception that occurs, by returning an appropriate response, + or re-raising the error. + """ + if isinstance(exc, exceptions.Throttled): + # Throttle wait header + self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait + + if isinstance(exc, exceptions.APIException): + return Response({'detail': exc.detail}, status=exc.status_code) + elif isinstance(exc, Http404): + return Response({'detail': 'Not found'}, + status=status.HTTP_404_NOT_FOUND) + elif isinstance(exc, PermissionDenied): + return Response({'detail': 'Permission denied'}, + status=status.HTTP_403_FORBIDDEN) + raise + + # Note: session based authentication is explicitly CSRF validated, + # all other authentication is CSRF exempt. + @csrf_exempt + def dispatch(self, request, *args, **kwargs): + """ + `.dispatch()` is pretty much the same as Django's regular dispatch, + but with extra hooks for startup, finalize, and exception handling. + """ + request = self.initialize_request(request, *args, **kwargs) + self.request = request + self.args = args + self.kwargs = kwargs + self.headers = self.default_response_headers # deprecate? + + try: + self.initial(request, *args, **kwargs) + + # Get the appropriate handler method + if request.method.lower() in self.http_method_names: + handler = getattr(self, request.method.lower(), + self.http_method_not_allowed) + else: + handler = self.http_method_not_allowed + + response = handler(request, *args, **kwargs) + + except Exception as exc: + response = self.handle_exception(exc) + + self.response = self.finalize_response(request, response, *args, **kwargs) + return self.response + + def options(self, request, *args, **kwargs): + """ + Handler method for HTTP 'OPTIONS' request. + We may as well implement this as Django will otherwise provide + a less useful default implementation. + """ + return Response(self.metadata(request), status=status.HTTP_200_OK) |
