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 | ||||
|     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> | ||||
|                     } | ||||
|                     @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) { | ||||
|                       <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> | ||||
|               <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-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-date i18n-title title="Date created" formControlName="created" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)" | ||||
|               [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)" | ||||
|               (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)" | ||||
|   | ||||
| @@ -208,7 +208,7 @@ export class DocumentDetailComponent | ||||
|   documentForm: FormGroup = new FormGroup({ | ||||
|     title: new FormControl(''), | ||||
|     content: new FormControl(''), | ||||
|     created_date: new FormControl(), | ||||
|     created: new FormControl(), | ||||
|     correspondent: new FormControl(), | ||||
|     document_type: new FormControl(), | ||||
|     storage_path: new FormControl(), | ||||
| @@ -490,7 +490,7 @@ export class DocumentDetailComponent | ||||
|           this.store = new BehaviorSubject({ | ||||
|             title: doc.title, | ||||
|             content: doc.content, | ||||
|             created_date: doc.created_date, | ||||
|             created: doc.created, | ||||
|             correspondent: doc.correspondent, | ||||
|             document_type: doc.document_type, | ||||
|             storage_path: doc.storage_path, | ||||
|   | ||||
| @@ -112,14 +112,14 @@ | ||||
|               @if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) { | ||||
|                 <ng-template #dateTooltip> | ||||
|                   <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>Modified: {{ document.modified | customDate }}</span> | ||||
|                   </div> | ||||
|                 </ng-template> | ||||
|                 @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"> | ||||
|                     <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> | ||||
|                 } | ||||
|                 @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"> | ||||
|               <ng-template #dateTooltip> | ||||
|                 <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>Modified: {{ document.modified | customDate }}</span> | ||||
|                 </div> | ||||
|               </ng-template> | ||||
|               <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> | ||||
|                 <small>{{document.created_date | customDate:'mediumDate'}}</small> | ||||
|                 <small>{{document.created | customDate:'mediumDate'}}</small> | ||||
|               </div> | ||||
|             </div> | ||||
|           } | ||||
|   | ||||
| @@ -348,7 +348,7 @@ | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.CREATED)) { | ||||
|                   <td> | ||||
|                     {{d.created_date | customDate}} | ||||
|                     {{d.created | customDate}} | ||||
|                   </td> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.ADDED)) { | ||||
|   | ||||
| @@ -130,9 +130,6 @@ export interface Document extends ObjectWithPermissions { | ||||
|   // UTC | ||||
|   created?: Date | ||||
|  | ||||
|   // localized date | ||||
|   created_date?: Date | ||||
|  | ||||
|   modified?: Date | ||||
|  | ||||
|   added?: Date | ||||
|   | ||||
| @@ -190,8 +190,6 @@ export class DocumentService extends AbstractPaperlessService<Document> { | ||||
|   } | ||||
|  | ||||
|   patch(o: Document): Observable<Document> { | ||||
|     // we want to only set created_date | ||||
|     delete o.created | ||||
|     o.remove_inbox_tags = !!this.settingsService.get( | ||||
|       SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS | ||||
|     ) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ const base_url = new URL(document.baseURI) | ||||
| export const environment = { | ||||
|   production: true, | ||||
|   apiBaseUrl: document.baseURI + 'api/', | ||||
|   apiVersion: '8', // match src/paperless/settings.py | ||||
|   apiVersion: '9', // match src/paperless/settings.py | ||||
|   appTitle: 'Paperless-ngx', | ||||
|   version: '2.15.3', | ||||
|   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.translation import gettext as _ | ||||
| from drf_spectacular.utils import extend_schema_field | ||||
| from drf_spectacular.utils import extend_schema_serializer | ||||
| from drf_writable_nested.serializers import NestedUpdateMixin | ||||
| from guardian.core import ObjectPermissionChecker | ||||
| from guardian.shortcuts import get_users_with_perms | ||||
| @@ -891,6 +892,9 @@ class NotesSerializer(serializers.ModelSerializer): | ||||
|         return ret | ||||
|  | ||||
|  | ||||
| @extend_schema_serializer( | ||||
|     deprecate_fields=["created_date"], | ||||
| ) | ||||
| class DocumentSerializer( | ||||
|     OwnedObjectSerializer, | ||||
|     NestedUpdateMixin, | ||||
| @@ -943,6 +947,22 @@ class DocumentSerializer( | ||||
|         doc = super().to_representation(instance) | ||||
|         if self.truncate_content and "content" in self.fields: | ||||
|             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 | ||||
|  | ||||
|     def validate(self, attrs): | ||||
| @@ -968,6 +988,9 @@ class DocumentSerializer( | ||||
|             instance.created = validated_data.get("created_date") | ||||
|             instance.save() | ||||
|         if "created_date" in validated_data: | ||||
|             logger.warning( | ||||
|                 "created_date is deprecated, use created instead", | ||||
|             ) | ||||
|             validated_data.pop("created_date") | ||||
|         if instance.custom_fields.count() > 0 and "custom_fields" in validated_data: | ||||
|             incoming_custom_fields = [ | ||||
|   | ||||
| @@ -171,6 +171,38 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): | ||||
|         results = response.data["results"] | ||||
|         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): | ||||
|         """ | ||||
|         GIVEN: | ||||
|   | ||||
| @@ -342,10 +342,10 @@ REST_FRAMEWORK = { | ||||
|         "rest_framework.authentication.SessionAuthentication", | ||||
|     ], | ||||
|     "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 | ||||
|     # 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 | ||||
|     "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon