mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Fix: reflect doc links in bulk modify custom fields (#8962)
This commit is contained in:
		| @@ -12,6 +12,7 @@ from celery import shared_task | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
|  | from django.utils import timezone | ||||||
|  |  | ||||||
| from documents.data_models import ConsumableDocument | from documents.data_models import ConsumableDocument | ||||||
| from documents.data_models import DocumentMetadataOverrides | from documents.data_models import DocumentMetadataOverrides | ||||||
| @@ -177,6 +178,12 @@ def modify_custom_fields( | |||||||
|                 field_id=field_id, |                 field_id=field_id, | ||||||
|                 defaults=defaults, |                 defaults=defaults, | ||||||
|             ) |             ) | ||||||
|  |             if ( | ||||||
|  |                 custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK | ||||||
|  |                 and value | ||||||
|  |             ): | ||||||
|  |                 doc = Document.objects.get(id=doc_id) | ||||||
|  |                 reflect_doclinks(doc, custom_field, value) | ||||||
|     CustomFieldInstance.objects.filter( |     CustomFieldInstance.objects.filter( | ||||||
|         document_id__in=affected_docs, |         document_id__in=affected_docs, | ||||||
|         field_id__in=remove_custom_fields, |         field_id__in=remove_custom_fields, | ||||||
| @@ -447,3 +454,87 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]: | |||||||
|         logger.exception(f"Error deleting pages from document {doc.id}: {e}") |         logger.exception(f"Error deleting pages from document {doc.id}: {e}") | ||||||
|  |  | ||||||
|     return "OK" |     return "OK" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def reflect_doclinks( | ||||||
|  |     document: Document, | ||||||
|  |     field: CustomField, | ||||||
|  |     target_doc_ids: list[int], | ||||||
|  | ): | ||||||
|  |     """ | ||||||
|  |     Add or remove 'symmetrical' links to `document` on all `target_doc_ids` | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     if target_doc_ids is None: | ||||||
|  |         target_doc_ids = [] | ||||||
|  |  | ||||||
|  |     # Check if any documents are going to be removed from the current list of links and remove the symmetrical links | ||||||
|  |     current_field_instance = CustomFieldInstance.objects.filter( | ||||||
|  |         field=field, | ||||||
|  |         document=document, | ||||||
|  |     ).first() | ||||||
|  |     if current_field_instance is not None and current_field_instance.value is not None: | ||||||
|  |         for doc_id in current_field_instance.value: | ||||||
|  |             if doc_id not in target_doc_ids: | ||||||
|  |                 remove_doclink( | ||||||
|  |                     document=document, | ||||||
|  |                     field=field, | ||||||
|  |                     target_doc_id=doc_id, | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |     # Create an instance if target doc doesn't have this field or append it to an existing one | ||||||
|  |     existing_custom_field_instances = { | ||||||
|  |         custom_field.document_id: custom_field | ||||||
|  |         for custom_field in CustomFieldInstance.objects.filter( | ||||||
|  |             field=field, | ||||||
|  |             document_id__in=target_doc_ids, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     custom_field_instances_to_create = [] | ||||||
|  |     custom_field_instances_to_update = [] | ||||||
|  |     for target_doc_id in target_doc_ids: | ||||||
|  |         target_doc_field_instance = existing_custom_field_instances.get( | ||||||
|  |             target_doc_id, | ||||||
|  |         ) | ||||||
|  |         if target_doc_field_instance is None: | ||||||
|  |             custom_field_instances_to_create.append( | ||||||
|  |                 CustomFieldInstance( | ||||||
|  |                     document_id=target_doc_id, | ||||||
|  |                     field=field, | ||||||
|  |                     value_document_ids=[document.id], | ||||||
|  |                 ), | ||||||
|  |             ) | ||||||
|  |         elif target_doc_field_instance.value is None: | ||||||
|  |             target_doc_field_instance.value_document_ids = [document.id] | ||||||
|  |             custom_field_instances_to_update.append(target_doc_field_instance) | ||||||
|  |         elif document.id not in target_doc_field_instance.value: | ||||||
|  |             target_doc_field_instance.value_document_ids.append(document.id) | ||||||
|  |             custom_field_instances_to_update.append(target_doc_field_instance) | ||||||
|  |  | ||||||
|  |     CustomFieldInstance.objects.bulk_create(custom_field_instances_to_create) | ||||||
|  |     CustomFieldInstance.objects.bulk_update( | ||||||
|  |         custom_field_instances_to_update, | ||||||
|  |         ["value_document_ids"], | ||||||
|  |     ) | ||||||
|  |     Document.objects.filter(id__in=target_doc_ids).update(modified=timezone.now()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def remove_doclink( | ||||||
|  |     document: Document, | ||||||
|  |     field: CustomField, | ||||||
|  |     target_doc_id: int, | ||||||
|  | ): | ||||||
|  |     """ | ||||||
|  |     Removes a 'symmetrical' link to `document` from the target document's existing custom field instance | ||||||
|  |     """ | ||||||
|  |     target_doc_field_instance = CustomFieldInstance.objects.filter( | ||||||
|  |         document_id=target_doc_id, | ||||||
|  |         field=field, | ||||||
|  |     ).first() | ||||||
|  |     if ( | ||||||
|  |         target_doc_field_instance is not None | ||||||
|  |         and document.id in target_doc_field_instance.value | ||||||
|  |     ): | ||||||
|  |         target_doc_field_instance.value.remove(document.id) | ||||||
|  |         target_doc_field_instance.save() | ||||||
|  |     Document.objects.filter(id=target_doc_id).update(modified=timezone.now()) | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ from django.core.validators import DecimalValidator | |||||||
| from django.core.validators import MaxLengthValidator | from django.core.validators import MaxLengthValidator | ||||||
| from django.core.validators import RegexValidator | from django.core.validators import RegexValidator | ||||||
| from django.core.validators import integer_validator | from django.core.validators import integer_validator | ||||||
| from django.utils import timezone |  | ||||||
| from django.utils.crypto import get_random_string | 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 _ | ||||||
| @@ -647,7 +646,7 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer): | |||||||
|  |  | ||||||
|         if custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK: |         if custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK: | ||||||
|             # prior to update so we can look for any docs that are going to be removed |             # prior to update so we can look for any docs that are going to be removed | ||||||
|             self.reflect_doclinks(document, custom_field, validated_data["value"]) |             bulk_edit.reflect_doclinks(document, custom_field, validated_data["value"]) | ||||||
|  |  | ||||||
|         # Actually update or create the instance, providing the value |         # Actually update or create the instance, providing the value | ||||||
|         # to fill in the correct attribute based on the type |         # to fill in the correct attribute based on the type | ||||||
| @@ -767,89 +766,6 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer): | |||||||
|  |  | ||||||
|         return ret |         return ret | ||||||
|  |  | ||||||
|     def reflect_doclinks( |  | ||||||
|         self, |  | ||||||
|         document: Document, |  | ||||||
|         field: CustomField, |  | ||||||
|         target_doc_ids: list[int], |  | ||||||
|     ): |  | ||||||
|         """ |  | ||||||
|         Add or remove 'symmetrical' links to `document` on all `target_doc_ids` |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         if target_doc_ids is None: |  | ||||||
|             target_doc_ids = [] |  | ||||||
|  |  | ||||||
|         # Check if any documents are going to be removed from the current list of links and remove the symmetrical links |  | ||||||
|         current_field_instance = CustomFieldInstance.objects.filter( |  | ||||||
|             field=field, |  | ||||||
|             document=document, |  | ||||||
|         ).first() |  | ||||||
|         if ( |  | ||||||
|             current_field_instance is not None |  | ||||||
|             and current_field_instance.value is not None |  | ||||||
|         ): |  | ||||||
|             for doc_id in current_field_instance.value: |  | ||||||
|                 if doc_id not in target_doc_ids: |  | ||||||
|                     self.remove_doclink(document, field, doc_id) |  | ||||||
|  |  | ||||||
|         # Create an instance if target doc doesn't have this field or append it to an existing one |  | ||||||
|         existing_custom_field_instances = { |  | ||||||
|             custom_field.document_id: custom_field |  | ||||||
|             for custom_field in CustomFieldInstance.objects.filter( |  | ||||||
|                 field=field, |  | ||||||
|                 document_id__in=target_doc_ids, |  | ||||||
|             ) |  | ||||||
|         } |  | ||||||
|         custom_field_instances_to_create = [] |  | ||||||
|         custom_field_instances_to_update = [] |  | ||||||
|         for target_doc_id in target_doc_ids: |  | ||||||
|             target_doc_field_instance = existing_custom_field_instances.get( |  | ||||||
|                 target_doc_id, |  | ||||||
|             ) |  | ||||||
|             if target_doc_field_instance is None: |  | ||||||
|                 custom_field_instances_to_create.append( |  | ||||||
|                     CustomFieldInstance( |  | ||||||
|                         document_id=target_doc_id, |  | ||||||
|                         field=field, |  | ||||||
|                         value_document_ids=[document.id], |  | ||||||
|                     ), |  | ||||||
|                 ) |  | ||||||
|             elif target_doc_field_instance.value is None: |  | ||||||
|                 target_doc_field_instance.value_document_ids = [document.id] |  | ||||||
|                 custom_field_instances_to_update.append(target_doc_field_instance) |  | ||||||
|             elif document.id not in target_doc_field_instance.value: |  | ||||||
|                 target_doc_field_instance.value_document_ids.append(document.id) |  | ||||||
|                 custom_field_instances_to_update.append(target_doc_field_instance) |  | ||||||
|  |  | ||||||
|         CustomFieldInstance.objects.bulk_create(custom_field_instances_to_create) |  | ||||||
|         CustomFieldInstance.objects.bulk_update( |  | ||||||
|             custom_field_instances_to_update, |  | ||||||
|             ["value_document_ids"], |  | ||||||
|         ) |  | ||||||
|         Document.objects.filter(id__in=target_doc_ids).update(modified=timezone.now()) |  | ||||||
|  |  | ||||||
|     @staticmethod |  | ||||||
|     def remove_doclink( |  | ||||||
|         document: Document, |  | ||||||
|         field: CustomField, |  | ||||||
|         target_doc_id: int, |  | ||||||
|     ): |  | ||||||
|         """ |  | ||||||
|         Removes a 'symmetrical' link to `document` from the target document's existing custom field instance |  | ||||||
|         """ |  | ||||||
|         target_doc_field_instance = CustomFieldInstance.objects.filter( |  | ||||||
|             document_id=target_doc_id, |  | ||||||
|             field=field, |  | ||||||
|         ).first() |  | ||||||
|         if ( |  | ||||||
|             target_doc_field_instance is not None |  | ||||||
|             and document.id in target_doc_field_instance.value |  | ||||||
|         ): |  | ||||||
|             target_doc_field_instance.value.remove(document.id) |  | ||||||
|             target_doc_field_instance.save() |  | ||||||
|         Document.objects.filter(id=target_doc_id).update(modified=timezone.now()) |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = CustomFieldInstance |         model = CustomFieldInstance | ||||||
|         fields = [ |         fields = [ | ||||||
| @@ -951,7 +867,7 @@ class DocumentSerializer( | |||||||
|                 ): |                 ): | ||||||
|                     # Doc link field is being removed entirely |                     # Doc link field is being removed entirely | ||||||
|                     for doc_id in custom_field_instance.value: |                     for doc_id in custom_field_instance.value: | ||||||
|                         CustomFieldInstanceSerializer.remove_doclink( |                         bulk_edit.remove_doclink( | ||||||
|                             instance, |                             instance, | ||||||
|                             custom_field_instance.field, |                             custom_field_instance.field, | ||||||
|                             doc_id, |                             doc_id, | ||||||
|   | |||||||
| @@ -268,7 +268,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase): | |||||||
|         ) |         ) | ||||||
|         cf3 = CustomField.objects.create( |         cf3 = CustomField.objects.create( | ||||||
|             name="cf3", |             name="cf3", | ||||||
|             data_type=CustomField.FieldDataType.STRING, |             data_type=CustomField.FieldDataType.DOCUMENTLINK, | ||||||
|         ) |         ) | ||||||
|         CustomFieldInstance.objects.create( |         CustomFieldInstance.objects.create( | ||||||
|             document=self.doc2, |             document=self.doc2, | ||||||
| @@ -282,9 +282,14 @@ class TestBulkEdit(DirectoriesMixin, TestCase): | |||||||
|             document=self.doc2, |             document=self.doc2, | ||||||
|             field=cf3, |             field=cf3, | ||||||
|         ) |         ) | ||||||
|  |         doc3: Document = Document.objects.create( | ||||||
|  |             title="doc3", | ||||||
|  |             content="content", | ||||||
|  |             checksum="D3", | ||||||
|  |         ) | ||||||
|         bulk_edit.modify_custom_fields( |         bulk_edit.modify_custom_fields( | ||||||
|             [self.doc1.id, self.doc2.id], |             [self.doc1.id, self.doc2.id], | ||||||
|             add_custom_fields={cf2.id: None, cf3.id: "value"}, |             add_custom_fields={cf2.id: None, cf3.id: [doc3.id]}, | ||||||
|             remove_custom_fields=[cf.id], |             remove_custom_fields=[cf.id], | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -301,7 +306,7 @@ class TestBulkEdit(DirectoriesMixin, TestCase): | |||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.doc1.custom_fields.get(field=cf3).value, |             self.doc1.custom_fields.get(field=cf3).value, | ||||||
|             "value", |             [doc3.id], | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.doc2.custom_fields.count(), |             self.doc2.custom_fields.count(), | ||||||
| @@ -309,7 +314,13 @@ class TestBulkEdit(DirectoriesMixin, TestCase): | |||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             self.doc2.custom_fields.get(field=cf3).value, |             self.doc2.custom_fields.get(field=cf3).value, | ||||||
|             "value", |             [doc3.id], | ||||||
|  |         ) | ||||||
|  |         # assert reflect document link | ||||||
|  |         doc3.refresh_from_db() | ||||||
|  |         self.assertEqual( | ||||||
|  |             doc3.custom_fields.first().value, | ||||||
|  |             [self.doc2.id, self.doc1.id], | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.async_task.assert_called_once() |         self.async_task.assert_called_once() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon