Support shared by me filter rule, rename is_shared property, add missing db migration

This commit is contained in:
shamoon 2023-12-09 10:52:06 -08:00
parent 491f0b9c87
commit 93806597b7
15 changed files with 193 additions and 10 deletions

View File

@ -80,7 +80,7 @@ django_checks() {
search_index() {
local -r index_version=7
local -r index_version=8
local -r index_version_file=${DATA_DIR}/.index_version
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then

View File

@ -38,6 +38,16 @@
<small i18n>Shared with me</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.SHARED_BY_ME)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME" fill="currentColor" class="buttonicon-sm">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<div class="me-1">
<small i18n>Shared by me</small>
</div>
</button>
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="setFilter(OwnerFilterType.UNOWNED)" [disabled]="disabled">
<div class="selected-icon me-1">
<svg *ngIf="selectionModel.ownerFilter === OwnerFilterType.UNOWNED" fill="currentColor" class="buttonicon-sm">

View File

@ -145,6 +145,15 @@ describe('PermissionsFilterDropdownComponent', () => {
userID: null,
})
component.setFilter(OwnerFilterType.SHARED_BY_ME)
expect(ownerFilterSetResult).toEqual({
excludeUsers: [],
hideUnowned: false,
includeUsers: [],
ownerFilter: OwnerFilterType.SHARED_BY_ME,
userID: currentUserID,
})
component.setFilter(OwnerFilterType.UNOWNED)
expect(ownerFilterSetResult).toEqual({
excludeUsers: [],

View File

@ -32,6 +32,7 @@ export enum OwnerFilterType {
NOT_SELF = 2,
OTHERS = 3,
UNOWNED = 4,
SHARED_BY_ME = 5,
}
@Component({
@ -108,6 +109,13 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = []
this.selectionModel.hideUnowned = false
} else if (
this.selectionModel.ownerFilter === OwnerFilterType.SHARED_BY_ME
) {
this.selectionModel.userID = this.settingsService.currentUser.id
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = []
this.selectionModel.hideUnowned = false
} else if (this.selectionModel.ownerFilter === OwnerFilterType.UNOWNED) {
this.selectionModel.userID = null
this.selectionModel.includeUsers = []

View File

@ -112,7 +112,7 @@
</svg>
<small>{{document.owner | username}}</small>
</div>
<div *ngIf="document.is_shared" class="list-group-item bg-light text-dark p-1 border-0">
<div *ngIf="document.is_shared_by_requester" class="list-group-item bg-light text-dark p-1 border-0">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>

View File

@ -77,7 +77,7 @@
</svg>
<small>{{document.owner | username}}</small>
</div>
<div *ngIf="document.is_shared" class="ps-0 p-1">
<div *ngIf="document.is_shared_by_requester" class="ps-0 p-1">
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#people-fill"/>
</svg>

View File

@ -47,6 +47,7 @@ import {
FILTER_OWNER_DOES_NOT_INCLUDE,
FILTER_OWNER_ISNULL,
FILTER_CUSTOM_FIELDS,
FILTER_SHARED_BY_USER,
} from 'src/app/data/filter-rule-type'
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
@ -826,6 +827,16 @@ describe('FilterEditorComponent', () => {
expect(component.permissionsSelectionModel.hideUnowned).toBeTruthy()
}))
it('should ingest filter rules for shared by me', fakeAsync(() => {
component.filterRules = [
{
rule_type: FILTER_SHARED_BY_USER,
value: '2',
},
]
expect(component.permissionsSelectionModel.userID).toEqual(2)
}))
// GET filterRules
it('should convert user input to correct filter rules on text field search title + content', fakeAsync(() => {
@ -1453,13 +1464,28 @@ describe('FilterEditorComponent', () => {
])
}))
it('should convert user input to correct filter on permissions select unowned', fakeAsync(() => {
it('should convert user input to correct filter on permissions select shared by me', fakeAsync(() => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
const unownedButton = permissionsDropdown.queryAll(By.css('button'))[4]
unownedButton.triggerEventHandler('click')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_SHARED_BY_USER,
value: '1',
},
])
}))
it('should convert user input to correct filter on permissions select unowned', fakeAsync(() => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
const unownedButton = permissionsDropdown.queryAll(By.css('button'))[5]
unownedButton.triggerEventHandler('click')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_OWNER_ISNULL,

View File

@ -49,6 +49,7 @@ import {
FILTER_OWNER_ISNULL,
FILTER_OWNER_ANY,
FILTER_CUSTOM_FIELDS,
FILTER_SHARED_BY_USER,
} from 'src/app/data/filter-rule-type'
import {
FilterableDropdownSelectionModel,
@ -503,6 +504,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
parseInt(rule.value, 10)
)
break
case FILTER_SHARED_BY_USER:
this.permissionsSelectionModel.ownerFilter =
OwnerFilterType.SHARED_BY_ME
if (rule.value)
this.permissionsSelectionModel.userID = parseInt(rule.value, 10)
break
case FILTER_OWNER_ISNULL:
if (rule.value === 'true' || rule.value === '1') {
this.permissionsSelectionModel.hideUnowned = false
@ -801,6 +808,13 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
rule_type: FILTER_OWNER_ANY,
value: this.permissionsSelectionModel.includeUsers?.join(','),
})
} else if (
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.SHARED_BY_ME
) {
filterRules.push({
rule_type: FILTER_SHARED_BY_USER,
value: this.permissionsSelectionModel.userID.toString(),
})
} else if (
this.permissionsSelectionModel.ownerFilter == OwnerFilterType.UNOWNED
) {

View File

@ -45,6 +45,7 @@ export const FILTER_OWNER = 32
export const FILTER_OWNER_ANY = 33
export const FILTER_OWNER_ISNULL = 34
export const FILTER_OWNER_DOES_NOT_INCLUDE = 35
export const FILTER_SHARED_BY_USER = 37
export const FILTER_CUSTOM_FIELDS = 36
@ -273,6 +274,12 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
datatype: 'number',
multi: true,
},
{
id: FILTER_SHARED_BY_USER,
filtervar: 'shared_by__id',
datatype: 'number',
multi: true,
},
{
id: FILTER_CUSTOM_FIELDS,
filtervar: 'custom_fields__icontains',

View File

@ -18,5 +18,5 @@ export interface ObjectWithPermissions extends ObjectWithId {
user_can_change?: boolean
is_shared?: boolean
is_shared_by_requester?: boolean
}

View File

@ -1,7 +1,12 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.db.models import OuterRef
from django.db.models import Q
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
from rest_framework_guardian.filters import ObjectPermissionsFilter
from documents.models import Correspondent
@ -101,6 +106,39 @@ class TitleContentFilter(Filter):
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=OuterRef("pk"),
).values("user_id"),
),
)
.annotate(
num_shared_groups=Count(
GroupObjectPermission.objects.filter(
content_type=ctype,
object_pk=OuterRef("pk"),
).values("group_id"),
),
)
.filter(
Q(num_shared_users__gt=0) | Q(num_shared_groups__gt=0),
)
if value is not None
else qs
)
class CustomFieldsFilter(Filter):
def filter(self, qs, value):
if value:
@ -144,6 +182,8 @@ class DocumentFilterSet(FilterSet):
custom_fields__icontains = CustomFieldsFilter()
shared_by__id = SharedByUser()
class Meta:
model = Document
fields = {

View File

@ -75,6 +75,7 @@ def get_schema():
viewer_id=KEYWORD(commas=True),
checksum=TEXT(),
original_filename=TEXT(sortable=True),
is_shared=BOOLEAN(),
)
@ -167,6 +168,7 @@ def update_document(writer: AsyncWriter, doc: Document):
viewer_id=viewer_ids if viewer_ids else None,
checksum=doc.checksum,
original_filename=doc.original_filename,
is_shared=len(viewer_ids) > 0,
)
@ -194,6 +196,7 @@ class DelayedQuery:
"document_type": ("type", ["id", "id__in", "id__none", "isnull"]),
"storage_path": ("path", ["id", "id__in", "id__none", "isnull"]),
"owner": ("owner", ["id", "id__in", "id__none", "isnull"]),
"shared_by": ("shared_by", ["id"]),
"tags": ("tag", ["id__all", "id__in", "id__none"]),
"added": ("added", ["date__lt", "date__gt"]),
"created": ("created", ["date__lt", "date__gt"]),
@ -233,7 +236,11 @@ class DelayedQuery:
continue
if query_filter == "id":
criterias.append(query.Term(f"{field}_id", value))
if param == "shared_by":
criterias.append(query.Term("is_shared", True))
criterias.append(query.Term("owner_id", value))
else:
criterias.append(query.Term(f"{field}_id", value))
elif query_filter == "id__in":
in_filter = []
for object_id in value.split(","):

