Add SUPER simulation review sample
This commit is contained in:
@@ -83,6 +83,7 @@ public sealed class ExamSimulationTests
|
||||
var completed = ExamSimulationStore.ToHistorySummary(new ExamSimulation
|
||||
{
|
||||
State = ExamSimulationStates.Completed,
|
||||
IsSample = true,
|
||||
Items = BuildItems(20),
|
||||
Score = new ExamSimulationScore { PredictedScore = 221, RangeLow = 204, RangeHigh = 238, Correct = 15, Total = 20 }
|
||||
});
|
||||
@@ -91,6 +92,7 @@ public sealed class ExamSimulationTests
|
||||
Assert.Equal(3, active.CurrentBlock);
|
||||
Assert.Equal(221, completed.PredictedScore);
|
||||
Assert.Equal(15, completed.Correct);
|
||||
Assert.True(completed.IsSample);
|
||||
}
|
||||
|
||||
private static List<ExamSimulationItem> BuildItems(int count, bool wasSeen = false) =>
|
||||
|
||||
@@ -33,6 +33,14 @@ public class ExamSimulationsController(ExamSimulationStore simulations) : Contro
|
||||
catch (InvalidOperationException exception) { return Conflict(exception.Message); }
|
||||
}
|
||||
|
||||
[HttpPost("sample-completed")]
|
||||
[Authorize(Roles = "SUPER")]
|
||||
public async Task<IActionResult> CreateSampleCompleted()
|
||||
{
|
||||
try { return Ok(ExamSimulationStore.ToHistorySummary(await simulations.CreateSampleCompletedAsync(UserId()))); }
|
||||
catch (InvalidOperationException exception) { return Conflict(exception.Message); }
|
||||
}
|
||||
|
||||
[HttpGet("active")]
|
||||
public async Task<IActionResult> Active()
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ public class ExamSimulation
|
||||
public string UserId { get; set; } = "";
|
||||
public string State { get; set; } = ExamSimulationStates.BetweenBlocks;
|
||||
public string ModelVersion { get; set; } = ExamSimulationScoring.ModelVersion;
|
||||
public bool IsSample { get; set; }
|
||||
public List<ExamSimulationItem> Items { get; set; } = [];
|
||||
public List<ExamSimulationResponse> Responses { get; set; } = [];
|
||||
public int CurrentBlock { get; set; } = -1;
|
||||
@@ -141,6 +142,7 @@ public class ExamSimulationHistorySummary
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public string State { get; set; } = "";
|
||||
public bool IsSample { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime UpdatedUtc { get; set; }
|
||||
public DateTime? CompletedUtc { get; set; }
|
||||
|
||||
@@ -97,7 +97,12 @@ public class ExamSimulationStore
|
||||
return simulation;
|
||||
}
|
||||
|
||||
public async Task<ExamSimulationStateResponse> BuildResultsPreviewAsync(string userId)
|
||||
public async Task<ExamSimulationStateResponse> BuildResultsPreviewAsync(string userId) =>
|
||||
await ToResponseAsync(await BuildSampleAsync(userId, false));
|
||||
|
||||
public Task<ExamSimulation> CreateSampleCompletedAsync(string userId) => BuildSampleAsync(userId, true);
|
||||
|
||||
private async Task<ExamSimulation> BuildSampleAsync(string userId, bool persist)
|
||||
{
|
||||
var bank = (await _questions.GetPublishedAsync()).Where(question => question.Choices.Count >= 5).ToList();
|
||||
if (bank.Count < RequiredQuestions)
|
||||
@@ -130,19 +135,25 @@ public class ExamSimulationStore
|
||||
};
|
||||
}).ToList();
|
||||
var responseMap = responses.ToDictionary(item => item.QuestionExternalId, StringComparer.Ordinal);
|
||||
return new ExamSimulationStateResponse
|
||||
var now = DateTime.UtcNow;
|
||||
var simulation = new ExamSimulation
|
||||
{
|
||||
Id = "preview",
|
||||
UserId = userId,
|
||||
State = ExamSimulationStates.Completed,
|
||||
IsSample = true,
|
||||
CurrentBlock = BlockSizes.Length - 1,
|
||||
BreakSecondsRemaining = 3300,
|
||||
Items = items,
|
||||
Responses = responses,
|
||||
Score = ExamSimulationScoring.Calculate(items, responseMap, questionMap),
|
||||
CreatedUtc = now,
|
||||
UpdatedUtc = now,
|
||||
CompletedUtc = now,
|
||||
BlueprintTargets = targets,
|
||||
BlueprintActual = items.GroupBy(item => item.BlueprintCategory).ToDictionary(group => group.Key, group => group.Count()),
|
||||
ReviewQuestions = items.Select(item => QuestionResponse.FromModelWithChoices(
|
||||
questionMap[item.QuestionExternalId], item.PresentedChoiceIds, item.QuestionElo)).ToList()
|
||||
BlueprintActual = items.GroupBy(item => item.BlueprintCategory).ToDictionary(group => group.Key, group => group.Count())
|
||||
};
|
||||
if (persist) await _simulations.InsertOneAsync(simulation);
|
||||
return simulation;
|
||||
}
|
||||
|
||||
public async Task<ExamSimulation?> FindActiveAsync(string userId) =>
|
||||
@@ -205,6 +216,7 @@ public class ExamSimulationStore
|
||||
{
|
||||
Id = item.Id ?? "",
|
||||
State = item.State,
|
||||
IsSample = item.IsSample,
|
||||
CreatedUtc = item.CreatedUtc,
|
||||
UpdatedUtc = item.UpdatedUtc,
|
||||
CompletedUtc = item.CompletedUtc,
|
||||
|
||||
@@ -8,6 +8,7 @@ signal state_changed
|
||||
signal request_failed(message: String)
|
||||
signal outcome_changed
|
||||
signal history_changed
|
||||
signal sample_created
|
||||
|
||||
var api_url := DEFAULT_API_URL
|
||||
var outcome_api_url := "https://s3q.ranaze.com/api/step3-outcomes"
|
||||
@@ -59,6 +60,10 @@ func load_results_preview() -> void:
|
||||
_send("preview", HTTPClient.METHOD_GET, "/preview-results")
|
||||
|
||||
|
||||
func create_sample_completed() -> void:
|
||||
_send("sample_create", HTTPClient.METHOD_POST, "/sample-completed")
|
||||
|
||||
|
||||
func start_next_block() -> void:
|
||||
if simulation.is_empty():
|
||||
return
|
||||
@@ -177,6 +182,9 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri
|
||||
history_next_before = "" if next_before == null else str(next_before)
|
||||
history_changed.emit()
|
||||
return
|
||||
if action == "sample_create":
|
||||
sample_created.emit()
|
||||
return
|
||||
if action.begins_with("outcome_"):
|
||||
outcome = parsed
|
||||
outcome_changed.emit()
|
||||
|
||||
@@ -7,6 +7,7 @@ var list: VBoxContainer
|
||||
var status_label: Label
|
||||
var load_more_button: Button
|
||||
var pending_simulation_id := ""
|
||||
var sample_button: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -18,6 +19,8 @@ func _ready() -> void:
|
||||
ExamSimulationService.state_changed.connect(_on_simulation_loaded)
|
||||
if not ExamSimulationService.request_failed.is_connected(_on_request_failed):
|
||||
ExamSimulationService.request_failed.connect(_on_request_failed)
|
||||
if not ExamSimulationService.sample_created.is_connected(_on_sample_created):
|
||||
ExamSimulationService.sample_created.connect(_on_sample_created)
|
||||
var top := HBoxContainer.new()
|
||||
add_child(top)
|
||||
var back := Button.new()
|
||||
@@ -38,6 +41,14 @@ func _ready() -> void:
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
Style.set_label(status_label, 16, Style.MUTED)
|
||||
add_child(status_label)
|
||||
sample_button = Button.new()
|
||||
sample_button.text = "Create Sample Completed Simulation"
|
||||
sample_button.tooltip_text = "SUPER only; creates test review data without changing QBank statistics"
|
||||
sample_button.visible = AuthState.role.to_upper() == "SUPER"
|
||||
sample_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
Style.style_chip(sample_button, Style.AMBER)
|
||||
sample_button.pressed.connect(_create_sample)
|
||||
add_child(sample_button)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -95,7 +106,7 @@ func _summary_text(summary: Dictionary) -> String:
|
||||
var state := str(summary.get("state", ""))
|
||||
var date := str(summary.get("createdUtc", "")).left(10)
|
||||
if state == "completed":
|
||||
return "%s | Completed\nPredicted %d (%d-%d) | %d/%d correct" % [date, int(summary.get("predictedScore", 0)), int(summary.get("rangeLow", 0)), int(summary.get("rangeHigh", 0)), int(summary.get("correct", 0)), int(summary.get("total", 0))]
|
||||
return "%s | %sCompleted\nPredicted %d (%d-%d) | %d/%d correct" % [date, "Sample | " if bool(summary.get("isSample", false)) else "", int(summary.get("predictedScore", 0)), int(summary.get("rangeLow", 0)), int(summary.get("rangeHigh", 0)), int(summary.get("correct", 0)), int(summary.get("total", 0))]
|
||||
if state == "block_active":
|
||||
return "%s | Active\nResume Block %d of %d (timer continues)" % [date, int(summary.get("currentBlock", 0)) + 1, int(summary.get("totalBlocks", 12))]
|
||||
if state == "between_blocks":
|
||||
@@ -135,12 +146,28 @@ func _load_more() -> void:
|
||||
ExamSimulationService.load_history(ExamSimulationService.history_next_before)
|
||||
|
||||
|
||||
func _create_sample() -> void:
|
||||
sample_button.disabled = true
|
||||
status_label.text = "Creating sample simulation..."
|
||||
ExamSimulationService.create_sample_completed()
|
||||
|
||||
|
||||
func _on_sample_created() -> void:
|
||||
if not visible:
|
||||
return
|
||||
sample_button.disabled = false
|
||||
status_label.text = "Sample created. Refreshing history..."
|
||||
ExamSimulationService.load_history()
|
||||
|
||||
|
||||
func _on_request_failed(message: String) -> void:
|
||||
if not visible:
|
||||
return
|
||||
pending_simulation_id = ""
|
||||
status_label.text = message
|
||||
load_more_button.disabled = false
|
||||
if sample_button != null:
|
||||
sample_button.disabled = false
|
||||
|
||||
|
||||
static func _action_for_state(state: String) -> String:
|
||||
|
||||
Reference in New Issue
Block a user