paperless-ngx/src/documents/views.py

1362 lines
46 KiB
Python
Raw Normal View History

import itertools
2022-04-01 15:31:10 -07:00
import json
import logging
2020-11-25 14:48:36 +01:00
import os
2022-12-08 11:10:13 -08:00
import re
import tempfile
2022-04-01 01:48:05 -07:00
import urllib
import zipfile
from datetime import datetime
from pathlib import Path
from time import mktime
from unicodedata import normalize
from urllib.parse import quote
2020-11-25 14:48:36 +01:00
2022-12-26 13:43:30 -08:00
import pathvalidate
from django.conf import settings
from django.contrib.auth.models import User
from django.db.models import Case
from django.db.models import Count
from django.db.models import IntegerField
from django.db.models import Max
from django.db.models import Sum
from django.db.models import When
2023-03-18 01:42:41 -07:00
from django.db.models.functions import Length
2020-12-28 15:59:06 +01:00
from django.db.models.functions import Lower
2023-04-25 09:59:24 -07:00
from django.http import Http404
from django.http import HttpResponse
from django.http import HttpResponseBadRequest
2023-04-25 09:59:24 -07:00
from django.http import HttpResponseForbidden
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import get_language
from django.views import View
from django.views.decorators.cache import cache_control
from django.views.generic import TemplateView
from django_filters.rest_framework import DjangoFilterBackend
2023-01-01 08:59:43 -08:00
from langdetect import detect
2022-04-01 01:48:05 -07:00
from packaging import version as packaging_version
from rest_framework import parsers
from rest_framework.decorators import action
2021-04-03 21:50:23 +02:00
from rest_framework.exceptions import NotFound
from rest_framework.filters import OrderingFilter
from rest_framework.filters import SearchFilter
2021-03-16 20:47:45 +01:00
from rest_framework.generics import GenericAPIView
2022-12-05 23:41:17 -08:00
from rest_framework.mixins import CreateModelMixin
from rest_framework.mixins import DestroyModelMixin
from rest_framework.mixins import ListModelMixin
from rest_framework.mixins import RetrieveModelMixin
from rest_framework.mixins import UpdateModelMixin
2016-03-01 18:57:12 +00:00
from rest_framework.permissions import IsAuthenticated
2020-11-12 21:09:45 +01:00
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet
from rest_framework.viewsets import ModelViewSet
2022-05-23 15:22:14 -07:00
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.viewsets import ViewSet
2023-04-25 09:59:24 -07:00
from documents import bulk_edit
from documents.bulk_download import ArchiveOnlyStrategy
from documents.bulk_download import OriginalAndArchiveStrategy
from documents.bulk_download import OriginalsOnlyStrategy
from documents.classifier import load_classifier
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource
from documents.filters import CorrespondentFilterSet
from documents.filters import DocumentFilterSet
from documents.filters import DocumentTypeFilterSet
2023-04-25 09:59:24 -07:00
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
from documents.filters import ShareLinkFilterSet
from documents.filters import StoragePathFilterSet
from documents.filters import TagFilterSet
from documents.matching import match_correspondents
from documents.matching import match_document_types
from documents.matching import match_storage_paths
from documents.matching import match_tags
from documents.models import ConsumptionTemplate
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import Document
from documents.models import DocumentType
from documents.models import Note
from documents.models import PaperlessTask
from documents.models import SavedView
from documents.models import ShareLink
from documents.models import StoragePath
from documents.models import Tag
from documents.parsers import get_parser_class_for_mime_type
from documents.parsers import parse_date_generator
2023-04-25 09:59:24 -07:00
from documents.permissions import PaperlessAdminPermissions
from documents.permissions import PaperlessObjectPermissions
2023-04-26 09:51:26 -07:00
from documents.permissions import get_objects_for_user_owner_aware
2023-04-25 09:59:24 -07:00
from documents.permissions import has_perms_owner_aware
from documents.permissions import set_permissions_for_object
from documents.serialisers import AcknowledgeTasksViewSerializer
from documents.serialisers import BulkDownloadSerializer
from documents.serialisers import BulkEditObjectPermissionsSerializer
from documents.serialisers import BulkEditSerializer
from documents.serialisers import ConsumptionTemplateSerializer
from documents.serialisers import CorrespondentSerializer
from documents.serialisers import CustomFieldSerializer
from documents.serialisers import DocumentListSerializer
from documents.serialisers import DocumentSerializer
from documents.serialisers import DocumentTypeSerializer
from documents.serialisers import PostDocumentSerializer
from documents.serialisers import SavedViewSerializer
from documents.serialisers import ShareLinkSerializer
from documents.serialisers import StoragePathSerializer
from documents.serialisers import TagSerializer
from documents.serialisers import TagSerializerVersion1
from documents.serialisers import TasksViewSerializer
from documents.serialisers import UiSettingsViewSerializer
2023-04-25 09:59:24 -07:00
from documents.tasks import consume_file
from paperless import version
from paperless.db import GnuPG
from paperless.views import StandardPagination
if settings.AUDIT_LOG_ENABLED:
from auditlog.models import LogEntry
2021-02-05 01:10:29 +01:00
logger = logging.getLogger("paperless.api")
2016-03-03 18:09:10 +00:00
class IndexView(TemplateView):
template_name = "index.html"
2016-02-16 09:28:34 +00:00
def get_frontend_language(self):
if hasattr(
self.request.user,
"ui_settings",
) and self.request.user.ui_settings.settings.get("language"):
lang = self.request.user.ui_settings.settings.get("language")
else:
lang = get_language()
2021-01-02 01:19:01 +01:00
# This is here for the following reason:
# Django identifies languages in the form "en-us"
# However, angular generates locales as "en-US".
# this translates between these two forms.
if "-" in lang:
2022-02-27 15:26:41 +01:00
first = lang[: lang.index("-")]
second = lang[lang.index("-") + 1 :]
2021-01-02 01:19:01 +01:00
return f"{first}-{second.upper()}"
else:
return lang
2020-12-17 21:46:56 +01:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2022-02-27 15:26:41 +01:00
context["cookie_prefix"] = settings.COOKIE_PREFIX
context["username"] = self.request.user.username
context["full_name"] = self.request.user.get_full_name()
context["styles_css"] = f"frontend/{self.get_frontend_language()}/styles.css"
context["runtime_js"] = f"frontend/{self.get_frontend_language()}/runtime.js"
context[
"polyfills_js"
] = f"frontend/{self.get_frontend_language()}/polyfills.js"
context["main_js"] = f"frontend/{self.get_frontend_language()}/main.js"
2022-02-27 15:26:41 +01:00
context[
"webmanifest"
2023-11-13 09:09:56 -08:00
] = f"frontend/{self.get_frontend_language()}/manifest.webmanifest"
2022-02-27 15:26:41 +01:00
context[
"apple_touch_icon"
2023-11-13 09:09:56 -08:00
] = f"frontend/{self.get_frontend_language()}/apple-touch-icon.png"
2020-12-17 21:46:56 +01:00
return context
2016-02-16 09:28:34 +00:00
2022-12-05 23:41:17 -08:00
class PassUserMixin(CreateModelMixin):
"""
Pass a user object to serializer
"""
def get_serializer(self, *args, **kwargs):
kwargs.setdefault("user", self.request.user)
kwargs.setdefault(
"full_perms",
self.request.query_params.get("full_perms", False),
)
2022-12-05 23:41:17 -08:00
return super().get_serializer(*args, **kwargs)
class CorrespondentViewSet(ModelViewSet, PassUserMixin):
model = Correspondent
2020-11-21 14:03:45 +01:00
queryset = Correspondent.objects.annotate(
document_count=Count("documents"),
last_correspondence=Max("documents__created"),
2022-02-27 15:26:41 +01:00
).order_by(Lower("name"))
2020-11-21 14:03:45 +01:00
serializer_class = CorrespondentSerializer
2016-02-21 00:55:38 +00:00
pagination_class = StandardPagination
2022-12-05 22:56:03 -08:00
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (
DjangoFilterBackend,
OrderingFilter,
2023-01-30 14:37:09 -08:00
ObjectOwnedOrGrantedPermissionsFilter,
2022-12-05 22:56:03 -08:00
)
2020-11-17 22:31:43 +01:00
filterset_class = CorrespondentFilterSet
2020-11-21 14:03:45 +01:00
ordering_fields = (
"name",
"matching_algorithm",
"match",
"document_count",
2022-02-27 15:26:41 +01:00
"last_correspondence",
)
2016-02-16 09:28:34 +00:00
2023-03-08 19:03:59 -08:00
class TagViewSet(ModelViewSet, PassUserMixin):
2016-02-16 09:28:34 +00:00
model = Tag
2020-11-21 14:03:45 +01:00
2022-02-27 15:26:41 +01:00
queryset = Tag.objects.annotate(document_count=Count("documents")).order_by(
Lower("name"),
2022-02-27 15:26:41 +01:00
)
2020-11-21 14:03:45 +01:00
2022-12-06 20:14:33 -08:00
def get_serializer_class(self, *args, **kwargs):
2021-02-24 23:54:19 +01:00
if int(self.request.version) == 1:
return TagSerializerVersion1
else:
return TagSerializer
2016-02-21 00:55:38 +00:00
pagination_class = StandardPagination
2022-12-05 22:56:03 -08:00
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
2022-12-06 20:14:33 -08:00
filter_backends = (
DjangoFilterBackend,
OrderingFilter,
2023-01-30 14:37:09 -08:00
ObjectOwnedOrGrantedPermissionsFilter,
2022-12-06 20:14:33 -08:00
)
2020-11-17 22:31:43 +01:00
filterset_class = TagFilterSet
2023-01-17 06:32:46 -08:00
ordering_fields = ("color", "name", "matching_algorithm", "match", "document_count")
2016-02-16 09:28:34 +00:00
2022-12-05 23:41:17 -08:00
class DocumentTypeViewSet(ModelViewSet, PassUserMixin):
2018-09-05 15:25:14 +02:00
model = DocumentType
2020-11-21 14:03:45 +01:00
queryset = DocumentType.objects.annotate(
document_count=Count("documents"),
2022-02-27 15:26:41 +01:00
).order_by(Lower("name"))
2020-11-21 14:03:45 +01:00
2018-09-05 15:25:14 +02:00
serializer_class = DocumentTypeSerializer
pagination_class = StandardPagination
2022-12-05 22:56:03 -08:00
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
2022-12-06 20:14:33 -08:00
filter_backends = (
DjangoFilterBackend,
OrderingFilter,
2023-01-30 14:37:09 -08:00
ObjectOwnedOrGrantedPermissionsFilter,
2022-12-06 20:14:33 -08:00
)
2020-11-17 22:31:43 +01:00
filterset_class = DocumentTypeFilterSet
ordering_fields = ("name", "matching_algorithm", "match", "document_count")
2018-09-05 15:25:14 +02:00
2022-02-27 15:26:41 +01:00
class DocumentViewSet(
PassUserMixin,
2022-02-27 15:26:41 +01:00
RetrieveModelMixin,
UpdateModelMixin,
DestroyModelMixin,
ListModelMixin,
GenericViewSet,
):
2016-02-16 09:28:34 +00:00
model = Document
2023-03-17 16:36:08 -07:00
queryset = Document.objects.annotate(num_notes=Count("notes"))
2016-02-16 09:28:34 +00:00
serializer_class = DocumentSerializer
2016-02-21 00:55:38 +00:00
pagination_class = StandardPagination
2022-12-05 22:56:03 -08:00
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (
DjangoFilterBackend,
SearchFilter,
OrderingFilter,
2023-01-30 14:37:09 -08:00
ObjectOwnedOrGrantedPermissionsFilter,
)
2020-11-17 22:31:43 +01:00
filterset_class = DocumentFilterSet
2016-03-09 01:05:46 +00:00
search_fields = ("title", "correspondent__name", "content")
2016-03-13 16:45:12 +00:00
ordering_fields = (
2020-11-21 14:03:45 +01:00
"id",
"title",
"correspondent__name",
"document_type__name",
"created",
"modified",
"added",
2022-02-27 15:26:41 +01:00
"archive_serial_number",
2023-03-17 16:36:08 -07:00
"num_notes",
2023-05-02 00:38:32 -07:00
"owner",
2022-02-27 15:26:41 +01:00
)
def get_queryset(self):
2023-03-17 16:36:08 -07:00
return Document.objects.distinct().annotate(num_notes=Count("notes"))
def get_serializer(self, *args, **kwargs):
2022-02-27 15:26:41 +01:00
fields_param = self.request.query_params.get("fields", None)
fields = fields_param.split(",") if fields_param else None
truncate_content = self.request.query_params.get("truncate_content", "False")
2022-02-27 15:26:41 +01:00
kwargs.setdefault("context", self.get_serializer_context())
kwargs.setdefault("fields", fields)
kwargs.setdefault("truncate_content", truncate_content.lower() in ["true", "1"])
kwargs.setdefault(
"full_perms",
self.request.query_params.get("full_perms", False),
)
return super().get_serializer(*args, **kwargs)
def update(self, request, *args, **kwargs):
response = super().update(request, *args, **kwargs)
2021-02-15 13:26:36 +01:00
from documents import index
2022-02-27 15:26:41 +01:00
index.add_or_update_document(self.get_object())
return response
def destroy(self, request, *args, **kwargs):
2021-02-15 13:26:36 +01:00
from documents import index
2022-02-27 15:26:41 +01:00
index.remove_document_from_index(self.get_object())
return super().destroy(request, *args, **kwargs)
2020-11-25 14:48:36 +01:00
@staticmethod
def original_requested(request):
return (
2022-02-27 15:26:41 +01:00
"original" in request.query_params
and request.query_params["original"] == "true"
2020-11-25 14:48:36 +01:00
)
2020-11-25 14:48:36 +01:00
def file_response(self, pk, request, disposition):
doc = Document.objects.get(id=pk)
if request.user is not None and not has_perms_owner_aware(
request.user,
"view_document",
doc,
):
return HttpResponseForbidden("Insufficient permissions")
return serve_file(
doc=doc,
use_archive=not self.original_requested(request)
and doc.has_archive_version,
disposition=disposition,
2022-02-27 15:26:41 +01:00
)
def get_metadata(self, file, mime_type):
2020-12-08 15:28:09 +01:00
if not os.path.isfile(file):
return None
parser_class = get_parser_class_for_mime_type(mime_type)
if parser_class:
2021-01-04 23:05:16 +01:00
parser = parser_class(progress_callback=None, logging_group=None)
try:
return parser.extract_metadata(file, mime_type)
except Exception:
# TODO: cover GPG errors, remove later.
return []
else:
return []
2020-12-08 15:28:09 +01:00
def get_filesize(self, filename):
if os.path.isfile(filename):
return os.stat(filename).st_size
else:
return None
2022-02-27 15:26:41 +01:00
@action(methods=["get"], detail=True)
def metadata(self, request, pk=None):
try:
doc = Document.objects.get(pk=pk)
if request.user is not None and not has_perms_owner_aware(
request.user,
"view_document",
doc,
):
return HttpResponseForbidden("Insufficient permissions")
2021-01-29 16:45:23 +01:00
except Document.DoesNotExist:
raise Http404
2020-12-08 16:09:47 +01:00
2021-01-29 16:45:23 +01:00
meta = {
"original_checksum": doc.checksum,
"original_size": self.get_filesize(doc.source_path),
2021-01-29 16:45:23 +01:00
"original_mime_type": doc.mime_type,
"media_filename": doc.filename,
"has_archive_version": doc.has_archive_version,
2022-02-27 15:26:41 +01:00
"original_metadata": self.get_metadata(doc.source_path, doc.mime_type),
"archive_checksum": doc.archive_checksum,
2022-02-27 15:26:41 +01:00
"archive_media_filename": doc.archive_filename,
"original_filename": doc.original_filename,
2021-01-29 16:45:23 +01:00
}
2023-01-01 08:59:43 -08:00
lang = "en"
try:
lang = detect(doc.content)
except Exception:
pass
meta["lang"] = lang
if doc.has_archive_version:
2022-02-27 15:26:41 +01:00
meta["archive_size"] = self.get_filesize(doc.archive_path)
meta["archive_metadata"] = self.get_metadata(
doc.archive_path,
"application/pdf",
2022-02-27 15:26:41 +01:00
)
2021-01-29 16:45:23 +01:00
else:
2022-02-27 15:26:41 +01:00
meta["archive_size"] = None
meta["archive_metadata"] = None
2021-01-29 16:45:23 +01:00
return Response(meta)
2020-12-08 16:09:47 +01:00
2022-02-27 15:26:41 +01:00
@action(methods=["get"], detail=True)
2021-01-29 16:45:23 +01:00
def suggestions(self, request, pk=None):
doc = get_object_or_404(Document, pk=pk)
if request.user is not None and not has_perms_owner_aware(
request.user,
"view_document",
doc,
):
return HttpResponseForbidden("Insufficient permissions")
classifier = load_classifier()
2021-01-29 16:45:23 +01:00
dates = []
if settings.NUMBER_OF_SUGGESTED_DATES > 0:
gen = parse_date_generator(doc.filename, doc.content)
dates = sorted(
{i for i in itertools.islice(gen, settings.NUMBER_OF_SUGGESTED_DATES)},
)
2022-02-27 15:26:41 +01:00
return Response(
{
2023-04-15 22:47:36 -07:00
"correspondents": [
c.id for c in match_correspondents(doc, classifier, request.user)
],
"tags": [t.id for t in match_tags(doc, classifier, request.user)],
2022-02-27 15:26:41 +01:00
"document_types": [
2023-04-15 22:47:36 -07:00
dt.id for dt in match_document_types(doc, classifier, request.user)
],
"storage_paths": [
dt.id for dt in match_storage_paths(doc, classifier, request.user)
2022-02-27 15:26:41 +01:00
],
"dates": [
date.strftime("%Y-%m-%d") for date in dates if date is not None
],
},
2022-02-27 15:26:41 +01:00
)
@action(methods=["get"], detail=True)
def preview(self, request, pk=None):
try:
2022-02-27 15:26:41 +01:00
response = self.file_response(pk, request, "inline")
return response
except (FileNotFoundError, Document.DoesNotExist):
raise Http404
2022-02-27 15:26:41 +01:00
@action(methods=["get"], detail=True)
@method_decorator(cache_control(public=False, max_age=315360000))
def thumb(self, request, pk=None):
try:
doc = Document.objects.get(id=pk)
if request.user is not None and not has_perms_owner_aware(
request.user,
"view_document",
doc,
):
return HttpResponseForbidden("Insufficient permissions")
if doc.storage_type == Document.STORAGE_TYPE_GPG:
handle = GnuPG.decrypted(doc.thumbnail_file)
else:
handle = doc.thumbnail_file
2021-02-09 21:53:10 +01:00
# TODO: Send ETag information and use that to send new thumbnails
# if available
return HttpResponse(handle, content_type="image/webp")
except (FileNotFoundError, Document.DoesNotExist):
raise Http404
2022-02-27 15:26:41 +01:00
@action(methods=["get"], detail=True)
def download(self, request, pk=None):
try:
2022-02-27 15:26:41 +01:00
return self.file_response(pk, request, "attachment")
except (FileNotFoundError, Document.DoesNotExist):
raise Http404
2023-03-17 16:36:08 -07:00
def getNotes(self, doc):
2022-08-07 12:41:30 -07:00
return [
{
"id": c.id,
2023-03-17 16:36:08 -07:00
"note": c.note,
"created": c.created,
"user": {
"id": c.user.id,
2022-08-07 12:41:30 -07:00
"username": c.user.username,
2022-11-13 21:31:46 -08:00
"first_name": c.user.first_name,
"last_name": c.user.last_name,
},
}
2023-03-17 16:36:08 -07:00
for c in Note.objects.filter(document=doc).order_by("-created")
]
@action(methods=["get", "post", "delete"], detail=True)
2023-03-17 16:36:08 -07:00
def notes(self, request, pk=None):
currentUser = request.user
2022-08-07 12:41:30 -07:00
try:
doc = Document.objects.get(pk=pk)
if currentUser is not None and not has_perms_owner_aware(
currentUser,
"view_document",
doc,
):
return HttpResponseForbidden("Insufficient permissions to view notes")
2022-08-07 12:41:30 -07:00
except Document.DoesNotExist:
raise Http404
2022-08-07 12:41:30 -07:00
if request.method == "GET":
2022-08-07 12:41:30 -07:00
try:
2023-03-17 16:36:08 -07:00
return Response(self.getNotes(doc))
2022-08-07 12:41:30 -07:00
except Exception as e:
logger.warning(f"An error occurred retrieving notes: {e!s}")
2022-08-24 14:16:47 -07:00
return Response(
{"error": "Error retrieving notes, check logs for more detail."},
2022-08-24 14:16:47 -07:00
)
elif request.method == "POST":
2022-08-07 12:41:30 -07:00
try:
if currentUser is not None and not has_perms_owner_aware(
currentUser,
"change_document",
doc,
):
return HttpResponseForbidden(
"Insufficient permissions to create notes",
)
2023-03-17 16:36:08 -07:00
c = Note.objects.create(
document=doc,
2023-03-17 16:36:08 -07:00
note=request.data["note"],
user=currentUser,
)
c.save()
# If audit log is enabled make an entry in the log
# about this note change
if settings.AUDIT_LOG_ENABLED:
LogEntry.objects.log_create(
instance=doc,
changes=json.dumps(
{
"Note Added": ["None", c.id],
},
),
action=LogEntry.Action.UPDATE,
)
2022-08-07 12:41:30 -07:00
doc.modified = timezone.now()
doc.save()
from documents import index
index.add_or_update_document(self.get_object())
2023-03-17 16:36:08 -07:00
return Response(self.getNotes(doc))
2022-08-07 12:41:30 -07:00
except Exception as e:
logger.warning(f"An error occurred saving note: {e!s}")
return Response(
{
2023-03-17 16:36:08 -07:00
"error": "Error saving note, check logs for more detail.",
},
)
elif request.method == "DELETE":
if currentUser is not None and not has_perms_owner_aware(
currentUser,
"change_document",
doc,
):
return HttpResponseForbidden("Insufficient permissions to delete notes")
2023-03-17 16:36:08 -07:00
note = Note.objects.get(id=int(request.GET.get("id")))
if settings.AUDIT_LOG_ENABLED:
LogEntry.objects.log_create(
instance=doc,
changes=json.dumps(
{
"Note Deleted": [note.id, "None"],
},
),
action=LogEntry.Action.UPDATE,
)
2023-03-17 16:36:08 -07:00
note.delete()
doc.modified = timezone.now()
doc.save()
from documents import index
index.add_or_update_document(self.get_object())
2023-03-17 16:36:08 -07:00
return Response(self.getNotes(doc))
2022-08-07 12:41:30 -07:00
return Response(
{
"error": "error",
},
)
2022-08-07 12:41:30 -07:00
@action(methods=["get"], detail=True)
def share_links(self, request, pk=None):
currentUser = request.user
try:
doc = Document.objects.get(pk=pk)
if currentUser is not None and not has_perms_owner_aware(
currentUser,
"change_document",
doc,
):
return HttpResponseForbidden(
"Insufficient permissions to add share link",
)
except Document.DoesNotExist:
raise Http404
if request.method == "GET":
now = timezone.now()
links = [
{
"id": c.id,
"created": c.created,
"expiration": c.expiration,
"slug": c.slug,
}
for c in ShareLink.objects.filter(document=doc)
.exclude(expiration__lt=now)
.order_by("-created")
]
return Response(links)
2016-03-01 18:57:12 +00:00
class SearchResultSerializer(DocumentSerializer, PassUserMixin):
def to_representation(self, instance):
2022-02-27 15:26:41 +01:00
doc = Document.objects.get(id=instance["id"])
2023-03-17 16:36:08 -07:00
notes = ",".join(
[str(c.note) for c in Note.objects.filter(document=instance["id"])],
)
r = super().to_representation(doc)
2022-02-27 15:26:41 +01:00
r["__search_hit__"] = {
"score": instance.score,
2023-01-04 19:06:51 -08:00
"highlights": instance.highlights("content", text=doc.content),
2023-03-17 16:36:08 -07:00
"note_highlights": instance.highlights("notes", text=notes)
2022-02-27 15:26:41 +01:00
if doc
else None,
2022-02-27 15:26:41 +01:00
"rank": instance.rank,
}
2021-04-04 00:04:00 +02:00
return r
class UnifiedSearchViewSet(DocumentViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.searcher = None
def get_serializer_class(self):
if self._is_search_request():
return SearchResultSerializer
else:
return DocumentSerializer
def _is_search_request(self):
2022-02-27 15:26:41 +01:00
return (
"query" in self.request.query_params
or "more_like_id" in self.request.query_params
)
def filter_queryset(self, queryset):
if self._is_search_request():
from documents import index
if "query" in self.request.query_params:
query_class = index.DelayedFullTextQuery
elif "more_like_id" in self.request.query_params:
query_class = index.DelayedMoreLikeThisQuery
else:
raise ValueError
return query_class(
self.searcher,
self.request.query_params,
2022-02-27 15:26:41 +01:00
self.paginator.get_page_size(self.request),
self.request.user,
2022-02-27 15:26:41 +01:00
)
else:
return super().filter_queryset(queryset)
def list(self, request, *args, **kwargs):
if self._is_search_request():
from documents import index
2022-02-27 15:26:41 +01:00
try:
with index.open_index_searcher() as s:
self.searcher = s
return super().list(request)
2021-04-03 21:50:23 +02:00
except NotFound:
raise
except Exception as e:
logger.warning(f"An error occurred listing search results: {e!s}")
2023-05-11 12:56:01 -07:00
return HttpResponseBadRequest(
"Error listing search results, check logs for more detail.",
)
else:
return super().list(request)
@action(detail=False, methods=["GET"], name="Get Next ASN")
def next_asn(self, request, *args, **kwargs):
return Response(
(
Document.objects.filter(archive_serial_number__gte=0)
.order_by("archive_serial_number")
.last()
.archive_serial_number
or 0
)
+ 1,
)
2021-02-06 17:02:00 +01:00
class LogViewSet(ViewSet):
2022-11-24 14:26:32 -08:00
permission_classes = (IsAuthenticated, PaperlessAdminPermissions)
2021-02-06 17:02:00 +01:00
log_files = ["paperless", "mail"]
2023-02-23 14:32:58 -08:00
def get_log_filename(self, log):
return os.path.join(settings.LOGGING_DIR, f"{log}.log")
2021-02-06 17:02:00 +01:00
def retrieve(self, request, pk=None, *args, **kwargs):
2021-02-06 17:21:32 +01:00
if pk not in self.log_files:
raise Http404
2021-02-06 17:02:00 +01:00
2023-02-23 14:32:58 -08:00
filename = self.get_log_filename(pk)
2021-02-06 17:02:00 +01:00
if not os.path.isfile(filename):
raise Http404
2021-02-06 17:02:00 +01:00
with open(filename) as f:
2021-02-06 17:21:32 +01:00
lines = [line.rstrip() for line in f.readlines()]
2021-02-06 17:02:00 +01:00
return Response(lines)
def list(self, request, *args, **kwargs):
2023-02-23 14:32:58 -08:00
exist = [
log for log in self.log_files if os.path.isfile(self.get_log_filename(log))
]
return Response(exist)
2022-12-05 23:41:17 -08:00
class SavedViewViewSet(ModelViewSet, PassUserMixin):
2020-12-12 15:46:56 +01:00
model = SavedView
queryset = SavedView.objects.all()
serializer_class = SavedViewSerializer
pagination_class = StandardPagination
2022-12-05 22:56:03 -08:00
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
2020-12-12 15:46:56 +01:00
def get_queryset(self):
user = self.request.user
return SavedView.objects.filter(owner=user)
2020-12-12 15:46:56 +01:00
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
2020-12-12 15:46:56 +01:00
class BulkEditView(GenericAPIView, PassUserMixin):
2020-12-06 14:39:53 +01:00
permission_classes = (IsAuthenticated,)
serializer_class = BulkEditSerializer
parser_classes = (parsers.JSONParser,)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.request.user
2020-12-11 14:30:18 +01:00
method = serializer.validated_data.get("method")
parameters = serializer.validated_data.get("parameters")
documents = serializer.validated_data.get("documents")
if not user.is_superuser:
document_objs = Document.objects.filter(pk__in=documents)
has_perms = (
all((doc.owner == user or doc.owner is None) for doc in document_objs)
if method == bulk_edit.set_permissions
else all(
has_perms_owner_aware(user, "change_document", doc)
for doc in document_objs
)
)
if not has_perms:
return HttpResponseForbidden("Insufficient permissions")
2020-12-11 14:30:18 +01:00
try:
# TODO: parameter validation
result = method(documents, **parameters)
return Response({"result": result})
except Exception as e:
logger.warning(f"An error occurred performing bulk edit: {e!s}")
2023-05-11 12:56:01 -07:00
return HttpResponseBadRequest(
"Error performing bulk edit, check logs for more detail.",
)
2020-12-11 14:30:18 +01:00
2020-12-06 14:39:53 +01:00
2021-03-16 20:47:45 +01:00
class PostDocumentView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = PostDocumentSerializer
parser_classes = (parsers.MultiPartParser,)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
2022-02-27 15:26:41 +01:00
doc_name, doc_data = serializer.validated_data.get("document")
correspondent_id = serializer.validated_data.get("correspondent")
document_type_id = serializer.validated_data.get("document_type")
tag_ids = serializer.validated_data.get("tags")
title = serializer.validated_data.get("title")
created = serializer.validated_data.get("created")
archive_serial_number = serializer.validated_data.get("archive_serial_number")
t = int(mktime(datetime.now().timetuple()))
os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
temp_file_path = Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR)) / Path(
2022-12-26 13:43:30 -08:00
pathvalidate.sanitize_filename(doc_name),
)
temp_file_path.write_bytes(doc_data)
os.utime(temp_file_path, times=(t, t))
input_doc = ConsumableDocument(
source=DocumentSource.ApiUpload,
original_file=temp_file_path,
)
input_doc_overrides = DocumentMetadataOverrides(
filename=doc_name,
title=title,
correspondent_id=correspondent_id,
document_type_id=document_type_id,
tag_ids=tag_ids,
created=created,
asn=archive_serial_number,
owner_id=request.user.id,
)
async_task = consume_file.delay(
input_doc,
input_doc_overrides,
2022-02-27 15:26:41 +01:00
)
return Response(async_task.id)
2021-03-16 20:47:45 +01:00
class SelectionDataView(GenericAPIView):
2020-12-27 12:43:05 +01:00
permission_classes = (IsAuthenticated,)
serializer_class = DocumentListSerializer
2020-12-27 12:43:05 +01:00
parser_classes = (parsers.MultiPartParser, parsers.JSONParser)
def post(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
2022-02-27 15:26:41 +01:00
ids = serializer.validated_data.get("documents")
2020-12-27 12:43:05 +01:00
2020-12-27 12:54:47 +01:00
correspondents = Correspondent.objects.annotate(
2022-02-27 15:26:41 +01:00
document_count=Count(
Case(When(documents__id__in=ids, then=1), output_field=IntegerField()),
),
2022-02-27 15:26:41 +01:00
)
tags = Tag.objects.annotate(
document_count=Count(
Case(When(documents__id__in=ids, then=1), output_field=IntegerField()),
),
2022-02-27 15:26:41 +01:00
)
types = DocumentType.objects.annotate(
document_count=Count(
Case(When(documents__id__in=ids, then=1), output_field=IntegerField()),
),
2022-02-27 15:26:41 +01:00
)
Feature: Dynamic document storage pathes (#916) * Added devcontainer * Add feature storage pathes * Exclude tests and add versioning * Check escaping * Check escaping * Check quoting * Echo * Escape * Escape : * Double escape \ * Escaping * Remove if * Escape colon * Missing \ * Esacpe : * Escape all * test * Remove sed * Fix exclude * Remove SED command * Add LD_LIBRARY_PATH * Adjusted to v1.7 * Updated test-cases * Remove devcontainer * Removed internal build-file * Run pre-commit * Corrected flak8 error * Adjusted to v1.7 * Updated test-cases * Corrected flak8 error * Adjusted to new plural translations * Small adjustments due to code-review backend * Adjusted line-break * Removed PAPERLESS prefix from settings variables * Corrected style change due to search+replace * First documentation draft * Revert changes to Pipfile * Add sphinx-autobuild with keep-outdated * Revert merge error that results in wrong storage path is evaluated * Adjust styles of generated files ... * Adds additional testing to cover dynamic storage path functionality * Remove unnecessary condition * Add hint to edit storage path dialog * Correct spelling of pathes to paths * Minor documentation tweaks * Minor typo * improving wrapping of filter editor buttons with new storage path button * Update .gitignore * Fix select border radius in non input-groups * Better storage path edit hint * Add note to edit storage path dialog re document_renamer * Add note to bulk edit storage path re document_renamer * Rename FILTER_STORAGE_DIRECTORY to PATH * Fix broken filter rule parsing * Show default storage if unspecified * Remove note re storage path on bulk edit * Add basic validation of filename variables Co-authored-by: Markus Kling <markus@markus-kling.net> Co-authored-by: Trenton Holmes <holmes.trenton@gmail.com> Co-authored-by: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Co-authored-by: Quinn Casey <quinn@quinncasey.com>
2022-05-19 23:42:25 +02:00
storage_paths = StoragePath.objects.annotate(
document_count=Count(
Case(When(documents__id__in=ids, then=1), output_field=IntegerField()),
),
)
2022-02-27 15:26:41 +01:00
r = Response(
{
"selected_correspondents": [
{"id": t.id, "document_count": t.document_count}
for t in correspondents
],
"selected_tags": [
{"id": t.id, "document_count": t.document_count} for t in tags
],
"selected_document_types": [
{"id": t.id, "document_count": t.document_count} for t in types
],
Feature: Dynamic document storage pathes (#916) * Added devcontainer * Add feature storage pathes * Exclude tests and add versioning * Check escaping * Check escaping * Check quoting * Echo * Escape * Escape : * Double escape \ * Escaping * Remove if * Escape colon * Missing \ * Esacpe : * Escape all * test * Remove sed * Fix exclude * Remove SED command * Add LD_LIBRARY_PATH * Adjusted to v1.7 * Updated test-cases * Remove devcontainer * Removed internal build-file * Run pre-commit * Corrected flak8 error * Adjusted to v1.7 * Updated test-cases * Corrected flak8 error * Adjusted to new plural translations * Small adjustments due to code-review backend * Adjusted line-break * Removed PAPERLESS prefix from settings variables * Corrected style change due to search+replace * First documentation draft * Revert changes to Pipfile * Add sphinx-autobuild with keep-outdated * Revert merge error that results in wrong storage path is evaluated * Adjust styles of generated files ... * Adds additional testing to cover dynamic storage path functionality * Remove unnecessary condition * Add hint to edit storage path dialog * Correct spelling of pathes to paths * Minor documentation tweaks * Minor typo * improving wrapping of filter editor buttons with new storage path button * Update .gitignore * Fix select border radius in non input-groups * Better storage path edit hint * Add note to edit storage path dialog re document_renamer * Add note to bulk edit storage path re document_renamer * Rename FILTER_STORAGE_DIRECTORY to PATH * Fix broken filter rule parsing * Show default storage if unspecified * Remove note re storage path on bulk edit * Add basic validation of filename variables Co-authored-by: Markus Kling <markus@markus-kling.net> Co-authored-by: Trenton Holmes <holmes.trenton@gmail.com> Co-authored-by: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Co-authored-by: Quinn Casey <quinn@quinncasey.com>
2022-05-19 23:42:25 +02:00
"selected_storage_paths": [
{"id": t.id, "document_count": t.document_count}
for t in storage_paths
],
},
2022-02-27 15:26:41 +01:00
)
2020-12-27 12:43:05 +01:00
return r
2020-10-27 17:07:13 +01:00
class SearchAutoCompleteView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
user = self.request.user if hasattr(self.request, "user") else None
2022-02-27 15:26:41 +01:00
if "term" in request.query_params:
term = request.query_params["term"]
2020-10-27 17:07:13 +01:00
else:
2020-11-17 14:20:28 +01:00
return HttpResponseBadRequest("Term required")
2020-10-27 17:07:13 +01:00
2022-02-27 15:26:41 +01:00
if "limit" in request.query_params:
limit = int(request.query_params["limit"])
2020-11-17 14:20:28 +01:00
if limit <= 0:
return HttpResponseBadRequest("Invalid limit")
2020-10-27 17:07:13 +01:00
else:
limit = 10
2021-02-15 13:26:36 +01:00
from documents import index
ix = index.open_index()
return Response(
index.autocomplete(
ix,
term,
limit,
user,
),
)
2020-10-31 00:56:20 +01:00
class StatisticsView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
2023-04-26 09:51:26 -07:00
user = request.user if request.user is not None else None
documents = (
Document.objects.all()
if user is None
else get_objects_for_user_owner_aware(
user,
"documents.view_document",
Document,
)
)
tags = (
Tag.objects.all()
if user is None
else get_objects_for_user_owner_aware(user, "documents.view_tag", Tag)
)
correspondent_count = (
Correspondent.objects.count()
if user is None
else len(
get_objects_for_user_owner_aware(
user,
"documents.view_correspondent",
Correspondent,
),
)
)
document_type_count = (
DocumentType.objects.count()
if user is None
else len(
get_objects_for_user_owner_aware(
user,
"documents.view_documenttype",
DocumentType,
),
)
)
storage_path_count = (
StoragePath.objects.count()
if user is None
else len(
get_objects_for_user_owner_aware(
user,
"documents.view_storagepath",
StoragePath,
),
)
)
2023-04-26 09:51:26 -07:00
documents_total = documents.count()
2023-03-18 01:42:41 -07:00
2023-04-26 09:51:26 -07:00
inbox_tag = tags.filter(is_inbox_tag=True)
2023-03-18 01:42:41 -07:00
documents_inbox = (
2023-04-26 09:51:26 -07:00
documents.filter(tags__is_inbox_tag=True).distinct().count()
2023-03-18 01:42:41 -07:00
if inbox_tag.exists()
else None
)
document_file_type_counts = (
2023-04-26 09:51:26 -07:00
documents.values("mime_type")
2023-03-18 01:42:41 -07:00
.annotate(mime_type_count=Count("mime_type"))
.order_by("-mime_type_count")
if documents_total > 0
else []
2023-03-18 01:42:41 -07:00
)
character_count = (
2023-04-26 09:51:26 -07:00
documents.annotate(
characters=Length("content"),
2022-02-27 15:26:41 +01:00
)
.aggregate(Sum("characters"))
.get("characters__sum")
2023-03-18 01:42:41 -07:00
)
2022-02-27 15:26:41 +01:00
return Response(
{
"documents_total": documents_total,
"documents_inbox": documents_inbox,
2023-03-18 01:42:41 -07:00
"inbox_tag": inbox_tag.first().pk if inbox_tag.exists() else None,
"document_file_type_counts": document_file_type_counts,
"character_count": character_count,
"tag_count": len(tags),
"correspondent_count": correspondent_count,
"document_type_count": document_type_count,
"storage_path_count": storage_path_count,
},
2022-02-27 15:26:41 +01:00
)
2021-03-16 20:47:45 +01:00
class BulkDownloadView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = BulkDownloadSerializer
parser_classes = (parsers.JSONParser,)
def post(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
2022-02-27 15:26:41 +01:00
ids = serializer.validated_data.get("documents")
compression = serializer.validated_data.get("compression")
content = serializer.validated_data.get("content")
follow_filename_format = serializer.validated_data.get("follow_formatting")
os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
2021-02-21 00:21:43 +01:00
temp = tempfile.NamedTemporaryFile(
dir=settings.SCRATCH_DIR,
suffix="-compressed-archive",
delete=False,
2022-02-27 15:26:41 +01:00
)
2022-02-27 15:26:41 +01:00
if content == "both":
strategy_class = OriginalAndArchiveStrategy
2022-02-27 15:26:41 +01:00
elif content == "originals":
strategy_class = OriginalsOnlyStrategy
else:
strategy_class = ArchiveOnlyStrategy
with zipfile.ZipFile(temp.name, "w", compression) as zipf:
strategy = strategy_class(zipf, follow_filename_format)
for id in ids:
doc = Document.objects.get(id=id)
strategy.add_document(doc)
with open(temp.name, "rb") as f:
response = HttpResponse(f, content_type="application/zip")
response["Content-Disposition"] = '{}; filename="{}"'.format(
"attachment",
"documents.zip",
2022-02-27 15:26:41 +01:00
)
return response
2022-04-01 01:48:05 -07:00
2022-12-05 23:41:17 -08:00
class StoragePathViewSet(ModelViewSet, PassUserMixin):
model = StoragePath
Feature: Dynamic document storage pathes (#916) * Added devcontainer * Add feature storage pathes * Exclude tests and add versioning * Check escaping * Check escaping * Check quoting * Echo * Escape * Escape : * Double escape \ * Escaping * Remove if * Escape colon * Missing \ * Esacpe : * Escape all * test * Remove sed * Fix exclude * Remove SED command * Add LD_LIBRARY_PATH * Adjusted to v1.7 * Updated test-cases * Remove devcontainer * Removed internal build-file * Run pre-commit * Corrected flak8 error * Adjusted to v1.7 * Updated test-cases * Corrected flak8 error * Adjusted to new plural translations * Small adjustments due to code-review backend * Adjusted line-break * Removed PAPERLESS prefix from settings variables * Corrected style change due to search+replace * First documentation draft * Revert changes to Pipfile * Add sphinx-autobuild with keep-outdated * Revert merge error that results in wrong storage path is evaluated * Adjust styles of generated files ... * Adds additional testing to cover dynamic storage path functionality * Remove unnecessary condition * Add hint to edit storage path dialog * Correct spelling of pathes to paths * Minor documentation tweaks * Minor typo * improving wrapping of filter editor buttons with new storage path button * Update .gitignore * Fix select border radius in non input-groups * Better storage path edit hint * Add note to edit storage path dialog re document_renamer * Add note to bulk edit storage path re document_renamer * Rename FILTER_STORAGE_DIRECTORY to PATH * Fix broken filter rule parsing * Show default storage if unspecified * Remove note re storage path on bulk edit * Add basic validation of filename variables Co-authored-by: Markus Kling <markus@markus-kling.net> Co-authored-by: Trenton Holmes <holmes.trenton@gmail.com> Co-authored-by: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Co-authored-by: Quinn Casey <quinn@quinncasey.com>
2022-05-19 23:42:25 +02:00
queryset = StoragePath.objects.annotate(document_count=Count("documents")).order_by(
Lower("name"),
)
serializer_class = StoragePathSerializer
pagination_class = StandardPagination
2022-12-05 22:56:03 -08:00
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (
DjangoFilterBackend,
OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter,
)
Feature: Dynamic document storage pathes (#916) * Added devcontainer * Add feature storage pathes * Exclude tests and add versioning * Check escaping * Check escaping * Check quoting * Echo * Escape * Escape : * Double escape \ * Escaping * Remove if * Escape colon * Missing \ * Esacpe : * Escape all * test * Remove sed * Fix exclude * Remove SED command * Add LD_LIBRARY_PATH * Adjusted to v1.7 * Updated test-cases * Remove devcontainer * Removed internal build-file * Run pre-commit * Corrected flak8 error * Adjusted to v1.7 * Updated test-cases * Corrected flak8 error * Adjusted to new plural translations * Small adjustments due to code-review backend * Adjusted line-break * Removed PAPERLESS prefix from settings variables * Corrected style change due to search+replace * First documentation draft * Revert changes to Pipfile * Add sphinx-autobuild with keep-outdated * Revert merge error that results in wrong storage path is evaluated * Adjust styles of generated files ... * Adds additional testing to cover dynamic storage path functionality * Remove unnecessary condition * Add hint to edit storage path dialog * Correct spelling of pathes to paths * Minor documentation tweaks * Minor typo * improving wrapping of filter editor buttons with new storage path button * Update .gitignore * Fix select border radius in non input-groups * Better storage path edit hint * Add note to edit storage path dialog re document_renamer * Add note to bulk edit storage path re document_renamer * Rename FILTER_STORAGE_DIRECTORY to PATH * Fix broken filter rule parsing * Show default storage if unspecified * Remove note re storage path on bulk edit * Add basic validation of filename variables Co-authored-by: Markus Kling <markus@markus-kling.net> Co-authored-by: Trenton Holmes <holmes.trenton@gmail.com> Co-authored-by: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Co-authored-by: Quinn Casey <quinn@quinncasey.com>
2022-05-19 23:42:25 +02:00
filterset_class = StoragePathFilterSet
ordering_fields = ("name", "path", "matching_algorithm", "match", "document_count")
2022-05-07 08:11:10 -07:00
class UiSettingsView(GenericAPIView):
permission_classes = (IsAuthenticated,)
2022-05-07 08:11:10 -07:00
serializer_class = UiSettingsViewSerializer
def get(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = User.objects.get(pk=request.user.id)
2022-09-30 12:30:23 -07:00
ui_settings = {}
2022-05-07 08:11:10 -07:00
if hasattr(user, "ui_settings"):
2022-09-30 12:30:23 -07:00
ui_settings = user.ui_settings.settings
if "update_checking" in ui_settings:
2022-09-30 12:30:23 -07:00
ui_settings["update_checking"][
"backend_setting"
] = settings.ENABLE_UPDATE_CHECK
else:
ui_settings["update_checking"] = {
"backend_setting": settings.ENABLE_UPDATE_CHECK,
}
user_resp = {
"id": user.id,
"username": user.username,
"is_superuser": user.is_superuser,
"groups": list(user.groups.values_list("id", flat=True)),
}
if len(user.first_name) > 0:
user_resp["first_name"] = user.first_name
if len(user.last_name) > 0:
user_resp["last_name"] = user.last_name
2022-12-08 11:10:13 -08:00
# strip <app_label>.
roles = map(lambda perm: re.sub(r"^\w+.", "", perm), user.get_all_permissions())
return Response(
{
"user": user_resp,
2022-09-30 12:30:23 -07:00
"settings": ui_settings,
2022-11-11 18:33:04 +00:00
"permissions": roles,
},
)
def post(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(user=self.request.user)
return Response(
{
"success": True,
},
)
class RemoteVersionView(GenericAPIView):
def get(self, request, format=None):
remote_version = "0.0.0"
is_greater_than_current = False
current_version = packaging_version.parse(version.__full_version_str__)
try:
req = urllib.request.Request(
"https://api.github.com/repos/paperlessngx/"
"paperlessngx/releases/latest",
)
# Ensure a JSON response
req.add_header("Accept", "application/json")
with urllib.request.urlopen(req) as response:
remote = response.read().decode("utf8")
try:
remote_json = json.loads(remote)
remote_version = remote_json["tag_name"]
# Basically PEP 616 but that only went in 3.9
if remote_version.startswith("ngx-"):
remote_version = remote_version[len("ngx-") :]
except ValueError:
logger.debug("An error occurred parsing remote version json")
except urllib.error.URLError:
logger.debug("An error occurred checking for available updates")
is_greater_than_current = (
packaging_version.parse(
remote_version,
)
> current_version
)
return Response(
{
"version": remote_version,
"update_available": is_greater_than_current,
},
)
2022-05-23 15:22:14 -07:00
class TasksViewSet(ReadOnlyModelViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = TasksViewSerializer
def get_queryset(self):
queryset = (
PaperlessTask.objects.filter(
acknowledged=False,
)
.order_by("date_created")
.reverse()
)
task_id = self.request.query_params.get("task_id")
if task_id is not None:
queryset = PaperlessTask.objects.filter(task_id=task_id)
return queryset
class AcknowledgeTasksView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = AcknowledgeTasksViewSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
tasks = serializer.validated_data.get("tasks")
try:
result = PaperlessTask.objects.filter(id__in=tasks).update(
acknowledged=True,
)
return Response({"result": result})
2022-05-23 15:22:14 -07:00
except Exception:
return HttpResponseBadRequest()
class ShareLinkViewSet(ModelViewSet, PassUserMixin):
model = ShareLink
queryset = ShareLink.objects.all()
serializer_class = ShareLinkSerializer
pagination_class = StandardPagination
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
filter_backends = (
DjangoFilterBackend,
OrderingFilter,
ObjectOwnedOrGrantedPermissionsFilter,
)
filterset_class = ShareLinkFilterSet
ordering_fields = ("created", "expiration", "document")
class SharedLinkView(View):
authentication_classes = []
permission_classes = []
def get(self, request, slug):
share_link = ShareLink.objects.filter(slug=slug).first()
if share_link is None:
return HttpResponseRedirect("/accounts/login/?sharelink_notfound=1")
if share_link.expiration is not None and share_link.expiration < timezone.now():
return HttpResponseRedirect("/accounts/login/?sharelink_expired=1")
return serve_file(
doc=share_link.document,
use_archive=share_link.file_version == "archive",
disposition="inline",
)
def serve_file(doc: Document, use_archive: bool, disposition: str):
if use_archive:
file_handle = doc.archive_file
filename = doc.get_public_filename(archive=True)
mime_type = "application/pdf"
else:
file_handle = doc.source_file
filename = doc.get_public_filename()
mime_type = doc.mime_type
# Support browser previewing csv files by using text mime type
if mime_type in {"application/csv", "text/csv"} and disposition == "inline":
mime_type = "text/plain"
if doc.storage_type == Document.STORAGE_TYPE_GPG:
file_handle = GnuPG.decrypted(file_handle)
response = HttpResponse(file_handle, content_type=mime_type)
# Firefox is not able to handle unicode characters in filename field
# RFC 5987 addresses this issue
# see https://datatracker.ietf.org/doc/html/rfc5987#section-4.2
# Chromium cannot handle commas in the filename
filename_normalized = normalize("NFKD", filename.replace(",", "_")).encode(
"ascii",
"ignore",
)
filename_encoded = quote(filename)
content_disposition = (
f"{disposition}; "
f'filename="{filename_normalized}"; '
f"filename*=utf-8''{filename_encoded}"
)
response["Content-Disposition"] = content_disposition
return response
class BulkEditObjectPermissionsView(GenericAPIView, PassUserMixin):
permission_classes = (IsAuthenticated,)
serializer_class = BulkEditObjectPermissionsSerializer
parser_classes = (parsers.JSONParser,)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.request.user
object_type = serializer.validated_data.get("object_type")
object_ids = serializer.validated_data.get("objects")
object_class = serializer.get_object_class(object_type)
permissions = serializer.validated_data.get("permissions")
owner = serializer.validated_data.get("owner")
if not user.is_superuser:
objs = object_class.objects.filter(pk__in=object_ids)
has_perms = all((obj.owner == user or obj.owner is None) for obj in objs)
if not has_perms:
return HttpResponseForbidden("Insufficient permissions")
try:
qs = object_class.objects.filter(id__in=object_ids)
if "owner" in serializer.validated_data:
qs.update(owner=owner)
if "permissions" in serializer.validated_data:
for obj in qs:
set_permissions_for_object(permissions, obj)
return Response({"result": "OK"})
except Exception as e:
logger.warning(f"An error occurred performing bulk permissions edit: {e!s}")
return HttpResponseBadRequest(
"Error performing bulk permissions edit, check logs for more detail.",
)
class ConsumptionTemplateViewSet(ModelViewSet):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = ConsumptionTemplateSerializer
pagination_class = StandardPagination
model = ConsumptionTemplate
queryset = ConsumptionTemplate.objects.all().order_by("name")
class CustomFieldViewSet(ModelViewSet):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = CustomFieldSerializer
pagination_class = StandardPagination
model = CustomField
queryset = CustomField.objects.all().order_by("-created")