Add SUPER exam results preview
This commit is contained in:
@@ -25,6 +25,14 @@ public class ExamSimulationsController(ExamSimulationStore simulations) : Contro
|
||||
catch (InvalidOperationException exception) { return Conflict(exception.Message); }
|
||||
}
|
||||
|
||||
[HttpGet("preview-results")]
|
||||
[Authorize(Roles = "SUPER")]
|
||||
public async Task<IActionResult> PreviewResults()
|
||||
{
|
||||
try { return Ok(await simulations.BuildResultsPreviewAsync(UserId())); }
|
||||
catch (InvalidOperationException exception) { return Conflict(exception.Message); }
|
||||
}
|
||||
|
||||
[HttpGet("active")]
|
||||
public async Task<IActionResult> Active()
|
||||
{
|
||||
|
||||
@@ -93,6 +93,54 @@ public class ExamSimulationStore
|
||||
return simulation;
|
||||
}
|
||||
|
||||
public async Task<ExamSimulationStateResponse> BuildResultsPreviewAsync(string userId)
|
||||
{
|
||||
var bank = (await _questions.GetPublishedAsync()).Where(question => question.Choices.Count >= 5).ToList();
|
||||
if (bank.Count < RequiredQuestions)
|
||||
throw new InvalidOperationException($"At least {RequiredQuestions} published questions are required.");
|
||||
var targets = BuildTargets();
|
||||
var selected = SelectForm(bank, [], targets);
|
||||
var items = selected.Select((question, index) => new ExamSimulationItem
|
||||
{
|
||||
QuestionExternalId = question.ExternalId,
|
||||
BlockIndex = BlockIndexForPosition(index),
|
||||
Position = PositionWithinBlock(index),
|
||||
PresentedChoiceIds = SelectChoices(question),
|
||||
QuestionElo = 1000,
|
||||
WasSeen = false,
|
||||
BlueprintCategory = BlueprintCategory(question)
|
||||
}).ToList();
|
||||
var questionMap = selected.ToDictionary(question => question.ExternalId, StringComparer.Ordinal);
|
||||
var responses = items.Select((item, index) =>
|
||||
{
|
||||
var question = questionMap[item.QuestionExternalId];
|
||||
var choiceId = index % 3 == 0
|
||||
? item.PresentedChoiceIds.First(id => id != question.CorrectChoiceId)
|
||||
: question.CorrectChoiceId;
|
||||
return new ExamSimulationResponse
|
||||
{
|
||||
QuestionExternalId = item.QuestionExternalId,
|
||||
ChoiceId = choiceId,
|
||||
ElapsedSeconds = 75 + index % 45,
|
||||
Flagged = index % 11 == 0
|
||||
};
|
||||
}).ToList();
|
||||
var responseMap = responses.ToDictionary(item => item.QuestionExternalId, StringComparer.Ordinal);
|
||||
return new ExamSimulationStateResponse
|
||||
{
|
||||
Id = "preview",
|
||||
State = ExamSimulationStates.Completed,
|
||||
CurrentBlock = BlockSizes.Length - 1,
|
||||
BreakSecondsRemaining = 3300,
|
||||
Responses = responses,
|
||||
Score = ExamSimulationScoring.Calculate(items, responseMap, questionMap),
|
||||
BlueprintTargets = targets,
|
||||
BlueprintActual = items.GroupBy(item => item.BlueprintCategory).ToDictionary(group => group.Key, group => group.Count()),
|
||||
ReviewQuestions = items.Select(item => QuestionResponse.FromModel(
|
||||
questionMap[item.QuestionExternalId], questionElo: item.QuestionElo, includeChoicePool: true)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ExamSimulation?> FindActiveAsync(string userId) =>
|
||||
await _simulations.Find(item => item.UserId == userId &&
|
||||
(item.State == ExamSimulationStates.BetweenBlocks || item.State == ExamSimulationStates.BlockActive))
|
||||
@@ -261,6 +309,28 @@ public class ExamSimulationStore
|
||||
private Task ReplaceAsync(ExamSimulation simulation) =>
|
||||
_simulations.ReplaceOneAsync(item => item.Id == simulation.Id && item.UserId == simulation.UserId, simulation);
|
||||
|
||||
private static int BlockIndexForPosition(int position)
|
||||
{
|
||||
var consumed = 0;
|
||||
for (var block = 0; block < BlockSizes.Length; block++)
|
||||
{
|
||||
if (position < consumed + BlockSizes[block]) return block;
|
||||
consumed += BlockSizes[block];
|
||||
}
|
||||
return BlockSizes.Length - 1;
|
||||
}
|
||||
|
||||
private static int PositionWithinBlock(int position)
|
||||
{
|
||||
var consumed = 0;
|
||||
foreach (var size in BlockSizes)
|
||||
{
|
||||
if (position < consumed + size) return position - consumed;
|
||||
consumed += size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static ExamSimulationQuestionResponse SafeQuestion(Question question, List<string> presentedIds) => new()
|
||||
{
|
||||
ExternalId = question.ExternalId,
|
||||
|
||||
@@ -41,6 +41,10 @@ func create_simulation() -> void:
|
||||
_send("create", HTTPClient.METHOD_POST, "")
|
||||
|
||||
|
||||
func load_results_preview() -> void:
|
||||
_send("preview", HTTPClient.METHOD_GET, "/preview-results")
|
||||
|
||||
|
||||
func start_next_block() -> void:
|
||||
if simulation.is_empty():
|
||||
return
|
||||
|
||||
@@ -13,6 +13,7 @@ var bank_error_label: Label
|
||||
var retry_button: Button
|
||||
var simulation_button: Button
|
||||
var simulation_status_label: Label
|
||||
var simulation_preview_link: LinkButton
|
||||
var pending_simulation_launch := false
|
||||
var current_stage := "exam"
|
||||
var pending_question_bank_launch := false
|
||||
@@ -32,6 +33,8 @@ func _ready() -> void:
|
||||
ExamSimulationService.state_changed.connect(_on_simulation_state_changed)
|
||||
if not ExamSimulationService.request_failed.is_connected(_on_simulation_failed):
|
||||
ExamSimulationService.request_failed.connect(_on_simulation_failed)
|
||||
if not AuthState.auth_changed.is_connected(_refresh_simulation_preview_visibility):
|
||||
AuthState.auth_changed.connect(_refresh_simulation_preview_visibility)
|
||||
|
||||
_build_exam_view()
|
||||
_build_mode_view()
|
||||
@@ -44,6 +47,7 @@ func on_show(payload: Variant = null) -> void:
|
||||
elif str(payload) == "exam":
|
||||
_show_stage("exam")
|
||||
_refresh_bank_state()
|
||||
_refresh_simulation_preview_visibility()
|
||||
ExamSimulationService.load_readiness()
|
||||
|
||||
|
||||
@@ -160,6 +164,16 @@ func _build_mode_view() -> void:
|
||||
Style.set_label(simulation_status_label, 15, Style.MUTED)
|
||||
mode_view.add_child(simulation_status_label)
|
||||
|
||||
simulation_preview_link = LinkButton.new()
|
||||
simulation_preview_link.text = "Temporary: preview completed exam results"
|
||||
simulation_preview_link.tooltip_text = "SUPER only; generates a nonpersistent simulated result"
|
||||
simulation_preview_link.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
simulation_preview_link.add_theme_color_override("font_color", Style.CYAN)
|
||||
simulation_preview_link.add_theme_color_override("font_hover_color", Style.MAGENTA)
|
||||
simulation_preview_link.pressed.connect(_preview_simulation_results)
|
||||
mode_view.add_child(simulation_preview_link)
|
||||
_refresh_simulation_preview_visibility()
|
||||
|
||||
bank_status_label = Label.new()
|
||||
bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(bank_status_label, 16, Style.CYAN)
|
||||
@@ -243,6 +257,17 @@ func _select_simulation() -> void:
|
||||
ExamSimulationService.create_simulation()
|
||||
|
||||
|
||||
func _preview_simulation_results() -> void:
|
||||
pending_simulation_launch = true
|
||||
simulation_preview_link.disabled = true
|
||||
ExamSimulationService.load_results_preview()
|
||||
|
||||
|
||||
func _refresh_simulation_preview_visibility() -> void:
|
||||
if simulation_preview_link != null:
|
||||
simulation_preview_link.visible = AuthState.role.to_upper() == "SUPER"
|
||||
|
||||
|
||||
func _on_simulation_state_changed() -> void:
|
||||
_refresh_simulation_state()
|
||||
if not pending_simulation_launch or ExamSimulationService.simulation.is_empty():
|
||||
@@ -269,6 +294,8 @@ func _launch_simulation_block() -> void:
|
||||
|
||||
func _on_simulation_failed(message: String) -> void:
|
||||
pending_simulation_launch = false
|
||||
if simulation_preview_link != null:
|
||||
simulation_preview_link.disabled = false
|
||||
if simulation_status_label != null:
|
||||
simulation_status_label.text = message
|
||||
_refresh_simulation_state()
|
||||
|
||||
Reference in New Issue
Block a user