"""Contains some global views that are not specific to a single app.
This module contains some general redirect and error views (e.g. 404) as well as specific mixins and view classes
which can be used within the apps.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, cast, Protocol, Self
from django import forms as dj_forms
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.views.generic.base import RedirectView
from django.views.generic.edit import FormMixin
from django.views.generic.list import BaseListView, ListView, MultipleObjectTemplateResponseMixin
from trustpoint.logger import LoggerMixin
if TYPE_CHECKING:
from collections.abc import Callable
from django.http import HttpRequest
[docs]
class IndexView(RedirectView):
"""View that redirects to the index home page."""
[docs]
permanent: bool = False
[docs]
pattern_name: str = 'home:dashboard'
[docs]
class ListInDetailView(ListView):
"""Helper view that combines a DetailView and a ListView.
This is useful for displaying a list within a DetailView.
Note that 'model' and 'context_object_name' refer to the ListView.
Use 'detail_model' and 'detail_context_object_name' for the DetailView.
"""
[docs]
detail_context_object_name = 'object'
[docs]
def get(self, *args: Any, **kwargs: Any) -> HttpResponse:
self.object = self.get_object()
return super().get(*args, **kwargs)
[docs]
def get_queryset_for_object(self):
return self.detail_model.objects.all()
[docs]
def get_object(self) -> models.odel:
queryset = self.get_queryset_for_object()
pk = self.kwargs.get('pk')
if pk is None:
exc_msg = 'detail object pk expected in url'
raise AttributeError(exc_msg)
return get_object_or_404(queryset, pk=pk)
[docs]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context[self.detail_context_object_name] = self.object
return context
[docs]
class SupportsGetContextData(Protocol):
"""For typing to provide super().get_context_data()."""
[docs]
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
...
[docs]
class ParentSupportsGetContextData(SupportsGetContextData, Protocol):
"""For typing to provide super().get_context_data()."""
[docs]
class SortableTableMixin[T: models.Model]:
"""Adds utility for sorting a ListView query by URL parameters.
default_sort_param must be set in the view to specify default sorting order.
"""
[docs]
default_sort_param: str
[docs]
def get_queryset(self) -> models.QuerySet[Any]:
if hasattr(self, 'queryset') and self.queryset is not None:
queryset = self.queryset
else:
queryset = self.model.objects.all()
sort_param = self.request.GET.get('sort', self.default_sort_param)
if hasattr(self.model, 'is_active'):
return queryset.order_by('-is_active', sort_param)
return queryset.order_by(sort_param)
[docs]
def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(*args, **kwargs)
# Get current sorting column
sort_param = self.request.GET.get('sort', self.default_sort_param)
# Pass sorting details to the template
context['current_sort'] = sort_param
return context
[docs]
class SortableTableFromListMixin:
"""Adds utility for sorting a ListView query by URL parameters.
default_sort_param must be set in the view to specify default sorting order.
Use instead of SortableTableMixin when you have a list of dicts instead of a Django queryset.
"""
@staticmethod
[docs]
def _sort_list_of_dicts(list_of_dicts: list[dict], sort_param: str) -> list[dict]:
"""Sorts a list of dictionaries by the given sort parameter.
Args:
list_of_dicts: List of dictionaries to sort.
sort_param: The parameter to sort by. Prefix with '-' for descending order.
Returns:
The sorted list of dictionaries.
"""
return sorted(list_of_dicts, key=lambda x: x[sort_param.lstrip('-')], reverse=sort_param.startswith('-'))
[docs]
def get_queryset(self) -> list[dict[str, str]]:
if hasattr(self, 'queryset') and self.queryset is not None:
queryset = self.queryset
else:
queryset = self.model.objects.all()
# Get sort parameter (e.g., "name" or "-name")
sort_param = self.request.GET.get('sort', self.default_sort_param)
queryset_type = type(queryset)
return self._sort_list_of_dicts(queryset, sort_param)
[docs]
def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(*args, **kwargs)
# Get current sorting column
sort_param = self.request.GET.get('sort', self.default_sort_param)
# Pass sorting details to the template
context['current_sort'] = sort_param
return context
[docs]
class ContextDataMixin:
[docs]
def get_context_data(self, **kwargs: Any) -> dict:
"""Adds attributes prefixed with context_ to the context_data if it does not exist.
Note:
If another succeeding class in the MRO has another get_context_data method,
this method will be called after setting the attributes to the context_data.
Example:
Lets consider context_page_category.
Then the attribute page_category with the value of context_page_category is
added to the context_data if page_category does not already exist in the context_data.
Example:
The following Mixin will add 'page_category': 'pki', and 'page_name': 'endpoint_profiles'
to the context data.
class EndpointProfilesExtraContextMixin(ContextDataMixin):
\"\"\"Mixin which adds context_data for the PKI -> Endpoint Profiles pages.\"\"\"
context_page_category = 'pki'
context_page_name = 'endpoint_profiles'
"""
prefix = 'context_'
for attr in dir(self):
if attr.startswith(prefix) and len(attr) > len(prefix):
kwargs.setdefault(attr[len(prefix) :], getattr(self, attr))
super_get_context_method = getattr(super(), 'get_context_data', None)
if super_get_context_method is None:
return kwargs
return super_get_context_method(**kwargs)
[docs]
class BaseBulkDeleteView(FormMixin, BaseListView):
"""Base view for bulk deletion of objects."""
[docs]
def post(self, *_args: tuple[Any], **_kwargs: dict[str, Any]) -> HttpResponse:
"""Handles POST requests to the BulkDeleteView."""
self.queryset = self.get_queryset()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
return self.form_invalid(form)
[docs]
def get_success_url(self) -> str:
"""Returns the URL to redirect to after a successful deletion."""
if self.success_url:
return self.success_url
exc_msg = 'No URL to redirect to. Provide a success_url.'
raise ImproperlyConfigured(exc_msg)
[docs]
class PrimaryKeyListFromPrimaryKeyString:
@staticmethod
[docs]
def get_pks_as_list(pks: str) -> list[str]:
if pks:
pks_list = pks.split('/')
# removing possible trailing empty string
if pks_list[-1] == '':
del pks_list[-1]
if len(pks_list) != len(set(pks_list)):
raise Http404('Duplicates in query primary key list found.')
return pks_list
return []
[docs]
class PrimaryKeyQuerysetFromUrlMixin(PrimaryKeyListFromPrimaryKeyString):
[docs]
def get_pks_path(self) -> str:
return self.kwargs.get('pks')
[docs]
def get_queryset(self) -> None | models.QuerySet:
if self.queryset:
return self.queryset
pks = self.get_pks_as_list(self.get_pks_path())
if not pks:
return self.model.objects.all()
queryset = self.model.objects.filter(pk__in=pks)
if len(pks) != len(queryset):
queryset = None
self.queryset = queryset
return queryset
[docs]
class BulkDeleteView(MultipleObjectTemplateResponseMixin, PrimaryKeyQuerysetFromUrlMixin, BaseBulkDeleteView):
pass
[docs]
THRESHOLD_LOGGER_HTTP_STATUS: int = 400
[docs]
class LoggedHttpResponse(HttpResponse, LoggerMixin):
"""Custom HttpResponse that logs and prints error messages automatically."""
def __init__(self, content: str | bytes = b'', status: int | None = None, *args: Any, **kwargs: Any) -> None:
"""Initialize the LoggedHttpResponse instance.
Args:
content (Any): The content of the response.
status (Optional[int], optional): The HTTP status code of the response. Defaults to None.
*args (Any): Additional positional arguments passed to HttpResponse.
**kwargs (Any): Additional keyword arguments passed to HttpResponse.
"""
if status and status >= THRESHOLD_LOGGER_HTTP_STATUS:
if isinstance(content, bytes):
content = content.decode('utf-8')
self.logger.error('ERROR (%s): %s', status, content)
else:
self.logger.info('SUCCESS (%s)', status)
super().__init__(content, *args, status=status, **kwargs)