测试gitnore

This commit is contained in:
ladeng07
2022-05-06 15:45:57 +08:00
parent 12f390949b
commit 51552904f9
2347 changed files with 120102 additions and 53549 deletions
@@ -1,3 +1,3 @@
from django.views.generic.base import View
__all__ = ["View"]
__all__ = ['View']
+31 -37
View File
@@ -97,7 +97,7 @@ CSRF_FAILURE_TEMPLATE = """
{% endif %}
</body>
</html>
""" # NOQA
"""
CSRF_FAILURE_TEMPLATE_NAME = "403_csrf.html"
@@ -106,46 +106,40 @@ def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
Default view used when request fails CSRF protection
"""
from django.middleware.csrf import REASON_NO_CSRF_COOKIE, REASON_NO_REFERER
c = {
"title": _("Forbidden"),
"main": _("CSRF verification failed. Request aborted."),
"reason": reason,
"no_referer": reason == REASON_NO_REFERER,
"no_referer1": _(
"You are seeing this message because this HTTPS site requires a "
"“Referer header” to be sent by your web browser, but none was "
"sent. This header is required for security reasons, to ensure "
"that your browser is not being hijacked by third parties."
),
"no_referer2": _(
"If you have configured your browser to disable “Referer” headers, "
"please re-enable them, at least for this site, or for HTTPS "
"connections, or for “same-origin” requests."
),
"no_referer3": _(
'title': _("Forbidden"),
'main': _("CSRF verification failed. Request aborted."),
'reason': reason,
'no_referer': reason == REASON_NO_REFERER,
'no_referer1': _(
'You are seeing this message because this HTTPS site requires a '
'“Referer header” to be sent by your Web browser, but none was '
'sent. This header is required for security reasons, to ensure '
'that your browser is not being hijacked by third parties.'),
'no_referer2': _(
'If you have configured your browser to disable “Referer” headers, '
'please re-enable them, at least for this site, or for HTTPS '
'connections, or for “same-origin” requests.'),
'no_referer3': _(
'If you are using the <meta name="referrer" '
'content="no-referrer"> tag or including the “Referrer-Policy: '
"no-referrer” header, please remove them. The CSRF protection "
"requires the “Referer” header to do strict referer checking. If "
"youre concerned about privacy, use alternatives like "
'<a rel="noreferrer" …> for links to third-party sites.'
),
"no_cookie": reason == REASON_NO_CSRF_COOKIE,
"no_cookie1": _(
'content=\"no-referrer\"> tag or including the “Referrer-Policy: '
'no-referrer” header, please remove them. The CSRF protection '
'requires the “Referer” header to do strict referer checking. If '
'youre concerned about privacy, use alternatives like '
'<a rel=\"noreferrer\" …> for links to third-party sites.'),
'no_cookie': reason == REASON_NO_CSRF_COOKIE,
'no_cookie1': _(
"You are seeing this message because this site requires a CSRF "
"cookie when submitting forms. This cookie is required for "
"security reasons, to ensure that your browser is not being "
"hijacked by third parties."
),
"no_cookie2": _(
"If you have configured your browser to disable cookies, please "
"re-enable them, at least for this site, or for “same-origin” "
"requests."
),
"DEBUG": settings.DEBUG,
"docs_version": get_docs_version(),
"more": _("More information is available with DEBUG=True."),
"hijacked by third parties."),
'no_cookie2': _(
'If you have configured your browser to disable cookies, please '
're-enable them, at least for this site, or for “same-origin” '
'requests.'),
'DEBUG': settings.DEBUG,
'docs_version': get_docs_version(),
'more': _("More information is available with DEBUG=True."),
}
try:
t = loader.get_template(template_name)
@@ -157,4 +151,4 @@ def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
else:
# Raise if a developer-specified template doesn't exist.
raise
return HttpResponseForbidden(t.render(c), content_type="text/html")
return HttpResponseForbidden(t.render(c), content_type='text/html')
+130 -192
View File
@@ -23,18 +23,10 @@ from django.utils.version import get_docs_version
# works even if the template loader is broken.
DEBUG_ENGINE = Engine(
debug=True,
libraries={"i18n": "django.templatetags.i18n"},
libraries={'i18n': 'django.templatetags.i18n'},
)
def builtin_template_path(name):
"""
Return a path to a builtin template.
Avoid calling this function at the module level or in a class-definition
because __file__ may not exist, e.g. in frozen environments.
"""
return Path(__file__).parent / "templates" / name
CURRENT_DIR = Path(__file__).parent
class ExceptionCycleWarning(UserWarning):
@@ -48,7 +40,6 @@ class CallableSettingWrapper:
* Not to break the debug page if the callable forbidding to set attributes
(#23070).
"""
def __init__(self, callable_setting):
self._wrapped = callable_setting
@@ -62,14 +53,12 @@ def technical_500_response(request, exc_type, exc_value, tb, status_code=500):
the values returned from sys.exc_info() and friends.
"""
reporter = get_exception_reporter_class(request)(request, exc_type, exc_value, tb)
if request.accepts("text/html"):
if request.accepts('text/html'):
html = reporter.get_traceback_html()
return HttpResponse(html, status=status_code, content_type="text/html")
return HttpResponse(html, status=status_code, content_type='text/html')
else:
text = reporter.get_traceback_text()
return HttpResponse(
text, status=status_code, content_type="text/plain; charset=utf-8"
)
return HttpResponse(text, status=status_code, content_type='text/plain; charset=utf-8')
@functools.lru_cache()
@@ -80,16 +69,12 @@ def get_default_exception_reporter_filter():
def get_exception_reporter_filter(request):
default_filter = get_default_exception_reporter_filter()
return getattr(request, "exception_reporter_filter", default_filter)
return getattr(request, 'exception_reporter_filter', default_filter)
def get_exception_reporter_class(request):
default_exception_reporter_class = import_string(
settings.DEFAULT_EXCEPTION_REPORTER
)
return getattr(
request, "exception_reporter_class", default_exception_reporter_class
)
default_exception_reporter_class = import_string(settings.DEFAULT_EXCEPTION_REPORTER)
return getattr(request, 'exception_reporter_class', default_exception_reporter_class)
class SafeExceptionReporterFilter:
@@ -97,11 +82,8 @@ class SafeExceptionReporterFilter:
Use annotations made by the sensitive_post_parameters and
sensitive_variables decorators to filter out sensitive information.
"""
cleansed_substitute = "********************"
hidden_settings = _lazy_re_compile(
"API|TOKEN|KEY|SECRET|PASS|SIGNATURE", flags=re.I
)
cleansed_substitute = '********************'
hidden_settings = _lazy_re_compile('API|TOKEN|KEY|SECRET|PASS|SIGNATURE', flags=re.I)
def cleanse_setting(self, key, value):
"""
@@ -118,9 +100,9 @@ class SafeExceptionReporterFilter:
elif isinstance(value, dict):
cleansed = {k: self.cleanse_setting(k, v) for k, v in value.items()}
elif isinstance(value, list):
cleansed = [self.cleanse_setting("", v) for v in value]
cleansed = [self.cleanse_setting('', v) for v in value]
elif isinstance(value, tuple):
cleansed = tuple([self.cleanse_setting("", v) for v in value])
cleansed = tuple([self.cleanse_setting('', v) for v in value])
else:
cleansed = value
@@ -144,7 +126,7 @@ class SafeExceptionReporterFilter:
"""
Return a dictionary of request.META with sensitive values redacted.
"""
if not hasattr(request, "META"):
if not hasattr(request, 'META'):
return {}
return {k: self.cleanse_setting(k, v) for k, v in request.META.items()}
@@ -163,7 +145,7 @@ class SafeExceptionReporterFilter:
This mitigates leaking sensitive POST parameters if something like
request.POST['nonexistent_key'] throws an exception (#21098).
"""
sensitive_post_parameters = getattr(request, "sensitive_post_parameters", [])
sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', [])
if self.is_active(request) and sensitive_post_parameters:
multivaluedict = multivaluedict.copy()
for param in sensitive_post_parameters:
@@ -179,12 +161,10 @@ class SafeExceptionReporterFilter:
if request is None:
return {}
else:
sensitive_post_parameters = getattr(
request, "sensitive_post_parameters", []
)
sensitive_post_parameters = getattr(request, 'sensitive_post_parameters', [])
if self.is_active(request) and sensitive_post_parameters:
cleansed = request.POST.copy()
if sensitive_post_parameters == "__ALL__":
if sensitive_post_parameters == '__ALL__':
# Cleanse all parameters.
for k in cleansed:
cleansed[k] = self.cleansed_substitute
@@ -205,7 +185,7 @@ class SafeExceptionReporterFilter:
# MultiValueDicts will have a return value.
is_multivalue_dict = isinstance(value, MultiValueDict)
except Exception as e:
return "{!r} while evaluating {!r}".format(e, value)
return '{!r} while evaluating {!r}'.format(e, value)
if is_multivalue_dict:
# Cleanse MultiValueDicts (request.POST is the one we usually care about)
@@ -222,20 +202,18 @@ class SafeExceptionReporterFilter:
current_frame = tb_frame.f_back
sensitive_variables = None
while current_frame is not None:
if (
current_frame.f_code.co_name == "sensitive_variables_wrapper"
and "sensitive_variables_wrapper" in current_frame.f_locals
):
if (current_frame.f_code.co_name == 'sensitive_variables_wrapper' and
'sensitive_variables_wrapper' in current_frame.f_locals):
# The sensitive_variables decorator was used, so we take note
# of the sensitive variables' names.
wrapper = current_frame.f_locals["sensitive_variables_wrapper"]
sensitive_variables = getattr(wrapper, "sensitive_variables", None)
wrapper = current_frame.f_locals['sensitive_variables_wrapper']
sensitive_variables = getattr(wrapper, 'sensitive_variables', None)
break
current_frame = current_frame.f_back
cleansed = {}
if self.is_active(request) and sensitive_variables:
if sensitive_variables == "__ALL__":
if sensitive_variables == '__ALL__':
# Cleanse all variables
for name in tb_frame.f_locals:
cleansed[name] = self.cleansed_substitute
@@ -253,16 +231,14 @@ class SafeExceptionReporterFilter:
for name, value in tb_frame.f_locals.items():
cleansed[name] = self.cleanse_special_types(request, value)
if (
tb_frame.f_code.co_name == "sensitive_variables_wrapper"
and "sensitive_variables_wrapper" in tb_frame.f_locals
):
if (tb_frame.f_code.co_name == 'sensitive_variables_wrapper' and
'sensitive_variables_wrapper' in tb_frame.f_locals):
# For good measure, obfuscate the decorated function's arguments in
# the sensitive_variables decorator's frame, in case the variables
# associated with those arguments were meant to be obfuscated from
# the decorated function's frame.
cleansed["func_args"] = self.cleansed_substitute
cleansed["func_kwargs"] = self.cleansed_substitute
cleansed['func_args'] = self.cleansed_substitute
cleansed['func_kwargs'] = self.cleansed_substitute
return cleansed.items()
@@ -272,11 +248,11 @@ class ExceptionReporter:
@property
def html_template_path(self):
return builtin_template_path("technical_500.html")
return CURRENT_DIR / 'templates' / 'technical_500.html'
@property
def text_template_path(self):
return builtin_template_path("technical_500.txt")
return CURRENT_DIR / 'templates' / 'technical_500.txt'
def __init__(self, request, exc_type, exc_value, tb, is_email=False):
self.request = request
@@ -286,21 +262,10 @@ class ExceptionReporter:
self.tb = tb
self.is_email = is_email
self.template_info = getattr(self.exc_value, "template_debug", None)
self.template_info = getattr(self.exc_value, 'template_debug', None)
self.template_does_not_exist = False
self.postmortem = None
def _get_raw_insecure_uri(self):
"""
Return an absolute URI from variables available in this request. Skip
allowed hosts protection, so may return insecure URI.
"""
return "{scheme}://{host}{path}".format(
scheme=self.request.scheme,
host=self.request._get_raw_host(),
path=self.request.get_full_path(),
)
def get_traceback_data(self):
"""Return a dictionary containing traceback information."""
if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist):
@@ -309,27 +274,26 @@ class ExceptionReporter:
frames = self.get_traceback_frames()
for i, frame in enumerate(frames):
if "vars" in frame:
if 'vars' in frame:
frame_vars = []
for k, v in frame["vars"]:
for k, v in frame['vars']:
v = pprint(v)
# Trim large blobs of data
if len(v) > 4096:
v = "%s… <trimmed %d bytes string>" % (v[0:4096], len(v))
v = '%s… <trimmed %d bytes string>' % (v[0:4096], len(v))
frame_vars.append((k, v))
frame["vars"] = frame_vars
frame['vars'] = frame_vars
frames[i] = frame
unicode_hint = ""
unicode_hint = ''
if self.exc_type and issubclass(self.exc_type, UnicodeError):
start = getattr(self.exc_value, "start", None)
end = getattr(self.exc_value, "end", None)
start = getattr(self.exc_value, 'start', None)
end = getattr(self.exc_value, 'end', None)
if start is not None and end is not None:
unicode_str = self.exc_value.args[1]
unicode_hint = force_str(
unicode_str[max(start - 5, 0) : min(end + 5, len(unicode_str))],
"ascii",
errors="replace",
unicode_str[max(start - 5, 0):min(end + 5, len(unicode_str))],
'ascii', errors='replace'
)
from django import get_version
@@ -341,60 +305,56 @@ class ExceptionReporter:
except Exception:
# request.user may raise OperationalError if the database is
# unavailable, for example.
user_str = "[unable to retrieve the current user]"
user_str = '[unable to retrieve the current user]'
c = {
"is_email": self.is_email,
"unicode_hint": unicode_hint,
"frames": frames,
"request": self.request,
"request_meta": self.filter.get_safe_request_meta(self.request),
"user_str": user_str,
"filtered_POST_items": list(
self.filter.get_post_parameters(self.request).items()
),
"settings": self.filter.get_safe_settings(),
"sys_executable": sys.executable,
"sys_version_info": "%d.%d.%d" % sys.version_info[0:3],
"server_time": timezone.now(),
"django_version_info": get_version(),
"sys_path": sys.path,
"template_info": self.template_info,
"template_does_not_exist": self.template_does_not_exist,
"postmortem": self.postmortem,
'is_email': self.is_email,
'unicode_hint': unicode_hint,
'frames': frames,
'request': self.request,
'request_meta': self.filter.get_safe_request_meta(self.request),
'user_str': user_str,
'filtered_POST_items': list(self.filter.get_post_parameters(self.request).items()),
'settings': self.filter.get_safe_settings(),
'sys_executable': sys.executable,
'sys_version_info': '%d.%d.%d' % sys.version_info[0:3],
'server_time': timezone.now(),
'django_version_info': get_version(),
'sys_path': sys.path,
'template_info': self.template_info,
'template_does_not_exist': self.template_does_not_exist,
'postmortem': self.postmortem,
}
if self.request is not None:
c["request_GET_items"] = self.request.GET.items()
c["request_FILES_items"] = self.request.FILES.items()
c["request_COOKIES_items"] = self.request.COOKIES.items()
c["request_insecure_uri"] = self._get_raw_insecure_uri()
c['request_GET_items'] = self.request.GET.items()
c['request_FILES_items'] = self.request.FILES.items()
c['request_COOKIES_items'] = self.request.COOKIES.items()
# Check whether exception info is available
if self.exc_type:
c["exception_type"] = self.exc_type.__name__
c['exception_type'] = self.exc_type.__name__
if self.exc_value:
c["exception_value"] = str(self.exc_value)
c['exception_value'] = str(self.exc_value)
if frames:
c["lastframe"] = frames[-1]
c['lastframe'] = frames[-1]
return c
def get_traceback_html(self):
"""Return HTML version of debug 500 HTTP error page."""
with self.html_template_path.open(encoding="utf-8") as fh:
with self.html_template_path.open(encoding='utf-8') as fh:
t = DEBUG_ENGINE.from_string(fh.read())
c = Context(self.get_traceback_data(), use_l10n=False)
return t.render(c)
def get_traceback_text(self):
"""Return plain text version of debug 500 HTTP error page."""
with self.text_template_path.open(encoding="utf-8") as fh:
with self.text_template_path.open(encoding='utf-8') as fh:
t = DEBUG_ENGINE.from_string(fh.read())
c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False)
return t.render(c)
def _get_source(self, filename, loader, module_name):
source = None
if hasattr(loader, "get_source"):
if hasattr(loader, 'get_source'):
try:
source = loader.get_source(module_name)
except ImportError:
@@ -403,15 +363,13 @@ class ExceptionReporter:
source = source.splitlines()
if source is None:
try:
with open(filename, "rb") as fp:
with open(filename, 'rb') as fp:
source = fp.read().splitlines()
except OSError:
pass
return source
def _get_lines_from_file(
self, filename, lineno, context_lines, loader=None, module_name=None
):
def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, module_name=None):
"""
Return context_lines before and after lineno from file.
Return (pre_context_lineno, pre_context, context_line, post_context).
@@ -424,15 +382,15 @@ class ExceptionReporter:
# apply tokenize.detect_encoding to decode the source into a
# string, then we should do that ourselves.
if isinstance(source[0], bytes):
encoding = "ascii"
encoding = 'ascii'
for line in source[:2]:
# File coding may be specified. Match pattern from PEP-263
# (https://www.python.org/dev/peps/pep-0263/)
match = re.search(rb"coding[:=]\s*([-\w.]+)", line)
match = re.search(br'coding[:=]\s*([-\w.]+)', line)
if match:
encoding = match[1].decode("ascii")
encoding = match[1].decode('ascii')
break
source = [str(sline, encoding, "replace") for sline in source]
source = [str(sline, encoding, 'replace') for sline in source]
lower_bound = max(0, lineno - context_lines)
upper_bound = lineno + context_lines
@@ -440,15 +398,15 @@ class ExceptionReporter:
try:
pre_context = source[lower_bound:lineno]
context_line = source[lineno]
post_context = source[lineno + 1 : upper_bound]
post_context = source[lineno + 1:upper_bound]
except IndexError:
return None, [], None, []
return lower_bound, pre_context, context_line, post_context
def _get_explicit_or_implicit_cause(self, exc_value):
explicit = getattr(exc_value, "__cause__", None)
suppress_context = getattr(exc_value, "__suppress_context__", None)
implicit = getattr(exc_value, "__context__", None)
explicit = getattr(exc_value, '__cause__', None)
suppress_context = getattr(exc_value, '__suppress_context__', None)
implicit = getattr(exc_value, '__context__', None)
return explicit or (None if suppress_context else implicit)
def get_traceback_frames(self):
@@ -486,58 +444,47 @@ class ExceptionReporter:
def get_exception_traceback_frames(self, exc_value, tb):
exc_cause = self._get_explicit_or_implicit_cause(exc_value)
exc_cause_explicit = getattr(exc_value, "__cause__", True)
exc_cause_explicit = getattr(exc_value, '__cause__', True)
if tb is None:
yield {
"exc_cause": exc_cause,
"exc_cause_explicit": exc_cause_explicit,
"tb": None,
"type": "user",
'exc_cause': exc_cause,
'exc_cause_explicit': exc_cause_explicit,
'tb': None,
'type': 'user',
}
while tb is not None:
# Support for __traceback_hide__ which is used by a few libraries
# to hide internal frames.
if tb.tb_frame.f_locals.get("__traceback_hide__"):
if tb.tb_frame.f_locals.get('__traceback_hide__'):
tb = tb.tb_next
continue
filename = tb.tb_frame.f_code.co_filename
function = tb.tb_frame.f_code.co_name
lineno = tb.tb_lineno - 1
loader = tb.tb_frame.f_globals.get("__loader__")
module_name = tb.tb_frame.f_globals.get("__name__") or ""
(
pre_context_lineno,
pre_context,
context_line,
post_context,
) = self._get_lines_from_file(
filename,
lineno,
7,
loader,
module_name,
loader = tb.tb_frame.f_globals.get('__loader__')
module_name = tb.tb_frame.f_globals.get('__name__') or ''
pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(
filename, lineno, 7, loader, module_name,
)
if pre_context_lineno is None:
pre_context_lineno = lineno
pre_context = []
context_line = "<source code not available>"
context_line = '<source code not available>'
post_context = []
yield {
"exc_cause": exc_cause,
"exc_cause_explicit": exc_cause_explicit,
"tb": tb,
"type": "django" if module_name.startswith("django.") else "user",
"filename": filename,
"function": function,
"lineno": lineno + 1,
"vars": self.filter.get_traceback_frame_variables(
self.request, tb.tb_frame
),
"id": id(tb),
"pre_context": pre_context,
"context_line": context_line,
"post_context": post_context,
"pre_context_lineno": pre_context_lineno + 1,
'exc_cause': exc_cause,
'exc_cause_explicit': exc_cause_explicit,
'tb': tb,
'type': 'django' if module_name.startswith('django.') else 'user',
'filename': filename,
'function': function,
'lineno': lineno + 1,
'vars': self.filter.get_traceback_frame_variables(self.request, tb.tb_frame),
'id': id(tb),
'pre_context': pre_context,
'context_line': context_line,
'post_context': post_context,
'pre_context_lineno': pre_context_lineno + 1,
}
tb = tb.tb_next
@@ -545,32 +492,30 @@ class ExceptionReporter:
def technical_404_response(request, exception):
"""Create a technical 404 error response. `exception` is the Http404."""
try:
error_url = exception.args[0]["path"]
error_url = exception.args[0]['path']
except (IndexError, TypeError, KeyError):
error_url = request.path_info[1:] # Trim leading slash
try:
tried = exception.args[0]["tried"]
tried = exception.args[0]['tried']
except (IndexError, TypeError, KeyError):
resolved = True
tried = request.resolver_match.tried if request.resolver_match else None
else:
resolved = False
if not tried or ( # empty URLconf
request.path == "/"
and len(tried) == 1
and len(tried[0]) == 1 # default URLconf
and getattr(tried[0][0], "app_name", "")
== getattr(tried[0][0], "namespace", "")
== "admin"
):
if (not tried or ( # empty URLconf
request.path == '/' and
len(tried) == 1 and # default URLconf
len(tried[0]) == 1 and
getattr(tried[0][0], 'app_name', '') == getattr(tried[0][0], 'namespace', '') == 'admin'
)):
return default_urlconf(request)
urlconf = getattr(request, "urlconf", settings.ROOT_URLCONF)
urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF)
if isinstance(urlconf, types.ModuleType):
urlconf = urlconf.__name__
caller = ""
caller = ''
try:
resolver_match = resolve(request.path)
except Http404:
@@ -578,45 +523,38 @@ def technical_404_response(request, exception):
else:
obj = resolver_match.func
if hasattr(obj, "view_class"):
obj = obj.view_class
if hasattr(obj, "__name__"):
if hasattr(obj, '__name__'):
caller = obj.__name__
elif hasattr(obj, "__class__") and hasattr(obj.__class__, "__name__"):
elif hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'):
caller = obj.__class__.__name__
if hasattr(obj, "__module__"):
if hasattr(obj, '__module__'):
module = obj.__module__
caller = "%s.%s" % (module, caller)
caller = '%s.%s' % (module, caller)
with builtin_template_path("technical_404.html").open(encoding="utf-8") as fh:
with Path(CURRENT_DIR, 'templates', 'technical_404.html').open(encoding='utf-8') as fh:
t = DEBUG_ENGINE.from_string(fh.read())
reporter_filter = get_default_exception_reporter_filter()
c = Context(
{
"urlconf": urlconf,
"root_urlconf": settings.ROOT_URLCONF,
"request_path": error_url,
"urlpatterns": tried,
"resolved": resolved,
"reason": str(exception),
"request": request,
"settings": reporter_filter.get_safe_settings(),
"raising_view_name": caller,
}
)
return HttpResponseNotFound(t.render(c), content_type="text/html")
c = Context({
'urlconf': urlconf,
'root_urlconf': settings.ROOT_URLCONF,
'request_path': error_url,
'urlpatterns': tried,
'resolved': resolved,
'reason': str(exception),
'request': request,
'settings': reporter_filter.get_safe_settings(),
'raising_view_name': caller,
})
return HttpResponseNotFound(t.render(c), content_type='text/html')
def default_urlconf(request):
"""Create an empty URLconf 404 error response."""
with builtin_template_path("default_urlconf.html").open(encoding="utf-8") as fh:
with Path(CURRENT_DIR, 'templates', 'default_urlconf.html').open(encoding='utf-8') as fh:
t = DEBUG_ENGINE.from_string(fh.read())
c = Context(
{
"version": get_docs_version(),
}
)
c = Context({
'version': get_docs_version(),
})
return HttpResponse(t.render(c), content_type="text/html")
return HttpResponse(t.render(c), content_type='text/html')
@@ -20,9 +20,7 @@ def cache_page(timeout, *, cache=None, key_prefix=None):
into account on caching -- just like the middleware does.
"""
return decorator_from_middleware_with_args(CacheMiddleware)(
page_timeout=timeout,
cache_alias=cache,
key_prefix=key_prefix,
page_timeout=timeout, cache_alias=cache, key_prefix=key_prefix,
)
@@ -30,19 +28,10 @@ def cache_control(**kwargs):
def _cache_controller(viewfunc):
@wraps(viewfunc)
def _cache_controlled(request, *args, **kw):
# Ensure argument looks like a request.
if not hasattr(request, "META"):
raise TypeError(
"cache_control didn't receive an HttpRequest. If you are "
"decorating a classmethod, be sure to use "
"@method_decorator."
)
response = viewfunc(request, *args, **kw)
patch_cache_control(response, **kwargs)
return response
return _cache_controlled
return _cache_controller
@@ -50,17 +39,9 @@ def never_cache(view_func):
"""
Decorator that adds headers to a response so that it will never be cached.
"""
@wraps(view_func)
def _wrapped_view_func(request, *args, **kwargs):
# Ensure argument looks like a request.
if not hasattr(request, "META"):
raise TypeError(
"never_cache didn't receive an HttpRequest. If you are "
"decorating a classmethod, be sure to use @method_decorator."
)
response = view_func(request, *args, **kwargs)
add_never_cache_headers(response)
return response
return _wrapped_view_func
@@ -11,13 +11,11 @@ def xframe_options_deny(view_func):
def some_view(request):
...
"""
def wrapped_view(*args, **kwargs):
resp = view_func(*args, **kwargs)
if resp.get("X-Frame-Options") is None:
resp["X-Frame-Options"] = "DENY"
if resp.get('X-Frame-Options') is None:
resp['X-Frame-Options'] = 'DENY'
return resp
return wraps(view_func)(wrapped_view)
@@ -31,13 +29,11 @@ def xframe_options_sameorigin(view_func):
def some_view(request):
...
"""
def wrapped_view(*args, **kwargs):
resp = view_func(*args, **kwargs)
if resp.get("X-Frame-Options") is None:
resp["X-Frame-Options"] = "SAMEORIGIN"
if resp.get('X-Frame-Options') is None:
resp['X-Frame-Options'] = 'SAMEORIGIN'
return resp
return wraps(view_func)(wrapped_view)
@@ -50,10 +46,8 @@ def xframe_options_exempt(view_func):
def some_view(request):
...
"""
def wrapped_view(*args, **kwargs):
resp = view_func(*args, **kwargs)
resp.xframe_options_exempt = True
return resp
return wraps(view_func)(wrapped_view)
@@ -10,6 +10,5 @@ def no_append_slash(view_func):
# nicer if they don't have side effects, so return a new function.
def wrapped_view(*args, **kwargs):
return view_func(*args, **kwargs)
wrapped_view.should_append_slash = False
return wraps(view_func)(wrapped_view)
@@ -19,7 +19,7 @@ class _EnsureCsrfToken(CsrfViewMiddleware):
requires_csrf_token = decorator_from_middleware(_EnsureCsrfToken)
requires_csrf_token.__name__ = "requires_csrf_token"
requires_csrf_token.__name__ = 'requires_csrf_token'
requires_csrf_token.__doc__ = """
Use this decorator on views that need a correct csrf_token available to
RequestContext, but without the CSRF protection that csrf_protect
@@ -39,7 +39,7 @@ class _EnsureCsrfCookie(CsrfViewMiddleware):
ensure_csrf_cookie = decorator_from_middleware(_EnsureCsrfCookie)
ensure_csrf_cookie.__name__ = "ensure_csrf_cookie"
ensure_csrf_cookie.__name__ = 'ensure_csrf_cookie'
ensure_csrf_cookie.__doc__ = """
Use this decorator to ensure that a view sets a CSRF cookie, whether or not it
uses the csrf_token template tag, or the CsrfViewMiddleware is used.
@@ -52,6 +52,5 @@ def csrf_exempt(view_func):
# if they don't have side effects, so return a new function.
def wrapped_view(*args, **kwargs):
return view_func(*args, **kwargs)
wrapped_view.csrf_exempt = True
return wraps(view_func)(wrapped_view)
@@ -28,8 +28,8 @@ def sensitive_variables(*variables):
"""
if len(variables) == 1 and callable(variables[0]):
raise TypeError(
"sensitive_variables() must be called to use it as a decorator, "
"e.g., use @sensitive_variables(), not @sensitive_variables."
'sensitive_variables() must be called to use it as a decorator, '
'e.g., use @sensitive_variables(), not @sensitive_variables.'
)
def decorator(func):
@@ -38,11 +38,9 @@ def sensitive_variables(*variables):
if variables:
sensitive_variables_wrapper.sensitive_variables = variables
else:
sensitive_variables_wrapper.sensitive_variables = "__ALL__"
sensitive_variables_wrapper.sensitive_variables = '__ALL__'
return func(*func_args, **func_kwargs)
return sensitive_variables_wrapper
return decorator
@@ -71,26 +69,23 @@ def sensitive_post_parameters(*parameters):
"""
if len(parameters) == 1 and callable(parameters[0]):
raise TypeError(
"sensitive_post_parameters() must be called to use it as a "
"decorator, e.g., use @sensitive_post_parameters(), not "
"@sensitive_post_parameters."
'sensitive_post_parameters() must be called to use it as a '
'decorator, e.g., use @sensitive_post_parameters(), not '
'@sensitive_post_parameters.'
)
def decorator(view):
@functools.wraps(view)
def sensitive_post_parameters_wrapper(request, *args, **kwargs):
if not isinstance(request, HttpRequest):
raise TypeError(
"sensitive_post_parameters didn't receive an HttpRequest "
"object. If you are decorating a classmethod, make sure "
"to use @method_decorator."
)
assert isinstance(request, HttpRequest), (
"sensitive_post_parameters didn't receive an HttpRequest. "
"If you are decorating a classmethod, be sure to use "
"@method_decorator."
)
if parameters:
request.sensitive_post_parameters = parameters
else:
request.sensitive_post_parameters = "__ALL__"
request.sensitive_post_parameters = '__ALL__'
return view(request, *args, **kwargs)
return sensitive_post_parameters_wrapper
return decorator
@@ -2,11 +2,11 @@
Decorators for views based on HTTP headers.
"""
from calendar import timegm
from functools import wraps
from django.http import HttpResponseNotAllowed
from django.middleware.http import ConditionalGetMiddleware
from django.utils import timezone
from django.utils.cache import get_conditional_response
from django.utils.decorators import decorator_from_middleware
from django.utils.http import http_date, quote_etag
@@ -26,24 +26,19 @@ def require_http_methods(request_method_list):
Note that request methods should be in uppercase.
"""
def decorator(func):
@wraps(func)
def inner(request, *args, **kwargs):
if request.method not in request_method_list:
response = HttpResponseNotAllowed(request_method_list)
log_response(
"Method Not Allowed (%s): %s",
request.method,
request.path,
'Method Not Allowed (%s): %s', request.method, request.path,
response=response,
request=request,
)
return response
return func(request, *args, **kwargs)
return inner
return decorator
@@ -54,9 +49,7 @@ require_POST = require_http_methods(["POST"])
require_POST.__doc__ = "Decorator to require that a view only accepts the POST method."
require_safe = require_http_methods(["GET", "HEAD"])
require_safe.__doc__ = (
"Decorator to require that a view only accepts safe methods: GET and HEAD."
)
require_safe.__doc__ = "Decorator to require that a view only accepts safe methods: GET and HEAD."
def condition(etag_func=None, last_modified_func=None):
@@ -81,7 +74,6 @@ def condition(etag_func=None, last_modified_func=None):
will add the generated ETag and Last-Modified headers to the response if
the headers aren't already set and if the request's method is safe.
"""
def decorator(func):
@wraps(func)
def inner(request, *args, **kwargs):
@@ -90,9 +82,7 @@ def condition(etag_func=None, last_modified_func=None):
if last_modified_func:
dt = last_modified_func(request, *args, **kwargs)
if dt:
if not timezone.is_aware(dt):
dt = timezone.make_aware(dt, timezone.utc)
return int(dt.timestamp())
return timegm(dt.utctimetuple())
# The value from etag_func() could be quoted or unquoted.
res_etag = etag_func(request, *args, **kwargs) if etag_func else None
@@ -110,16 +100,15 @@ def condition(etag_func=None, last_modified_func=None):
# Set relevant headers on the response if they don't already exist
# and if the request method is safe.
if request.method in ("GET", "HEAD"):
if res_last_modified and not response.has_header("Last-Modified"):
response.headers["Last-Modified"] = http_date(res_last_modified)
if request.method in ('GET', 'HEAD'):
if res_last_modified and not response.has_header('Last-Modified'):
response.headers['Last-Modified'] = http_date(res_last_modified)
if res_etag:
response.headers.setdefault("ETag", res_etag)
response.headers.setdefault('ETag', res_etag)
return response
return inner
return decorator
@@ -14,16 +14,13 @@ def vary_on_headers(*headers):
Note that the header names are not case-sensitive.
"""
def decorator(func):
@wraps(func)
def inner_func(*args, **kwargs):
response = func(*args, **kwargs)
patch_vary_headers(response, headers)
return response
return inner_func
return decorator
@@ -36,11 +33,9 @@ def vary_on_cookie(func):
def index(request):
...
"""
@wraps(func)
def inner_func(*args, **kwargs):
response = func(*args, **kwargs)
patch_vary_headers(response, ("Cookie",))
patch_vary_headers(response, ('Cookie',))
return response
return inner_func
+25 -31
View File
@@ -1,18 +1,16 @@
from urllib.parse import quote
from django.http import (
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound,
HttpResponseServerError,
)
from django.template import Context, Engine, TemplateDoesNotExist, loader
from django.views.decorators.csrf import requires_csrf_token
ERROR_404_TEMPLATE_NAME = "404.html"
ERROR_403_TEMPLATE_NAME = "403.html"
ERROR_400_TEMPLATE_NAME = "400.html"
ERROR_500_TEMPLATE_NAME = "500.html"
ERROR_404_TEMPLATE_NAME = '404.html'
ERROR_403_TEMPLATE_NAME = '403.html'
ERROR_400_TEMPLATE_NAME = '400.html'
ERROR_500_TEMPLATE_NAME = '500.html'
ERROR_PAGE_TEMPLATE = """
<!doctype html>
<html lang="en">
@@ -26,11 +24,9 @@ ERROR_PAGE_TEMPLATE = """
"""
# These views can be called when CsrfViewMiddleware.process_view() not run,
# This can be called when CsrfViewMiddleware.process_view has not run,
# therefore need @requires_csrf_token in case the template needs
# {% csrf_token %}.
@requires_csrf_token
def page_not_found(request, exception, template_name=ERROR_404_TEMPLATE_NAME):
"""
@@ -56,13 +52,13 @@ def page_not_found(request, exception, template_name=ERROR_404_TEMPLATE_NAME):
if isinstance(message, str):
exception_repr = message
context = {
"request_path": quote(request.path),
"exception": exception_repr,
'request_path': quote(request.path),
'exception': exception_repr,
}
try:
template = loader.get_template(template_name)
body = template.render(context, request)
content_type = None # Django will use 'text/html'.
content_type = None # Django will use 'text/html'.
except TemplateDoesNotExist:
if template_name != ERROR_404_TEMPLATE_NAME:
# Reraise if it's a missing custom template.
@@ -70,14 +66,13 @@ def page_not_found(request, exception, template_name=ERROR_404_TEMPLATE_NAME):
# Render template (even though there are no substitutions) to allow
# inspecting the context in tests.
template = Engine().from_string(
ERROR_PAGE_TEMPLATE
% {
"title": "Not Found",
"details": "The requested resource was not found on this server.",
ERROR_PAGE_TEMPLATE % {
'title': 'Not Found',
'details': 'The requested resource was not found on this server.',
},
)
body = template.render(Context(context))
content_type = "text/html"
content_type = 'text/html'
return HttpResponseNotFound(body, content_type=content_type)
@@ -96,8 +91,8 @@ def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
# Reraise if it's a missing custom template.
raise
return HttpResponseServerError(
ERROR_PAGE_TEMPLATE % {"title": "Server Error (500)", "details": ""},
content_type="text/html",
ERROR_PAGE_TEMPLATE % {'title': 'Server Error (500)', 'details': ''},
content_type='text/html',
)
return HttpResponseServerError(template.render())
@@ -117,24 +112,23 @@ def bad_request(request, exception, template_name=ERROR_400_TEMPLATE_NAME):
# Reraise if it's a missing custom template.
raise
return HttpResponseBadRequest(
ERROR_PAGE_TEMPLATE % {"title": "Bad Request (400)", "details": ""},
content_type="text/html",
ERROR_PAGE_TEMPLATE % {'title': 'Bad Request (400)', 'details': ''},
content_type='text/html',
)
# No exception content is passed to the template, to not disclose any
# sensitive information.
# No exception content is passed to the template, to not disclose any sensitive information.
return HttpResponseBadRequest(template.render())
# This can be called when CsrfViewMiddleware.process_view has not run,
# therefore need @requires_csrf_token in case the template needs
# {% csrf_token %}.
@requires_csrf_token
def permission_denied(request, exception, template_name=ERROR_403_TEMPLATE_NAME):
"""
Permission denied (403) handler.
Templates: :template:`403.html`
Context:
exception
The message from the exception which triggered the 403 (if one was
supplied).
Context: None
If the template does not exist, an Http403 response containing the text
"403 Forbidden" (as per RFC 7231) will be returned.
@@ -146,9 +140,9 @@ def permission_denied(request, exception, template_name=ERROR_403_TEMPLATE_NAME)
# Reraise if it's a missing custom template.
raise
return HttpResponseForbidden(
ERROR_PAGE_TEMPLATE % {"title": "403 Forbidden", "details": ""},
content_type="text/html",
ERROR_PAGE_TEMPLATE % {'title': '403 Forbidden', 'details': ''},
content_type='text/html',
)
return HttpResponseForbidden(
template.render(request=request, context={"exception": str(exception)})
template.render(request=request, context={'exception': str(exception)})
)
@@ -1,39 +1,22 @@
from django.views.generic.base import RedirectView, TemplateView, View
from django.views.generic.dates import (
ArchiveIndexView,
DateDetailView,
DayArchiveView,
MonthArchiveView,
TodayArchiveView,
WeekArchiveView,
YearArchiveView,
ArchiveIndexView, DateDetailView, DayArchiveView, MonthArchiveView,
TodayArchiveView, WeekArchiveView, YearArchiveView,
)
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from django.views.generic.edit import (
CreateView, DeleteView, FormView, UpdateView,
)
from django.views.generic.list import ListView
__all__ = [
"View",
"TemplateView",
"RedirectView",
"ArchiveIndexView",
"YearArchiveView",
"MonthArchiveView",
"WeekArchiveView",
"DayArchiveView",
"TodayArchiveView",
"DateDetailView",
"DetailView",
"FormView",
"CreateView",
"UpdateView",
"DeleteView",
"ListView",
"GenericViewError",
'View', 'TemplateView', 'RedirectView', 'ArchiveIndexView',
'YearArchiveView', 'MonthArchiveView', 'WeekArchiveView', 'DayArchiveView',
'TodayArchiveView', 'DateDetailView', 'DetailView', 'FormView',
'CreateView', 'UpdateView', 'DeleteView', 'ListView', 'GenericViewError',
]
class GenericViewError(Exception):
"""A problem in a generic view."""
pass
@@ -1,18 +1,16 @@
import logging
from functools import update_wrapper
from django.core.exceptions import ImproperlyConfigured
from django.http import (
HttpResponse,
HttpResponseGone,
HttpResponseNotAllowed,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
HttpResponse, HttpResponseGone, HttpResponseNotAllowed,
HttpResponsePermanentRedirect, HttpResponseRedirect,
)
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.decorators import classonlymethod
logger = logging.getLogger("django.request")
logger = logging.getLogger('django.request')
class ContextMixin:
@@ -20,11 +18,10 @@ class ContextMixin:
A default context mixin that passes the keyword arguments received by
get_context_data() as the template context.
"""
extra_context = None
def get_context_data(self, **kwargs):
kwargs.setdefault("view", self)
kwargs.setdefault('view', self)
if self.extra_context is not None:
kwargs.update(self.extra_context)
return kwargs
@@ -36,16 +33,7 @@ class View:
dispatch-by-method and simple sanity checking.
"""
http_method_names = [
"get",
"post",
"put",
"patch",
"delete",
"head",
"options",
"trace",
]
http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']
def __init__(self, **kwargs):
"""
@@ -63,44 +51,37 @@ class View:
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError(
"The method name %s is not accepted as a keyword argument "
"to %s()." % (key, cls.__name__)
'The method name %s is not accepted as a keyword argument '
'to %s().' % (key, cls.__name__)
)
if not hasattr(cls, key):
raise TypeError(
"%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key)
)
raise TypeError("%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
self.setup(request, *args, **kwargs)
if not hasattr(self, "request"):
if not hasattr(self, 'request'):
raise AttributeError(
"%s instance has no 'request' attribute. Did you override "
"setup() and forget to call super()?" % cls.__name__
)
return self.dispatch(request, *args, **kwargs)
view.view_class = cls
view.view_initkwargs = initkwargs
# __name__ and __qualname__ are intentionally left unchanged as
# view_class should be used to robustly determine the name of the view
# instead.
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
view.__annotations__ = cls.dispatch.__annotations__
# Copy possible attributes set by decorators, e.g. @csrf_exempt, from
# the dispatch method.
view.__dict__.update(cls.dispatch.__dict__)
# take name and docstring from class
update_wrapper(view, cls, updated=())
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view
def setup(self, request, *args, **kwargs):
"""Initialize attributes shared by all view methods."""
if hasattr(self, "get") and not hasattr(self, "head"):
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
self.request = request
self.args = args
@@ -111,27 +92,23 @@ class View:
# defer to the error handler. Also defer to the error handler if the
# request method isn't on the approved list.
if request.method.lower() in self.http_method_names:
handler = getattr(
self, request.method.lower(), self.http_method_not_allowed
)
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
return handler(request, *args, **kwargs)
def http_method_not_allowed(self, request, *args, **kwargs):
logger.warning(
"Method Not Allowed (%s): %s",
request.method,
request.path,
extra={"status_code": 405, "request": request},
'Method Not Allowed (%s): %s', request.method, request.path,
extra={'status_code': 405, 'request': request}
)
return HttpResponseNotAllowed(self._allowed_methods())
def options(self, request, *args, **kwargs):
"""Handle responding to requests for the OPTIONS HTTP verb."""
response = HttpResponse()
response.headers["Allow"] = ", ".join(self._allowed_methods())
response.headers["Content-Length"] = "0"
response.headers['Allow'] = ', '.join(self._allowed_methods())
response.headers['Content-Length'] = '0'
return response
def _allowed_methods(self):
@@ -140,7 +117,6 @@ class View:
class TemplateResponseMixin:
"""A mixin that can be used to render a template."""
template_name = None
template_engine = None
response_class = TemplateResponse
@@ -153,13 +129,13 @@ class TemplateResponseMixin:
Pass response_kwargs to the constructor of the response class.
"""
response_kwargs.setdefault("content_type", self.content_type)
response_kwargs.setdefault('content_type', self.content_type)
return self.response_class(
request=self.request,
template=self.get_template_names(),
context=context,
using=self.template_engine,
**response_kwargs,
**response_kwargs
)
def get_template_names(self):
@@ -170,8 +146,7 @@ class TemplateResponseMixin:
if self.template_name is None:
raise ImproperlyConfigured(
"TemplateResponseMixin requires either a definition of "
"'template_name' or an implementation of 'get_template_names()'"
)
"'template_name' or an implementation of 'get_template_names()'")
else:
return [self.template_name]
@@ -180,7 +155,6 @@ class TemplateView(TemplateResponseMixin, ContextMixin, View):
"""
Render a template. Pass keyword arguments from the URLconf to the context.
"""
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
@@ -188,7 +162,6 @@ class TemplateView(TemplateResponseMixin, ContextMixin, View):
class RedirectView(View):
"""Provide a redirect on any GET request."""
permanent = False
url = None
pattern_name = None
@@ -207,7 +180,7 @@ class RedirectView(View):
else:
return None
args = self.request.META.get("QUERY_STRING", "")
args = self.request.META.get('QUERY_STRING', '')
if args and self.query_string:
url = "%s?%s" % (url, args)
return url
@@ -221,7 +194,8 @@ class RedirectView(View):
return HttpResponseRedirect(url)
else:
logger.warning(
"Gone: %s", request.path, extra={"status_code": 410, "request": request}
'Gone: %s', request.path,
extra={'status_code': 410, 'request': request}
)
return HttpResponseGone()
@@ -9,19 +9,16 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from django.views.generic.base import View
from django.views.generic.detail import (
BaseDetailView,
SingleObjectTemplateResponseMixin,
BaseDetailView, SingleObjectTemplateResponseMixin,
)
from django.views.generic.list import (
MultipleObjectMixin,
MultipleObjectTemplateResponseMixin,
MultipleObjectMixin, MultipleObjectTemplateResponseMixin,
)
class YearMixin:
"""Mixin for views manipulating year-based data."""
year_format = "%Y"
year_format = '%Y'
year = None
def get_year_format(self):
@@ -36,21 +33,21 @@ class YearMixin:
year = self.year
if year is None:
try:
year = self.kwargs["year"]
year = self.kwargs['year']
except KeyError:
try:
year = self.request.GET["year"]
year = self.request.GET['year']
except KeyError:
raise Http404(_("No year specified"))
return year
def get_next_year(self, date):
"""Get the next valid year."""
return _get_next_prev(self, date, is_previous=False, period="year")
return _get_next_prev(self, date, is_previous=False, period='year')
def get_previous_year(self, date):
"""Get the previous valid year."""
return _get_next_prev(self, date, is_previous=True, period="year")
return _get_next_prev(self, date, is_previous=True, period='year')
def _get_next_year(self, date):
"""
@@ -70,8 +67,7 @@ class YearMixin:
class MonthMixin:
"""Mixin for views manipulating month-based data."""
month_format = "%b"
month_format = '%b'
month = None
def get_month_format(self):
@@ -86,21 +82,21 @@ class MonthMixin:
month = self.month
if month is None:
try:
month = self.kwargs["month"]
month = self.kwargs['month']
except KeyError:
try:
month = self.request.GET["month"]
month = self.request.GET['month']
except KeyError:
raise Http404(_("No month specified"))
return month
def get_next_month(self, date):
"""Get the next valid month."""
return _get_next_prev(self, date, is_previous=False, period="month")
return _get_next_prev(self, date, is_previous=False, period='month')
def get_previous_month(self, date):
"""Get the previous valid month."""
return _get_next_prev(self, date, is_previous=True, period="month")
return _get_next_prev(self, date, is_previous=True, period='month')
def _get_next_month(self, date):
"""
@@ -123,8 +119,7 @@ class MonthMixin:
class DayMixin:
"""Mixin for views manipulating day-based data."""
day_format = "%d"
day_format = '%d'
day = None
def get_day_format(self):
@@ -139,21 +134,21 @@ class DayMixin:
day = self.day
if day is None:
try:
day = self.kwargs["day"]
day = self.kwargs['day']
except KeyError:
try:
day = self.request.GET["day"]
day = self.request.GET['day']
except KeyError:
raise Http404(_("No day specified"))
return day
def get_next_day(self, date):
"""Get the next valid day."""
return _get_next_prev(self, date, is_previous=False, period="day")
return _get_next_prev(self, date, is_previous=False, period='day')
def get_previous_day(self, date):
"""Get the previous valid day."""
return _get_next_prev(self, date, is_previous=True, period="day")
return _get_next_prev(self, date, is_previous=True, period='day')
def _get_next_day(self, date):
"""
@@ -170,8 +165,7 @@ class DayMixin:
class WeekMixin:
"""Mixin for views manipulating week-based data."""
week_format = "%U"
week_format = '%U'
week = None
def get_week_format(self):
@@ -186,21 +180,21 @@ class WeekMixin:
week = self.week
if week is None:
try:
week = self.kwargs["week"]
week = self.kwargs['week']
except KeyError:
try:
week = self.request.GET["week"]
week = self.request.GET['week']
except KeyError:
raise Http404(_("No week specified"))
return week
def get_next_week(self, date):
"""Get the next valid week."""
return _get_next_prev(self, date, is_previous=False, period="week")
return _get_next_prev(self, date, is_previous=False, period='week')
def get_previous_week(self, date):
"""Get the previous valid week."""
return _get_next_prev(self, date, is_previous=True, period="week")
return _get_next_prev(self, date, is_previous=True, period='week')
def _get_next_week(self, date):
"""
@@ -224,9 +218,9 @@ class WeekMixin:
The first day according to the week format is 0 and the last day is 6.
"""
week_format = self.get_week_format()
if week_format in {"%W", "%V"}: # week starts on Monday
if week_format in {'%W', '%V'}: # week starts on Monday
return date.weekday()
elif week_format == "%U": # week starts on Sunday
elif week_format == '%U': # week starts on Sunday
return (date.weekday() + 1) % 7
else:
raise ValueError("unknown week format: %s" % week_format)
@@ -234,16 +228,13 @@ class WeekMixin:
class DateMixin:
"""Mixin class for views manipulating date-based data."""
date_field = None
allow_future = False
def get_date_field(self):
"""Get the name of the date field to be used to filter by."""
if self.date_field is None:
raise ImproperlyConfigured(
"%s.date_field is required." % self.__class__.__name__
)
raise ImproperlyConfigured("%s.date_field is required." % self.__class__.__name__)
return self.date_field
def get_allow_future(self):
@@ -291,8 +282,8 @@ class DateMixin:
since = self._make_date_lookup_arg(date)
until = self._make_date_lookup_arg(date + datetime.timedelta(days=1))
return {
"%s__gte" % date_field: since,
"%s__lt" % date_field: until,
'%s__gte' % date_field: since,
'%s__lt' % date_field: until,
}
else:
# Skip self._make_date_lookup_arg, it's a no-op in this branch.
@@ -301,29 +292,28 @@ class DateMixin:
class BaseDateListView(MultipleObjectMixin, DateMixin, View):
"""Abstract base class for date-based views displaying a list of objects."""
allow_empty = False
date_list_period = "year"
date_list_period = 'year'
def get(self, request, *args, **kwargs):
self.date_list, self.object_list, extra_context = self.get_dated_items()
context = self.get_context_data(
object_list=self.object_list, date_list=self.date_list, **extra_context
object_list=self.object_list,
date_list=self.date_list,
**extra_context
)
return self.render_to_response(context)
def get_dated_items(self):
"""Obtain the list of dates and items."""
raise NotImplementedError(
"A DateView must provide an implementation of get_dated_items()"
)
raise NotImplementedError('A DateView must provide an implementation of get_dated_items()')
def get_ordering(self):
"""
Return the field or fields to use for ordering the queryset; use the
date field by default.
"""
return "-%s" % self.get_date_field() if self.ordering is None else self.ordering
return '-%s' % self.get_date_field() if self.ordering is None else self.ordering
def get_dated_queryset(self, **lookup):
"""
@@ -338,19 +328,16 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View):
if not allow_future:
now = timezone.now() if self.uses_datetime_field else timezone_today()
qs = qs.filter(**{"%s__lte" % date_field: now})
qs = qs.filter(**{'%s__lte' % date_field: now})
if not allow_empty:
# When pagination is enabled, it's better to do a cheap query
# than to load the unpaginated queryset in memory.
is_empty = not qs if paginate_by is None else not qs.exists()
if is_empty:
raise Http404(
_("No %(verbose_name_plural)s available")
% {
"verbose_name_plural": qs.model._meta.verbose_name_plural,
}
)
raise Http404(_("No %(verbose_name_plural)s available") % {
'verbose_name_plural': qs.model._meta.verbose_name_plural,
})
return qs
@@ -361,7 +348,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View):
"""
return self.date_list_period
def get_date_list(self, queryset, date_type=None, ordering="ASC"):
def get_date_list(self, queryset, date_type=None, ordering='ASC'):
"""
Get a date list by calling `queryset.dates/datetimes()`, checking
along the way for empty lists that aren't allowed.
@@ -377,9 +364,8 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View):
date_list = queryset.dates(date_field, date_type, ordering)
if date_list is not None and not date_list and not allow_empty:
raise Http404(
_("No %(verbose_name_plural)s available")
% {
"verbose_name_plural": queryset.model._meta.verbose_name_plural,
_("No %(verbose_name_plural)s available") % {
'verbose_name_plural': queryset.model._meta.verbose_name_plural,
}
)
@@ -390,13 +376,12 @@ class BaseArchiveIndexView(BaseDateListView):
"""
Base class for archives of date-based items. Requires a response mixin.
"""
context_object_name = "latest"
context_object_name = 'latest'
def get_dated_items(self):
"""Return (date_list, items, extra_context) for this request."""
qs = self.get_dated_queryset()
date_list = self.get_date_list(qs, ordering="DESC")
date_list = self.get_date_list(qs, ordering='DESC')
if not date_list:
qs = qs.none()
@@ -406,14 +391,12 @@ class BaseArchiveIndexView(BaseDateListView):
class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView):
"""Top-level archive of date-based items."""
template_name_suffix = "_archive"
template_name_suffix = '_archive'
class BaseYearArchiveView(YearMixin, BaseDateListView):
"""List of objects published in a given year."""
date_list_period = "month"
date_list_period = 'month'
make_object_list = False
def get_dated_items(self):
@@ -426,8 +409,8 @@ class BaseYearArchiveView(YearMixin, BaseDateListView):
since = self._make_date_lookup_arg(date)
until = self._make_date_lookup_arg(self._get_next_year(date))
lookup_kwargs = {
"%s__gte" % date_field: since,
"%s__lt" % date_field: until,
'%s__gte' % date_field: since,
'%s__lt' % date_field: until,
}
qs = self.get_dated_queryset(**lookup_kwargs)
@@ -438,15 +421,11 @@ class BaseYearArchiveView(YearMixin, BaseDateListView):
# to find information about the model.
qs = qs.none()
return (
date_list,
qs,
{
"year": date,
"next_year": self.get_next_year(date),
"previous_year": self.get_previous_year(date),
},
)
return (date_list, qs, {
'year': date,
'next_year': self.get_next_year(date),
'previous_year': self.get_previous_year(date),
})
def get_make_object_list(self):
"""
@@ -458,14 +437,12 @@ class BaseYearArchiveView(YearMixin, BaseDateListView):
class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView):
"""List of objects published in a given year."""
template_name_suffix = "_archive_year"
template_name_suffix = '_archive_year'
class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
"""List of objects published in a given month."""
date_list_period = "day"
date_list_period = 'day'
def get_dated_items(self):
"""Return (date_list, items, extra_context) for this request."""
@@ -473,35 +450,29 @@ class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
month = self.get_month()
date_field = self.get_date_field()
date = _date_from_string(
year, self.get_year_format(), month, self.get_month_format()
)
date = _date_from_string(year, self.get_year_format(),
month, self.get_month_format())
since = self._make_date_lookup_arg(date)
until = self._make_date_lookup_arg(self._get_next_month(date))
lookup_kwargs = {
"%s__gte" % date_field: since,
"%s__lt" % date_field: until,
'%s__gte' % date_field: since,
'%s__lt' % date_field: until,
}
qs = self.get_dated_queryset(**lookup_kwargs)
date_list = self.get_date_list(qs)
return (
date_list,
qs,
{
"month": date,
"next_month": self.get_next_month(date),
"previous_month": self.get_previous_month(date),
},
)
return (date_list, qs, {
'month': date,
'next_month': self.get_next_month(date),
'previous_month': self.get_previous_month(date),
})
class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView):
"""List of objects published in a given month."""
template_name_suffix = "_archive_month"
template_name_suffix = '_archive_month'
class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
@@ -514,71 +485,55 @@ class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
date_field = self.get_date_field()
week_format = self.get_week_format()
week_choices = {"%W": "1", "%U": "0", "%V": "1"}
week_choices = {'%W': '1', '%U': '0', '%V': '1'}
try:
week_start = week_choices[week_format]
except KeyError:
raise ValueError(
"Unknown week format %r. Choices are: %s"
% (
week_format,
", ".join(sorted(week_choices)),
)
)
raise ValueError('Unknown week format %r. Choices are: %s' % (
week_format,
', '.join(sorted(week_choices)),
))
year_format = self.get_year_format()
if week_format == "%V" and year_format != "%G":
if week_format == '%V' and year_format != '%G':
raise ValueError(
"ISO week directive '%s' is incompatible with the year "
"directive '%s'. Use the ISO year '%%G' instead."
% (
week_format,
year_format,
"directive '%s'. Use the ISO year '%%G' instead." % (
week_format, year_format,
)
)
date = _date_from_string(year, year_format, week_start, "%w", week, week_format)
date = _date_from_string(year, year_format, week_start, '%w', week, week_format)
since = self._make_date_lookup_arg(date)
until = self._make_date_lookup_arg(self._get_next_week(date))
lookup_kwargs = {
"%s__gte" % date_field: since,
"%s__lt" % date_field: until,
'%s__gte' % date_field: since,
'%s__lt' % date_field: until,
}
qs = self.get_dated_queryset(**lookup_kwargs)
return (
None,
qs,
{
"week": date,
"next_week": self.get_next_week(date),
"previous_week": self.get_previous_week(date),
},
)
return (None, qs, {
'week': date,
'next_week': self.get_next_week(date),
'previous_week': self.get_previous_week(date),
})
class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView):
"""List of objects published in a given week."""
template_name_suffix = "_archive_week"
template_name_suffix = '_archive_week'
class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
"""List of objects published on a given day."""
def get_dated_items(self):
"""Return (date_list, items, extra_context) for this request."""
year = self.get_year()
month = self.get_month()
day = self.get_day()
date = _date_from_string(
year,
self.get_year_format(),
month,
self.get_month_format(),
day,
self.get_day_format(),
)
date = _date_from_string(year, self.get_year_format(),
month, self.get_month_format(),
day, self.get_day_format())
return self._get_dated_items(date)
@@ -590,22 +545,17 @@ class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
lookup_kwargs = self._make_single_date_lookup(date)
qs = self.get_dated_queryset(**lookup_kwargs)
return (
None,
qs,
{
"day": date,
"previous_day": self.get_previous_day(date),
"next_day": self.get_next_day(date),
"previous_month": self.get_previous_month(date),
"next_month": self.get_next_month(date),
},
)
return (None, qs, {
'day': date,
'previous_day': self.get_previous_day(date),
'next_day': self.get_next_day(date),
'previous_month': self.get_previous_month(date),
'next_month': self.get_next_month(date)
})
class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView):
"""List of objects published on a given day."""
template_name_suffix = "_archive_day"
@@ -619,7 +569,6 @@ class BaseTodayArchiveView(BaseDayArchiveView):
class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView):
"""List of objects published today."""
template_name_suffix = "_archive_day"
@@ -628,35 +577,26 @@ class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailV
Detail view of a single object on a single date; this differs from the
standard DetailView by accepting a year/month/day in the URL.
"""
def get_object(self, queryset=None):
"""Get the object this request displays."""
year = self.get_year()
month = self.get_month()
day = self.get_day()
date = _date_from_string(
year,
self.get_year_format(),
month,
self.get_month_format(),
day,
self.get_day_format(),
)
date = _date_from_string(year, self.get_year_format(),
month, self.get_month_format(),
day, self.get_day_format())
# Use a custom queryset if provided
qs = self.get_queryset() if queryset is None else queryset
if not self.get_allow_future() and date > datetime.date.today():
raise Http404(
_(
"Future %(verbose_name_plural)s not available because "
"%(class_name)s.allow_future is False."
)
% {
"verbose_name_plural": qs.model._meta.verbose_name_plural,
"class_name": self.__class__.__name__,
}
)
raise Http404(_(
"Future %(verbose_name_plural)s not available because "
"%(class_name)s.allow_future is False."
) % {
'verbose_name_plural': qs.model._meta.verbose_name_plural,
'class_name': self.__class__.__name__,
})
# Filter down a queryset from self.queryset using the date from the
# URL. This'll get passed as the queryset to DetailView.get_object,
@@ -672,13 +612,10 @@ class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView):
Detail view of a single object on a single date; this differs from the
standard DetailView by accepting a year/month/day in the URL.
"""
template_name_suffix = "_detail"
template_name_suffix = '_detail'
def _date_from_string(
year, year_format, month="", month_format="", day="", day_format="", delim="__"
):
def _date_from_string(year, year_format, month='', month_format='', day='', day_format='', delim='__'):
"""
Get a datetime.date object given a format string and a year, month, and day
(only year is mandatory). Raise a 404 for an invalid date.
@@ -688,13 +625,10 @@ def _date_from_string(
try:
return datetime.datetime.strptime(datestr, format).date()
except ValueError:
raise Http404(
_("Invalid date string “%(datestr)s” given format “%(format)s")
% {
"datestr": datestr,
"format": format,
}
)
raise Http404(_('Invalid date string “%(datestr)s” given format “%(format)s') % {
'datestr': datestr,
'format': format,
})
def _get_next_prev(generic_view, date, is_previous, period):
@@ -727,8 +661,8 @@ def _get_next_prev(generic_view, date, is_previous, period):
allow_empty = generic_view.get_allow_empty()
allow_future = generic_view.get_allow_future()
get_current = getattr(generic_view, "_get_current_%s" % period)
get_next = getattr(generic_view, "_get_next_%s" % period)
get_current = getattr(generic_view, '_get_current_%s' % period)
get_next = getattr(generic_view, '_get_next_%s' % period)
# Bounds of the current interval
start, end = get_current(date), get_next(date)
@@ -752,10 +686,10 @@ def _get_next_prev(generic_view, date, is_previous, period):
# Construct a lookup and an ordering depending on whether we're doing
# a previous date or a next date lookup.
if is_previous:
lookup = {"%s__lt" % date_field: generic_view._make_date_lookup_arg(start)}
ordering = "-%s" % date_field
lookup = {'%s__lt' % date_field: generic_view._make_date_lookup_arg(start)}
ordering = '-%s' % date_field
else:
lookup = {"%s__gte" % date_field: generic_view._make_date_lookup_arg(end)}
lookup = {'%s__gte' % date_field: generic_view._make_date_lookup_arg(end)}
ordering = date_field
# Filter out objects in the future if appropriate.
@@ -766,7 +700,7 @@ def _get_next_prev(generic_view, date, is_previous, period):
now = timezone.now()
else:
now = timezone_today()
lookup["%s__lte" % date_field] = now
lookup['%s__lte' % date_field] = now
qs = generic_view.get_queryset().filter(**lookup).order_by(ordering)
@@ -9,13 +9,12 @@ class SingleObjectMixin(ContextMixin):
"""
Provide the ability to retrieve a single object for further manipulation.
"""
model = None
queryset = None
slug_field = "slug"
slug_field = 'slug'
context_object_name = None
slug_url_kwarg = "slug"
pk_url_kwarg = "pk"
slug_url_kwarg = 'slug'
pk_url_kwarg = 'pk'
query_pk_and_slug = False
def get_object(self, queryset=None):
@@ -52,10 +51,8 @@ class SingleObjectMixin(ContextMixin):
# Get the single item from the filtered queryset
obj = queryset.get()
except queryset.model.DoesNotExist:
raise Http404(
_("No %(verbose_name)s found matching the query")
% {"verbose_name": queryset.model._meta.verbose_name}
)
raise Http404(_("No %(verbose_name)s found matching the query") %
{'verbose_name': queryset.model._meta.verbose_name})
return obj
def get_queryset(self):
@@ -72,7 +69,9 @@ class SingleObjectMixin(ContextMixin):
raise ImproperlyConfigured(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
"%(cls)s.get_queryset()." % {
'cls': self.__class__.__name__
}
)
return self.queryset.all()
@@ -93,7 +92,7 @@ class SingleObjectMixin(ContextMixin):
"""Insert the single object into the context dict."""
context = {}
if self.object:
context["object"] = self.object
context['object'] = self.object
context_object_name = self.get_context_object_name(self.object)
if context_object_name:
context[context_object_name] = self.object
@@ -103,7 +102,6 @@ class SingleObjectMixin(ContextMixin):
class BaseDetailView(SingleObjectMixin, View):
"""A base view for displaying a single object."""
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
@@ -112,7 +110,7 @@ class BaseDetailView(SingleObjectMixin, View):
class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
template_name_field = None
template_name_suffix = "_detail"
template_name_suffix = '_detail'
def get_template_names(self):
"""
@@ -143,25 +141,17 @@ class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
# only use this if the object in question is a model.
if isinstance(self.object, models.Model):
object_meta = self.object._meta
names.append(
"%s/%s%s.html"
% (
object_meta.app_label,
object_meta.model_name,
self.template_name_suffix,
)
)
elif getattr(self, "model", None) is not None and issubclass(
self.model, models.Model
):
names.append(
"%s/%s%s.html"
% (
self.model._meta.app_label,
self.model._meta.model_name,
self.template_name_suffix,
)
)
names.append("%s/%s%s.html" % (
object_meta.app_label,
object_meta.model_name,
self.template_name_suffix
))
elif getattr(self, 'model', None) is not None and issubclass(self.model, models.Model):
names.append("%s/%s%s.html" % (
self.model._meta.app_label,
self.model._meta.model_name,
self.template_name_suffix
))
# If we still haven't managed to find any template names, we should
# re-raise the ImproperlyConfigured to alert the user.
@@ -1,20 +1,14 @@
import warnings
from django.core.exceptions import ImproperlyConfigured
from django.forms import Form
from django.forms import models as model_forms
from django.http import HttpResponseRedirect
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
from django.views.generic.detail import (
BaseDetailView,
SingleObjectMixin,
SingleObjectTemplateResponseMixin,
BaseDetailView, SingleObjectMixin, SingleObjectTemplateResponseMixin,
)
class FormMixin(ContextMixin):
"""Provide a way to show and handle a form in a request."""
initial = {}
form_class = None
success_url = None
@@ -41,17 +35,15 @@ class FormMixin(ContextMixin):
def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""
kwargs = {
"initial": self.get_initial(),
"prefix": self.get_prefix(),
'initial': self.get_initial(),
'prefix': self.get_prefix(),
}
if self.request.method in ("POST", "PUT"):
kwargs.update(
{
"data": self.request.POST,
"files": self.request.FILES,
}
)
if self.request.method in ('POST', 'PUT'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
})
return kwargs
def get_success_url(self):
@@ -70,14 +62,13 @@ class FormMixin(ContextMixin):
def get_context_data(self, **kwargs):
"""Insert the form into the context dict."""
if "form" not in kwargs:
kwargs["form"] = self.get_form()
if 'form' not in kwargs:
kwargs['form'] = self.get_form()
return super().get_context_data(**kwargs)
class ModelFormMixin(FormMixin, SingleObjectMixin):
"""Provide a way to show and handle a ModelForm in a request."""
fields = None
def get_form_class(self):
@@ -92,7 +83,7 @@ class ModelFormMixin(FormMixin, SingleObjectMixin):
if self.model is not None:
# If a model has been explicitly provided, use it
model = self.model
elif getattr(self, "object", None) is not None:
elif getattr(self, 'object', None) is not None:
# If this view is operating on a single object, use
# the class of that object
model = self.object.__class__
@@ -112,8 +103,8 @@ class ModelFormMixin(FormMixin, SingleObjectMixin):
def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""
kwargs = super().get_form_kwargs()
if hasattr(self, "object"):
kwargs.update({"instance": self.object})
if hasattr(self, 'object'):
kwargs.update({'instance': self.object})
return kwargs
def get_success_url(self):
@@ -126,8 +117,7 @@ class ModelFormMixin(FormMixin, SingleObjectMixin):
except AttributeError:
raise ImproperlyConfigured(
"No URL to redirect to. Either provide a url or define"
" a get_absolute_url method on the Model."
)
" a get_absolute_url method on the Model.")
return url
def form_valid(self, form):
@@ -138,7 +128,6 @@ class ModelFormMixin(FormMixin, SingleObjectMixin):
class ProcessFormView(View):
"""Render a form on GET and processes it on POST."""
def get(self, request, *args, **kwargs):
"""Handle GET requests: instantiate a blank version of the form."""
return self.render_to_response(self.get_context_data())
@@ -174,7 +163,6 @@ class BaseCreateView(ModelFormMixin, ProcessFormView):
Using this base class requires subclassing to provide a response mixin.
"""
def get(self, request, *args, **kwargs):
self.object = None
return super().get(request, *args, **kwargs)
@@ -188,8 +176,7 @@ class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
"""
View for creating a new object, with a response rendered by a template.
"""
template_name_suffix = "_form"
template_name_suffix = '_form'
class BaseUpdateView(ModelFormMixin, ProcessFormView):
@@ -198,7 +185,6 @@ class BaseUpdateView(ModelFormMixin, ProcessFormView):
Using this base class requires subclassing to provide a response mixin.
"""
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
@@ -210,13 +196,11 @@ class BaseUpdateView(ModelFormMixin, ProcessFormView):
class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView):
"""View for updating an object, with a response rendered by a template."""
template_name_suffix = "_form"
template_name_suffix = '_form'
class DeletionMixin:
"""Provide the ability to delete objects."""
success_url = None
def delete(self, request, *args, **kwargs):
@@ -237,58 +221,21 @@ class DeletionMixin:
if self.success_url:
return self.success_url.format(**self.object.__dict__)
else:
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
raise ImproperlyConfigured(
"No URL to redirect to. Provide a success_url.")
# RemovedInDjango50Warning.
class DeleteViewCustomDeleteWarning(Warning):
pass
class BaseDeleteView(DeletionMixin, FormMixin, BaseDetailView):
class BaseDeleteView(DeletionMixin, BaseDetailView):
"""
Base view for deleting an object.
Using this base class requires subclassing to provide a response mixin.
"""
form_class = Form
def __init__(self, *args, **kwargs):
# RemovedInDjango50Warning.
if self.__class__.delete is not DeletionMixin.delete:
warnings.warn(
f"DeleteView uses FormMixin to handle POST requests. As a "
f"consequence, any custom deletion logic in "
f"{self.__class__.__name__}.delete() handler should be moved "
f"to form_valid().",
DeleteViewCustomDeleteWarning,
stacklevel=2,
)
super().__init__(*args, **kwargs)
def post(self, request, *args, **kwargs):
# Set self.object before the usual form processing flow.
# Inlined because having DeletionMixin as the first base, for
# get_success_url(), makes leveraging super() with ProcessFormView
# overly complex.
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
success_url = self.get_success_url()
self.object.delete()
return HttpResponseRedirect(success_url)
class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
"""
View for deleting an object retrieved with self.get_object(), with a
response rendered by a template.
"""
template_name_suffix = "_confirm_delete"
template_name_suffix = '_confirm_delete'
@@ -8,7 +8,6 @@ from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
class MultipleObjectMixin(ContextMixin):
"""A mixin for views manipulating multiple objects."""
allow_empty = True
queryset = None
model = None
@@ -16,7 +15,7 @@ class MultipleObjectMixin(ContextMixin):
paginate_orphans = 0
context_object_name = None
paginator_class = Paginator
page_kwarg = "page"
page_kwarg = 'page'
ordering = None
def get_queryset(self):
@@ -36,7 +35,9 @@ class MultipleObjectMixin(ContextMixin):
raise ImproperlyConfigured(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
"%(cls)s.get_queryset()." % {
'cls': self.__class__.__name__
}
)
ordering = self.get_ordering()
if ordering:
@@ -53,30 +54,25 @@ class MultipleObjectMixin(ContextMixin):
def paginate_queryset(self, queryset, page_size):
"""Paginate the queryset, if needed."""
paginator = self.get_paginator(
queryset,
page_size,
orphans=self.get_paginate_orphans(),
allow_empty_first_page=self.get_allow_empty(),
)
queryset, page_size, orphans=self.get_paginate_orphans(),
allow_empty_first_page=self.get_allow_empty())
page_kwarg = self.page_kwarg
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
try:
page_number = int(page)
except ValueError:
if page == "last":
if page == 'last':
page_number = paginator.num_pages
else:
raise Http404(
_("Page is not “last”, nor can it be converted to an int.")
)
raise Http404(_('Page is not “last”, nor can it be converted to an int.'))
try:
page = paginator.page(page_number)
return (paginator, page, page.object_list, page.has_other_pages())
except InvalidPage as e:
raise Http404(
_("Invalid page (%(page_number)s): %(message)s")
% {"page_number": page_number, "message": str(e)}
)
raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
'page_number': page_number,
'message': str(e)
})
def get_paginate_by(self, queryset):
"""
@@ -84,17 +80,12 @@ class MultipleObjectMixin(ContextMixin):
"""
return self.paginate_by
def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
):
def get_paginator(self, queryset, per_page, orphans=0,
allow_empty_first_page=True, **kwargs):
"""Return an instance of the paginator for this view."""
return self.paginator_class(
queryset,
per_page,
orphans=orphans,
allow_empty_first_page=allow_empty_first_page,
**kwargs,
)
queryset, per_page, orphans=orphans,
allow_empty_first_page=allow_empty_first_page, **kwargs)
def get_paginate_orphans(self):
"""
@@ -114,8 +105,8 @@ class MultipleObjectMixin(ContextMixin):
"""Get the name of the item to be used in the context."""
if self.context_object_name:
return self.context_object_name
elif hasattr(object_list, "model"):
return "%s_list" % object_list.model._meta.model_name
elif hasattr(object_list, 'model'):
return '%s_list' % object_list.model._meta.model_name
else:
return None
@@ -125,21 +116,19 @@ class MultipleObjectMixin(ContextMixin):
page_size = self.get_paginate_by(queryset)
context_object_name = self.get_context_object_name(queryset)
if page_size:
paginator, page, queryset, is_paginated = self.paginate_queryset(
queryset, page_size
)
paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
context = {
"paginator": paginator,
"page_obj": page,
"is_paginated": is_paginated,
"object_list": queryset,
'paginator': paginator,
'page_obj': page,
'is_paginated': is_paginated,
'object_list': queryset
}
else:
context = {
"paginator": None,
"page_obj": None,
"is_paginated": False,
"object_list": queryset,
'paginator': None,
'page_obj': None,
'is_paginated': False,
'object_list': queryset
}
if context_object_name is not None:
context[context_object_name] = queryset
@@ -149,7 +138,6 @@ class MultipleObjectMixin(ContextMixin):
class BaseListView(MultipleObjectMixin, View):
"""A base view for displaying a list of objects."""
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
allow_empty = self.get_allow_empty()
@@ -158,27 +146,21 @@ class BaseListView(MultipleObjectMixin, View):
# When pagination is enabled and object_list is a queryset,
# it's better to do a cheap query than to load the unpaginated
# queryset in memory.
if self.get_paginate_by(self.object_list) is not None and hasattr(
self.object_list, "exists"
):
if self.get_paginate_by(self.object_list) is not None and hasattr(self.object_list, 'exists'):
is_empty = not self.object_list.exists()
else:
is_empty = not self.object_list
if is_empty:
raise Http404(
_("Empty list and “%(class_name)s.allow_empty” is False.")
% {
"class_name": self.__class__.__name__,
}
)
raise Http404(_('Empty list and “%(class_name)s.allow_empty” is False.') % {
'class_name': self.__class__.__name__,
})
context = self.get_context_data()
return self.render_to_response(context)
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
"""Mixin for responding with a template and list of objects."""
template_name_suffix = "_list"
template_name_suffix = '_list'
def get_template_names(self):
"""
@@ -196,18 +178,14 @@ class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
# app and model name. This name gets put at the end of the template
# name list so that user-supplied names override the automatically-
# generated ones.
if hasattr(self.object_list, "model"):
if hasattr(self.object_list, 'model'):
opts = self.object_list.model._meta
names.append(
"%s/%s%s.html"
% (opts.app_label, opts.model_name, self.template_name_suffix)
)
names.append("%s/%s%s.html" % (opts.app_label, opts.model_name, self.template_name_suffix))
elif not names:
raise ImproperlyConfigured(
"%(cls)s requires either a 'template_name' attribute "
"or a get_queryset() method that returns a QuerySet."
% {
"cls": self.__class__.__name__,
"or a get_queryset() method that returns a QuerySet." % {
'cls': self.__class__.__name__,
}
)
return names
+52 -76
View File
@@ -10,11 +10,13 @@ from django.template import Context, Engine
from django.urls import translate_url
from django.utils.formats import get_format
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import check_for_language, get_language
from django.utils.translation import (
LANGUAGE_SESSION_KEY, check_for_language, get_language,
)
from django.utils.translation.trans_real import DjangoTranslation
from django.views.generic import View
LANGUAGE_QUERY_PARAMETER = "language"
LANGUAGE_QUERY_PARAMETER = 'language'
def set_language(request):
@@ -28,32 +30,36 @@ def set_language(request):
redirect to the page in the request (the 'next' parameter) without changing
any state.
"""
next_url = request.POST.get("next", request.GET.get("next"))
next_url = request.POST.get('next', request.GET.get('next'))
if (
next_url or request.accepts("text/html")
) and not url_has_allowed_host_and_scheme(
url=next_url,
allowed_hosts={request.get_host()},
require_https=request.is_secure(),
(next_url or request.accepts('text/html')) and
not url_has_allowed_host_and_scheme(
url=next_url,
allowed_hosts={request.get_host()},
require_https=request.is_secure(),
)
):
next_url = request.META.get("HTTP_REFERER")
next_url = request.META.get('HTTP_REFERER')
if not url_has_allowed_host_and_scheme(
url=next_url,
allowed_hosts={request.get_host()},
require_https=request.is_secure(),
):
next_url = "/"
next_url = '/'
response = HttpResponseRedirect(next_url) if next_url else HttpResponse(status=204)
if request.method == "POST":
if request.method == 'POST':
lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER)
if lang_code and check_for_language(lang_code):
if next_url:
next_trans = translate_url(next_url, lang_code)
if next_trans != next_url:
response = HttpResponseRedirect(next_trans)
if hasattr(request, 'session'):
# Storing the language in the session is deprecated.
# (RemovedInDjango40Warning)
request.session[LANGUAGE_SESSION_KEY] = lang_code
response.set_cookie(
settings.LANGUAGE_COOKIE_NAME,
lang_code,
settings.LANGUAGE_COOKIE_NAME, lang_code,
max_age=settings.LANGUAGE_COOKIE_AGE,
path=settings.LANGUAGE_COOKIE_PATH,
domain=settings.LANGUAGE_COOKIE_DOMAIN,
@@ -67,20 +73,11 @@ def set_language(request):
def get_formats():
"""Return all formats strings required for i18n to work."""
FORMAT_SETTINGS = (
"DATE_FORMAT",
"DATETIME_FORMAT",
"TIME_FORMAT",
"YEAR_MONTH_FORMAT",
"MONTH_DAY_FORMAT",
"SHORT_DATE_FORMAT",
"SHORT_DATETIME_FORMAT",
"FIRST_DAY_OF_WEEK",
"DECIMAL_SEPARATOR",
"THOUSAND_SEPARATOR",
"NUMBER_GROUPING",
"DATE_INPUT_FORMATS",
"TIME_INPUT_FORMATS",
"DATETIME_INPUT_FORMATS",
'DATE_FORMAT', 'DATETIME_FORMAT', 'TIME_FORMAT',
'YEAR_MONTH_FORMAT', 'MONTH_DAY_FORMAT', 'SHORT_DATE_FORMAT',
'SHORT_DATETIME_FORMAT', 'FIRST_DAY_OF_WEEK', 'DECIMAL_SEPARATOR',
'THOUSAND_SEPARATOR', 'NUMBER_GROUPING',
'DATE_INPUT_FORMATS', 'TIME_INPUT_FORMATS', 'DATETIME_INPUT_FORMATS'
)
return {attr: get_format(attr) for attr in FORMAT_SETTINGS}
@@ -188,7 +185,7 @@ js_catalog_template = r"""
}
};
{% endautoescape %}
""" # NOQA
"""
class JavaScriptCatalog(View):
@@ -203,37 +200,31 @@ class JavaScriptCatalog(View):
want to do that as JavaScript messages go to the djangojs domain. This
might be needed if you deliver your JavaScript source from Django templates.
"""
domain = "djangojs"
domain = 'djangojs'
packages = None
def get(self, request, *args, **kwargs):
locale = get_language()
domain = kwargs.get("domain", self.domain)
domain = kwargs.get('domain', self.domain)
# If packages are not provided, default to all installed packages, as
# DjangoTranslation without localedirs harvests them all.
packages = kwargs.get("packages", "")
packages = packages.split("+") if packages else self.packages
packages = kwargs.get('packages', '')
packages = packages.split('+') if packages else self.packages
paths = self.get_paths(packages) if packages else None
self.translation = DjangoTranslation(locale, domain=domain, localedirs=paths)
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def get_paths(self, packages):
allowable_packages = {
app_config.name: app_config for app_config in apps.get_app_configs()
}
app_configs = [
allowable_packages[p] for p in packages if p in allowable_packages
]
allowable_packages = {app_config.name: app_config for app_config in apps.get_app_configs()}
app_configs = [allowable_packages[p] for p in packages if p in allowable_packages]
if len(app_configs) < len(packages):
excluded = [p for p in packages if p not in allowable_packages]
raise ValueError(
"Invalid package(s) provided to JavaScriptCatalog: %s"
% ",".join(excluded)
'Invalid package(s) provided to JavaScriptCatalog: %s' % ','.join(excluded)
)
# paths of requested packages
return [os.path.join(app.path, "locale") for app in app_configs]
return [os.path.join(app.path, 'locale') for app in app_configs]
@property
def _num_plurals(self):
@@ -241,7 +232,7 @@ class JavaScriptCatalog(View):
Return the number of plurals for this catalog language, or 2 if no
plural string is available.
"""
match = re.search(r"nplurals=\s*(\d+)", self._plural_string or "")
match = re.search(r'nplurals=\s*(\d+)', self._plural_string or '')
if match:
return int(match[1])
return 2
@@ -252,10 +243,10 @@ class JavaScriptCatalog(View):
Return the plural string (including nplurals) for this catalog language,
or None if no plural string is available.
"""
if "" in self.translation._catalog:
for line in self.translation._catalog[""].split("\n"):
if line.startswith("Plural-Forms:"):
return line.split(":", 1)[1].strip()
if '' in self.translation._catalog:
for line in self.translation._catalog[''].split('\n'):
if line.startswith('Plural-Forms:'):
return line.split(':', 1)[1].strip()
return None
def get_plural(self):
@@ -264,11 +255,7 @@ class JavaScriptCatalog(View):
# This should be a compiled function of a typical plural-form:
# Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 :
# n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
plural = [
el.strip()
for el in plural.split(";")
if el.strip().startswith("plural=")
][0].split("=", 1)[1]
plural = [el.strip() for el in plural.split(';') if el.strip().startswith('plural=')][0].split('=', 1)[1]
return plural
def get_catalog(self):
@@ -276,14 +263,10 @@ class JavaScriptCatalog(View):
num_plurals = self._num_plurals
catalog = {}
trans_cat = self.translation._catalog
trans_fallback_cat = (
self.translation._fallback._catalog if self.translation._fallback else {}
)
trans_fallback_cat = self.translation._fallback._catalog if self.translation._fallback else {}
seen_keys = set()
for key, value in itertools.chain(
trans_cat.items(), trans_fallback_cat.items()
):
if key == "" or key in seen_keys:
for key, value in itertools.chain(trans_cat.items(), trans_fallback_cat.items()):
if key == '' or key in seen_keys:
continue
if isinstance(key, str):
catalog[key] = value
@@ -294,33 +277,27 @@ class JavaScriptCatalog(View):
raise TypeError(key)
seen_keys.add(key)
for k, v in pdict.items():
catalog[k] = [v.get(i, "") for i in range(num_plurals)]
catalog[k] = [v.get(i, '') for i in range(num_plurals)]
return catalog
def get_context_data(self, **kwargs):
return {
"catalog": self.get_catalog(),
"formats": get_formats(),
"plural": self.get_plural(),
'catalog': self.get_catalog(),
'formats': get_formats(),
'plural': self.get_plural(),
}
def render_to_response(self, context, **response_kwargs):
def indent(s):
return s.replace("\n", "\n ")
return s.replace('\n', '\n ')
template = Engine().from_string(js_catalog_template)
context["catalog_str"] = (
indent(json.dumps(context["catalog"], sort_keys=True, indent=2))
if context["catalog"]
else None
)
context["formats_str"] = indent(
json.dumps(context["formats"], sort_keys=True, indent=2)
)
context['catalog_str'] = indent(
json.dumps(context['catalog'], sort_keys=True, indent=2)
) if context['catalog'] else None
context['formats_str'] = indent(json.dumps(context['formats'], sort_keys=True, indent=2))
return HttpResponse(
template.render(Context(context)), 'text/javascript; charset="utf-8"'
)
return HttpResponse(template.render(Context(context)), 'text/javascript; charset="utf-8"')
class JSONCatalog(JavaScriptCatalog):
@@ -340,6 +317,5 @@ class JSONCatalog(JavaScriptCatalog):
"plural": '...' # Expression for plural forms, or null.
}
"""
def render_to_response(self, context, **response_kwargs):
return JsonResponse(context)
+23 -28
View File
@@ -7,12 +7,13 @@ import posixpath
import re
from pathlib import Path
from django.http import FileResponse, Http404, HttpResponse, HttpResponseNotModified
from django.http import (
FileResponse, Http404, HttpResponse, HttpResponseNotModified,
)
from django.template import Context, Engine, TemplateDoesNotExist, loader
from django.utils._os import safe_join
from django.utils.http import http_date, parse_http_date
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy
from django.utils.translation import gettext as _, gettext_lazy
def serve(request, path, document_root=None, show_indexes=False):
@@ -31,23 +32,22 @@ def serve(request, path, document_root=None, show_indexes=False):
but if you'd like to override it, you can create a template called
``static/directory_index.html``.
"""
path = posixpath.normpath(path).lstrip("/")
path = posixpath.normpath(path).lstrip('/')
fullpath = Path(safe_join(document_root, path))
if fullpath.is_dir():
if show_indexes:
return directory_index(path, fullpath)
raise Http404(_("Directory indexes are not allowed here."))
if not fullpath.exists():
raise Http404(_("%(path)s” does not exist") % {"path": fullpath})
raise Http404(_('%(path)s” does not exist') % {'path': fullpath})
# Respect the If-Modified-Since header.
statobj = fullpath.stat()
if not was_modified_since(
request.META.get("HTTP_IF_MODIFIED_SINCE"), statobj.st_mtime, statobj.st_size
):
if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
statobj.st_mtime, statobj.st_size):
return HttpResponseNotModified()
content_type, encoding = mimetypes.guess_type(str(fullpath))
content_type = content_type or "application/octet-stream"
response = FileResponse(fullpath.open("rb"), content_type=content_type)
content_type = content_type or 'application/octet-stream'
response = FileResponse(fullpath.open('rb'), content_type=content_type)
response.headers["Last-Modified"] = http_date(statobj.st_mtime)
if encoding:
response.headers["Content-Encoding"] = encoding
@@ -82,32 +82,26 @@ template_translatable = gettext_lazy("Index of %(directory)s")
def directory_index(path, fullpath):
try:
t = loader.select_template(
[
"static/directory_index.html",
"static/directory_index",
]
)
t = loader.select_template([
'static/directory_index.html',
'static/directory_index',
])
except TemplateDoesNotExist:
t = Engine(libraries={"i18n": "django.templatetags.i18n"}).from_string(
DEFAULT_DIRECTORY_INDEX_TEMPLATE
)
t = Engine(libraries={'i18n': 'django.templatetags.i18n'}).from_string(DEFAULT_DIRECTORY_INDEX_TEMPLATE)
c = Context()
else:
c = {}
files = []
for f in fullpath.iterdir():
if not f.name.startswith("."):
if not f.name.startswith('.'):
url = str(f.relative_to(fullpath))
if f.is_dir():
url += "/"
url += '/'
files.append(url)
c.update(
{
"directory": path + "/",
"file_list": files,
}
)
c.update({
'directory': path + '/',
'file_list': files,
})
return HttpResponse(t.render(c))
@@ -128,7 +122,8 @@ def was_modified_since(header=None, mtime=0, size=0):
try:
if header is None:
raise ValueError
matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, re.IGNORECASE)
matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
re.IGNORECASE)
header_mtime = parse_http_date(matches[1])
header_len = matches[3]
if header_len and int(header_len) != size:
@@ -16,7 +16,6 @@
h3 { margin:1em 0 .5em 0; }
h4 { margin:0 0 .5em 0; font-weight: normal; }
code, pre { font-size: 100%; white-space: pre-wrap; }
summary { cursor: pointer; }
table { border:1px solid #ccc; border-collapse: collapse; width:100%; background:white; }
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
thead th {
@@ -41,8 +40,8 @@
div.context ol.context-line li span { position:absolute; right:32px; }
.user div.context ol.context-line li { background-color:#bbb; color:#000; }
.user div.context ol li { color:#666; }
div.commands, summary.commands { margin-left: 40px; }
div.commands a, summary.commands { color:#555; text-decoration:none; }
div.commands { margin-left: 40px; }
div.commands a { color:#555; text-decoration:none; }
.user div.commands a { color: black; }
#summary { background: #ffc; }
#summary h2 { font-weight: normal; color: #666; }
@@ -72,6 +71,7 @@
}
}
window.onload = function() {
hideAll(document.querySelectorAll('table.vars'));
hideAll(document.querySelectorAll('ol.pre-context'));
hideAll(document.querySelectorAll('ol.post-context'));
hideAll(document.querySelectorAll('div.pastebin'));
@@ -85,6 +85,14 @@
}
return false;
}
function varToggle(link, id) {
toggle('v' + id);
var s = link.getElementsByTagName('span')[0];
var uarr = String.fromCharCode(0x25b6);
var darr = String.fromCharCode(0x25bc);
s.textContent = s.textContent == uarr ? darr : uarr;
return false;
}
function switchPastebinFriendly(link) {
s1 = "Switch to copy-and-paste view";
s2 = "Switch back to interactive view";
@@ -108,7 +116,7 @@
</tr>
<tr>
<th>Request URL:</th>
<td>{{ request_insecure_uri }}</td>
<td>{{ request.get_raw_uri }}</td>
</tr>
{% endif %}
<tr>
@@ -246,14 +254,13 @@
{% endif %}
{% if frame.vars %}
{% if is_email %}
<div class="commands">
<h2>Local Vars</h2>
</div>
{% else %}
<details>
<summary class="commands">Local vars</summary>
{% endif %}
<div class="commands">
{% if is_email %}
<h2>Local Vars</h2>
{% else %}
<a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a>
{% endif %}
</div>
<table class="vars" id="v{{ frame.id }}">
<thead>
<tr>
@@ -270,14 +277,13 @@
{% endfor %}
</tbody>
</table>
{% if not is_email %}</details>{% endif %}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% if not is_email %}
<form action="https://dpaste.com/" name="pasteform" id="pasteform" method="post">
{% if not is_email %}
<div id="pastebinTraceback" class="pastebin">
<input type="hidden" name="language" value="PythonConsole">
<input type="hidden" name="title"
@@ -289,7 +295,7 @@ Environment:
{% if request %}
Request Method: {{ request.META.REQUEST_METHOD }}
Request URL: {{ request_insecure_uri }}
Request URL: {{ request.get_raw_uri }}
{% endif %}
Django Version: {{ django_version_info }}
Python Version: {{ sys_version_info }}
@@ -327,9 +333,9 @@ Exception Value: {{ exception_value|force_escape }}
<input type="submit" value="Share this traceback on a public website">
</div>
</form>
{% endif %}
</div>
{% endif %}
{% endif %}
<div id="requestinfo">
<h2>Request information</h2>
@@ -2,7 +2,7 @@
{% firstof exception_value 'No exception message supplied' %}
{% if request %}
Request Method: {{ request.META.REQUEST_METHOD }}
Request URL: {{ request_insecure_uri }}{% endif %}
Request URL: {{ request.get_raw_uri }}{% endif %}
Django Version: {{ django_version_info }}
Python Executable: {{ sys_executable }}
Python Version: {{ sys_version_info }}