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); }
|
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")]
|
[HttpGet("active")]
|
||||||
public async Task<IActionResult> Active()
|
public async Task<IActionResult> Active()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -93,6 +93,54 @@ public class ExamSimulationStore
|
|||||||
return simulation;
|
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) =>
|
public async Task<ExamSimulation?> FindActiveAsync(string userId) =>
|
||||||
await _simulations.Find(item => item.UserId == userId &&
|
await _simulations.Find(item => item.UserId == userId &&
|
||||||
(item.State == ExamSimulationStates.BetweenBlocks || item.State == ExamSimulationStates.BlockActive))
|
(item.State == ExamSimulationStates.BetweenBlocks || item.State == ExamSimulationStates.BlockActive))
|
||||||
@@ -261,6 +309,28 @@ public class ExamSimulationStore
|
|||||||
private Task ReplaceAsync(ExamSimulation simulation) =>
|
private Task ReplaceAsync(ExamSimulation simulation) =>
|
||||||
_simulations.ReplaceOneAsync(item => item.Id == simulation.Id && item.UserId == simulation.UserId, 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()
|
private static ExamSimulationQuestionResponse SafeQuestion(Question question, List<string> presentedIds) => new()
|
||||||
{
|
{
|
||||||
ExternalId = question.ExternalId,
|
ExternalId = question.ExternalId,
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ func create_simulation() -> void:
|
|||||||
_send("create", HTTPClient.METHOD_POST, "")
|
_send("create", HTTPClient.METHOD_POST, "")
|
||||||
|
|
||||||
|
|
||||||
|
func load_results_preview() -> void:
|
||||||
|
_send("preview", HTTPClient.METHOD_GET, "/preview-results")
|
||||||
|
|
||||||
|
|
||||||
func start_next_block() -> void:
|
func start_next_block() -> void:
|
||||||
if simulation.is_empty():
|
if simulation.is_empty():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ var bank_error_label: Label
|
|||||||
var retry_button: Button
|
var retry_button: Button
|
||||||
var simulation_button: Button
|
var simulation_button: Button
|
||||||
var simulation_status_label: Label
|
var simulation_status_label: Label
|
||||||
|
var simulation_preview_link: LinkButton
|
||||||
var pending_simulation_launch := false
|
var pending_simulation_launch := false
|
||||||
var current_stage := "exam"
|
var current_stage := "exam"
|
||||||
var pending_question_bank_launch := false
|
var pending_question_bank_launch := false
|
||||||
@@ -32,6 +33,8 @@ func _ready() -> void:
|
|||||||
ExamSimulationService.state_changed.connect(_on_simulation_state_changed)
|
ExamSimulationService.state_changed.connect(_on_simulation_state_changed)
|
||||||
if not ExamSimulationService.request_failed.is_connected(_on_simulation_failed):
|
if not ExamSimulationService.request_failed.is_connected(_on_simulation_failed):
|
||||||
ExamSimulationService.request_failed.connect(_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_exam_view()
|
||||||
_build_mode_view()
|
_build_mode_view()
|
||||||
@@ -44,6 +47,7 @@ func on_show(payload: Variant = null) -> void:
|
|||||||
elif str(payload) == "exam":
|
elif str(payload) == "exam":
|
||||||
_show_stage("exam")
|
_show_stage("exam")
|
||||||
_refresh_bank_state()
|
_refresh_bank_state()
|
||||||
|
_refresh_simulation_preview_visibility()
|
||||||
ExamSimulationService.load_readiness()
|
ExamSimulationService.load_readiness()
|
||||||
|
|
||||||
|
|
||||||
@@ -160,6 +164,16 @@ func _build_mode_view() -> void:
|
|||||||
Style.set_label(simulation_status_label, 15, Style.MUTED)
|
Style.set_label(simulation_status_label, 15, Style.MUTED)
|
||||||
mode_view.add_child(simulation_status_label)
|
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 = Label.new()
|
||||||
bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
Style.set_label(bank_status_label, 16, Style.CYAN)
|
Style.set_label(bank_status_label, 16, Style.CYAN)
|
||||||
@@ -243,6 +257,17 @@ func _select_simulation() -> void:
|
|||||||
ExamSimulationService.create_simulation()
|
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:
|
func _on_simulation_state_changed() -> void:
|
||||||
_refresh_simulation_state()
|
_refresh_simulation_state()
|
||||||
if not pending_simulation_launch or ExamSimulationService.simulation.is_empty():
|
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:
|
func _on_simulation_failed(message: String) -> void:
|
||||||
pending_simulation_launch = false
|
pending_simulation_launch = false
|
||||||
|
if simulation_preview_link != null:
|
||||||
|
simulation_preview_link.disabled = false
|
||||||
if simulation_status_label != null:
|
if simulation_status_label != null:
|
||||||
simulation_status_label.text = message
|
simulation_status_label.text = message
|
||||||
_refresh_simulation_state()
|
_refresh_simulation_state()
|
||||||
|
|||||||
Reference in New Issue
Block a user