Bug
Wenn ein Lecturer im Wizard ein Deployment erstellt, sehen die zugeordneten Studenten das Deployment nicht unter GET /api/v1/student/deployments — Response: 0 Deployments.
Root Cause
Der Student-List-Query in src/api/student.py:102-120 inner-joined über die ganze Kette:
Deployment → DeploymentInstance → DeploymentInstanceAccess
→ CourseGroup (via access.group_id)
→ GroupMember (via group.id)
→ CourseMember (via group_member.course_member_id, left_at IS NULL)
→ user_id == student
Es gibt im gesamten Backend keinen einzigen Code-Pfad, der CourseMember- oder GroupMember-Rows aus dem Wizard-Payload anlegt. grep -rn "CourseMember(" über src/ liefert ausschließlich die Modell-Definition und Tests — keine db.add(CourseMember(...)) in irgendeinem Service.
UserSyncService (src/services/user_sync_service.py) schreibt nur in users.
DeploymentService.create_deployment() (src/services/deployment_service.py:147-154) legt den Course an, aber keine Mitgliedschaften.
deploy_tasks.persist_credentials() (src/tasks/deploy_tasks.py:268) persistiert nur DeploymentInstanceAccess.group_id.
- Der einzige Endpoint, der
GroupMember erzeugt, ist POST /api/v1/courses/{id}/groups/{group_id}/members (src/api/courses.py:495) — und der setzt voraus, dass die CourseMember-Rows schon existieren (Filter Zeile 467).
→ CourseMember.user_id == student_user_id matcht nie → Join leer → 0 Deployments.
Bestätigung: tests/api/test_student_routes.py:130-134 legt CourseMember+GroupMember manuell in der Session an — der Test zeigt also, wovon der Read-Pfad ausgeht, aber im echten Code passiert das nirgends.
Wann ist das eingeschlichen worden
Commit der course_group_id//student/*-Read-Seite hat das Schema-Requirement eingeführt, ohne die Write-Seite mitzunehmen. Der Read-Pfad erwartet Mitgliedschafts-Rows, die niemand anlegt.
Fix (Vorschlag — Backend owns it)
In DeploymentService.create_deployment() direkt nach dem Auto-Create des Course (Zeilen 147-154) über stack_assignments[].groups[].students[] iterieren und idempotent upserten:
CourseMember(course_id, user_id=student.id) — falls fehlt
CourseGroup(course_id, name=group_name) — falls fehlt (returned id als course_group_id verwenden)
GroupMember(group_id, course_member_id) — falls fehlt
Vorteil: Wizard kann course_group_id: null schicken und es funktioniert trotzdem, Frontend-Chicken-and-Egg (siehe Frontend-Issue) löst sich automatisch. Idempotenz wichtig wegen Retry-Flow.
Alternative (worse): einen neuen POST /courses/{id}/members Endpoint anbieten, den das Frontend separat aufruft — drei Extra-Round-Trips pro Deploy plus Race-Conditions.
Verwandt
Frontend-Issue (Chicken-and-Egg bei internalCourseId): siehe DoziLab/appstore-frontend.
Wie reproduzieren
- Lecturer-Login, neuen Course (Keycloak-Group) wählen, der noch nie deployed wurde.
- Wizard durchklicken, mind. 1 Student zuordnen, Deployment abschicken → succeeded.
- Als zugeordneter Student einloggen →
/api/v1/student/deployments liefert [].
Bug
Wenn ein Lecturer im Wizard ein Deployment erstellt, sehen die zugeordneten Studenten das Deployment nicht unter
GET /api/v1/student/deployments— Response: 0 Deployments.Root Cause
Der Student-List-Query in src/api/student.py:102-120 inner-joined über die ganze Kette:
Es gibt im gesamten Backend keinen einzigen Code-Pfad, der
CourseMember- oderGroupMember-Rows aus dem Wizard-Payload anlegt.grep -rn "CourseMember("übersrc/liefert ausschließlich die Modell-Definition und Tests — keinedb.add(CourseMember(...))in irgendeinem Service.UserSyncService(src/services/user_sync_service.py) schreibt nur inusers.DeploymentService.create_deployment()(src/services/deployment_service.py:147-154) legt denCoursean, aber keine Mitgliedschaften.deploy_tasks.persist_credentials()(src/tasks/deploy_tasks.py:268) persistiert nurDeploymentInstanceAccess.group_id.GroupMembererzeugt, istPOST /api/v1/courses/{id}/groups/{group_id}/members(src/api/courses.py:495) — und der setzt voraus, dass dieCourseMember-Rows schon existieren (Filter Zeile 467).→
CourseMember.user_id == student_user_idmatcht nie → Join leer → 0 Deployments.Bestätigung:
tests/api/test_student_routes.py:130-134legtCourseMember+GroupMembermanuell in der Session an — der Test zeigt also, wovon der Read-Pfad ausgeht, aber im echten Code passiert das nirgends.Wann ist das eingeschlichen worden
Commit der
course_group_id//student/*-Read-Seite hat das Schema-Requirement eingeführt, ohne die Write-Seite mitzunehmen. Der Read-Pfad erwartet Mitgliedschafts-Rows, die niemand anlegt.Fix (Vorschlag — Backend owns it)
In
DeploymentService.create_deployment()direkt nach dem Auto-Create desCourse(Zeilen 147-154) überstack_assignments[].groups[].students[]iterieren und idempotent upserten:CourseMember(course_id, user_id=student.id)— falls fehltCourseGroup(course_id, name=group_name)— falls fehlt (returnedidalscourse_group_idverwenden)GroupMember(group_id, course_member_id)— falls fehltVorteil: Wizard kann
course_group_id: nullschicken und es funktioniert trotzdem, Frontend-Chicken-and-Egg (siehe Frontend-Issue) löst sich automatisch. Idempotenz wichtig wegen Retry-Flow.Alternative (worse): einen neuen
POST /courses/{id}/membersEndpoint anbieten, den das Frontend separat aufruft — drei Extra-Round-Trips pro Deploy plus Race-Conditions.Verwandt
Frontend-Issue (Chicken-and-Egg bei
internalCourseId): siehe DoziLab/appstore-frontend.Wie reproduzieren
/api/v1/student/deploymentsliefert[].