223 lines
7.4 KiB
GDScript
223 lines
7.4 KiB
GDScript
extends Node
|
|
|
|
const EndpointConfig := preload("res://scripts/autoload/api_endpoint_config.gd")
|
|
const DEFAULT_API_URL := "https://us3q.ranaze.com/api/exam-simulations"
|
|
|
|
signal readiness_changed
|
|
signal state_changed
|
|
signal request_failed(message: String)
|
|
signal outcome_changed
|
|
signal history_changed
|
|
|
|
var api_url := DEFAULT_API_URL
|
|
var outcome_api_url := "https://us3q.ranaze.com/api/step3-outcomes"
|
|
var request: HTTPRequest
|
|
var readiness := {}
|
|
var simulation := {}
|
|
var outcome := {}
|
|
var history: Array = []
|
|
var history_next_before := ""
|
|
var history_next_cursor := ""
|
|
var is_loading := false
|
|
var pending_action := ""
|
|
var request_queue: Array = []
|
|
var request_access_token := ""
|
|
|
|
|
|
func _ready() -> void:
|
|
api_url = EndpointConfig.resolve(
|
|
str(ProjectSettings.get_setting("step3/exam_simulation_api_url", DEFAULT_API_URL)),
|
|
"/api/exam-simulations")
|
|
request = HTTPRequest.new()
|
|
request.request_completed.connect(_on_request_completed)
|
|
add_child(request)
|
|
outcome_api_url = EndpointConfig.resolve("https://us3q.ranaze.com/api/step3-outcomes", "/api/step3-outcomes")
|
|
|
|
|
|
func load_readiness() -> void:
|
|
_send("readiness", HTTPClient.METHOD_GET, "/readiness")
|
|
|
|
|
|
func load_active() -> void:
|
|
_send("active", HTTPClient.METHOD_GET, "/active")
|
|
|
|
|
|
func load_history(cursor: String = "", status: String = "all", sort_order: String = "newest") -> void:
|
|
var suffix := "/history?limit=20&status=%s&sort=%s" % [status.uri_encode(), sort_order.uri_encode()]
|
|
if not cursor.is_empty():
|
|
suffix += "&cursor=%s" % cursor.uri_encode()
|
|
_send("history_append" if not cursor.is_empty() else "history", HTTPClient.METHOD_GET, suffix)
|
|
|
|
|
|
func load_simulation(simulation_id: String) -> void:
|
|
_send("completed_get", HTTPClient.METHOD_GET, "/%s" % simulation_id.uri_encode())
|
|
|
|
|
|
func create_simulation() -> void:
|
|
_send("create", HTTPClient.METHOD_POST, "")
|
|
|
|
|
|
func load_results_preview() -> void:
|
|
_send("preview", HTTPClient.METHOD_GET, "/preview-results")
|
|
|
|
|
|
func start_next_block() -> void:
|
|
if simulation.is_empty():
|
|
return
|
|
_send("start", HTTPClient.METHOD_POST, "/%s/blocks/start" % str(simulation.get("id", "")))
|
|
|
|
|
|
func save_response(question_id: String, choice_id: String, eliminated: Array, flagged: bool, elapsed_seconds: int) -> void:
|
|
if simulation.is_empty():
|
|
return
|
|
_send("save", HTTPClient.METHOD_PUT, "/%s/responses" % str(simulation.get("id", "")), {
|
|
"questionExternalId": question_id,
|
|
"choiceId": choice_id,
|
|
"eliminatedChoiceIds": eliminated,
|
|
"flagged": flagged,
|
|
"elapsedSeconds": elapsed_seconds
|
|
})
|
|
|
|
|
|
func submit_block() -> void:
|
|
if simulation.is_empty():
|
|
return
|
|
_send("submit", HTTPClient.METHOD_POST, "/%s/blocks/submit" % str(simulation.get("id", "")))
|
|
|
|
|
|
func abandon() -> void:
|
|
if simulation.is_empty():
|
|
return
|
|
_send("abandon", HTTPClient.METHOD_POST, "/%s/abandon" % str(simulation.get("id", "")))
|
|
|
|
|
|
func load_outcome() -> void:
|
|
_send_absolute("outcome_get", HTTPClient.METHOD_GET, "%s/me" % outcome_api_url.rstrip("/"))
|
|
|
|
|
|
func save_outcome(official_score: int, exam_date: String) -> void:
|
|
_send_absolute("outcome_save", HTTPClient.METHOD_PUT, "%s/me" % outcome_api_url.rstrip("/"), {
|
|
"officialScore": official_score,
|
|
"examDate": "%sT00:00:00Z" % exam_date,
|
|
"consent": true
|
|
})
|
|
|
|
|
|
func delete_outcome() -> void:
|
|
_send_absolute("outcome_delete", HTTPClient.METHOD_DELETE, "%s/me" % outcome_api_url.rstrip("/"))
|
|
|
|
|
|
func get_block_deadline_unix() -> float:
|
|
var value := str(simulation.get("blockDeadlineUtc", ""))
|
|
if value == "":
|
|
return 0.0
|
|
return Time.get_unix_time_from_datetime_string(value)
|
|
|
|
|
|
func get_current_questions() -> Array:
|
|
return simulation.get("questions", [])
|
|
|
|
|
|
func _send(action: String, method: int, suffix: String, payload: Dictionary = {}) -> void:
|
|
_send_absolute(action, method, "%s%s" % [api_url.rstrip("/"), suffix], payload)
|
|
|
|
|
|
func _send_absolute(action: String, method: int, url: String, payload: Dictionary = {}) -> void:
|
|
if not AuthState.can_send_authenticated_request():
|
|
return
|
|
if is_loading:
|
|
request_queue.append({"action": action, "method": method, "url": url, "payload": payload.duplicate(true)})
|
|
return
|
|
is_loading = true
|
|
pending_action = action
|
|
request_access_token = AuthState.access_token
|
|
var headers := PackedStringArray(["Authorization: Bearer %s" % request_access_token])
|
|
var body := ""
|
|
if not payload.is_empty():
|
|
headers.append("Content-Type: application/json")
|
|
body = JSON.stringify(payload)
|
|
var err := request.request(url, headers, method, body)
|
|
if err != OK:
|
|
is_loading = false
|
|
pending_action = ""
|
|
request_failed.emit("The simulation request could not be started.")
|
|
|
|
|
|
func _on_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
|
var action := pending_action
|
|
pending_action = ""
|
|
is_loading = false
|
|
call_deferred("_process_request_queue")
|
|
if response_code == 401:
|
|
AuthState.handle_unauthorized(request_access_token)
|
|
if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
|
var server_message := body.get_string_from_utf8().strip_edges()
|
|
if server_message.length() > 240:
|
|
server_message = server_message.left(240)
|
|
var message := "Simulation request failed with status %d." % response_code
|
|
if not server_message.is_empty():
|
|
message = "%s %s" % [message, server_message]
|
|
request_failed.emit(message)
|
|
return
|
|
if response_code == 204:
|
|
if action == "active" or action == "abandon":
|
|
simulation = {}
|
|
state_changed.emit()
|
|
elif action == "outcome_delete" or action == "outcome_get":
|
|
outcome = {}
|
|
outcome_changed.emit()
|
|
return
|
|
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
|
|
if typeof(parsed) != TYPE_DICTIONARY:
|
|
request_failed.emit("The simulation API returned an invalid response.")
|
|
return
|
|
if action == "readiness":
|
|
readiness = parsed
|
|
readiness_changed.emit()
|
|
return
|
|
if action == "history" or action == "history_append":
|
|
var items: Array = parsed.get("items", [])
|
|
history = history + items if action == "history_append" else items
|
|
var next_before: Variant = parsed.get("nextBefore", null)
|
|
history_next_before = "" if next_before == null else str(next_before)
|
|
var next_cursor: Variant = parsed.get("nextCursor", null)
|
|
history_next_cursor = "" if next_cursor == null else str(next_cursor)
|
|
history_changed.emit()
|
|
return
|
|
if action.begins_with("outcome_"):
|
|
outcome = parsed
|
|
outcome_changed.emit()
|
|
return
|
|
simulation = _normalize_simulation(parsed)
|
|
state_changed.emit()
|
|
|
|
|
|
func _process_request_queue() -> void:
|
|
if is_loading or request_queue.is_empty():
|
|
return
|
|
var queued: Dictionary = request_queue.pop_front()
|
|
_send_absolute(str(queued.get("action", "")), int(queued.get("method", HTTPClient.METHOD_GET)), str(queued.get("url", "")), queued.get("payload", {}))
|
|
|
|
|
|
func _normalize_simulation(raw: Dictionary) -> Dictionary:
|
|
var normalized := raw.duplicate(true)
|
|
var normalized_questions: Array = []
|
|
for question in raw.get("questions", []):
|
|
if typeof(question) != TYPE_DICTIONARY:
|
|
continue
|
|
normalized_questions.append({
|
|
"id": str(question.get("externalId", "")),
|
|
"stem": str(question.get("stem", "")),
|
|
"lead_in": str(question.get("leadIn", "")),
|
|
"choices": question.get("choices", []),
|
|
"discipline": str(question.get("discipline", "")),
|
|
"system": str(question.get("system", "")),
|
|
"performance_category": str(question.get("performanceCategory", "")),
|
|
"labs": question.get("labs", []),
|
|
"media": question.get("media", []),
|
|
"correct_choice_id": "",
|
|
"explanation": ""
|
|
})
|
|
normalized["questions"] = normalized_questions
|
|
return normalized
|