fabiocaccamo/django-maintenance-mode

View on GitHub
maintenance_mode/http.py

Summary

Maintainability
B
6 hrs
Test Coverage
import re
import sys

from django.conf import settings
from django.contrib.auth import logout
from django.core.exceptions import ImproperlyConfigured
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.urls import NoReverseMatch, Resolver404, resolve, reverse
from django.utils.cache import add_never_cache_headers
from django.utils.module_loading import import_string

from maintenance_mode.core import get_maintenance_mode
from maintenance_mode.utils import get_client_ip_address


def get_maintenance_response_context(request):
    context = {}
    if settings.MAINTENANCE_MODE_GET_CONTEXT:
        try:
            get_request_context_func = import_string(
                settings.MAINTENANCE_MODE_GET_CONTEXT
            )
        except ImportError as error:
            raise ImproperlyConfigured(
                "settings.MAINTENANCE_MODE_GET_CONTEXT is not a valid function path."
            ) from error

        context = get_request_context_func(request=request)
    return context


def get_maintenance_response(request):
    """
    Return a '503 Service Unavailable' HTML or JSON response based on user preference.
    """
    if settings.MAINTENANCE_MODE_REDIRECT_URL:
        return redirect(settings.MAINTENANCE_MODE_REDIRECT_URL)

    response = None
    response_type = settings.MAINTENANCE_MODE_RESPONSE_TYPE
    if response_type == "html":
        response = get_maintenance_html_response(request)
    elif response_type == "json":
        response = get_maintenance_json_response(request)
    else:
        raise ImproperlyConfigured(
            "settings.MAINTENANCE_MODE_RESPONSE_TYPE value must be 'html' or 'json'."
        )

    if settings.MAINTENANCE_MODE_RETRY_AFTER:
        response["Retry-After"] = settings.MAINTENANCE_MODE_RETRY_AFTER

    add_never_cache_headers(response)
    return response


def get_maintenance_html_response(request):
    """
    Return an HTML response for maintenance.
    """
    context = get_maintenance_response_context(request)
    response = render(
        request,
        settings.MAINTENANCE_MODE_TEMPLATE,
        status=settings.MAINTENANCE_MODE_STATUS_CODE,
        context=context,
    )
    return response


def get_maintenance_json_response(request):
    """
    Return a JSON response for maintenance.
    """
    context = get_maintenance_response_context(request)
    response = JsonResponse(
        context,
        status=settings.MAINTENANCE_MODE_STATUS_CODE,
    )
    return response


def _need_maintenance_from_view(request):
    try:
        view_match = resolve(request.path)
        view_func = view_match[0]
        view_dict = view_func.__dict__

        view_force_maintenance_mode_off = view_dict.get(
            "force_maintenance_mode_off", False
        )
        if view_force_maintenance_mode_off:
            # view has 'force_maintenance_mode_off' decorator
            return False

        view_force_maintenance_mode_on = view_dict.get(
            "force_maintenance_mode_on", False
        )
        if view_force_maintenance_mode_on:
            # view has 'force_maintenance_mode_on' decorator
            return True

    except Resolver404:
        pass


def _need_maintenance_from_url(request):
    try:
        url_off = reverse("maintenance_mode_off")
        resolve(url_off)
        if url_off == request.path_info:
            return False
    except NoReverseMatch:
        # maintenance_mode.urls not added
        pass


def _need_maintenance_ignore_users(request):
    if not hasattr(request, "user"):
        return

    user = request.user

    if settings.MAINTENANCE_MODE_LOGOUT_AUTHENTICATED_USER and user.is_authenticated:
        logout(request)
        user = request.user

    if settings.MAINTENANCE_MODE_IGNORE_ANONYMOUS_USER and user.is_anonymous:
        return False

    if settings.MAINTENANCE_MODE_IGNORE_AUTHENTICATED_USER and user.is_authenticated:
        return False

    if settings.MAINTENANCE_MODE_IGNORE_STAFF and user.is_staff:
        return False

    if settings.MAINTENANCE_MODE_IGNORE_SUPERUSER and user.is_superuser:
        return False


def _need_maintenance_ignore_admin_site(request):
    if not settings.MAINTENANCE_MODE_IGNORE_ADMIN_SITE:
        return

    try:
        request_path = request.path if request.path else ""
        if not request_path.endswith("/"):
            request_path += "/"

        admin_url = reverse("admin:index")
        if request_path.startswith(admin_url):
            return False
    except NoReverseMatch:
        # admin.urls not added
        pass


def _need_maintenance_ignore_tests(request):
    if not settings.MAINTENANCE_MODE_IGNORE_TESTS:
        return

    is_testing = False

    if (len(sys.argv) > 0 and "runtests" in sys.argv[0]) or (
        len(sys.argv) > 1 and sys.argv[1] == "test"
    ):
        # python runtests.py | python manage.py test | python
        # setup.py test | django-admin.py test
        is_testing = True

    if is_testing:
        return False


def _need_maintenance_ignore_ip_addresses(request):
    if not settings.MAINTENANCE_MODE_IGNORE_IP_ADDRESSES:
        return

    if settings.MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS:
        try:
            get_client_ip_address_func = import_string(
                settings.MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS
            )
        except ImportError as error:
            raise ImproperlyConfigured(
                "settings.MAINTENANCE_MODE_GET_CLIENT_IP_ADDRESS "
                "is not a valid function path."
            ) from error
        else:
            client_ip_address = get_client_ip_address_func(request)
    else:
        client_ip_address = get_client_ip_address(request)

    for ip_address in settings.MAINTENANCE_MODE_IGNORE_IP_ADDRESSES:
        ip_address_re = re.compile(ip_address)
        if ip_address_re.match(client_ip_address):
            return False


def _need_maintenance_ignore_urls(request):
    if not settings.MAINTENANCE_MODE_IGNORE_URLS:
        return

    for url in settings.MAINTENANCE_MODE_IGNORE_URLS:
        if not isinstance(url, re.Pattern):
            url = str(url)
        url_re = re.compile(url)
        if url_re.match(request.path_info):
            return False


def _need_maintenance_redirects(request):
    if not settings.MAINTENANCE_MODE_REDIRECT_URL:
        return

    redirect_url_re = re.compile(settings.MAINTENANCE_MODE_REDIRECT_URL)

    if redirect_url_re.match(request.path_info):
        return False


def need_maintenance_response(request):
    """
    Tells if the given request needs a maintenance response or not.
    """

    value = _need_maintenance_from_view(request)
    if isinstance(value, bool):
        return value

    value = get_maintenance_mode()
    if not value:
        return value

    value = _need_maintenance_from_url(request)
    if isinstance(value, bool):
        return value

    value = _need_maintenance_ignore_users(request)
    if isinstance(value, bool):
        return value

    value = _need_maintenance_ignore_admin_site(request)
    if isinstance(value, bool):
        return value

    value = _need_maintenance_ignore_tests(request)
    if isinstance(value, bool):
        return value

    value = _need_maintenance_ignore_ip_addresses(request)
    if isinstance(value, bool):
        return value

    value = _need_maintenance_ignore_urls(request)
    if isinstance(value, bool):
        return value

    value = _need_maintenance_redirects(request)
    if isinstance(value, bool):
        return value

    return True