mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-11 23:59:31 -06:00
et tu, mypy?
This commit is contained in:
@@ -811,17 +811,17 @@ def edit_pdf(
|
|||||||
if delete_original and len(pdf_docs) == 1:
|
if delete_original and len(pdf_docs) == 1:
|
||||||
overrides.asn = root_doc.archive_serial_number
|
overrides.asn = root_doc.archive_serial_number
|
||||||
for idx, pdf in enumerate(pdf_docs, start=1):
|
for idx, pdf in enumerate(pdf_docs, start=1):
|
||||||
filepath: Path = (
|
version_filepath: Path = (
|
||||||
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
||||||
/ f"{root_doc.id}_edit_{idx}.pdf"
|
/ f"{root_doc.id}_edit_{idx}.pdf"
|
||||||
)
|
)
|
||||||
pdf.remove_unreferenced_resources()
|
pdf.remove_unreferenced_resources()
|
||||||
pdf.save(filepath)
|
pdf.save(version_filepath)
|
||||||
consume_tasks.append(
|
consume_tasks.append(
|
||||||
consume_file.s(
|
consume_file.s(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
original_file=filepath,
|
original_file=version_filepath,
|
||||||
),
|
),
|
||||||
overrides,
|
overrides,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@@ -55,7 +56,12 @@ def _resolve_effective_doc(pk: int, request) -> Document | None:
|
|||||||
|
|
||||||
# Default behavior: if pk is a root doc, prefer its newest child version
|
# Default behavior: if pk is a root doc, prefer its newest child version
|
||||||
if request_doc.root_document_id is None:
|
if request_doc.root_document_id is None:
|
||||||
latest = root_doc.versions.only("id").order_by("id").last()
|
latest = (
|
||||||
|
Document.objects.filter(root_document=root_doc)
|
||||||
|
.only("id")
|
||||||
|
.order_by("id")
|
||||||
|
.last()
|
||||||
|
)
|
||||||
return latest or root_doc
|
return latest or root_doc
|
||||||
|
|
||||||
# pk is already a version
|
# pk is already a version
|
||||||
@@ -167,7 +173,7 @@ def preview_last_modified(request, pk: int) -> datetime | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def thumbnail_last_modified(request, pk: int) -> datetime | None:
|
def thumbnail_last_modified(request: Any, pk: int) -> datetime | None:
|
||||||
"""
|
"""
|
||||||
Returns the filesystem last modified either from cache or from filesystem.
|
Returns the filesystem last modified either from cache or from filesystem.
|
||||||
Cache should be (slightly?) faster than filesystem
|
Cache should be (slightly?) faster than filesystem
|
||||||
|
|||||||
@@ -103,9 +103,10 @@ class ConsumerStatusShortMessage(str, Enum):
|
|||||||
|
|
||||||
class ConsumerPluginMixin:
|
class ConsumerPluginMixin:
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from logging import Logger
|
||||||
from logging import LoggerAdapter
|
from logging import LoggerAdapter
|
||||||
|
|
||||||
log: "LoggerAdapter"
|
log: "LoggerAdapter" # type: ignore[type-arg]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -126,7 +127,7 @@ class ConsumerPluginMixin:
|
|||||||
f"Document root document id: {input_doc.root_document_id}",
|
f"Document root document id: {input_doc.root_document_id}",
|
||||||
)
|
)
|
||||||
root_document = Document.objects.get(pk=input_doc.root_document_id)
|
root_document = Document.objects.get(pk=input_doc.root_document_id)
|
||||||
version_index = root_document.versions.count()
|
version_index = Document.objects.filter(root_document=root_document).count()
|
||||||
self.filename += f"_v{version_index}"
|
self.filename += f"_v{version_index}"
|
||||||
|
|
||||||
def _send_progress(
|
def _send_progress(
|
||||||
@@ -530,11 +531,15 @@ class ConsumerPlugin(
|
|||||||
settings.AUDIT_LOG_ENABLED
|
settings.AUDIT_LOG_ENABLED
|
||||||
and self.metadata.actor_id is not None
|
and self.metadata.actor_id is not None
|
||||||
):
|
):
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import (
|
||||||
|
LogEntry, # type: ignore[import-untyped]
|
||||||
|
)
|
||||||
|
|
||||||
actor = User.objects.filter(pk=self.metadata.actor_id).first()
|
actor = User.objects.filter(pk=self.metadata.actor_id).first()
|
||||||
if actor is not None:
|
if actor is not None:
|
||||||
from auditlog.context import set_actor
|
from auditlog.context import ( # type: ignore[import-untyped]
|
||||||
|
set_actor,
|
||||||
|
)
|
||||||
|
|
||||||
with set_actor(actor):
|
with set_actor(actor):
|
||||||
original_document.save()
|
original_document.save()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
import pathvalidate
|
import pathvalidate
|
||||||
@@ -156,12 +155,7 @@ class StoragePath(MatchingModel):
|
|||||||
verbose_name_plural = _("storage paths")
|
verbose_name_plural = _("storage paths")
|
||||||
|
|
||||||
|
|
||||||
class Document(SoftDeleteModel, ModelWithOwner):
|
class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-missing]
|
||||||
if TYPE_CHECKING:
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
|
|
||||||
versions: "QuerySet[Document]"
|
|
||||||
|
|
||||||
correspondent = models.ForeignKey(
|
correspondent = models.ForeignKey(
|
||||||
Correspondent,
|
Correspondent,
|
||||||
blank=True,
|
blank=True,
|
||||||
@@ -1748,5 +1742,5 @@ class WorkflowRun(SoftDeleteModel):
|
|||||||
verbose_name = _("workflow run")
|
verbose_name = _("workflow run")
|
||||||
verbose_name_plural = _("workflow runs")
|
verbose_name_plural = _("workflow runs")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return f"WorkflowRun of {self.workflow} at {self.run_at} on {self.document}"
|
return f"WorkflowRun of {self.workflow} at {self.run_at} on {self.document}"
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ from datetime import datetime
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Any
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
import magic
|
import magic
|
||||||
from celery import states
|
from celery import states
|
||||||
@@ -89,6 +91,8 @@ if TYPE_CHECKING:
|
|||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from rest_framework.relations import ManyRelatedField
|
||||||
|
from rest_framework.relations import RelatedField
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.serializers")
|
logger = logging.getLogger("paperless.serializers")
|
||||||
@@ -1071,6 +1075,14 @@ class DocumentVersionInfoSerializer(serializers.Serializer):
|
|||||||
is_root = serializers.BooleanField()
|
is_root = serializers.BooleanField()
|
||||||
|
|
||||||
|
|
||||||
|
class _DocumentVersionInfo(TypedDict):
|
||||||
|
id: int
|
||||||
|
added: datetime
|
||||||
|
version_label: str | None
|
||||||
|
checksum: str | None
|
||||||
|
is_root: bool
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_serializer(
|
@extend_schema_serializer(
|
||||||
deprecate_fields=["created_date"],
|
deprecate_fields=["created_date"],
|
||||||
)
|
)
|
||||||
@@ -1091,7 +1103,9 @@ class DocumentSerializer(
|
|||||||
duplicate_documents = SerializerMethodField()
|
duplicate_documents = SerializerMethodField()
|
||||||
|
|
||||||
notes = NotesSerializer(many=True, required=False, read_only=True)
|
notes = NotesSerializer(many=True, required=False, read_only=True)
|
||||||
root_document = serializers.PrimaryKeyRelatedField(read_only=True)
|
root_document: RelatedField[Document, Document, Any] | ManyRelatedField = (
|
||||||
|
serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
|
)
|
||||||
versions = SerializerMethodField()
|
versions = SerializerMethodField()
|
||||||
|
|
||||||
custom_fields = CustomFieldInstanceSerializer(
|
custom_fields = CustomFieldInstanceSerializer(
|
||||||
@@ -1129,6 +1143,8 @@ class DocumentSerializer(
|
|||||||
@extend_schema_field(DocumentVersionInfoSerializer(many=True))
|
@extend_schema_field(DocumentVersionInfoSerializer(many=True))
|
||||||
def get_versions(self, obj):
|
def get_versions(self, obj):
|
||||||
root_doc = obj if obj.root_document_id is None else obj.root_document
|
root_doc = obj if obj.root_document_id is None else obj.root_document
|
||||||
|
if root_doc is None:
|
||||||
|
return []
|
||||||
versions_qs = Document.objects.filter(root_document=root_doc).only(
|
versions_qs = Document.objects.filter(root_document=root_doc).only(
|
||||||
"id",
|
"id",
|
||||||
"added",
|
"added",
|
||||||
@@ -1137,7 +1153,7 @@ class DocumentSerializer(
|
|||||||
)
|
)
|
||||||
versions = [*versions_qs, root_doc]
|
versions = [*versions_qs, root_doc]
|
||||||
|
|
||||||
def build_info(doc: Document) -> dict[str, object]:
|
def build_info(doc: Document) -> _DocumentVersionInfo:
|
||||||
return {
|
return {
|
||||||
"id": doc.id,
|
"id": doc.id,
|
||||||
"added": doc.added,
|
"added": doc.added,
|
||||||
@@ -2249,7 +2265,7 @@ class TasksViewSerializer(OwnedObjectSerializer):
|
|||||||
return list(duplicates.values("id", "title", "deleted_at"))
|
return list(duplicates.values("id", "title", "deleted_at"))
|
||||||
|
|
||||||
|
|
||||||
class RunTaskViewSerializer(serializers.Serializer):
|
class RunTaskViewSerializer(serializers.Serializer[dict[str, Any]]):
|
||||||
task_name = serializers.ChoiceField(
|
task_name = serializers.ChoiceField(
|
||||||
choices=PaperlessTask.TaskName.choices,
|
choices=PaperlessTask.TaskName.choices,
|
||||||
label="Task Name",
|
label="Task Name",
|
||||||
@@ -2257,7 +2273,7 @@ class RunTaskViewSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AcknowledgeTasksViewSerializer(serializers.Serializer):
|
class AcknowledgeTasksViewSerializer(serializers.Serializer[dict[str, Any]]):
|
||||||
tasks = serializers.ListField(
|
tasks = serializers.ListField(
|
||||||
required=True,
|
required=True,
|
||||||
label="Tasks",
|
label="Tasks",
|
||||||
@@ -3004,7 +3020,7 @@ class TrashSerializer(SerializerWithPerms):
|
|||||||
write_only=True,
|
write_only=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_documents(self, documents):
|
def validate_documents(self, documents: list[int]) -> list[int]:
|
||||||
count = Document.deleted_objects.filter(id__in=documents).count()
|
count = Document.deleted_objects.filter(id__in=documents).count()
|
||||||
if not count == len(documents):
|
if not count == len(documents):
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry # type: ignore[import-untyped]
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|||||||
@@ -1291,12 +1291,12 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
@mock.patch("pikepdf.open")
|
@mock.patch("pikepdf.open")
|
||||||
def test_remove_password_creates_consumable_document(
|
def test_remove_password_creates_consumable_document(
|
||||||
self,
|
self,
|
||||||
mock_open,
|
mock_open: mock.Mock,
|
||||||
mock_mkdtemp,
|
mock_mkdtemp: mock.Mock,
|
||||||
mock_consume_file,
|
mock_consume_file: mock.Mock,
|
||||||
mock_group,
|
mock_group: mock.Mock,
|
||||||
mock_chord,
|
mock_chord: mock.Mock,
|
||||||
):
|
) -> None:
|
||||||
doc = self.doc2
|
doc = self.doc2
|
||||||
temp_dir = self.dirs.scratch_dir / "remove-password"
|
temp_dir = self.dirs.scratch_dir / "remove-password"
|
||||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -1305,8 +1305,8 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
fake_pdf = mock.MagicMock()
|
fake_pdf = mock.MagicMock()
|
||||||
fake_pdf.pages = [mock.Mock(), mock.Mock()]
|
fake_pdf.pages = [mock.Mock(), mock.Mock()]
|
||||||
|
|
||||||
def save_side_effect(target_path):
|
def save_side_effect(target_path: Path) -> None:
|
||||||
Path(target_path).write_bytes(b"password removed")
|
target_path.write_bytes(b"password removed")
|
||||||
|
|
||||||
fake_pdf.save.side_effect = save_side_effect
|
fake_pdf.save.side_effect = save_side_effect
|
||||||
mock_open.return_value.__enter__.return_value = fake_pdf
|
mock_open.return_value.__enter__.return_value = fake_pdf
|
||||||
@@ -1348,13 +1348,13 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
@mock.patch("pikepdf.open")
|
@mock.patch("pikepdf.open")
|
||||||
def test_remove_password_deletes_original(
|
def test_remove_password_deletes_original(
|
||||||
self,
|
self,
|
||||||
mock_open,
|
mock_open: mock.Mock,
|
||||||
mock_mkdtemp,
|
mock_mkdtemp: mock.Mock,
|
||||||
mock_consume_file,
|
mock_consume_file: mock.Mock,
|
||||||
mock_group,
|
mock_group: mock.Mock,
|
||||||
mock_chord,
|
mock_chord: mock.Mock,
|
||||||
mock_delete,
|
mock_delete: mock.Mock,
|
||||||
):
|
) -> None:
|
||||||
doc = self.doc2
|
doc = self.doc2
|
||||||
temp_dir = self.dirs.scratch_dir / "remove-password-delete"
|
temp_dir = self.dirs.scratch_dir / "remove-password-delete"
|
||||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -1363,8 +1363,8 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
fake_pdf = mock.MagicMock()
|
fake_pdf = mock.MagicMock()
|
||||||
fake_pdf.pages = [mock.Mock(), mock.Mock()]
|
fake_pdf.pages = [mock.Mock(), mock.Mock()]
|
||||||
|
|
||||||
def save_side_effect(target_path):
|
def save_side_effect(target_path: Path) -> None:
|
||||||
Path(target_path).write_bytes(b"password removed")
|
target_path.write_bytes(b"password removed")
|
||||||
|
|
||||||
fake_pdf.save.side_effect = save_side_effect
|
fake_pdf.save.side_effect = save_side_effect
|
||||||
mock_open.return_value.__enter__.return_value = fake_pdf
|
mock_open.return_value.__enter__.return_value = fake_pdf
|
||||||
@@ -1387,7 +1387,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
mock_delete.si.assert_called_once_with([doc.id])
|
mock_delete.si.assert_called_once_with([doc.id])
|
||||||
|
|
||||||
@mock.patch("pikepdf.open")
|
@mock.patch("pikepdf.open")
|
||||||
def test_remove_password_open_failure(self, mock_open):
|
def test_remove_password_open_failure(self, mock_open: mock.Mock) -> None:
|
||||||
mock_open.side_effect = RuntimeError("wrong password")
|
mock_open.side_effect = RuntimeError("wrong password")
|
||||||
|
|
||||||
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
|
with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from collections import deque
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import mktime
|
from time import mktime
|
||||||
|
from typing import Any
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from unicodedata import normalize
|
from unicodedata import normalize
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@@ -37,8 +38,10 @@ from django.db.models import Sum
|
|||||||
from django.db.models import When
|
from django.db.models import When
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.db.models.manager import Manager
|
from django.db.models.manager import Manager
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
from django.http import FileResponse
|
from django.http import FileResponse
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.http import HttpResponseBadRequest
|
from django.http import HttpResponseBadRequest
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
@@ -83,6 +86,7 @@ from rest_framework.mixins import ListModelMixin
|
|||||||
from rest_framework.mixins import RetrieveModelMixin
|
from rest_framework.mixins import RetrieveModelMixin
|
||||||
from rest_framework.mixins import UpdateModelMixin
|
from rest_framework.mixins import UpdateModelMixin
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
@@ -825,6 +829,8 @@ class DocumentViewSet(
|
|||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
root_doc = doc if doc.root_document_id is None else doc.root_document
|
root_doc = doc if doc.root_document_id is None else doc.root_document
|
||||||
|
if root_doc is None:
|
||||||
|
raise Http404
|
||||||
if request.user is not None and not has_perms_owner_aware(
|
if request.user is not None and not has_perms_owner_aware(
|
||||||
request.user,
|
request.user,
|
||||||
"view_document",
|
"view_document",
|
||||||
@@ -889,7 +895,7 @@ class DocumentViewSet(
|
|||||||
):
|
):
|
||||||
raise Http404
|
raise Http404
|
||||||
return candidate
|
return candidate
|
||||||
latest = root_doc.versions.order_by("id").last()
|
latest = Document.objects.filter(root_document=root_doc).order_by("id").last()
|
||||||
return latest or root_doc
|
return latest or root_doc
|
||||||
|
|
||||||
def file_response(self, pk, request, disposition):
|
def file_response(self, pk, request, disposition):
|
||||||
@@ -1922,7 +1928,7 @@ class SavedViewViewSet(ModelViewSet, PassUserMixin):
|
|||||||
.prefetch_related("filter_rules")
|
.prefetch_related("filter_rules")
|
||||||
)
|
)
|
||||||
|
|
||||||
def perform_create(self, serializer) -> None:
|
def perform_create(self, serializer: serializers.BaseSerializer[Any]) -> None:
|
||||||
serializer.save(owner=self.request.user)
|
serializer.save(owner=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
@@ -3466,7 +3472,7 @@ class CustomFieldViewSet(ModelViewSet):
|
|||||||
|
|
||||||
queryset = CustomField.objects.all().order_by("-created")
|
queryset = CustomField.objects.all().order_by("-created")
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self) -> QuerySet[CustomField]:
|
||||||
filter = (
|
filter = (
|
||||||
Q(fields__document__deleted_at__isnull=True)
|
Q(fields__document__deleted_at__isnull=True)
|
||||||
if self.request.user is None or self.request.user.is_superuser
|
if self.request.user is None or self.request.user.is_superuser
|
||||||
@@ -3779,11 +3785,16 @@ class TrashView(ListModelMixin, PassUserMixin):
|
|||||||
|
|
||||||
queryset = Document.deleted_objects.all()
|
queryset = Document.deleted_objects.all()
|
||||||
|
|
||||||
def get(self, request, format=None):
|
def get(self, request: Request, format: str | None = None) -> Response:
|
||||||
self.serializer_class = DocumentSerializer
|
self.serializer_class = DocumentSerializer
|
||||||
return self.list(request, format)
|
return self.list(request, format)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(
|
||||||
|
self,
|
||||||
|
request: Request,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Response | HttpResponse:
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
@@ -3807,7 +3818,7 @@ class TrashView(ListModelMixin, PassUserMixin):
|
|||||||
return Response({"result": "OK", "doc_ids": doc_ids})
|
return Response({"result": "OK", "doc_ids": doc_ids})
|
||||||
|
|
||||||
|
|
||||||
def serve_logo(request, filename=None):
|
def serve_logo(request: HttpRequest, filename: str | None = None) -> FileResponse:
|
||||||
"""
|
"""
|
||||||
Serves the configured logo file with Content-Disposition: attachment.
|
Serves the configured logo file with Content-Disposition: attachment.
|
||||||
Prevents inline execution of SVGs. See GHSA-6p53-hqqw-8j62
|
Prevents inline execution of SVGs. See GHSA-6p53-hqqw-8j62
|
||||||
|
|||||||
Reference in New Issue
Block a user