web/lib/django/views/decorators/http.py
author ymh <ymh.work@gmail.com>
Thu, 05 Aug 2010 17:28:09 +0200
changeset 50 012451a812f1
parent 38 77b6da96e6f1
permissions -rw-r--r--
Merge with a2711e44ba5de8b1675d7e0ee6aaa4a6c56a9b46
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
38
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     1
"""
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     2
Decorators for views based on HTTP headers.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     3
"""
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     4
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     5
try:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     6
    from functools import wraps
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     7
except ImportError:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     8
    from django.utils.functional import wraps  # Python 2.4 fallback.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
     9
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    10
from calendar import timegm
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    11
from datetime import timedelta
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    12
from email.Utils import formatdate
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    13
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    14
from django.utils.decorators import decorator_from_middleware, available_attrs
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    15
from django.utils.http import parse_etags, quote_etag
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    16
from django.middleware.http import ConditionalGetMiddleware
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    17
from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    18
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    19
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    20
conditional_page = decorator_from_middleware(ConditionalGetMiddleware)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    21
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    22
def require_http_methods(request_method_list):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    23
    """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    24
    Decorator to make a view only accept particular request methods.  Usage::
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    25
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    26
        @require_http_methods(["GET", "POST"])
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    27
        def my_view(request):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    28
            # I can assume now that only GET or POST requests make it this far
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    29
            # ...
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    30
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    31
    Note that request methods should be in uppercase.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    32
    """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    33
    def decorator(func):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    34
        def inner(request, *args, **kwargs):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    35
            if request.method not in request_method_list:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    36
                return HttpResponseNotAllowed(request_method_list)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    37
            return func(request, *args, **kwargs)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    38
        return wraps(func, assigned=available_attrs(func))(inner)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    39
    return decorator
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    40
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    41
require_GET = require_http_methods(["GET"])
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    42
require_GET.__doc__ = "Decorator to require that a view only accept the GET method."
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    43
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    44
require_POST = require_http_methods(["POST"])
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    45
require_POST.__doc__ = "Decorator to require that a view only accept the POST method."
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    46
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    47
def condition(etag_func=None, last_modified_func=None):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    48
    """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    49
    Decorator to support conditional retrieval (or change) for a view
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    50
    function.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    51
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    52
    The parameters are callables to compute the ETag and last modified time for
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    53
    the requested resource, respectively. The callables are passed the same
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    54
    parameters as the view itself. The Etag function should return a string (or
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    55
    None if the resource doesn't exist), whilst the last_modified function
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    56
    should return a datetime object (or None if the resource doesn't exist).
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    57
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    58
    If both parameters are provided, all the preconditions must be met before
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    59
    the view is processed.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    60
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    61
    This decorator will either pass control to the wrapped view function or
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    62
    return an HTTP 304 response (unmodified) or 412 response (preconditions
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    63
    failed), depending upon the request method.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    64
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    65
    Any behavior marked as "undefined" in the HTTP spec (e.g. If-none-match
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    66
    plus If-modified-since headers) will result in the view function being
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    67
    called.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    68
    """
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    69
    def decorator(func):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    70
        def inner(request, *args, **kwargs):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    71
            # Get HTTP request headers
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    72
            if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE")
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    73
            if_none_match = request.META.get("HTTP_IF_NONE_MATCH")
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    74
            if_match = request.META.get("HTTP_IF_MATCH")
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    75
            if if_none_match or if_match:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    76
                # There can be more than one ETag in the request, so we
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    77
                # consider the list of values.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    78
                try:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    79
                    etags = parse_etags(if_none_match or if_match)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    80
                except ValueError:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    81
                    # In case of invalid etag ignore all ETag headers.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    82
                    # Apparently Opera sends invalidly quoted headers at times
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    83
                    # (we should be returning a 400 response, but that's a
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    84
                    # little extreme) -- this is Django bug #10681.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    85
                    if_none_match = None
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    86
                    if_match = None
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    87
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    88
            # Compute values (if any) for the requested resource.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    89
            if etag_func:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    90
                res_etag = etag_func(request, *args, **kwargs)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    91
            else:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    92
                res_etag = None
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    93
            if last_modified_func:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    94
                dt = last_modified_func(request, *args, **kwargs)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    95
                if dt:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    96
                    res_last_modified = formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT'
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    97
                else:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    98
                    res_last_modified = None
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
    99
            else:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   100
                res_last_modified = None
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   101
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   102
            response = None
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   103
            if not ((if_match and (if_modified_since or if_none_match)) or
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   104
                    (if_match and if_none_match)):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   105
                # We only get here if no undefined combinations of headers are
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   106
                # specified.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   107
                if ((if_none_match and (res_etag in etags or
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   108
                        "*" in etags and res_etag)) and
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   109
                        (not if_modified_since or
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   110
                            res_last_modified == if_modified_since)):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   111
                    if request.method in ("GET", "HEAD"):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   112
                        response = HttpResponseNotModified()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   113
                    else:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   114
                        response = HttpResponse(status=412)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   115
                elif if_match and ((not res_etag and "*" in etags) or
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   116
                        (res_etag and res_etag not in etags)):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   117
                    response = HttpResponse(status=412)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   118
                elif (not if_none_match and if_modified_since and
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   119
                        request.method == "GET" and
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   120
                        res_last_modified == if_modified_since):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   121
                    response = HttpResponseNotModified()
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   122
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   123
            if response is None:
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   124
                response = func(request, *args, **kwargs)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   125
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   126
            # Set relevant headers on the response if they don't already exist.
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   127
            if res_last_modified and not response.has_header('Last-Modified'):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   128
                response['Last-Modified'] = res_last_modified
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   129
            if res_etag and not response.has_header('ETag'):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   130
                response['ETag'] = quote_etag(res_etag)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   131
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   132
            return response
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   133
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   134
        return inner
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   135
    return decorator
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   136
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   137
# Shortcut decorators for common cases based on ETag or Last-Modified only
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   138
def etag(etag_func):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   139
    return condition(etag_func=etag_func)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   140
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   141
def last_modified(last_modified_func):
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   142
    return condition(last_modified_func=last_modified_func)
77b6da96e6f1 update django
ymh <ymh.work@gmail.com>
parents:
diff changeset
   143