mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Fix: handle created change with api version increment, use created only on frontend, deprecate created_date (#9962)
This commit is contained in:
		| @@ -418,3 +418,9 @@ Initial API version. | |||||||
|  |  | ||||||
| -   The user field of document notes now returns a simplified user object | -   The user field of document notes now returns a simplified user object | ||||||
|     rather than just the user ID. |     rather than just the user ID. | ||||||
|  |  | ||||||
|  | #### Version 9 | ||||||
|  |  | ||||||
|  | -   The document `created` field is now a date, not a datetime. The | ||||||
|  |     `created_date` field is considered deprecated and will be removed in a | ||||||
|  |     future version. | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ | |||||||
|                       <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a> |                       <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a> | ||||||
|                     } |                     } | ||||||
|                     @case (DisplayField.CREATED) { |                     @case (DisplayField.CREATED) { | ||||||
|                       <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created_date | customDate}}</a> |                       <a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created | customDate}}</a> | ||||||
|                     } |                     } | ||||||
|                     @case (DisplayField.TITLE) { |                     @case (DisplayField.TITLE) { | ||||||
|                       <a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a> |                       <a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a> | ||||||
|   | |||||||
| @@ -129,8 +129,8 @@ | |||||||
|             <div> |             <div> | ||||||
|               <pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text> |               <pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text> | ||||||
|               <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" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" | ||||||
|               [error]="error?.created_date"></pngx-input-date> |               [error]="error?.created"></pngx-input-date> | ||||||
|               <pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Correspondent)" |               <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)" [hideAddButton]="createDisabled(DataType.Correspondent)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select> |               (createNew)="createCorrespondent($event)" [hideAddButton]="createDisabled(DataType.Correspondent)" [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, DataType.DocumentType)" |               <pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.DocumentType)" | ||||||
|   | |||||||
| @@ -208,7 +208,7 @@ export class DocumentDetailComponent | |||||||
|   documentForm: FormGroup = new FormGroup({ |   documentForm: FormGroup = new FormGroup({ | ||||||
|     title: new FormControl(''), |     title: new FormControl(''), | ||||||
|     content: new FormControl(''), |     content: new FormControl(''), | ||||||
|     created_date: new FormControl(), |     created: new FormControl(), | ||||||
|     correspondent: new FormControl(), |     correspondent: new FormControl(), | ||||||
|     document_type: new FormControl(), |     document_type: new FormControl(), | ||||||
|     storage_path: new FormControl(), |     storage_path: new FormControl(), | ||||||
| @@ -490,7 +490,7 @@ export class DocumentDetailComponent | |||||||
|           this.store = new BehaviorSubject({ |           this.store = new BehaviorSubject({ | ||||||
|             title: doc.title, |             title: doc.title, | ||||||
|             content: doc.content, |             content: doc.content, | ||||||
|             created_date: doc.created_date, |             created: doc.created, | ||||||
|             correspondent: doc.correspondent, |             correspondent: doc.correspondent, | ||||||
|             document_type: doc.document_type, |             document_type: doc.document_type, | ||||||
|             storage_path: doc.storage_path, |             storage_path: doc.storage_path, | ||||||
|   | |||||||
| @@ -112,14 +112,14 @@ | |||||||
|               @if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) { |               @if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) { | ||||||
|                 <ng-template #dateTooltip> |                 <ng-template #dateTooltip> | ||||||
|                   <div class="d-flex flex-column text-light"> |                   <div class="d-flex flex-column text-light"> | ||||||
|                     <span i18n>Created: {{ document.created_date | customDate }}</span> |                     <span i18n>Created: {{ document.created | customDate }}</span> | ||||||
|                     <span i18n>Added: {{ document.added | customDate }}</span> |                     <span i18n>Added: {{ document.added | customDate }}</span> | ||||||
|                     <span i18n>Modified: {{ document.modified | customDate }}</span> |                     <span i18n>Modified: {{ document.modified | customDate }}</span> | ||||||
|                   </div> |                   </div> | ||||||
|                 </ng-template> |                 </ng-template> | ||||||
|                 @if (displayFields.includes(DisplayField.CREATED)) { |                 @if (displayFields.includes(DisplayField.CREATED)) { | ||||||
|                   <div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip"> |                   <div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip"> | ||||||
|                     <i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small> |                     <i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created | customDate:'mediumDate'}}</small> | ||||||
|                   </div> |                   </div> | ||||||
|                 } |                 } | ||||||
|                 @if (displayFields.includes(DisplayField.ADDED)) { |                 @if (displayFields.includes(DisplayField.ADDED)) { | ||||||
|   | |||||||
| @@ -73,14 +73,14 @@ | |||||||
|             <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between"> |             <div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between"> | ||||||
|               <ng-template #dateTooltip> |               <ng-template #dateTooltip> | ||||||
|                 <div class="d-flex flex-column text-light"> |                 <div class="d-flex flex-column text-light"> | ||||||
|                   <span i18n>Created: {{ document.created_date | customDate }}</span> |                   <span i18n>Created: {{ document.created | customDate }}</span> | ||||||
|                   <span i18n>Added: {{ document.added | customDate }}</span> |                   <span i18n>Added: {{ document.added | customDate }}</span> | ||||||
|                   <span i18n>Modified: {{ document.modified | customDate }}</span> |                   <span i18n>Modified: {{ document.modified | customDate }}</span> | ||||||
|                 </div> |                 </div> | ||||||
|               </ng-template> |               </ng-template> | ||||||
|               <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip"> |               <div class="ps-0 p-1" placement="top" [ngbTooltip]="dateTooltip"> | ||||||
|                 <i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs> |                 <i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs> | ||||||
|                 <small>{{document.created_date | customDate:'mediumDate'}}</small> |                 <small>{{document.created | customDate:'mediumDate'}}</small> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           } |           } | ||||||
|   | |||||||
| @@ -348,7 +348,7 @@ | |||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.CREATED)) { |                 @if (activeDisplayFields.includes(DisplayField.CREATED)) { | ||||||
|                   <td> |                   <td> | ||||||
|                     {{d.created_date | customDate}} |                     {{d.created | customDate}} | ||||||
|                   </td> |                   </td> | ||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.ADDED)) { |                 @if (activeDisplayFields.includes(DisplayField.ADDED)) { | ||||||
|   | |||||||
| @@ -130,9 +130,6 @@ export interface Document extends ObjectWithPermissions { | |||||||
|   // UTC |   // UTC | ||||||
|   created?: Date |   created?: Date | ||||||
|  |  | ||||||
|   // localized date |  | ||||||
|   created_date?: Date |  | ||||||
|  |  | ||||||
|   modified?: Date |   modified?: Date | ||||||
|  |  | ||||||
|   added?: Date |   added?: Date | ||||||
|   | |||||||
| @@ -190,8 +190,6 @@ export class DocumentService extends AbstractPaperlessService<Document> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   patch(o: Document): Observable<Document> { |   patch(o: Document): Observable<Document> { | ||||||
|     // we want to only set created_date |  | ||||||
|     delete o.created |  | ||||||
|     o.remove_inbox_tags = !!this.settingsService.get( |     o.remove_inbox_tags = !!this.settingsService.get( | ||||||
|       SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS |       SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ const base_url = new URL(document.baseURI) | |||||||
| export const environment = { | export const environment = { | ||||||
|   production: true, |   production: true, | ||||||
|   apiBaseUrl: document.baseURI + 'api/', |   apiBaseUrl: document.baseURI + 'api/', | ||||||
|   apiVersion: '8', // match src/paperless/settings.py |   apiVersion: '9', // match src/paperless/settings.py | ||||||
|   appTitle: 'Paperless-ngx', |   appTitle: 'Paperless-ngx', | ||||||
|   version: '2.15.3', |   version: '2.15.3', | ||||||
|   webSocketHost: window.location.host, |   webSocketHost: window.location.host, | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ from django.utils.crypto import get_random_string | |||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from drf_spectacular.utils import extend_schema_field | from drf_spectacular.utils import extend_schema_field | ||||||
|  | from drf_spectacular.utils import extend_schema_serializer | ||||||
| from drf_writable_nested.serializers import NestedUpdateMixin | from drf_writable_nested.serializers import NestedUpdateMixin | ||||||
| from guardian.core import ObjectPermissionChecker | from guardian.core import ObjectPermissionChecker | ||||||
| from guardian.shortcuts import get_users_with_perms | from guardian.shortcuts import get_users_with_perms | ||||||
| @@ -891,6 +892,9 @@ class NotesSerializer(serializers.ModelSerializer): | |||||||
|         return ret |         return ret | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @extend_schema_serializer( | ||||||
|  |     deprecate_fields=["created_date"], | ||||||
|  | ) | ||||||
| class DocumentSerializer( | class DocumentSerializer( | ||||||
|     OwnedObjectSerializer, |     OwnedObjectSerializer, | ||||||
|     NestedUpdateMixin, |     NestedUpdateMixin, | ||||||
| @@ -943,6 +947,22 @@ class DocumentSerializer( | |||||||
|         doc = super().to_representation(instance) |         doc = super().to_representation(instance) | ||||||
|         if self.truncate_content and "content" in self.fields: |         if self.truncate_content and "content" in self.fields: | ||||||
|             doc["content"] = doc.get("content")[0:550] |             doc["content"] = doc.get("content")[0:550] | ||||||
|  |  | ||||||
|  |         request = self.context.get("request") | ||||||
|  |         api_version = int( | ||||||
|  |             request.version if request else settings.REST_FRAMEWORK["DEFAULT_VERSION"], | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if api_version < 9: | ||||||
|  |             # provide created as a datetime for backwards compatibility | ||||||
|  |             from django.utils import timezone | ||||||
|  |  | ||||||
|  |             doc["created"] = timezone.make_aware( | ||||||
|  |                 datetime.combine( | ||||||
|  |                     instance.created, | ||||||
|  |                     datetime.min.time(), | ||||||
|  |                 ), | ||||||
|  |             ).isoformat() | ||||||
|         return doc |         return doc | ||||||
|  |  | ||||||
|     def validate(self, attrs): |     def validate(self, attrs): | ||||||
| @@ -968,6 +988,9 @@ class DocumentSerializer( | |||||||
|             instance.created = validated_data.get("created_date") |             instance.created = validated_data.get("created_date") | ||||||
|             instance.save() |             instance.save() | ||||||
|         if "created_date" in validated_data: |         if "created_date" in validated_data: | ||||||
|  |             logger.warning( | ||||||
|  |                 "created_date is deprecated, use created instead", | ||||||
|  |             ) | ||||||
|             validated_data.pop("created_date") |             validated_data.pop("created_date") | ||||||
|         if instance.custom_fields.count() > 0 and "custom_fields" in validated_data: |         if instance.custom_fields.count() > 0 and "custom_fields" in validated_data: | ||||||
|             incoming_custom_fields = [ |             incoming_custom_fields = [ | ||||||
|   | |||||||
| @@ -171,6 +171,38 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): | |||||||
|         results = response.data["results"] |         results = response.data["results"] | ||||||
|         self.assertEqual(len(results[0]), 0) |         self.assertEqual(len(results[0]), 0) | ||||||
|  |  | ||||||
|  |     def test_document_legacy_created_format(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Existing document | ||||||
|  |         WHEN: | ||||||
|  |             - Document is requested with api version ≥ 9 | ||||||
|  |             - Document is requested with api version < 9 | ||||||
|  |         THEN: | ||||||
|  |             - Document created field is returned as date | ||||||
|  |             - Document created field is returned as datetime | ||||||
|  |         """ | ||||||
|  |         doc = Document.objects.create( | ||||||
|  |             title="none", | ||||||
|  |             checksum="123", | ||||||
|  |             mime_type="application/pdf", | ||||||
|  |             created=date(2023, 1, 1), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         response = self.client.get( | ||||||
|  |             f"/api/documents/{doc.pk}/", | ||||||
|  |             headers={"Accept": "application/json; version=8"}, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||||
|  |         self.assertRegex(response.data["created"], r"^2023-01-01T00:00:00.*$") | ||||||
|  |  | ||||||
|  |         response = self.client.get( | ||||||
|  |             f"/api/documents/{doc.pk}/", | ||||||
|  |             headers={"Accept": "application/json; version=9"}, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, status.HTTP_200_OK) | ||||||
|  |         self.assertEqual(response.data["created"], "2023-01-01") | ||||||
|  |  | ||||||
|     def test_document_update_with_created_date(self): |     def test_document_update_with_created_date(self): | ||||||
|         """ |         """ | ||||||
|         GIVEN: |         GIVEN: | ||||||
|   | |||||||
| @@ -342,10 +342,10 @@ REST_FRAMEWORK = { | |||||||
|         "rest_framework.authentication.SessionAuthentication", |         "rest_framework.authentication.SessionAuthentication", | ||||||
|     ], |     ], | ||||||
|     "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning", |     "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning", | ||||||
|     "DEFAULT_VERSION": "8",  # match src-ui/src/environments/environment.prod.ts |     "DEFAULT_VERSION": "9",  # match src-ui/src/environments/environment.prod.ts | ||||||
|     # Make sure these are ordered and that the most recent version appears |     # Make sure these are ordered and that the most recent version appears | ||||||
|     # last. See api.md#api-versioning when adding new versions. |     # last. See api.md#api-versioning when adding new versions. | ||||||
|     "ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6", "7", "8"], |     "ALLOWED_VERSIONS": ["1", "2", "3", "4", "5", "6", "7", "8", "9"], | ||||||
|     # DRF Spectacular default schema |     # DRF Spectacular default schema | ||||||
|     "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", |     "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon