Move simulation history into profile
This commit is contained in:
@@ -71,6 +71,28 @@ public sealed class ExamSimulationTests
|
|||||||
Assert.Equal(expected, ExamSimulationStore.BlueprintCategory(new Question { System = system }));
|
Assert.Equal(expected, ExamSimulationStore.BlueprintCategory(new Question { System = system }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HistorySummarySupportsScoredAndUnscoredStates()
|
||||||
|
{
|
||||||
|
var active = ExamSimulationStore.ToHistorySummary(new ExamSimulation
|
||||||
|
{
|
||||||
|
State = ExamSimulationStates.BlockActive,
|
||||||
|
CurrentBlock = 3,
|
||||||
|
Items = BuildItems(20)
|
||||||
|
});
|
||||||
|
var completed = ExamSimulationStore.ToHistorySummary(new ExamSimulation
|
||||||
|
{
|
||||||
|
State = ExamSimulationStates.Completed,
|
||||||
|
Items = BuildItems(20),
|
||||||
|
Score = new ExamSimulationScore { PredictedScore = 221, RangeLow = 204, RangeHigh = 238, Correct = 15, Total = 20 }
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Null(active.PredictedScore);
|
||||||
|
Assert.Equal(3, active.CurrentBlock);
|
||||||
|
Assert.Equal(221, completed.PredictedScore);
|
||||||
|
Assert.Equal(15, completed.Correct);
|
||||||
|
}
|
||||||
|
|
||||||
private static List<ExamSimulationItem> BuildItems(int count, bool wasSeen = false) =>
|
private static List<ExamSimulationItem> BuildItems(int count, bool wasSeen = false) =>
|
||||||
Enumerable.Range(0, count).Select(index => new ExamSimulationItem
|
Enumerable.Range(0, count).Select(index => new ExamSimulationItem
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,6 +53,19 @@ public class ExamSimulationsController(ExamSimulationStore simulations) : Contro
|
|||||||
return Ok(await simulations.GetCompletedAsync(UserId(), limit, cursor));
|
return Ok(await simulations.GetCompletedAsync(UserId(), limit, cursor));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("history")]
|
||||||
|
public async Task<IActionResult> History([FromQuery] int limit = 20, [FromQuery] string? before = null)
|
||||||
|
{
|
||||||
|
DateTime? cursor = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(before))
|
||||||
|
{
|
||||||
|
if (!DateTime.TryParse(before, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed))
|
||||||
|
return BadRequest("The simulation-history cursor is invalid.");
|
||||||
|
cursor = parsed;
|
||||||
|
}
|
||||||
|
return Ok(await simulations.GetHistoryAsync(UserId(), limit, cursor));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<IActionResult> Get(string id)
|
public async Task<IActionResult> Get(string id)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -137,6 +137,28 @@ public class CompletedExamSimulationPage
|
|||||||
public DateTime? NextBefore { get; set; }
|
public DateTime? NextBefore { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ExamSimulationHistorySummary
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string State { get; set; } = "";
|
||||||
|
public DateTime CreatedUtc { get; set; }
|
||||||
|
public DateTime UpdatedUtc { get; set; }
|
||||||
|
public DateTime? CompletedUtc { get; set; }
|
||||||
|
public int CurrentBlock { get; set; }
|
||||||
|
public int TotalBlocks { get; set; } = 12;
|
||||||
|
public int? PredictedScore { get; set; }
|
||||||
|
public int? RangeLow { get; set; }
|
||||||
|
public int? RangeHigh { get; set; }
|
||||||
|
public int? Correct { get; set; }
|
||||||
|
public int Total { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ExamSimulationHistoryPage
|
||||||
|
{
|
||||||
|
public List<ExamSimulationHistorySummary> Items { get; set; } = [];
|
||||||
|
public DateTime? NextBefore { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class Step3Outcome
|
public class Step3Outcome
|
||||||
{
|
{
|
||||||
[BsonId]
|
[BsonId]
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ public class ExamSimulationStore
|
|||||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State)));
|
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State)));
|
||||||
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State).Descending(item => item.CompletedUtc)));
|
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Ascending(item => item.State).Descending(item => item.CompletedUtc)));
|
||||||
|
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||||
|
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UserId).Descending(item => item.CreatedUtc)));
|
||||||
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
_simulations.Indexes.CreateOne(new CreateIndexModel<ExamSimulation>(
|
||||||
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UpdatedUtc)));
|
Builders<ExamSimulation>.IndexKeys.Ascending(item => item.UpdatedUtc)));
|
||||||
_outcomes.Indexes.CreateOne(new CreateIndexModel<Step3Outcome>(
|
_outcomes.Indexes.CreateOne(new CreateIndexModel<Step3Outcome>(
|
||||||
@@ -183,6 +185,37 @@ public class ExamSimulationStore
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ExamSimulationHistoryPage> GetHistoryAsync(string userId, int limit, DateTime? before)
|
||||||
|
{
|
||||||
|
limit = Math.Clamp(limit, 1, 50);
|
||||||
|
var filter = Builders<ExamSimulation>.Filter.Eq(item => item.UserId, userId);
|
||||||
|
if (before is not null)
|
||||||
|
filter &= Builders<ExamSimulation>.Filter.Lt(item => item.CreatedUtc, before.Value);
|
||||||
|
var simulations = await _simulations.Find(filter).SortByDescending(item => item.CreatedUtc).Limit(limit + 1).ToListAsync();
|
||||||
|
var hasMore = simulations.Count > limit;
|
||||||
|
if (hasMore) simulations.RemoveAt(simulations.Count - 1);
|
||||||
|
return new ExamSimulationHistoryPage
|
||||||
|
{
|
||||||
|
Items = simulations.Select(ToHistorySummary).ToList(),
|
||||||
|
NextBefore = hasMore && simulations.Count > 0 ? simulations[^1].CreatedUtc : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExamSimulationHistorySummary ToHistorySummary(ExamSimulation item) => new()
|
||||||
|
{
|
||||||
|
Id = item.Id ?? "",
|
||||||
|
State = item.State,
|
||||||
|
CreatedUtc = item.CreatedUtc,
|
||||||
|
UpdatedUtc = item.UpdatedUtc,
|
||||||
|
CompletedUtc = item.CompletedUtc,
|
||||||
|
CurrentBlock = item.CurrentBlock,
|
||||||
|
PredictedScore = item.Score?.PredictedScore,
|
||||||
|
RangeLow = item.Score?.RangeLow,
|
||||||
|
RangeHigh = item.Score?.RangeHigh,
|
||||||
|
Correct = item.Score?.Correct,
|
||||||
|
Total = item.Score?.Total ?? item.Items.Count
|
||||||
|
};
|
||||||
|
|
||||||
public async Task<ExamSimulation> StartNextBlockAsync(ExamSimulation simulation)
|
public async Task<ExamSimulation> StartNextBlockAsync(ExamSimulation simulation)
|
||||||
{
|
{
|
||||||
if (simulation.State != ExamSimulationStates.BetweenBlocks)
|
if (simulation.State != ExamSimulationStates.BetweenBlocks)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ signal readiness_changed
|
|||||||
signal state_changed
|
signal state_changed
|
||||||
signal request_failed(message: String)
|
signal request_failed(message: String)
|
||||||
signal outcome_changed
|
signal outcome_changed
|
||||||
signal completed_history_changed
|
signal history_changed
|
||||||
|
|
||||||
var api_url := DEFAULT_API_URL
|
var api_url := DEFAULT_API_URL
|
||||||
var outcome_api_url := "https://s3q.ranaze.com/api/step3-outcomes"
|
var outcome_api_url := "https://s3q.ranaze.com/api/step3-outcomes"
|
||||||
@@ -15,8 +15,8 @@ var request: HTTPRequest
|
|||||||
var readiness := {}
|
var readiness := {}
|
||||||
var simulation := {}
|
var simulation := {}
|
||||||
var outcome := {}
|
var outcome := {}
|
||||||
var completed_history: Array = []
|
var history: Array = []
|
||||||
var completed_history_next_before := ""
|
var history_next_before := ""
|
||||||
var is_loading := false
|
var is_loading := false
|
||||||
var pending_action := ""
|
var pending_action := ""
|
||||||
var request_queue: Array = []
|
var request_queue: Array = []
|
||||||
@@ -40,8 +40,8 @@ func load_active() -> void:
|
|||||||
_send("active", HTTPClient.METHOD_GET, "/active")
|
_send("active", HTTPClient.METHOD_GET, "/active")
|
||||||
|
|
||||||
|
|
||||||
func load_completed_history(before: String = "") -> void:
|
func load_history(before: String = "") -> void:
|
||||||
var suffix := "/completed?limit=20"
|
var suffix := "/history?limit=20"
|
||||||
if not before.is_empty():
|
if not before.is_empty():
|
||||||
suffix += "&before=%s" % before.uri_encode()
|
suffix += "&before=%s" % before.uri_encode()
|
||||||
_send("history_append" if not before.is_empty() else "history", HTTPClient.METHOD_GET, suffix)
|
_send("history_append" if not before.is_empty() else "history", HTTPClient.METHOD_GET, suffix)
|
||||||
@@ -172,10 +172,10 @@ func _on_request_completed(result: int, response_code: int, _headers: PackedStri
|
|||||||
return
|
return
|
||||||
if action == "history" or action == "history_append":
|
if action == "history" or action == "history_append":
|
||||||
var items: Array = parsed.get("items", [])
|
var items: Array = parsed.get("items", [])
|
||||||
completed_history = completed_history + items if action == "history_append" else items
|
history = history + items if action == "history_append" else items
|
||||||
var next_before: Variant = parsed.get("nextBefore", null)
|
var next_before: Variant = parsed.get("nextBefore", null)
|
||||||
completed_history_next_before = "" if next_before == null else str(next_before)
|
history_next_before = "" if next_before == null else str(next_before)
|
||||||
completed_history_changed.emit()
|
history_changed.emit()
|
||||||
return
|
return
|
||||||
if action.begins_with("outcome_"):
|
if action.begins_with("outcome_"):
|
||||||
outcome = parsed
|
outcome = parsed
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ func _build_ui() -> void:
|
|||||||
screens_container.add_child(simulation_review)
|
screens_container.add_child(simulation_review)
|
||||||
screens["simulation_review"] = simulation_review
|
screens["simulation_review"] = simulation_review
|
||||||
|
|
||||||
|
var simulation_history := preload("res://scripts/ui/simulation_history.gd").new()
|
||||||
|
simulation_history.name = "SimulationHistory"
|
||||||
|
simulation_history.main = self
|
||||||
|
screens_container.add_child(simulation_history)
|
||||||
|
screens["simulation_history"] = simulation_history
|
||||||
|
|
||||||
var profile := preload("res://scripts/ui/progress_profile.gd").new()
|
var profile := preload("res://scripts/ui/progress_profile.gd").new()
|
||||||
profile.name = "ProgressProfile"
|
profile.name = "ProgressProfile"
|
||||||
profile.main = self
|
profile.main = self
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ func _ready() -> void:
|
|||||||
)
|
)
|
||||||
row.add_child(reset)
|
row.add_child(reset)
|
||||||
|
|
||||||
|
var simulations := Button.new()
|
||||||
|
simulations.text = "Simulation History"
|
||||||
|
Style.style_chip(simulations, Style.CYAN)
|
||||||
|
simulations.pressed.connect(func(): main.show_screen("simulation_history"))
|
||||||
|
row.add_child(simulations)
|
||||||
|
|
||||||
reset_qbank_button = Button.new()
|
reset_qbank_button = Button.new()
|
||||||
reset_qbank_button.text = "Reset QBank"
|
reset_qbank_button.text = "Reset QBank"
|
||||||
Style.style_chip(reset_qbank_button, Style.AMBER)
|
Style.style_chip(reset_qbank_button, Style.AMBER)
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
extends VBoxContainer
|
||||||
|
|
||||||
|
const Style := preload("res://scripts/ui/ui_style.gd")
|
||||||
|
|
||||||
|
var main: Control
|
||||||
|
var list: VBoxContainer
|
||||||
|
var status_label: Label
|
||||||
|
var load_more_button: Button
|
||||||
|
var pending_simulation_id := ""
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_theme_constant_override("separation", 12)
|
||||||
|
if not ExamSimulationService.history_changed.is_connected(_render_history):
|
||||||
|
ExamSimulationService.history_changed.connect(_render_history)
|
||||||
|
if not ExamSimulationService.state_changed.is_connected(_on_simulation_loaded):
|
||||||
|
ExamSimulationService.state_changed.connect(_on_simulation_loaded)
|
||||||
|
if not ExamSimulationService.request_failed.is_connected(_on_request_failed):
|
||||||
|
ExamSimulationService.request_failed.connect(_on_request_failed)
|
||||||
|
var top := HBoxContainer.new()
|
||||||
|
add_child(top)
|
||||||
|
var back := Button.new()
|
||||||
|
back.text = "< Profile"
|
||||||
|
Style.style_chip(back, Style.CYAN)
|
||||||
|
back.pressed.connect(func(): main.show_screen("profile"))
|
||||||
|
top.add_child(back)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "Simulation History"
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
Style.set_label(title, 30, Style.TEXT)
|
||||||
|
top.add_child(title)
|
||||||
|
var balance := Control.new()
|
||||||
|
balance.custom_minimum_size = Vector2(100, 0)
|
||||||
|
top.add_child(balance)
|
||||||
|
status_label = Label.new()
|
||||||
|
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
Style.set_label(status_label, 16, Style.MUTED)
|
||||||
|
add_child(status_label)
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
add_child(scroll)
|
||||||
|
list = VBoxContainer.new()
|
||||||
|
list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
list.add_theme_constant_override("separation", 8)
|
||||||
|
scroll.add_child(list)
|
||||||
|
load_more_button = Button.new()
|
||||||
|
load_more_button.text = "Load More"
|
||||||
|
load_more_button.visible = false
|
||||||
|
load_more_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
|
Style.style_chip(load_more_button, Style.CYAN)
|
||||||
|
load_more_button.pressed.connect(_load_more)
|
||||||
|
add_child(load_more_button)
|
||||||
|
|
||||||
|
|
||||||
|
func on_show(payload: Variant = null) -> void:
|
||||||
|
pending_simulation_id = ""
|
||||||
|
if str(payload) == "preserve" and not ExamSimulationService.history.is_empty():
|
||||||
|
return
|
||||||
|
status_label.text = "Loading simulation history..."
|
||||||
|
load_more_button.disabled = true
|
||||||
|
ExamSimulationService.load_history()
|
||||||
|
|
||||||
|
|
||||||
|
func _render_history() -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
for child in list.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
if ExamSimulationService.history.is_empty():
|
||||||
|
status_label.text = "No simulations yet."
|
||||||
|
else:
|
||||||
|
status_label.text = "Select a completed simulation to review it, or resume an active simulation."
|
||||||
|
for raw_summary in ExamSimulationService.history:
|
||||||
|
var summary: Dictionary = raw_summary
|
||||||
|
var state := str(summary.get("state", ""))
|
||||||
|
var action := _action_for_state(state)
|
||||||
|
var button := Button.new()
|
||||||
|
button.text = _summary_text(summary)
|
||||||
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
button.custom_minimum_size = Vector2(0, 70)
|
||||||
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
button.disabled = action == "none"
|
||||||
|
Style.style_chip(button, _state_color(state))
|
||||||
|
if action != "none":
|
||||||
|
button.pressed.connect(_open_simulation.bind(str(summary.get("id", ""))))
|
||||||
|
list.add_child(button)
|
||||||
|
load_more_button.visible = not ExamSimulationService.history_next_before.is_empty()
|
||||||
|
load_more_button.disabled = false
|
||||||
|
|
||||||
|
|
||||||
|
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))]
|
||||||
|
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":
|
||||||
|
return "%s | In progress\n%d of %d blocks complete" % [date, int(summary.get("currentBlock", -1)) + 1, int(summary.get("totalBlocks", 12))]
|
||||||
|
return "%s | Abandoned\nThis simulation cannot be resumed." % date
|
||||||
|
|
||||||
|
|
||||||
|
func _open_simulation(simulation_id: String) -> void:
|
||||||
|
pending_simulation_id = simulation_id
|
||||||
|
status_label.text = "Opening simulation..."
|
||||||
|
ExamSimulationService.load_simulation(simulation_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_simulation_loaded() -> void:
|
||||||
|
if not visible or pending_simulation_id.is_empty() or ExamSimulationService.simulation.is_empty():
|
||||||
|
return
|
||||||
|
pending_simulation_id = ""
|
||||||
|
var state := str(ExamSimulationService.simulation.get("state", ""))
|
||||||
|
if state == "completed":
|
||||||
|
main.show_screen("simulation_results", {"simulation": ExamSimulationService.simulation, "return_screen": "simulation_history"})
|
||||||
|
elif state == "block_active":
|
||||||
|
BlockSessionService.start_simulation_block(
|
||||||
|
ExamSimulationService.get_current_questions(),
|
||||||
|
int(ExamSimulationService.simulation.get("currentBlock", 0)),
|
||||||
|
int(ExamSimulationService.simulation.get("totalBlocks", 12)),
|
||||||
|
ExamSimulationService.get_block_deadline_unix())
|
||||||
|
BlockSessionService.restore_simulation_responses(ExamSimulationService.simulation.get("responses", []))
|
||||||
|
main.show_screen("player")
|
||||||
|
elif state == "between_blocks":
|
||||||
|
main.show_screen("simulation_break")
|
||||||
|
else:
|
||||||
|
status_label.text = "This simulation can no longer be resumed."
|
||||||
|
|
||||||
|
|
||||||
|
func _load_more() -> void:
|
||||||
|
load_more_button.disabled = true
|
||||||
|
ExamSimulationService.load_history(ExamSimulationService.history_next_before)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_request_failed(message: String) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
pending_simulation_id = ""
|
||||||
|
status_label.text = message
|
||||||
|
load_more_button.disabled = false
|
||||||
|
|
||||||
|
|
||||||
|
static func _action_for_state(state: String) -> String:
|
||||||
|
if state == "completed": return "results"
|
||||||
|
if state == "block_active" or state == "between_blocks": return "resume"
|
||||||
|
return "none"
|
||||||
|
|
||||||
|
|
||||||
|
static func _state_color(state: String) -> Color:
|
||||||
|
if state == "completed": return Style.LIME
|
||||||
|
if state == "block_active" or state == "between_blocks": return Style.CYAN
|
||||||
|
return Style.MUTED
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dg3a6hdg4w2pd
|
||||||
@@ -12,6 +12,8 @@ var outcome_consent: CheckBox
|
|||||||
var outcome_status: Label
|
var outcome_status: Label
|
||||||
var outcome_delete_button: Button
|
var outcome_delete_button: Button
|
||||||
var current_simulation := {}
|
var current_simulation := {}
|
||||||
|
var return_screen := "selection"
|
||||||
|
var done_button: Button
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -47,7 +49,7 @@ func _ready() -> void:
|
|||||||
review_button.custom_minimum_size = Vector2(240, 50)
|
review_button.custom_minimum_size = Vector2(240, 50)
|
||||||
review_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
review_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
Style.style_primary_button(review_button)
|
Style.style_primary_button(review_button)
|
||||||
review_button.pressed.connect(func(): main.show_screen("simulation_review", current_simulation))
|
review_button.pressed.connect(func(): main.show_screen("simulation_review", {"simulation": current_simulation, "return_screen": return_screen}))
|
||||||
content.add_child(review_button)
|
content.add_child(review_button)
|
||||||
|
|
||||||
var outcome_heading := Label.new()
|
var outcome_heading := Label.new()
|
||||||
@@ -91,17 +93,20 @@ func _ready() -> void:
|
|||||||
if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome):
|
if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome):
|
||||||
ExamSimulationService.outcome_changed.connect(_refresh_outcome)
|
ExamSimulationService.outcome_changed.connect(_refresh_outcome)
|
||||||
|
|
||||||
var done := Button.new()
|
done_button = Button.new()
|
||||||
done.text = "Return to study modes"
|
done_button.text = "Return to study modes"
|
||||||
done.custom_minimum_size = Vector2(240, 50)
|
done_button.custom_minimum_size = Vector2(240, 50)
|
||||||
done.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
done_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
Style.style_primary_button(done)
|
Style.style_primary_button(done_button)
|
||||||
done.pressed.connect(func(): main.show_screen("selection", "step3"))
|
done_button.pressed.connect(_return_from_results)
|
||||||
add_child(done)
|
add_child(done_button)
|
||||||
|
|
||||||
|
|
||||||
func on_show(payload: Variant = null) -> void:
|
func on_show(payload: Variant = null) -> void:
|
||||||
var simulation: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else ExamSimulationService.simulation
|
var wrapper: Dictionary = payload if typeof(payload) == TYPE_DICTIONARY else {}
|
||||||
|
var simulation: Dictionary = wrapper.get("simulation", wrapper) if not wrapper.is_empty() else ExamSimulationService.simulation
|
||||||
|
return_screen = str(wrapper.get("return_screen", "selection"))
|
||||||
|
done_button.text = "Return to simulation history" if return_screen == "simulation_history" else "Return to study modes"
|
||||||
current_simulation = simulation
|
current_simulation = simulation
|
||||||
var score: Dictionary = simulation.get("score", {})
|
var score: Dictionary = simulation.get("score", {})
|
||||||
var predicted := int(score.get("predictedScore", 0))
|
var predicted := int(score.get("predictedScore", 0))
|
||||||
@@ -121,6 +126,13 @@ func on_show(payload: Variant = null) -> void:
|
|||||||
ExamSimulationService.load_outcome()
|
ExamSimulationService.load_outcome()
|
||||||
|
|
||||||
|
|
||||||
|
func _return_from_results() -> void:
|
||||||
|
if return_screen == "simulation_history":
|
||||||
|
main.show_screen("simulation_history", "preserve")
|
||||||
|
else:
|
||||||
|
main.show_screen("selection", "step3")
|
||||||
|
|
||||||
|
|
||||||
func _clear(container: Control) -> void:
|
func _clear(container: Control) -> void:
|
||||||
for child in container.get_children():
|
for child in container.get_children():
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var empty_label: Label
|
|||||||
var media_requests: Array[HTTPRequest] = []
|
var media_requests: Array[HTTPRequest] = []
|
||||||
var image_overlay: Control
|
var image_overlay: Control
|
||||||
var image_texture: TextureRect
|
var image_texture: TextureRect
|
||||||
|
var return_screen := "selection"
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -29,7 +30,7 @@ func _ready() -> void:
|
|||||||
var back := Button.new()
|
var back := Button.new()
|
||||||
back.text = "< Results"
|
back.text = "< Results"
|
||||||
Style.style_chip(back, Style.CYAN)
|
Style.style_chip(back, Style.CYAN)
|
||||||
back.pressed.connect(func(): main.show_screen("simulation_results", simulation))
|
back.pressed.connect(func(): main.show_screen("simulation_results", {"simulation": simulation, "return_screen": return_screen}))
|
||||||
toolbar.add_child(back)
|
toolbar.add_child(back)
|
||||||
progress_label = Label.new()
|
progress_label = Label.new()
|
||||||
progress_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
progress_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
@@ -90,7 +91,9 @@ func _ready() -> void:
|
|||||||
|
|
||||||
func on_show(payload: Variant = null) -> void:
|
func on_show(payload: Variant = null) -> void:
|
||||||
if typeof(payload) == TYPE_DICTIONARY:
|
if typeof(payload) == TYPE_DICTIONARY:
|
||||||
simulation = payload
|
var wrapper: Dictionary = payload
|
||||||
|
simulation = wrapper.get("simulation", wrapper)
|
||||||
|
return_screen = str(wrapper.get("return_screen", "selection"))
|
||||||
questions = simulation.get("reviewQuestions", [])
|
questions = simulation.get("reviewQuestions", [])
|
||||||
responses.clear()
|
responses.clear()
|
||||||
for response in simulation.get("responses", []):
|
for response in simulation.get("responses", []):
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ 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 simulation_preview_link: LinkButton
|
||||||
var completed_history_list: VBoxContainer
|
|
||||||
var completed_history_more: Button
|
|
||||||
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
|
||||||
@@ -37,8 +35,6 @@ func _ready() -> void:
|
|||||||
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):
|
if not AuthState.auth_changed.is_connected(_refresh_simulation_preview_visibility):
|
||||||
AuthState.auth_changed.connect(_refresh_simulation_preview_visibility)
|
AuthState.auth_changed.connect(_refresh_simulation_preview_visibility)
|
||||||
if not ExamSimulationService.completed_history_changed.is_connected(_refresh_completed_history):
|
|
||||||
ExamSimulationService.completed_history_changed.connect(_refresh_completed_history)
|
|
||||||
|
|
||||||
_build_exam_view()
|
_build_exam_view()
|
||||||
_build_mode_view()
|
_build_mode_view()
|
||||||
@@ -53,7 +49,6 @@ func on_show(payload: Variant = null) -> void:
|
|||||||
_refresh_bank_state()
|
_refresh_bank_state()
|
||||||
_refresh_simulation_preview_visibility()
|
_refresh_simulation_preview_visibility()
|
||||||
ExamSimulationService.load_readiness()
|
ExamSimulationService.load_readiness()
|
||||||
ExamSimulationService.load_completed_history()
|
|
||||||
|
|
||||||
|
|
||||||
func on_hide() -> void:
|
func on_hide() -> void:
|
||||||
@@ -179,26 +174,6 @@ func _build_mode_view() -> void:
|
|||||||
mode_view.add_child(simulation_preview_link)
|
mode_view.add_child(simulation_preview_link)
|
||||||
_refresh_simulation_preview_visibility()
|
_refresh_simulation_preview_visibility()
|
||||||
|
|
||||||
var history_heading := Label.new()
|
|
||||||
history_heading.text = "Completed simulations"
|
|
||||||
Style.set_label(history_heading, 20, Style.TEXT)
|
|
||||||
mode_view.add_child(history_heading)
|
|
||||||
completed_history_list = VBoxContainer.new()
|
|
||||||
completed_history_list.add_theme_constant_override("separation", 6)
|
|
||||||
completed_history_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
var history_scroll := ScrollContainer.new()
|
|
||||||
history_scroll.custom_minimum_size = Vector2(0, 150)
|
|
||||||
history_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
history_scroll.add_child(completed_history_list)
|
|
||||||
mode_view.add_child(history_scroll)
|
|
||||||
completed_history_more = Button.new()
|
|
||||||
completed_history_more.text = "Load more"
|
|
||||||
completed_history_more.visible = false
|
|
||||||
completed_history_more.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
|
||||||
Style.style_chip(completed_history_more, Style.CYAN)
|
|
||||||
completed_history_more.pressed.connect(func(): ExamSimulationService.load_completed_history(ExamSimulationService.completed_history_next_before))
|
|
||||||
mode_view.add_child(completed_history_more)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -294,30 +269,6 @@ func _refresh_simulation_preview_visibility() -> void:
|
|||||||
simulation_preview_link.visible = false
|
simulation_preview_link.visible = false
|
||||||
|
|
||||||
|
|
||||||
func _refresh_completed_history() -> void:
|
|
||||||
if completed_history_list == null:
|
|
||||||
return
|
|
||||||
for child in completed_history_list.get_children():
|
|
||||||
child.queue_free()
|
|
||||||
for raw_summary in ExamSimulationService.completed_history:
|
|
||||||
var summary: Dictionary = raw_summary
|
|
||||||
var button := Button.new()
|
|
||||||
button.text = "%s | Predicted %d (%d-%d) | %d/%d correct" % [
|
|
||||||
str(summary.get("completedUtc", "")).left(10), 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))]
|
|
||||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
||||||
Style.style_chip(button, Style.CYAN)
|
|
||||||
button.pressed.connect(_open_completed_simulation.bind(str(summary.get("id", ""))))
|
|
||||||
completed_history_list.add_child(button)
|
|
||||||
completed_history_more.visible = not ExamSimulationService.completed_history_next_before.is_empty()
|
|
||||||
|
|
||||||
|
|
||||||
func _open_completed_simulation(simulation_id: String) -> void:
|
|
||||||
pending_simulation_launch = true
|
|
||||||
ExamSimulationService.load_simulation(simulation_id)
|
|
||||||
|
|
||||||
|
|
||||||
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():
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var history = load("res://scripts/ui/simulation_history.gd")
|
||||||
|
_assert(history._action_for_state("completed") == "results", "completed simulations should open results")
|
||||||
|
_assert(history._action_for_state("block_active") == "resume", "active blocks should resume")
|
||||||
|
_assert(history._action_for_state("between_blocks") == "resume", "between-block simulations should resume")
|
||||||
|
_assert(history._action_for_state("abandoned") == "none", "abandoned simulations should be read-only")
|
||||||
|
quit(1 if failures > 0 else 0)
|
||||||
|
|
||||||
|
|
||||||
|
func _assert(condition: bool, message: String) -> void:
|
||||||
|
if condition:
|
||||||
|
return
|
||||||
|
failures += 1
|
||||||
|
push_error(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://babu2x7xudmxc
|
||||||
Reference in New Issue
Block a user