diff --git a/src/documents/management/commands/document_importer.py b/src/documents/management/commands/document_importer.py index 22c626eba..f56159c81 100644 --- a/src/documents/management/commands/document_importer.py +++ b/src/documents/management/commands/document_importer.py @@ -34,7 +34,7 @@ from documents.settings import EXPORTER_ARCHIVE_NAME from documents.settings import EXPORTER_CRYPTO_SETTINGS_NAME from documents.settings import EXPORTER_FILE_NAME from documents.settings import EXPORTER_THUMBNAIL_NAME -from documents.signals.handlers import update_cf_instance_documents +from documents.signals.handlers import check_paths_and_prune_custom_fields from documents.signals.handlers import update_filename_and_move_files from documents.utils import copy_file_with_basic_stats from paperless import version @@ -262,7 +262,7 @@ class Command(CryptMixin, BaseCommand): ), disable_signal( post_save, - receiver=update_cf_instance_documents, + receiver=check_paths_and_prune_custom_fields, sender=CustomField, ), ): diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 73aee2936..381108123 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -367,21 +367,6 @@ class CannotMoveFilesException(Exception): pass -# should be disabled in /src/documents/management/commands/document_importer.py handle -@receiver(models.signals.post_save, sender=CustomField) -def update_cf_instance_documents(sender, instance: CustomField, **kwargs): - """ - 'Select' custom field instances get their end-user value (e.g. in file names) from the select_options in extra_data, - which is contained in the custom field itself. So when the field is changed, we (may) need to update the file names - of all documents that have this custom field. - """ - if ( - instance.data_type == CustomField.FieldDataType.SELECT - ): # Only select fields, for now - for cf_instance in instance.fields.all(): - update_filename_and_move_files(sender, cf_instance) - - # should be disabled in /src/documents/management/commands/document_importer.py handle @receiver(models.signals.post_save, sender=CustomFieldInstance) @receiver(models.signals.m2m_changed, sender=Document.tags.through) @@ -520,6 +505,34 @@ def update_filename_and_move_files( ) +# should be disabled in /src/documents/management/commands/document_importer.py handle +@receiver(models.signals.post_save, sender=CustomField) +def check_paths_and_prune_custom_fields(sender, instance: CustomField, **kwargs): + """ + When a custom field is updated: + 1. 'Select' custom field instances get their end-user value (e.g. in file names) from the select_options in extra_data, + which is contained in the custom field itself. So when the field is changed, we (may) need to update the file names + of all documents that have this custom field. + 2. If a 'Select' field option was removed, we need to nullify the custom field instances that have the option. + """ + if ( + instance.data_type == CustomField.FieldDataType.SELECT + ): # Only select fields, for now + for cf_instance in instance.fields.all(): + options = instance.extra_data.get("select_options", []) + try: + next( + option["label"] + for option in options + if option["id"] == cf_instance.value + ) + except StopIteration: + # The value of this custom field instance is not in the select options anymore + cf_instance.value_select = None + cf_instance.save() + update_filename_and_move_files(sender, cf_instance) + + def set_log_entry(sender, document: Document, logging_group=None, **kwargs): ct = ContentType.objects.get(model="document") user = User.objects.get(username="consumer") diff --git a/src/documents/tests/test_api_custom_fields.py b/src/documents/tests/test_api_custom_fields.py index db9b87306..11911f6ab 100644 --- a/src/documents/tests/test_api_custom_fields.py +++ b/src/documents/tests/test_api_custom_fields.py @@ -209,6 +209,69 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase): ], ) + def test_custom_field_select_options_pruned(self): + """ + GIVEN: + - Select custom field exists and document instance with one of the options + WHEN: + - API request to remove an option from the select field + THEN: + - The option is removed from the field + - The option is removed from the document instance + """ + custom_field_select = CustomField.objects.create( + name="Select Field", + data_type=CustomField.FieldDataType.SELECT, + extra_data={ + "select_options": [ + {"label": "Option 1", "id": "abc-123"}, + {"label": "Option 2", "id": "def-456"}, + {"label": "Option 3", "id": "ghi-789"}, + ], + }, + ) + + doc = Document.objects.create( + title="WOW", + content="the content", + checksum="123", + mime_type="application/pdf", + ) + CustomFieldInstance.objects.create( + document=doc, + field=custom_field_select, + value_text="abc-123", + ) + + resp = self.client.patch( + f"{self.ENDPOINT}{custom_field_select.id}/", + json.dumps( + { + "extra_data": { + "select_options": [ + {"label": "Option 1", "id": "abc-123"}, + {"label": "Option 3", "id": "ghi-789"}, + ], + }, + }, + ), + content_type="application/json", + ) + self.assertEqual(resp.status_code, status.HTTP_200_OK) + + data = resp.json() + + self.assertCountEqual( + data["extra_data"]["select_options"], + [ + {"label": "Option 1", "id": "abc-123"}, + {"label": "Option 3", "id": "ghi-789"}, + ], + ) + + doc.refresh_from_db() + self.assertEqual(doc.custom_fields.first().value, None) + def test_create_custom_field_monetary_validation(self): """ GIVEN: