From 7e3b528bdae0d49c4958261c8da565daec0d5c52 Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 13 Oct 2026 16:03:41 -0400 Subject: [PATCH 1/4] closes OpenConceptLab/ocl_issues#2911 | Load base_entities.yaml only on a new database, so deploys stop resetting the OCL org pre_startup.sh ran `loaddata core/fixtures/*` on every container start. loaddata overwrites an existing row in full, so each deploy put the OCL org (pk 1) back to its 2020 seed values and wiped its text, description, logo and overview. The new `load_fixtures` command loads every file in core/fixtures as before, but skips base_entities.yaml once the OCL org exists. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018wMfkJ379WBjtcv8ZiWpT4 --- .../management/commands/load_fixtures.py | 22 +++++++++++ core/common/tests_load_fixtures.py | 39 +++++++++++++++++++ core/fixtures/auth_groups.yaml | 4 +- pre_startup.sh | 2 +- 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 core/common/management/commands/load_fixtures.py create mode 100644 core/common/tests_load_fixtures.py diff --git a/core/common/management/commands/load_fixtures.py b/core/common/management/commands/load_fixtures.py new file mode 100644 index 000000000..63beaf4b0 --- /dev/null +++ b/core/common/management/commands/load_fixtures.py @@ -0,0 +1,22 @@ +import os +from glob import glob + +from django.conf import settings +from django.core.management import BaseCommand, call_command + +from core.orgs.models import Organization + +FIXTURES_DIR = os.path.join(settings.BASE_DIR, 'core', 'fixtures') +BASE_ENTITIES = os.path.join(FIXTURES_DIR, 'base_entities.yaml') + + +class Command(BaseCommand): + help = 'load core/fixtures on container start; base_entities.yaml only when the OCL org does not exist yet' + + def handle(self, *args, **options): + fixtures = sorted(glob(os.path.join(FIXTURES_DIR, '*'))) + # loaddata overwrites an existing row in full, which reset the OCL org's text, logo and overview + # to their defaults on every deploy (OpenConceptLab/ocl_issues#2911). Seed it on a new database only. + if Organization.objects.filter(id=1).exists(): + fixtures.remove(BASE_ENTITIES) + call_command('loaddata', *fixtures, verbosity=options['verbosity']) diff --git a/core/common/tests_load_fixtures.py b/core/common/tests_load_fixtures.py new file mode 100644 index 000000000..8d9800a4b --- /dev/null +++ b/core/common/tests_load_fixtures.py @@ -0,0 +1,39 @@ +from django.core.management import call_command + +from core.capabilities.models import Capability +from core.common.tests import OCLTestCase +from core.orgs.models import Organization + + +class LoadFixturesCommandTest(OCLTestCase): + def test_keeps_an_existing_ocl_org(self): + org = Organization.objects.get(id=1) + org.name = 'Open Concept Lab' + org.text = '

About OCL

' + org.description = 'Shared reference content' + org.overview = {'background': {'image': 'https://example.org/banner.png'}} + org.save() + + call_command('load_fixtures', verbosity=0) + + org.refresh_from_db() + self.assertEqual(org.name, 'Open Concept Lab') + self.assertEqual(org.text, '

About OCL

