from django.apps import AppConfig


class DjangoErrorsUiConfig(AppConfig):
    name = "django_custom_error_views"

django_custom_error_views/apps.py
adinhodovic/django-custom-error-views
"""
Django errors UI views
"""

from urllib.parse import quote

from django.conf import settings
from django.http import (
    HttpResponseBadRequest,
    HttpResponseForbidden,
    HttpResponseNotFound,
    HttpResponseServerError,
)
from django.template import 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"


def extract_settings(key):
    """
    Extract settings from DJANGO_CUSTOM_ERROR_VIEWS
    """
    custom_settings = getattr(settings, "DJANGO_CUSTOM_ERROR_VIEWS", None)
    if not custom_settings:
        return {}
    context = {}
    error_settings = custom_settings.get(key)
    if custom_settings.get("company_logo"):
        context["company_logo"] = custom_settings["company_logo"]

    if error_settings:
        title = error_settings.get("title")
        if title:
            context["title"] = title
        description = error_settings.get("description")
        if description:
            context["description"] = description
        extra_content = error_settings.get("extra_content")
        if extra_content:
            context["extra_content"] = extra_content
        render_exception = error_settings.get("render_exception")
        if render_exception:
            context["render_exception"] = render_exception
    return context


@requires_csrf_token
def handler404(request, exception, template_name=ERROR_404_TEMPLATE_NAME):
    """
    Default 404 handler.

    Templates: :template:`404.html`
    Context:
        request_path
            The path of the requested URL (e.g., '/app/pages/bad_page/'). It's
            quoted to prevent a content injection attack.
        exception
            The message from the exception which triggered the 404 (if one was
            supplied), or the exception class name
    """
    settings_404 = extract_settings("404")

    exception_repr = exception.__class__.__name__
    # Try to get an "interesting" exception message, if any (and not the ugly
    # Resolver404 dictionary)
    try:
        message = exception.args[0]
    except (AttributeError, IndexError):
        pass
    else:
        if isinstance(message, str):
            exception_repr = message

    context = {
        "request_path": quote(request.path),
        "exception": exception_repr,
    } | settings_404

    template = loader.get_template(template_name)
    body = template.render(context, request)
    return HttpResponseNotFound(body)


@requires_csrf_token
def handler500(request, template_name=ERROR_500_TEMPLATE_NAME):
    """
    500 error handler.
    """
    settings_500 = extract_settings("500")

    template = loader.get_template(template_name)
    body = template.render(settings_500, request)

    return HttpResponseServerError(body)


@requires_csrf_token
def handler400(
    request, exception, template_name=ERROR_400_TEMPLATE_NAME
):  # pylint: disable=unused-argument
    """
    400 error handler.
    """
    settings_400 = extract_settings("400")

    template = loader.get_template(template_name)
    body = template.render(settings_400, request)
    # No exception content is passed to the template, to not disclose any
    # sensitive information.
    return HttpResponseBadRequest(body)


