From d65de00ca149814a4d149b989598a316ff630d63 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:27:33 -0700 Subject: [PATCH] Fix: correct setting ai_enabled to false via UI (#13987) --- src/documents/tests/test_api_app_config.py | 22 ++++++++++++- src/paperless/config.py | 28 ++++++++++++---- ...ter_applicationconfiguration_ai_enabled.py | 28 ++++++++++++++++ src/paperless/models.py | 1 - src/paperless/tests/test_config_precedence.py | 32 +++++++++++++++++++ src/paperless/views.py | 7 +++- 6 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 src/paperless/migrations/0016_alter_applicationconfiguration_ai_enabled.py create mode 100644 src/paperless/tests/test_config_precedence.py diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index c3fb416c2..b98c0d7da 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -76,7 +76,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): "remote_ocr_api_key": None, "remote_ocr_endpoint": None, "remote_ocr_mode": None, - "ai_enabled": False, + "ai_enabled": None, "llm_embedding_backend": None, "llm_embedding_model": None, "llm_embedding_endpoint": None, @@ -949,6 +949,26 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): ) mock_update.assert_called_once() + @override_settings(AI_ENABLED=True, LLM_EMBEDDING_BACKEND=None) + def test_external_ai_setting_triggers_index_update(self) -> None: + config = ApplicationConfiguration.objects.first() + assert config is not None + config.ai_enabled = None + config.llm_embedding_backend = None + config.save() + + with ( + patch("documents.tasks.llmindex_index.apply_async") as mock_update, + patch("paperless.views.llm_index_exists", return_value=False), + ): + self.client.patch( + f"{self.ENDPOINT}1/", + json.dumps({"llm_embedding_backend": "openai-like"}), + content_type="application/json", + ) + + mock_update.assert_called_once() + def test_update_llm_embedding_chunk_size_triggers_rebuild(self) -> None: config = ApplicationConfiguration.objects.first() assert config is not None diff --git a/src/paperless/config.py b/src/paperless/config.py index d99d9e837..aabdfabd0 100644 --- a/src/paperless/config.py +++ b/src/paperless/config.py @@ -133,21 +133,27 @@ class BarcodeConfig(BaseConfig): app_config = self._get_config_instance() self.barcodes_enabled = ( - app_config.barcodes_enabled or settings.CONSUMER_ENABLE_BARCODES + app_config.barcodes_enabled + if app_config.barcodes_enabled is not None + else settings.CONSUMER_ENABLE_BARCODES ) self.barcode_enable_tiff_support = ( app_config.barcode_enable_tiff_support - or settings.CONSUMER_BARCODE_TIFF_SUPPORT + if app_config.barcode_enable_tiff_support is not None + else settings.CONSUMER_BARCODE_TIFF_SUPPORT ) self.barcode_string = ( app_config.barcode_string or settings.CONSUMER_BARCODE_STRING ) self.barcode_retain_split_pages = ( app_config.barcode_retain_split_pages - or settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES + if app_config.barcode_retain_split_pages is not None + else settings.CONSUMER_BARCODE_RETAIN_SPLIT_PAGES ) self.barcode_enable_asn = ( - app_config.barcode_enable_asn or settings.CONSUMER_ENABLE_ASN_BARCODE + app_config.barcode_enable_asn + if app_config.barcode_enable_asn is not None + else settings.CONSUMER_ENABLE_ASN_BARCODE ) self.barcode_asn_prefix = ( app_config.barcode_asn_prefix or settings.CONSUMER_ASN_BARCODE_PREFIX @@ -160,13 +166,17 @@ class BarcodeConfig(BaseConfig): app_config.barcode_max_pages or settings.CONSUMER_BARCODE_MAX_PAGES ) self.barcode_enable_tag = ( - app_config.barcode_enable_tag or settings.CONSUMER_ENABLE_TAG_BARCODE + app_config.barcode_enable_tag + if app_config.barcode_enable_tag is not None + else settings.CONSUMER_ENABLE_TAG_BARCODE ) self.barcode_tag_mapping = ( app_config.barcode_tag_mapping or settings.CONSUMER_TAG_BARCODE_MAPPING ) self.barcode_tag_split = ( - app_config.barcode_tag_split or settings.CONSUMER_TAG_BARCODE_SPLIT + app_config.barcode_tag_split + if app_config.barcode_tag_split is not None + else settings.CONSUMER_TAG_BARCODE_SPLIT ) @@ -248,7 +258,11 @@ class AIConfig(BaseConfig): def __post_init__(self) -> None: app_config = self._get_config_instance() - self.ai_enabled = app_config.ai_enabled or settings.AI_ENABLED + self.ai_enabled = ( + app_config.ai_enabled + if app_config.ai_enabled is not None + else settings.AI_ENABLED + ) self.llm_embedding_backend = ( app_config.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND ) diff --git a/src/paperless/migrations/0016_alter_applicationconfiguration_ai_enabled.py b/src/paperless/migrations/0016_alter_applicationconfiguration_ai_enabled.py new file mode 100644 index 000000000..1b61f9692 --- /dev/null +++ b/src/paperless/migrations/0016_alter_applicationconfiguration_ai_enabled.py @@ -0,0 +1,28 @@ +from django.db import migrations +from django.db import models + + +def normalize_ai_enabled(apps, schema_editor): + application_configuration = apps.get_model( + "paperless", + "ApplicationConfiguration", + ) + application_configuration.objects.filter(ai_enabled=False).update(ai_enabled=None) + + +class Migration(migrations.Migration): + dependencies = [ + ("paperless", "0015_applicationconfiguration_remote_ocr_mode"), + ] + + operations = [ + migrations.AlterField( + model_name="applicationconfiguration", + name="ai_enabled", + field=models.BooleanField( + null=True, + verbose_name="Enables AI features", + ), + ), + migrations.RunPython(normalize_ai_enabled, migrations.RunPython.noop), + ] diff --git a/src/paperless/models.py b/src/paperless/models.py index 73eaabfee..64fc45fc0 100644 --- a/src/paperless/models.py +++ b/src/paperless/models.py @@ -348,7 +348,6 @@ class ApplicationConfiguration(AbstractSingletonModel): ai_enabled = models.BooleanField( verbose_name=_("Enables AI features"), null=True, - default=False, ) llm_embedding_backend = models.CharField( diff --git a/src/paperless/tests/test_config_precedence.py b/src/paperless/tests/test_config_precedence.py new file mode 100644 index 000000000..2cc9aecf3 --- /dev/null +++ b/src/paperless/tests/test_config_precedence.py @@ -0,0 +1,32 @@ +from django.test import TestCase +from django.test import override_settings + +from paperless.config import AIConfig +from paperless.config import BarcodeConfig +from paperless.models import ApplicationConfiguration + + +class TestBooleanConfigPrecedence(TestCase): + @override_settings(CONSUMER_ENABLE_BARCODES=True) + def test_database_false_overrides_barcode_environment_setting(self) -> None: + config, _ = ApplicationConfiguration.objects.get_or_create() + config.barcodes_enabled = False + config.save() + + self.assertFalse(BarcodeConfig().barcodes_enabled) + + @override_settings(AI_ENABLED=True) + def test_database_false_overrides_ai_environment_setting(self) -> None: + config, _ = ApplicationConfiguration.objects.get_or_create() + config.ai_enabled = False + config.save() + + self.assertFalse(AIConfig().ai_enabled) + + @override_settings(AI_ENABLED=True) + def test_null_ai_setting_uses_environment_setting(self) -> None: + config, _ = ApplicationConfiguration.objects.get_or_create() + config.ai_enabled = None + config.save() + + self.assertTrue(AIConfig().ai_enabled) diff --git a/src/paperless/views.py b/src/paperless/views.py index 2e3fb4d82..ba1e6738d 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -443,8 +443,13 @@ class ApplicationConfigurationViewSet(ModelViewSet[ApplicationConfiguration]): new_llm_embedding_backend = ( new_instance.llm_embedding_backend or settings.LLM_EMBEDDING_BACKEND ) + new_ai_enabled = ( + new_instance.ai_enabled + if new_instance.ai_enabled is not None + else settings.AI_ENABLED + ) new_ai_index_enabled = bool( - new_instance.ai_enabled and new_llm_embedding_backend, + new_ai_enabled and new_llm_embedding_backend, ) new_llm_embedding_chunk_size = ( new_instance.llm_embedding_chunk_size or settings.LLM_EMBEDDING_CHUNK_SIZE