From ea1f814e9aff5eeb459c02d5e6050d276c490ce3 Mon Sep 17 00:00:00 2001 From: "Project parkour-dev @ molgen" Date: Tue, 6 Oct 2026 16:24:35 +0200 Subject: [PATCH 1/3] Add regression tests for orphaned LibraryPreparation/Pooling admin 500 LibraryPreparation.sample and Pooling.library/sample are on_delete=SET_NULL; deleting a sample leaves rows whose __str__ and admin columns dereference None, so /admin/library_preparation/ librarypreparation/ returned HTTP 500 on parkour-dev (2026-10-06, samples deleted in the admin on 2026-09-15). Pooling has the same bug, not seen live yet. All 7 tests fail on 26.10.05 and pass with maxplanck-ie/parkour2#370. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VCWKtNqYoCiW18CWkWCfBi --- CLAUDE.md | 2 +- .../test_libraryprep_orphan.py | 46 ++++++++++++++++++ tests/parkour2/pooling/test_pooling_orphan.py | 48 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/parkour2/library_preparation/test_libraryprep_orphan.py create mode 100644 tests/parkour2/pooling/test_pooling_orphan.py diff --git a/CLAUDE.md b/CLAUDE.md index 5d5011e..04b2bd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -198,7 +198,7 @@ Reported upstream (draft: `/project/parkour-dev/git/issue-migration-history-rewr ### Regression tests against the built checkout -`./test-parkour2.sh [manage.py test args]` runs `tests/parkour2//test_*.py` against `${PROJECT}/${BUILD_parkour2}`. It copies each file into its app for the run and removes it on exit, so `git pull` stays clean. It needs that tree's Postgres running, and Django creates/drops its own `test_parkour2` DB. Current content: the orphaned-IndexPair admin 500, fixed upstream by maxplanck-ie/parkour2#370 (open as of 2026-10-05). Once that's in the pinned release, the test should pass. +`./test-parkour2.sh [manage.py test args]` runs `tests/parkour2//test_*.py` against `${PROJECT}/${BUILD_parkour2}`. It copies each file into its app for the run and removes it on exit, so `git pull` stays clean. It needs that tree's Postgres running, and Django creates/drops its own `test_parkour2` DB. Current content: admin HTTP 500s caused by rows whose `on_delete=SET_NULL` target was deleted, for IndexPair (no IndexType; seen 2026-10-02), LibraryPreparation (no Sample; seen 2026-10-06) and Pooling (no Library/Sample; not seen yet). All three are fixed upstream by maxplanck-ie/parkour2#370 (open as of 2026-10-06). Once that's in the pinned release, all tests should pass. Until then, a live 500 of this kind is fixed by deleting the orphaned rows (back them up with `dumpdata` first). To find such pages before users do, render every admin changelist in-process as a superuser (`admin.site._registry`, `RequestFactory`, `changelist_view`). ## Source download strategy diff --git a/tests/parkour2/library_preparation/test_libraryprep_orphan.py b/tests/parkour2/library_preparation/test_libraryprep_orphan.py new file mode 100644 index 0000000..386a38a --- /dev/null +++ b/tests/parkour2/library_preparation/test_libraryprep_orphan.py @@ -0,0 +1,46 @@ +"""Regression test: LibraryPreparation whose Sample was deleted. + +LibraryPreparation.sample is OneToOneField(on_delete=SET_NULL), so deleting +a sample leaves its LibraryPreparation row behind with sample=None. +LibraryPreparation.__str__ and the admin list columns then dereference +self.sample.name, which makes /admin/library_preparation/librarypreparation/ +return HTTP 500 for everyone (seen on parkour-dev 2026-10-06; the samples +had been deleted in the admin on 2026-09-15). + +Fixed upstream in maxplanck-ie/parkour2#370. +""" + +from common.tests import BaseTestCase +from django.urls import reverse + +from .models import LibraryPreparation +from .tests import create_library_preparation_obj + + +class OrphanedLibraryPreparationTest(BaseTestCase): + def setUp(self): + user = self.create_user() + library_prep_obj = create_library_preparation_obj( + self._get_random_name(), user, 2 + ) + library_prep_obj.sample.delete() + self.library_prep_obj = LibraryPreparation.objects.filter( + pk=library_prep_obj.pk + ).first() + + def test_str_without_sample(self): + if self.library_prep_obj is None: + self.skipTest("row was cascade-deleted with its sample") + self.assertIsNone(self.library_prep_obj.sample) + self.assertEqual(str(self.library_prep_obj), "") + + def test_admin_changelist_with_orphaned_library_preparation(self): + admin_user = self.create_user(email="admin@test.io") + admin_user.is_superuser = True + admin_user.save() + self.client.force_login(admin_user) + + response = self.client.get( + reverse("admin:library_preparation_librarypreparation_changelist") + ) + self.assertEqual(response.status_code, 200) diff --git a/tests/parkour2/pooling/test_pooling_orphan.py b/tests/parkour2/pooling/test_pooling_orphan.py new file mode 100644 index 0000000..b155613 --- /dev/null +++ b/tests/parkour2/pooling/test_pooling_orphan.py @@ -0,0 +1,48 @@ +"""Regression test: Pooling whose Library/Sample was deleted. + +Pooling.library and Pooling.sample are OneToOneField(on_delete=SET_NULL), +so deleting the library or sample leaves the Pooling row behind with +neither set. Pooling.__str__ and the admin list columns then dereference +None, and /admin/pooling/pooling/ would return HTTP 500. Not seen live yet; +same bug as LibraryPreparation (see test_libraryprep_orphan.py). + +Fixed upstream in maxplanck-ie/parkour2#370. +""" + +from common.tests import BaseTestCase +from django.urls import reverse + +from .models import Pooling +from .tests import create_pooling_object + + +class OrphanedPoolingTest(BaseTestCase): + def setUp(self): + self.user = self.create_user() + + def _orphan(self, **kwargs): + pooling_object = create_pooling_object(self.user, **kwargs) + (pooling_object.library or pooling_object.sample).delete() + return Pooling.objects.filter(pk=pooling_object.pk).first() + + def _assert_str_empty(self, pooling_object): + if pooling_object is None: + self.skipTest("row was cascade-deleted with its library/sample") + self.assertEqual(str(pooling_object), "") + + def test_str_without_library(self): + self._assert_str_empty(self._orphan(add_library=True)) + + def test_str_without_sample(self): + self._assert_str_empty(self._orphan(add_sample=True)) + + def test_admin_changelist_with_orphaned_pooling(self): + self._orphan(add_library=True) + self._orphan(add_sample=True) + admin_user = self.create_user(email="admin@test.io") + admin_user.is_superuser = True + admin_user.save() + self.client.force_login(admin_user) + + response = self.client.get(reverse("admin:pooling_pooling_changelist")) + self.assertEqual(response.status_code, 200) From bb3dd43256e64f61b6e9bc7bf5bc20befea1b046 Mon Sep 17 00:00:00 2001 From: "Project parkour-dev @ molgen" Date: Wed, 7 Oct 2026 09:36:03 +0200 Subject: [PATCH 2/3] Point LibraryPreparation/Pooling regression tests at upstream PR #373 The fix was split out of #370, which the maintainer took over for the IndexPair archive/guarded-delete rework, into its own PR #373. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VCWKtNqYoCiW18CWkWCfBi --- CLAUDE.md | 2 +- tests/parkour2/library_preparation/test_libraryprep_orphan.py | 2 +- tests/parkour2/pooling/test_pooling_orphan.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 04b2bd4..8b75eac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -198,7 +198,7 @@ Reported upstream (draft: `/project/parkour-dev/git/issue-migration-history-rewr ### Regression tests against the built checkout -`./test-parkour2.sh [manage.py test args]` runs `tests/parkour2//test_*.py` against `${PROJECT}/${BUILD_parkour2}`. It copies each file into its app for the run and removes it on exit, so `git pull` stays clean. It needs that tree's Postgres running, and Django creates/drops its own `test_parkour2` DB. Current content: admin HTTP 500s caused by rows whose `on_delete=SET_NULL` target was deleted, for IndexPair (no IndexType; seen 2026-10-02), LibraryPreparation (no Sample; seen 2026-10-06) and Pooling (no Library/Sample; not seen yet). All three are fixed upstream by maxplanck-ie/parkour2#370 (open as of 2026-10-06). Once that's in the pinned release, all tests should pass. Until then, a live 500 of this kind is fixed by deleting the orphaned rows (back them up with `dumpdata` first). To find such pages before users do, render every admin changelist in-process as a superuser (`admin.site._registry`, `RequestFactory`, `changelist_view`). +`./test-parkour2.sh [manage.py test args]` runs `tests/parkour2//test_*.py` against `${PROJECT}/${BUILD_parkour2}`. It copies each file into its app for the run and removes it on exit, so `git pull` stays clean. It needs that tree's Postgres running, and Django creates/drops its own `test_parkour2` DB. Current content: admin HTTP 500s caused by rows whose `on_delete=SET_NULL` target was deleted, for IndexPair (no IndexType; seen 2026-10-02), LibraryPreparation (no Sample; seen 2026-10-06) and Pooling (no Library/Sample; not seen yet). Upstream fixes: IndexPair in maxplanck-ie/parkour2#370 (taken over by the maintainer, who added archive-instead-of-delete for index records), LibraryPreparation and Pooling in maxplanck-ie/parkour2#373; both open as of 2026-10-07. Once both are in the pinned release, all tests should pass. Until then, a live 500 of this kind is fixed by deleting the orphaned rows (back them up with `dumpdata` first). To find such pages before users do, render every admin changelist in-process as a superuser (`admin.site._registry`, `RequestFactory`, `changelist_view`). ## Source download strategy diff --git a/tests/parkour2/library_preparation/test_libraryprep_orphan.py b/tests/parkour2/library_preparation/test_libraryprep_orphan.py index 386a38a..0c91d08 100644 --- a/tests/parkour2/library_preparation/test_libraryprep_orphan.py +++ b/tests/parkour2/library_preparation/test_libraryprep_orphan.py @@ -7,7 +7,7 @@ return HTTP 500 for everyone (seen on parkour-dev 2026-10-06; the samples had been deleted in the admin on 2026-09-15). -Fixed upstream in maxplanck-ie/parkour2#370. +Fixed upstream in maxplanck-ie/parkour2#373 (the IndexPair case is #370). """ from common.tests import BaseTestCase diff --git a/tests/parkour2/pooling/test_pooling_orphan.py b/tests/parkour2/pooling/test_pooling_orphan.py index b155613..7699984 100644 --- a/tests/parkour2/pooling/test_pooling_orphan.py +++ b/tests/parkour2/pooling/test_pooling_orphan.py @@ -6,7 +6,7 @@ None, and /admin/pooling/pooling/ would return HTTP 500. Not seen live yet; same bug as LibraryPreparation (see test_libraryprep_orphan.py). -Fixed upstream in maxplanck-ie/parkour2#370. +Fixed upstream in maxplanck-ie/parkour2#373 (the IndexPair case is #370). """ from common.tests import BaseTestCase From cb13ac08a15b33df4e89ea94d33798ed9df75260 Mon Sep 17 00:00:00 2001 From: "Project parkour-dev @ molgen" Date: Tue, 20 Oct 2026 16:26:50 +0200 Subject: [PATCH 3/3] Track parkour2 develop as dev release 2026-10-20-master Upstream develop (30d25b2e) contains our admin-500 fixes #370 and #373 plus the catalog delete guard #375, but no release tag yet. All 7 local regression tests and upstream's 206 tests for the touched apps pass on it. Dev cutover from 26.10.05-dev: 13 new migrations, schema identical to a fresh migrate, admin sweep 0/30 failing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VCWKtNqYoCiW18CWkWCfBi --- CLAUDE.md | 4 ++-- build.profile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8b75eac..8a32ba0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -136,7 +136,7 @@ The nginx config (`nginx.conf.build.sh`) serves parkour2's split frontend/backen Clones the branch/tag named by `BUILD_parkour2` (e.g. `parkour2-develop` → branch `develop`, `parkour2-26.07.16` → tag `26.07.16`) into `${BUILD_DIR}`, caching a `.tar` there to speed up the initial clone on future full rebuilds. That local `.tar`/directory is only a seed for the first clone into `${PROJECT}/${BUILD_parkour2}` - once that checkout exists, its `origin` remote is pointed at the real `https://github.com/maxplanck-ie/parkour2.git` (not the local cache) and `git pull origin ` is run on every build, so a branch pin like `develop` actually tracks new upstream commits instead of freezing at whatever the cache had. Installs Python deps via `uv pip install --system` using the compiled requirements file matching the running Python minor version (`backend/requirements//base.txt`). Builds the Vue3 frontend with `npm run build`. Generates `backend/.env` (secret key, database URL, CSRF trusted origin) and runs `manage.py collectstatic`. -**Version pin (`build.profile`'s `BUILD_parkour2`) is the release tag `parkour2-26.10.05`.** It tracked `develop` from 26.07 until 26.10.05 to pick up fixes (e.g. maxplanck-ie/parkour2#310) before they were released. With a tag pin, `git pull origin ` on each build is a no-op, so rebuilds are reproducible. Upstream tags are not strictly immutable, though: `26.10.05` was re-pointed from `5abec953` to `073d4b6b` (adding a CHANGELOG-only commit) on release day, so check `git ls-remote --tags` when an exact commit matters. Changing `BUILD_parkour2` also changes the checkout directory (`${PROJECT}/${BUILD_parkour2}`), and with it the paths in the regenerated `startstop.sh`, `nginx.conf` and `profile`. The running services keep using the old checkout until they're restarted. Also, `develop` can rewrite already-applied migrations (it did in September 2026), so after any upgrade compare `pg_dump --schema-only` against a from-scratch `migrate`. +**Version pin (`build.profile`'s `BUILD_parkour2`) is the branch `parkour2-develop`** (dev release `2026-10-20-master`, built from `develop` at `30d25b2e`), to pick up our merged fixes maxplanck-ie/parkour2#370/#373 and the catalog delete guard #375 before a release contains them. Earlier it was the tag `parkour2-26.10.05`, and before that `develop` again from 26.07 (for #310). A branch pin follows whatever `develop` is at build time, so a rebuild is not reproducible; record the commit (`git -C ${PROJECT}/parkour2-develop log -1`) and switch back to a release tag once one includes these fixes. With a tag pin, `git pull origin ` on each build is a no-op, so rebuilds are reproducible. Upstream tags are not strictly immutable, though: `26.10.05` was re-pointed from `5abec953` to `073d4b6b` (adding a CHANGELOG-only commit) on release day, so check `git ls-remote --tags` when an exact commit matters. Changing `BUILD_parkour2` also changes the checkout directory (`${PROJECT}/${BUILD_parkour2}`), and with it the paths in the regenerated `startstop.sh`, `nginx.conf` and `profile`. The running services keep using the old checkout until they're restarted. Also, `develop` can rewrite already-applied migrations (it did in September 2026), so after any upgrade compare `pg_dump --schema-only` against a from-scratch `migrate`. ### `backend/media` persistence across releases @@ -198,7 +198,7 @@ Reported upstream (draft: `/project/parkour-dev/git/issue-migration-history-rewr ### Regression tests against the built checkout -`./test-parkour2.sh [manage.py test args]` runs `tests/parkour2//test_*.py` against `${PROJECT}/${BUILD_parkour2}`. It copies each file into its app for the run and removes it on exit, so `git pull` stays clean. It needs that tree's Postgres running, and Django creates/drops its own `test_parkour2` DB. Current content: admin HTTP 500s caused by rows whose `on_delete=SET_NULL` target was deleted, for IndexPair (no IndexType; seen 2026-10-02), LibraryPreparation (no Sample; seen 2026-10-06) and Pooling (no Library/Sample; not seen yet). Upstream fixes: IndexPair in maxplanck-ie/parkour2#370 (taken over by the maintainer, who added archive-instead-of-delete for index records), LibraryPreparation and Pooling in maxplanck-ie/parkour2#373; both open as of 2026-10-07. Once both are in the pinned release, all tests should pass. Until then, a live 500 of this kind is fixed by deleting the orphaned rows (back them up with `dumpdata` first). To find such pages before users do, render every admin changelist in-process as a superuser (`admin.site._registry`, `RequestFactory`, `changelist_view`). +`./test-parkour2.sh [manage.py test args]` runs `tests/parkour2//test_*.py` against `${PROJECT}/${BUILD_parkour2}`. It copies each file into its app for the run and removes it on exit, so `git pull` stays clean. It needs that tree's Postgres running, and Django creates/drops its own `test_parkour2` DB. Current content: admin HTTP 500s caused by rows whose `on_delete=SET_NULL` target was deleted, for IndexPair (no IndexType; seen 2026-10-02), LibraryPreparation (no Sample; seen 2026-10-06) and Pooling (no Library/Sample; not seen yet). Upstream fixes: IndexPair in maxplanck-ie/parkour2#370 (taken over by the maintainer, who added archive-instead-of-delete for index records), LibraryPreparation and Pooling in maxplanck-ie/parkour2#373; both merged into `develop` on 2026-10-08, and all 7 tests pass on `develop` `30d25b2e` (2026-10-20). Drop `tests/parkour2/` once we're back on a release tag that contains both. On a build without them, a live 500 of this kind is fixed by deleting the orphaned rows (back them up with `dumpdata` first). To find such pages before users do, render every admin changelist in-process as a superuser (`admin.site._registry`, `RequestFactory`, `changelist_view`). ## Source download strategy diff --git a/build.profile b/build.profile index 603f663..9225940 100644 --- a/build.profile +++ b/build.profile @@ -1,6 +1,6 @@ -BUILD_TAG=26.10.05 +BUILD_TAG=2026-10-20-master -BUILD_parkour2=parkour2-26.10.05 +BUILD_parkour2=parkour2-develop BUILD_DIR=build