mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Fix: allow removing dead document links from UI, validate via API (#8081)
This commit is contained in:
		| @@ -706,7 +706,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context> |           <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context> | ||||||
|           <context context-type="linenumber">51</context> |           <context context-type="linenumber">57</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context> | ||||||
| @@ -4857,18 +4857,29 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context> |           <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context> | ||||||
|           <context context-type="linenumber">43</context> |           <context context-type="linenumber">43</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context> | ||||||
|  |           <context context-type="linenumber">49</context> | ||||||
|  |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1388712764439031120" datatype="html"> |       <trans-unit id="1388712764439031120" datatype="html"> | ||||||
|         <source>Open link</source> |         <source>Open link</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context> |           <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context> | ||||||
|           <context context-type="linenumber">44</context> |           <context context-type="linenumber">45</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/input/url/url.component.html</context> |           <context context-type="sourcefile">src/app/components/common/input/url/url.component.html</context> | ||||||
|           <context context-type="linenumber">14</context> |           <context context-type="linenumber">14</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|  |       <trans-unit id="6595008830732269870" datatype="html"> | ||||||
|  |         <source>Not found</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context> | ||||||
|  |           <context context-type="linenumber">50</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|       <trans-unit id="5676637575587497817" datatype="html"> |       <trans-unit id="5676637575587497817" datatype="html"> | ||||||
|         <source>Search for documents</source> |         <source>Search for documents</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|   | |||||||
| @@ -41,9 +41,15 @@ | |||||||
|     <ng-template ng-label-tmp let-document="item"> |     <ng-template ng-label-tmp let-document="item"> | ||||||
|       <div class="d-flex align-items-center"> |       <div class="d-flex align-items-center"> | ||||||
|         <button class="btn p-0 lh-1" *ngIf="!disabled" (click)="unselect(document)" title="Remove link" i18n-title><i-bs name="x"></i-bs></button> |         <button class="btn p-0 lh-1" *ngIf="!disabled" (click)="unselect(document)" title="Remove link" i18n-title><i-bs name="x"></i-bs></button> | ||||||
|         <a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();" title="Open link" i18n-title> |         @if (document.title) { | ||||||
|           <i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{document.title}}</span> |           <a routerLink="/documents/{{document.id}}" class="badge bg-light text-primary" (mousedown)="$event.stopImmediatePropagation();" title="Open link" i18n-title> | ||||||
|         </a> |             <i-bs width="0.9em" height="0.9em" name="file-text"></i-bs> <span>{{document.title}}</span> | ||||||
|  |           </a> | ||||||
|  |         } @else { | ||||||
|  |           <span class="badge bg-light text-muted" (click)="unselect(document)" title="Remove link" i18n-title> | ||||||
|  |             <i-bs width="0.9em" height="0.9em" name="exclamation-triangle-fill"></i-bs> <span i18n>Not found</span> | ||||||
|  |           </span> | ||||||
|  |         } | ||||||
|       </div> |       </div> | ||||||
|     </ng-template> |     </ng-template> | ||||||
|     <ng-template ng-loadingspinner-tmp> |     <ng-template ng-loadingspinner-tmp> | ||||||
|   | |||||||
| @@ -71,9 +71,9 @@ export class DocumentLinkComponent | |||||||
|         .pipe(takeUntil(this.unsubscribeNotifier)) |         .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|         .subscribe((documentResults) => { |         .subscribe((documentResults) => { | ||||||
|           this.loading = false |           this.loading = false | ||||||
|           this.selectedDocuments = documentIDs |           this.selectedDocuments = documentIDs.map((id) => | ||||||
|             .map((id) => documentResults.results.find((d) => d.id === id)) |             documentResults.results.find((d) => d.id === id) | ||||||
|             .filter((d) => d) |           ) | ||||||
|           super.writeValue(documentIDs) |           super.writeValue(documentIDs) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| @@ -114,7 +114,7 @@ export class DocumentLinkComponent | |||||||
|  |  | ||||||
|   unselect(document: Document): void { |   unselect(document: Document): void { | ||||||
|     this.selectedDocuments = this.selectedDocuments.filter( |     this.selectedDocuments = this.selectedDocuments.filter( | ||||||
|       (d) => d.id !== document.id |       (d) => d && d.id !== document.id | ||||||
|     ) |     ) | ||||||
|     this.onChange(this.selectedDocuments.map((d) => d.id)) |     this.onChange(this.selectedDocuments.map((d) => d.id)) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -651,6 +651,14 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer): | |||||||
|                     raise serializers.ValidationError( |                     raise serializers.ValidationError( | ||||||
|                         f"Value must be index of an element in {select_options}", |                         f"Value must be index of an element in {select_options}", | ||||||
|                     ) |                     ) | ||||||
|  |             elif field.data_type == CustomField.FieldDataType.DOCUMENTLINK: | ||||||
|  |                 doc_ids = data["value"] | ||||||
|  |                 if Document.objects.filter(id__in=doc_ids).count() != len( | ||||||
|  |                     data["value"], | ||||||
|  |                 ): | ||||||
|  |                     raise serializers.ValidationError( | ||||||
|  |                         "Some documents in value don't exist or were specified twice.", | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|         return data |         return data | ||||||
|  |  | ||||||
|   | |||||||
| @@ -740,6 +740,42 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase): | |||||||
|         self.assertEqual(CustomFieldInstance.objects.count(), 0) |         self.assertEqual(CustomFieldInstance.objects.count(), 0) | ||||||
|         self.assertEqual(len(doc.custom_fields.all()), 0) |         self.assertEqual(len(doc.custom_fields.all()), 0) | ||||||
|  |  | ||||||
|  |     def test_custom_field_value_documentlink_validation(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Document & custom field exist | ||||||
|  |         WHEN: | ||||||
|  |             - API request to set a field value to a document that does not exist | ||||||
|  |         THEN: | ||||||
|  |             - HTTP 400 is returned | ||||||
|  |             - No field instance is created or attached to the document | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         doc = Document.objects.create( | ||||||
|  |             title="WOW", | ||||||
|  |             content="the content", | ||||||
|  |             checksum="123", | ||||||
|  |             mime_type="application/pdf", | ||||||
|  |         ) | ||||||
|  |         custom_field_documentlink = CustomField.objects.create( | ||||||
|  |             name="Test Custom Field Doc Link", | ||||||
|  |             data_type=CustomField.FieldDataType.DOCUMENTLINK, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         resp = self.client.patch( | ||||||
|  |             f"/api/documents/{doc.id}/", | ||||||
|  |             data={ | ||||||
|  |                 "custom_fields": [ | ||||||
|  |                     {"field": custom_field_documentlink.id, "value": [999]}, | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|  |             format="json", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) | ||||||
|  |         self.assertEqual(CustomFieldInstance.objects.count(), 0) | ||||||
|  |         self.assertEqual(len(doc.custom_fields.all()), 0) | ||||||
|  |  | ||||||
|     def test_custom_field_not_null(self): |     def test_custom_field_not_null(self): | ||||||
|         """ |         """ | ||||||
|         GIVEN: |         GIVEN: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon