From d73fd5f2cfdeb92534d74d278663088a7da22a2d Mon Sep 17 00:00:00 2001 From: hz Date: Tue, 14 Jul 2026 22:14:38 -0500 Subject: [PATCH] Move outcome calibration to profile --- scripts/ui/progress_profile.gd | 135 +++++++++++++++++++++++++++++- scripts/ui/simulation_results.gd | 70 ---------------- tests/outcome_profile_test.gd | 16 ++++ tests/outcome_profile_test.gd.uid | 1 + 4 files changed, 150 insertions(+), 72 deletions(-) create mode 100644 tests/outcome_profile_test.gd create mode 100644 tests/outcome_profile_test.gd.uid diff --git a/scripts/ui/progress_profile.gd b/scripts/ui/progress_profile.gd index 5c0159e..705b576 100644 --- a/scripts/ui/progress_profile.gd +++ b/scripts/ui/progress_profile.gd @@ -6,6 +6,12 @@ var main: Control var profile_text: RichTextLabel var reset_qbank_button: Button var reset_qbank_pending := false +var outcome_score: SpinBox +var outcome_date: LineEdit +var outcome_consent: CheckBox +var outcome_status: Label +var outcome_delete_button: Button +var outcome_grid: GridContainer func _ready() -> void: set_anchors_preset(Control.PRESET_FULL_RECT) @@ -16,16 +22,26 @@ func _ready() -> void: Style.set_label(title, 34, Style.CYAN) add_child(title) + var scroll := ScrollContainer.new() + scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + add_child(scroll) + var content := VBoxContainer.new() + content.size_flags_horizontal = Control.SIZE_EXPAND_FILL + content.add_theme_constant_override("separation", 14) + scroll.add_child(content) + var panel := PanelContainer.new() panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("405475"))) - panel.size_flags_vertical = Control.SIZE_EXPAND_FILL - add_child(panel) + panel.custom_minimum_size.y = 300 + content.add_child(panel) profile_text = RichTextLabel.new() profile_text.bbcode_enabled = true profile_text.size_flags_vertical = Control.SIZE_EXPAND_FILL panel.add_child(profile_text) + _build_outcome_panel(content) + var row := HBoxContainer.new() var back := Button.new() back.text = "Back" @@ -54,6 +70,13 @@ func _ready() -> void: row.add_child(reset_qbank_button) add_child(row) + if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome): + ExamSimulationService.outcome_changed.connect(_refresh_outcome) + if not ExamSimulationService.request_failed.is_connected(_on_outcome_request_failed): + ExamSimulationService.request_failed.connect(_on_outcome_request_failed) + resized.connect(_refresh_outcome_layout) + _refresh_outcome_layout() + func on_show(_payload: Variant = null) -> void: _reset_qbank_confirmation() @@ -94,6 +117,114 @@ func on_show(_payload: Variant = null) -> void: block.get("total", 0) ] profile_text.text = text + outcome_status.text = "Loading official outcome..." + ExamSimulationService.load_outcome() + + +func _build_outcome_panel(parent: VBoxContainer) -> void: + var panel := PanelContainer.new() + panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("405475"))) + parent.add_child(panel) + var section := VBoxContainer.new() + section.add_theme_constant_override("separation", 10) + panel.add_child(section) + var heading := Label.new() + heading.text = "Official Step 3 Outcome" + Style.set_label(heading, 22, Style.TEXT) + section.add_child(heading) + var note := Label.new() + note.text = "After receiving your official result, you may voluntarily submit it. You can edit or delete it at any time; calibration uses de-identified aggregates." + note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + Style.set_label(note, 15, Style.MUTED) + section.add_child(note) + outcome_grid = GridContainer.new() + outcome_grid.add_theme_constant_override("h_separation", 10) + outcome_grid.add_theme_constant_override("v_separation", 10) + section.add_child(outcome_grid) + outcome_score = SpinBox.new() + outcome_score.min_value = 1 + outcome_score.max_value = 300 + outcome_score.value = 200 + outcome_score.custom_minimum_size = Vector2(120, 44) + outcome_score.size_flags_horizontal = Control.SIZE_EXPAND_FILL + outcome_grid.add_child(outcome_score) + outcome_date = LineEdit.new() + outcome_date.placeholder_text = "Exam date: YYYY-MM-DD" + outcome_date.custom_minimum_size = Vector2(220, 44) + outcome_date.size_flags_horizontal = Control.SIZE_EXPAND_FILL + outcome_grid.add_child(outcome_date) + outcome_consent = CheckBox.new() + outcome_consent.text = "I consent to calibration use" + outcome_consent.size_flags_horizontal = Control.SIZE_EXPAND_FILL + outcome_grid.add_child(outcome_consent) + var actions := HBoxContainer.new() + actions.add_theme_constant_override("separation", 8) + outcome_grid.add_child(actions) + var save_button := Button.new() + save_button.text = "Save outcome" + Style.style_chip(save_button, Style.CYAN) + save_button.pressed.connect(_save_outcome) + actions.add_child(save_button) + outcome_delete_button = Button.new() + outcome_delete_button.text = "Delete outcome" + Style.style_chip(outcome_delete_button, Style.RED) + outcome_delete_button.pressed.connect(_delete_outcome) + actions.add_child(outcome_delete_button) + outcome_status = Label.new() + outcome_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + Style.set_label(outcome_status, 15, Style.MUTED) + section.add_child(outcome_status) + + +func _refresh_outcome_layout() -> void: + if outcome_grid != null: + outcome_grid.columns = 1 if size.x < 900.0 else 4 + + +static func validate_outcome(score: int, exam_date: String, consent: bool) -> String: + if score < 1 or score > 300: + return "Enter an official score from 1 through 300." + if not consent: + return "Consent is required before saving an outcome." + var date_pattern := RegEx.new() + date_pattern.compile("^\\d{4}-\\d{2}-\\d{2}$") + if date_pattern.search(exam_date) == null: + return "Enter the exam date as YYYY-MM-DD." + return "" + + +func _save_outcome() -> void: + var validation_error := validate_outcome(int(outcome_score.value), outcome_date.text.strip_edges(), outcome_consent.button_pressed) + if not validation_error.is_empty(): + outcome_status.text = validation_error + return + outcome_status.text = "Saving..." + ExamSimulationService.save_outcome(int(outcome_score.value), outcome_date.text.strip_edges()) + + +func _delete_outcome() -> void: + outcome_status.text = "Deleting..." + ExamSimulationService.delete_outcome() + + +func _refresh_outcome() -> void: + if ExamSimulationService.outcome.is_empty(): + outcome_score.value = 200 + outcome_date.text = "" + outcome_consent.button_pressed = false + outcome_delete_button.visible = false + outcome_status.text = "No official outcome is stored." + return + outcome_score.value = int(ExamSimulationService.outcome.get("officialScore", 200)) + outcome_date.text = str(ExamSimulationService.outcome.get("examDate", "")).left(10) + outcome_consent.button_pressed = true + outcome_delete_button.visible = true + outcome_status.text = "Official outcome saved." + + +func _on_outcome_request_failed(message: String) -> void: + if visible: + outcome_status.text = message func _on_reset_qbank_pressed() -> void: diff --git a/scripts/ui/simulation_results.gd b/scripts/ui/simulation_results.gd index 3f26311..446b804 100644 --- a/scripts/ui/simulation_results.gd +++ b/scripts/ui/simulation_results.gd @@ -6,11 +6,6 @@ var main: Control var score_label: Label var summary_label: Label var category_list: VBoxContainer -var outcome_score: SpinBox -var outcome_date: LineEdit -var outcome_consent: CheckBox -var outcome_status: Label -var outcome_delete_button: Button var current_simulation := {} var return_screen := "selection" var done_button: Button @@ -52,47 +47,6 @@ func _ready() -> void: review_button.pressed.connect(func(): main.show_screen("simulation_review", {"simulation": current_simulation, "return_screen": return_screen})) content.add_child(review_button) - var outcome_heading := Label.new() - outcome_heading.text = "Help calibrate future predictions" - Style.set_label(outcome_heading, 22, Style.TEXT) - content.add_child(outcome_heading) - var outcome_note := Label.new() - outcome_note.text = "After receiving your official result, you may voluntarily submit it. You can edit or delete it at any time; calibration uses de-identified aggregates." - outcome_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - Style.set_label(outcome_note, 15, Style.MUTED) - content.add_child(outcome_note) - var outcome_row := HBoxContainer.new() - outcome_row.add_theme_constant_override("separation", 10) - content.add_child(outcome_row) - outcome_score = SpinBox.new() - outcome_score.min_value = 1 - outcome_score.max_value = 300 - outcome_score.value = 200 - outcome_score.custom_minimum_size = Vector2(120, 44) - outcome_row.add_child(outcome_score) - outcome_date = LineEdit.new() - outcome_date.placeholder_text = "Exam date: YYYY-MM-DD" - outcome_date.custom_minimum_size = Vector2(220, 44) - outcome_row.add_child(outcome_date) - outcome_consent = CheckBox.new() - outcome_consent.text = "I consent to calibration use" - outcome_row.add_child(outcome_consent) - var save_outcome := Button.new() - save_outcome.text = "Save outcome" - Style.style_chip(save_outcome, Style.CYAN) - save_outcome.pressed.connect(_save_outcome) - outcome_row.add_child(save_outcome) - outcome_delete_button = Button.new() - outcome_delete_button.text = "Delete outcome" - Style.style_chip(outcome_delete_button, Style.RED) - outcome_delete_button.pressed.connect(ExamSimulationService.delete_outcome) - outcome_row.add_child(outcome_delete_button) - outcome_status = Label.new() - Style.set_label(outcome_status, 15, Style.MUTED) - content.add_child(outcome_status) - if not ExamSimulationService.outcome_changed.is_connected(_refresh_outcome): - ExamSimulationService.outcome_changed.connect(_refresh_outcome) - done_button = Button.new() done_button.text = "Return to study modes" done_button.custom_minimum_size = Vector2(240, 50) @@ -123,7 +77,6 @@ func on_show(payload: Variant = null) -> void: label.text = "%s: %d/%d" % [category, int(value.get("correct", 0)), int(value.get("total", 0))] Style.set_label(label, 16, Style.TEXT) category_list.add_child(label) - ExamSimulationService.load_outcome() func _return_from_results() -> void: @@ -136,26 +89,3 @@ func _return_from_results() -> void: func _clear(container: Control) -> void: for child in container.get_children(): child.queue_free() - - -func _save_outcome() -> void: - if not outcome_consent.button_pressed: - outcome_status.text = "Consent is required before saving an outcome." - return - if outcome_date.text.length() != 10: - outcome_status.text = "Enter the exam date as YYYY-MM-DD." - return - ExamSimulationService.save_outcome(int(outcome_score.value), outcome_date.text) - outcome_status.text = "Saving..." - - -func _refresh_outcome() -> void: - if ExamSimulationService.outcome.is_empty(): - outcome_status.text = "No official outcome is stored." - outcome_delete_button.visible = false - return - outcome_score.value = int(ExamSimulationService.outcome.get("officialScore", 200)) - outcome_date.text = str(ExamSimulationService.outcome.get("examDate", "")).left(10) - outcome_consent.button_pressed = true - outcome_delete_button.visible = true - outcome_status.text = "Official outcome saved." diff --git a/tests/outcome_profile_test.gd b/tests/outcome_profile_test.gd new file mode 100644 index 0000000..853b92a --- /dev/null +++ b/tests/outcome_profile_test.gd @@ -0,0 +1,16 @@ +extends SceneTree + +func _initialize() -> void: + var ProgressProfile = load("res://scripts/ui/progress_profile.gd") + _assert_equal(ProgressProfile.validate_outcome(236, "2026-07-14", true), "", "valid outcome") + _assert_equal(ProgressProfile.validate_outcome(236, "2026-07-14", false), "Consent is required before saving an outcome.", "consent") + _assert_equal(ProgressProfile.validate_outcome(236, "07/14/2026", true), "Enter the exam date as YYYY-MM-DD.", "date format") + _assert_equal(ProgressProfile.validate_outcome(301, "2026-07-14", true), "Enter an official score from 1 through 300.", "score range") + print("outcome_profile_test: PASS") + quit(0) + + +func _assert_equal(actual: Variant, expected: Variant, label: String) -> void: + if actual != expected: + push_error("%s: expected %s, got %s" % [label, expected, actual]) + quit(1) diff --git a/tests/outcome_profile_test.gd.uid b/tests/outcome_profile_test.gd.uid new file mode 100644 index 0000000..92536d2 --- /dev/null +++ b/tests/outcome_profile_test.gd.uid @@ -0,0 +1 @@ +uid://2lbr8upjlj08