svthalia/concrexit

View on GitHub
website/thaliawebsite/views.py

Summary

Maintainability
A
35 mins
Test Coverage
"""General views for the website."""

from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.views import LogoutView as BaseLogoutView
from django.contrib.auth.views import PasswordResetView
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views.generic import ListView, TemplateView
from django.views.generic.base import View

from django_otp import user_has_device
from django_ratelimit.decorators import ratelimit
from two_factor.views import LoginView


class IndexView(TemplateView):
    template_name = "index.html"


@method_decorator(staff_member_required, "dispatch")
class TestCrashView(View):
    """Test view to intentionally crash to test the error handling."""

    def dispatch(self, request, *args, **kwargs) -> HttpResponse:
        if not request.user.is_superuser:
            return HttpResponseForbidden("This is not for you")
        raise Exception("Test exception")


class PagedView(ListView):
    """A ListView with automatic pagination."""

    def get_context_data(self, **kwargs) -> dict:
        context = super().get_context_data(**kwargs)
        page = context["page_obj"].number
        paginator = context["paginator"]

        # Show the two pages before and after the current page
        page_range_start = max(1, page - 2)
        page_range_stop = min(page + 3, paginator.num_pages + 1)

        # Add extra pages if we show less than 5 pages
        page_range_start = min(page_range_start, page_range_stop - 5)
        page_range_start = max(1, page_range_start)

        # Add extra pages if we still show less than 5 pages
        page_range_stop = max(page_range_stop, page_range_start + 5)
        page_range_stop = min(page_range_stop, paginator.num_pages + 1)

        page_range = range(page_range_start, page_range_stop)

        querydict = self.request.GET.copy()

        if "page" in querydict:
            del querydict["page"]

        context.update(
            {
                "page_range": page_range,
                "base_url": (
                    f"{self.request.path}?{querydict.urlencode()}&"
                    if querydict
                    else f"{self.request.path}?"
                ),
            }
        )

        return context


class RateLimitedPasswordResetView(PasswordResetView):
    @method_decorator(ratelimit(key="ip", rate="5/h"))
    def post(self, request, *args, **kwargs):
        return super().post(request, *args, **kwargs)


class RateLimitedLoginView(LoginView):
    @method_decorator(ratelimit(key="ip", rate="30/h"))
    @method_decorator(ratelimit(key="post:username", rate="30/h"))
    def post(self, request, *args, **kwargs):
        return super().post(request, *args, **kwargs)


class LogoutView(BaseLogoutView):
    # Allow GET logout still (this was deprecated in Django 5.0).
    http_method_names = ["get", "post", "options"]

    def get(self, request, *args, **kwargs):
        return self.post(request, *args, **kwargs)


def rate_limited_view(request, *args, **kwargs):
    return HttpResponse("You are rate limited", status=429)


def admin_unauthorized_view(request):
    if not request.member:
        url = "/user/account/login"
        args = request.META.get("QUERY_STRING", "")
        if args:
            url = f"{url}?{args}"
        return redirect(url)
    elif not request.member.is_staff and not request.member.is_superuser:
        raise PermissionDenied("You are not allowed to access the administration page.")
    elif not user_has_device(request.member):
        messages.error(
            request,
            "You need to set up two-factor authentication to access the administration page.",
        )
        return redirect("two_factor:setup")
    else:
        return redirect(request.GET.get("next", "/"))