Support shared by me filter rule, rename is_shared property, add missing db migration
This commit is contained in:
parent
491f0b9c87
commit
93806597b7
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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: [],
|
||||
|
@ -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 = []
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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',
|
||||
|
@ -18,5 +18,5 @@ export interface ObjectWithPermissions extends ObjectWithId {
|
||||
|
||||
user_can_change?: boolean
|
||||
|
||||
is_shared?: boolean
|
||||
is_shared_by_requester?: boolean
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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(","):
|
||||
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
@ -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(
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user