From 7454ce5a20704c5feadd922702f167a77b82a3b6 Mon Sep 17 00:00:00 2001 From: Trenton Holmes <797416+stumpylog@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:51:23 -0700 Subject: [PATCH] Try to automatically migrate user's DB settings values --- docs/migration-v3.md | 4 + .../0008_replace_skip_archive_file.py | 76 ++++++++++++---- ...est_migration_replace_skip_archive_file.py | 89 +++++++++++++++++++ 3 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 src/paperless/tests/test_migration_replace_skip_archive_file.py diff --git a/docs/migration-v3.md b/docs/migration-v3.md index c76996cc0..b428defeb 100644 --- a/docs/migration-v3.md +++ b/docs/migration-v3.md @@ -146,6 +146,10 @@ The new settings are independent: - [`PAPERLESS_OCR_MODE`](configuration.md#PAPERLESS_OCR_MODE) controls OCR: `auto` (default), `force`, `redo`, `off`. - [`PAPERLESS_ARCHIVE_FILE_GENERATION`](configuration.md#PAPERLESS_ARCHIVE_FILE_GENERATION) controls archive production: `auto` (default), `always`, `never`. +### Database configuration + +If you changed OCR settings via the admin UI (ApplicationConfiguration), the database values are **migrated automatically** during the upgrade. `mode` values (`skip` / `skip_noarchive`) are mapped to their new equivalents and `skip_archive_file` values are converted to the new `archive_file_generation` field. After upgrading, review the OCR settings in the admin UI to confirm the migrated values match your intent. + ### Action Required Remove any `PAPERLESS_OCR_SKIP_ARCHIVE_FILE` variable from your environment. If you relied on `OCR_MODE=skip` or `OCR_MODE=skip_noarchive`, update accordingly: diff --git a/src/paperless/migrations/0008_replace_skip_archive_file.py b/src/paperless/migrations/0008_replace_skip_archive_file.py index b80512f76..b6c4fbbee 100644 --- a/src/paperless/migrations/0008_replace_skip_archive_file.py +++ b/src/paperless/migrations/0008_replace_skip_archive_file.py @@ -3,6 +3,44 @@ from django.db import migrations from django.db import models +_MODE_MAP = { + "skip": "auto", + "redo": "redo", + "force": "force", + "skip_noarchive": "auto", +} + +_ARCHIVE_MAP = { + # never skip -> always generate + "never": "always", + # skip when text present -> auto + "with_text": "auto", + # always skip -> never generate + "always": "never", +} + + +def migrate_old_values(apps, schema_editor): + ApplicationConfiguration = apps.get_model("paperless", "ApplicationConfiguration") + for config in ApplicationConfiguration.objects.all(): + old_mode = config.mode + old_skip = config.skip_archive_file + + # Map the old mode value + if old_mode in _MODE_MAP: + config.mode = _MODE_MAP[old_mode] + + # Map skip_archive_file -> archive_file_generation + if old_skip in _ARCHIVE_MAP: + config.archive_file_generation = _ARCHIVE_MAP[old_skip] + + # skip_noarchive implied no archive file; set that if the user + # didn't already have an explicit skip_archive_file preference + if old_mode == "skip_noarchive" and old_skip is None: + config.archive_file_generation = "never" + + config.save() + class Migration(migrations.Migration): dependencies = [ @@ -10,21 +48,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name="applicationconfiguration", - name="skip_archive_file", - ), - migrations.AddField( - model_name="applicationconfiguration", - name="archive_file_generation", - field=models.CharField( - blank=True, - choices=[("auto", "auto"), ("always", "always"), ("never", "never")], - max_length=8, - null=True, - verbose_name="Controls archive file generation", - ), - ), + # 1. Update mode choices in-place (old values still in the column) migrations.AlterField( model_name="applicationconfiguration", name="mode", @@ -41,4 +65,26 @@ class Migration(migrations.Migration): verbose_name="Sets the OCR mode", ), ), + # 2. Add the new field + migrations.AddField( + model_name="applicationconfiguration", + name="archive_file_generation", + field=models.CharField( + blank=True, + choices=[("auto", "auto"), ("always", "always"), ("never", "never")], + max_length=8, + null=True, + verbose_name="Controls archive file generation", + ), + ), + # 3. Migrate data from old values to new + migrations.RunPython( + migrate_old_values, + migrations.RunPython.noop, + ), + # 4. Drop the old field + migrations.RemoveField( + model_name="applicationconfiguration", + name="skip_archive_file", + ), ] diff --git a/src/paperless/tests/test_migration_replace_skip_archive_file.py b/src/paperless/tests/test_migration_replace_skip_archive_file.py new file mode 100644 index 000000000..cb31ffb35 --- /dev/null +++ b/src/paperless/tests/test_migration_replace_skip_archive_file.py @@ -0,0 +1,89 @@ +from documents.tests.utils import TestMigrations + + +class TestMigrateSkipArchiveFile(TestMigrations): + migrate_from = "0007_optimize_integer_field_sizes" + migrate_to = "0008_replace_skip_archive_file" + + def setUpBeforeMigration(self, apps): + ApplicationConfiguration = apps.get_model( + "paperless", + "ApplicationConfiguration", + ) + ApplicationConfiguration.objects.all().delete() + ApplicationConfiguration.objects.create( + pk=1, + mode="skip", + skip_archive_file="always", + ) + ApplicationConfiguration.objects.create( + pk=2, + mode="redo", + skip_archive_file="with_text", + ) + ApplicationConfiguration.objects.create( + pk=3, + mode="force", + skip_archive_file="never", + ) + ApplicationConfiguration.objects.create( + pk=4, + mode="skip_noarchive", + skip_archive_file=None, + ) + ApplicationConfiguration.objects.create( + pk=5, + mode="skip_noarchive", + skip_archive_file="never", + ) + ApplicationConfiguration.objects.create(pk=6, mode=None, skip_archive_file=None) + + def _get_config(self, pk): + ApplicationConfiguration = self.apps.get_model( + "paperless", + "ApplicationConfiguration", + ) + return ApplicationConfiguration.objects.get(pk=pk) + + def test_skip_mapped_to_auto(self): + config = self._get_config(1) + assert config.mode == "auto" + + def test_skip_archive_always_mapped_to_never(self): + config = self._get_config(1) + assert config.archive_file_generation == "never" + + def test_redo_unchanged(self): + config = self._get_config(2) + assert config.mode == "redo" + + def test_skip_archive_with_text_mapped_to_auto(self): + config = self._get_config(2) + assert config.archive_file_generation == "auto" + + def test_force_unchanged(self): + config = self._get_config(3) + assert config.mode == "force" + + def test_skip_archive_never_mapped_to_always(self): + config = self._get_config(3) + assert config.archive_file_generation == "always" + + def test_skip_noarchive_mapped_to_auto(self): + config = self._get_config(4) + assert config.mode == "auto" + + def test_skip_noarchive_implies_archive_never(self): + config = self._get_config(4) + assert config.archive_file_generation == "never" + + def test_skip_noarchive_explicit_skip_archive_takes_precedence(self): + """skip_archive_file=never maps to always, not overridden by skip_noarchive.""" + config = self._get_config(5) + assert config.mode == "auto" + assert config.archive_file_generation == "always" + + def test_null_values_remain_null(self): + config = self._get_config(6) + assert config.mode is None + assert config.archive_file_generation is None