Move outcome calibration to profile
This commit is contained in:
@@ -6,6 +6,12 @@ var main: Control
|
|||||||
var profile_text: RichTextLabel
|
var profile_text: RichTextLabel
|
||||||
var reset_qbank_button: Button
|
var reset_qbank_button: Button
|
||||||
var reset_qbank_pending := false
|
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:
|
func _ready() -> void:
|
||||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
@@ -16,16 +22,26 @@ func _ready() -> void:
|
|||||||
Style.set_label(title, 34, Style.CYAN)
|
Style.set_label(title, 34, Style.CYAN)
|
||||||
add_child(title)
|
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()
|
var panel := PanelContainer.new()
|
||||||
panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("405475")))
|
panel.add_theme_stylebox_override("panel", Style.panel_box(Color("172337"), Color("405475")))
|
||||||
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
panel.custom_minimum_size.y = 300
|
||||||
add_child(panel)
|
content.add_child(panel)
|
||||||
|
|
||||||
profile_text = RichTextLabel.new()
|
profile_text = RichTextLabel.new()
|
||||||
profile_text.bbcode_enabled = true
|
profile_text.bbcode_enabled = true
|
||||||
profile_text.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
profile_text.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
panel.add_child(profile_text)
|
panel.add_child(profile_text)
|
||||||
|
|
||||||
|
_build_outcome_panel(content)
|
||||||
|
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
var back := Button.new()
|
var back := Button.new()
|
||||||
back.text = "Back"
|
back.text = "Back"
|
||||||
@@ -54,6 +70,13 @@ func _ready() -> void:
|
|||||||
row.add_child(reset_qbank_button)
|
row.add_child(reset_qbank_button)
|
||||||
add_child(row)
|
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:
|
func on_show(_payload: Variant = null) -> void:
|
||||||
_reset_qbank_confirmation()
|
_reset_qbank_confirmation()
|
||||||
@@ -94,6 +117,114 @@ func on_show(_payload: Variant = null) -> void:
|
|||||||
block.get("total", 0)
|
block.get("total", 0)
|
||||||
]
|
]
|
||||||
profile_text.text = text
|
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:
|
func _on_reset_qbank_pressed() -> void:
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ var main: Control
|
|||||||
var score_label: Label
|
var score_label: Label
|
||||||
var summary_label: Label
|
var summary_label: Label
|
||||||
var category_list: VBoxContainer
|
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 current_simulation := {}
|
||||||
var return_screen := "selection"
|
var return_screen := "selection"
|
||||||
var done_button: Button
|
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}))
|
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()
|
|
||||||
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 = Button.new()
|
||||||
done_button.text = "Return to study modes"
|
done_button.text = "Return to study modes"
|
||||||
done_button.custom_minimum_size = Vector2(240, 50)
|
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))]
|
label.text = "%s: %d/%d" % [category, int(value.get("correct", 0)), int(value.get("total", 0))]
|
||||||
Style.set_label(label, 16, Style.TEXT)
|
Style.set_label(label, 16, Style.TEXT)
|
||||||
category_list.add_child(label)
|
category_list.add_child(label)
|
||||||
ExamSimulationService.load_outcome()
|
|
||||||
|
|
||||||
|
|
||||||
func _return_from_results() -> void:
|
func _return_from_results() -> void:
|
||||||
@@ -136,26 +89,3 @@ func _return_from_results() -> void:
|
|||||||
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()
|
||||||
|
|
||||||
|
|
||||||
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."
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://2lbr8upjlj08
|
||||||
Reference in New Issue
Block a user