@requires_csrf_token
def handler403(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).
    """
    settings_403 = extract_settings("403")

    context = {"exception": str(exception)} | settings_403
    template = loader.get_template(template_name)
    body = template.render(context, request)
    return HttpResponseForbidden(body)

django_custom_error_views/views.py
adinhodovic/django-custom-error-views
#!/usr/bin/env python
from __future__ import absolute_import, unicode_literals

import os
import sys

if __name__ == "__main__":
    os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings"
    from django.core.management import execute_from_command_line

    execute_from_command_line(sys.argv)

manage.py
adinhodovic/django-custom-error-views
from django.apps import AppConfig


class Config(AppConfig):
    name = "tests"

tests/apps.py
adinhodovic/django-custom-error-views
from pathlib import Path

DEBUG = True
USE_TZ = True

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "very-secret"

DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db.sqlite3"}}

ROOT_URLCONF = "tests.urls"

DJANGO_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.messages",
    "django.contrib.sessions",
    "django.contrib.sites",
    "django.contrib.staticfiles",
]

THIRD_PARTY_APPS = [
    "django_extensions",
    "django_custom_error_views",
    "django_browser_reload",
    "debug_toolbar",
]

LOCAL_APPS = ["tests"]

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "django_browser_reload.middleware.BrowserReloadMiddleware",
    "debug_toolbar.middleware.DebugToolbarMiddleware",
]

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ]
        },
    }
]

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
]

INTERNAL_IPS = ["127.0.0.1"]
ALLOWED_HOSTS = ["*"]
SITE_ID = 1
STATIC_URL = "/static/"
MEDIA_URL = "/media/"
MEDIA_ROOT = Path(__file__).parent / "media"

tests/settings.py
adinhodovic/django-custom-error-views
import pytest

from django_custom_error_views.views import extract_settings


def test_extract_settings(settings):
    company_logo = ("https://www.example.com/logo.png",)
    settings.DJANGO_CUSTOM_ERROR_VIEWS = {
        "company_logo": company_logo,
        "400": {
            "title": "Custom 400 error.",
            "description": "Custom 400 description.",
        },
        "403": {"extra_content": "403 extras.", "render_exception": True},
        "404": {},
    }

    assert extract_settings("400") == {
        "company_logo": company_logo,
        "title": "Custom 400 error.",
        "description": "Custom 400 description.",
    }

    assert extract_settings("403") == {
        "company_logo": company_logo,
        "extra_content": "403 extras.",
        "render_exception": True,
    }

    assert extract_settings("404") == {
        "company_logo": company_logo,
    }


@pytest.mark.parametrize(
    "page",
    [
        "400",
        "403",
        "404",
        "500",
    ],
)
def test_error_views(page, client):
    res = client.get(f"/{page}/")
    assert res.status_code == int(page)
    assert res.templates[0].name == f"{page}.html"
    assert (
        f'src="/static/django_custom_error_views/visual-{page}.jpg"'
        in res.content.decode()
    )


@pytest.mark.parametrize(
    "page",
    [
        "400",
        "403",
        "404",
        "500",
    ],
)
def test_error_views_settings(page, client, settings):
    settings.DJANGO_CUSTOM_ERROR_VIEWS = {
        "company_logo": "https://www.example.com/logo.png",
        "400": {
            "title": "Custom 400 error.",
            "description": "Custom 400 description.",
            "extra_content": "400 extras.",
        },
        "403": {
            "title": "Custom 403 Error.",
            "description": "Custom 403 description.",
            "extra_content": "403 extras.",
            "render_exception": True,
            "exception": "Permission Denied",
        },
        "404": {
            "title": "Custom 404 Error.",
            "description": "Custom 404 description.",
            "extra_content": "404 extras.",
            "render_exception": True,
            "exception": "Page not Found",
        },
        "500": {
            "title": "Custom 500 Error.",
            "description": "Custom 500 description.",
            "extra_content": "500 extras.",
        },
    }

    res = client.get(f"/{page}/")
    assert res.status_code == int(page)
    assert res.templates[0].name == f"{page}.html"

    content = res.content.decode()
    assert f'src="/static/django_custom_error_views/visual-{page}.jpg"' in content
    assert settings.DJANGO_CUSTOM_ERROR_VIEWS["company_logo"] in content

    assert settings.DJANGO_CUSTOM_ERROR_VIEWS[page]["title"] in content
    assert settings.DJANGO_CUSTOM_ERROR_VIEWS[page]["description"] in content
    assert settings.DJANGO_CUSTOM_ERROR_VIEWS[page]["extra_content"] in content
    if settings.DJANGO_CUSTOM_ERROR_VIEWS[page].get("render_exception"):
        assert settings.DJANGO_CUSTOM_ERROR_VIEWS[page]["exception"] in content

tests/test_views.py
adinhodovic/django-custom-error-views
"""
URL Configuration for django_custom_error_views
"""

from django.contrib import admin
from django.urls import include, path

from django_custom_error_views.views import handler400 as ui_handler400
from django_custom_error_views.views import handler403 as ui_handler403
from django_custom_error_views.views import handler404 as ui_handler404
from django_custom_error_views.views import handler500 as ui_handler500

handler400 = (
    "django_custom_error_views.views.handler400"  # pylint: disable=invalid-name
)
handler403 = (
    "django_custom_error_views.views.handler403"  # pylint: disable=invalid-name
)
handler404 = (
    "django_custom_error_views.views.handler404"  # pylint: disable=invalid-name
)
handler500 = (
    "django_custom_error_views.views.handler500"  # pylint: disable=invalid-name
)


urlpatterns = [
    path("admin/", admin.site.urls),
    path("__reload__/", include("django_browser_reload.urls")),
    path("__debug__/", include("debug_toolbar.urls")),
    path(
        "400/",
        ui_handler400,
        kwargs={"exception": Exception("Bad Request!")},
    ),
    path(
        "403/",
        ui_handler403,
        kwargs={"exception": Exception("Permission Denied")},
    ),
    path(
        "404/",
        ui_handler404,
        kwargs={"exception": Exception("Page not Found")},
    ),
    path("500/", ui_handler500),
]

tests/urls.py
adinhodovic/django-custom-error-views