') + self.assertEqual(org.description, 'Shared reference content') + self.assertEqual(org.overview, {'background': {'image': 'https://example.org/banner.png'}}) + + def test_reloads_the_other_fixtures(self): + Capability.objects.filter(id=1).update(description='changed') + + call_command('load_fixtures', verbosity=0) + + self.assertEqual(Capability.objects.get(id=1).description, 'Number of map projects a user may create') + + def test_seeds_the_ocl_org_on_a_new_database(self): + Organization.objects.filter(id=1).delete() + + call_command('load_fixtures', verbosity=0) + + org = Organization.objects.get(id=1) + self.assertEqual(org.mnemonic, 'OCL') + self.assertEqual(org.name, 'OCL') diff --git a/core/fixtures/auth_groups.yaml b/core/fixtures/auth_groups.yaml index 2c4ab0bfb..7b9c0a3d7 100644 --- a/core/fixtures/auth_groups.yaml +++ b/core/fixtures/auth_groups.yaml @@ -1,7 +1,7 @@ # The Mapper-gating and authoring Django permissions (ocl_issues#2780/#2782/#2766, ocl_online#230). Plan groups granting # them are seeded by `manage.py seed_group_capabilities` from deployment config. -# pk is pinned so re-running `loaddata core/fixtures/*` on every container start -# (pre_startup.sh) updates these same rows instead of inserting duplicates on the +# pk is pinned so reloading the fixtures on every container start (`manage.py +# load_fixtures` in pre_startup.sh) updates these same rows instead of inserting duplicates on the # unique (content_type, codename) constraint - same reasoning as the group pks below, # but starting well above them: auth_permission already had 252 rows (max id 252, # confirmed against the dev database) and keeps growing as new models are added, unlike diff --git a/pre_startup.sh b/pre_startup.sh index 8ce4e112c..00e2d3457 100755 --- a/pre_startup.sh +++ b/pre_startup.sh @@ -25,7 +25,7 @@ export NO_ENCODER=TRUE echo "Running DB migrations" python manage.py migrate echo "Importing base entities" -python manage.py loaddata core/fixtures/* +python manage.py load_fixtures echo "Seeding default group capability limits" python manage.py seed_group_capabilities From ea7de7ac2baba256daf5f7b60989c5da2005ac88 Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 13 Oct 2026 16:08:48 -0400 Subject: [PATCH 2/4] OpenConceptLab/ocl_issues#2911 | setup_superuser restores ocladmin's superuser and staff flags itself The base_entities.yaml reload used to reset them on every start. Without it, a demoted ocladmin made setup_superuser's lookup (is_superuser=True) fail and stop the container from starting (Codex review pass 1, F1.1). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018wMfkJ379WBjtcv8ZiWpT4 --- core/common/management/commands/setup_superuser.py | 7 +++++-- core/common/tests_load_fixtures.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/common/management/commands/setup_superuser.py b/core/common/management/commands/setup_superuser.py index 95b1e5af2..aca73a5f6 100644 --- a/core/common/management/commands/setup_superuser.py +++ b/core/common/management/commands/setup_superuser.py @@ -10,10 +10,13 @@ class Command(BaseCommand): help = 'setup superuser' def handle(self, *args, **options): - user = UserProfile.objects.get(username='ocladmin', is_superuser=True) + # base_entities.yaml no longer reloads on existing databases (OpenConceptLab/ocl_issues#2911), so set the + # flags it used to restore here, or a demoted ocladmin would stop the container from starting + user = UserProfile.objects.get(username='ocladmin') + user.is_superuser = True + user.is_staff = True user.groups.add(*Group.objects.filter(name__in=[SUPERADMIN_GROUP, STAFF_GROUP])) user.set_password(settings.API_SUPERUSER_PASSWORD) user.save() user.set_token(settings.API_SUPERUSER_TOKEN) user.save() - diff --git a/core/common/tests_load_fixtures.py b/core/common/tests_load_fixtures.py index 8d9800a4b..53795fb78 100644 --- a/core/common/tests_load_fixtures.py +++ b/core/common/tests_load_fixtures.py @@ -3,6 +3,7 @@ from core.capabilities.models import Capability from core.common.tests import OCLTestCase from core.orgs.models import Organization +from core.users.models import UserProfile class LoadFixturesCommandTest(OCLTestCase): @@ -29,6 +30,16 @@ def test_reloads_the_other_fixtures(self): self.assertEqual(Capability.objects.get(id=1).description, 'Number of map projects a user may create') + def test_startup_restores_a_demoted_ocladmin(self): + UserProfile.objects.filter(username='ocladmin').update(is_superuser=False, is_staff=False) + + call_command('load_fixtures', verbosity=0) + call_command('setup_superuser') + + ocladmin = UserProfile.objects.get(username='ocladmin') + self.assertTrue(ocladmin.is_superuser) + self.assertTrue(ocladmin.is_staff) + def test_seeds_the_ocl_org_on_a_new_database(self): Organization.objects.filter(id=1).delete() From 72473eeb88438bac122878f9996e827853841dd8 Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 13 Oct 2026 16:13:56 -0400 Subject: [PATCH 3/4] OpenConceptLab/ocl_issues#2911 | Seed only the OCL org on a new database; keep reloading ocladmin as before Moves the OCL org from base_entities.yaml to ocl_org.yaml, which load_fixtures skips once the org exists. base_entities.yaml (ocladmin) reloads on every start as before, so a demoted or deactivated ocladmin is restored as it always was, and setup_superuser goes back to master (Codex review pass 2, F2.1, and pass 1, F1.1). The fresh-database test now deletes both the org and ocladmin (F2.2). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018wMfkJ379WBjtcv8ZiWpT4 --- core/common/management/commands/load_fixtures.py | 6 +++--- .../common/management/commands/setup_superuser.py | 7 ++----- core/common/tests.py | 6 +++--- core/common/tests_load_fixtures.py | 15 +++++++++------ core/fixtures/base_entities.yaml | 11 ----------- core/fixtures/ocl_org.yaml | 13 +++++++++++++ 6 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 core/fixtures/ocl_org.yaml diff --git a/core/common/management/commands/load_fixtures.py b/core/common/management/commands/load_fixtures.py index 63beaf4b0..2a4f266c2 100644 --- a/core/common/management/commands/load_fixtures.py +++ b/core/common/management/commands/load_fixtures.py @@ -7,16 +7,16 @@ from core.orgs.models import Organization FIXTURES_DIR = os.path.join(settings.BASE_DIR, 'core', 'fixtures') -BASE_ENTITIES = os.path.join(FIXTURES_DIR, 'base_entities.yaml') +OCL_ORG = os.path.join(FIXTURES_DIR, 'ocl_org.yaml') class Command(BaseCommand): - help = 'load core/fixtures on container start; base_entities.yaml only when the OCL org does not exist yet' + help = 'load core/fixtures on container start; ocl_org.yaml only when the OCL org does not exist yet' def handle(self, *args, **options): fixtures = sorted(glob(os.path.join(FIXTURES_DIR, '*'))) # loaddata overwrites an existing row in full, which reset the OCL org's text, logo and overview # to their defaults on every deploy (OpenConceptLab/ocl_issues#2911). Seed it on a new database only. if Organization.objects.filter(id=1).exists(): - fixtures.remove(BASE_ENTITIES) + fixtures.remove(OCL_ORG) call_command('loaddata', *fixtures, verbosity=options['verbosity']) diff --git a/core/common/management/commands/setup_superuser.py b/core/common/management/commands/setup_superuser.py index aca73a5f6..95b1e5af2 100644 --- a/core/common/management/commands/setup_superuser.py +++ b/core/common/management/commands/setup_superuser.py @@ -10,13 +10,10 @@ class Command(BaseCommand): help = 'setup superuser' def handle(self, *args, **options): - # base_entities.yaml no longer reloads on existing databases (OpenConceptLab/ocl_issues#2911), so set the - # flags it used to restore here, or a demoted ocladmin would stop the container from starting - user = UserProfile.objects.get(username='ocladmin') - user.is_superuser = True - user.is_staff = True + user = UserProfile.objects.get(username='ocladmin', is_superuser=True) user.groups.add(*Group.objects.filter(name__in=[SUPERADMIN_GROUP, STAFF_GROUP])) user.set_password(settings.API_SUPERUSER_PASSWORD) user.save() user.set_token(settings.API_SUPERUSER_TOKEN) user.save() + diff --git a/core/common/tests.py b/core/common/tests.py index 4194bf67c..015e44435 100644 --- a/core/common/tests.py +++ b/core/common/tests.py @@ -276,7 +276,7 @@ class OCLAPITransactionTestCase(APITransactionTestCase, BaseTestCase): @classmethod def setUpClass(cls): super().setUpClass() - call_command("loaddata", "core/fixtures/base_entities.yaml") + call_command("loaddata", "core/fixtures/base_entities.yaml", "core/fixtures/ocl_org.yaml") call_command("loaddata", "core/fixtures/auth_groups.yaml") call_command("loaddata", "core/fixtures/capabilities.yaml") call_command("loaddata", "core/fixtures/toggles.json") @@ -289,7 +289,7 @@ class OCLAPITestCase(APITestCase, BaseTestCase): @classmethod def setUpClass(cls): super().setUpClass() - call_command("loaddata", "core/fixtures/base_entities.yaml") + call_command("loaddata", "core/fixtures/base_entities.yaml", "core/fixtures/ocl_org.yaml") call_command("loaddata", "core/fixtures/auth_groups.yaml") call_command("loaddata", "core/fixtures/capabilities.yaml") call_command("loaddata", "core/fixtures/toggles.json") @@ -306,7 +306,7 @@ class OCLTestCase(TestCase, BaseTestCase): @classmethod def setUpClass(cls): super().setUpClass() - call_command("loaddata", "core/fixtures/base_entities.yaml") + call_command("loaddata", "core/fixtures/base_entities.yaml", "core/fixtures/ocl_org.yaml") call_command("loaddata", "core/fixtures/auth_groups.yaml") call_command("loaddata", "core/fixtures/capabilities.yaml") call_command("loaddata", "core/fixtures/toggles.json") diff --git a/core/common/tests_load_fixtures.py b/core/common/tests_load_fixtures.py index 53795fb78..69e95d2f6 100644 --- a/core/common/tests_load_fixtures.py +++ b/core/common/tests_load_fixtures.py @@ -30,8 +30,9 @@ def test_reloads_the_other_fixtures(self): self.assertEqual(Capability.objects.get(id=1).description, 'Number of map projects a user may create') - def test_startup_restores_a_demoted_ocladmin(self): - UserProfile.objects.filter(username='ocladmin').update(is_superuser=False, is_staff=False) + def test_startup_restores_a_demoted_or_deactivated_ocladmin(self): + UserProfile.objects.filter(username='ocladmin').update( + is_superuser=False, is_staff=False, is_active=False, verified=False) call_command('load_fixtures', verbosity=0) call_command('setup_superuser') @@ -39,12 +40,14 @@ def test_startup_restores_a_demoted_ocladmin(self): ocladmin = UserProfile.objects.get(username='ocladmin') self.assertTrue(ocladmin.is_superuser) self.assertTrue(ocladmin.is_staff) + self.assertTrue(ocladmin.is_active) + self.assertTrue(ocladmin.verified) - def test_seeds_the_ocl_org_on_a_new_database(self): + def test_seeds_the_ocl_org_and_ocladmin_on_a_new_database(self): Organization.objects.filter(id=1).delete() + UserProfile.objects.filter(id=1).delete() call_command('load_fixtures', verbosity=0) - org = Organization.objects.get(id=1) - self.assertEqual(org.mnemonic, 'OCL') - self.assertEqual(org.name, 'OCL') + self.assertEqual(Organization.objects.get(id=1).mnemonic, 'OCL') + self.assertTrue(UserProfile.objects.get(id=1, username='ocladmin').is_superuser) diff --git a/core/fixtures/base_entities.yaml b/core/fixtures/base_entities.yaml index 52ce2dffe..5cdbc0b9a 100644 --- a/core/fixtures/base_entities.yaml +++ b/core/fixtures/base_entities.yaml @@ -1,14 +1,3 @@ -- model: "orgs.organization" - pk: 1 - fields: - name: OCL - company: OCL - mnemonic: OCL - website: "http://openconceptlab.org" - created_at: "2020-06-08T08:00:00+00:00" - updated_at: "2020-06-08T08:00:00+00:00" - created_by: 1 - updated_by: 1 - model: "users.userprofile" pk: 1 fields: diff --git a/core/fixtures/ocl_org.yaml b/core/fixtures/ocl_org.yaml new file mode 100644 index 000000000..b931d582c --- /dev/null +++ b/core/fixtures/ocl_org.yaml @@ -0,0 +1,13 @@ +# Loaded on a new database only (`manage.py load_fixtures`): reloading it would overwrite the OCL org's +# text, description, logo and overview on every container start (OpenConceptLab/ocl_issues#2911). +- model: "orgs.organization" + pk: 1 + fields: + name: OCL + company: OCL + mnemonic: OCL + website: "http://openconceptlab.org" + created_at: "2020-06-08T08:00:00+00:00" + updated_at: "2020-06-08T08:00:00+00:00" + created_by: 1 + updated_by: 1 From 229a7a5fe8d0ab5e2f0da7cc5d8b94572695559d Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 13 Oct 2026 16:28:01 -0400 Subject: [PATCH 4/4] OpenConceptLab/ocl_issues#2911 | Simplification review: shorter keep-org test, drop a repeated comment Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018wMfkJ379WBjtcv8ZiWpT4 --- .../common/management/commands/load_fixtures.py | 2 -- core/common/tests_load_fixtures.py | 17 ++++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/core/common/management/commands/load_fixtures.py b/core/common/management/commands/load_fixtures.py index 2a4f266c2..082a122f7 100644 --- a/core/common/management/commands/load_fixtures.py +++ b/core/common/management/commands/load_fixtures.py @@ -15,8 +15,6 @@ class Command(BaseCommand): def handle(self, *args, **options): fixtures = sorted(glob(os.path.join(FIXTURES_DIR, '*'))) - # loaddata overwrites an existing row in full, which reset the OCL org's text, logo and overview - # to their defaults on every deploy (OpenConceptLab/ocl_issues#2911). Seed it on a new database only. if Organization.objects.filter(id=1).exists(): fixtures.remove(OCL_ORG) call_command('loaddata', *fixtures, verbosity=options['verbosity']) diff --git a/core/common/tests_load_fixtures.py b/core/common/tests_load_fixtures.py index 69e95d2f6..be26e0ca0 100644 --- a/core/common/tests_load_fixtures.py +++ b/core/common/tests_load_fixtures.py @@ -8,20 +8,15 @@ class LoadFixturesCommandTest(OCLTestCase): def test_keeps_an_existing_ocl_org(self): - org = Organization.objects.get(id=1) - org.name = 'Open Concept Lab' - org.text = '

About OCL

' - org.description = 'Shared reference content' - org.overview = {'background': {'image': 'https://example.org/banner.png'}} - org.save() + edits = { + 'name': 'Open Concept Lab', 'text': '

About OCL

', 'description': 'Shared reference content', + 'overview': {'background': {'image': 'https://example.org/banner.png'}}, + } + Organization.objects.filter(id=1).update(**edits) call_command('load_fixtures', verbosity=0) - org.refresh_from_db() - self.assertEqual(org.name, 'Open Concept Lab') - self.assertEqual(org.text, '

About OCL

') - self.assertEqual(org.description, 'Shared reference content') - self.assertEqual(org.overview, {'background': {'image': 'https://example.org/banner.png'}}) + self.assertEqual(Organization.objects.filter(id=1).values(*edits).get(), edits) def test_reloads_the_other_fixtures(self): Capability.objects.filter(id=1).update(description='changed')