diff --git a/backup/moodle2/backup_capquiz_stepslib.php b/backup/moodle2/backup_capquiz_stepslib.php index cce9f11..af6797e 100644 --- a/backup/moodle2/backup_capquiz_stepslib.php +++ b/backup/moodle2/backup_capquiz_stepslib.php @@ -34,6 +34,10 @@ protected function define_structure() { $question = new backup_nested_element('question', ['id'], [ 'question_id', 'question_list_id', 'rating' ]); + $questionratings = new backup_nested_element('questionratings'); + $questionrating = new backup_nested_element('question_rating', ['id'], [ + 'capquiz_question_id', 'rating', 'manual', 'timecreated' + ]); $questionselections = new backup_nested_element('questionselections'); $questionselection = new backup_nested_element('questionselection', ['id'], [ 'capquiz_id', 'strategy', 'configuration' @@ -46,32 +50,46 @@ protected function define_structure() { $user = new backup_nested_element('user', ['id'], [ 'user_id', 'capquiz_id', 'rating', 'highest_level' ]); + $userratings = new backup_nested_element('userratings'); + $userrating = new backup_nested_element('user_rating', ['id'], [ + 'capquiz_user_id', 'rating', 'manual', 'timecreated' + ]); $attempts = new backup_nested_element('attempts'); $attempt = new backup_nested_element('attempt', ['id'], [ - 'slot', 'user_id', 'question_id', 'reviewed', 'answered', 'time_answered', 'time_reviewed', 'feedback' + 'slot', 'user_id', 'question_id', 'reviewed', 'answered', 'time_answered', 'time_reviewed', + 'question_rating_id', 'previous_question_rating_id', 'user_rating_id', 'previous_user_rating_id', 'feedback' ]); // Build the tree. - $capquiz->add_child($users); - $users->add_child($user); - $user->add_child($attempts); - $attempts->add_child($attempt); + $capquiz->add_child($questionlist); + $questionlist->add_child($questions); + $questions->add_child($question); + $question->add_child($questionratings); + $questionratings->add_child($questionrating); + $capquiz->add_child($questionselections); $questionselections->add_child($questionselection); + $capquiz->add_child($ratingsystems); $ratingsystems->add_child($ratingsystem); - $capquiz->add_child($questionlist); - $questionlist->add_child($questions); - $questions->add_child($question); + + $capquiz->add_child($users); + $users->add_child($user); + $user->add_child($userratings); + $userratings->add_child($userrating); + $user->add_child($attempts); + $attempts->add_child($attempt); // Define sources. $capquiz->set_source_table('capquiz', ['id' => backup::VAR_ACTIVITYID]); $questionlist->set_source_table('capquiz_question_list', ['capquiz_id' => backup::VAR_PARENTID]); $question->set_source_table('capquiz_question', ['question_list_id' => backup::VAR_PARENTID]); + $questionrating->set_source_table('capquiz_question_rating', ['capquiz_question_id' => backup::VAR_PARENTID]); $questionselection->set_source_table('capquiz_question_selection', ['capquiz_id' => backup::VAR_PARENTID]); $ratingsystem->set_source_table('capquiz_rating_system', ['capquiz_id' => backup::VAR_PARENTID]); if ($this->get_setting_value('userinfo')) { $user->set_source_table('capquiz_user', ['capquiz_id' => backup::VAR_PARENTID]); + $userrating->set_source_table('capquiz_user_rating', ['capquiz_user_id' => backup::VAR_PARENTID]); $attempt->set_source_table('capquiz_attempt', ['user_id' => backup::VAR_PARENTID]); } diff --git a/backup/moodle2/restore_capquiz_stepslib.php b/backup/moodle2/restore_capquiz_stepslib.php index 4825c0e..9d8b9e5 100644 --- a/backup/moodle2/restore_capquiz_stepslib.php +++ b/backup/moodle2/restore_capquiz_stepslib.php @@ -30,10 +30,13 @@ protected function define_structure() { $this->add_question_usages($questionlist, $paths); $paths[] = $questionlist; $paths[] = new restore_path_element('capquiz_question', '/activity/capquiz/questionlist/questions/question'); + $paths[] = new restore_path_element( + 'capquiz_question_rating', '/activity/capquiz/questionlist/questions/question/questionratings/question_rating'); $paths[] = new restore_path_element('capquiz_question_selection', '/activity/capquiz/questionselections/questionselection'); $paths[] = new restore_path_element('capquiz_rating_system', '/activity/capquiz/ratingsystems/ratingsystem'); if ($this->get_setting_value('userinfo')) { $paths[] = new restore_path_element('capquiz_user', '/activity/capquiz/users/user'); + $paths[] = new restore_path_element('capquiz_user_rating', '/activity/capquiz/users/user/userratings/user_rating'); $paths[] = new restore_path_element('capquiz_attempt', '/activity/capquiz/users/user/attempts/attempt'); } return $this->prepare_activity_structure($paths); @@ -71,6 +74,15 @@ protected function process_capquiz_question($data) { $this->set_mapping('capquiz_question', $oldid, $newitemid); } + protected function process_capquiz_question_rating($data) { + global $DB; + $data = (object)$data; + $oldid = $data->id; + $data->capquiz_question_id = $this->get_new_parentid('capquiz_question'); + $newitemid = $DB->insert_record('capquiz_question_rating', $data); + $this->set_mapping('capquiz_question_rating', $oldid, $newitemid); + } + protected function process_capquiz_question_selection($data) { global $DB; $data = (object)$data; @@ -97,6 +109,16 @@ protected function process_capquiz_user($data) { $data->capquiz_id = $this->get_new_parentid('capquiz'); $newitemid = $DB->insert_record('capquiz_user', $data); $this->set_mapping('capquiz_user', $oldid, $newitemid); + + } + + protected function process_capquiz_user_rating($data) { + global $DB; + $data = (object)$data; + $oldid = $data->id; + $data->capquiz_user_id = $this->get_new_parentid('capquiz_user'); + $newitemid = $DB->insert_record('capquiz_user_rating', $data); + $this->set_mapping('capquiz_user_rating', $oldid, $newitemid); } protected function process_capquiz_attempt($data) { @@ -105,6 +127,10 @@ protected function process_capquiz_attempt($data) { $oldid = $data->id; $data->user_id = $this->get_new_parentid('capquiz_user'); $data->question_id = $this->get_mappingid('capquiz_question', $data->question_id); + $data->question_rating_id = $this->get_mappingid('capquiz_question_rating', $data->question_rating_id); + $data->previous_question_rating_id = $this->get_mappingid('capquiz_question_rating', $data->previous_question_rating_id); + $data->user_rating_id = $this->get_mappingid('capquiz_user_rating', $data->user_rating_id); + $data->previous_user_rating_id = $this->get_mappingid('capquiz_user_rating', $data->previous_user_rating_id); $newitemid = $DB->insert_record('capquiz_attempt', $data); $this->set_mapping('capquiz_attempt', $oldid, $newitemid); } diff --git a/classes/capquiz.php b/classes/capquiz.php index 35f1149..ee30533 100755 --- a/classes/capquiz.php +++ b/classes/capquiz.php @@ -70,6 +70,10 @@ public function id() : int { return $this->record->id; } + public function name() : string { + return $this->record->name; + } + public function is_published() : bool { return $this->record->published; } diff --git a/classes/capquiz_action_performer.php b/classes/capquiz_action_performer.php index 9d0dd41..5d68439 100755 --- a/classes/capquiz_action_performer.php +++ b/classes/capquiz_action_performer.php @@ -118,7 +118,7 @@ public static function set_question_rating(capquiz $capquiz) { } $rating = optional_param('rating', null, PARAM_FLOAT); if ($rating !== null) { - $question->set_rating($rating); + $question->set_rating($rating, true); } capquiz_urls::redirect_to_url(capquiz_urls::view_question_list_url()); } @@ -154,7 +154,8 @@ private static function create_capquiz_question(int $questionid, capquiz_questio $ratedquestion->question_list_id = $list->id(); $ratedquestion->question_id = $questionid; $ratedquestion->rating = $rating; - $DB->insert_record('capquiz_question', $ratedquestion); + $capquizquestionid = $DB->insert_record('capquiz_question', $ratedquestion, true); + capquiz_question_rating::insert_question_rating_entry($capquizquestionid, $rating); } private static function remove_capquiz_question(int $questionid, int $qlistid) { diff --git a/classes/capquiz_question.php b/classes/capquiz_question.php index 1c795f1..1438254 100755 --- a/classes/capquiz_question.php +++ b/classes/capquiz_question.php @@ -30,6 +30,9 @@ class capquiz_question { /** @var \stdClass $record */ private $record; + /** @var capquiz_question_rating $rating */ + private $rating; + public function __construct(\stdClass $record) { global $DB; $this->record = $record; @@ -42,6 +45,12 @@ public function __construct(\stdClass $record) { $this->record->name = get_string('missing_question', 'capquiz'); $this->record->text = $this->record->name; } + $rating = capquiz_question_rating::latest_question_rating_by_question($record->id); + if (is_null($rating)) { + $this->rating = capquiz_question_rating::insert_question_rating_entry($this->id(), $this->rating()); + } else { + $this->rating = $rating; + } } public static function load(int $questionid) { @@ -73,10 +82,18 @@ public function rating() : float { return $this->record->rating; } - public function set_rating(float $rating) : bool { + public function get_capquiz_question_rating() : capquiz_question_rating { + return $this->rating; + } + + public function set_rating($rating, bool $manual = false) { global $DB; $this->record->rating = $rating; - return $DB->update_record('capquiz_question', $this->record); + $DB->update_record('capquiz_question', $this->record); + + $questionrating = capquiz_question_rating::create_question_rating($this, $rating, $manual); + $this->rating = $questionrating; + } public function name() : string { diff --git a/classes/capquiz_question_attempt.php b/classes/capquiz_question_attempt.php index 8fa4ba1..a037dd5 100755 --- a/classes/capquiz_question_attempt.php +++ b/classes/capquiz_question_attempt.php @@ -163,6 +163,11 @@ public function is_correctly_answered() : bool { return $moodleattempt->get_state()->is_correct(); } + public function get_state() : bool { + $moodleattempt = $this->quba->get_question_attempt($this->question_slot()); + return $moodleattempt->get_state(); + } + public function is_reviewed() : bool { return $this->record->reviewed; } @@ -215,6 +220,26 @@ public function update_student_comment(string $feedback) { $DB->update_record('capquiz_attempt', $this->record); } + public function set_question_rating(capquiz_question_rating $rating, $previous = false) { + global $DB; + if (!$previous) { + $this->record->question_rating_id = $rating->id(); + } else { + $this->record->previous_question_rating_id = $rating->id(); + } + $DB->update_record('capquiz_attempt', $this->record); + } + + public function set_user_rating(capquiz_user_rating $rating, $previous = false) { + global $DB; + if (!$previous) { + $this->record->user_rating_id = $rating->id(); + } else { + $this->record->previous_user_rating_id = $rating->id(); + } + $DB->update_record('capquiz_attempt', $this->record); + } + /** * @param int $questionid * @return array diff --git a/classes/capquiz_question_engine.php b/classes/capquiz_question_engine.php index e056549..5d55715 100755 --- a/classes/capquiz_question_engine.php +++ b/classes/capquiz_question_engine.php @@ -81,6 +81,7 @@ public function attempt_answered(capquiz_user $user, capquiz_question_attempt $a } $ratingsystem = $this->ratingsystemloader->rating_system(); $attempt->mark_as_answered(); + $attempt->set_user_rating($user->get_capquiz_user_rating(), true); $question = $this->capquiz->question_list()->question($attempt->question_id()); if ($attempt->is_correctly_answered()) { $ratingsystem->update_user_rating($user, $question, 1); @@ -88,6 +89,7 @@ public function attempt_answered(capquiz_user $user, capquiz_question_attempt $a } else { $ratingsystem->update_user_rating($user, $question, 0); } + $attempt->set_user_rating($user->get_capquiz_user_rating()); $previousattempt = capquiz_question_attempt::previous_attempt($this->capquiz, $user); if ($previousattempt) { $this->update_question_rating($previousattempt, $attempt); @@ -127,6 +129,9 @@ private function update_question_rating(capquiz_question_attempt $previous, capq $previouscorrect = $previous->is_correctly_answered(); $currentquestion = $this->capquiz->question_list()->question($current->question_id()); $previousquestion = $this->capquiz->question_list()->question($previous->question_id()); + + $current->set_question_rating($currentquestion->get_capquiz_question_rating(), true); + if (!$currentquestion || !$previousquestion) { return; } @@ -135,6 +140,8 @@ private function update_question_rating(capquiz_question_attempt $previous, capq } else if (!$previouscorrect && $currentcorrect) { $ratingsystem->question_victory_ratings($previousquestion, $currentquestion); } + + $current->set_question_rating($currentquestion->get_capquiz_question_rating()); } } diff --git a/classes/capquiz_question_list.php b/classes/capquiz_question_list.php index 090a275..19cc3ee 100755 --- a/classes/capquiz_question_list.php +++ b/classes/capquiz_question_list.php @@ -183,7 +183,8 @@ public function merge(capquiz_question_list $that) { $newquestion->question_list_id = $this->id(); $newquestion->question_id = $question->question_id(); $newquestion->rating = $question->rating(); - $DB->insert_record('capquiz_question', $newquestion); + $capquizquestionid = $DB->insert_record('capquiz_question', $newquestion, true); + capquiz_question_rating::insert_question_rating_entry($capquizquestionid, $newquestion->rating); } } } @@ -230,7 +231,8 @@ private function copy_questions_to_list(int $qlistid) { $record = $question->entry(); $record->id = null; $record->question_list_id = $qlistid; - $DB->insert_record('capquiz_question', $record); + $capquizquestionid = $DB->insert_record('capquiz_question', $record, true); + capquiz_question_rating::insert_question_rating_entry($capquizquestionid, $record->rating); } } diff --git a/classes/capquiz_question_rating.php b/classes/capquiz_question_rating.php new file mode 100755 index 0000000..416e426 --- /dev/null +++ b/classes/capquiz_question_rating.php @@ -0,0 +1,121 @@ +. + +namespace mod_capquiz; + +use dml_exception; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +/** + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquiz_question_rating { + + /** @var stdClass $record */ + private $record; + + /** + * capquiz_question constructor. + * @param stdClass $record + * @throws dml_exception + */ + public function __construct(stdClass $record) { + $this->record = $record; + } + + public static function load_question_rating(int $questionratingid) { + global $DB; + $record = $DB->get_record('capquiz_question_rating', ['id' => $questionratingid]); + if ($record === false) { + return null; + } + return new capquiz_question_rating($record); + } + + public static function create_question_rating(capquiz_question $question, float $rating, bool $manual = false) { + return self::insert_question_rating_entry($question->id(), $rating, $manual); + } + + /** + * @param int $questionid + * @param float $rating + * @param int|null $attemptid + * @return capquiz_question_rating|null + */ + public static function insert_question_rating_entry(int $questionid, float $rating, bool $manual = false) { + global $DB; + + $record = new stdClass(); + $record->capquiz_question_id = $questionid; + $record->rating = $rating; + $record->manual = $manual; + $record->timecreated = time(); + try { + $ratingid = $DB->insert_record('capquiz_question_rating', $record); + $record->id = $ratingid; + return new capquiz_question_rating($record); + } catch (dml_exception $e) { + return null; + } + } + + /** + * Load information about the latest question rating for an attempt from the database. + * + * @param int $attemptid + * @return capquiz_question_rating + * @throws dml_exception + */ + public static function latest_question_rating_by_question($questionid) { + global $DB; + $sql = "SELECT cqr.* + FROM {capquiz_question_rating} cqr + JOIN {capquiz_question} cq ON cq.id = cqr.capquiz_question_id + WHERE cqr.id = ( + SELECT MAX(cqr2.id) + FROM {capquiz_question_rating} cqr2 + JOIN {capquiz_question} cq2 ON cq2.id = cqr2.capquiz_question_id + WHERE cq2.id = cq.id + ) + AND cq.id = :question_id"; + $record = $DB->get_record_sql($sql, ['question_id' => $questionid]); + + return $record ? new capquiz_question_rating($record) : null; + } + + public function id(): int { + return $this->record->id; + } + + public function timecreated(): string { + return $this->record->timecreated; + } + + public function rating(): float { + return $this->record->rating; + } + + public function set_rating(float $rating) { + global $DB; + $this->record->rating = $rating; + $DB->update_record('capquiz_question_rating', $this->record); + } +} diff --git a/classes/capquiz_urls.php b/classes/capquiz_urls.php index 8e364c9..21cf764 100755 --- a/classes/capquiz_urls.php +++ b/classes/capquiz_urls.php @@ -16,12 +16,18 @@ namespace mod_capquiz; +use coding_exception; +use moodle_url; + defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/mod/capquiz/report/reportlib.php'); + /** * @package mod_capquiz * @author Aleksander Skrede * @author Sebastian S. Gundersen + * @author André Storhaug * @copyright 2019 NTNU * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -35,30 +41,50 @@ class capquiz_urls { public static $urlviewgrading = '/mod/capquiz/view_grading.php'; public static $urlviewcomments = '/mod/capquiz/view_comments.php'; public static $urlviewimport = '/mod/capquiz/view_import.php'; + public static $urlviewreport = '/mod/capquiz/view_report.php'; public static $urledit = '/mod/capquiz/edit.php'; public static $urlviewcreateqlist = '/mod/capquiz/view_create_question_list.php'; public static $urlviewratingsystemconfig = '/mod/capquiz/view_rating_system.php'; - public static function redirect(\moodle_url $target) : \moodle_url { + public static function redirect(moodle_url $target): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'redirect'); $url->param('target-url', $target->out_as_local_url()); return $url; } - public static function redirect_to_front_page() { + public static function create_view_url(string $relativeurl): moodle_url { global $CFG; - redirect(new \moodle_url($CFG->wwwroot)); + $url = new moodle_url($CFG->wwwroot . $relativeurl); + $url->param('id', self::require_course_module_id_param()); + return $url; } - public static function redirect_to_url(\moodle_url $url) { - redirect($url); + /** + * @return int + * @throws coding_exception + */ + public static function require_course_module_id_param(): int { + $id = optional_param('id', 0, PARAM_INT); + if ($id !== 0) { + return $id; + } + return required_param('cmid', PARAM_INT); + } + + public static function redirect_to_front_page() { + global $CFG; + redirect(new moodle_url($CFG->wwwroot)); } public static function redirect_to_dashboard() { self::redirect_to_url(self::create_view_url(self::$urlview)); } + public static function redirect_to_url(moodle_url $url) { + redirect($url); + } + public static function redirect_to_previous() { header('Location: ' . $_SERVER['HTTP_REFERER']); exit; @@ -72,133 +98,125 @@ public static function set_page_url(capquiz $capquiz, string $url) { $PAGE->set_url(self::create_view_url($url)); } - /** - * @return int - * @throws \coding_exception - */ - public static function require_course_module_id_param() : int { - $id = optional_param('id', 0, PARAM_INT); - if ($id !== 0) { - return $id; - } - return required_param('cmid', PARAM_INT); - } - - public static function view_url() : \moodle_url { + public static function view_url(): moodle_url { return self::create_view_url(self::$urlview); } - public static function view_question_list_url(int $questionpage = 0) : \moodle_url { + public static function view_question_list_url(int $questionpage = 0): moodle_url { $url = self::create_view_url(self::$urledit); $url->param('qpage', $questionpage); return $url; } - public static function view_rating_system_url() : \moodle_url { + public static function view_rating_system_url(): moodle_url { return self::create_view_url(self::$urlviewratingsystemconfig); } - public static function view_grading_url() : \moodle_url { + public static function view_grading_url(): moodle_url { return self::create_view_url(self::$urlviewgrading); } - public static function view_classlist_url() : \moodle_url { + public static function view_classlist_url(): moodle_url { return self::create_view_url(self::$urlviewclasslist); } - public static function view_create_question_list_url() : \moodle_url { + public static function view_create_question_list_url(): moodle_url { return self::create_view_url(self::$urlviewcreateqlist); } - public static function view_comments_url() : \moodle_url { + public static function view_comments_url(): moodle_url { return self::create_view_url(self::$urlviewcomments); } - public static function view_import_url() : \moodle_url { + public static function view_import_url(): moodle_url { return self::create_view_url(self::$urlviewimport); } - public static function add_question_to_list_url(int $questionid) : \moodle_url { + public static function view_report_url($mode = ''): moodle_url { + return self::report_url(self::$urlviewreport, $mode); + } + + public static function report_url(string $relativeurl, $mode): moodle_url { + $url = self::create_view_url($relativeurl); + if ($mode !== '') { + $url->param('mode', $mode); + } + return $url; + } + + public static function add_question_to_list_url(int $questionid): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'add-question'); $url->param('question-id', $questionid); return $url; } - public static function remove_question_from_list_url(int $questionid) : \moodle_url { + public static function remove_question_from_list_url(int $questionid): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'remove-question'); $url->param('question-id', $questionid); return $url; } - public static function question_list_publish_url(capquiz_question_list $qlist) : \moodle_url { + public static function question_list_publish_url(capquiz_question_list $qlist): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'publish-question-list'); $url->param('question-list-id', $qlist->id()); return $url; } - public static function question_list_create_template_url(capquiz_question_list $qlist) : \moodle_url { + public static function question_list_create_template_url(capquiz_question_list $qlist): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'create-question-list-template'); $url->param('question-list-id', $qlist->id()); return $url; } - public static function question_list_select_url(capquiz_question_list $qlist) : \moodle_url { + public static function question_list_select_url(capquiz_question_list $qlist): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'set-question-list'); $url->param('question-list-id', $qlist->id()); return $url; } - public static function set_question_rating_url(int $questionid) : \moodle_url { + public static function set_question_rating_url(int $questionid): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'set-question-rating'); $url->param('question-id', $questionid); return $url; } - public static function regrade_all_url() : \moodle_url { + public static function regrade_all_url(): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'regrade-all'); return $url; } - public static function merge_qlist(int $qlistid) : \moodle_url { + public static function merge_qlist(int $qlistid): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'merge_qlist'); $url->param('qlistid', $qlistid); return $url; } - public static function delete_qlist(int $qlistid) : \moodle_url { + public static function delete_qlist(int $qlistid): moodle_url { $url = self::create_view_url(self::$urlaction); $url->param('action', 'delete_qlist'); $url->param('qlistid', $qlistid); return $url; } - public static function response_submit_url(capquiz_question_attempt $attempt) : \moodle_url { + public static function response_submit_url(capquiz_question_attempt $attempt): moodle_url { $url = self::create_view_url(self::$urlasync); $url->param('action', 'answered'); $url->param('attempt', $attempt->id()); return $url; } - public static function response_reviewed_url(capquiz_question_attempt $attempt) : \moodle_url { + public static function response_reviewed_url(capquiz_question_attempt $attempt): moodle_url { $url = self::create_view_url(self::$urlasync); $url->param('action', 'reviewed'); $url->param('attempt', $attempt->id()); return $url; } - - public static function create_view_url(string $relativeurl) : \moodle_url { - global $CFG; - $url = new \moodle_url($CFG->wwwroot . $relativeurl); - $url->param('id', self::require_course_module_id_param()); - return $url; - } - } diff --git a/classes/capquiz_user.php b/classes/capquiz_user.php index 988d183..ea35df3 100755 --- a/classes/capquiz_user.php +++ b/classes/capquiz_user.php @@ -33,6 +33,9 @@ class capquiz_user { /** @var \stdClass $user */ private $user; + /** @var capquiz_user_rating $rating */ + private $rating; + /** * capquiz_user constructor. * @param \stdClass $record @@ -42,6 +45,13 @@ public function __construct(\stdClass $record) { global $DB; $this->record = $record; $this->user = $DB->get_record('user', ['id' => $this->record->user_id]); + + $rating = capquiz_user_rating::latest_user_rating_by_user($record->id); + if (is_null($rating)) { + $this->rating = capquiz_user_rating::insert_user_rating_entry($this->id(), $this->rating()); + } else { + $this->rating = $rating; + } } /** @@ -59,7 +69,8 @@ public static function load_user(capquiz $capquiz, int $moodleuserid) { $record->user_id = $moodleuserid; $record->capquiz_id = $capquiz->id(); $record->rating = $capquiz->default_user_rating(); - $DB->insert_record('capquiz_user', $record); + $capquizuserid = $DB->insert_record('capquiz_user', $record, true); + capquiz_user_rating::insert_user_rating_entry($capquizuserid, $record->rating); return self::load_db_entry($capquiz, $moodleuserid); } @@ -102,6 +113,10 @@ public function rating() : float { return $this->record->rating; } + public function get_capquiz_user_rating() : capquiz_user_rating { + return $this->rating; + } + public function highest_stars_achieved() : int { return $this->record->highest_level; } @@ -116,10 +131,13 @@ public function set_highest_star(int $higheststar) { $DB->update_record('capquiz_user', $this->record); } - public function set_rating(float $rating) { + public function set_rating($rating, bool $manual = false) { global $DB; $this->record->rating = $rating; $DB->update_record('capquiz_user', $this->record); + + $userrating = capquiz_user_rating::create_user_rating($this, $rating, $manual); + $this->rating = $userrating; } /** @@ -137,4 +155,6 @@ private static function load_db_entry(capquiz $capquiz, int $moodleuserid) { return $entry ? new capquiz_user($entry) : null; } + + } diff --git a/classes/capquiz_user_rating.php b/classes/capquiz_user_rating.php new file mode 100755 index 0000000..3eb354b --- /dev/null +++ b/classes/capquiz_user_rating.php @@ -0,0 +1,122 @@ +. + +namespace mod_capquiz; + +use dml_exception; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +/** + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 NTNU + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquiz_user_rating { + + /** @var stdClass $record */ + private $record; + + /** + * capquiz_user constructor. + * @param stdClass $record + * @throws dml_exception + */ + public function __construct(stdClass $record) { + $this->record = $record; + } + + public static function load_user_rating(int $questionratingid) { + global $DB; + $record = $DB->get_record('capquiz_question_rating', ['id' => $questionratingid]); + if ($record === false) { + return null; + } + return new capquiz_user_rating($record); + } + + public static function create_user_rating(capquiz_user $user, $rating, bool $manual = false) { + return self::insert_user_rating_entry($user->id(), $rating, $manual); + } + + /** + * Load information about the latest user rating for an capquiz user from the database. + * + * @param int $attemptid + * @return capquiz_user_rating + * @throws dml_exception + */ + public static function latest_user_rating_by_user($userid) { + global $DB; + $sql = "SELECT cur.* + FROM {capquiz_user_rating} cur + JOIN {capquiz_user} cu ON cu.id = cur.capquiz_user_id + WHERE cur.id = ( + SELECT MAX(cur2.id) + FROM {capquiz_user_rating} cur2 + JOIN {capquiz_user} cu2 ON cu2.id = cur2.capquiz_user_id + WHERE cu2.id = cu.id + ) + AND cu.id = :user_id"; + $record = $DB->get_record_sql($sql, ['user_id' => $userid]); + + return $record ? new capquiz_user_rating($record) : null; + } + + /** + * @param int $userid capquiz_user id + * @param float $rating + * @param null $attemptid + * @return capquiz_user_rating|null + */ + public static function insert_user_rating_entry(int $userid, float $rating, bool $manual = false) { + global $DB, $USER; + + $record = new stdClass(); + $record->capquiz_user_id = $userid; + $record->rating = $rating; + $record->manual = $manual; + $record->timecreated = time(); + $record->user_id = $USER->id; + try { + $ratingid = $DB->insert_record('capquiz_user_rating', $record); + $record->id = $ratingid; + return new capquiz_user_rating($record); + } catch (dml_exception $e) { + return null; + } + } + + public function id(): int { + return $this->record->id; + } + + public function timecreated(): string { + return $this->user->timecreated; + } + + public function rating(): float { + return $this->record->rating; + } + + public function set_rating(float $rating) { + global $DB; + $this->record->rating = $rating; + $DB->update_record('capquiz_user_rating', $this->record); + } +} diff --git a/classes/output/renderer.php b/classes/output/renderer.php index 984dfe4..cc20c07 100755 --- a/classes/output/renderer.php +++ b/classes/output/renderer.php @@ -58,7 +58,8 @@ private function tabs(string $activetab) { $this->tab('view_grading', 'grading', capquiz_urls::view_grading_url()), $this->tab('view_classlist', 'classlist', capquiz_urls::view_classlist_url()), $this->tab('view_comments', 'comments', capquiz_urls::view_comments_url()), - $this->tab('view_import', 'other_question_lists', capquiz_urls::view_import_url()) + $this->tab('view_import', 'other_question_lists', capquiz_urls::view_import_url()), + $this->tab('view_report', 'reports', capquiz_urls::view_report_url()), ]; return print_tabs([$tabs], $activetab, null, null, true); } @@ -164,4 +165,7 @@ public function display_grading_configuration(capquiz $capquiz) { $this->display_tabbed_view(new grading_configuration_renderer($capquiz, $this), 'view_grading'); } + public function display_report(capquiz $capquiz) { + $this->display_tabbed_view(new report_renderer($capquiz, $this), 'view_report'); + } } diff --git a/classes/output/report_renderer.php b/classes/output/report_renderer.php new file mode 100644 index 0000000..b0ed87f --- /dev/null +++ b/classes/output/report_renderer.php @@ -0,0 +1,90 @@ +. + +namespace mod_capquiz\output; + +use capquiz_exception; +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_urls; +use mod_capquiz\report\capquiz_report_factory; +use tabobject; + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/../../report/reportfactory.php'); + +/** + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report_renderer { + + /** @var capquiz $capquiz */ + private $capquiz; + + /** @var renderer $renderer */ + private $renderer; + + public function __construct(capquiz $capquiz, renderer $renderer) { + $this->capquiz = $capquiz; + $this->renderer = $renderer; + } + + public function render() { + global $OUTPUT; + $html = ''; + $download = optional_param('download', '', PARAM_RAW); + $mode = optional_param('mode', '', PARAM_ALPHA); + + $reportlist = capquiz_report_list($this->capquiz->context()); + if (empty($reportlist)) { + throw new capquiz_exception('erroraccessingreport'); + } + if ($mode == '') { + // Default to first accessible report and redirect. + capquiz_urls::redirect_to_url(capquiz_urls::view_report_url(reset($reportlist))); + } + if (!in_array($mode, $reportlist)) { + throw new capquiz_exception('erroraccessingreport'); + } + $report = capquiz_report_factory::make($mode); + $this->setup_report(); + + $row = array(); + foreach ($reportlist as $rep) { + $row[] = new tabobject('capquiz_' . $rep, capquiz_urls::view_report_url($rep), + get_string('pluginname', 'capquizreport_' . $rep)); + } + $tabs[] = $row; + + $html .= print_tabs($tabs, 'capquiz_' . $mode, null, null, true); + + ob_start(); + $report->display($this->capquiz, $this->capquiz->course_module(), $this->capquiz->course(), $download); + $html .= ob_get_clean(); + + return $html; + + } + + private function setup_report() { + global $PAGE; + $PAGE->set_pagelayout('report'); + } +} + diff --git a/classes/plugininfo/capquizreport.php b/classes/plugininfo/capquizreport.php new file mode 100644 index 0000000..34d00a3 --- /dev/null +++ b/classes/plugininfo/capquizreport.php @@ -0,0 +1,36 @@ +. + +/** + * Subplugin info class. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_capquiz\plugininfo; + +use core\plugininfo\base; + +defined('MOODLE_INTERNAL') || die(); + + +class capquizreport extends base { + public function is_uninstall_allowed() { + return true; + } +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index fd7e38d..2e06035 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -26,13 +26,19 @@ namespace mod_capquiz\privacy; +use coding_exception; +use context; +use context_module; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\helper; use core_privacy\local\request\transform; use core_privacy\local\request\writer; -use mod_capquiz\capquiz; +use dml_exception; +use moodle_exception; +use question_display_options; +use stdClass; defined('MOODLE_INTERNAL') || die(); @@ -53,10 +59,10 @@ class provider implements /** * Returns meta data about this system. - * @param collection $items The initialised collection to add metadata to. + * @param collection $items The initialised collection to add metadata to. * @return collection A listing of user data stored through this system. */ - public static function get_metadata(collection $items) : collection { + public static function get_metadata(collection $items): collection { // The table 'capquiz' stores a record for each capquiz. // It does not contain user personal data, but data is returned from it for contextual requirements. @@ -89,6 +95,16 @@ public static function get_metadata(collection $items) : collection { 'highest_level' => 'privacy:metadata:capquiz_user:highest_level', ], 'privacy:metadata:capquiz_user'); + // The table 'capquiz_user_rating' stores a record of each user rating in each capquiz attempt. + // This is to kep track of rating and achievement level, in addition to provide a historical log. + // It contains a capquiz_user_id which links to the capquiz_user and contains information about that user. + $items->add_database_table('capquiz_user_rating', [ + 'capquiz_user_id' => 'privacy:metadata:capquiz_user_rating:capquiz_user_id', + 'rating' => 'privacy:metadata:capquiz_user_rating:rating', + 'manual' => 'privacy:metadata:capquiz_user_rating:manual', + 'timecreated' => 'privacy:metadata:capquiz_user_rating:timecreated' + ], 'privacy:metadata:capquiz_user_rating'); + // CAPQuiz links to the 'core_question' subsystem for all question functionality. $items->add_subsystem_link('core_question', [], 'privacy:metadata:core_question'); return $items; @@ -97,10 +113,10 @@ public static function get_metadata(collection $items) : collection { /** * Get the list of contexts where the specified user has attempted a capquiz. * - * @param int $userid The user to search. + * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ - public static function get_contexts_for_userid(int $userid) : contextlist { + public static function get_contexts_for_userid(int $userid): contextlist { $sql = 'SELECT cx.id FROM {context} cx JOIN {course_modules} cm @@ -128,10 +144,10 @@ public static function get_contexts_for_userid(int $userid) : contextlist { /** * Export all user data for the specified user, in the specified contexts. * - * @param approved_contextlist $contextlist The approved contexts to export information for. - * @throws \coding_exception - * @throws \dml_exception - * @throws \moodle_exception + * @param approved_contextlist $contextlist The approved contexts to export information for. + * @throws coding_exception + * @throws dml_exception + * @throws moodle_exception */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; @@ -148,6 +164,7 @@ public static function export_user_data(approved_contextlist $contextlist) { ca.time_reviewed AS timereviewed, ca.time_answered AS timeanswered, ca.feedback AS feedback, + cu.id AS capuserid, cql.question_usage_id AS qubaid FROM {context} cx JOIN {course_modules} cm @@ -164,7 +181,7 @@ public static function export_user_data(approved_contextlist $contextlist) { ON cu.capquiz_id = cq.id AND cu.user_id = :userid JOIN {capquiz_attempt} ca - ON ca.user_id = ca.user_id + ON ca.user_id = cu.id WHERE cx.id {$contextsql}"; $params = [ 'contextlevel' => CONTEXT_MODULE, @@ -174,36 +191,83 @@ public static function export_user_data(approved_contextlist $contextlist) { $params += $contextparams; $qubaidforcontext = []; $attempts = $DB->get_recordset_sql($sql, $params); + + $context = null; foreach ($attempts as $attempt) { - $context = \context_module::instance($attempt->cmid); + if (!$context || $context->instanceid != $attempt->cmid) { + // This row belongs to the different data module than the previous row. + // Start new data module. + $context = context_module::instance($attempt->cmid); + } $qubaidforcontext[$context->id] = $attempt->qubaid; // Store the quiz attempt data. - $data = new \stdClass(); + $data = new stdClass(); $data->timereviewed = transform::datetime($attempt->timereviewed); $data->timeanswered = transform::datetime($attempt->timeanswered); $data->feedback = $attempt->feedback; - $subcontext = [$attempt->capattemptid]; + + // The capquiz attempt data is organised in: {Course name}/{CAPQuiz activity name}/{Attempts}/{_X}/data.json + // where X is the attempt number. + $subcontext = [ + get_string('attempts', 'capquiz'), + get_string('attempt', 'capquiz') . " $attempt->capattemptid" + ]; + writer::with_context($context)->export_data($subcontext, $data); + + static::export_user_rating($context, $attempt->capuserid); } $attempts->close(); + // The capquiz question data is organised in: {Course name}/{CAPQuiz activity name}/{Questions}/{_X}/data.json + // where X is the question attempt number. + /* TODO we should rather organize the questions data and steps in: + {Course name}/{CAPQuiz activity name}/{Attempts}/{_X}/Question/ + where X is the attempt number.*/ foreach ($contextlist as $context) { - $options = new \question_display_options(); + $options = new question_display_options(); $options->context = $context; $data = helper::get_context_data($context, $user); helper::export_context_files($context, $user); writer::with_context($context)->export_data([], $data); // This attempt was made by the user. They 'own' all data on it. Store the question usage data. - \core_question\privacy\provider::export_question_usage($user->id, $context, [], $qubaidforcontext[$context->id], $options, true); + \core_question\privacy\provider::export_question_usage( + $user->id, $context, [], $qubaidforcontext[$context->id], $options, true + ); + } + } + + public static function export_user_rating(context $context, int $userid) { + global $DB; + $sql = "SELECT cur.id AS ratingid, + cur.rating AS rating, + cur.manual AS manual, + cur.timecreated AS timecreated + FROM {capquiz_user} cu + JOIN {capquiz_user_rating} cur + ON cur.capquiz_user_id = cu.id + WHERE cu.id = :userid"; + $ratings = $DB->get_recordset_sql($sql, ['userid' => $userid]); + + foreach ($ratings as $rating) { + $data = new stdClass(); + $data->rating = $rating->rating; + $data->manual = $rating->manual; + $data->timecreated = transform::datetime($rating->timecreated); + $subcontext = [ + get_string('userratings', 'capquiz'), + get_string('userrating', 'capquiz') . " $rating->ratingid" + ]; + writer::with_context($context)->export_data($subcontext, $data); } } /** * Delete all data for all users in the specified context. * - * @param \context $context The specific context to delete data for. + * @param context $context The specific context to delete data for. */ - public static function delete_data_for_all_users_in_context(\context $context) { + public static function delete_data_for_all_users_in_context(context $context) { global $DB; if ($context->contextlevel != CONTEXT_MODULE) { return; @@ -214,7 +278,8 @@ public static function delete_data_for_all_users_in_context(\context $context) { } $users = $DB->get_records('capquiz_user', ['capquiz_id' => $cm->instance]); foreach ($users as $user) { - $DB->delete_records('capquiz_attempt', ['user_id' => $user->user_id]); + $DB->delete_records('capquiz_attempt', ['user_id' => $user->id]); + $DB->delete_records('capquiz_user_rating', ['capquiz_user_id' => $user->id]); } $DB->delete_records('capquiz_user', ['capquiz_id' => $cm->instance]); \core_question\privacy\provider::delete_data_for_all_users_in_context($context); @@ -223,7 +288,7 @@ public static function delete_data_for_all_users_in_context(\context $context) { /** * Delete all user data for the specified user, in the specified contexts. * - * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { global $DB; @@ -236,10 +301,10 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { $user = $DB->get_record('capquiz_user', ['capquiz_id' => $cm->instance, 'user_id' => $userid]); if ($user) { $DB->delete_records('capquiz_attempt', ['user_id' => $user->id]); + $DB->delete_records('capquiz_user_rating', ['capquiz_user_id' => $user->id]); $DB->delete_records('capquiz_user', ['capquiz_id' => $cm->instance, 'user_id' => $userid]); } } \core_question\privacy\provider::delete_data_for_user($contextlist); } - } diff --git a/classes/rating_system/elo_rating/elo_rating_system.php b/classes/rating_system/elo_rating/elo_rating_system.php index 659af86..b6090aa 100755 --- a/classes/rating_system/elo_rating/elo_rating_system.php +++ b/classes/rating_system/elo_rating/elo_rating_system.php @@ -58,8 +58,8 @@ public function default_configuration() { public function update_user_rating(capquiz_user $user, capquiz_question $question, float $score) { $current = $user->rating(); $factor = $this->studentkfactor; - $updated = $current + $factor * ($score - $this->expected_result($current, $question->rating())); - $user->set_rating($updated); + $newrating = $current + $factor * ($score - $this->expected_result($current, $question->rating())); + $user->set_rating($newrating); } public function question_victory_ratings(capquiz_question $winner, capquiz_question $loser) { diff --git a/db/access.php b/db/access.php index a35cc1c..7a3cbf0 100755 --- a/db/access.php +++ b/db/access.php @@ -44,5 +44,36 @@ 'coursecreator' => CAP_ALLOW, 'manager' => CAP_ALLOW ] - ] + ], + // View the capquiz reports. + 'mod/capquiz:viewreports' => [ + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ] + ], + // Delete attempts using the overview report. + 'mod/capquiz:deleteattempts' => [ + 'riskbitmask' => RISK_DATALOSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ] + ], + // Edit the quiz settings, add and remove questions. + 'mod/capquiz:manage' => [ + 'riskbitmask' => RISK_SPAM, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ] + ], ]; diff --git a/db/install.xml b/db/install.xml index 65be112..979385f 100755 --- a/db/install.xml +++ b/db/install.xml @@ -84,14 +84,54 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
diff --git a/db/subplugins.php b/db/subplugins.php new file mode 100644 index 0000000..32c26f7 --- /dev/null +++ b/db/subplugins.php @@ -0,0 +1,29 @@ +. + +/** + * Sub-plugin definitions for the capquiz module. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$subplugins = array( + 'capquizreport' => 'mod/capquiz/report', +); diff --git a/db/upgrade.php b/db/upgrade.php index 08272b5..8ae879b 100755 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -84,5 +84,93 @@ function xmldb_capquiz_upgrade($oldversion) { } upgrade_mod_savepoint(true, 2019062553, 'capquiz'); } + if ($oldversion < 2019071800) { + // Define table capquiz_user_rating to be created. + $utable = new xmldb_table('capquiz_user_rating'); + + // Adding fields to table capquiz_user_rating. + $utable->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $utable->add_field('capquiz_user_id', XMLDB_TYPE_INTEGER, '11', null, XMLDB_NOTNULL, null, null); + $utable->add_field('rating', XMLDB_TYPE_FLOAT, '11', null, XMLDB_NOTNULL, null, null); + $utable->add_field('manual', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, 0); + $utable->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table capquiz_user_rating. + $utable->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $utable->add_key('capquiz_user_id', XMLDB_KEY_FOREIGN, array('capquiz_user_id'), 'capquiz_user', array('id')); + + // Adding indexes to table capquiz_user_rating. + $utable->add_index('timecreated', XMLDB_INDEX_NOTUNIQUE, array('timecreated')); + + // Conditionally launch create table for enrol_lti_lti2_consumer. + if (!$dbman->table_exists($utable)) { + $dbman->create_table($utable); + } + + // Define table capquiz_question_rating to be created. + $qtable = new xmldb_table('capquiz_question_rating'); + + // Adding fields to table capquiz_question_rating. + $qtable->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $qtable->add_field('capquiz_question_id', XMLDB_TYPE_INTEGER, '11', null, null, null, null); + $qtable->add_field('rating', XMLDB_TYPE_FLOAT, '11', null, XMLDB_NOTNULL, null, 0); + $qtable->add_field('manual', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, 0); + $qtable->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table capquiz_question_rating. + $qtable->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $qtable->add_key('capquiz_question_id', XMLDB_KEY_FOREIGN, array('capquiz_question_id'), 'capquiz_question', array('id')); + + // Adding indexes to table capquiz_question_rating. + $qtable->add_index('timecreated', XMLDB_INDEX_NOTUNIQUE, array('timecreated')); + + // Conditionally launch create table for capquiz_question_rating. + if (!$dbman->table_exists($qtable)) { + $dbman->create_table($qtable); + } + + $atable = new xmldb_table('capquiz_attempt'); + $aqrfield = new xmldb_field( + 'question_rating_id', XMLDB_TYPE_INTEGER, 11, null, null, null, null); + $aqrkey = new xmldb_key( + 'question_rating_id', XMLDB_KEY_FOREIGN, array('question_rating_id'), 'capquiz_question_rating', array('id')); + $aprevqrfield = new xmldb_field( + 'previous_question_rating_id', + XMLDB_TYPE_INTEGER, 11, null, null, null, null); + $aprevqrkey = new xmldb_key( + 'previous_question_rating_id', + XMLDB_KEY_FOREIGN, array('previous_question_rating_id'), 'capquiz_question_rating', array('id')); + + if (!$dbman->field_exists($atable, $aqrfield)) { + $dbman->add_field($atable, $aqrfield); + $dbman->add_key($atable, $aqrkey); + } + if (!$dbman->field_exists($atable, $aprevqrfield)) { + $dbman->add_field($atable, $aprevqrfield); + $dbman->add_key($atable, $aprevqrkey); + } + + $aurfield = new xmldb_field( + 'user_rating_id', XMLDB_TYPE_INTEGER, 11, null, null, null, null); + $aurkey = new xmldb_key( + 'user_rating_id', XMLDB_KEY_FOREIGN, array('user_rating_id'), 'capquiz_user_rating', array('id')); + $aprevurfield = new xmldb_field( + 'previous_user_rating_id', + XMLDB_TYPE_INTEGER, 11, null, null, null, null); + $aprevurkey = new xmldb_key( + 'previous_user_rating_id', + XMLDB_KEY_FOREIGN, array('previous_user_rating_id'), 'capquiz_user_rating', array('id')); + + if (!$dbman->field_exists($atable, $aurfield)) { + $dbman->add_field($atable, $aurfield); + $dbman->add_key($atable, $aurkey); + } + if (!$dbman->field_exists($atable, $aprevurfield)) { + $dbman->add_field($atable, $aprevurfield); + $dbman->add_key($atable, $aprevurkey); + } + + upgrade_mod_savepoint(true, 2019071800, 'capquiz'); + } return true; } diff --git a/lang/en/capquiz.php b/lang/en/capquiz.php index 54bc1ed..faa8106 100755 --- a/lang/en/capquiz.php +++ b/lang/en/capquiz.php @@ -32,11 +32,14 @@ $string['capquiz:addinstance'] = 'Add an instance of CAPQuiz'; $string['capquiz:instructor'] = 'Edit CAPQuiz instances'; $string['capquiz:student'] = 'Attempt CAPQuiz instances'; +$string['capquiz:viewreports'] = 'View capquiz reports'; +$string['capquiz:deleteattempts'] = 'Delete capquiz attempts'; +$string['capquiz:manage'] = 'Manage capquizzes'; $string['questions_in_list'] = 'Questions in the list'; $string['add_a_quiz_question'] = 'Add a question to the list'; $string['add_the_quiz_question'] = 'Add the question to the list'; -$string['add_to_quiz'] = 'Add to quiz'; +$string['add_to_quiz'] = 'Add to capquiz'; $string['question_list'] = 'Question list'; $string['question_lists'] = 'Question lists'; @@ -94,6 +97,9 @@ $string['create_question_list'] = 'Create question list'; $string['other_question_lists'] = 'Other question lists'; $string['nothing_here_yet'] = 'Nothing here yet'; +$string['reports'] = 'Reports'; +$string['attempts'] = 'Attempts'; +$string['attempt'] = 'Attempt'; $string['missing_question'] = 'This question is missing.'; @@ -115,7 +121,7 @@ $string['nothing_to_configure_for_strategy'] = 'There is nothing to configure for this strategy'; $string['update_rating_explanation'] = '

The question ratings can be edited below. Changes are saved automatically.

'; -$string['question_list_no_questions'] = 'This quiz has no questions. Add some questions from the list to the right'; +$string['question_list_no_questions'] = 'This capquiz has no questions. Add some questions from the list to the right'; $string['n_closest'] = 'N-closest'; $string['chronological'] = 'Chronological'; $string['no_strategy_specified'] = 'No strategy specified'; @@ -145,7 +151,7 @@ $string['tooltip_no_star'] = 'You have yet to achieve this star.'; $string['tooltip_help_star'] = 'Every student has a proficiency rating in the CAPQuiz activity. This increases when successfully answering a question, and decreases with wrong answers. Stars are achieved at certain rating levels, and never lost. I.e. a student can sometimes lose rating points and fall below a star\'s threshold, without losing the star. It is suggested that a certain number of stars are required for a compulsory assignment. Hover your mouse over a star to see rating details.'; -$string['select_template'] = 'Select one of these templates for your quiz'; +$string['select_template'] = 'Select one of these templates for your capquiz'; $string['no_templates_created'] = 'No templates have been created.'; $string['create_own_template'] = 'You can also create your own'; @@ -164,7 +170,7 @@ $string['question_k_factor_specified_rule'] = 'Question k-factor must be specified'; $string['k_factor_numeric_rule'] = 'K-factor must be a numeric value'; -$string['publish_explanation'] = '

Students are unable to answer questions as long as the quiz is not published. This is useful if you\'re still building your question list and modifying question ratings. Similarly, modifying the default student rating before a quiz has been published ensures that all students are given the same initial rating.

Students can answer questions once the quiz has been published. After this point you can still modify your question list and assign different rating to questions. However, modifying the default student rating will not influence rating of students that has already entered the quiz, but will influence the initial rating of students that has yet to enter the quiz.

Once CAPQuiz has been published, it can not be reverted and will be visible to students.

'; +$string['publish_explanation'] = '

Students are unable to answer questions as long as the capquiz is not published. This is useful if you\'re still building your question list and modifying question ratings. Similarly, modifying the default student rating before a capquiz has been published ensures that all students are given the same initial rating.

Students can answer questions once the capquiz has been published. After this point you can still modify your question list and assign different rating to questions. However, modifying the default student rating will not influence rating of students that has already entered the capquiz, but will influence the initial rating of students that has yet to enter the capquiz.

Once CAPQuiz has been published, it can not be reverted and will be visible to students.

'; $string['template_explanation'] = '

A template is a read-only copy of a question list. Templates allow instructors to reuse question lists between courses or semesters, and can be shared with other instructors. Since a template is a copy of it\'s original question list, instructors can be sure that ratings won\'t be influenced when sharing between CAPQuiz instances. However, if multiple question lists are created from the same template, any changes made to the original question in the question bank will be visible in all templates and question lists. This includes renaming the question title, changing correct answers, descriptions and marks.

'; $string['template_no_questions_in_list'] = '

There doesn\'t seem to be any questions in the question list for this CAPQuiz instance. Creating a template requires questions in the question list. Add some questions and come back to create your template.

'; $string['publish_no_questions_in_list'] = '

There doesn\'t seem to be any questions in the question list for this CAPQuiz instance. You must have at least one question before you can publish

'; @@ -173,6 +179,10 @@ $string['no_question_list_assigned'] = 'No question list has been assigned'; $string['published'] = 'Published'; $string['not_published'] = 'Not published'; +$string['question_list_not_published'] = 'The question list is not yet published'; + +$string['question_list_settings'] = 'Question list settings'; +$string['you_finished_capquiz'] = 'You have finished this capquiz!'; $string['problem_with_question_header'] = 'Is there a problem with this question? Send feedback to your instructor here.'; $string['problem_with_question_details'] = 'If you have feedback for this question, please type it below. The text will be sent to your instructor. You will be able to continue editing this when you have submitted your answer.'; @@ -193,3 +203,21 @@ $string['privacy:metadata:capquiz_user:userid'] = 'The CAPQuiz user.'; $string['privacy:metadata:capquiz_user:rating'] = 'The rating of the user.'; $string['privacy:metadata:capquiz_user:highest_level'] = 'The user\'s highest number of stars achieved.'; + +$string['privacy:metadata:capquiz_user_rating'] = 'Details about each user rating created in a CAPQuiz.'; +$string['privacy:metadata:capquiz_user_rating:capquiz_user_id'] = 'The user who\'s rating it is.'; +$string['privacy:metadata:capquiz_user_rating:rating'] = 'The user\'s rating.'; +$string['privacy:metadata:capquiz_user_rating:manual'] = 'Whether or not the user rating was created manually'; +$string['privacy:metadata:capquiz_user_rating:timecreated'] = 'The time that the user rating was created'; + +$string['userratings'] = 'User ratings'; +$string['userrating'] = 'User rating'; +$string['questionrating'] = 'Question rating'; +$string['report'] = 'report'; + +$string['subplugintype_capquizreport'] = 'Report'; +$string['subplugintype_capquizreport_plural'] = 'Reports'; +$string['erroraccessingreport'] = 'You cannot access this report'; + +$string['true'] = 'True'; +$string['false'] = 'False'; diff --git a/locallib.php b/locallib.php new file mode 100644 index 0000000..e9b715e --- /dev/null +++ b/locallib.php @@ -0,0 +1,49 @@ +. + +/** + * Library of internal classes and functions for module CAPQuiz + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base class for all the types of exception we throw. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquiz_exception extends moodle_exception { + /** + * capquiz_exception constructor. + * @param string $errorcode The name of the language string containing the error message. + * Normally this should be in the error.php lang file. + * @param string $module The language file to get the error message from. + * @param string $link The url where the user will be prompted to continue. + * If no url is provided the user will be directed to the site index page. + * @param object $a Extra words and phrases that might be required in the error string + * @param string $debuginfo optional debugging information + */ + public function __construct($errorcode, $module = 'capquiz', $link = '', $a = null, $debuginfo = null) { + parent::__construct($errorcode, $module, $link, $a, $debuginfo); + } +} diff --git a/report/attempts/attempts_form.php b/report/attempts/attempts_form.php new file mode 100644 index 0000000..5d5e86f --- /dev/null +++ b/report/attempts/attempts_form.php @@ -0,0 +1,81 @@ +. + +/** + * CAPQuiz attempts settings form definition. + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace capquizreport_attempts; + +use mod_capquiz\report\capquiz_attempts_report_form; +use MoodleQuickForm; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_form.php'); + +/** + * This is the settings form for the capquiz attempts report. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquizreport_attempts_settings_form extends capquiz_attempts_report_form { + + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + if (!($data['urating'] + || $data['uprevrating'] + || $data['qrating'] + || $data['qprevrating'] + || $data['ansstate'] + || $data['qtext'] + || $data['resp'] + || $data['right'])) { + $errors['coloptions'] = get_string('reportmustselectstate', 'quiz'); + } + + return $errors; + } + + protected function other_preference_fields(MoodleQuickForm $mform) { + $mform->addGroup(array( + $mform->createElement('advcheckbox', 'ansstate', '', + get_string('ansstate', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'urating', '', + get_string('urating', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'uprevrating', '', + get_string('uprevrating', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'qrating', '', + get_string('qrating', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'qprevrating', '', + get_string('qprevrating', 'capquizreport_attempts')), + $mform->createElement('advcheckbox', 'qtext', '', + get_string('questiontext', 'quiz_responses')), + $mform->createElement('advcheckbox', 'resp', '', + get_string('response', 'quiz_responses')), + $mform->createElement('advcheckbox', 'right', '', + get_string('rightanswer', 'quiz_responses')), + ), 'coloptions', get_string('showthe', 'quiz_responses'), array(' '), false); + } +} diff --git a/report/attempts/attempts_options.php b/report/attempts/attempts_options.php new file mode 100644 index 0000000..31cadb5 --- /dev/null +++ b/report/attempts/attempts_options.php @@ -0,0 +1,171 @@ +. + +/** + * Class to store the options for a {@link capquiz_attempts_report}. + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace capquizreport_attempts; + +use context_module; +use mod_capquiz\report\capquiz_attempts_report; +use mod_capquiz\report\capquiz_attempts_report_options; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_options.php'); + + +/** + * Class to store the options for a {@link capquiz_attempts_report}. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquizreport_attempts_options extends capquiz_attempts_report_options { + + /** @var bool whether to show the question answer state (correct or wrong) columns. */ + public $showansstate = true; + + /** @var bool whether to show the question rating columns. */ + public $showqrating = true; + + /** @var bool whether to show the previous question rating columns. */ + public $showqprevrating = true; + + /** @var bool whether to show the user rating columns. */ + public $showurating = true; + + /** @var bool whether to show the previous user rating columns. */ + public $showuprevrating = true; + + /** @var bool whether to show the question text columns. */ + public $showqtext = false; + + /** @var bool whether to show the students' response columns. */ + public $showresponses = false; + + /** @var bool whether to show the correct response columns. */ + public $showright = false; + + public function get_initial_form_data() { + $toform = parent::get_initial_form_data(); + $toform->ansstate = $this->showansstate; + $toform->urating = $this->showurating; + $toform->uprevrating = $this->showuprevrating; + $toform->qrating = $this->showqrating; + $toform->qprevrating = $this->showqprevrating; + $toform->qtext = $this->showqtext; + $toform->resp = $this->showresponses; + $toform->right = $this->showright; + + return $toform; + } + + public function setup_from_form_data($fromform) { + parent::setup_from_form_data($fromform); + + $this->showansstate = $fromform->ansstate; + $this->showurating = $fromform->urating; + $this->showuprevrating = $fromform->uprevrating; + $this->showqrating = $fromform->qrating; + $this->showqprevrating = $fromform->qprevrating; + $this->showqtext = $fromform->qtext; + $this->showresponses = $fromform->resp; + $this->showright = $fromform->right; + } + + public function setup_from_params() { + parent::setup_from_params(); + + $this->showansstate = optional_param('ansstate', $this->showansstate, PARAM_BOOL); + $this->showurating = optional_param('urating', $this->showurating, PARAM_BOOL); + $this->showuprevrating = optional_param('uprevrating', $this->showuprevrating, PARAM_BOOL); + $this->showqrating = optional_param('qrating', $this->showqrating, PARAM_BOOL); + $this->showqprevrating = optional_param('qprevrating', $this->showqprevrating, PARAM_BOOL); + $this->showqtext = optional_param('qtext', $this->showqtext, PARAM_BOOL); + $this->showresponses = optional_param('resp', $this->showresponses, PARAM_BOOL); + $this->showright = optional_param('right', $this->showright, PARAM_BOOL); + } + + public function setup_from_user_preferences() { + parent::setup_from_user_preferences(); + + $this->showansstate = get_user_preferences('capquizreport_attempts_ansstate', $this->showansstate); + $this->showurating = get_user_preferences('capquizreport_attempts_urating', $this->showurating); + $this->showuprevrating = get_user_preferences('capquizreport_attempts_uprevrating', $this->showuprevrating); + $this->showqrating = get_user_preferences('capquizreport_attempts_qrating', $this->showqrating); + $this->showqprevrating = get_user_preferences('capquizreport_attempts_qprevrating', $this->showqprevrating); + $this->showqtext = get_user_preferences('capquizreport_attempts_qtext', $this->showqtext); + $this->showresponses = get_user_preferences('capquizreport_attempts_resp', $this->showresponses); + $this->showright = get_user_preferences('capquizreport_attempts_right', $this->showright); + } + + public function update_user_preferences() { + parent::update_user_preferences(); + + set_user_preference('capquizreport_attempts_ansstate', $this->showansstate); + set_user_preference('capquizreport_attempts_urating', $this->showurating); + set_user_preference('capquizreport_attempts_uprevrating', $this->showuprevrating); + set_user_preference('capquizreport_attempts_qrating', $this->showqrating); + set_user_preference('capquizreport_attempts_qprevrating', $this->showqprevrating); + set_user_preference('capquizreport_attempts_qtext', $this->showqtext); + set_user_preference('capquizreport_attempts_resp', $this->showresponses); + set_user_preference('capquizreport_attempts_right', $this->showright); + } + + public function resolve_dependencies() { + parent::resolve_dependencies(); + + if (!$this->showansstate + && !$this->showurating + && !$this->showuprevrating + && !$this->showqrating + && !$this->showqprevrating + && !$this->showqtext + && !$this->showresponses + && !$this->showright) { + // We have to show at least something. + $this->showansstate = true; + $this->showurating = true; + $this->showqrating = true; + } + + // We only want to show the checkbox to delete attempts + // if the user has permissions and if the report mode is showing attempts. + $this->checkboxcolumn = has_capability('mod/capquiz:deleteattempts', context_module::instance($this->cm->id)) + && ($this->attempts != capquiz_attempts_report::ENROLLED_WITHOUT); + } + + protected function get_url_params() { + $params = parent::get_url_params(); + $params['ansstate'] = $this->showansstate; + $params['urating'] = $this->showurating; + $params['uprevrating'] = $this->showuprevrating; + $params['qrating'] = $this->showqrating; + $params['qprevrating'] = $this->showqprevrating; + $params['qtext'] = $this->showqtext; + $params['resp'] = $this->showresponses; + $params['right'] = $this->showright; + return $params; + } +} diff --git a/report/attempts/attempts_table.php b/report/attempts/attempts_table.php new file mode 100644 index 0000000..37bb6f3 --- /dev/null +++ b/report/attempts/attempts_table.php @@ -0,0 +1,267 @@ +. + +/** + * This file defines the capquiz attempts table for showing question attempts. + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace capquizreport_attempts; + +use core\dml\sql_join; +use mod_capquiz\report\capquiz_attempts_report_options; +use mod_capquiz\report\capquiz_attempts_report_table; +use moodle_url; +use quiz_responses_options; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport_table.php'); +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); + + +/** + * This is a table subclass for displaying the capquiz attempts report. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquizreport_attempts_table extends capquiz_attempts_report_table { + + /** + * Constructor + * @param object $capquiz + * @param context $context + * @param quiz_responses_options $options + * @param sql_join $groupstudentsjoins + * @param sql_join $studentsjoins + * @param array $questions + * @param moodle_url $reporturl + */ + public function __construct($capquiz, $context, capquiz_attempts_report_options $options, + sql_join $studentsjoins, $questions, $reporturl) { + parent::__construct('mod-capquiz-report-attempts-report', $capquiz, $context, + $options, $studentsjoins, $questions, $reporturl); + } + + public function build_table() { + if (!$this->rawdata) { + return; + } + + $this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetime')); + parent::build_table(); + } + + public function other_cols($colname, $attempt) { + switch ($colname) { + case 'question': + return $this->data_col($attempt->slot, 'questionsummary', $attempt); + case 'response': + return $this->data_col($attempt->slot, 'responsesummary', $attempt); + case 'right': + return $this->data_col($attempt->slot, 'rightanswer', $attempt); + default: + return null; + } + } + + public function data_col($slot, $field, $attempt) { + if ($attempt->usageid == 0) { + return '-'; + } + $value = $this->field_from_extra_data($attempt, $slot, $field); + + if (is_null($value)) { + $summary = '-'; + } else { + $summary = trim($value); + } + + if ($this->is_downloading() && $this->is_downloading() != 'html') { + return $summary; + } + $summary = s($summary); + + if ($this->is_downloading()) { + return $summary; + } + + if ($field === 'responsesummary') { + return $this->make_review_link($summary, $attempt, $slot); + + } else { + return $summary; + } + } + + /** + * Column text from the extra data loaded in load_extra_data(), before html formatting etc. + * + * @param object $attempt + * @param int $slot + * @param string $field + * @return string + */ + protected function field_from_extra_data($attempt, $slot, $field) { + if (!isset($this->lateststeps[$attempt->usageid][$slot])) { + return '-'; + } + $stepdata = $this->lateststeps[$attempt->usageid][$slot]; + + if (property_exists($stepdata, $field . 'full')) { + $value = $stepdata->{$field . 'full'}; + } else { + $value = $stepdata->$field; + } + return $value; + } + + /** + * Generate the display of the answer state column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_answerstate($attempt) { + if (is_null($attempt->attempt)) { + return '-'; + } + if ($attempt->usageid == 0) { + return '-'; + } + + $state = $this->slot_state($attempt, $attempt->slot); + if ($this->is_downloading()) { + return $state; + } else { + return $this->make_review_link($state, $attempt, $attempt->slot); + } + } + + /** + * Generate the display of the user rating column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_userrating($attempt) { + if ($attempt->userrating) { + return $attempt->userrating; + } else { + return '-'; + } + } + + /** + * Generate the display of the question rating column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_questionrating($attempt) { + if ($attempt->questionrating) { + return $attempt->questionrating; + } else { + return '-'; + } + } + + /** + * Generate the display of the previous user rating column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_prevuserrating($attempt) { + if ($attempt->userrating) { + return $attempt->prevuserrating; + } else { + return '-'; + } + } + + /** + * Generate the display of the previous question rating column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_prevquestionrating($attempt) { + global $OUTPUT; + if ($attempt->prevquestionrating) { + $warningicon = $OUTPUT->pix_icon('i/warning', get_string('rating_manually_updated', 'capquizreport_attempts'), + 'moodle', array('class' => 'icon')); + + if (!$this->is_downloading() && $attempt->manualprevqrating) { + return $warningicon . $attempt->prevquestionrating; + } else { + return $attempt->prevquestionrating; + } + } else { + return '-'; + } + } + + /** + * Generate the display of the previous question manual rating column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_prevquestionratingmanual($attempt) { + if (is_null($attempt->manualprevqrating)) { + return '-'; + } + $ismanual = ($attempt->manualprevqrating) ? 'true' : 'false'; + $manualprevqrating = get_string($ismanual, 'capquiz'); + return $manualprevqrating; + } + + protected function requires_latest_steps_loaded() { + if ($this->options->showansstate + || $this->options->showqtext + || $this->options->showresponses + || $this->options->showright) { + return true; + } else { + return false; + } + } + + protected function is_latest_step_column($column) { + if (preg_match('/^(?:question|response|right)/', $column, $matches)) { + return $matches[1]; + } + return false; + } + + protected function update_sql_after_count($fields, $from, $where, $params) { + $fields .= ', + cqr.rating AS questionrating, + pcqr.rating AS prevquestionrating, + pcqr.manual AS manualprevqrating, + cur.rating AS userrating, + pcur.rating AS prevuserrating, + pcur.rating AS manualprevurating'; + + $from .= "\nLEFT JOIN {capquiz_question_rating} cqr ON cqr.id = ca.question_rating_id"; + $from .= "\nLEFT JOIN {capquiz_question_rating} pcqr ON pcqr.id = ca.previous_question_rating_id"; + $from .= "\nLEFT JOIN {capquiz_user_rating} cur ON cur.id = ca.user_rating_id"; + $from .= "\nLEFT JOIN {capquiz_user_rating} pcur ON pcur.id = ca.previous_user_rating_id"; + + return [$fields, $from, $where, $params]; + } +} diff --git a/report/attempts/classes/privacy/provider.php b/report/attempts/classes/privacy/provider.php new file mode 100644 index 0000000..9132056 --- /dev/null +++ b/report/attempts/classes/privacy/provider.php @@ -0,0 +1,51 @@ +. + +/** + * Privacy Subsystem implementation for capquizreport_attempts. + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace capquizreport_attempts\privacy; + +use core_privacy\local\metadata\null_provider; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for capquizreport_attempts implementing null_provider. + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/report/attempts/lang/en/capquizreport_attempts.php b/report/attempts/lang/en/capquizreport_attempts.php new file mode 100644 index 0000000..b824f27 --- /dev/null +++ b/report/attempts/lang/en/capquizreport_attempts.php @@ -0,0 +1,50 @@ +. + +/** + * Strings for component 'capquizreport_attempts', language 'en' + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Attempts'; +$string['privacy:metadata'] = 'The capquizreport attempts plugin does not store any personal data.'; +$string['attemptsfilename'] = 'attempts'; + +$string['prevuserrating'] = 'Previous user rating'; +$string['prevquestionrating'] = 'Previous question rating'; +$string['uprevrating'] = 'previous user rating'; +$string['qprevrating'] = 'previous question rating'; +$string['qrating'] = 'question rating'; +$string['urating'] = 'user rating'; +$string['ansstate'] = 'answer state'; +$string['answerstate'] = 'Answer state'; +$string['userid'] = 'User id'; +$string['questionid'] = 'Question id'; +$string['question'] = 'Question'; +$string['response'] = 'Response'; +$string['rightanswer'] = 'Right answer'; +$string['true'] = 'True'; +$string['false'] = 'False'; +$string['correct'] = 'Correct'; +$string['wrong'] = 'Wrong'; +$string['timeanswered'] = 'Time answered'; +$string['timereviewed'] = 'Time reviewed'; +$string['prevquestionratingmanual'] = 'Rating manually updated'; +$string['rating_manually_updated'] = 'Rating has been manually updated'; diff --git a/report/attempts/report.php b/report/attempts/report.php new file mode 100644 index 0000000..780ef55 --- /dev/null +++ b/report/attempts/report.php @@ -0,0 +1,205 @@ +. + +/** + * CAPQuiz attempts report class. + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace capquizreport_attempts; + +use context_course; +use mod_capquiz\report\capquiz_attempts_report; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/capquiz/report/attemptsreport.php'); +require_once(__DIR__ . '/attempts_form.php'); +require_once(__DIR__ . '/attempts_table.php'); +require_once(__DIR__ . '/attempts_options.php'); + +/** + * The capquiz attempts report provides summary information about each question in + * a capquiz, compared to the whole capquiz. It also provides a drill-down to more + * detailed information about each question. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquizreport_attempts_report extends capquiz_attempts_report { + + public function display($capquiz, $cm, $course, $download) { + global $OUTPUT, $DB; + + list($studentsjoins) = $this->init( + 'attempts', 'capquizreport_attempts\capquizreport_attempts_settings_form', $capquiz, $cm, $course); + + $this->options = new capquizreport_attempts_options('attempts', $capquiz, $cm, $course); + + if ($fromform = $this->form->get_data()) { + $this->options->process_settings_from_form($fromform); + + } else { + $this->options->process_settings_from_params(); + } + + $this->form->set_data($this->options->get_initial_form_data()); + + // Load the required questions. + $questions = capquiz_report_get_questions($capquiz); + + // Prepare for downloading, if applicable. + $courseshortname = format_string($course->shortname, true, + array('context' => context_course::instance($course->id))); + + $table = new capquizreport_attempts_table($capquiz, $this->context, + $this->options, $studentsjoins, $questions, $this->options->get_url()); + $filename = capquiz_report_download_filename(get_string('attemptsfilename', 'capquizreport_attempts'), + $courseshortname, $capquiz->name()); + $table->is_downloading($this->options->download, $filename, + $courseshortname . ' ' . format_string($capquiz->name(), true)); + if ($table->is_downloading()) { + raise_memory_limit(MEMORY_EXTRA); + } + + $hasstudents = false; + if (!empty($studentsjoins->joins)) { + $sql = "SELECT DISTINCT u.id + FROM {user} u + $studentsjoins->joins + WHERE $studentsjoins->wheres"; + $hasstudents = $DB->record_exists_sql($sql, $studentsjoins->params); + } + + // TODO enable when support for attempt deletion is implemented {@link delete_selected_attempts}. + // $this->process_actions($capquiz, $cm, $studentsjoins, $this->options->get_url()); + + $hasquestions = capquiz_has_questions($capquiz->id()); + // Start output. + if (!$table->is_downloading()) { + // Only print headers if not asked to download data. + $this->print_standard_header_and_messages($cm, $course, $capquiz, + $this->options, $hasquestions, $hasstudents); + + // Print the display options. + $this->form->display(); + } + + if ($hasquestions && !empty($questions) && ($hasstudents || $this->options->attempts == self::ALL_WITH)) { + + $table->setup_sql_queries($studentsjoins); + + // Define table columns. + $columns = array(); + $headers = array(); + + if (!$table->is_downloading() && $this->options->checkboxcolumn) { + $columns[] = 'checkbox'; + $headers[] = null; + } + + $this->add_user_columns($table, $columns, $headers); + + if ($table->is_downloading()) { + $this->add_uesrid_column($columns, $headers); + $this->add_questionid_column($columns, $headers); + } + + if ($this->options->showansstate) { + $columns[] = 'answerstate'; + $headers[] = get_string('answerstate', 'capquizreport_attempts'); + } + + $this->add_rating_columns($columns, $headers); + + if ($table->is_downloading()) { + $columns[] = 'prevquestionratingmanual'; + $headers[] = get_string('prevquestionratingmanual', 'capquizreport_attempts'); + } + + if ($table->is_downloading()) { + $this->add_time_columns($columns, $headers); + } + + if ($this->options->showqtext) { + $columns[] = 'question'; + $headers[] = get_string('question', 'capquizreport_attempts'); + } + if ($this->options->showresponses) { + $columns[] = 'response'; + $headers[] = get_string('response', 'capquizreport_attempts'); + } + if ($this->options->showright) { + $columns[] = 'right'; + $headers[] = get_string('rightanswer', 'capquizreport_attempts'); + } + + $table->define_columns($columns); + $table->define_headers($headers); + $table->sortable(true, 'uniqueid'); + + // Set up the table. + $table->define_baseurl($this->options->get_url()); + + $this->configure_user_columns($table); + + $table->no_sorting('answerstate'); + $table->no_sorting('question'); + $table->no_sorting('response'); + $table->no_sorting('right'); + + $table->set_attribute('id', 'responses'); + + $table->collapsible(true); + + $table->out($this->options->pagesize, true); + } + return true; + } + + protected function add_rating_columns(array &$columns, array &$headers) { + if ($this->options->showurating) { + $this->add_user_rating_column($columns, $headers); + } + if ($this->options->showuprevrating) { + $columns[] = 'prevuserrating'; + $headers[] = get_string('prevuserrating', 'capquizreport_attempts'); + } + + if ($this->options->showqrating) { + $this->add_question_rating_column($columns, $headers); + } + if ($this->options->showqprevrating) { + $columns[] = 'prevquestionrating'; + $headers[] = get_string('prevquestionrating', 'capquizreport_attempts'); + } + } + + protected function add_user_rating_column(array &$columns, array &$headers) { + $columns[] = 'userrating'; + $headers[] = get_string('userrating', 'capquiz'); + } + + protected function add_question_rating_column(array &$columns, array &$headers) { + $columns[] = 'questionrating'; + $headers[] = get_string('questionrating', 'capquiz'); + } +} diff --git a/report/attempts/upgrade.txt b/report/attempts/upgrade.txt new file mode 100644 index 0000000..0f1004b --- /dev/null +++ b/report/attempts/upgrade.txt @@ -0,0 +1,2 @@ +This files describes API changes in /mod/capquiz/report/attempts/*, +information provided here is intended especially for developers. diff --git a/report/attempts/version.php b/report/attempts/version.php new file mode 100644 index 0000000..64168b0 --- /dev/null +++ b/report/attempts/version.php @@ -0,0 +1,35 @@ +. + +/** + * CAPQuiz attempts report version information. + * + * @package capquizreport_attempts + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2019071800; +$plugin->requires = 2016120500; +$plugin->component = 'capquizreport_attempts'; +$plugin->maturity = MATURITY_STABLE; +$plugin->release = 'v0.1.0'; +$plugin->dependencies = array( + 'mod_capquiz' => 2019071800, // The CAPQuiz plugin version 2019071800 or higher must be present. +); diff --git a/report/attemptsreport.php b/report/attemptsreport.php new file mode 100644 index 0000000..23b0475 --- /dev/null +++ b/report/attemptsreport.php @@ -0,0 +1,278 @@ +. + +/** + * The file defines a base class that can be used to build a report like the + * overview or responses report, that has one row per attempt. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_capquiz\report; + +use context_module; +use core\dml\sql_join; +use mod_quiz_attempts_report_form; +use mod_quiz_attempts_report_options; +use moodle_url; +use stdClass; +use table_sql; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); +require_once($CFG->dirroot . '/mod/capquiz/report/report.php'); + + +/** + * Base class for capquiz reports that are basically a table with one row for each attempt. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class capquiz_attempts_report extends report { + /** @var int default page size for reports. */ + const DEFAULT_PAGE_SIZE = 30; + + /** @var string constant used for the options, means all users with attempts. */ + const ALL_WITH = 'all_with'; + /** @var string constant used for the options, means only enrolled users with attempts. */ + const ENROLLED_WITH = 'enrolled_with'; + /** @var string constant used for the options, means only enrolled users without attempts. */ + const ENROLLED_WITHOUT = 'enrolled_without'; + /** @var string constant used for the options, means all enrolled users. */ + const ENROLLED_ALL = 'enrolled_any'; + + /** @var string the mode this report is. */ + protected $mode; + + /** @var object the capquiz context. */ + protected $context; + + /** @var mod_quiz_attempts_report_form The settings form to use. */ + protected $form; + + /** @var object mod_quiz_attempts_report_options the options affecting this report. */ + protected $options = null; + + /** + * Initialise various aspects of this report. + * + * @param string $mode + * @param string $formclass + * @param object $capquiz + * @param object $cm + * @param object $course + * @return array with four elements: + * 0 => integer the current group id (0 for none). + * 1 => \core\dml\sql_join Contains joins, wheres, params for all the students in this course. + * 2 => \core\dml\sql_join Contains joins, wheres, params for all the students in the current group. + * 3 => \core\dml\sql_join Contains joins, wheres, params for all the students to show in the report. + * Will be the same as either element 1 or 2. + */ + protected function init($mode, $formclass, $capquiz, $cm, $course) { + $this->mode = $mode; + + $this->context = context_module::instance($cm->id); + + $studentsjoins = get_enrolled_with_capabilities_join($this->context); + + $this->form = new $formclass($this->get_base_url(), + array('capquiz' => $capquiz, 'context' => $this->context)); + + return array($studentsjoins); + } + + + /** + * Get the base URL for this report. + * @return moodle_url the URL. + */ + protected function get_base_url() { + return new moodle_url('/mod/capquiz/view_report.php', + array('id' => $this->context->instanceid, 'mode' => $this->mode)); + } + + /** + * Outputs the things you commonly want at the top of a capquiz report. + * + * Calls through to {@link print_header_and_tabs()} and then + * outputs the standard group selector, number of attempts summary, + * and messages to cover common cases when the report can't be shown. + * + * @param stdClass $cm the course_module information. + * @param stdClass $course the course settings. + * @param stdClass $capquiz the capquiz settings. + * @param mod_quiz_attempts_report_options $options the current report settings. + * @param int $currentgroup the current group. + * @param bool $hasquestions whether there are any questions in the capquiz. + * @param bool $hasstudents whether there are any relevant students. + */ + protected function print_standard_header_and_messages($cm, $course, $capquiz, + $options, $hasquestions, $hasstudents) { + global $OUTPUT; + + echo $this->print_header_and_tabs($cm, $course, $capquiz, $this->mode); + + // Print information on the number of existing attempts. + if ($strattemptnum = capquiz_num_attempt_summary($capquiz, true)) { + echo '
' . $strattemptnum . '
'; + } + + if (!$hasquestions) { + echo capquiz_no_questions_message($capquiz, $cm, $this->context); + } else if (!$capquiz->is_published()) { + echo capquiz_not_published_message($capquiz, $cm, $this->context); + } else if (!$hasstudents) { + echo $OUTPUT->notification(get_string('nostudentsyet')); + } + + } + + /** + * Add all the user-related columns to the $columns and $headers arrays. + * @param table_sql $table the table being constructed. + * @param array $columns the list of columns. Added to. + * @param array $headers the columns headings. Added to. + */ + protected function add_user_columns($table, &$columns, &$headers) { + global $CFG; + if (!$table->is_downloading() && $CFG->grade_report_showuserimage) { + $columns[] = 'picture'; + $headers[] = ''; + } + if (!$table->is_downloading()) { + $columns[] = 'fullname'; + $headers[] = get_string('name'); + } else { + $columns[] = 'lastname'; + $headers[] = get_string('lastname'); + $columns[] = 'firstname'; + $headers[] = get_string('firstname'); + } + + // When downloading, some extra fields are always displayed (because + // there's no space constraint) so do not include in extra-field list. + $extrafields = get_extra_user_fields($this->context, + $table->is_downloading() ? array('institution', 'department', 'email') : array()); + foreach ($extrafields as $field) { + $columns[] = $field; + $headers[] = get_user_field_name($field); + } + + if ($table->is_downloading()) { + $columns[] = 'institution'; + $headers[] = get_string('institution'); + + $columns[] = 'department'; + $headers[] = get_string('department'); + + $columns[] = 'email'; + $headers[] = get_string('email'); + } + } + + /** + * Add the state column to the $columns and $headers arrays. + * @param array $columns the list of columns. Added to. + * @param array $headers the columns headings. Added to. + */ + protected function add_questionid_column(&$columns, &$headers) { + $columns[] = 'questionid'; + $headers[] = get_string('questionid', 'capquizreport_attempts'); + } + + /** + * Add the state column to the $columns and $headers arrays. + * @param array $columns the list of columns. Added to. + * @param array $headers the columns headings. Added to. + */ + protected function add_uesrid_column(&$columns, &$headers) { + $columns[] = 'userid'; + $headers[] = get_string('userid', 'capquizreport_attempts'); + } + + /** + * Add all the time-related columns to the $columns and $headers arrays. + * @param array $columns the list of columns. Added to. + * @param array $headers the columns headings. Added to. + */ + protected function add_time_columns(&$columns, &$headers) { + $columns[] = 'timeanswered'; + $headers[] = get_string('timeanswered', 'capquizreport_attempts'); + + $columns[] = 'timereviewed'; + $headers[] = get_string('timereviewed', 'capquizreport_attempts'); + + } + + /** + * Set the display options for the user-related columns in the table. + * @param table_sql $table the table being constructed. + */ + protected function configure_user_columns($table) { + $table->column_suppress('picture'); + $table->column_suppress('fullname'); + $extrafields = get_extra_user_fields($this->context); + foreach ($extrafields as $field) { + $table->column_suppress($field); + } + + $table->column_class('picture', 'picture'); + $table->column_class('lastname', 'bold'); + $table->column_class('firstname', 'bold'); + $table->column_class('fullname', 'bold'); + } + + /** + * Process any submitted actions. + * @param object $quiz the capquiz settings. + * @param object $cm the cm object for the capquiz. + * @param int $currentgroup the currently selected group. + * @param sql_join $groupstudentsjoins (joins, wheres, params) the students in the current group. + * @param sql_join $allowedjoins (joins, wheres, params) the users whose attempt this user is allowed to modify. + * @param moodle_url $redirecturl where to redircet to after a successful action. + */ + protected function process_actions($quiz, $cm, sql_join $allowedjoins, $redirecturl) { + if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) { + if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) { + require_capability('mod/capquiz:deleteattempts', $this->context); + $this->delete_selected_attempts($quiz, $cm, $attemptids, $allowedjoins); + redirect($redirecturl); + } + } + } + + /** + * Delete the capquiz attempts + * @param object $capquiz the capquiz settings. Attempts that don't belong to + * this capquiz are not deleted. + * @param object $cm the course_module object. + * @param array $attemptids the list of attempt ids to delete. + * @param sql_join $allowedjoins (joins, wheres, params) This list of userids that are visible in the report. + * Users can only delete attempts that they are allowed to see in the report. + * Empty means all users. + */ + protected function delete_selected_attempts($capquiz, $cm, $attemptids, sql_join $allowedjoins) { + global $DB; + // TODO implement to add support for attempt deletion. + + } +} diff --git a/report/attemptsreport_form.php b/report/attemptsreport_form.php new file mode 100644 index 0000000..9469f10 --- /dev/null +++ b/report/attemptsreport_form.php @@ -0,0 +1,90 @@ +. + +/** + * Base class for the settings form for {@link capquiz_attempts_report}s. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_capquiz\report; + +use moodleform; +use MoodleQuickForm; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + + +/** + * Base class for the settings form for {@link capquiz_attempts_report}s. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class capquiz_attempts_report_form extends moodleform { + + public function validation($data, $files) { + $errors = parent::validation($data, $files); + return $errors; + } + + protected function definition() { + $mform = $this->_form; + + $mform->addElement('header', 'preferencespage', + get_string('reportwhattoinclude', 'quiz')); + + $this->standard_attempt_fields($mform); + $this->other_attempt_fields($mform); + + $mform->addElement('header', 'preferencesuser', + get_string('reportdisplayoptions', 'quiz')); + + $this->standard_preference_fields($mform); + $this->other_preference_fields($mform); + + $mform->addElement('submit', 'submitbutton', + get_string('showreport', 'quiz')); + } + + protected function standard_attempt_fields(MoodleQuickForm $mform) { + + $mform->addElement('select', 'attempts', get_string('reportattemptsfrom', 'quiz'), array( + capquiz_attempts_report::ENROLLED_WITH => get_string('reportuserswith', 'quiz'), + // capquiz_attempts_report::ENROLLED_WITHOUT => get_string('reportuserswithout', 'quiz'), + // capquiz_attempts_report::ENROLLED_ALL => get_string('reportuserswithorwithout', 'quiz'), + capquiz_attempts_report::ALL_WITH => get_string('reportusersall', 'quiz'), + )); + + } + + protected function other_attempt_fields(MoodleQuickForm $mform) { + } + + protected function standard_preference_fields(MoodleQuickForm $mform) { + $mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz')); + $mform->setType('pagesize', PARAM_INT); + } + + protected function other_preference_fields(MoodleQuickForm $mform) { + } +} diff --git a/report/attemptsreport_options.php b/report/attemptsreport_options.php new file mode 100644 index 0000000..fcd0e32 --- /dev/null +++ b/report/attemptsreport_options.php @@ -0,0 +1,186 @@ +. + +/** + * Base class for the options that control what is visible in an {@link quiz_attempts_report}. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_capquiz\report; + +use mod_capquiz\capquiz; +use moodle_url; +use stdClass; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/formslib.php'); + + +/** + * Base class for the options that control what is visible in an {@link quiz_attempts_report}. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquiz_attempts_report_options { + + /** @var string the report mode. */ + public $mode; + + /** @var object the settings for the capquiz being reported on. */ + public $capquiz; + + /** @var object the course module objects for the capquiz being reported on. */ + public $cm; + + /** @var object the course settings for the course the capquiz is in. */ + public $course; + + /** + * @var string capquiz_attempts_report::ALL_WITH or capquiz_attempts_report::ENROLLED_WITH + * capquiz_attempts_report::ENROLLED_WITHOUT or capquiz_attempts_report::ENROLLED_ALL + */ + public $attempts = capquiz_attempts_report::ENROLLED_WITH; + + /** @var int Number of attempts to show per page. */ + public $pagesize = capquiz_attempts_report::DEFAULT_PAGE_SIZE; + + /** @var string whether the data should be downloaded in some format, or '' to display it. */ + public $download = ''; + + /** @var bool whether the report table should have a column of checkboxes. */ + public $checkboxcolumn = false; + + /** + * Constructor. + * @param string $mode which report these options are for. + * @param object $capquiz the settings for the capquiz being reported on. + * @param object $cm the course module objects for the capquiz being reported on. + * @param object $coures the course settings for the coures this capquiz is in. + */ + public function __construct($mode, capquiz $capquiz, $cm, $course) { + $this->mode = $mode; + $this->capquiz = $capquiz; + $this->cm = $cm; + $this->course = $course; + } + + /** + * Get the URL parameters required to show the report with these options. + * @return array URL parameter name => value. + */ + protected function get_url_params() { + $params = array( + 'id' => $this->cm->id, + 'mode' => $this->mode, + 'attempts' => $this->attempts, + ); + + return $params; + } + + /** + * Get the URL to show the report with these options. + * @return moodle_url the URL. + */ + public function get_url() { + return new moodle_url('/mod/capquiz/view_report.php', $this->get_url_params()); + } + + /** + * Process the data we get when the settings form is submitted. This includes + * updating the fields of this class, and updating the user preferences + * where appropriate. + * @param object $fromform The data from $mform->get_data() from the settings form. + */ + public function process_settings_from_form($fromform) { + $this->setup_from_form_data($fromform); + $this->resolve_dependencies(); + $this->update_user_preferences(); + } + + /** + * Set up this preferences object using optional_param (using user_preferences + * to set anything not specified by the params. + */ + public function process_settings_from_params() { + $this->setup_from_user_preferences(); + $this->setup_from_params(); + $this->resolve_dependencies(); + } + + /** + * Get the current value of the settings to pass to the settings form. + */ + public function get_initial_form_data() { + $toform = new stdClass(); + $toform->attempts = $this->attempts; + $toform->pagesize = $this->pagesize; + + return $toform; + } + + /** + * Set the fields of this object from the form data. + * @param object $fromform The data from $mform->get_data() from the settings form. + */ + public function setup_from_form_data($fromform) { + $this->attempts = $fromform->attempts; + $this->pagesize = $fromform->pagesize; + } + + /** + * Set the fields of this object from the URL parameters. + */ + public function setup_from_params() { + $this->attempts = optional_param('attempts', $this->attempts, PARAM_ALPHAEXT); + $this->pagesize = optional_param('pagesize', $this->pagesize, PARAM_INT); + $this->download = optional_param('download', $this->download, PARAM_ALPHA); + } + + /** + * Set the fields of this object from the user's preferences. + * (For those settings that are backed by user-preferences). + */ + public function setup_from_user_preferences() { + $this->pagesize = get_user_preferences('capquiz_report_pagesize', $this->pagesize); + } + + /** + * Update the user preferences so they match the settings in this object. + * (For those settings that are backed by user-preferences). + */ + public function update_user_preferences() { + set_user_preference('capquiz_report_pagesize', $this->pagesize); + } + + /** + * Check the settings, and remove any 'impossible' combinations. + */ + public function resolve_dependencies() { + if ($this->pagesize < 1) { + $this->pagesize = capquiz_attempts_report::DEFAULT_PAGE_SIZE; + } + } + + +} diff --git a/report/attemptsreport_table.php b/report/attemptsreport_table.php new file mode 100644 index 0000000..5103dfe --- /dev/null +++ b/report/attemptsreport_table.php @@ -0,0 +1,538 @@ +. + +/** + * Base class for the table used by a {@link quiz_attempts_report}. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_capquiz\report; + +use coding_exception; +use core\dml\sql_join; +use html_writer; +use mod_quiz_attempts_report_options; +use moodle_url; +use qubaid_condition; +use qubaid_list; +use question_engine_data_mapper; +use question_state; +use stdClass; +use table_sql; +use user_picture; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/tablelib.php'); + + +/** + * Base class for the table used by a {@link capquiz_attempts_report}. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class capquiz_attempts_report_table extends table_sql { + public $useridfield = 'userid'; + + /** @var moodle_url the URL of this report. */ + protected $reporturl; + + /** @var array the display options. */ + protected $displayoptions; + + /** + * @var array information about the latest step of each question. + * Loaded by {@link load_question_latest_steps()}, if applicable. + */ + protected $lateststeps = null; + + /** @var object the capquiz settings for the capquiz we are reporting on. */ + protected $capquiz; + + /** @var context the capquiz context. */ + protected $context; + + /** @var object mod_quiz_attempts_report_options the options affecting this report. */ + protected $options; + + /** @var sql_join Contains joins, wheres, params to find the students in the course. */ + protected $studentsjoins; + + /** @var object the questions that comprise this capquiz.. */ + protected $questions; + + /** @var bool whether to include the column with checkboxes to select each attempt. */ + protected $includecheckboxes; + + /** + * Constructor + * @param string $uniqueid + * @param object $quiz + * @param context $context + * @param mod_quiz_attempts_report_options $options + * @param sql_join $groupstudentsjoins Contains joins, wheres, params + * @param sql_join $studentsjoins Contains joins, wheres, params + * @param array $questions + * @param moodle_url $reporturl + */ + public function __construct($uniqueid, $quiz, $context, + capquiz_attempts_report_options $options, sql_join $studentsjoins, + $questions, $reporturl) { + parent::__construct($uniqueid); + $this->capquiz = $quiz; + $this->context = $context; + $this->studentsjoins = $studentsjoins; + $this->questions = $questions; + $this->includecheckboxes = $options->checkboxcolumn; + $this->reporturl = $reporturl; + $this->options = $options; + } + + /** + * Generate the display of the checkbox column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_checkbox($attempt) { + if ($attempt->attempt) { + return ''; + } else { + return ''; + } + } + + /** + * Generate the display of the user's picture column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_picture($attempt) { + global $OUTPUT; + $user = new stdClass(); + $additionalfields = explode(',', user_picture::fields()); + $user = username_load_fields_from_object($user, $attempt, null, $additionalfields); + $user->id = $attempt->userid; + return $OUTPUT->user_picture($user); + } + + /** + * Generate the display of the user's full name column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_fullname($attempt) { + $html = parent::col_fullname($attempt); + if ($this->is_downloading() || empty($attempt->attempt)) { + return $html; + } + return $html; /*. html_writer::empty_tag('br') . html_writer::link( + new moodle_url('/mod/capquiz/review.php', array('attempt' => $attempt->attempt)), + get_string('reviewattempt', 'quiz'), array('class' => 'reviewlink'))*/ + } + + /** + * Generate the display of the time answered column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_timeanswered($attempt) { + if ($attempt->attempt) { + return userdate($attempt->timeanswered, $this->strtimeformat); + } else { + return '-'; + } + } + + /** + * Generate the display of the time answered column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_timereviewed($attempt) { + if ($attempt->attempt) { + return userdate($attempt->timereviewed, $this->strtimeformat); + } else { + return '-'; + } + } + + + /** + * Generate the display of the question id column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_questionid($attempt) { + if ($attempt->questionid) { + return $attempt->questionid; + } else { + return '-'; + } + } + + /** + * Generate the display of the user id column. + * @param object $attempt the table row being output. + * @return string HTML content to go inside the td. + */ + public function col_userid($attempt) { + if ($attempt->userid) { + return $attempt->userid; + } else { + return '-'; + } + } + + /** + * Make a link to review an individual question in a popup window. + * + * @param string $data HTML fragment. The text to make into the link. + * @param object $attempt data for the row of the table being output. + * @param int $slot the number used to identify this question within this usage. + */ + public function make_review_link($data, $attempt, $slot) { + global $OUTPUT; + + $feedbackimg = ''; + $state = $this->slot_state($attempt, $slot); + if ($state->is_finished() && $state != question_state::$needsgrading) { + $feedbackimg = $this->icon_for_fraction($this->slot_fraction($attempt, $slot)); + } + + $output = html_writer::tag('span', $feedbackimg . html_writer::tag('span', + $data, array('class' => $state->get_state_class(true))), array('class' => 'que')); + + $reviewparams = array('attempt' => $attempt->attempt, 'slot' => $slot); + if (isset($attempt->try)) { + $reviewparams['step'] = $this->step_no_for_try($attempt->usageid, $slot, $attempt->try); + } + + // TODO enable this when capquiz implements a "review question attempt" page. + /*$url = new moodle_url('/mod/capquiz/reviewquestion.php', $reviewparams); + $output = $OUTPUT->action_link($url, $output, + new popup_action('click', $url, 'reviewquestion', + array('height' => 450, 'width' => 650)), + array('title' => get_string('reviewresponse', 'quiz')));*/ + + return $output; + } + + /** + * @param object $attempt the row data + * @param int $slot + * @return question_state + */ + protected function slot_state($attempt, $slot) { + $stepdata = $this->lateststeps[$attempt->usageid][$slot]; + return question_state::get($stepdata->state); + } + + /** + * Return an appropriate icon (green tick, red cross, etc.) for a grade. + * @param float $fraction grade on a scale 0..1. + * @return string html fragment. + */ + protected function icon_for_fraction($fraction) { + global $OUTPUT; + + $feedbackclass = question_state::graded_state_for_fraction($fraction)->get_feedback_class(); + return $OUTPUT->pix_icon('i/grade_' . $feedbackclass, get_string($feedbackclass, 'question'), + 'moodle', array('class' => 'icon')); + } + + /** + * @param object $attempt the row data + * @param int $slot + * @return float + */ + protected function slot_fraction($attempt, $slot) { + $stepdata = $this->lateststeps[$attempt->usageid][$slot]; + return $stepdata->fraction; + } + + /** + * Set up the SQL queries (count rows, and get data). + * + * @param sql_join $allowedjoins (joins, wheres, params) defines allowed users for the report. + */ + public function setup_sql_queries($allowedjoins) { + list($fields, $from, $where, $params) = $this->base_sql($allowedjoins); + + // The WHERE clause is vital here, because some parts of tablelib.php will expect to + // add bits like ' AND x = 1' on the end, and that needs to leave to valid SQL. + $this->set_count_sql("SELECT COUNT(1) FROM (SELECT $fields FROM $from WHERE $where) temp WHERE 1 = 1", $params); + + list($fields, $from, $where, $params) = $this->update_sql_after_count($fields, $from, $where, $params); + $this->set_sql($fields, $from, $where, $params); + } + + /** + * Contruct all the parts of the main database query. + * @param sql_join $allowedstudentsjoins (joins, wheres, params) defines allowed users for the report. + * @return array with 4 elements ($fields, $from, $where, $params) that can be used to + * build the actual database query. + */ + public function base_sql(sql_join $allowedstudentsjoins) { + global $DB; + + $fields = 'DISTINCT ' . $DB->sql_concat('u.id', "'#'", 'COALESCE(ca.id, 0)') . ' AS uniqueid,'; + + $extrafields = get_extra_user_fields_sql($this->context, 'u', '', + array('id', 'idnumber', 'firstname', 'lastname', 'picture', + 'imagealt', 'institution', 'department', 'email')); + $allnames = get_all_user_name_fields(true, 'u'); + $fields .= ' + cql.question_usage_id AS usageid, + ca.id AS attempt, + u.id AS userid, + u.idnumber, ' . $allnames . ', + u.picture, + u.imagealt, + u.institution, + u.department, + u.email' . $extrafields . ', + ca.slot, + ca.time_answered AS timeanswered, + ca.time_reviewed AS timereviewed, + cq.question_id AS questionid'; + + // This part is the same for all cases. Join the users and capquiz_attempts tables. + $from = " {user} u"; + $from .= "\nJOIN {capquiz_user} cu ON u.id = cu.user_id"; + $from .= "\nLEFT JOIN {capquiz_question_list} cql + ON cql.capquiz_id = :capquizid + AND cql.is_template = 0"; + + $from .= "\nJOIN {question_usages} qu ON qu.id = cql.question_usage_id"; + $from .= "\nJOIN {question_attempts} qa ON qa.questionusageid = qu.id"; + + $from .= "\nJOIN {capquiz_attempt} ca ON ca.user_id = cu.id AND ca.slot = qa.slot"; + $from .= "\nJOIN {capquiz_question} cq ON cq.question_list_id = cql.id AND cq.id = ca.question_id"; + + $params = array('capquizid' => $this->capquiz->id()); + + switch ($this->options->attempts) { + case capquiz_attempts_report::ALL_WITH: + // Show all attempts, including students who are no longer in the course. + $where = 'ca.id IS NOT NULL'; + break; + case capquiz_attempts_report::ENROLLED_WITH: + // Show only students with attempts. + $from .= "\n" . $allowedstudentsjoins->joins; + $where = "ca.id IS NOT NULL AND " . $allowedstudentsjoins->wheres; + $params = array_merge($params, $allowedstudentsjoins->params); + break; + /* + case capquiz_attempts_report::ENROLLED_WITHOUT: + // Show only students without attempts. + $from .= "\n" . $allowedstudentsjoins->joins; + $where = "ca.id IS NULL AND " . $allowedstudentsjoins->wheres; + $params = array_merge($params, $allowedstudentsjoins->params); + break; + case capquiz_attempts_report::ENROLLED_ALL: + // Show all students with or without attempts. + $from .= "\n" . $allowedstudentsjoins->joins; + $where = $allowedstudentsjoins->wheres; + $params = array_merge($params, $allowedstudentsjoins->params); + break; + */ + } + + return array($fields, $from, $where, $params); + } + + /** + * A chance for subclasses to modify the SQL after the count query has been generated, + * and before the full query is constructed. + * @param string $fields SELECT list. + * @param string $from JOINs part of the SQL. + * @param string $where WHERE clauses. + * @param array $params Query params. + * @return array with 4 elements ($fields, $from, $where, $params) as from base_sql. + */ + protected function update_sql_after_count($fields, $from, $where, $params) { + return [$fields, $from, $where, $params]; + } + + public function query_db($pagesize, $useinitialsbar = true) { + parent::query_db($pagesize, $useinitialsbar); + + if ($this->requires_extra_data()) { + $this->load_extra_data(); + } + } + + /** + * Does this report require loading any more data after the main query. After the main query then + * you can use $this->get + * + * @return bool should {@link query_db()} call {@link load_extra_data}? + */ + protected function requires_extra_data() { + return $this->requires_latest_steps_loaded(); + } + + /** + * Does this report require the detailed information for each question from the + * question_attempts_steps table? + * @return bool should {@link load_extra_data} call {@link load_question_latest_steps}? + */ + protected function requires_latest_steps_loaded() { + return false; + } + + /** + * Load any extra data after main query. At this point you can call {@link get_qubaids_condition} to get the condition that + * limits the query to just the question usages shown in this report page or alternatively for all attempts if downloading a + * full report. + */ + protected function load_extra_data() { + $this->lateststeps = $this->load_question_latest_steps(); + } + + /** + * Load information about the latest state of selected questions in selected attempts. + * + * The results are returned as an two dimensional array $qubaid => $slot => $dataobject + * + * @param qubaid_condition|null $qubaids used to restrict which usages are included + * in the query. See {@link qubaid_condition}. + * @return array of records. See the SQL in this function to see the fields available. + */ + protected function load_question_latest_steps(qubaid_condition $qubaids = null) { + if ($qubaids === null) { + $qubaids = $this->get_qubaids_condition(); + } + + $dm = new question_engine_data_mapper(); + $latesstepdata = $dm->load_questions_usages_latest_steps( + $qubaids, array_keys($this->questions)); + + $lateststeps = array(); + foreach ($latesstepdata as $step) { + $lateststeps[$step->questionusageid][$step->slot] = $step; + } + + return $lateststeps; + } + + /** + * Get an appropriate qubaid_condition for loading more data about the + * attempts we are displaying. + * @return qubaid_condition + */ + protected function get_qubaids_condition() { + if (is_null($this->rawdata)) { + throw new coding_exception( + 'Cannot call get_qubaids_condition until the main data has been loaded.'); + } + $qubaids = array(); + foreach ($this->rawdata as $attempt) { + if ($attempt->usageid > 0) { + $qubaids[] = $attempt->usageid; + } + } + + return new qubaid_list($qubaids); + } + + public function get_sort_columns() { + // Add attemptid as a final tie-break to the sort. This ensures that + // Attempts by the same student appear in order when just sorting by name. + $sortcolumns = parent::get_sort_columns(); + $sortcolumns['ca.id'] = SORT_ASC; + return $sortcolumns; + } + + public function wrap_html_start() { + if ($this->is_downloading() || !$this->includecheckboxes) { + return; + } + + $url = $this->options->get_url(); + $url->param('sesskey', sesskey()); + + echo '
'; + echo '
'; + + echo html_writer::input_hidden_params($url); + echo '
'; + } + + public function wrap_html_finish() { + global $PAGE; + if ($this->is_downloading() || !$this->includecheckboxes) { + return; + } + + echo '
'; + echo '' . + get_string('selectall', 'quiz') . ' / '; + echo '' . + get_string('selectnone', 'quiz') . ' '; + $PAGE->requires->js_amd_inline(" + require(['jquery'], function($) { + $('#checkattempts').click(function(e) { + $('#attemptsform').find('input:checkbox').prop('checked', true); + e.preventDefault(); + }); + $('#uncheckattempts').click(function(e) { + $('#attemptsform').find('input:checkbox').prop('checked', false); + e.preventDefault(); + }); + });"); + echo '  '; + + // TODO enable when support for attempt deletion is added {@link delete_selected_attempts}. + // $this->submit_buttons(); + echo '
'; + + // Close the form. + echo '
'; + echo '
'; + } + + /** + * Is this a column that depends on joining to the latest state information? + * If so, return the corresponding slot. If not, return false. + * @param string $column a column name + * @return int false if no, else a slot. + */ + protected function is_latest_step_column($column) { + return false; + } + + /** + * Output any submit buttons required by the $this->includecheckboxes form. + */ + protected function submit_buttons() { + global $PAGE; + if (has_capability('mod/capquiz:deleteattempts', $this->context)) { + echo ''; + $PAGE->requires->event_handler('#deleteattemptsbutton', 'click', 'M.util.show_confirm_dialog', + array('message' => get_string('deleteattemptcheck', 'quiz'))); + } + } + +} diff --git a/report/report.php b/report/report.php new file mode 100644 index 0000000..e477be5 --- /dev/null +++ b/report/report.php @@ -0,0 +1,86 @@ +. + +/** + * Base class for capquiz report plugins. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_capquiz\report; + +defined('MOODLE_INTERNAL') || die(); + +use context_module; +use mod_capquiz\capquiz; +use stdClass; + +/** + * Base class for capquiz report plugins. + * + * Doesn't do anything on it's own -- it needs to be extended. + * This class displays capquiz reports. + * + * This file can refer to itself as report.php to pass variables + * to itself - all these will also be globally available. + * + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report { + /** + * displays the full report + * @param capquiz $capquiz capquiz object + * @param stdClass $cm - course_module object + * @param stdClass $course - course object + * @param string $download - type of download being requested + */ + public function display($capquiz, $cm, $course, $download) { + // This function renders the html for the report. + return true; + } + + /** + * allows the plugin to control who can see this plugin. + * @return boolean + */ + public function canview($contextmodule) { + return true; + } + + /** + * Initialise some parts of $PAGE and start output. + * + * @param object $cm the course_module information. + * @param object $coures the course settings. + * @param object $capquiz the capquiz settings. + * @param string $reportmode the report name. + */ + public function print_header_and_tabs($cm, $course, $capquiz, $reportmode = 'attempts') { + global $PAGE, $OUTPUT; + // Print the page header. + $PAGE->set_title($capquiz->name()); + $PAGE->set_heading($course->fullname); + $context = context_module::instance($cm->id); + echo $OUTPUT->heading(format_string( + get_string('pluginname', 'capquizreport_' . $reportmode) . ' ' . get_string('report', 'capquiz'), + true, array('context' => $context))); + } +} diff --git a/report/reportfactory.php b/report/reportfactory.php new file mode 100644 index 0000000..867ae7a --- /dev/null +++ b/report/reportfactory.php @@ -0,0 +1,71 @@ +. + +namespace mod_capquiz\report; + +use capquiz_exception; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/capquiz/locallib.php'); + +/** + * Capquiz report factory. Provides a convenient way to create an capquiz report of any type. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class capquiz_report_factory { + + /** + * Create an capquiz report of a given type and return it. + * @param string $type the required type. + * @param $capquiz + * @param $cm + * @param $course + * @return capquiz_attempts_report the requested capquiz report. + * @throws capquiz_exception + */ + public static function make($type) { + $class = self::class_for_type($type); + + return new $class(); + } + + /** + * The class name corresponding to an report type. + * @param string $type report type name. + * @return string corresponding class name. + */ + protected static function class_for_type($type) { + global $CFG; + $typelc = strtolower($type); + $file = $CFG->dirroot . '/mod/capquiz/report/' . $type . '/report.php'; + $class = "capquizreport_{$typelc}\\capquizreport_{$typelc}_report"; + if (!is_readable($file)) { + throw new capquiz_exception('capquiz_report_factory: unknown report type ' . $type); + } + include_once($file); + + if (!class_exists($class)) { + throw new capquiz_exception('capquiz_report_factory: report type ' . $type . + ' does not define the expected class ' . $class); + } + return $class; + } +} diff --git a/report/reportlib.php b/report/reportlib.php new file mode 100644 index 0000000..dcbd17a --- /dev/null +++ b/report/reportlib.php @@ -0,0 +1,196 @@ +. + +/** + * Helper functions for the capquiz reports. + * + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +use mod_capquiz\capquiz; +use mod_capquiz\capquiz_urls; +use mod_capquiz\report\capquiz_report_factory; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/mod/capquiz/lib.php'); +require_once($CFG->libdir . '/filelib.php'); + +/* Generates and returns list of available CAPQuiz report sub-plugins + * + * @param context context level to check caps against + * @return array list of valid reports present + */ +function capquiz_report_list($context) { + static $reportlist; + if (!empty($reportlist)) { + return $reportlist; + } + $installed = core_component::get_plugin_list('capquizreport'); + foreach ($installed as $reportname => $notused) { + $report = capquiz_report_factory::make($reportname); + + if ($report->canview($context)) { + $reportlist[] = $reportname; + } + continue; + } + return $reportlist; +} + +/** + * Create a filename for use when downloading data from a capquiz report. It is + * expected that this will be passed to flexible_table::is_downloading, which + * cleans the filename of bad characters and adds the file extension. + * @param string $report the type of report. + * @param string $courseshortname the course shortname. + * @param string $capquizname the capquiz name. + * @return string the filename. + */ +function capquiz_report_download_filename($report, $courseshortname, $capquizname) { + return $courseshortname . '-' . format_string($capquizname, true) . '-' . $report; +} + +/** + * Are there any questions in this capquiz? + * @param int $capquizid the capquizid id. + */ +function capquiz_has_questions($capquizid) { + global $DB; + $sql = 'SELECT cq.id + FROM {capquiz_question} cq + JOIN {capquiz_question_list} cql ON cql.capquiz_id = :capquizid AND cql.is_template = 0 + WHERE cq.question_list_id = cql.id'; + return $DB->record_exists_sql($sql, ['capquizid' => $capquizid]); +} + +/** + * Get the questions in this capquiz, in order. + * @param object $capquiz the capquiz. + * @return array of slot => $question object with fields + * ->slot, ->id, ->qtype, ->length. + */ +function capquiz_report_get_questions(capquiz $capquiz) { + global $DB; + $qsbyslot = $DB->get_records_sql(" + SELECT DISTINCT + ca.slot, + q.id, + q.qtype, + q.length + + FROM {question} q + JOIN {capquiz_question} cq ON cq.question_id = q.id + JOIN {capquiz_question_list} cql ON cql.id = cq.question_list_id AND cql.is_template = 0 + JOIN {question_usages} qu ON qu.id = cql.question_usage_id + JOIN {question_attempts} qa ON qa.questionusageid = qu.id + JOIN {capquiz_attempt} ca ON ca.question_id = cq.id AND ca.slot = qa.slot + + WHERE cql.capquiz_id = ? + AND q.length > 0 + + ORDER BY ca.slot", array($capquiz->id())); + + $number = 1; + foreach ($qsbyslot as $question) { + $question->number = $number; + $number += $question->length; + $question->type = $question->qtype; + } + + return $qsbyslot; +} + +/** + * Return a textual summary of the number of attempts that have been made at a particular quiz, + * returns '' if no attempts have been made yet, unless $returnzero is passed as true. + * + * @param capquiz $capquiz + * @param bool $returnzero if false (default), when no attempts have been + * made '' is returned instead of 'Attempts: 0'. + * @return string a string like "Attempts: 123". + */ +function capquiz_num_attempt_summary(capquiz $capquiz, $returnzero = false) { + $numattempts = capquiz_report_num_attempt($capquiz); + if ($numattempts || $returnzero) { + return get_string('attemptsnum', 'quiz', $numattempts); + } + return ''; +} + +/** + * Returns the number of CAPQuiz attempts. + * + * @param capquiz $capquiz + * @return int number of answered CAPQuiz attempts + * @throws dml_exception + */ +function capquiz_report_num_attempt(capquiz $capquiz): int { + global $DB; + $sql = 'SELECT COUNT(ca.id) + FROM {capquiz_attempt} ca + JOIN {capquiz_question_list} cql ON cql.capquiz_id = :capquizid AND cql.is_template = 0 + JOIN {question_usages} qu ON qu.id = cql.question_usage_id + JOIN {question_attempts} qa ON qa.questionusageid = qu.id AND qa.slot = ca.slot + JOIN {capquiz_question} cq ON cq.question_list_id = cql.id AND cq.id = ca.question_id'; + $attempts = $DB->count_records_sql($sql, ['capquizid' => $capquiz->id()]); + return $attempts; +} + + +/** + * Generate a message saying that this capquiz has no questions, with a button to + * go to the edit page, if the user has the right capability. + * @param object $quiz the quiz settings. + * @param object $cm the course_module object. + * @param object $context the quiz context. + * @return string HTML to output. + */ +function capquiz_no_questions_message($quiz, $cm, $context) { + global $OUTPUT; + + $output = ''; + $output .= $OUTPUT->notification(get_string('noquestions', 'quiz')); + if (has_capability('mod/capquiz:manage', $context)) { + $output .= $OUTPUT->single_button(capquiz_urls::view_question_list_url(), get_string('editquiz', 'quiz'), 'get'); + } + + return $output; +} + +/** + * Generate a message saying that this capquiz has no questions, with a button to + * go to the dashboard page (question list settings), if the user has the right capability. + * @param object $quiz the quiz settings. + * @param object $cm the course_module object. + * @param object $context the quiz context. + * @return string HTML to output. + */ +function capquiz_not_published_message($quiz, $cm, $context) { + global $OUTPUT; + + $output = ''; + $output .= $OUTPUT->notification(get_string('question_list_not_published', 'capquiz')); + if (has_capability('mod/capquiz:manage', $context)) { + $output .= $OUTPUT->single_button(capquiz_urls::view_url(), get_string('question_list_settings', 'capquiz'), 'get'); + } + + return $output; +} \ No newline at end of file diff --git a/report/upgrade.txt b/report/upgrade.txt new file mode 100644 index 0000000..ed88223 --- /dev/null +++ b/report/upgrade.txt @@ -0,0 +1 @@ +This files describes API changes for capquiz report plugins. diff --git a/templates/classlist.mustache b/templates/classlist.mustache index fd50c77..5b5a5d8 100755 --- a/templates/classlist.mustache +++ b/templates/classlist.mustache @@ -32,9 +32,10 @@ } ], "regrade": { + "id": "id", "method": "post", "classes": "capquiz-regrade-all", - "url": "", + "url": "#", "primary": true, "label": "Regrade all", "diabled": false @@ -77,5 +78,5 @@ {{/users}} {{#regrade}} - {{>core/single_button}} + {{> core/single_button}} {{/regrade}} diff --git a/view_report.php b/view_report.php new file mode 100644 index 0000000..9d5dc76 --- /dev/null +++ b/view_report.php @@ -0,0 +1,44 @@ +. + +/** + * @package mod_capquiz + * @author André Storhaug + * @copyright 2019 Norwegian University of Science and Technology (NTNU) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_capquiz; + +require_once('../../config.php'); +require_once($CFG->dirroot . '/question/editlib.php'); +require_once($CFG->dirroot . '/mod/capquiz/lib.php'); + +$cmid = capquiz_urls::require_course_module_id_param(); +$cm = get_coursemodule_from_id('capquiz', $cmid, 0, false, MUST_EXIST); +require_login($cm->course, false, $cm); +$context = \context_module::instance($cmid); +require_capability('mod/capquiz:instructor', $context); + +$cmid = capquiz_urls::require_course_module_id_param(); +$capquiz = new capquiz($cmid); +if (!$capquiz) { + capquiz_urls::redirect_to_front_page(); +} + +capquiz_urls::set_page_url($capquiz, capquiz_urls::$urledit); +$renderer = $capquiz->renderer(); +$renderer->display_report($capquiz);