mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Enhancement: only include correspondent 'last_correspondence' if requested (#6792)
This commit is contained in:
		| @@ -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) { | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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) { | ||||||
|  |         case DataType.Correspondent: | ||||||
|           return { |           return { | ||||||
|             rule_type: FILTER_CORRESPONDENT, |             rule_type: FILTER_CORRESPONDENT, | ||||||
|             value: (i as Correspondent).id.toString(), |             value: (i as Correspondent).id.toString(), | ||||||
|           } |           } | ||||||
|       } else if (i.hasOwnProperty('path')) { |         case DataType.DocumentType: | ||||||
|         // Storage Path |           return { | ||||||
|  |             rule_type: FILTER_DOCUMENT_TYPE, | ||||||
|  |             value: (i as DocumentType).id.toString(), | ||||||
|  |           } | ||||||
|  |         case DataType.StoragePath: | ||||||
|           return { |           return { | ||||||
|             rule_type: FILTER_STORAGE_PATH, |             rule_type: FILTER_STORAGE_PATH, | ||||||
|             value: (i as StoragePath).id.toString(), |             value: (i as StoragePath).id.toString(), | ||||||
|           } |           } | ||||||
|       } else if (i.hasOwnProperty('is_inbox_tag')) { |         case DataType.Tag: | ||||||
|         // Tag |  | ||||||
|           return { |           return { | ||||||
|             rule_type: FILTER_HAS_TAGS_ALL, |             rule_type: FILTER_HAS_TAGS_ALL, | ||||||
|             value: (i as Tag).id.toString(), |             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(), |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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}"?` | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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/" | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon