diff --git a/classes/api/attempt_scored.php b/classes/api/attempt_scored.php index 9c9ad8e..a0b218d 100644 --- a/classes/api/attempt_scored.php +++ b/classes/api/attempt_scored.php @@ -33,8 +33,8 @@ class attempt_scored extends attempt { /** @var string */ public string $scoringstate; - /** @var string */ - public string $scoringcode; + /** @var scoring_code */ + public scoring_code $scoringcode; /** @var float|null */ public ?float $score = null; @@ -48,9 +48,9 @@ class attempt_scored extends attempt { * @param int $variant * @param attempt_ui $ui * @param string $scoringstate - * @param string $scoringcode + * @param scoring_code $scoringcode */ - public function __construct(int $variant, attempt_ui $ui, string $scoringstate, string $scoringcode) { + public function __construct(int $variant, attempt_ui $ui, string $scoringstate, scoring_code $scoringcode) { parent::__construct($variant, $ui); $this->scoringstate = $scoringstate; diff --git a/classes/api/scoring_code.php b/classes/api/scoring_code.php new file mode 100644 index 0000000..90496d2 --- /dev/null +++ b/classes/api/scoring_code.php @@ -0,0 +1,32 @@ +. + +namespace qtype_questionpy\api; + +/** + * Possible scoring states. + * + * @package qtype_questionpy + * @author Jan Britz + * @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +enum scoring_code: string { + case automatically_scored = "AUTOMATICALLY_SCORED"; + case needs_manual_scoring = "NEEDS_MANUAL_SCORING"; + case response_not_scorable = "RESPONSE_NOT_SCORABLE"; + case invalid_response = "INVALID_RESPONSE"; +} diff --git a/classes/event/grading_response_failed.php b/classes/event/grading_response_failed.php new file mode 100644 index 0000000..2bfb4d5 --- /dev/null +++ b/classes/event/grading_response_failed.php @@ -0,0 +1,66 @@ +. + +namespace qtype_questionpy\event; + +use moodle_exception; +use moodle_url; + +/** + * Grading attempt failed event. + * + * @package qtype_questionpy + * @author Jan Britz + * @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class grading_response_failed extends \core\event\base { + /** + * Initialise event parameters. + */ + protected function init() { + $this->data['crud'] = 'c'; + $this->data['edulevel'] = self::LEVEL_TEACHING; + } + + /** + * Returns localised event name. + * + * @return string + * @throws moodle_exception + */ + public static function get_name() { + return get_string('event_grading_response_failed', 'qtype_questionpy'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return $this->other['description']; + } + + /** + * Validate our custom data. + */ + public function validate_data() { + if (!isset($this->other['description'])) { + throw new \coding_exception('"description" is required in "other".'); + } + } +} diff --git a/classes/event/request_failed.php b/classes/event/starting_attempt_failed.php similarity index 64% rename from classes/event/request_failed.php rename to classes/event/starting_attempt_failed.php index c65b3b4..b336457 100644 --- a/classes/event/request_failed.php +++ b/classes/event/starting_attempt_failed.php @@ -20,14 +20,14 @@ use moodle_url; /** - * QuestionPy application server request failed event. + * Starting attempt failed event. * * @package qtype_questionpy * @author Jan Britz * @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class request_failed extends \core\event\base { +class starting_attempt_failed extends \core\event\base { /** * Initialise event parameters. */ @@ -43,7 +43,7 @@ protected function init() { * @throws moodle_exception */ public static function get_name() { - return get_string('event_request_failed', 'qtype_questionpy'); + return get_string('event_starting_attempt_failed', 'qtype_questionpy'); } /** @@ -52,31 +52,15 @@ public static function get_name() { * @return string */ public function get_description() { - return "{$this->other['message']}\nThere was an error requesting the application server:\n{$this->other['info']}"; - } - - /** - * Returns the url to the QuestionPy plugin settings. - * - * @throws moodle_exception - */ - public function get_url() { - return new moodle_url('/admin/settings.php', ['section' => 'qtypesettingquestionpy']); + return $this->other['description']; } /** * Validate our custom data. - * - * Require the following fields: - * - url - * - payload - * - message - * - * Throw \coding_exception or debugging() notice in case of any problems. */ public function validate_data() { - if (!isset($this->other['message'], $this->other['info'])) { - throw new \coding_exception('"message" and "info" are required in "other".'); + if (!isset($this->other['description'])) { + throw new \coding_exception('"description" is required in "other".'); } } } diff --git a/classes/event/viewing_attempt_failed.php b/classes/event/viewing_attempt_failed.php new file mode 100644 index 0000000..2f19a27 --- /dev/null +++ b/classes/event/viewing_attempt_failed.php @@ -0,0 +1,66 @@ +. + +namespace qtype_questionpy\event; + +use moodle_exception; +use moodle_url; + +/** + * Viewing attempt failed event. + * + * @package qtype_questionpy + * @author Jan Britz + * @copyright 2024 TU Berlin, innoCampus {@link https://www.questionpy.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class viewing_attempt_failed extends \core\event\base { + /** + * Initialise event parameters. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + } + + /** + * Returns localised event name. + * + * @return string + * @throws moodle_exception + */ + public static function get_name() { + return get_string('event_viewing_attempt_failed', 'qtype_questionpy'); + } + + /** + * Returns non-localised event description with id's for admin use only. + * + * @return string + */ + public function get_description() { + return $this->other['description']; + } + + /** + * Validate our custom data. + */ + public function validate_data() { + if (!isset($this->other['description'])) { + throw new \coding_exception('"description" is required in "other".'); + } + } +} diff --git a/lang/en/qtype_questionpy.php b/lang/en/qtype_questionpy.php index e207480..8d42ef5 100644 --- a/lang/en/qtype_questionpy.php +++ b/lang/en/qtype_questionpy.php @@ -26,7 +26,9 @@ $string['curl_exec_error'] = 'Error while fetching from server. Error number: {$a}'; $string['curl_init_error'] = 'Could not initialize cURL. Error number: {$a}'; $string['curl_set_opt_error'] = 'Failed to set cURL option. Error number: {$a}'; -$string['event_request_failed'] = 'Server request failed'; +$string['event_grading_response_failed'] = 'Grading response failed'; +$string['event_starting_attempt_failed'] = 'Starting attempt failed'; +$string['event_viewing_attempt_failed'] = 'Viewing attempt failed'; $string['form_fallback_element_text'] = "The QuestionPy package is using a form element not supported by the Moodle" . " plugin. Please ensure you are using a compatible package or contact your administrators."; $string['formerror_noqpy_package'] = 'Selected file must be of type .qpy'; diff --git a/question.php b/question.php index ef3cc8b..9b50f30 100644 --- a/question.php +++ b/question.php @@ -24,6 +24,7 @@ use qtype_questionpy\api\api; use qtype_questionpy\api\attempt_ui; +use qtype_questionpy\api\scoring_code; use qtype_questionpy\question_ui_metadata_extractor; /** @@ -99,10 +100,10 @@ private function update_ui(attempt_ui $ui): void { * being started. Can be used to store state. * @param int $variant which variant of this question to start. Will be between * 1 and {@see get_num_variants()} inclusive. - * @throws moodle_exception + * @throws Throwable */ public function start_attempt(question_attempt_step $step, $variant): void { - global $PAGE; + global $PAGE, $USER; try { $attempt = $this->api->package($this->packagehash, $this->packagefile)->start_attempt($this->questionstate, $variant); @@ -110,17 +111,20 @@ public function start_attempt(question_attempt_step $step, $variant): void { $step->set_qt_var(self::QT_VAR_ATTEMPT_STATE, $attempt->attemptstate); $this->scoringstate = null; $this->update_ui($attempt->ui); - } catch (\Throwable $t) { + } catch (Throwable $t) { // Trigger server request error event. + $message = "The user with id '{$USER->id}' encountered an error in the question with id '{$this->id}' when starting " . + "a new attempt in the {$PAGE->context->get_context_name()} with id '{$PAGE->context->id}':\n{$t->getMessage()}"; $params = [ 'context' => $PAGE->context, 'relateduserid' => $step->get_user_id(), 'other' => [ - 'message' => 'Could not start the attempt.', - 'info' => $t->getMessage(), + 'description' => $message, ], ]; - \qtype_questionpy\event\request_failed::create($params)->trigger(); + \qtype_questionpy\event\starting_attempt_failed::create($params)->trigger(); + debugging($message); + throw $t; } } @@ -140,7 +144,7 @@ public function start_attempt(question_attempt_step $step, $variant): void { * @throws moodle_exception */ public function apply_attempt_state(question_attempt_step $step) { - global $PAGE; + global $PAGE, $USER; $attemptstate = $step->get_qt_var(self::QT_VAR_ATTEMPT_STATE); if (is_null($attemptstate)) { @@ -159,17 +163,19 @@ public function apply_attempt_state(question_attempt_step $step) { $this->scoringstate ); $this->update_ui($attempt->ui); - } catch (\Throwable $t) { + } catch (Throwable $t) { // Trigger server request error event. + $message = "The user with id '{$USER->id}' encountered an error in the question with id '{$this->id}' when viewing " . + "an attempt in the {$PAGE->context->get_context_name()} with id '{$PAGE->context->id}':\n{$t->getMessage()}"; $params = [ 'context' => $PAGE->context, 'relateduserid' => $step->get_user_id(), 'other' => [ - 'message' => 'Could not view the attempt.', - 'info' => $t->getMessage(), + 'description' => $message, ], ]; - \qtype_questionpy\event\request_failed::create($params)->trigger(); + \qtype_questionpy\event\viewing_attempt_failed::create($params)->trigger(); + debugging($message); } } @@ -279,7 +285,7 @@ public function get_validation_error(array $response) { * @throws moodle_exception */ public function grade_response(array $response): array { - global $PAGE; + global $PAGE, $USER; try { $attemptscored = $this->api->package($this->packagehash, $this->packagefile)->score_attempt( @@ -289,17 +295,20 @@ public function grade_response(array $response): array { $response ); $this->update_ui($attemptscored->ui); - } catch (\Throwable $t) { + } catch (Throwable $t) { // Trigger server request error event. + $message = "The user with id '{$USER->id}' encountered an error while grading a response of the question with id " . + "'{$this->id}' in the {$PAGE->context->get_context_name()} with id '{$PAGE->context->id}':" . + "\n{$t->getMessage()}"; $params = [ 'context' => $PAGE->context, // TODO: It would be nice to set a 'relateduserid'. 'other' => [ - 'message' => 'Could not grade the response.', - 'info' => $t->getMessage(), + 'description' => $message, ], ]; - \qtype_questionpy\event\request_failed::create($params)->trigger(); + \qtype_questionpy\event\grading_response_failed::create($params)->trigger(); + debugging($message); // As the server was not able to score the response, we mark this question with manual scoring. return [0, question_state::$needsgrading]; @@ -307,23 +316,12 @@ public function grade_response(array $response): array { // TODO: Persist scoring state. We need to set a qtvar, but we don't have access to the pending step here. $this->scoringstate = $attemptscored->scoringstate; - switch ($attemptscored->scoringcode) { - case "AUTOMATICALLY_SCORED": - $newqstate = question_state::graded_state_for_fraction($attemptscored->score); - break; - case "NEEDS_MANUAL_SCORING": - // TODO: Shouldn't this be question_state::$needsgrading? - $newqstate = question_state::$finished; - break; - case "RESPONSE_NOT_SCORABLE": - $newqstate = question_state::$gaveup; - break; - case "INVALID_RESPONSE": - $newqstate = question_state::$invalid; - break; - default: - throw new coding_exception("Unrecognized scoring code: $attemptscored->scoringcode"); - } + $newqstate = match ($attemptscored->scoringcode) { + scoring_code::automatically_scored => question_state::graded_state_for_fraction($attemptscored->score), + scoring_code::needs_manual_scoring => question_state::$needsgrading, + scoring_code::response_not_scorable => question_state::$gaveup, + scoring_code::invalid_response => question_state::$invalid, + }; return [$attemptscored->score, $newqstate]; } diff --git a/tests/localizer_test.php b/tests/localizer_test.php index 8e8603a..23c5875 100644 --- a/tests/localizer_test.php +++ b/tests/localizer_test.php @@ -16,6 +16,10 @@ namespace qtype_questionpy; +use qtype_questionpy\api\attempt_scored; +use qtype_questionpy\array_converter\array_converter; +use qtype_questionpy\array_converter\converter_config; + /** * Unit tests for the questionpy question type class. *