paperless-ngx/src/documents/filters.py

255 lines
7.6 KiB
Python
Raw Normal View History

from django.contrib.contenttypes.models import ContentType
from django.db.models import CharField
from django.db.models import Count
from django.db.models import OuterRef
from django.db.models import Q
from django.db.models.functions import Cast
from django_filters.rest_framework import BooleanFilter
from django_filters.rest_framework import Filter
from django_filters.rest_framework import FilterSet
from guardian.utils import get_group_obj_perms_model
from guardian.utils import get_user_obj_perms_model
2022-12-05 22:56:03 -08:00
from rest_framework_guardian.filters import ObjectPermissionsFilter
2016-03-09 01:05:46 +00:00
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import Document
from documents.models import DocumentType
from documents.models import Log
from documents.models import ShareLink
from documents.models import StoragePath
from documents.models import Tag
2016-03-09 01:05:46 +00:00
2020-10-21 12:16:25 +02:00
CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
ID_KWARGS = ["in", "exact"]
2021-01-04 00:38:29 +01:00
INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"]
DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"]
2018-09-23 15:38:31 +01:00
class CorrespondentFilterSet(FilterSet):
2018-09-02 21:26:06 +01:00
class Meta:
2016-03-09 01:05:46 +00:00
model = Correspondent
fields = {
"id": ID_KWARGS,
"name": CHAR_KWARGS,
}
2016-03-09 01:05:46 +00:00
class TagFilterSet(FilterSet):
2018-09-02 21:26:06 +01:00
class Meta:
2016-03-09 01:05:46 +00:00
model = Tag
fields = {
"id": ID_KWARGS,
"name": CHAR_KWARGS,
}
2018-09-05 15:25:14 +02:00
class DocumentTypeFilterSet(FilterSet):
2018-12-11 12:26:44 +01:00
class Meta:
2018-09-05 15:25:14 +02:00
model = DocumentType
fields = {
"id": ID_KWARGS,
"name": CHAR_KWARGS,
}
class StoragePathFilterSet(FilterSet):
class Meta:
model = StoragePath
fields = {
"id": ID_KWARGS,
"name": CHAR_KWARGS,
"path": CHAR_KWARGS,
}
2018-09-05 15:25:14 +02:00
class ObjectFilter(Filter):
def __init__(self, exclude=False, in_list=False, field_name=""):
super().__init__()
2020-12-07 15:25:06 +01:00
self.exclude = exclude
2021-01-20 18:35:03 -08:00
self.in_list = in_list
self.field_name = field_name
2020-12-07 15:25:06 +01:00
2020-11-04 00:01:08 +01:00
def filter(self, qs, value):
if not value:
return qs
try:
object_ids = [int(x) for x in value.split(",")]
2020-11-04 00:01:08 +01:00
except ValueError:
return qs
2021-01-20 18:35:03 -08:00
if self.in_list:
qs = qs.filter(**{f"{self.field_name}__id__in": object_ids}).distinct()
2021-01-20 18:35:03 -08:00
else:
for obj_id in object_ids:
2021-01-20 18:35:03 -08:00
if self.exclude:
qs = qs.exclude(**{f"{self.field_name}__id": obj_id})
2021-01-20 18:35:03 -08:00
else:
qs = qs.filter(**{f"{self.field_name}__id": obj_id})
2020-11-04 00:01:08 +01:00
return qs
class InboxFilter(Filter):
def filter(self, qs, value):
2022-02-27 15:26:41 +01:00
if value == "true":
2020-11-04 00:01:08 +01:00
return qs.filter(tags__is_inbox_tag=True)
2022-02-27 15:26:41 +01:00
elif value == "false":
2020-11-04 00:01:08 +01:00
return qs.exclude(tags__is_inbox_tag=True)
else:
return qs
class TitleContentFilter(Filter):
def filter(self, qs, value):
if value:
2022-02-27 15:26:41 +01:00
return qs.filter(Q(title__icontains=value) | Q(content__icontains=value))
else:
return qs
class SharedByUser(Filter):
def filter(self, qs, value):
ctype = ContentType.objects.get_for_model(self.model)
UserObjectPermission = get_user_obj_perms_model()
GroupObjectPermission = get_group_obj_perms_model()
return (
qs.filter(
owner_id=value,
)
.annotate(
num_shared_users=Count(
UserObjectPermission.objects.filter(
content_type=ctype,
object_pk=Cast(OuterRef("pk"), CharField()),
).values("user_id"),
),
)
.annotate(
num_shared_groups=Count(
GroupObjectPermission.objects.filter(
content_type=ctype,
object_pk=Cast(OuterRef("pk"), CharField()),
).values("group_id"),
),
)
.filter(
Q(num_shared_users__gt=0) | Q(num_shared_groups__gt=0),
)
if value is not None
else qs
)
class CustomFieldFilterSet(FilterSet):
class Meta:
model = CustomField
fields = {
"id": ID_KWARGS,
"name": CHAR_KWARGS,
}
class CustomFieldsFilter(Filter):
def filter(self, qs, value):
if value:
return (
qs.filter(custom_fields__field__name__icontains=value)
| qs.filter(custom_fields__value_text__icontains=value)
| qs.filter(custom_fields__value_bool__icontains=value)
| qs.filter(custom_fields__value_int__icontains=value)
| qs.filter(custom_fields__value_date__icontains=value)
| qs.filter(custom_fields__value_url__icontains=value)
)
else:
return qs
class DocumentFilterSet(FilterSet):
is_tagged = BooleanFilter(
label="Is tagged",
field_name="tags",
lookup_expr="isnull",
exclude=True,
2018-09-23 15:38:31 +01:00
)
tags__id__all = ObjectFilter(field_name="tags")
2020-11-04 00:01:08 +01:00
tags__id__none = ObjectFilter(field_name="tags", exclude=True)
2020-12-07 15:25:06 +01:00
tags__id__in = ObjectFilter(field_name="tags", in_list=True)
correspondent__id__none = ObjectFilter(field_name="correspondent", exclude=True)
document_type__id__none = ObjectFilter(field_name="document_type", exclude=True)
storage_path__id__none = ObjectFilter(field_name="storage_path", exclude=True)
2021-01-20 18:35:03 -08:00
2020-11-04 00:01:08 +01:00
is_in_inbox = InboxFilter()
title_content = TitleContentFilter()
2023-05-02 00:38:32 -07:00
owner__id__none = ObjectFilter(field_name="owner", exclude=True)
custom_fields__icontains = CustomFieldsFilter()
shared_by__id = SharedByUser()
2018-09-02 21:26:06 +01:00
class Meta:
model = Document
fields = {
"id": ID_KWARGS,
2018-09-23 15:38:31 +01:00
"title": CHAR_KWARGS,
2020-10-21 12:16:25 +02:00
"content": CHAR_KWARGS,
"archive_serial_number": INT_KWARGS,
"created": DATE_KWARGS,
"added": DATE_KWARGS,
"modified": DATE_KWARGS,
"original_filename": CHAR_KWARGS,
"checksum": CHAR_KWARGS,
"correspondent": ["isnull"],
2020-10-21 12:16:25 +02:00
"correspondent__id": ID_KWARGS,
2018-09-23 15:38:31 +01:00
"correspondent__name": CHAR_KWARGS,
2020-10-21 12:16:25 +02:00
"tags__id": ID_KWARGS,
2018-09-23 15:38:31 +01:00
"tags__name": CHAR_KWARGS,
"document_type": ["isnull"],
2020-10-21 12:16:25 +02:00
"document_type__id": ID_KWARGS,
"document_type__name": CHAR_KWARGS,
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_path": ["isnull"],
"storage_path__id": ID_KWARGS,
"storage_path__name": CHAR_KWARGS,
2023-05-02 00:38:32 -07:00
"owner": ["isnull"],
"owner__id": ID_KWARGS,
"custom_fields": ["icontains"],
}
2020-11-02 01:24:56 +01:00
class LogFilterSet(FilterSet):
class Meta:
model = Log
2022-02-27 15:26:41 +01:00
fields = {"level": INT_KWARGS, "created": DATE_KWARGS, "group": ID_KWARGS}
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
class ShareLinkFilterSet(FilterSet):
class Meta:
model = ShareLink
fields = {
"created": DATE_KWARGS,
"expiration": DATE_KWARGS,
}
2023-01-30 14:37:09 -08:00
class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
2022-12-05 22:56:03 -08:00
"""
A filter backend that limits results to those where the requesting user
has read object level permissions, owns the objects, or objects without
an owner (for backwards compat)
"""
def filter_queryset(self, request, queryset, view):
objects_with_perms = super().filter_queryset(request, queryset, view)
objects_owned = queryset.filter(owner=request.user)
objects_unowned = queryset.filter(owner__isnull=True)
return objects_with_perms | objects_owned | objects_unowned