Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <branch>` 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/<major.minor>/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 <tag>` 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 <tag>` 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

Expand Down Expand Up @@ -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/<app>/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/<app>/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

Expand Down
4 changes: 2 additions & 2 deletions build.profile
Original file line number Diff line number Diff line change
@@ -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

Expand Down
46 changes: 46 additions & 0 deletions tests/parkour2/library_preparation/test_libraryprep_orphan.py
Original file line number Diff line number Diff line change
@@ -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#373 (the IndexPair case is #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)
48 changes: 48 additions & 0 deletions tests/parkour2/pooling/test_pooling_orphan.py
Original file line number Diff line number Diff line change
@@ -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#373 (the IndexPair case is #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)