View File

@ -0,0 +1,60 @@
# Generated by Django 4.2.7 on 2023-12-09 18:13
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1042_consumptiontemplate_assign_custom_fields_and_more"),
]
operations = [
migrations.AlterField(
model_name="savedviewfilterrule",
name="rule_type",
field=models.PositiveIntegerField(
choices=[
(0, "title contains"),
(1, "content contains"),
(2, "ASN is"),
(3, "correspondent is"),
(4, "document type is"),
(5, "is in inbox"),
(6, "has tag"),
(7, "has any tag"),
(8, "created before"),
(9, "created after"),
(10, "created year is"),
(11, "created month is"),
(12, "created day is"),
(13, "added before"),
(14, "added after"),
(15, "modified before"),
(16, "modified after"),
(17, "does not have tag"),
(18, "does not have ASN"),
(19, "title or content contains"),
(20, "fulltext query"),
(21, "more like this"),
(22, "has tags in"),
(23, "ASN greater than"),
(24, "ASN less than"),
(25, "storage path is"),
(26, "has correspondent in"),
(27, "does not have correspondent in"),
(28, "has document type in"),
(29, "does not have document type in"),
(30, "has storage path in"),
(31, "does not have storage path in"),
(32, "owner is"),
(33, "has owner in"),
(34, "does not have owner"),
(35, "does not have owner in"),
(36, "has custom field value"),
(37, "is shared by me"),
],
verbose_name="rule type",
),
),
]

