7. Oefenruns, delen en voortgang
Deze sectie beschrijft de opslag van unieke oefenruns, ontvangen gedeelde oefeningen, server-side voortgang per vraag en uniforme statistieken voor geschiedenis, detailweergave en meekijkfunctionaliteit.
7.1 ExerciseRuns
| Tabelnaam | Categorie | Doel / verantwoordelijkheid | Gerelateerde tabellen |
|---|---|---|---|
| ExerciseRuns | Oefenresultaten | Hoofdtabel voor een unieke oefenrun van een gebruiker, inclusief uniforme metadata, totalen, statistieken en de module-specifieke JSON/base64-payload. | Users, TeacherLevels, Categories, Exercises, ExerciseModules, ExerciseRunProgress, SharedExercises |
| Veldnaam | Type | Default | PK | FK | Verwijst naar | Unique | Nullable | Index | Opmerking |
|---|---|---|---|---|---|---|---|---|---|
| Id | uniqueidentifier | - | J | N | - | J | N | J | Primaire sleutel. GUID wordt in de applicatiecode gegenereerd; geen database-default. |
| UserId | uniqueidentifier | - | N | N | Users.Id | N | N | J | Soft link naar identity.Users.Id; gebruiker die de oefening uitvoert of heeft uitgevoerd. Geen harde database-FK vanwege modulegrens. |
| LevelId | uniqueidentifier | - | N | N | TeacherLevels.Id | N | N | J | Soft link naar catalog.TeacherLevels.Id; actief niveau op het moment van genereren. Geen harde database-FK vanwege modulegrens. |
| CategoryId | uniqueidentifier | - | N | N | Categories.Id | N | N | J | Soft link naar catalog.Categories.Id; centrale categoriecontext van de run. Geen harde database-FK vanwege modulegrens. |
| ExerciseId | uniqueidentifier | - | N | N | Exercises.Id | N | N | J | Soft link naar catalog.Exercises.Id; oefening waarop de run is gebaseerd. Geen harde database-FK vanwege modulegrens. |
| ExerciseModuleId | uniqueidentifier | - | N | N | ExerciseModules.Id | N | N | J | Soft link naar catalog.ExerciseModules.Id; technische moduleversie die de vraag- en antwoordstructuur bepaalt. Geen harde database-FK vanwege modulegrens. |
| RequestedQuestionCount | int | 0 | N | N | - | N | N | N | Aantal opgaven dat bij genereren is aangevraagd. |
| QuestionDataJsonBase64 | nvarchar(max) | - | N | N | - | N | N | N | Volledige module-specifieke payload met vraag-, antwoord- en voortgangsdata, opgeslagen als JSON in base64-vorm. |
| TotalQuestions | int | 0 | N | N | - | N | N | N | Uniform totaal aantal vragen in deze run. |
| CorrectCount | int | 0 | N | N | - | N | N | N | Totaal aantal goed beantwoorde vragen, berekend en daarna direct uitleesbaar. |
| IncorrectCount | int | 0 | N | N | - | N | N | N | Totaal aantal fout beantwoorde vragen. |
| DunnoCount | int | 0 | N | N | - | N | N | N | Totaal aantal vragen dat als “Geen idee” is gemarkeerd. |
| CompletedQuestionCount | int | 0 | N | N | - | N | N | N | Aantal vragen waarvan de voortgang server-side is afgerond. |
| AverageTimeSeconds | decimal(10,2) | 0 | N | N | - | N | N | N | Gemiddelde tijd per vraag op basis van uniforme timingwaarden. |
| MedianTimeSeconds | decimal(10,2) | 0 | N | N | - | N | N | N | Mediaan van de beantwoordingstijd per vraag. |
| LowerBoundSeconds | decimal(10,2) | 0 | N | N | - | N | N | N | Ondergrens van de gebruikte statistische bandbreedte. |
| UpperBoundSeconds | decimal(10,2) | 0 | N | N | - | N | N | N | Bovengrens van de gebruikte statistische bandbreedte. |
| LowerOutlierCount | int | 0 | N | N | - | N | N | N | Aantal waarden onder de ondergrens. |
| UpperOutlierCount | int | 0 | N | N | - | N | N | N | Aantal waarden boven de bovengrens. |
| CreatedAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | J | Moment waarop de run is aangemaakt. |
| StartedAtUtc | datetime2 | null | N | N | - | N | J | N | Moment waarop de eerste vraag voor het eerst is getoond. |
| CompletedAtUtc | datetime2 | null | N | N | - | N | J | J | Moment waarop alle vragen zijn afgerond en statistieken definitief zijn berekend. |
| LastActivityAtUtc | datetime2 | null | N | N | - | N | J | J | Laatste server-side update binnen de run, relevant voor live meekijken en herstel na onderbreking. |
| IsCompleted | bit | 0 | N | N | - | N | N | J | Geeft aan of de run formeel is afgerond en in geschiedenis/resultaten zichtbaar mag zijn. |
| IsTestRun | bit | 0 | N | N | - | N | N | J | Markeert docent-testoefeningen die niet permanent in geschiedenis/resultaten terechtkomen. |
| DuplicateOfExerciseRunId | uniqueidentifier | null | N | J | ExerciseRuns.Id | N | J | J | Verwijzing naar de bronrun wanneer deze run een duplicaat is met dezelfde vragen in andere volgorde. |
| SharedExerciseId | uniqueidentifier | null | N | J | SharedExercises.Id | N | J | J | Verwijzing naar het administratieve shared-record wanneer deze run is ontstaan doordat een ontvanger een gedeelde oefening daadwerkelijk heeft gestart. |
Validaties / constraints
- UserId, LevelId, CategoryId, ExerciseId en ExerciseModuleId zijn verplicht.
- IsCompleted = 1 vereist een CompletedAtUtc-waarde en definitief berekende totalen.
- RequestedQuestionCount moet binnen de door de oefening bepaalde minimum- en maximumgrenzen vallen, met een absoluut systeemmaximum van 100.
Business rules
- Elke run is uniek per gebruiker.
- DuplicateOfExerciseRunId verwijst naar dezelfde inhoud in een andere volgorde voor dezelfde gebruiker; SharedExerciseId verwijst naar een gedeelde oefening die eerst administratief is ontvangen en daarna daadwerkelijk gestart.
- IsTestRun onderscheidt docenten-tests van reguliere leerlingruns.
- Een run verwijst historisch naar de concrete oefening en de concrete moduleversie die op dat moment golden.
- LevelId, CategoryId, ExerciseId en ExerciseModuleId vormen samen de historische context van de run en blijven na aanmaak of afronding ongewijzigd, ook wanneer centrale categorieën of koppelingen later migreren.
Lifecycle / gedrag
- Bij genereren wordt een runrecord aangemaakt.
- Na het eerste tonen van een vraag wordt StartedAtUtc gevuld.
- Na elk antwoord schrijft de server de relevante voortgang bij in ExerciseRunProgress, worden de uniforme totalen/statistieken op ExerciseRuns bijgewerkt en deelt de actuele stand met eventuele meekijkers.
- Bij afronden worden de definitieve statistieken éénmalig berekend en wordt IsCompleted = 1 gezet.
- Wanneer een run ontstaat vanuit een gedeelde oefening, verwijst SharedExerciseId naar het bijbehorende administratieve shared-record.
- Een dagelijkse TickerQ-taak ruimt niet-opgeruimde testruns op.
- Een latere categoriemigratie herschrijft deze historische runcontext niet.
Designkeuzes
- Module-specifieke vraag- en antwoordstructuren blijven bewust in JSON/base64 opgeslagen omdat technische modules sterk kunnen variëren in parameters en antwoordvormen, terwijl uniforme statistiekvelden juist direct relationeel en doorzoekbaar moeten zijn.
Foreign keys op databaseniveau
- Harde foreign keys op databaseniveau: DuplicateOfExerciseRunId -> ExerciseRuns.Id; SharedExerciseId -> SharedExercises.Id.
Functionele / logische verwijzingen zonder harde FK
UserIdis een soft link naaridentity.Users.Id.LevelId,CategoryId,ExerciseIdenExerciseModuleIdzijn soft links naar het catalogusdomein. Deze velden worden niet als harde database-FK afgedwongen vanwege de modulegrens.- Deze soft links vormen samen met snapshotwaarden in payload/exportmodellen de historische runcontext. De runcontext wordt na aanmaak of afronding niet stilzwijgend herschreven door latere catalogus- of modulemigraties.
- De velden QuestionDataJsonBase64 bevatten vrije of modulespecifieke payload en zijn bewust niet relationeel uitgesplitst naar harde foreign keys.
- Historische runreconstructie gebruikt de opgeslagen runcontext, uniforme runvelden,
ExerciseModuleIden de module-specifieke payload. Wanneer de payload een modulekey en schema-/payloadversie bevat, zoalsmoduleKeyenschemaVersion, gebruikt de module die waarden voor backwards-compatible interpretatie. Hiervoor worden geen extra relationele kolommen zoalsConfigSchemaVersiongeïntroduceerd.
FK + snapshot
- FK + snapshot: niet van toepassing binnen deze tabel.
7.2 SharedExercises
| Tabelnaam | Categorie | Doel / verantwoordelijkheid | Gerelateerde tabellen |
|---|---|---|---|
| SharedExercises | Oefenresultaten | Administratieve registratie van ontvangen gedeelde oefeningen voordat de ontvanger een echte exercise run start, inclusief herkomst, snapshots en soft delete. | ExerciseRuns, Users |
| Veldnaam | Type | Default | PK | FK | Verwijst naar | Unique | Nullable | Index | Opmerking |
|---|---|---|---|---|---|---|---|---|---|
| Id | uniqueidentifier | - | J | N | - | J | N | J | Primaire sleutel. GUID wordt in de applicatiecode gegenereerd; geen database-default. |
| SourceExerciseRunId | uniqueidentifier | - | N | J | ExerciseRuns.Id | N | N | J | Bronrun waarvan de gedeelde inhoud afkomstig is. |
| SharedByUserId | uniqueidentifier | - | N | N | Users.Id | N | N | J | Soft link naar identity.Users.Id; gebruiker die de oefening heeft gedeeld. Geen harde database-FK vanwege modulegrens. |
| SharedToUserId | uniqueidentifier | - | N | N | Users.Id | N | N | J | Soft link naar identity.Users.Id; gebruiker die de gedeelde oefening heeft ontvangen. Geen harde database-FK vanwege modulegrens. |
| StartedExerciseRunId | uniqueidentifier | null | N | J | ExerciseRuns.Id | J | J | J | Run die ontstaat zodra de ontvanger de gedeelde oefening daadwerkelijk start. |
| LevelSnapshotText | nvarchar(150) | - | N | N | - | N | N | N | Tekstuele snapshot van het niveau zoals zichtbaar op het moment van delen. |
| CategorySnapshotText | nvarchar(150) | - | N | N | - | N | N | N | Tekstuele snapshot van de categorie zoals zichtbaar op het moment van delen. |
| ExerciseSnapshotText | nvarchar(150) | - | N | N | - | N | N | N | Tekstuele snapshot van de oefening zoals zichtbaar op het moment van delen. |
| SharedAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | J | Moment waarop het administratieve shared-record is aangemaakt. |
| StartedAtUtc | datetime2 | null | N | N | - | N | J | J | Moment waarop de ontvanger de gedeelde oefening voor het eerst echt start. |
| CompletedAtUtc | datetime2 | null | N | N | - | N | J | J | Moment waarop de uit deze share ontstane run is afgerond, gespiegeld voor snelle overzichtsweergave. |
| IsDeleted | bit | 0 | N | N | - | N | N | J | Soft delete-vlag voor verwijderen uit het eigen overzicht van de ontvanger. |
| DeletedAtUtc | datetime2 | null | N | N | - | N | J | J | Moment waarop de ontvanger de gedeelde oefening uit het eigen overzicht verwijdert. |
| DeletedByUserId | uniqueidentifier | null | N | N | Users.Id | N | J | N | Soft link naar identity.Users.Id; gebruiker die de soft delete uitvoerde. Geen harde database-FK vanwege modulegrens. |
| CreatedAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | J | Aanmaakmoment van het record. |
| UpdatedAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | N | Laatste wijzigingsmoment. |
Validaties / constraints
- SourceExerciseRunId, SharedByUserId en SharedToUserId zijn verplicht.
- StartedExerciseRunId is uniek wanneer gevuld, zodat één shared-record maximaal tot één echte run leidt.
Business rules
CompletedAtUtc op SharedExercises is een afgeleid overzichtsveld dat het afrondmoment van de daaruit ontstane run spiegelt. Het primaire afrondmomentdomein blijft de echte exercise run.
- Deze tabel registreert ontvangen gedeelde oefeningen vóórdat de ontvanger een echte run start.
- Het shared-record bevat daarom zowel de herkomst-run als de deler en ontvanger, plus snapshots van niveau, categorie en oefening voor stabiele weergave.
- Wanneer de broncategorie later administratief is gemigreerd, blijven de snapshotwaarden ongewijzigd en blijft een nieuw uit het shared-record gestart runrecord functioneel gekoppeld aan de oorspronkelijke categoriecontext van de bronrun.
Lifecycle / gedrag
- Een afzender kan een eenmaal gedeelde oefening niet terugtrekken.
- De ontvanger kan het shared-record wel uit het eigen overzicht verwijderen via soft delete.
- Start- en afrondmomenten worden gevuld zodra de ontvanger daadwerkelijk een run start en voltooit.
- Gedeelde oefeningen leven eerst in SharedExercises en pas later eventueel in een echte run.
Foreign keys op databaseniveau
- Harde foreign keys op databaseniveau: SourceExerciseRunId -> ExerciseRuns.Id; StartedExerciseRunId -> ExerciseRuns.Id.
Functionele / logische verwijzingen zonder harde FK
SharedByUserId,SharedToUserIdenDeletedByUserIdzijn soft links naaridentity.Users.Id. De deelactie en zichtbaarheid worden applicatief gevalideerd via Identity/Relationships-contracten.SourceExerciseRunIdenStartedExerciseRunIdblijven harde FK's binnen het practice-domein.
FK + snapshot
- FK + snapshot: niet van toepassing binnen deze tabel.
7.3 ExerciseRunProgress
| Tabelnaam | Categorie | Doel / verantwoordelijkheid | Gerelateerde tabellen |
|---|---|---|---|
| ExerciseRunProgress | Oefenresultaten | Operationele voortgang per vraag binnen een oefenrun, bedoeld voor server-side opslag na elk antwoord, abrupt onderbreken en live meekijken. | ExerciseRuns |
| Veldnaam | Type | Default | PK | FK | Verwijst naar | Unique | Nullable | Index | Opmerking |
|---|---|---|---|---|---|---|---|---|---|
| Id | uniqueidentifier | - | J | N | - | J | N | J | Primaire sleutel. GUID wordt in de applicatiecode gegenereerd; geen database-default. |
| ExerciseRunId | uniqueidentifier | - | N | J | ExerciseRuns.Id | N | N | J | Run waar deze voortgangsregel bij hoort. |
| SequenceNumber | int | 0 | N | N | - | N | N | J | Volgorde van de vraag binnen deze specifieke run. |
| QuestionStateJsonBase64 | nvarchar(max) | - | N | N | - | N | N | N | Module-specifieke toestand van de vraag, inclusief parameters, antwoordstructuur en lokale voortgang. |
| FirstShownAtUtc | datetime2 | null | N | N | - | N | J | N | Moment waarop de vraag voor het eerst aan de gebruiker is getoond. |
| AnsweredAtUtc | datetime2 | null | N | N | - | N | J | N | Moment waarop de gebruiker de vraag heeft bevestigd en de server de uitkomst verwerkt. |
| IsCorrect | bit | null | N | N | - | N | J | N | Uniforme markering of de vraag uiteindelijk goed is beantwoord. |
| IsDunno | bit | 0 | N | N | - | N | N | N | Geeft aan of de vraag als “Geen idee” is gemarkeerd. |
| IsCompleted | bit | 0 | N | N | - | N | N | J | Geeft aan of de server de vraag volledig heeft verwerkt en meegerekend in de totalen. |
| UpdatedAtUtc | datetime2 | sysutcdatetime() | N | N | - | N | N | J | Laatste wijzigingsmoment van deze voortgangsregel. |
Validaties / constraints
- Actieve combinatie ExerciseRunId + SequenceNumber is uniek.
- QuestionStateJsonBase64 is verplicht omdat de technische module-specifieke vraag- en antwoordtoestand hierin wordt vastgelegd.
- AnsweredAtUtc en IsCorrect mogen alleen worden gevuld nadat de server de beantwoording heeft verwerkt.
Business rules
IsCompleted op vraagniveau betekent dat de server deze specifieke vraag volledig heeft verwerkt. Dit staat los van IsCompleted op ExerciseRuns, dat de afronding van de volledige oefenrun aanduidt.
- Na elk antwoord wordt de betreffende voortgangsregel bijgewerkt en worden de uniforme totalen/statistieken op ExerciseRuns herberekend of incrementeel aangepast.
- Deze tabel ondersteunt abrupt onderbreken en live meekijken doordat de actuele stand per vraag centraal beschikbaar blijft.
- De inhoud van QuestionStateJsonBase64 blijft module-specifiek, terwijl timing en juistheid uniform zijn.
Lifecycle / gedrag
- Voor iedere gegenereerde vraag wordt een progressieregel aangemaakt.
- Bij eerste weergave wordt FirstShownAtUtc gevuld.
- Bij beantwoording schrijft de server AnsweredAtUtc, IsCorrect, IsDunno, IsCompleted en de bijgewerkte vraagtoestand weg.
- De gegevens blijven historisch bewaard zolang de bijbehorende run bewaard blijft.
- Bij accountverwijdering van de uitvoerende gebruiker blijven bestaande runs gekoppeld aan het geanonimiseerde Users-record historisch bewaard. Niet-afgeronde runs krijgen geen aparte eindstatus, blijven IsCompleted = 0 en worden functioneel niet meer hervatbaar.
Designkeuzes
- Deze tabel vormt bewust de hybride middenweg tussen volledig relationele vraagopslag en één grote run-payload.
- De module-inhoud blijft flexibel in JSON/base64, terwijl veel geschreven operationele voortgang apart kan worden opgeslagen voor performance, herstelbaarheid en live meekijken.
Foreign keys op databaseniveau
- Harde foreign keys op databaseniveau: ExerciseRunId -> ExerciseRuns.Id.
Functionele / logische verwijzingen zonder harde FK
- De velden QuestionStateJsonBase64 bevatten vrije of modulespecifieke payload en zijn bewust niet relationeel uitgesplitst naar harde foreign keys.
- Ook voortgangspayloads mogen modulespecifieke schema-informatie bevatten wanneer dat nodig is om oudere voortgang veilig te renderen of te controleren; normale rapportage blijft gebaseerd op uniforme run- en voortgangsvelden.
FK + snapshot
- FK + snapshot: niet van toepassing binnen deze tabel.
7.4 Relatievoorwaarden voor gedeelde oefeningen
7.4.1 Delen vereist actieve relatie
- Het aanmaken van een nieuw
SharedExercises-record vereist server-side controle dat de deler en ontvanger op dat moment een actieve relatie hebben die delen toestaat. - Voor leerling-naar-leerling delen is dit in de uitgewerkte relatie-usecases de actieve
Friendship-relatie. - Frontend-zichtbaarheid van een deelknop of ontvangerlijst is geen autorisatie; de relatie wordt opnieuw gecontroleerd bij het daadwerkelijk delen.
7.4.2 Gevolgen van ontkoppelen
- Het ontkoppelen van een relatie verwijdert bestaande
SharedExercises-records niet. - Een ontvanger behoudt bestaande ontvangen gedeelde oefeningen en eventueel daaruit gestarte oefenruns, tenzij een afzonderlijke verwijder- of retentieregel van toepassing is.
- Na ontkoppeling mogen geen nieuwe gedeelde oefeningen tussen dezelfde gebruikers worden aangemaakt op basis van de gedeactiveerde relatie.
- Historische runs en snapshots blijven ongewijzigd, ook wanneer de relatie later wordt ontkoppeld.
7.4.3 Ouder/voogdrelaties en oefenen
- Een
GuardianStudent-relatie geeft ouder/voogd-inzage en waar toegestaan live meekijken, maar geeft geen recht om namens de leerling oefenruns te starten, te beantwoorden of te wijzigen. - Oefenrunmutaties blijven gekoppeld aan de uitvoerende leerlinggebruiker.
7.5 Account-lifecycle binnen oefenruns en delen
7.5.1 Accountverwijdering en open oefenruns
Bij accountverwijdering of anonimisering worden open oefenruns van de gebruiker niet afgerond. Zij blijven administratief niet-afgerond, maar zijn niet langer hervatbaar en verschijnen niet in normale geschiedenis- of statistiekweergaven.
Afgeronde oefenruns, resultaten en historie blijven waar nodig beschikbaar onder geanonimiseerde identiteit, zodat historische overzichten, audit en rapportages niet worden herschreven of hard verwijderd.
7.5.2 Logout en oefenruns
Uitloggen tijdens een niet-afgeronde oefenrun geldt niet als afronding van die oefening. De run blijft alleen hervatbaar wanneer de gebruiker later opnieuw met een geldig actief account en geldige context inlogt. Logout mag geen oefenrun, resultaat, relatie, ticket, systeembericht of systeemnotificatie aanmaken of inhoudelijk wijzigen.
7.6 Ouder-/voogd- en beheerdercontext bij runs
| Onderwerp | Aanscherping |
|---|---|
| Ouder-/voogdresultaten | Ouder-/voogdresultaatinzage leest dezelfde historische ExerciseRuns, voortgang, totalen en statistiekvelden als leerling- en docentweergaven. Er ontstaat geen aparte ouderresultaatdata. |
| Alle niveaus | Een ouder/voogd mag resultaten en geschiedenis van alle niveaus van gekoppelde kinderen raadplegen, zolang de ouder-/voogdrelatie actief is. |
| Geen oefenstart | Een ouder/voogd kan geen oefening genereren, starten, hervatten of afronden namens het kind. |
| Resultaat-PDF | PDF-export binnen oudercontext gebruikt dezelfde historische runcontext als leerling- en docentexport, maar wordt server-side begrensd door de actieve ouder-/voogdrelatie. |
| Live meekijken | Live meekijken is read-only en gebruikt server-side opgeslagen voortgang als bron. SignalR is transport, niet de bron van waarheid. |
| Categorie- en modulemigratie | Categorie- of modulemigraties mogen historische runs, gedeelde oefeningen en bestaande resultaten niet herschrijven of onleesbaar maken. |
| Read-only resultaatflows | Samenvatting, geschiedenis, filters, detail, statistieken en PDF-export wijzigen geen ExerciseRuns, ExerciseRunProgress, antwoorden, scores of statistiekvelden. |
| Lege toestanden | Geen afgeronde runs of lege filterresultaten zijn geldige readmodeltoestanden en veroorzaken geen foutstatus of datamutatie. |
| Actieve live voortgang | Live meekijken leest ExerciseRunProgress en uniforme runvelden na server-side verwerking van leerlingacties. Ouder/voogd-publicatie van updates schrijft zelf geen voortgang. |
| Browse-modus | Door vragen bladeren tijdens live meekijken is lokale UI-state; de actuele vraag blijft afgeleid uit de opgeslagen voortgang. |
| Verbindingsverlies | Reconnect en definitieve verbreking beïnvloeden geen oefenrun. Alleen de live audit/subscription wordt beëindigd of hervat. |