diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 86e6398ec..35d252b5b 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -109,13 +109,13 @@ - - - - + @for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index b8a6389f2..7dcf4e9f7 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -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, diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 23753f55b..a80e401e2 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -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(), + } } }) diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts index 2d02ba983..c0053353b 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts @@ -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 { + 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}"?` } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index 3fbf18e09..9453affd5 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -52,7 +52,7 @@ export abstract class ManagementListComponent implements OnInit, OnDestroy { constructor( - private service: AbstractNameFilterService, + protected service: AbstractNameFilterService, private modalService: NgbModal, private editDialogComponent: any, private toastService: ToastService, @@ -81,8 +81,8 @@ export abstract class ManagementListComponent public isLoading: boolean = false private nameFilterDebounce: Subject - private unsubscribeNotifier: Subject = new Subject() - private _nameFilter: string + protected unsubscribeNotifier: Subject = new Subject() + protected _nameFilter: string public selectedObjects: Set = new Set() public togggleAll: boolean = false diff --git a/src-ui/src/app/services/rest/abstract-name-filter-service.ts b/src-ui/src/app/services/rest/abstract-name-filter-service.ts index 1018f0fa2..03c7e5470 100644 --- a/src-ui/src/app/services/rest/abstract-name-filter-service.ts +++ b/src-ui/src/app/services/rest/abstract-name-filter-service.ts @@ -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 } diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index c92765e69..d7a06e181 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -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 diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index 65f379261..1a55a936c 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -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/" diff --git a/src/documents/views.py b/src/documents/views.py index 8b3486f76..91b99b610 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -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