View File

@ -455,6 +455,8 @@ class SavedViewFilterRule(models.Model):
(33, _("has owner in")),
(34, _("does not have owner")),
(35, _("does not have owner in")),
(36, _("has custom field value")),
(37, _("is shared by me")),
]
saved_view = models.ForeignKey(

View File

@ -163,7 +163,7 @@ class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin):
try:
if full_perms:
self.fields.pop("user_can_change")
self.fields.pop("is_shared")
self.fields.pop("is_shared_by_requester")
else:
self.fields.pop("permissions")
except KeyError:
@ -209,7 +209,7 @@ class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin):
)
)
def get_is_shared(self, obj: Document):
def get_is_shared_by_requester(self, obj: Document):
ctype = ContentType.objects.get_for_model(obj)
UserObjectPermission = get_user_obj_perms_model()
GroupObjectPermission = get_group_obj_perms_model()
@ -228,7 +228,7 @@ class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin):
permissions = SerializerMethodField(read_only=True)
user_can_change = SerializerMethodField(read_only=True)
is_shared = SerializerMethodField(read_only=True)
is_shared_by_requester = SerializerMethodField(read_only=True)
set_permissions = serializers.DictField(
label="Set permissions",
@ -578,7 +578,7 @@ class DocumentSerializer(
"owner",
"permissions",
"user_can_change",
"is_shared",
"is_shared_by_requester",
"set_permissions",
"notes",
"custom_fields",