From 487d3a6262da5cab804414b3662de15a948f39ca Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 2 May 2023 00:38:32 -0700 Subject: [PATCH 1/5] Support owner API query vars --- src/documents/filters.py | 4 + src/documents/index.py | 29 +++- ...036_alter_savedviewfilterrule_rule_type.py | 58 ++++++++ src/documents/models.py | 4 + src/documents/tests/test_api.py | 128 ++++++++++++++++++ src/documents/views.py | 1 + 6 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py diff --git a/src/documents/filters.py b/src/documents/filters.py index 56a490bc0..53ef0391c 100644 --- a/src/documents/filters.py +++ b/src/documents/filters.py @@ -105,6 +105,8 @@ class DocumentFilterSet(FilterSet): title_content = TitleContentFilter() + owner__id__none = ObjectFilter(field_name="owner", exclude=True) + class Meta: model = Document fields = { @@ -125,6 +127,8 @@ class DocumentFilterSet(FilterSet): "storage_path": ["isnull"], "storage_path__id": ID_KWARGS, "storage_path__name": CHAR_KWARGS, + "owner": ["isnull"], + "owner__id": ID_KWARGS, } diff --git a/src/documents/index.py b/src/documents/index.py index 0b0493514..540bce9d5 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -208,11 +208,13 @@ class DelayedQuery: for document_type_id in v.split(","): criterias.append(query.Not(query.Term("type_id", document_type_id))) elif k == "correspondent__isnull": - criterias.append(query.Term("has_correspondent", v == "false")) + criterias.append( + query.Term("has_correspondent", self.evalBoolean(v)), + ) elif k == "is_tagged": - criterias.append(query.Term("has_tag", v == "true")) + criterias.append(query.Term("has_tag", self.evalBoolean(v))) elif k == "document_type__isnull": - criterias.append(query.Term("has_type", v == "false")) + criterias.append(query.Term("has_type", self.evalBoolean(v) is False)) elif k == "created__date__lt": criterias.append( query.DateRange("created", start=None, end=isoparse(v)), @@ -236,7 +238,19 @@ class DelayedQuery: for storage_path_id in v.split(","): criterias.append(query.Not(query.Term("path_id", storage_path_id))) elif k == "storage_path__isnull": - criterias.append(query.Term("has_path", v == "false")) + criterias.append(query.Term("has_path", self.evalBoolean(v) is False)) + elif k == "owner__isnull": + criterias.append(query.Term("has_owner", self.evalBoolean(v) is False)) + elif k == "owner__id": + criterias.append(query.Term("owner_id", v)) + elif k == "owner__id__in": + owners_in = [] + for owner_id in v.split(","): + owners_in.append(query.Term("owner_id", owner_id)) + criterias.append(query.Or(owners_in)) + elif k == "owner__id__none": + for owner_id in v.split(","): + criterias.append(query.Not(query.Term("owner_id", owner_id))) user_criterias = [query.Term("has_owner", False)] if "user" in self.query_params: @@ -254,6 +268,12 @@ class DelayedQuery: else: return query.Or(user_criterias) if len(user_criterias) > 0 else None + def evalBoolean(self, val): + if val == "false" or val == "0": + return False + if val == "true" or val == "1": + return True + def _get_query_sortedby(self): if "ordering" not in self.query_params: return None, False @@ -269,6 +289,7 @@ class DelayedQuery: "document_type__name": "type", "archive_serial_number": "asn", "num_notes": "num_notes", + "owner": "owner", } if field.startswith("-"): diff --git a/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py b/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py new file mode 100644 index 000000000..e65586ad8 --- /dev/null +++ b/src/documents/migrations/1036_alter_savedviewfilterrule_rule_type.py @@ -0,0 +1,58 @@ +# Generated by Django 4.1.7 on 2023-05-04 04:11 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("documents", "1035_rename_comment_note"), + ] + + 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"), + ], + verbose_name="rule type", + ), + ), + ] diff --git a/src/documents/models.py b/src/documents/models.py index d28664a68..e3f14a8b2 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -448,6 +448,10 @@ class SavedViewFilterRule(models.Model): (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")), ] saved_view = models.ForeignKey( diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index dd872fe78..416a0adfd 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -469,6 +469,98 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): results = response.data["results"] self.assertEqual(len(results), 0) + def test_document_owner_filters(self): + """ + GIVEN: + - Documents with owners, with and without granted permissions + WHEN: + - User filters by owner + THEN: + - Owner filters work correctly but still respect permissions + """ + u1 = User.objects.create_user("user1") + u2 = User.objects.create_user("user2") + u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + + u1_doc1 = Document.objects.create( + title="none1", + checksum="A", + mime_type="application/pdf", + owner=u1, + ) + Document.objects.create( + title="none2", + checksum="B", + mime_type="application/pdf", + owner=u2, + ) + u0_doc1 = Document.objects.create( + title="none3", + checksum="C", + mime_type="application/pdf", + ) + u1_doc2 = Document.objects.create( + title="none4", + checksum="D", + mime_type="application/pdf", + owner=u1, + ) + u2_doc2 = Document.objects.create( + title="none5", + checksum="E", + mime_type="application/pdf", + owner=u2, + ) + + self.client.force_authenticate(user=u1) + assign_perm("view_document", u1, u2_doc2) + + # Will not show any u1 docs or u2_doc1 which isn't shared + response = self.client.get(f"/api/documents/?owner__id__none={u1.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 2) + self.assertCountEqual( + [results[0]["id"], results[1]["id"]], + [u0_doc1.id, u2_doc2.id], + ) + + # Will not show any u1 docs, u0_doc1 which has no owner or u2_doc1 which isn't shared + response = self.client.get( + f"/api/documents/?owner__id__none={u1.id}&owner__isnull=false", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u2_doc2.id]) + + # Will not show any u1 docs, u2_doc2 which is shared but has owner + response = self.client.get( + f"/api/documents/?owner__id__none={u1.id}&owner__isnull=true", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u0_doc1.id]) + + # Will not show any u1 docs or u2_doc1 which is not shared + response = self.client.get(f"/api/documents/?owner__id={u2.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 1) + self.assertCountEqual([results[0]["id"]], [u2_doc2.id]) + + # Will not show u2_doc1 which is not shared + response = self.client.get(f"/api/documents/?owner__id__in={u1.id},{u2.id}") + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertEqual(len(results), 3) + self.assertCountEqual( + [results[0]["id"], results[1]["id"], results[2]["id"]], + [u1_doc1.id, u1_doc2.id, u2_doc2.id], + ) + def test_search(self): d1 = Document.objects.create( title="invoice", @@ -1112,18 +1204,30 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(r.data["count"], 2) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 2) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get( + f"/api/documents/?query=test&owner__id__none={u1.id}&owner__isnull=true", + ) + self.assertEqual(r.data["count"], 1) self.client.force_authenticate(user=u2) r = self.client.get("/api/documents/?query=test") self.assertEqual(r.data["count"], 3) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 3) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u2.id}") + self.assertEqual(r.data["count"], 1) self.client.force_authenticate(user=superuser) r = self.client.get("/api/documents/?query=test") self.assertEqual(r.data["count"], 4) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 4) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}") + self.assertEqual(r.data["count"], 3) def test_search_filtering_with_object_perms(self): """ @@ -1153,6 +1257,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(r.data["count"], 2) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 2) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get(f"/api/documents/?query=test&owner__id={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get("/api/documents/?query=test&owner__isnull=true") + self.assertEqual(r.data["count"], 1) assign_perm("view_document", u1, d2) assign_perm("view_document", u1, d3) @@ -1166,6 +1278,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(r.data["count"], 4) r = self.client.get("/api/documents/?query=test&document_type__id__none=1") self.assertEqual(r.data["count"], 4) + r = self.client.get(f"/api/documents/?query=test&owner__id__none={u1.id}") + self.assertEqual(r.data["count"], 3) + r = self.client.get(f"/api/documents/?query=test&owner__id={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get(f"/api/documents/?query=test&owner__id__in={u1.id}") + self.assertEqual(r.data["count"], 1) + r = self.client.get("/api/documents/?query=test&owner__isnull=true") + self.assertEqual(r.data["count"], 1) def test_search_sorting(self): u1 = User.objects.create_user("user1") @@ -1247,6 +1367,14 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): search_query("&ordering=-num_notes"), [d1.id, d3.id, d2.id], ) + self.assertListEqual( + search_query("&ordering=owner"), + [d1.id, d2.id, d3.id], + ) + self.assertListEqual( + search_query("&ordering=-owner"), + [d3.id, d2.id, d1.id], + ) def test_statistics(self): doc1 = Document.objects.create( diff --git a/src/documents/views.py b/src/documents/views.py index bfe2b3e6f..e859571f2 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -264,6 +264,7 @@ class DocumentViewSet( "added", "archive_serial_number", "num_notes", + "owner", ) def get_queryset(self): From c2b5451fe42b5b848abd3d109acb5f60152758e5 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 2 May 2023 20:41:41 -0700 Subject: [PATCH 2/5] Add frontend owner filtering Add owner to doc cards, table Frontend testing for owner filtering --- .../e2e/documents/documents-list.cy.ts | 2 +- .../cypress/e2e/documents/query-params.cy.ts | 68 +++++++-- .../cypress/fixtures/documents/documents.json | 58 ++++++++ src-ui/src/app/app.module.ts | 9 +- .../date-dropdown.component.html | 2 +- ...permissions-filter-dropdown.component.html | 81 +++++++++++ ...permissions-filter-dropdown.component.scss | 8 ++ .../permissions-filter-dropdown.component.ts | 132 ++++++++++++++++++ .../document-card-large.component.html | 6 + .../document-card-large.component.ts | 2 +- .../document-card-small.component.html | 31 ++-- .../document-card-small.component.ts | 2 +- .../document-list.component.html | 10 ++ .../filter-editor.component.html | 10 +- .../filter-editor/filter-editor.component.ts | 87 +++++++++++- src-ui/src/app/data/filter-rule-type.ts | 29 ++++ src-ui/src/app/pipes/username.pipe.ts | 42 ++++++ .../src/app/services/rest/document.service.ts | 1 + 18 files changed, 549 insertions(+), 31 deletions(-) create mode 100644 src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html create mode 100644 src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.scss create mode 100644 src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.ts create mode 100644 src-ui/src/app/pipes/username.pipe.ts diff --git a/src-ui/cypress/e2e/documents/documents-list.cy.ts b/src-ui/cypress/e2e/documents/documents-list.cy.ts index 5c17ef5d9..847d038b8 100644 --- a/src-ui/cypress/e2e/documents/documents-list.cy.ts +++ b/src-ui/cypress/e2e/documents/documents-list.cy.ts @@ -150,7 +150,7 @@ describe('documents-list', () => { cy.contains('button', 'Corresp 11').click() cy.contains('label', 'Exclude').click() }) - cy.contains('One document') + cy.contains('3 documents') }) it('should apply tags', () => { diff --git a/src-ui/cypress/e2e/documents/query-params.cy.ts b/src-ui/cypress/e2e/documents/query-params.cy.ts index eb160e9de..78a85d185 100644 --- a/src-ui/cypress/e2e/documents/query-params.cy.ts +++ b/src-ui/cypress/e2e/documents/query-params.cy.ts @@ -190,6 +190,36 @@ describe('documents query params', () => { response.count = response.results.length } + if (req.query.hasOwnProperty('owner__id')) { + response.results = ( + documentsJson.results as Array + ).filter((d) => d.owner == req.query['owner__id']) + response.count = response.results.length + } else if (req.query.hasOwnProperty('owner__id__in')) { + const owners = req.query['owner__id__in'] + .toString() + .split(',') + .map((o) => parseInt(o)) + response.results = ( + documentsJson.results as Array + ).filter((d) => owners.includes(d.owner)) + response.count = response.results.length + } else if (req.query.hasOwnProperty('owner__id__none')) { + const owners = req.query['owner__id__none'] + .toString() + .split(',') + .map((o) => parseInt(o)) + response.results = ( + documentsJson.results as Array + ).filter((d) => !owners.includes(d.owner)) + response.count = response.results.length + } else if (req.query.hasOwnProperty('owner__isnull')) { + response.results = ( + documentsJson.results as Array + ).filter((d) => d.owner === null) + response.count = response.results.length + } + req.reply(response) }) }) @@ -202,7 +232,7 @@ describe('documents query params', () => { it('should show a list of documents reverse sorted by created', () => { cy.visit('/documents?sort=created&reverse=true') - cy.get('app-document-card-small').first().contains('sit amet') + cy.get('app-document-card-small').first().contains('Doc 6') }) it('should show a list of documents sorted by added', () => { @@ -212,7 +242,7 @@ describe('documents query params', () => { it('should show a list of documents reverse sorted by added', () => { cy.visit('/documents?sort=added&reverse=true') - cy.get('app-document-card-small').first().contains('sit amet') + cy.get('app-document-card-small').first().contains('Doc 6') }) it('should show a list of documents filtered by any tags', () => { @@ -222,12 +252,12 @@ describe('documents query params', () => { it('should show a list of documents filtered by excluded tags', () => { cy.visit('/documents?sort=created&reverse=true&tags__id__none=2,4') - cy.contains('One document') + cy.contains('3 documents') }) it('should show a list of documents filtered by no tags', () => { cy.visit('/documents?sort=created&reverse=true&is_tagged=0') - cy.contains('One document') + cy.contains('3 documents') }) it('should show a list of documents filtered by document type', () => { @@ -242,7 +272,7 @@ describe('documents query params', () => { it('should show a list of documents filtered by no document type', () => { cy.visit('/documents?sort=created&reverse=true&document_type__isnull=1') - cy.contains('One document') + cy.contains('3 documents') }) it('should show a list of documents filtered by correspondent', () => { @@ -257,7 +287,7 @@ describe('documents query params', () => { it('should show a list of documents filtered by no correspondent', () => { cy.visit('/documents?sort=created&reverse=true&correspondent__isnull=1') - cy.contains('One document') + cy.contains('3 documents') }) it('should show a list of documents filtered by storage path', () => { @@ -267,7 +297,7 @@ describe('documents query params', () => { it('should show a list of documents filtered by no storage path', () => { cy.visit('/documents?sort=created&reverse=true&storage_path__isnull=1') - cy.contains('3 documents') + cy.contains('5 documents') }) it('should show a list of documents filtered by title or content', () => { @@ -312,7 +342,7 @@ describe('documents query params', () => { cy.visit( '/documents?sort=created&reverse=true&created__date__gt=2022-03-23' ) - cy.contains('3 documents') + cy.contains('5 documents') }) it('should show a list of documents filtered by created date less than', () => { @@ -324,7 +354,7 @@ describe('documents query params', () => { it('should show a list of documents filtered by added date greater than', () => { cy.visit('/documents?sort=created&reverse=true&added__date__gt=2022-03-24') - cy.contains('2 documents') + cy.contains('4 documents') }) it('should show a list of documents filtered by added date less than', () => { @@ -338,4 +368,24 @@ describe('documents query params', () => { ) cy.contains('2 documents') }) + + it('should show a list of documents filtered by owner', () => { + cy.visit('/documents?owner__id=15') + cy.contains('One document') + }) + + it('should show a list of documents filtered by multiple owners', () => { + cy.visit('/documents?owner__id__in=6,15') + cy.contains('2 documents') + }) + + it('should show a list of documents filtered by excluded owners', () => { + cy.visit('/documents?owner__id__none=6') + cy.contains('5 documents') + }) + + it('should show a list of documents filtered by null owner', () => { + cy.visit('/documents?owner__isnull=true') + cy.contains('4 documents') + }) }) diff --git a/src-ui/cypress/fixtures/documents/documents.json b/src-ui/cypress/fixtures/documents/documents.json index 6b284f7b2..a33e4e43f 100644 --- a/src-ui/cypress/fixtures/documents/documents.json +++ b/src-ui/cypress/fixtures/documents/documents.json @@ -143,6 +143,64 @@ } }, "notes": [] + }, + { + "id": 5, + "correspondent": null, + "document_type": null, + "storage_path": null, + "title": "Doc 5", + "content": "Test document 5", + "tags": [], + "created": "2023-05-01T07:24:18Z", + "created_date": "2023-05-02", + "modified": "2023-05-02T07:24:23.264859Z", + "added": "2023-05-02T07:24:22.922631Z", + "archive_serial_number": null, + "original_file_name": "doc5.pdf", + "archived_file_name": "doc5.pdf", + "owner": 15, + "user_can_change": true, + "permissions": { + "view": { + "users": [1], + "groups": [] + }, + "change": { + "users": [], + "groups": [] + } + }, + "notes": [] + }, + { + "id": 6, + "correspondent": null, + "document_type": null, + "storage_path": null, + "title": "Doc 6", + "content": "Test document 6", + "tags": [], + "created": "2023-05-01T10:24:18Z", + "created_date": "2023-05-02", + "modified": "2023-05-02T10:24:23.264859Z", + "added": "2023-05-02T10:24:22.922631Z", + "archive_serial_number": null, + "original_file_name": "doc6.pdf", + "archived_file_name": "doc6.pdf", + "owner": 6, + "user_can_change": true, + "permissions": { + "view": { + "users": [1], + "groups": [] + }, + "change": { + "users": [], + "groups": [] + } + }, + "notes": [] } ] } diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index ed847d41a..6c6d13f6c 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -88,6 +88,10 @@ import { PermissionsUserComponent } from './components/common/input/permissions/ import { PermissionsGroupComponent } from './components/common/input/permissions/permissions-group/permissions-group.component' import { IfOwnerDirective } from './directives/if-owner.directive' import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive' +import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component' +import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component' +import { PermissionsFilterDropdownComponent } from './components/common/permissions-filter-dropdown/permissions-filter-dropdown.component' +import { UsernamePipe } from './pipes/username.pipe' import localeAr from '@angular/common/locales/ar' import localeBe from '@angular/common/locales/be' @@ -111,8 +115,6 @@ import localeSr from '@angular/common/locales/sr' import localeSv from '@angular/common/locales/sv' import localeTr from '@angular/common/locales/tr' import localeZh from '@angular/common/locales/zh' -import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component' -import { PermissionsFormComponent } from './components/common/input/permissions/permissions-form/permissions-form.component' registerLocaleData(localeAr) registerLocaleData(localeBe) @@ -213,6 +215,8 @@ function initializeApp(settings: SettingsService) { IfObjectPermissionsDirective, PermissionsDialogComponent, PermissionsFormComponent, + PermissionsFilterDropdownComponent, + UsernamePipe, ], imports: [ BrowserModule, @@ -253,6 +257,7 @@ function initializeApp(settings: SettingsService) { PermissionsGuard, DirtyDocGuard, DirtySavedViewGuard, + UsernamePipe, ], bootstrap: [AppComponent], }) diff --git a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html index 58634b4d0..40bce985d 100644 --- a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html +++ b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html @@ -6,7 +6,7 @@ +
+ + {{document.owner | username}} +
Score: diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts index 58a3dd4e4..d2153fb62 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts @@ -23,7 +23,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission export class DocumentCardLargeComponent extends ComponentWithPermissions { constructor( private documentService: DocumentService, - private settingsService: SettingsService + public settingsService: SettingsService ) { super() } diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index 7365fec36..f61d586c8 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -38,15 +38,15 @@
@@ -59,18 +59,23 @@
- {{document.created_date | customDate:'mediumDate'}}
-
- - #{{document.archive_serial_number}} -
+
+
+ + #{{document.archive_serial_number}} +
+
+ + {{document.owner | username}}
diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index fcec51ebf..62f44851e 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -24,7 +24,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission export class DocumentCardSmallComponent extends ComponentWithPermissions { constructor( private documentService: DocumentService, - private settingsService: SettingsService + public settingsService: SettingsService ) { super() } diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 0b7b06dbc..c0fba9325 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -142,6 +142,13 @@ [currentSortReverse]="list.sortReverse" (sort)="onSort($event)" i18n>Title + Owner {{d.title | documentTitle}} + + {{d.owner | username}} + diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html index 4e7851a57..e83688596 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html @@ -58,20 +58,26 @@ [documentCounts]="storagePathDocumentCounts" [allowSelectNone]="true">
-
+
-
+
+ +
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts index 10048f7d7..37a58c54c 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -43,6 +43,10 @@ import { FILTER_DOCUMENT_TYPE, FILTER_CORRESPONDENT, FILTER_STORAGE_PATH, + FILTER_OWNER, + FILTER_OWNER_DOES_NOT_INCLUDE, + FILTER_OWNER_ISNULL, + FILTER_OWNER_ANY, } from 'src/app/data/filter-rule-type' import { FilterableDropdownSelectionModel, @@ -59,6 +63,11 @@ import { PaperlessDocument } from 'src/app/data/paperless-document' import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { RelativeDate } from '../../common/date-dropdown/date-dropdown.component' +import { + OwnerFilterType, + PermissionsSelectionModel, +} from '../../common/permissions-filter-dropdown/permissions-filter-dropdown.component' +import { SettingsService } from 'src/app/services/settings.service' const TEXT_FILTER_TARGET_TITLE = 'title' const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content' @@ -136,6 +145,15 @@ export class FilterEditorComponent implements OnInit, OnDestroy { case FILTER_ASN: return $localize`ASN: ${rule.value}` + + case FILTER_OWNER: + return $localize`Owner: ${rule.value}` + + case FILTER_OWNER_DOES_NOT_INCLUDE: + return $localize`Owner not in: ${rule.value}` + + case FILTER_OWNER_ISNULL: + return $localize`Without an owner` } } @@ -147,7 +165,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy { private tagService: TagService, private correspondentService: CorrespondentService, private documentService: DocumentService, - private storagePathService: StoragePathService + private storagePathService: StoragePathService, + private settingsService: SettingsService ) {} @ViewChild('textFilterInput') @@ -241,6 +260,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy { dateCreatedRelativeDate: RelativeDate dateAddedRelativeDate: RelativeDate + permissionsSelectionModel = new PermissionsSelectionModel() + _unmodifiedFilterRules: FilterRule[] = [] _filterRules: FilterRule[] = [] @@ -274,6 +295,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { this.dateCreatedRelativeDate = null this.dateAddedRelativeDate = null this.textFilterModifier = TEXT_FILTER_MODIFIER_EQUALS + this.permissionsSelectionModel.clear() value.forEach((rule) => { switch (rule.rule_type) { @@ -441,6 +463,35 @@ export class FilterEditorComponent implements OnInit, OnDestroy { this.textFilterModifier = TEXT_FILTER_MODIFIER_LT this._textFilter = rule.value break + case FILTER_OWNER: + this.permissionsSelectionModel.ownerFilter = OwnerFilterType.SELF + this.permissionsSelectionModel.hideUnowned = false + if (rule.value) + this.permissionsSelectionModel.userID = parseInt(rule.value, 10) + break + case FILTER_OWNER_ANY: + this.permissionsSelectionModel.ownerFilter = OwnerFilterType.OTHERS + if (rule.value) + this.permissionsSelectionModel.includeUsers.push( + parseInt(rule.value, 10) + ) + break + case FILTER_OWNER_DOES_NOT_INCLUDE: + this.permissionsSelectionModel.ownerFilter = OwnerFilterType.NOT_SELF + if (rule.value) + this.permissionsSelectionModel.excludeUsers.push( + parseInt(rule.value, 10) + ) + break + case FILTER_OWNER_ISNULL: + if (rule.value === 'true' || rule.value === '1') { + this.permissionsSelectionModel.hideUnowned = false + this.permissionsSelectionModel.ownerFilter = OwnerFilterType.UNOWNED + } else { + this.permissionsSelectionModel.hideUnowned = + rule.value === 'false' || rule.value === '0' + break + } } }) this.rulesModified = filterRulesDiffer( @@ -702,6 +753,40 @@ export class FilterEditorComponent implements OnInit, OnDestroy { } } } + if (this.permissionsSelectionModel.ownerFilter == OwnerFilterType.SELF) { + filterRules.push({ + rule_type: FILTER_OWNER, + value: this.permissionsSelectionModel.userID.toString(), + }) + } else if ( + this.permissionsSelectionModel.ownerFilter == OwnerFilterType.NOT_SELF + ) { + filterRules.push({ + rule_type: FILTER_OWNER_DOES_NOT_INCLUDE, + value: this.permissionsSelectionModel.excludeUsers?.join(','), + }) + } else if ( + this.permissionsSelectionModel.ownerFilter == OwnerFilterType.OTHERS + ) { + filterRules.push({ + rule_type: FILTER_OWNER_ANY, + value: this.permissionsSelectionModel.includeUsers?.join(','), + }) + } else if ( + this.permissionsSelectionModel.ownerFilter == OwnerFilterType.UNOWNED + ) { + filterRules.push({ + rule_type: FILTER_OWNER_ISNULL, + value: 'true', + }) + } + + if (this.permissionsSelectionModel.hideUnowned) { + filterRules.push({ + rule_type: FILTER_OWNER_ISNULL, + value: 'false', + }) + } return filterRules } diff --git a/src-ui/src/app/data/filter-rule-type.ts b/src-ui/src/app/data/filter-rule-type.ts index 50b80b13b..f65f52fd2 100644 --- a/src-ui/src/app/data/filter-rule-type.ts +++ b/src-ui/src/app/data/filter-rule-type.ts @@ -41,6 +41,11 @@ export const FILTER_TITLE_CONTENT = 19 export const FILTER_FULLTEXT_QUERY = 20 export const FILTER_FULLTEXT_MORELIKE = 21 +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_RULE_TYPES: FilterRuleType[] = [ { id: FILTER_TITLE, @@ -242,6 +247,30 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ datatype: 'number', multi: false, }, + { + id: FILTER_OWNER, + filtervar: 'owner__id', + datatype: 'number', + multi: false, + }, + { + id: FILTER_OWNER_ANY, + filtervar: 'owner__id__in', + datatype: 'number', + multi: true, + }, + { + id: FILTER_OWNER_ISNULL, + filtervar: 'owner__isnull', + datatype: 'boolean', + multi: false, + }, + { + id: FILTER_OWNER_DOES_NOT_INCLUDE, + filtervar: 'owner__id__none', + datatype: 'number', + multi: true, + }, ] export interface FilterRuleType { diff --git a/src-ui/src/app/pipes/username.pipe.ts b/src-ui/src/app/pipes/username.pipe.ts new file mode 100644 index 000000000..79d2657a2 --- /dev/null +++ b/src-ui/src/app/pipes/username.pipe.ts @@ -0,0 +1,42 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { UserService } from '../services/rest/user.service' +import { + PermissionAction, + PermissionType, + PermissionsService, +} from '../services/permissions.service' +import { PaperlessUser } from '../data/paperless-user' + +@Pipe({ + name: 'username', +}) +export class UsernamePipe implements PipeTransform { + users: PaperlessUser[] + + constructor( + permissionsService: PermissionsService, + userService: UserService + ) { + if ( + permissionsService.currentUserCan( + PermissionAction.View, + PermissionType.User + ) + ) { + userService.listAll().subscribe((r) => (this.users = r.results)) + } + } + + transform(userID: number): string { + return this.users + ? this.getName(this.users.find((u) => u.id === userID)) ?? '' + : $localize`Shared` + } + + getName(user: PaperlessUser): string { + if (!user) return '' + const name = [user.first_name, user.last_name].join(' ') + if (name.length > 1) return name.trim() + return user.username + } +} diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 4ff2ee88f..08050ac85 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -23,6 +23,7 @@ export const DOCUMENT_SORT_FIELDS = [ { field: 'added', name: $localize`Added` }, { field: 'modified', name: $localize`Modified` }, { field: 'num_notes', name: $localize`Notes` }, + { field: 'owner', name: $localize`Owner` }, ] export const DOCUMENT_SORT_FIELDS_FULLTEXT = [ From 3c4dadd905ed9462466e98e105c5bd09d4743952 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 3 May 2023 19:37:36 -0700 Subject: [PATCH 3/5] Re-work filter editor, bulk editor & reset buttons --- .../app-frame/app-frame.component.html | 6 ++-- .../date-dropdown.component.html | 22 ++++++------ .../toggleable-dropdown-button.component.html | 12 +++---- ...permissions-filter-dropdown.component.html | 29 +++++++-------- .../bulk-editor/bulk-editor.component.html | 35 +++++++++---------- .../document-list.component.html | 15 +++++--- .../document-list/document-list.component.ts | 4 +++ .../filter-editor.component.html | 35 +++++++++---------- .../management-list.component.html | 6 ++-- .../manage/settings/settings.component.html | 4 +-- src-ui/src/styles.scss | 10 +++++- src-ui/src/theme.scss | 4 +++ src/documents/index.py | 2 +- src/documents/tests/test_api.py | 6 ++-- 14 files changed, 104 insertions(+), 86 deletions(-) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index be0240eb5..83fe13d81 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -18,8 +18,8 @@ @@ -107,7 +107,7 @@  {{d.title | documentTitle}} - + diff --git a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html index 40bce985d..05547fed6 100644 --- a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html +++ b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.html @@ -7,9 +7,9 @@
@@ -18,8 +18,8 @@
After
- - + + Clear @@ -29,8 +29,8 @@
@@ -41,8 +41,8 @@
Before
- - + + Clear @@ -52,8 +52,8 @@
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html index af935b0db..45c6240e4 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html +++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html @@ -1,18 +1,18 @@