Enhancement: only include correspondent 'last_correspondence' if requested (#6792)

This commit is contained in:
shamoon 2024-05-22 16:15:58 -07:00 committed by GitHub
parent c0c44b512c
commit 8abb0cd75d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 131 additions and 49 deletions

View File

@ -109,13 +109,13 @@
<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-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)" <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> [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> (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> (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> (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) { @for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
<div [formGroup]="customFieldFormFields.controls[i]"> <div [formGroup]="customFieldFormFields.controls[i]">
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) { @switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {

View File

@ -80,8 +80,9 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' 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 { 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 { 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 = { const doc: Document = {
id: 3, id: 3,
@ -783,10 +784,9 @@ describe('DocumentDetailComponent', () => {
const object = { const object = {
id: 22, id: 22,
name: 'Correspondent22', name: 'Correspondent22',
last_correspondence: new Date().toISOString(),
} as Correspondent } as Correspondent
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
component.filterDocuments([object]) component.filterDocuments([object], DataType.Correspondent)
expect(qfSpy).toHaveBeenCalledWith([ expect(qfSpy).toHaveBeenCalledWith([
{ {
rule_type: FILTER_CORRESPONDENT, rule_type: FILTER_CORRESPONDENT,
@ -799,7 +799,7 @@ describe('DocumentDetailComponent', () => {
initNormally() initNormally()
const object = { id: 22, name: 'DocumentType22' } as DocumentType const object = { id: 22, name: 'DocumentType22' } as DocumentType
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
component.filterDocuments([object]) component.filterDocuments([object], DataType.DocumentType)
expect(qfSpy).toHaveBeenCalledWith([ expect(qfSpy).toHaveBeenCalledWith([
{ {
rule_type: FILTER_DOCUMENT_TYPE, rule_type: FILTER_DOCUMENT_TYPE,
@ -816,7 +816,7 @@ describe('DocumentDetailComponent', () => {
path: '/foo/bar/', path: '/foo/bar/',
} as StoragePath } as StoragePath
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
component.filterDocuments([object]) component.filterDocuments([object], DataType.StoragePath)
expect(qfSpy).toHaveBeenCalledWith([ expect(qfSpy).toHaveBeenCalledWith([
{ {
rule_type: FILTER_STORAGE_PATH, rule_type: FILTER_STORAGE_PATH,
@ -842,7 +842,7 @@ describe('DocumentDetailComponent', () => {
text_color: '#000000', text_color: '#000000',
} as Tag } as Tag
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
component.filterDocuments([object1, object2]) component.filterDocuments([object1, object2], DataType.Tag)
expect(qfSpy).toHaveBeenCalledWith([ expect(qfSpy).toHaveBeenCalledWith([
{ {
rule_type: FILTER_HAS_TAGS_ALL, rule_type: FILTER_HAS_TAGS_ALL,

View 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 { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
import { HotKeyService } from 'src/app/services/hot-key.service' import { HotKeyService } from 'src/app/services/hot-key.service'
import { PDFDocumentProxy } from 'ng2-pdf-viewer' import { PDFDocumentProxy } from 'ng2-pdf-viewer'
import { DataType } from 'src/app/data/datatype'
enum DocumentDetailNavIDs { enum DocumentDetailNavIDs {
Details = 1, Details = 1,
@ -171,6 +172,8 @@ export class DocumentDetailComponent
public readonly ContentRenderType = ContentRenderType public readonly ContentRenderType = ContentRenderType
public readonly DataType = DataType
@ViewChild('nav') nav: NgbNav @ViewChild('nav') nav: NgbNav
@ViewChild('pdfPreview') set pdfPreview(element) { @ViewChild('pdfPreview') set pdfPreview(element) {
// this gets called when component added or removed from DOM // 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) => { const filterRules: FilterRule[] = items.flatMap((i) => {
if (i.hasOwnProperty('year')) { if (i.hasOwnProperty('year')) {
const isoDateAdapter = new ISODateAdapter() const isoDateAdapter = new ISODateAdapter()
@ -1017,30 +1020,28 @@ export class DocumentDetailComponent
value: dateBefore.toISOString().substring(0, 10), value: dateBefore.toISOString().substring(0, 10),
}, },
] ]
} else if (i.hasOwnProperty('last_correspondence')) { }
// Correspondent switch (type) {
return { case DataType.Correspondent:
rule_type: FILTER_CORRESPONDENT, return {
value: (i as Correspondent).id.toString(), rule_type: FILTER_CORRESPONDENT,
} value: (i as Correspondent).id.toString(),
} else if (i.hasOwnProperty('path')) { }
// Storage Path case DataType.DocumentType:
return { return {
rule_type: FILTER_STORAGE_PATH, rule_type: FILTER_DOCUMENT_TYPE,
value: (i as StoragePath).id.toString(), value: (i as DocumentType).id.toString(),
} }
} else if (i.hasOwnProperty('is_inbox_tag')) { case DataType.StoragePath:
// Tag return {
return { rule_type: FILTER_STORAGE_PATH,
rule_type: FILTER_HAS_TAGS_ALL, value: (i as StoragePath).id.toString(),
value: (i as Tag).id.toString(), }
} case DataType.Tag:
} else { return {
// Document Type, has no specific props rule_type: FILTER_HAS_TAGS_ALL,
return { value: (i as Tag).id.toString(),
rule_type: FILTER_DOCUMENT_TYPE, }
value: (i as DocumentType).id.toString(),
}
} }
}) })

View File

@ -12,6 +12,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { ManagementListComponent } from '../management-list/management-list.component' import { ManagementListComponent } from '../management-list/management-list.component'
import { takeUntil } from 'rxjs'
@Component({ @Component({
selector: 'pngx-correspondent-list', 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) { getDeleteMessage(object: Correspondent) {
return $localize`Do you really want to delete the correspondent "${object.name}"?` return $localize`Do you really want to delete the correspondent "${object.name}"?`
} }

View File

@ -52,7 +52,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
constructor( constructor(
private service: AbstractNameFilterService<T>, protected service: AbstractNameFilterService<T>,
private modalService: NgbModal, private modalService: NgbModal,
private editDialogComponent: any, private editDialogComponent: any,
private toastService: ToastService, private toastService: ToastService,
@ -81,8 +81,8 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
public isLoading: boolean = false public isLoading: boolean = false
private nameFilterDebounce: Subject<string> private nameFilterDebounce: Subject<string>
private unsubscribeNotifier: Subject<any> = new Subject() protected unsubscribeNotifier: Subject<any> = new Subject()
private _nameFilter: string protected _nameFilter: string
public selectedObjects: Set<number> = new Set() public selectedObjects: Set<number> = new Set()
public togggleAll: boolean = false public togggleAll: boolean = false

View File

@ -17,9 +17,10 @@ export abstract class AbstractNameFilterService<
sortField?: string, sortField?: string,
sortReverse?: boolean, sortReverse?: boolean,
nameFilter?: string, nameFilter?: string,
fullPerms?: boolean fullPerms?: boolean,
extraParams?: { [key: string]: any }
) { ) {
let params = {} let params = extraParams ?? {}
if (nameFilter) { if (nameFilter) {
params['name__icontains'] = nameFilter params['name__icontains'] = nameFilter
} }

View File

@ -291,7 +291,7 @@ class OwnedObjectSerializer(
class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer): class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer):
last_correspondence = serializers.DateTimeField(read_only=True) last_correspondence = serializers.DateTimeField(read_only=True, required=False)
class Meta: class Meta:
model = Correspondent model = Correspondent

View File

@ -1,8 +1,10 @@
import datetime
import json import json
from unittest import mock from unittest import mock
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@ -89,6 +91,57 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
results = response.data["results"] results = response.data["results"]
self.assertEqual(len(results), 2) 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): class TestApiStoragePaths(DirectoriesMixin, APITestCase):
ENDPOINT = "/api/storage_paths/" ENDPOINT = "/api/storage_paths/"

View File

@ -253,14 +253,7 @@ class PermissionsAwareDocumentCountMixin(PassUserMixin):
class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
model = Correspondent model = Correspondent
queryset = ( queryset = Correspondent.objects.select_related("owner").order_by(Lower("name"))
Correspondent.objects.prefetch_related("documents")
.annotate(
last_correspondence=Max("documents__created"),
)
.select_related("owner")
.order_by(Lower("name"))
)
serializer_class = CorrespondentSerializer serializer_class = CorrespondentSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
@ -279,6 +272,19 @@ class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
"last_correspondence", "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): class TagViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin):
model = Tag model = Tag