+
{{option.title}}
+ @if (isExternallyConfigured(option.config_key)) {
+ @if (isSet(option.key)) {
+
Overrides external
+ } @else {
+
Set externally
+ }
+ }
@if (isSet(option.key)) {
-
+ @if (isExternallyConfigured(option.config_key)) {
+
+ } @else {
+
+ }
}
diff --git a/src-ui/src/app/components/admin/config/config.component.spec.ts b/src-ui/src/app/components/admin/config/config.component.spec.ts
index a99171f93..650761c7d 100644
--- a/src-ui/src/app/components/admin/config/config.component.spec.ts
+++ b/src-ui/src/app/components/admin/config/config.component.spec.ts
@@ -163,6 +163,19 @@ describe('ConfigComponent', () => {
expect(component.configForm.get('barcodes_enabled').value).toBeNull()
})
+ it('should identify externally configured options', () => {
+ component.externallyConfiguredVariables = new Set([
+ 'PAPERLESS_OCR_LANGUAGE',
+ ])
+
+ expect(
+ component.isExternallyConfigured('PAPERLESS_OCR_LANGUAGE')
+ ).toBeTruthy()
+ expect(
+ component.isExternallyConfigured('PAPERLESS_OCR_OUTPUT_TYPE')
+ ).toBeFalsy()
+ })
+
it('should group options into sections within a category, or not', () => {
const sections = component.getCategorySections(ConfigCategory.OCR)
expect(sections).toEqual([null, ConfigSection.RemoteOCR])
diff --git a/src-ui/src/app/components/admin/config/config.component.ts b/src-ui/src/app/components/admin/config/config.component.ts
index 0e3f432e2..570eb7d55 100644
--- a/src-ui/src/app/components/admin/config/config.component.ts
+++ b/src-ui/src/app/components/admin/config/config.component.ts
@@ -69,6 +69,7 @@ export class ConfigComponent
public configForm = new FormGroup({})
public errors = {}
+ public externallyConfiguredVariables = new Set()
get optionCategories(): string[] {
return Object.values(ConfigCategory)
@@ -152,6 +153,9 @@ export class ConfigComponent
}
private initialize(config: PaperlessConfig) {
+ this.externallyConfiguredVariables = new Set(
+ config.externally_configured_variables ?? []
+ )
if (!this.store) {
this.store = new BehaviorSubject(config)
@@ -162,7 +166,9 @@ export class ConfigComponent
this.configForm.patchValue(state, { emitEvent: false })
})
- this.isDirty$ = dirtyCheck(this.configForm, this.store.asObservable())
+ this.isDirty$ = dirtyCheck(this.configForm, this.store.asObservable(), {
+ excludeKeys: ['externally_configured_variables'],
+ })
}
this.configForm.patchValue(config)
@@ -227,6 +233,10 @@ export class ConfigComponent
return this.configForm.get(key).value != null
}
+ public isExternallyConfigured(configKey: string): boolean {
+ return this.externallyConfiguredVariables.has(configKey)
+ }
+
public resetOption(key: string) {
this.configForm.get(key).setValue(null)
}
diff --git a/src-ui/src/app/data/paperless-config.ts b/src-ui/src/app/data/paperless-config.ts
index 92af2cec5..690d7aa4e 100644
--- a/src-ui/src/app/data/paperless-config.ts
+++ b/src-ui/src/app/data/paperless-config.ts
@@ -422,6 +422,7 @@ export const PaperlessConfigOptions: ConfigOption[] = [
]
export interface PaperlessConfig extends ObjectWithId {
+ externally_configured_variables: string[]
output_type: OutputTypeConfig
pages: number
language: string
diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py
index b98c0d7da..0ca6f4412 100644
--- a/src/documents/tests/test_api_app_config.py
+++ b/src/documents/tests/test_api_app_config.py
@@ -35,7 +35,8 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
THEN:
- Existing config
"""
- response = self.client.get(self.ENDPOINT, format="json")
+ with patch.dict("os.environ", {}, clear=True):
+ response = self.client.get(self.ENDPOINT, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -45,6 +46,7 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
response.data[0],
{
"id": 1,
+ "externally_configured_variables": [],
"output_type": None,
"pages": None,
"language": None,
@@ -91,6 +93,31 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
},
)
+ def test_api_get_config_reports_external_configuration_without_values(self) -> None:
+ with patch.dict(
+ "os.environ",
+ {
+ "PAPERLESS_OCR_LANGUAGE": "eng",
+ "PAPERLESS_REMOTE_OCR_API_KEY": "secret-value",
+ "PAPERLESS_FUTURE_SETTING": "future-value",
+ "UNRELATED_SETTING": "unrelated-value",
+ },
+ clear=True,
+ ):
+ response = self.client.get(self.ENDPOINT, format="json")
+
+ self.assertCountEqual(
+ response.data[0]["externally_configured_variables"],
+ [
+ "PAPERLESS_FUTURE_SETTING",
+ "PAPERLESS_OCR_LANGUAGE",
+ "PAPERLESS_REMOTE_OCR_API_KEY",
+ ],
+ )
+ self.assertNotContains(response, "secret-value")
+ self.assertNotContains(response, "future-value")
+ self.assertNotContains(response, "UNRELATED_SETTING")
+
def test_api_get_ui_settings_with_config(self) -> None:
"""
GIVEN:
diff --git a/src/paperless/serialisers.py b/src/paperless/serialisers.py
index 38baf1be5..f7cfc3d45 100644
--- a/src/paperless/serialisers.py
+++ b/src/paperless/serialisers.py
@@ -1,4 +1,5 @@
import logging
+import os
from io import BytesIO
import magic
@@ -212,6 +213,7 @@ class ProfileSerializer(PasswordValidationMixin, serializers.ModelSerializer[Use
class ApplicationConfigurationSerializer(
serializers.ModelSerializer[ApplicationConfiguration],
):
+ externally_configured_variables = serializers.SerializerMethodField()
user_args = serializers.JSONField(binary=True, allow_null=True)
barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True)
llm_api_key = ObfuscatedPasswordField(
@@ -227,6 +229,12 @@ class ApplicationConfigurationSerializer(
OBFUSCATED_FIELDS = ("llm_api_key", "remote_ocr_api_key")
+ def get_externally_configured_variables(
+ self,
+ instance: ApplicationConfiguration,
+ ) -> list[str]:
+ return sorted(name for name in os.environ if name.startswith("PAPERLESS_"))
+
def run_validation(self, data):
# Empty strings treated as None to avoid unexpected behavior
if "user_args" in data and data["user_args"] == "":