]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: only include correspondent 'last_correspondence' if requested (#6792)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Wed, 22 May 2024 23:15:58 +0000 (16:15 -0700)
committerGitHub <noreply@github.com>
Wed, 22 May 2024 23:15:58 +0000 (23:15 +0000)
src-ui/src/app/components/document-detail/document-detail.component.html
src-ui/src/app/components/document-detail/document-detail.component.spec.ts
src-ui/src/app/components/document-detail/document-detail.component.ts
src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts
src-ui/src/app/components/manage/management-list/management-list.component.ts
src-ui/src/app/services/rest/abstract-name-filter-service.ts
src/documents/serialisers.py
src/documents/tests/test_api_objects.py
src/documents/views.py

index 86e6398ec6454699464291425a5c59f5aa93970d..35d252b5bd1718903c4333651f696552a4475c36 100644 (file)
               <pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
               <pngx-input-date i18n-title title="Date created" formControlName="created_date" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
               [error]="error?.created_date"></pngx-input-date>
-              <pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
+              <pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Correspondent)"
               (createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select>
-              <pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
+              <pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.DocumentType)"
               (createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select>
-              <pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
+              <pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.StoragePath)"
               (createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
-              <pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
+              <pngx-input-tags formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Tag)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
               @for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
                 <div [formGroup]="customFieldFormFields.controls[i]">
                   @switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {
index b8a6389f2611c9d7003f1e1c700896e8e7a232b6..7dcf4e9f7a9ad22b52c4a3a43655a0eb94a84a18 100644 (file)
@@ -80,8 +80,9 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
 import { environment } from 'src/environments/environment'
 import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
 import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
-import { PdfViewerModule } from 'ng2-pdf-viewer'
 import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
+import { PdfViewerModule } from 'ng2-pdf-viewer'
+import { DataType } from 'src/app/data/datatype'
 
 const doc: Document = {
   id: 3,
@@ -783,10 +784,9 @@ describe('DocumentDetailComponent', () => {
     const object = {
       id: 22,
       name: 'Correspondent22',
-      last_correspondence: new Date().toISOString(),
     } as Correspondent
     const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
-    component.filterDocuments([object])
+    component.filterDocuments([object], DataType.Correspondent)
     expect(qfSpy).toHaveBeenCalledWith([
       {
         rule_type: FILTER_CORRESPONDENT,
@@ -799,7 +799,7 @@ describe('DocumentDetailComponent', () => {
     initNormally()
     const object = { id: 22, name: 'DocumentType22' } as DocumentType
     const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
-    component.filterDocuments([object])
+    component.filterDocuments([object], DataType.DocumentType)
     expect(qfSpy).toHaveBeenCalledWith([
       {
         rule_type: FILTER_DOCUMENT_TYPE,
@@ -816,7 +816,7 @@ describe('DocumentDetailComponent', () => {
       path: '/foo/bar/',
     } as StoragePath
     const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
-    component.filterDocuments([object])
+    component.filterDocuments([object], DataType.StoragePath)
     expect(qfSpy).toHaveBeenCalledWith([
       {
         rule_type: FILTER_STORAGE_PATH,
@@ -842,7 +842,7 @@ describe('DocumentDetailComponent', () => {
       text_color: '#000000',
     } as Tag
     const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
-    component.filterDocuments([object1, object2])
+    component.filterDocuments([object1, object2], DataType.Tag)
     expect(qfSpy).toHaveBeenCalledWith([
       {
         rule_type: FILTER_HAS_TAGS_ALL,
index 23753f55bcf02c14840563902878469764cf98df..a80e401e299cf40faa0da336199ee9e64e1af53d 100644 (file)
@@ -71,6 +71,7 @@ import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-co
 import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
 import { HotKeyService } from 'src/app/services/hot-key.service'
 import { PDFDocumentProxy } from 'ng2-pdf-viewer'
+import { DataType } from 'src/app/data/datatype'
 
 enum DocumentDetailNavIDs {
   Details = 1,
@@ -171,6 +172,8 @@ export class DocumentDetailComponent
 
   public readonly ContentRenderType = ContentRenderType
 
+  public readonly DataType = DataType
+
   @ViewChild('nav') nav: NgbNav
   @ViewChild('pdfPreview') set pdfPreview(element) {
     // this gets called when component added or removed from DOM
@@ -998,7 +1001,7 @@ export class DocumentDetailComponent
     )
   }
 
-  filterDocuments(items: ObjectWithId[] | NgbDateStruct[]) {
+  filterDocuments(items: ObjectWithId[] | NgbDateStruct[], type?: DataType) {
     const filterRules: FilterRule[] = items.flatMap((i) => {
       if (i.hasOwnProperty('year')) {
         const isoDateAdapter = new ISODateAdapter()
@@ -1017,30 +1020,28 @@ export class DocumentDetailComponent
             value: dateBefore.toISOString().substring(0, 10),
           },
         ]
-      } else if (i.hasOwnProperty('last_correspondence')) {
-        // Correspondent
-        return {
-          rule_type: FILTER_CORRESPONDENT,
-          value: (i as Correspondent).id.toString(),
-        }
-      } else if (i.hasOwnProperty('path')) {
-        // Storage Path
-        return {
-          rule_type: FILTER_STORAGE_PATH,
-          value: (i as StoragePath).id.toString(),
-        }
-      } else if (i.hasOwnProperty('is_inbox_tag')) {
-        // Tag
-        return {
-          rule_type: FILTER_HAS_TAGS_ALL,
-          value: (i as Tag).id.toString(),
-        }
-      } else {
-        // Document Type, has no specific props
-        return {
-          rule_type: FILTER_DOCUMENT_TYPE,
-          value: (i as DocumentType).id.toString(),
-        }
+      }
+      switch (type) {
+        case DataType.Correspondent:
+          return {
+            rule_type: FILTER_CORRESPONDENT,
+            value: (i as Correspondent).id.toString(),
+          }
+        case DataType.DocumentType:
+          return {
+            rule_type: FILTER_DOCUMENT_TYPE,
+            value: (i as DocumentType).id.toString(),
+          }
+        case DataType.StoragePath:
+          return {
+            rule_type: FILTER_STORAGE_PATH,
+            value: (i as StoragePath).id.toString(),
+          }
+        case DataType.Tag:
+          return {
+            rule_type: FILTER_HAS_TAGS_ALL,
+            value: (i as Tag).id.toString(),
+          }
       }
     })
 
index 2d02ba983631714c0b46dd2d7dcccb179f8e7c0e..c0053353bd1c74ecdbe4970387fdcdc509f26589 100644 (file)
@@ -12,6 +12,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
 import { ToastService } from 'src/app/services/toast.service'
 import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
 import { ManagementListComponent } from '../management-list/management-list.component'
+import { takeUntil } from 'rxjs'
 
 @Component({
   selector: 'pngx-correspondent-list',
@@ -63,6 +64,26 @@ export class CorrespondentListComponent extends ManagementListComponent<Correspo
     )
   }
 
+  public reloadData(): void {
+    this.isLoading = true
+    this.service
+      .listFiltered(
+        this.page,
+        null,
+        this.sortField,
+        this.sortReverse,
+        this._nameFilter,
+        true,
+        { last_correspondence: true }
+      )
+      .pipe(takeUntil(this.unsubscribeNotifier))
+      .subscribe((c) => {
+        this.data = c.results
+        this.collectionSize = c.count
+        this.isLoading = false
+      })
+  }
+
   getDeleteMessage(object: Correspondent) {
     return $localize`Do you really want to delete the correspondent "${object.name}"?`
   }
index 3fbf18e09ccc7d4a7d670084edfe12d10f11093a..9453affd550742ef222c83a8bad26b85521617ed 100644 (file)
@@ -52,7 +52,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
   implements OnInit, OnDestroy
 {
   constructor(
-    private service: AbstractNameFilterService<T>,
+    protected service: AbstractNameFilterService<T>,
     private modalService: NgbModal,
     private editDialogComponent: any,
     private toastService: ToastService,
@@ -81,8 +81,8 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
   public isLoading: boolean = false
 
   private nameFilterDebounce: Subject<string>
-  private unsubscribeNotifier: Subject<any> = new Subject()
-  private _nameFilter: string
+  protected unsubscribeNotifier: Subject<any> = new Subject()
+  protected _nameFilter: string
 
   public selectedObjects: Set<number> = new Set()
   public togggleAll: boolean = false
index 1018f0fa2998d3a5d493e42aabf2078937a8dac3..03c7e5470eadea23959b3c5be41f43a8467fc16b 100644 (file)
@@ -17,9 +17,10 @@ export abstract class AbstractNameFilterService<
     sortField?: string,
     sortReverse?: boolean,
     nameFilter?: string,
-    fullPerms?: boolean
+    fullPerms?: boolean,
+    extraParams?: { [key: string]: any }
   ) {
-    let params = {}
+    let params = extraParams ?? {}
     if (nameFilter) {
       params['name__icontains'] = nameFilter
     }
index c92765e69bec0aa46efbe295f4d2e96e891f5858..d7a06e181c714ce6a1b2fc25e3eac04911693110 100644 (file)
@@ -291,7 +291,7 @@ class OwnedObjectSerializer(
 
 
 class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer):
-    last_correspondence = serializers.DateTimeField(read_only=True)
+    last_correspondence = serializers.DateTimeField(read_only=True, required=False)
 
     class Meta:
         model = Correspondent
index 65f3792612bf6fa69975e206f3a125859d2f35e4..1a55a936c2203a7ab60e35809a8d3ef45ac4b22f 100644 (file)
@@ -1,8 +1,10 @@
+import datetime
 import json
 from unittest import mock
 
 from django.contrib.auth.models import Permission
 from django.contrib.auth.models import User
+from django.utils import timezone
 from rest_framework import status
 from rest_framework.test import APITestCase
 
@@ -89,6 +91,57 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
         results = response.data["results"]
         self.assertEqual(len(results), 2)
 
+    def test_correspondent_last_correspondence(self):
+        """
+        GIVEN:
+            - Correspondent with documents
+        WHEN:
+            - API is called
+        THEN:
+            - Last correspondence date is returned only if requested for list, and for detail
+        """
+
+        Document.objects.create(
+            mime_type="application/pdf",
+            correspondent=self.c1,
+            created=timezone.make_aware(datetime.datetime(2022, 1, 1)),
+            checksum="123",
+        )
+        Document.objects.create(
+            mime_type="application/pdf",
+            correspondent=self.c1,
+            created=timezone.make_aware(datetime.datetime(2022, 1, 2)),
+            checksum="456",
+        )
+
+        # Only if requested for list
+        response = self.client.get(
+            "/api/correspondents/",
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        results = response.data["results"]
+        self.assertNotIn("last_correspondence", results[0])
+
+        response = self.client.get(
+            "/api/correspondents/?last_correspondence=true",
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        results = response.data["results"]
+        self.assertIn(
+            "2022-01-02",
+            results[0]["last_correspondence"],
+        )
+
+        # Included in detail by default
+        response = self.client.get(
+            f"/api/correspondents/{self.c1.id}/",
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertIn(
+            "2022-01-02",
+            response.data["last_correspondence"],
+        )
+
 
 class TestApiStoragePaths(DirectoriesMixin, APITestCase):
     ENDPOINT = "/api/storage_paths/"
index 8b3486f76ab9270aa8ffdf25cf93d00f42fac180..91b99b6109ef3d2bf2ddc16f5c0e559b2c3b2422 100644 (file)
@@ -253,14 +253,7 @@ class PermissionsAwareDocumentCountMixin(PassUserMixin):
 class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
     model = Correspondent
 
-    queryset = (
-        Correspondent.objects.prefetch_related("documents")
-        .annotate(
-            last_correspondence=Max("documents__created"),
-        )
-        .select_related("owner")
-        .order_by(Lower("name"))
-    )
+    queryset = Correspondent.objects.select_related("owner").order_by(Lower("name"))
 
     serializer_class = CorrespondentSerializer
     pagination_class = StandardPagination
@@ -279,6 +272,19 @@ class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
         "last_correspondence",
     )
 
+    def list(self, request, *args, **kwargs):
+        if request.query_params.get("last_correspondence", None):
+            self.queryset = self.queryset.annotate(
+                last_correspondence=Max("documents__created"),
+            )
+        return super().list(request, *args, **kwargs)
+
+    def retrieve(self, request, *args, **kwargs):
+        self.queryset = self.queryset.annotate(
+            last_correspondence=Max("documents__created"),
+        )
+        return super().retrieve(request, *args, **kwargs)
+
 
 class TagViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
     model = Tag