├── README.md ├── adaptiveattempt.class.php ├── answerdistributiongraph.php ├── attempt.php ├── attemptfinished.php ├── attemptgraph.php ├── backup └── moodle2 │ ├── backup_adaptivequiz_activity_task.class.php │ ├── backup_adaptivequiz_stepslib.php │ ├── restore_adaptivequiz_activity_task.class.php │ └── restore_adaptivequiz_stepslib.php ├── catalgo.class.php ├── classes └── event │ ├── course_module_instance_list_viewed.php │ └── course_module_viewed.php ├── closeattempt.php ├── db ├── access.php ├── install.xml ├── log.php ├── uninstall.php └── upgrade.php ├── delattempt.php ├── fetchquestion.class.php ├── grade.php ├── index.php ├── lang ├── en │ └── adaptivequiz.php └── fr │ └── adaptivequiz.php ├── lib.php ├── locallib.php ├── mod_form.php ├── module.js ├── pix ├── attemptgraph.png ├── icon.gif ├── icon.png └── icon.svg ├── questionanalysis ├── lib │ ├── attempt_score.class.php │ ├── question_analyser.class.php │ ├── question_result.class.php │ ├── quiz_analyser.class.php │ └── statistics │ │ ├── answers_statistic.class.php │ │ ├── discrimination_statistic.class.php │ │ ├── percent_correct_statistic.class.php │ │ ├── question_statistic.interface.php │ │ └── times_used_statistic.class.php ├── overview.php ├── renderer.php └── single.php ├── renderer.php ├── requiredpassword.class.php ├── reviewattempt.php ├── styles.css ├── tests ├── adaptiveattempt_test.php ├── catalgo_test.php ├── dummycatalgo.class.php ├── dummyfetchquestion.class.php ├── fetchquestion_test.php ├── fixtures │ ├── mod_adaptivequiz.xml │ ├── mod_adaptivequiz_adaptiveattempt.xml │ ├── mod_adaptivequiz_catalgo.xml │ └── mod_adaptivequiz_findquestion.xml ├── generator │ └── lib.php ├── generator_test.php ├── lib_test.php ├── locallib_test.php └── renderer_test.php ├── version.php ├── view.php ├── viewattemptreport.php └── viewreport.php /answerdistributiongraph.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz - generate a graph of the question difficulties asked and the measured 19 | * ability of the test-taker as the test progressed. 20 | * 21 | * This module was created as a collaborative effort between Middlebury College 22 | * and Remote Learner. 23 | * 24 | * @package mod_adaptivequiz 25 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 | */ 28 | 29 | require_once(dirname(__FILE__).'/../../config.php'); 30 | require_once($CFG->dirroot.'/tag/lib.php'); 31 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 32 | require_once($CFG->dirroot.'/mod/adaptivequiz/catalgo.class.php'); 33 | require_once($CFG->dirroot.'/lib/graphlib.php'); 34 | 35 | $id = required_param('cmid', PARAM_INT); 36 | $uniqueid = required_param('uniqueid', PARAM_INT); 37 | $userid = required_param('userid', PARAM_INT); 38 | $page = optional_param('page', 0, PARAM_INT); 39 | 40 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 41 | print_error('invalidcoursemodule'); 42 | } 43 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 44 | print_error("coursemisconf"); 45 | } 46 | 47 | require_login($course, true, $cm); 48 | $context = context_module::instance($cm->id); 49 | 50 | require_capability('mod/adaptivequiz:viewreport', $context); 51 | 52 | $param = array('uniqueid' => $uniqueid, 'userid' => $userid, 'activityid' => $cm->instance); 53 | $sql = 'SELECT a.name, a.highestlevel, a.lowestlevel, a.startinglevel, aa.timecreated, aa.timemodified, aa.attemptstate, 54 | aa.attemptstopcriteria, aa.questionsattempted, aa.difficultysum, aa.standarderror, aa.measure 55 | FROM {adaptivequiz} a 56 | JOIN {adaptivequiz_attempt} aa ON a.id = aa.instance 57 | WHERE aa.uniqueid = :uniqueid 58 | AND aa.userid = :userid 59 | AND a.id = :activityid 60 | ORDER BY a.name ASC'; 61 | $adaptivequiz = $DB->get_record_sql($sql, $param); 62 | $user = $DB->get_record('user', array('id' => $userid)); 63 | if (!$user) { 64 | $user = new stdClass(); 65 | $user->firstname = get_string('unknownuser', 'adaptivequiz'); 66 | $user->lastname = '#'.$userid; 67 | } 68 | 69 | $g = new graph(750, 300); 70 | 71 | $a = new stdClass; 72 | $a->quiz_name = $adaptivequiz->name; 73 | $a->firstname = $user->firstname; 74 | $a->lastname = $user->lastname; 75 | $g->parameter['title'] = get_string('answerdistgraph_title', 'adaptivequiz', $a); 76 | 77 | $g->parameter['x_label'] = get_string('answerdistgraph_questiondifficulty', 'adaptivequiz'); 78 | $g->parameter['x_label_angle'] = 0; 79 | $g->parameter['y_label_left'] = get_string('answerdistgraph_numrightwrong', 'adaptivequiz'); 80 | $g->parameter['legend'] = 'none'; 81 | $g->parameter['legend_border'] = 'black'; 82 | $g->parameter['legend_offset'] = 4; 83 | $g->parameter['grid_colour'] = 'grayCC'; 84 | 85 | $g->parameter['y_resolution_left'] = 1; 86 | $g->parameter['y_decimal_left'] = 0; 87 | 88 | $g->parameter['shadow'] = 'grayCC'; // Set default shadow for all data sets. 89 | $g->parameter['bar_size'] = 2; 90 | $g->parameter['bar_spacing'] = 10; 91 | $g->parameter['zero_axis'] = 'black'; 92 | $g->parameter['inner_border_type'] = 'y-left'; // Only draw left y axis as zero axis already selected above. 93 | 94 | 95 | // Set up our data arrays. 96 | $difficulties = array(); 97 | $rightanswers = array(); 98 | $wronganswers = array(); 99 | 100 | for ($i = $adaptivequiz->lowestlevel; $i <= $adaptivequiz->highestlevel; $i++) { 101 | $difficulties[] = intval($i); 102 | $rightanswers[] = 0; 103 | $wronganswers[] = 0; 104 | } 105 | 106 | $quba = question_engine::load_questions_usage_by_activity($uniqueid); 107 | foreach ($quba->get_slots() as $i => $slot) { 108 | $question = $quba->get_question($slot); 109 | $tags = core_tag_tag::get_item_tags_array('', 'question', $question->id); 110 | $difficulty = adaptivequiz_get_difficulty_from_tags($tags); 111 | $correct = ($quba->get_question_mark($slot) > 0); 112 | 113 | $position = array_search($difficulty, $difficulties); 114 | if ($correct) { 115 | $rightanswers[$position]++; 116 | } else { 117 | $wronganswers[$position]--; 118 | } 119 | } 120 | 121 | $max = max(max($rightanswers), -1 * min($wronganswers)); 122 | 123 | $g->x_data = $difficulties; 124 | $g->y_data['right_answers'] = $rightanswers; 125 | $g->y_data['wrong_answers'] = $wronganswers; 126 | 127 | $g->y_format['right_answers'] = array('colour' => 'blue', 'bar' => 'fill', 'shadow' => 'none', 128 | 'legend' => get_string('answerdistgraph_right', 'adaptivequiz')); 129 | $g->y_format['wrong_answers'] = array('colour' => 'red', 'bar' => 'fill', 'shadow' => 'none', 130 | 'legend' => get_string('answerdistgraph_wrong', 'adaptivequiz')); 131 | 132 | $g->parameter['y_min_left'] = -1 * ($max + 1); 133 | $g->parameter['y_max_left'] = $max + 1; 134 | 135 | // Skip some y-axis labels so they aren't too crowded. 136 | $g->parameter['y_axis_text_left'] = ceil($max / 10); 137 | 138 | // Space out the bars so that their width slowly decreases as the number of ticks increases. 139 | $g->parameter['bar_spacing'] = max(4, round(700 / count($difficulties)) - 11 + round(8 * count($difficulties) / 100)); 140 | 141 | // Skip some x-axis labels for legibility. 142 | $g->parameter['x_axis_text'] = ceil(count($difficulties) / 50); 143 | 144 | $g->y_order = array('right_answers', 'wrong_answers'); 145 | 146 | $g->parameter['y_axis_gridlines'] = (2 * $max) + 3; 147 | 148 | 149 | $g->draw(); 150 | -------------------------------------------------------------------------------- /attemptfinished.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz attempt script 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../config.php'); 29 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 30 | 31 | $cmid = required_param('cmid', PARAM_INT); // Course module id. 32 | $instance = required_param('id', PARAM_INT); // activity instance id. 33 | $uniqueid = required_param('uattid', PARAM_INT); // attempt unique id. 34 | 35 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $cmid)) { 36 | print_error('invalidcoursemodule'); 37 | } 38 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 39 | print_error("coursemisconf"); 40 | } 41 | 42 | $adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*', MUST_EXIST); 43 | 44 | require_login($course, true, $cm); 45 | $context = context_module::instance($cm->id); 46 | 47 | // TODO - check if user has capability to attempt. 48 | 49 | // Check if this is the owner of the attempt. 50 | $validattempt = adaptivequiz_uniqueid_part_of_attempt($uniqueid, $instance, $USER->id); 51 | 52 | // Displayan error message if this is not the owner of the attempt. 53 | if (!$validattempt) { 54 | $url = new moodle_url('/mod/adaptivequiz/attempt.php', array('cmid' => $cm->id)); 55 | print_error('notyourattempt', 'adaptivequiz', $url); 56 | } 57 | 58 | $PAGE->set_url('/mod/adaptivequiz/view.php', array('id' => $cm->id)); 59 | $PAGE->set_title(format_string($adaptivequiz->name)); 60 | $PAGE->set_context($context); 61 | 62 | $output = $PAGE->get_renderer('mod_adaptivequiz'); 63 | 64 | // Init secure window if enabled. 65 | $popup = false; 66 | if (!empty($adaptivequiz->browsersecurity)) { 67 | $PAGE->blocks->show_only_fake_blocks(); 68 | $output->init_browser_security(); 69 | $popup = true; 70 | } else { 71 | $PAGE->set_heading(format_string($course->fullname)); 72 | } 73 | 74 | echo $output->print_attemptfeedback($adaptivequiz->attemptfeedback, $cm->id, $popup); -------------------------------------------------------------------------------- /attemptgraph.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz - generate a graph of the question difficulties asked and the measured 19 | * ability of the test-taker as the test progressed. 20 | * 21 | * This module was created as a collaborative effort between Middlebury College 22 | * and Remote Learner. 23 | * 24 | * @package mod_adaptivequiz 25 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 | */ 28 | 29 | require_once(dirname(__FILE__).'/../../config.php'); 30 | require_once($CFG->dirroot.'/tag/lib.php'); 31 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 32 | require_once($CFG->dirroot.'/mod/adaptivequiz/catalgo.class.php'); 33 | require_once($CFG->dirroot.'/lib/graphlib.php'); 34 | 35 | $id = required_param('cmid', PARAM_INT); 36 | $uniqueid = required_param('uniqueid', PARAM_INT); 37 | $userid = required_param('userid', PARAM_INT); 38 | $page = optional_param('page', 0, PARAM_INT); 39 | 40 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 41 | print_error('invalidcoursemodule'); 42 | } 43 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 44 | print_error("coursemisconf"); 45 | } 46 | 47 | require_login($course, true, $cm); 48 | $context = context_module::instance($cm->id); 49 | 50 | require_capability('mod/adaptivequiz:viewreport', $context); 51 | 52 | $param = array('uniqueid' => $uniqueid, 'userid' => $userid, 'activityid' => $cm->instance); 53 | $sql = 'SELECT a.name, a.highestlevel, a.lowestlevel, a.startinglevel, aa.timecreated, aa.timemodified, aa.attemptstate, 54 | aa.attemptstopcriteria, aa.questionsattempted, aa.difficultysum, aa.standarderror, aa.measure 55 | FROM {adaptivequiz} a 56 | JOIN {adaptivequiz_attempt} aa ON a.id = aa.instance 57 | WHERE aa.uniqueid = :uniqueid 58 | AND aa.userid = :userid 59 | AND a.id = :activityid 60 | ORDER BY a.name ASC'; 61 | $adaptivequiz = $DB->get_record_sql($sql, $param); 62 | $user = $DB->get_record('user', array('id' => $userid)); 63 | if (!$user) { 64 | $user = new stdClass(); 65 | $user->firstname = get_string('unknownuser', 'adaptivequiz'); 66 | $user->lastname = '#'.$userid; 67 | } 68 | 69 | $g = new graph(750, 300); 70 | $g->parameter['title'] = format_string($adaptivequiz->name).' for '.$user->firstname." ".$user->lastname; 71 | $g->parameter['y_label_left'] = get_string('attemptquestion_ability', 'adaptivequiz'); 72 | $g->parameter['legend'] = 'outside-top'; 73 | $g->parameter['legend_border'] = 'black'; 74 | $g->parameter['legend_offset'] = 4; 75 | $g->parameter['grid_colour'] = 'grayCC'; 76 | 77 | $qnumbers = array(); 78 | $qdifficulties = array(); 79 | $abilitymeasures = array(); 80 | $errormaximums = array(); 81 | $errorminimums = array(); 82 | $targetlevels = array(); 83 | 84 | $quba = question_engine::load_questions_usage_by_activity($uniqueid); 85 | $numattempted = 0; 86 | $difficultysum = 0; 87 | $sumcorrect = 0; 88 | $sumincorrect = 0; 89 | foreach ($quba->get_slots() as $i => $slot) { 90 | // The starting target difficulty is set by the test parameters. 91 | if ($i == 0) { 92 | $targetlevel = $adaptivequiz->startinglevel; 93 | } else { 94 | // Compute the target difficulty based on the last question. 95 | if ($questioncorrect) { 96 | $targetlevel = round(catalgo::map_logit_to_scale($qdifficultylogits + 2 / $numattempted, 97 | $adaptivequiz->highestlevel, $adaptivequiz->lowestlevel)); 98 | if ($targetlevel == $qdifficulty && $targetlevel < $adaptivequiz->highestlevel) { 99 | $targetlevel++; 100 | } 101 | } else { 102 | $targetlevel = round(catalgo::map_logit_to_scale($qdifficultylogits - 2 / $numattempted, 103 | $adaptivequiz->highestlevel, $adaptivequiz->lowestlevel)); 104 | if ($targetlevel == $qdifficulty && $targetlevel > $adaptivequiz->lowestlevel) { 105 | $targetlevel--; 106 | } 107 | } 108 | } 109 | 110 | $question = $quba->get_question($slot); 111 | $tags = core_tag_tag::get_item_tags_array('', 'question', $question->id); 112 | $qdifficulty = adaptivequiz_get_difficulty_from_tags($tags); 113 | $qdifficultylogits = catalgo::convert_linear_to_logit($qdifficulty, $adaptivequiz->lowestlevel, 114 | $adaptivequiz->highestlevel); 115 | $questioncorrect = ($quba->get_question_mark($slot) > 0); 116 | 117 | $numattempted++; 118 | $difficultysum = $difficultysum + $qdifficultylogits; 119 | if ($questioncorrect) { 120 | $sumcorrect++; 121 | } else { 122 | $sumincorrect++; 123 | } 124 | 125 | $abilitylogits = catalgo::estimate_measure($difficultysum, $numattempted, $sumcorrect, 126 | $sumincorrect); 127 | $abilityfraction = 1 / ( 1 + exp( (-1 * $abilitylogits) ) ); 128 | $ability = (($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel) * $abilityfraction) + $adaptivequiz->lowestlevel; 129 | 130 | $stderrlogits = catalgo::estimate_standard_error($numattempted, $sumcorrect, $sumincorrect); 131 | $stderr = catalgo::convert_logit_to_percent($stderrlogits); 132 | 133 | $qnumbers[] = $numattempted; 134 | $qdifficulties[] = $qdifficulty; 135 | $abilitymeasures[] = $ability; 136 | 137 | $errormaximums[] = min($adaptivequiz->highestlevel, 138 | $ability + ($stderr * ($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel))); 139 | $errorminimums[] = max($adaptivequiz->lowestlevel, 140 | $ability - ($stderr * ($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel))); 141 | 142 | $targetlevels[] = $targetlevel; 143 | } 144 | 145 | 146 | $g->x_data = $qnumbers; 147 | $g->y_data['qdiff'] = $qdifficulties; 148 | $g->y_data['ability'] = $abilitymeasures; 149 | $g->y_data['target_level'] = $targetlevels; 150 | $g->y_data['error_max'] = $errormaximums; 151 | $g->y_data['error_min'] = $errorminimums; 152 | 153 | $g->y_format['qdiff'] = array('colour' => 'blue', 'line' => 'brush', 'brush_size' => 2, 'shadow' => 'none', 154 | 'legend' => get_string('attemptquestion_level', 'adaptivequiz')); 155 | $g->y_format['target_level'] = array('colour' => 'green', 'line' => 'brush', 'brush_size' => 1, 'shadow' => 'none', 156 | 'legend' => get_string('graphlegend_target', 'adaptivequiz')); 157 | $g->y_format['ability'] = array('colour' => 'red', 'line' => 'brush', 'brush_size' => 2, 'shadow' => 'none', 158 | 'legend' => get_string('attemptquestion_ability', 'adaptivequiz')); 159 | $g->colour['pink'] = imagecolorallocate($g->image, 0xFF, 0xE5, 0xE5); 160 | $g->y_format['error_max'] = array('colour' => 'pink', 'area' => 'fill', 'shadow' => 'none', 161 | 'legend' => get_string('graphlegend_error', 'adaptivequiz')); 162 | $g->y_format['error_min'] = array('colour' => 'white', 'area' => 'fill', 'shadow' => 'none'); 163 | 164 | $g->parameter['y_min_left'] = $adaptivequiz->lowestlevel; 165 | $g->parameter['y_max_left'] = $adaptivequiz->highestlevel; 166 | $g->parameter['x_grid'] = 'none'; 167 | 168 | if ($adaptivequiz->highestlevel - $adaptivequiz->lowestlevel <= 20) { 169 | $g->parameter['y_axis_gridlines'] = $adaptivequiz->highestlevel - $adaptivequiz->lowestlevel + 1; 170 | $g->parameter['y_decimal_left'] = 0; 171 | } else { 172 | $g->parameter['y_axis_gridlines'] = 21; 173 | $g->parameter['y_decimal_left'] = 1; 174 | } 175 | 176 | // Ensure that the x-axis text isn't to cramped. 177 | $g->parameter['x_axis_text'] = ceil($numattempted / 40); 178 | 179 | 180 | // Draw in custom order to get grid lines on top instead of using $g->draw(). 181 | $g->y_order = array('error_max', 'error_min', 'target_level', 'qdiff', 'ability'); 182 | $g->init(); 183 | // After initializing with all data sets, reset the order to just the standard-error sets and draw them. 184 | $g->y_order = array('error_max', 'error_min'); 185 | $g->draw_data(); 186 | 187 | // Now draw the axis and text on top of the error ranges. 188 | $g->y_order = array('ability', 'error_max', 'target_level', 'qdiff', 'error_min'); 189 | $g->draw_y_axis(); 190 | $g->draw_text(); 191 | 192 | // Now reset the order and draw our lines. 193 | $g->y_order = array('qdiff', 'target_level', 'ability'); 194 | $g->draw_data(); 195 | 196 | $g->output(); 197 | -------------------------------------------------------------------------------- /backup/moodle2/backup_adaptivequiz_activity_task.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz backup files 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | require_once($CFG->dirroot.'/mod/adaptivequiz/backup/moodle2/backup_adaptivequiz_stepslib.php'); 30 | 31 | /** 32 | * Provides the steps to perform one complete backup of the adaptivequiz instance 33 | * 34 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 | */ 37 | class backup_adaptivequiz_activity_task extends backup_activity_task { 38 | /** 39 | * No specific settings for this activity 40 | */ 41 | protected function define_my_settings() { 42 | } 43 | 44 | /** 45 | * Defines backup steps to store the instance data and required questions 46 | */ 47 | protected function define_my_steps() { 48 | // Generate the adaptivequiz.xml file containing all the quiz information 49 | // and annotating used questions. 50 | $this->add_step(new backup_adaptivequiz_activity_structure_step('adaptivequiz_structure', 'adaptivequiz.xml')); 51 | 52 | // Note: Following steps must be present 53 | // in all the activities using question banks. 54 | 55 | // Process all the annotated questions to calculate the question 56 | // categories needing to be included in backup for this activity 57 | // plus the categories belonging to the activity context itself. 58 | $this->add_step(new backup_calculate_question_categories('activity_question_categories')); 59 | 60 | // Clean backup_temp_ids table from questions. We already 61 | // have used them to detect question_categories and aren't 62 | // needed anymore. 63 | $this->add_step(new backup_delete_temp_questions('clean_temp_questions')); 64 | } 65 | 66 | /** 67 | * Encodes URLs to the index.php and view.php scripts 68 | * @param string $content some HTML text that eventually contains URLs to the activity instance scripts 69 | * @return string the content with the URLs encoded 70 | */ 71 | public static function encode_content_links($content) { 72 | global $CFG; 73 | 74 | $base = preg_quote($CFG->wwwroot, '/'); 75 | 76 | // Link to the list of adatpivequizzes. 77 | $search = "/(".$base."\/mod\/adaptivequiz\/index.php\?id\=)([0-9]+)/"; 78 | $content = preg_replace($search, '$@ADAPTIVEQUIZINDEX*$2@$', $content); 79 | 80 | // Link to adaptivequiz view by moduleid. 81 | $search = "/(".$base."\/mod\/adaptivequiz\/view.php\?id\=)([0-9]+)/"; 82 | $content = preg_replace($search, '$@ADAPTIVEQUIZVIEWBYID*$2@$', $content); 83 | 84 | // Link to adaptivequiz view by adaptivequizid. 85 | $search = "/(".$base."\/mod\/adaptivequiz\/view.php\?q\=)([0-9]+)/"; 86 | $content = preg_replace($search, '$@ADAPTIVEQUIZVIEWBYQ*$2@$', $content); 87 | 88 | return $content; 89 | } 90 | } -------------------------------------------------------------------------------- /backup/moodle2/backup_adaptivequiz_stepslib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz backup files 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | 30 | /** 31 | * Define all the backup steps that will be used by the backup_adaptivequiz_activity_task 32 | * 33 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class backup_adaptivequiz_activity_structure_step extends backup_questions_activity_structure_step { 37 | 38 | /** 39 | * Define the backup structure 40 | * @return string the root element (adaptivequiz), wrapped into standard activity structure. 41 | */ 42 | protected function define_structure() { 43 | // To know if we are including userinfo. 44 | $userinfo = $this->get_setting_value('userinfo'); 45 | 46 | // Define each element separated. 47 | $nodes = array( 48 | 'name', 'intro', 'introformat', 'attempts', 'password', 'browsersecurity', 49 | 'attemptfeedback', 'attemptfeedbackformat', 'highestlevel', 'lowestlevel', 50 | 'minimumquestions', 'maximumquestions', 'standarderror', 'startinglevel', 51 | 'timecreated', 'timemodified'); 52 | $adaptivequiz = new backup_nested_element('adaptivequiz', array('id'), $nodes); 53 | 54 | // Attempts. 55 | $adaptiveattempts = new backup_nested_element('adaptiveattempts'); 56 | $nodes = array( 57 | 'userid', 'uniqueid', 'attemptstate', 'attemptstopcriteria', 'questionsattempted', 58 | 'difficultysum', 'standarderror', 'measure', 'timecreated', 'timemodified'); 59 | $adaptiveattempt = new backup_nested_element('adaptiveattempt', array('id'), $nodes); 60 | 61 | // This module is using questions, so produce the related question states and sessions. 62 | // attaching them to the $attempt element based in 'uniqueid' matching. 63 | $this->add_question_usages($adaptiveattempt, 'uniqueid'); 64 | 65 | // Activity to question categories reference. 66 | $adaptivequestioncats = new backup_nested_element('adatpivequestioncats'); 67 | $adaptivequestioncat = new backup_nested_element('adatpivequestioncat', array('id'), array('questioncategory')); 68 | 69 | // Build the tree. 70 | $adaptivequiz->add_child($adaptiveattempts); 71 | $adaptiveattempts->add_child($adaptiveattempt); 72 | 73 | $adaptivequiz->add_child($adaptivequestioncats); 74 | $adaptivequestioncats->add_child($adaptivequestioncat); 75 | 76 | // Define sources. 77 | $adaptivequiz->set_source_table('adaptivequiz', array('id' => backup::VAR_ACTIVITYID)); 78 | $adaptivequestioncat->set_source_table('adaptivequiz_question', array('instance' => backup::VAR_PARENTID)); 79 | 80 | // All the rest of elements only happen if we are including user info. 81 | if ($userinfo) { 82 | $sql = 'SELECT * 83 | FROM {adaptivequiz_attempt} 84 | WHERE instance = :instance'; 85 | $param = array('instance' => backup::VAR_PARENTID); 86 | $adaptiveattempt->set_source_sql($sql, $param); 87 | } 88 | 89 | // Define id annotations. 90 | $adaptivequestioncat->annotate_ids('question_categories', 'questioncategory'); 91 | $adaptiveattempt->annotate_ids('user', 'userid'); 92 | 93 | $adaptivequiz->annotate_files('mod_adaptivequiz', 'intro', null); // This file area hasn't itemid. 94 | 95 | // Return the root element (adaptivequiz), wrapped into standard activity structure. 96 | return $this->prepare_activity_structure($adaptivequiz); 97 | } 98 | } -------------------------------------------------------------------------------- /backup/moodle2/restore_adaptivequiz_activity_task.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz backup files 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | require_once($CFG->dirroot.'/mod/adaptivequiz/backup/moodle2/restore_adaptivequiz_stepslib.php'); 30 | 31 | /** 32 | * adaptivequiz restore task that provides all the settings and steps to perform one 33 | * complete restore of the activity 34 | * 35 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class restore_adaptivequiz_activity_task extends restore_activity_task { 39 | /** 40 | * Define (add) particular settings this activity can have 41 | */ 42 | protected function define_my_settings() { 43 | // No particular settings for this activity. 44 | } 45 | 46 | /** 47 | * Define (add) particular steps this activity can have 48 | */ 49 | protected function define_my_steps() { 50 | // Adaptivequiz only has one structure step. 51 | $this->add_step(new restore_adaptivequiz_activity_structure_step('adaptivequiz_structure', 'adaptivequiz.xml')); 52 | } 53 | 54 | /** 55 | * Define the contents in the activity that must be 56 | * processed by the link decoder 57 | * @return array an array of restore_decode_content objects 58 | */ 59 | public static function define_decode_contents() { 60 | $contents = array(); 61 | $contents[] = new restore_decode_content('adaptivequiz', array('intro'), 'adaptivequiz'); 62 | return $contents; 63 | } 64 | 65 | /** 66 | * Define the decoding rules for links belonging 67 | * to the activity to be executed by the link decoder 68 | * @return array an array of restore_decode_rule objects 69 | */ 70 | public static function define_decode_rules() { 71 | $rules = array(); 72 | 73 | $rules[] = new restore_decode_rule('ADAPTIVEQUIZVIEWBYID', '/mod/adaptivequiz/view.php?id=$1', 'course_module'); 74 | $rules[] = new restore_decode_rule('ADAPTIVEQUIZVIEWBYQ', '/mod/adaptivequiz/view.php?q=$1', 'adaptivequiz'); 75 | $rules[] = new restore_decode_rule('ADAPTIVEQUIZINDEX', '/mod/adaptivequiz/index.php?id=$1', 'course'); 76 | 77 | return $rules; 78 | } 79 | 80 | /** 81 | * Define the restore log rules that will be applied 82 | * by the {@link restore_logs_processor} when restoring 83 | * adaptivequiz logs. It must return one array 84 | * of {@link restore_log_rule} objects 85 | * @return array an array of restore_log_rule objects 86 | */ 87 | public static function define_restore_log_rules() { 88 | $rules = array(); 89 | // TODO update this method when logging statemtns have been added to the code. 90 | return $rules; 91 | } 92 | 93 | /** 94 | * Define the restore log rules that will be applied 95 | * by the {@link restore_logs_processor} when restoring 96 | * course logs. It must return one array 97 | * of {@link restore_log_rule} objects 98 | * 99 | * Note this rules are applied when restoring course logs 100 | * by the restore final task, but are defined here at 101 | * activity level. All them are rules not linked to any module instance (cmid = 0) 102 | * @return array an array of of restore_log_rule objects 103 | */ 104 | public static function define_restore_log_rules_for_course() { 105 | $rules = array(); 106 | return $rules; 107 | } 108 | } -------------------------------------------------------------------------------- /backup/moodle2/restore_adaptivequiz_stepslib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * This module was created as a collaborative effort between Middlebury College 19 | * and Remote Learner. 20 | * 21 | * @package mod_adaptivequiz 22 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | /** 29 | * Structure step to restore one adaptivequiz activity 30 | * 31 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class restore_adaptivequiz_activity_structure_step extends restore_questions_activity_structure_step { 35 | 36 | /** 37 | * Define the a structure for restoring the activity 38 | * @return backup_nested_element the $activitystructure wrapped by the common 'activity' element 39 | */ 40 | protected function define_structure() { 41 | $paths = array(); 42 | $userinfo = $this->get_setting_value('userinfo'); 43 | 44 | $adaptivequiz = new restore_path_element('adaptivequiz', '/activity/adaptivequiz'); 45 | $paths[] = $adaptivequiz; 46 | 47 | $paths[] = new restore_path_element('adaptivequiz_question', 48 | '/activity/adaptivequiz/adatpivequestioncats/adatpivequestioncat'); 49 | 50 | if ($userinfo) { 51 | $attempt = new restore_path_element('adaptivequiz_attempt', '/activity/adaptivequiz/adaptiveattempts/adaptiveattempt'); 52 | $paths[] = $attempt; 53 | 54 | // Add states and sessions. 55 | $this->add_question_usages($attempt, $paths); 56 | } 57 | 58 | // Return the paths wrapped into standard activity structure. 59 | return $this->prepare_activity_structure($paths); 60 | } 61 | 62 | /** 63 | * Process the adaptivequiz element 64 | * @param stdClass an object whose properties are nodes in the adatpviequiz structure 65 | */ 66 | protected function process_adaptivequiz($data) { 67 | global $CFG, $DB; 68 | 69 | $data = (object)$data; 70 | $oldid = $data->id; 71 | $data->course = $this->get_courseid(); 72 | 73 | $data->timecreated = $this->apply_date_offset($data->timecreated); 74 | $data->timemodified = $this->apply_date_offset($data->timemodified); 75 | // Insert the quiz record. 76 | $newitemid = $DB->insert_record('adaptivequiz', $data); 77 | // Immediately after inserting "activity" record, call this. 78 | $this->apply_activity_instance($newitemid); 79 | } 80 | 81 | /** 82 | * Process the activity instance to question categories relation structure\ 83 | * @param stdClass an object whose properties are nodes in the adatpviequiz_question structure 84 | */ 85 | protected function process_adaptivequiz_question($data) { 86 | global $DB; 87 | 88 | $data = (object)$data; 89 | $oldid = $data->id; 90 | 91 | $data->instance = $this->get_new_parentid('adaptivequiz'); 92 | // Check if catid is not empty and update the record with the new category id. 93 | $catid = $this->get_mappingid('question_category', $data->questioncategory); 94 | if (!empty($catid)) { 95 | $data->questioncategory = $catid; 96 | } 97 | $DB->insert_record('adaptivequiz_question', $data); 98 | } 99 | 100 | /** 101 | * Process the activity instance to question categories relation structure 102 | * @param stdClass an object whose properties are nodes in the adatpviequiz_attempt structure 103 | */ 104 | protected function process_adaptivequiz_attempt($data) { 105 | $data = (object)$data; 106 | $oldid = $data->id; 107 | 108 | $data->instance = $this->get_new_parentid('adaptivequiz'); 109 | $data->userid = $this->get_mappingid('user', $data->userid); 110 | $data->timemodified = $this->apply_date_offset($data->timemodified); 111 | 112 | // The data is actually inserted into the database later in inform_new_usage_id. 113 | $this->currentadatpivequizattempt = clone($data); 114 | } 115 | 116 | /** 117 | * This function assigns the new question usage by activity id to the attempt 118 | * @param int $newusageid a new question usage by activity id 119 | */ 120 | protected function inform_new_usage_id($newusageid) { 121 | global $DB; 122 | 123 | $data = $this->currentadatpivequizattempt; 124 | 125 | $oldid = $data->id; 126 | $data->uniqueid = $newusageid; 127 | 128 | $newitemid = $DB->insert_record('adaptivequiz_attempt', $data); 129 | 130 | // Save quiz_attempt->id mapping, because logs use it. (logs will be implemented latter). 131 | $this->set_mapping('adaptivequiz_attempt', $oldid, $newitemid, false); 132 | } 133 | 134 | /** 135 | * This function adds any files assocaited with the intro field after the restore process has run 136 | */ 137 | protected function after_execute() { 138 | parent::after_execute(); 139 | // Add quiz related files, no need to match by itemname (just internally handled context). 140 | $this->add_related_files('mod_adaptivequiz', 'intro', null); 141 | } 142 | } -------------------------------------------------------------------------------- /classes/event/course_module_instance_list_viewed.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * The mod_peerassess instance list viewed event. 19 | * 20 | * @package mod_adaptivequiz 21 | * @author Corey Wallis 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace mod_adaptivequiz\event; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | /** 30 | * The mod_adaptivequiz instance list viewed event class. 31 | * 32 | * @package mod_adaptivequiz 33 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 | */ 35 | class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { 36 | /** 37 | * Create the event from course record. 38 | * 39 | * @param \stdClass $course 40 | * @return course_module_instance_list_viewed 41 | */ 42 | public static function create_from_course(\stdClass $course) { 43 | $params = array( 44 | 'context' => \context_course::instance($course->id) 45 | ); 46 | $event = self::create($params); 47 | $event->add_record_snapshot('course', $course); 48 | return $event; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /classes/event/course_module_viewed.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the course module viewed event. 19 | * 20 | * @package mod_adaptivequiz 21 | * @author Corey Wallis 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace mod_adaptivequiz\event; 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | class course_module_viewed extends \core\event\course_module_viewed { 29 | protected function init() { 30 | $this->data['objecttable'] = 'adaptivequiz'; 31 | parent::init(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /closeattempt.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Confirmation page to close a student attempt 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../config.php'); 29 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 30 | 31 | $id = required_param('cmid', PARAM_INT); 32 | $uniqueid = required_param('uniqueid', PARAM_INT); 33 | $userid = required_param('userid', PARAM_INT); 34 | $confirm = optional_param('confirm', 0, PARAM_INT); 35 | 36 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 37 | print_error('invalidcoursemodule'); 38 | } 39 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 40 | print_error("coursemisconf"); 41 | } 42 | 43 | global $OUTPUT, $DB; 44 | 45 | require_login($course, true, $cm); 46 | $context = context_module::instance($cm->id); 47 | 48 | require_capability('mod/adaptivequiz:viewreport', $context); 49 | 50 | $param = array('uniqueid' => $uniqueid, 'userid' => $userid, 'activityid' => $cm->instance); 51 | $sql = 'SELECT a.name, aa.attemptstate, aa.timecreated, aa.timemodified, aa.id, u.firstname, u.lastname, aa.attemptstate, 52 | aa.questionsattempted, aa.measure, aa.standarderror AS stderror, a.highestlevel, a.lowestlevel 53 | FROM {adaptivequiz} a 54 | JOIN {adaptivequiz_attempt} aa ON a.id = aa.instance 55 | JOIN {user} u ON u.id = aa.userid 56 | WHERE aa.uniqueid = :uniqueid 57 | AND aa.userid = :userid 58 | AND a.id = :activityid 59 | ORDER BY a.name ASC'; 60 | $adaptivequiz = $DB->get_record_sql($sql, $param); 61 | 62 | $returnurl = new moodle_url('/mod/adaptivequiz/viewattemptreport.php', array('cmid' => $cm->id, 'userid' => $userid)); 63 | 64 | if (empty($adaptivequiz)) { 65 | print_error('errorclosingattempt', 'adaptivequiz', $returnurl); 66 | } 67 | 68 | if ($adaptivequiz->attemptstate == ADAPTIVEQUIZ_ATTEMPT_COMPLETED) { 69 | print_error('errorclosingattempt_alreadycomplete', 'adaptivequiz', $returnurl); 70 | } 71 | 72 | $PAGE->set_url('/mod/adaptivequiz/reviewattempt.php', array('cmid' => $cm->id)); 73 | $PAGE->set_title(format_string($adaptivequiz->name)); 74 | $PAGE->set_heading(format_string($course->fullname)); 75 | $PAGE->set_context($context); 76 | 77 | $renderer = $PAGE->get_renderer('mod_adaptivequiz'); 78 | 79 | // Are you sure confirmation message. 80 | global $USER; 81 | $a = new stdClass(); 82 | $a->name = format_string($adaptivequiz->firstname.' '.$adaptivequiz->lastname); 83 | $a->started = userdate($adaptivequiz->timecreated); 84 | $a->modified = userdate($adaptivequiz->timemodified); 85 | $a->num_questions = format_string($adaptivequiz->questionsattempted); 86 | $a->measure = $renderer->format_measure($adaptivequiz); 87 | $a->standarderror = $renderer->format_standard_error($adaptivequiz); 88 | $a->current_user_name = format_string($USER->firstname.' '.$USER->lastname); 89 | $a->current_user_id = format_string($USER->id); 90 | $a->now = userdate(time()); 91 | 92 | $message = html_writer::tag('p', get_string('confirmcloseattempt', 'adaptivequiz', $a)) 93 | .html_writer::tag('p', get_string('confirmcloseattemptstats', 'adaptivequiz', $a)) 94 | .html_writer::tag('p', get_string('confirmcloseattemptscore', 'adaptivequiz', $a)); 95 | 96 | if ($confirm) { 97 | // Close the attempt record and redirect. 98 | $statusmessage = get_string('attemptclosedstatus', 'adaptivequiz', $a); 99 | 100 | $closemessage = get_string('attemptclosed', 'adaptivequiz', $a); 101 | 102 | adaptivequiz_complete_attempt($uniqueid, $cm->instance, $userid, $adaptivequiz->stderror, $statusmessage); 103 | redirect($returnurl, $closemessage, 4); 104 | } 105 | 106 | $confirm = new moodle_url('/mod/adaptivequiz/closeattempt.php', array('uniqueid' => $uniqueid, 'cmid' => $cm->id, 107 | 'userid' => $userid, 'confirm' => 1)); 108 | echo $OUTPUT->header(); 109 | echo $OUTPUT->confirm($message, $confirm, $returnurl); 110 | echo $OUTPUT->footer(); -------------------------------------------------------------------------------- /db/access.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive testing capabilities definition 19 | * 20 | * @package mod 21 | * @subpackage adaptivequiz 22 | * @category access 23 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | $capabilities = array( 30 | // Ability to add a new adaptivequiz to the course. 31 | 'mod/adaptivequiz:addinstance' => array( 32 | 'riskbitmask' => RISK_XSS, 33 | 'captype' => 'write', 34 | 'contextlevel' => CONTEXT_COURSE, 35 | 'archetypes' => array( 36 | 'editingteacher' => CAP_ALLOW, 37 | 'manager' => CAP_ALLOW 38 | ), 39 | 'clonepermissionsfrom' => 'moodle/course:manageactivities' 40 | ), 41 | // Ability to view adaptivequiz report. 42 | 'mod/adaptivequiz:viewreport' => array( 43 | 'riskbitmask' => RISK_PERSONAL, 44 | 'captype' => 'write', 45 | 'contextlevel' => CONTEXT_COURSE, 46 | 'archetypes' => array( 47 | 'editingteacher' => CAP_ALLOW, 48 | 'manager' => CAP_ALLOW 49 | ), 50 | ), 51 | // Ability to view review pervious attempts. 52 | 'mod/adaptivequiz:reviewattempts' => array( 53 | 'riskbitmask' => RISK_PERSONAL, 54 | 'captype' => 'write', 55 | 'contextlevel' => CONTEXT_COURSE, 56 | 'archetypes' => array( 57 | 'editingteacher' => CAP_ALLOW, 58 | 'manager' => CAP_ALLOW 59 | ), 60 | ), 61 | // Ability to attempt the activity. 62 | 'mod/adaptivequiz:attempt' => array( 63 | 'riskbitmask' => RISK_SPAM, 64 | 'captype' => 'write', 65 | 'contextlevel' => CONTEXT_MODULE, 66 | 'archetypes' => array( 67 | 'student' => CAP_ALLOW 68 | ) 69 | ), 70 | ); 71 | -------------------------------------------------------------------------------- /db/install.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /db/log.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Definition of log events for the adaptive quiz module. 19 | * 20 | * @package mod_adaptivequiz 21 | * @category log 22 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | global $DB; 29 | 30 | $logs = array( 31 | array('module' => 'adaptivequiz', 'action' => 'view', 'mtable' => 'adaptivequiz', 'field' => 'name'), 32 | array('module' => 'adaptivequiz', 'action' => 'add', 'mtable' => 'adaptivequiz', 'field' => 'name'), 33 | array('module' => 'adaptivequiz', 'action' => 'update', 'mtable' => 'adaptivequiz', 'field' => 'name'), 34 | array('module' => 'adaptivequiz', 'action' => 'report', 'mtable' => 'adaptivequiz', 'field' => 'name'), 35 | array('module' => 'adaptivequiz', 'action' => 'attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'), 36 | array('module' => 'adaptivequiz', 'action' => 'submit', 'mtable' => 'adaptivequiz', 'field' => 'name'), 37 | array('module' => 'adaptivequiz', 'action' => 'review', 'mtable' => 'adaptivequiz', 'field' => 'name'), 38 | array('module' => 'adaptivequiz', 'action' => 'start attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'), 39 | array('module' => 'adaptivequiz', 'action' => 'close attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'), 40 | array('module' => 'adaptivequiz', 'action' => 'start attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'), 41 | array('module' => 'adaptivequiz', 'action' => 'continue attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'), 42 | array('module' => 'adaptivequiz', 'action' => 'start attempt', 'mtable' => 'adaptivequiz', 'field' => 'name'), 43 | ); 44 | -------------------------------------------------------------------------------- /db/uninstall.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive testing uninstall script 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | /** 29 | * Custom uninstallation procedure 30 | * @return bool: only returns truel 31 | */ 32 | function xmldb_adaptivequiz_uninstall() { 33 | return true; 34 | } 35 | -------------------------------------------------------------------------------- /db/upgrade.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz upgrade steps 19 | * 20 | * @package mod_adaptivequiz 21 | * @category log 22 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | 27 | /** 28 | * Quiz module upgrade function. 29 | * @param string $oldversion the version we are upgrading from. 30 | */ 31 | function xmldb_adaptivequiz_upgrade($oldversion) { 32 | global $CFG, $DB; 33 | 34 | $dbman = $DB->get_manager(); 35 | 36 | if ($oldversion < 2014020400) { 37 | // Define field grademethod. 38 | $table = new xmldb_table('adaptivequiz'); 39 | $field = new xmldb_field('grademethod', XMLDB_TYPE_INTEGER, '3', null, XMLDB_NOTNULL, null, 1, 'startinglevel'); 40 | 41 | // Conditionally add field grademethod. 42 | if (!$dbman->field_exists($table, $field)) { 43 | $dbman->add_field($table, $field); 44 | } 45 | 46 | // Quiz savepoint reached. 47 | upgrade_mod_savepoint(true, 2014020400, 'adaptivequiz'); 48 | } 49 | 50 | return true; 51 | } 52 | -------------------------------------------------------------------------------- /delattempt.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Confirmation page to remove student attempts 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../config.php'); 29 | 30 | $id = required_param('cmid', PARAM_INT); 31 | $uniqueid = required_param('uniqueid', PARAM_INT); 32 | $userid = required_param('userid', PARAM_INT); 33 | $confirm = optional_param('confirm', 0, PARAM_INT); 34 | 35 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 36 | print_error('invalidcoursemodule'); 37 | } 38 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 39 | print_error("coursemisconf"); 40 | } 41 | 42 | global $OUTPUT, $DB; 43 | 44 | require_login($course, true, $cm); 45 | $context = context_module::instance($cm->id); 46 | 47 | require_capability('mod/adaptivequiz:viewreport', $context); 48 | 49 | $param = array('uniqueid' => $uniqueid, 'userid' => $userid, 'activityid' => $cm->instance); 50 | $sql = 'SELECT a.name, aa.timemodified, aa.id, u.firstname, u.lastname 51 | FROM {adaptivequiz} a 52 | JOIN {adaptivequiz_attempt} aa ON a.id = aa.instance 53 | JOIN {user} u ON u.id = aa.userid 54 | WHERE aa.uniqueid = :uniqueid 55 | AND aa.userid = :userid 56 | AND a.id = :activityid 57 | ORDER BY a.name ASC'; 58 | $adaptivequiz = $DB->get_record_sql($sql, $param); 59 | 60 | $returnurl = new moodle_url('/mod/adaptivequiz/viewattemptreport.php', array('cmid' => $cm->id, 'userid' => $userid)); 61 | 62 | if (empty($adaptivequiz)) { 63 | print_error('errordeletingattempt', 'adaptivequiz', $returnurl); 64 | } 65 | 66 | $PAGE->set_url('/mod/adaptivequiz/reviewattempt.php', array('cmid' => $cm->id)); 67 | $PAGE->set_title(format_string($adaptivequiz->name)); 68 | $PAGE->set_heading(format_string($course->fullname)); 69 | $PAGE->set_context($context); 70 | 71 | // Are you usre confirmation message. 72 | $a = new stdClass(); 73 | $a->name = format_string($adaptivequiz->firstname.' '.$adaptivequiz->lastname); 74 | $a->timecompleted = userdate($adaptivequiz->timemodified); 75 | $message = get_string('confirmdeleteattempt', 'adaptivequiz', $a); 76 | 77 | if ($confirm) { 78 | // Remove attempt record and redirect. 79 | question_engine::delete_questions_usage_by_activity($uniqueid); 80 | $DB->delete_records('adaptivequiz_attempt', array('instance' => $cm->instance, 'uniqueid' => $uniqueid, 'userid' => $userid)); 81 | 82 | // Update the grade book with any changes. 83 | $adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance)); 84 | adaptivequiz_update_grades($adaptivequiz, $userid); 85 | 86 | $message = get_string('attemptdeleted', 'adaptivequiz', $a); 87 | redirect($returnurl, $message, 4); 88 | } 89 | 90 | $confirm = new moodle_url('/mod/adaptivequiz/delattempt.php', array('uniqueid' => $uniqueid, 'cmid' => $cm->id, 91 | 'userid' => $userid, 'confirm' => 1)); 92 | echo $OUTPUT->header(); 93 | echo $OUTPUT->confirm($message, $confirm, $returnurl); 94 | echo $OUTPUT->footer(); -------------------------------------------------------------------------------- /grade.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Redirect users who clicked on a link in the gradebook. 19 | */ 20 | require_once(dirname(__FILE__).'/../../config.php'); 21 | 22 | $id = required_param('id', PARAM_INT); // Course module ID. 23 | $itemnumber = optional_param('itemnumber', 0, PARAM_INT); // Item number, may be != 0 for activities that allow more than one 24 | // grade per user. 25 | $userid = optional_param('userid', 0, PARAM_INT); // Graded user ID (optional). 26 | 27 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 28 | print_error('invalidcoursemodule'); 29 | } 30 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 31 | print_error("coursemisconf"); 32 | } 33 | 34 | require_login($course, true, $cm); 35 | $context = context_module::instance($cm->id); 36 | if (has_capability('mod/adaptivequiz:viewreport', $context)) { 37 | $params = array('cmid' => $id); 38 | if ($userid) { 39 | $params['userid'] = $userid; 40 | $url = new moodle_url('/mod/adaptivequiz/viewattemptreport.php', $params); 41 | } else { 42 | $url = new moodle_url('/mod/adaptivequiz/viewreport.php', $params); 43 | } 44 | } else { 45 | $params = array('id' => $id); 46 | $url = new moodle_url('/mod/adaptivequiz/view.php', $params); 47 | } 48 | 49 | redirect($url); 50 | exit; 51 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Definition of log events for the adaptive quiz module. 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../config.php'); 29 | require_once($CFG->dirroot.'/mod/adaptivequiz/lib.php'); 30 | 31 | $id = required_param('id', PARAM_INT); // Course. 32 | 33 | $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); 34 | 35 | require_course_login($course); 36 | 37 | \mod_adaptivequiz\event\course_module_instance_list_viewed::create_from_course($course)->trigger(); 38 | 39 | $coursecontext = context_course::instance($course->id); 40 | 41 | $PAGE->set_url('/mod/adaptivequiz/index.php', array('id' => $id)); 42 | $PAGE->set_title(format_string($course->fullname)); 43 | $PAGE->set_heading(format_string($course->fullname)); 44 | $PAGE->set_context($coursecontext); 45 | 46 | echo $OUTPUT->header(); 47 | 48 | if (!$adaptivequizinstances = get_all_instances_in_course('adaptivequiz', $course)) { 49 | notice(get_string('nonewmodules', 'adaptivequiz'), new moodle_url('/course/view.php', array('id' => $course->id))); 50 | } 51 | 52 | $table = new html_table(); 53 | if ($course->format == 'weeks') { 54 | $table->head = array(get_string('week'), get_string('name')); 55 | $table->align = array('center', 'left'); 56 | } else if ($course->format == 'topics') { 57 | $table->head = array(get_string('topic'), get_string('name')); 58 | $table->align = array('center', 'left', 'left', 'left'); 59 | } else { 60 | $table->head = array(get_string('name')); 61 | $table->align = array('left', 'left', 'left'); 62 | } 63 | 64 | foreach ($adaptivequizinstances as $adaptivequizinstance) { 65 | if (!$adaptivequizinstance->visible) { 66 | $link = html_writer::link( 67 | new moodle_url('/mod/adaptivequiz/view.php', array('id' => $adaptivequizinstance->coursemodule)), 68 | format_string($adaptivequizinstance->name, true), 69 | array('class' => 'dimmed')); 70 | } else { 71 | $link = html_writer::link( 72 | new moodle_url('/mod/adaptivequiz/view.php', array('id' => $adaptivequizinstance->coursemodule)), 73 | format_string($adaptivequizinstance->name, true)); 74 | } 75 | 76 | if ($course->format == 'weeks' or $course->format == 'topics') { 77 | $table->data[] = array($adaptivequizinstance->section, $link); 78 | } else { 79 | $table->data[] = array($link); 80 | } 81 | } 82 | 83 | echo $OUTPUT->heading(get_string('modulenameplural', 'adaptivequiz'), 2); 84 | echo html_writer::table($table); 85 | echo $OUTPUT->footer(); 86 | -------------------------------------------------------------------------------- /mod_form.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive testing version information. 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | 30 | require_once($CFG->dirroot.'/course/moodleform_mod.php'); 31 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 32 | 33 | /** 34 | * Module instance settings form 35 | */ 36 | class mod_adaptivequiz_mod_form extends moodleform_mod { 37 | 38 | /** 39 | * Defines forms elements 40 | */ 41 | public function definition() { 42 | $mform = $this->_form; 43 | 44 | // Adding the "general" fieldset, where all the common settings are showed. 45 | $mform->addElement('header', 'general', get_string('general', 'form')); 46 | 47 | // Adding the standard "name" field. 48 | $mform->addElement('text', 'name', get_string('adaptivequizname', 'adaptivequiz'), array('size' => '64')); 49 | if (!empty($CFG->formatstringstriptags)) { 50 | $mform->setType('name', PARAM_TEXT); 51 | } else { 52 | $mform->setType('name', PARAM_CLEANHTML); 53 | } 54 | $mform->addRule('name', null, 'required', null, 'client'); 55 | $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); 56 | $mform->addHelpButton('name', 'adaptivequizname', 'adaptivequiz'); 57 | 58 | // Adding the standard "intro" and "introformat" fields... 59 | // Use the non deprecated function if it exists. 60 | if (method_exists($this, 'standard_intro_elements')) { 61 | $this->standard_intro_elements(); 62 | } else { 63 | // Deprecated as of Moodle 2.9 64 | $this->add_intro_editor(); 65 | } 66 | 67 | // Number of attempts. 68 | $attemptoptions = array('0' => get_string('unlimited')); 69 | for ($i = 1; $i <= ADAPTIVEQUIZMAXATTEMPT; $i++) { 70 | $attemptoptions[$i] = $i; 71 | } 72 | $mform->addElement('select', 'attempts', get_string('attemptsallowed', 'adaptivequiz'), $attemptoptions); 73 | $mform->setDefault('attempts', 0); 74 | $mform->addHelpButton('attempts', 'attemptsallowed', 'adaptivequiz'); 75 | 76 | // Require password to begin adaptivequiz attempt. 77 | $mform->addElement('passwordunmask', 'password', get_string('requirepassword', 'adaptivequiz')); 78 | $mform->setType('password', PARAM_TEXT); 79 | $mform->addHelpButton('password', 'requirepassword', 'adaptivequiz'); 80 | 81 | // Browser security choices. 82 | $options = array(get_string('no'), get_string('yes')); 83 | $mform->addElement('select', 'browsersecurity', get_string('browsersecurity', 'adaptivequiz'), $options); 84 | $mform->addHelpButton('browsersecurity', 'browsersecurity', 'adaptivequiz'); 85 | $mform->setDefault('browsersecurity', 0); 86 | 87 | // Retireve a list of available course categories. 88 | adaptivequiz_make_default_categories($this->context); 89 | $options = adaptivequiz_get_question_categories($this->context); 90 | $selquestcat = adaptivequiz_get_selected_question_cateogires($this->_instance); 91 | 92 | $select = $mform->addElement('select', 'questionpool', get_string('questionpool', 'adaptivequiz'), $options); 93 | $mform->addHelpButton('questionpool', 'questionpool', 'adaptivequiz'); 94 | $select->setMultiple(true); 95 | $mform->addRule('questionpool', null, 'required', null, 'client'); 96 | $mform->getElement('questionpool')->setSelected($selquestcat); 97 | 98 | $mform->addElement('text', 'startinglevel', get_string('startinglevel', 'adaptivequiz'), 99 | array('size' => '3', 'maxlength' => '3')); 100 | $mform->addHelpButton('startinglevel', 'startinglevel', 'adaptivequiz'); 101 | $mform->addRule('startinglevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client'); 102 | $mform->addRule('startinglevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client'); 103 | $mform->setType('startinglevel', PARAM_INT); 104 | 105 | $mform->addElement('text', 'lowestlevel', get_string('lowestlevel', 'adaptivequiz'), 106 | array('size' => '3', 'maxlength' => '3')); 107 | $mform->addHelpButton('lowestlevel', 'lowestlevel', 'adaptivequiz'); 108 | $mform->addRule('lowestlevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client'); 109 | $mform->addRule('lowestlevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client'); 110 | $mform->setType('lowestlevel', PARAM_INT); 111 | 112 | $mform->addElement('text', 'highestlevel', get_string('highestlevel', 'adaptivequiz'), 113 | array('size' => '3', 'maxlength' => '3')); 114 | $mform->addHelpButton('highestlevel', 'highestlevel', 'adaptivequiz'); 115 | $mform->addRule('highestlevel', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client'); 116 | $mform->addRule('highestlevel', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client'); 117 | $mform->setType('highestlevel', PARAM_INT); 118 | 119 | $mform->addElement('textarea', 'attemptfeedback', get_string('attemptfeedback', 'adaptivequiz'), 120 | 'wrap="virtual" rows="10" cols="50"'); 121 | $mform->addHelpButton('attemptfeedback', 'attemptfeedback', 'adaptivequiz'); 122 | $mform->setType('attemptfeedback', PARAM_NOTAGS); 123 | 124 | $mform->addElement('header', 'stopingconditionshdr', get_string('stopingconditionshdr', 'adaptivequiz')); 125 | 126 | $mform->addElement('text', 'minimumquestions', get_string('minimumquestions', 'adaptivequiz'), 127 | array('size' => '3', 'maxlength' => '3')); 128 | $mform->addHelpButton('minimumquestions', 'minimumquestions', 'adaptivequiz'); 129 | $mform->addRule('minimumquestions', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client'); 130 | $mform->addRule('minimumquestions', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client'); 131 | $mform->setType('minimumquestions', PARAM_INT); 132 | 133 | $mform->addElement('text', 'maximumquestions', get_string('maximumquestions', 'adaptivequiz'), 134 | array('size' => '3', 'maxlength' => '3')); 135 | $mform->addHelpButton('maximumquestions', 'maximumquestions', 'adaptivequiz'); 136 | $mform->addRule('maximumquestions', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client'); 137 | $mform->addRule('maximumquestions', get_string('formelementnumeric', 'adaptivequiz'), 'numeric', null, 'client'); 138 | $mform->setType('maximumquestions', PARAM_INT); 139 | 140 | $mform->addElement('text', 'standarderror', get_string('standarderror', 'adaptivequiz'), 141 | array('size' => '10', 'maxlength' => '10')); 142 | $mform->addHelpButton('standarderror', 'standarderror', 'adaptivequiz'); 143 | $mform->addRule('standarderror', get_string('formelementempty', 'adaptivequiz'), 'required', null, 'client'); 144 | $mform->addRule('standarderror', get_string('formelementdecimal', 'adaptivequiz'), 'numeric', null, 'client'); 145 | $mform->setDefault('standarderror', 5.0); 146 | $mform->setType('standarderror', PARAM_FLOAT); 147 | 148 | // Grade settings. 149 | $this->standard_grading_coursemodule_elements(); 150 | $mform->removeElement('grade'); 151 | 152 | // Grading method. 153 | $mform->addElement('select', 'grademethod', get_string('grademethod', 'adaptivequiz'), 154 | adaptivequiz_get_grading_options()); 155 | $mform->addHelpButton('grademethod', 'grademethod', 'adaptivequiz'); 156 | $mform->setDefault('grademethod', ADAPTIVEQUIZ_GRADEHIGHEST); 157 | $mform->disabledIf('grademethod', 'attempts', 'eq', 1); 158 | 159 | // Add standard elements, common to all modules. 160 | $this->standard_coursemodule_elements(); 161 | 162 | // Add standard buttons, common to all modules. 163 | $this->add_action_buttons(); 164 | } 165 | 166 | /** 167 | * Perform extra validation. @see validation() - in moodleform_mod.php 168 | * @param array $data: array of submitted form values 169 | * @param array $files: array of file data 170 | * @return array - array of form elements that didn't pass validation 171 | */ 172 | public function validation($data, $files) { 173 | $errors = array(); 174 | 175 | if (empty($data['questionpool'])) { 176 | $errors['questionpool'] = get_string('formquestionpool', 'adaptivequiz'); 177 | } 178 | 179 | // Validate for positivity. 180 | if (0 >= $data['minimumquestions']) { 181 | $errors['minimumquestions'] = get_string('formelementnegative', 'adaptivequiz'); 182 | } 183 | 184 | if (0 >= $data['maximumquestions']) { 185 | $errors['maximumquestions'] = get_string('formelementnegative', 'adaptivequiz'); 186 | } 187 | 188 | if (0 >= $data['startinglevel']) { 189 | $errors['startinglevel'] = get_string('formelementnegative', 'adaptivequiz'); 190 | } 191 | 192 | if (0 >= $data['lowestlevel']) { 193 | $errors['lowestlevel'] = get_string('formelementnegative', 'adaptivequiz'); 194 | } 195 | 196 | if (0 >= $data['highestlevel']) { 197 | $errors['highestlevel'] = get_string('formelementnegative', 'adaptivequiz'); 198 | } 199 | 200 | if ((float) 0 > (float) $data['standarderror'] || (float) 50 <= (float) $data['standarderror']) { 201 | $errors['standarderror'] = get_string('formstderror', 'adaptivequiz'); 202 | } 203 | 204 | // Validate higher and lower values. 205 | if ($data['minimumquestions'] >= $data['maximumquestions']) { 206 | $errors['minimumquestions'] = get_string('formminquestgreaterthan', 'adaptivequiz'); 207 | } 208 | 209 | if ($data['lowestlevel'] >= $data['highestlevel']) { 210 | $errors['lowestlevel'] = get_string('formlowlevelgreaterthan', 'adaptivequiz'); 211 | } 212 | 213 | if (!($data['startinglevel'] >= $data['lowestlevel'] && $data['startinglevel'] <= $data['highestlevel'])) { 214 | $errors['startinglevel'] = get_string('formstartleveloutofbounds', 'adaptivequiz'); 215 | } 216 | 217 | return $errors; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /module.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | /** 17 | * JavaScript library for the adaptivequiz module. 18 | * 19 | * This module was created as a collaborative effort between Middlebury College 20 | * and Remote Learner. 21 | * 22 | * @package mod_adaptivequiz 23 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | 27 | M.mod_adaptivequiz = M.mod_adaptivequiz || {}; 28 | 29 | M.mod_adaptivequiz.init_attempt_form = function(Y, url, secure) { 30 | // Check if the page is on the password required page 31 | if (null == document.getElementById('id_quizpassword')) { 32 | M.core_question_engine.init_form(Y, '#responseform'); 33 | M.core_formchangechecker.init({formid: 'responseform'}); 34 | } else { 35 | if ('1' == secure) { 36 | Y.on('click', function(e) { 37 | M.mod_adaptivequiz.secure_window.close(url, 0) 38 | }, '#id_cancel'); 39 | } 40 | } 41 | }; 42 | 43 | M.mod_adaptivequiz.secure_window = { 44 | init: function(Y) { 45 | if (window.location.href.substring(0, 4) == 'file') { 46 | window.location = 'about:blank'; 47 | } 48 | Y.delegate('contextmenu', M.mod_adaptivequiz.secure_window.prevent, document, '*'); 49 | Y.delegate('mousedown', M.mod_adaptivequiz.secure_window.prevent_mouse, document, '*'); 50 | Y.delegate('mouseup', M.mod_adaptivequiz.secure_window.prevent_mouse, document, '*'); 51 | Y.delegate('dragstart', M.mod_adaptivequiz.secure_window.prevent, document, '*'); 52 | Y.delegate('selectstart', M.mod_adaptivequiz.secure_window.prevent, document, '*'); 53 | Y.delegate('cut', M.mod_adaptivequiz.secure_window.prevent, document, '*'); 54 | Y.delegate('copy', M.mod_adaptivequiz.secure_window.prevent, document, '*'); 55 | Y.delegate('paste', M.mod_adaptivequiz.secure_window.prevent, document, '*'); 56 | M.mod_adaptivequiz.secure_window.clear_status; 57 | Y.on('beforeprint', function() { 58 | Y.one(document.body).setStyle('display', 'none'); 59 | }, window); 60 | Y.on('afterprint', function() { 61 | Y.one(document.body).setStyle('display', 'block'); 62 | }, window); 63 | Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'press:67,86,88+ctrl'); 64 | Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'up:67,86,88+ctrl'); 65 | Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'down:67,86,88+ctrl'); 66 | Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'press:67,86,88+meta'); 67 | Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'up:67,86,88+meta'); 68 | Y.on('key', M.mod_adaptivequiz.secure_window.prevent, '*', 'down:67,86,88+meta'); 69 | }, 70 | 71 | clear_status: function() { 72 | window.status = ''; 73 | setTimeout(M.mod_adaptivequiz.secure_window.clear_status, 10); 74 | }, 75 | 76 | prevent: function(e) { 77 | alert(M.str.adaptivequiz.functiondisabledbysecuremode); 78 | e.halt(); 79 | }, 80 | 81 | prevent_mouse: function(e) { 82 | if (e.button == 1 && /^(INPUT|TEXTAREA|BUTTON|SELECT|LABEL|A)$/i.test(e.target.get('tagName'))) { 83 | // Left click on a button or similar. No worries. 84 | return; 85 | } 86 | e.halt(); 87 | }, 88 | 89 | /** 90 | * Event handler for the adaptivequiz start attempt button. 91 | */ 92 | start_attempt_action: function(e, args) { 93 | if (args.startattemptwarning == '') { 94 | openpopup(e, args); 95 | } else { 96 | M.util.show_confirm_dialog(e, { 97 | message: args.startattemptwarning, 98 | callback: function() { 99 | openpopup(e, args); 100 | }, 101 | continuelabel: M.util.get_string('startattempt', 'quiz') 102 | }); 103 | } 104 | }, 105 | 106 | init_close_button: function(Y, url) { 107 | Y.on('click', function(e) { 108 | M.mod_adaptivequiz.secure_window.close(url, 0) 109 | }, '#secureclosebutton'); 110 | }, 111 | 112 | close: function(Y, url, delay) { 113 | setTimeout(function() { 114 | if (window.opener) { 115 | window.opener.document.location.reload(); 116 | window.close(); 117 | } else { 118 | window.location.href = url; 119 | } 120 | }, delay * 1000); 121 | } 122 | }; 123 | 124 | M.mod_adaptivequiz.init_comment_popup = function(Y) { 125 | // Add a close button to the window. 126 | var closebutton = Y.Node.create(''); 127 | closebutton.set('value', M.util.get_string('cancel', 'moodle')); 128 | Y.one('#id_submitbutton').ancestor().append(closebutton); 129 | Y.on('click', function() { window.close() }, closebutton); 130 | } 131 | 132 | M.mod_adaptivequiz.init_reviewattempt = function(Y) { 133 | Y.one('#adpq_scoring_table').hide(); 134 | Y.one('#adpq_scoring_table_link_icon').setContent('▶'); 135 | 136 | Y.use('node', function(Y) { 137 | Y.delegate('click', function(e) { 138 | if (e.currentTarget.get('id') === 'adpq_scoring_table_link') { 139 | var table = Y.one('#adpq_scoring_table'); 140 | table.toggleView(); 141 | if (table.getComputedStyle('display') === 'none') { 142 | Y.one('#adpq_scoring_table_link_icon').setContent('▶'); 143 | } else { 144 | Y.one('#adpq_scoring_table_link_icon').setContent('▼'); 145 | } 146 | } 147 | }, document, 'a'); 148 | }); 149 | }; -------------------------------------------------------------------------------- /pix/attemptgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middlebury/moodle-mod_adaptivequiz/c793dd9f55bee78e7664587f80565cf00d4d9a91/pix/attemptgraph.png -------------------------------------------------------------------------------- /pix/icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middlebury/moodle-mod_adaptivequiz/c793dd9f55bee78e7664587f80565cf00d4d9a91/pix/icon.gif -------------------------------------------------------------------------------- /pix/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/middlebury/moodle-mod_adaptivequiz/c793dd9f55bee78e7664587f80565cf00d4d9a91/pix/icon.png -------------------------------------------------------------------------------- /pix/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /questionanalysis/lib/attempt_score.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | 18 | /** 19 | * Attempt-score class 20 | * 21 | * This class provides access to various numeric representations of a score. 22 | * 23 | * This module was created as a collaborative effort between Middlebury College 24 | * and Remote Learner. 25 | * 26 | * @package mod_adaptivequiz 27 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 28 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 | */ 30 | class adaptivequiz_attempt_score { 31 | 32 | /** @var float $measuredabilitylogits The measured ability of the attempt in logits. */ 33 | protected $measuredabilitylogits = null; 34 | 35 | /** @var float $standarderrorlogits The standard error in the score in logits. */ 36 | protected $standarderrorlogits = null; 37 | 38 | /** @var float $lowestlevel The lowest level of question in the adaptive quiz. */ 39 | protected $lowestlevel = null; 40 | 41 | /** @var float $highestlevel The highest level of question in the adaptive quiz. */ 42 | protected $highestlevel = null; 43 | 44 | /** 45 | * Constructor 46 | * 47 | * @return void 48 | */ 49 | public function __construct ($measuredabilitylogits, $standarderrorlogits, $lowestlevel, $highestlevel) { 50 | $this->measuredabilitylogits = $measuredabilitylogits; 51 | $this->standarderrorlogits = $standarderrorlogits; 52 | $this->lowestlevel = $lowestlevel; 53 | $this->highestlevel = $highestlevel; 54 | } 55 | 56 | /** 57 | * Answer the measured ability in logits. 58 | * 59 | * @return float 60 | */ 61 | public function measured_ability_in_logits () { 62 | return $this->measuredabilitylogits; 63 | } 64 | 65 | /** 66 | * Answer the standard error in logits. 67 | * 68 | * @return float 69 | */ 70 | public function standard_error_in_logits () { 71 | return $this->standarderrorlogits; 72 | } 73 | 74 | /** 75 | * Answer the measured ability as a fraction 0-1. 76 | * 77 | * @return float 78 | */ 79 | public function measured_ability_in_fraction () { 80 | return catalgo::convert_logit_to_fraction($this->measuredabilitylogits); 81 | } 82 | 83 | /** 84 | * Answer the standard error a fraction 0-0.5. 85 | * 86 | * @return float 87 | */ 88 | public function standard_error_in_fraction () { 89 | return catalgo::convert_logit_to_percent($this->standarderrorlogits); 90 | } 91 | 92 | /** 93 | * Answer the measured ability on the adaptive quiz's scale 94 | * 95 | * @return float 96 | */ 97 | public function measured_ability_in_scale () { 98 | return catalgo::map_logit_to_scale($this->measuredabilitylogits, $this->highestlevel, $this->lowestlevel); 99 | } 100 | 101 | /** 102 | * Answer the standard error on the adaptive quiz's scale 103 | * 104 | * @return float 105 | */ 106 | public function standard_error_in_scale () { 107 | return catalgo::convert_logit_to_percent($this->standarderrorlogits) * ($this->highestlevel - $this->lowestlevel); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /questionanalysis/lib/question_analyser.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Question-analyser class 19 | * 20 | * This class provides a mechanism for analysing the usage, performance, and efficacy 21 | * of a single question in an adaptive quiz. 22 | * 23 | * This module was created as a collaborative effort between Middlebury College 24 | * and Remote Learner. 25 | * 26 | * @package mod_adaptivequiz 27 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 28 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 29 | */ 30 | class adaptivequiz_question_analyser { 31 | 32 | /** @var context the context this usage belongs to. */ 33 | protected $context; 34 | 35 | /** @var question_definition $definition The question */ 36 | protected $definition = null; 37 | 38 | /** @var float $level The question level */ 39 | protected $level = null; 40 | 41 | /** @var float $lowestlevel The lowest question-level in the adaptive quiz */ 42 | protected $lowestlevel = null; 43 | 44 | /** @var float $highestlevel The highest question-level in the adaptive quiz */ 45 | protected $highestlevel = null; 46 | 47 | /** @var array $results An array of the re objects */ 48 | protected $results = array(); 49 | 50 | /** @var array $statistics An array of the adaptivequiz_question_statistic added to this report */ 51 | protected $statistics = array(); 52 | 53 | /** @var array $statisticresults An array of the adaptivequiz_question_statistic_result added to this report */ 54 | protected $statisticresults = array(); 55 | 56 | /** 57 | * Constructor - Create a new analyser. 58 | * 59 | * @param object $context 60 | * @param question_definition $definition 61 | * @param float $level The level (0-1) of the question. 62 | * @return void 63 | */ 64 | public function __construct ($context, question_definition $definition, $level, $lowestlevel, $highestlevel) { 65 | $this->context = $context; 66 | $this->definition = $definition; 67 | $this->level = $level; 68 | $this->lowestlevel = $lowestlevel; 69 | $this->highestlevel = $highestlevel; 70 | } 71 | 72 | /** 73 | * Add an usage result for this question. 74 | * 75 | * @param adaptivequiz_attempt_score $score The user's score on this attempt. 76 | * @param boolean $correct True if the user answered correctly. 77 | * @param string $answer 78 | * @return void 79 | */ 80 | public function add_result ($attemptid, $user, adaptivequiz_attempt_score $score, $correct, $answer) { 81 | $result = new stdClass(); 82 | $result->attemptid = $attemptid; 83 | $result->user = $user; 84 | $result->score = $score; 85 | $result->correct = $correct; 86 | $result->answer = $answer; 87 | $this->results[] = $result; 88 | } 89 | 90 | /** 91 | * @return context the context this usage belongs to. 92 | */ 93 | public function get_owning_context() { 94 | return $this->context; 95 | } 96 | 97 | /** 98 | * Answer the question definition for this question. 99 | * 100 | * @return question_definition 101 | */ 102 | public function get_question_definition () { 103 | return $this->definition; 104 | } 105 | 106 | /** 107 | * Answer the question level for this question. 108 | * 109 | * @return int 110 | */ 111 | public function get_question_level () { 112 | return $this->level; 113 | } 114 | 115 | /** 116 | * Answer the question level for this question in logits. 117 | * 118 | * @return int 119 | */ 120 | public function get_question_level_in_logits () { 121 | return catalgo::convert_linear_to_logit($this->level, $this->lowestlevel, $this->highestlevel); 122 | } 123 | 124 | /** 125 | * Answer the results for this question 126 | * 127 | * @return array An array of stdClass objects. 128 | */ 129 | public function get_results () { 130 | return $this->results; 131 | } 132 | 133 | /** 134 | * Add and calculate a statistic. 135 | * 136 | * @param string $key A key to identify this statistic for sorting and printing. 137 | * @param adaptivequiz_question_statistic $statistic 138 | * @return void 139 | */ 140 | public function add_statistic ($key, adaptivequiz_question_statistic $statistic) { 141 | if (!empty($this->statistics[$key])) { 142 | throw new InvalidArgumentException("Statistic key '$key' is already in use."); 143 | } 144 | $this->statistics[$key] = $statistic; 145 | $this->statisticresults[$key] = $statistic->calculate($this); 146 | } 147 | 148 | /** 149 | * Answer a statistic result. 150 | * 151 | * @param string $key A key to identify this statistic. 152 | * @return adaptivequiz_question_statistic_result 153 | */ 154 | public function get_statistic_result ($key) { 155 | if (empty($this->statisticresults[$key])) { 156 | throw new InvalidArgumentException("Unknown statistic key '$key'."); 157 | } 158 | return $this->statisticresults[$key]; 159 | } 160 | 161 | /** 162 | * Utility function to map a logit value to this question's scale 163 | * 164 | * @param $logit 165 | * @return float Scaled value 166 | */ 167 | public function map_logit_to_scale ($logit) { 168 | return catalgo::map_logit_to_scale($logit, $this->highestlevel, $this->lowestlevel); 169 | } 170 | } -------------------------------------------------------------------------------- /questionanalysis/lib/question_result.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Question-result class 19 | * 20 | * This class stores information about a particular attempt's result on a question 21 | * 22 | * This module was created as a collaborative effort between Middlebury College 23 | * and Remote Learner. 24 | * 25 | * @package mod_adaptivequiz 26 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 27 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 | */ 29 | class adaptivequiz_question_result { 30 | 31 | /** @var float $_measuredability The measured ability of the user who attempted this question */ 32 | protected $_measuredability = null; 33 | 34 | /** @var boolean $_correct True if the user was correct in their answer */ 35 | protected $_correct = null; 36 | 37 | /** 38 | * Constructor - Create a new result. 39 | * 40 | * @param float $measuredability The measured ability (0-1) of the user in this attempt. 41 | * @param boolean $correct 42 | * @return void 43 | */ 44 | public function __construct ($measuredability, $correct) { 45 | if (!is_numeric($measuredability) || $measuredability < 0 || $measuredability > 1) { 46 | throw new InvalidArgumentException('$measuredability must be a float between 0 and 1.'); 47 | } 48 | $this->_measuredability = $measuredability; 49 | $this->_correct = (bool)$correct; 50 | } 51 | 52 | /** 53 | * Magic method to provide read-only access to our parameters 54 | * 55 | * @param $key 56 | * @return mixed 57 | */ 58 | public function __get ($key) { 59 | $param = '$_'.$key; 60 | if (isset($this->$param)) { 61 | return $this->$param; 62 | } else { 63 | throw new Exception('Unknown property, '.get_class($this).'->'.$key.'.'); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /questionanalysis/lib/quiz_analyser.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | require_once(dirname(__FILE__).'/question_analyser.class.php'); 18 | require_once(dirname(__FILE__).'/attempt_score.class.php'); 19 | require_once(dirname(__FILE__).'/statistics/question_statistic.interface.php'); 20 | require_once(dirname(__FILE__).'/../../catalgo.class.php'); 21 | require_once($CFG->dirroot.'/tag/lib.php'); 22 | 23 | /** 24 | * Questions-analyser class 25 | * 26 | * This class provides a mechanism for loading and analysing question usage, 27 | * performance, and efficacy. 28 | * 29 | * This module was created as a collaborative effort between Middlebury College 30 | * and Remote Learner. 31 | * 32 | * @package mod_adaptivequiz 33 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class adaptivequiz_quiz_analyser { 37 | 38 | /** @var array $questions An array of all questions loaded and their stats */ 39 | protected $questions = array(); 40 | 41 | /** @var array $statistics An array of the statistics added to this report */ 42 | protected $statistics = array(); 43 | 44 | /** 45 | * Constructor - Create a new analyser. 46 | * 47 | * @return void 48 | */ 49 | public function __construct () { 50 | 51 | } 52 | 53 | /** 54 | * Load attempts from an adaptive quiz instance 55 | * 56 | * @param int $instance 57 | * @return void 58 | */ 59 | public function load_attempts ($instance) { 60 | global $DB; 61 | 62 | $adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $instance), '*'); 63 | 64 | // Get all of the completed attempts for this adaptive quiz instance. 65 | $attempts = $DB->get_records('adaptivequiz_attempt', 66 | array('instance' => $instance, 'attemptstate' => ADAPTIVEQUIZ_ATTEMPT_COMPLETED)); 67 | 68 | foreach ($attempts as $attempt) { 69 | $user = $DB->get_record('user', array('id' => $attempt->userid)); 70 | if (!$user) { 71 | $user = new stdClass(); 72 | $user->firstname = get_string('unknownuser', 'adaptivequiz'); 73 | $user->lastname = '#'.$userid; 74 | } 75 | 76 | // For each attempt, get the attempt's final score. 77 | $score = new adaptivequiz_attempt_score($attempt->measure, $attempt->standarderror, $adaptivequiz->lowestlevel, 78 | $adaptivequiz->highestlevel); 79 | 80 | // For each attempt, loop through all questions asked and add that usage 81 | // to the question. 82 | $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid); 83 | foreach ($quba->get_slots() as $i => $slot) { 84 | $question = $quba->get_question($slot); 85 | 86 | // Create a question-analyser for the question. 87 | if (empty($this->questions[$question->id])) { 88 | $tags = tag_get_tags_array('question', $question->id); 89 | $difficulty = adaptivequiz_get_difficulty_from_tags($tags); 90 | $this->questions[$question->id] = new adaptivequiz_question_analyser($quba->get_owning_context(), $question, 91 | $difficulty, $adaptivequiz->lowestlevel, $adaptivequiz->highestlevel); 92 | } 93 | 94 | // Record the attempt score and the individual question result. 95 | $correct = ($quba->get_question_mark($slot) > 0); 96 | $answer = $quba->get_response_summary($slot); 97 | $this->questions[$question->id]->add_result($attempt->uniqueid, $user, $score, $correct, $answer); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Add a statistic to calculate. 104 | * 105 | * @param string $key A key to identify this statistic for sorting and printing. 106 | * @param adaptivequiz_question_statistic $statistic 107 | * @return void 108 | */ 109 | public function add_statistic ($key, adaptivequiz_question_statistic $statistic) { 110 | if (!empty($this->statistics[$key])) { 111 | throw new InvalidArgumentException("Statistic key '$key' is already in use."); 112 | } 113 | $this->statistics[$key] = $statistic; 114 | foreach ($this->questions as $question) { 115 | $question->add_statistic($key, $statistic); 116 | } 117 | } 118 | 119 | /** 120 | * Answer a header row. 121 | * 122 | * @return array 123 | */ 124 | public function get_header () { 125 | $header = array(); 126 | $header['id'] = get_string('id', 'adaptivequiz'); 127 | $header['name'] = get_string('adaptivequizname', 'adaptivequiz'); 128 | $header['level'] = get_string('attemptquestion_level', 'adaptivequiz'); 129 | foreach ($this->statistics as $key => $statistic) { 130 | $header[$key] = $statistic->get_display_name(); 131 | } 132 | return $header; 133 | } 134 | 135 | /** 136 | * Return an array of table records, sorted by the statisic given 137 | * 138 | * @param optional string $sort Which statistic to sort on. 139 | * @param optional string $direction ASC or DESC. 140 | * @return array 141 | */ 142 | public function get_records ($sort = null, $direction = 'ASC') { 143 | $records = array(); 144 | 145 | foreach ($this->questions as $question) { 146 | $record = array(); 147 | $record[] = $question->get_question_definition()->id; 148 | $record[] = $question->get_question_definition()->name; 149 | $record[] = $question->get_question_level(); 150 | foreach ($this->statistics as $key => $statistic) { 151 | $record[] = $question->get_statistic_result($key)->printable(); 152 | } 153 | $records[] = $record; 154 | } 155 | 156 | if ($direction != 'ASC' && $direction != 'DESC') { 157 | throw new InvalidArgumentException('Invalid sort direction. Must be SORT_ASC or SORT_DESC, \''.$direction.'\' given.'); 158 | } 159 | if ($direction == 'DESC') { 160 | $direction = SORT_DESC; 161 | } else { 162 | $direction = SORT_ASC; 163 | } 164 | 165 | if (!is_null($sort)) { 166 | $sortkeys = array(); 167 | foreach ($this->questions as $question) { 168 | if ($sort == 'name') { 169 | $sortkeys[] = $question->get_question_definition()->name; 170 | $sorttype = SORT_REGULAR; 171 | } else if ($sort == 'level') { 172 | $sortkeys[] = $question->get_question_level(); 173 | $sorttype = SORT_NUMERIC; 174 | } else { 175 | $sortkeys[] = $question->get_statistic_result($sort)->sortable(); 176 | $sorttype = SORT_NUMERIC; 177 | } 178 | } 179 | array_multisort($sortkeys, $direction, $sorttype, $records); 180 | } 181 | 182 | return $records; 183 | } 184 | 185 | /** 186 | * Answer a question-analyzer for a particular question id analyze 187 | * 188 | * @param int $qid The question id 189 | * @return adaptivequiz_question_analyser 190 | */ 191 | public function get_question_analyzer ($qid) { 192 | if (!isset($this->questions[$qid])) { 193 | throw new Exception('Question-id not found.'); 194 | } 195 | return $this->questions[$qid]; 196 | } 197 | 198 | /** 199 | * Answer the record for a single question 200 | * 201 | * @param int $qid The question id 202 | * @return array 203 | */ 204 | public function get_record ($qid) { 205 | $question = $this->get_question_analyzer($qid); 206 | $record = array(); 207 | $record[] = $question->get_question_definition()->id; 208 | $record[] = $question->get_question_definition()->name; 209 | $record[] = $question->get_question_level(); 210 | foreach ($this->statistics as $key => $statistic) { 211 | $record[] = $question->get_statistic_result($key)->printable(); 212 | } 213 | return $record; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /questionanalysis/lib/statistics/answers_statistic.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | require_once(dirname(__FILE__).'/question_statistic.interface.php'); 18 | 19 | /** 20 | * Questions-statistic interface 21 | * 22 | * This interface defines the methods required for pluggable statistics that may be added to the question analysis. 23 | * 24 | * This module was created as a collaborative effort between Middlebury College 25 | * and Remote Learner. 26 | * 27 | * @package mod_adaptivequiz 28 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | class adaptivequiz_answers_statistic implements adaptivequiz_question_statistic { 32 | 33 | /** 34 | * Answer a display-name for this statistic. 35 | * 36 | * @return string 37 | */ 38 | public function get_display_name () { 39 | return get_string('answers_display_name', 'adaptivequiz'); 40 | } 41 | 42 | /** 43 | * Calculate this statistic for a question's results 44 | * 45 | * @param adaptivequiz_question_analyser $analyser 46 | * @return adaptivequiz_question_statistic_result 47 | */ 48 | public function calculate (adaptivequiz_question_analyser $analyser) { 49 | // Sort the results. 50 | $results = $analyser->get_results(); 51 | foreach ($results as $result) { 52 | $sortkeys[] = $result->score->measured_ability_in_logits(); 53 | } 54 | array_multisort($sortkeys, SORT_NUMERIC, SORT_DESC, $results); 55 | 56 | // Sort the results into three arrays based on how far above or below the question-level the users are. 57 | $high = array(); 58 | $mid = array(); 59 | $low = array(); 60 | foreach ($results as $result) { 61 | $ceiling = $result->score->measured_ability_in_logits() + $result->score->standard_error_in_logits(); 62 | $floor = $result->score->measured_ability_in_logits() - $result->score->standard_error_in_logits(); 63 | if ($analyser->get_question_level_in_logits() < $floor) { 64 | // User is significantly above the question-level. 65 | $high[] = $result; 66 | } else if ($analyser->get_question_level_in_logits() > $ceiling) { 67 | // User is significantly below the question-level. 68 | $low[] = $result; 69 | } else { 70 | // User's ability overlaps the question level. 71 | $mid[] = $result; 72 | } 73 | } 74 | 75 | ob_start(); 76 | print html_writer::end_tag('tr'); 77 | print html_writer::start_tag('tr'); 78 | print html_writer::tag('th', get_string('attemptquestion_ability', 'adaptivequiz')); 79 | print html_writer::tag('th', get_string('user', 'adaptivequiz')); 80 | print html_writer::tag('th', get_string('result', 'adaptivequiz')); 81 | print html_writer::tag('th', get_string('answer', 'adaptivequiz')); 82 | print html_writer::tag('th', ''); 83 | print html_writer::end_tag('tr'); 84 | $headings = ob_get_clean(); 85 | 86 | ob_start(); 87 | print html_writer::start_tag('table', array('class' => 'adpq_answers_table')); 88 | 89 | print html_writer::start_tag('thead'); 90 | print html_writer::start_tag('tr'); 91 | print html_writer::tag('th', get_string('highlevelusers', 'adaptivequiz').':', 92 | array('colspan' => '5', 'class' => 'section')); 93 | print $headings; 94 | print html_writer::end_tag('thead'); 95 | 96 | print html_writer::start_tag('tbody', array('class' => 'adpq_highlevel')); 97 | if (count($high)) { 98 | foreach ($high as $result) { 99 | $this->print_user_result($analyser, $result); 100 | } 101 | } else { 102 | $this->print_empty_user_result(); 103 | } 104 | print html_writer::end_tag('tbody'); 105 | 106 | print html_writer::start_tag('thead'); 107 | print html_writer::start_tag('tr'); 108 | print html_writer::tag('th', get_string('midlevelusers', 'adaptivequiz').':', 109 | array('colspan' => '5', 'class' => 'section')); 110 | print $headings; 111 | print html_writer::end_tag('thead'); 112 | 113 | print html_writer::start_tag('tbody', array('class' => 'adpq_midlevel')); 114 | if (count($mid)) { 115 | foreach ($mid as $result) { 116 | $this->print_user_result($analyser, $result); 117 | } 118 | } else { 119 | $this->print_empty_user_result(); 120 | } 121 | print html_writer::end_tag('tbody'); 122 | 123 | print html_writer::start_tag('thead'); 124 | print html_writer::start_tag('tr'); 125 | print html_writer::tag('th', get_string('lowlevelusers', 'adaptivequiz').':', 126 | array('colspan' => '5', 'class' => 'section')); 127 | print $headings; 128 | print html_writer::end_tag('thead'); 129 | 130 | print html_writer::start_tag('tbody', array('class' => 'adpq_lowlevel')); 131 | if (count($low)) { 132 | foreach ($low as $result) { 133 | $this->print_user_result($analyser, $result); 134 | } 135 | } else { 136 | $this->print_empty_user_result(); 137 | } 138 | print html_writer::end_tag('tbody'); 139 | 140 | print html_writer::end_tag('table'); 141 | 142 | return new adaptivequiz_answers_statistic_result (count($results), ob_get_clean()); 143 | } 144 | 145 | /** 146 | * Print out a user result 147 | * 148 | * @param adaptivequiz_question_analyser $analyser 149 | * @param stdClass $result 150 | * @return void 151 | */ 152 | public function print_user_result (adaptivequiz_question_analyser $analyser, $result) { 153 | if ($result->correct) { 154 | $class = 'adpq_correct'; 155 | } else { 156 | $class = 'adpq_incorrect'; 157 | } 158 | $url = new moodle_url('/mod/adaptivequiz/reviewattempt.php', array( 159 | 'cmid' => $analyser->get_owning_context()->instanceid, 160 | 'uniqueid' => $result->attemptid, 161 | 'userid' => $result->user->id)); 162 | print html_writer::start_tag('tr', array('class' => $class)); 163 | print html_writer::tag('td', round($result->score->measured_ability_in_scale(), 2)); 164 | print html_writer::tag('td', $result->user->firstname." ".$result->user->lastname); 165 | print html_writer::tag('td', (($result->correct) ? "correct" : "incorrect")); 166 | print html_writer::tag('td', $result->answer); 167 | print html_writer::tag('td', html_writer::link($url, get_string('reviewattempt', 'adaptivequiz'))); 168 | print html_writer::end_tag('tr'); 169 | } 170 | 171 | /** 172 | * Print out an empty user-result row. 173 | * 174 | * @param adaptivequiz_question_analyser $analyser 175 | * @param stdClass $result 176 | * @return void 177 | */ 178 | public function print_empty_user_result () { 179 | print html_writer::start_tag('tr'); 180 | print html_writer::tag('td', ''); 181 | print html_writer::tag('td', ''); 182 | print html_writer::tag('td', ''); 183 | print html_writer::tag('td', ''); 184 | print html_writer::tag('td', ''); 185 | print html_writer::end_tag('tr'); 186 | } 187 | } 188 | 189 | /** 190 | * Questions-statistic-result interface 191 | * 192 | * This interface defines the methods required for pluggable statistic-results that may be added to the question analysis. 193 | * 194 | * This module was created as a collaborative effort between Middlebury College 195 | * and Remote Learner. 196 | * 197 | * @package mod_adaptivequiz 198 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 199 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 200 | */ 201 | class adaptivequiz_answers_statistic_result implements adaptivequiz_question_statistic_result { 202 | 203 | /** @var int $count */ 204 | protected $count = null; 205 | 206 | /** @var string $printable */ 207 | protected $printable = null; 208 | 209 | /** 210 | * Constructor 211 | * 212 | * @param int $count 213 | * @return void 214 | */ 215 | public function __construct ($count, $printable) { 216 | $this->count = $count; 217 | $this->printable = $printable; 218 | } 219 | 220 | /** 221 | * A sortable version of the result. 222 | * 223 | * @return mixed string or numeric 224 | */ 225 | public function sortable () { 226 | return $this->count; 227 | } 228 | 229 | /** 230 | * A printable version of the result. 231 | * 232 | * @param numeric $result 233 | * @return mixed string or numeric 234 | */ 235 | public function printable () { 236 | return $this->printable; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /questionanalysis/lib/statistics/discrimination_statistic.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | require_once(dirname(__FILE__).'/question_statistic.interface.php'); 18 | 19 | /** 20 | * Questions-statistic 21 | * 22 | * This interface defines the methods required for pluggable statistics that may be added to the question analysis. 23 | * 24 | * This module was created as a collaborative effort between Middlebury College 25 | * and Remote Learner. 26 | * 27 | * @package mod_adaptivequiz 28 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | class adaptivequiz_discrimination_statistic implements adaptivequiz_question_statistic { 32 | 33 | /** 34 | * Answer a display-name for this statistic. 35 | * 36 | * @return string 37 | */ 38 | public function get_display_name () { 39 | return get_string('discrimination_display_name', 'adaptivequiz'); 40 | } 41 | 42 | /** 43 | * Calculate this statistic for a question's results 44 | * 45 | * @param adaptivequiz_question_analyser $analyser 46 | * @return adaptivequiz_question_statistic_result 47 | */ 48 | public function calculate (adaptivequiz_question_analyser $analyser) { 49 | // Discrimination is generally defined as comparing the results of two sub-groups, 50 | // the top 27% of test-takers (the upper group) and the bottom 27% of test-takers (the lower group), 51 | // assuming a normal distribution of scores). 52 | // 53 | // Given that likely have a very sparse data-set we will instead categorize our 54 | // responses into the upper group if the respondent's overall ability measure minus the measure's standard error 55 | // is greater than the question's level. Likewise, responses will be categorized into the lower group if the respondent's 56 | // ability measure plus the measure's standard error is less than the question's level. 57 | // Responses where the user's ability measure and error-range include the question level will be ignored. 58 | 59 | $level = $analyser->get_question_level_in_logits(); 60 | $uppergroupsize = 0; 61 | $uppergroupcorrect = 0; 62 | $lowergroupsize = 0; 63 | $lowergroupcorrect = 0; 64 | 65 | foreach ($analyser->get_results() as $result) { 66 | if ($result->score->measured_ability_in_logits() - $result->score->standard_error_in_logits() > $level) { 67 | // Upper group. 68 | $uppergroupsize++; 69 | if ($result->correct) { 70 | $uppergroupcorrect++; 71 | } 72 | } else if ($result->score->measured_ability_in_logits() + $result->score->standard_error_in_logits() < $level) { 73 | // Lower Group. 74 | $lowergroupsize++; 75 | if ($result->correct) { 76 | $lowergroupcorrect++; 77 | } 78 | } 79 | } 80 | 81 | if ($uppergroupsize > 0 && $lowergroupsize > 0) { 82 | // We need at least one result in the upper and lower groups. 83 | $upperproportion = $uppergroupcorrect / $uppergroupsize; 84 | $lowerproportion = $lowergroupcorrect / $lowergroupsize; 85 | $discrimination = $upperproportion - $lowerproportion; 86 | return new adaptivequiz_discrimination_statistic_result ($discrimination); 87 | } else { 88 | // If we don't have any responses in the upper or lower group, then we don't have a meaningful result. 89 | return new adaptivequiz_discrimination_statistic_result (null); 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Questions-statistic-result 96 | * 97 | * This interface defines the methods required for pluggable statistic-results that may be added to the question analysis. 98 | * 99 | * This module was created as a collaborative effort between Middlebury College 100 | * and Remote Learner. 101 | * 102 | * @package mod_adaptivequiz 103 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 104 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 105 | */ 106 | class adaptivequiz_discrimination_statistic_result implements adaptivequiz_question_statistic_result { 107 | 108 | /** @var float $discrimination */ 109 | protected $discrimination = null; 110 | 111 | /** 112 | * Constructor 113 | * 114 | * @param float $discrimination 115 | * @return void 116 | */ 117 | public function __construct ($discrimination) { 118 | $this->discrimination = $discrimination; 119 | } 120 | 121 | /** 122 | * A sortable version of the result. 123 | * 124 | * @return mixed string or numeric 125 | */ 126 | public function sortable () { 127 | if (is_null($this->discrimination)) { 128 | return -2; 129 | } else { 130 | return $this->discrimination; 131 | } 132 | } 133 | 134 | /** 135 | * A printable version of the result. 136 | * 137 | * @param numeric $result 138 | * @return mixed string or numeric 139 | */ 140 | public function printable () { 141 | if (is_null($this->discrimination)) { 142 | return 'n/a'; 143 | } else { 144 | return round($this->discrimination, 3); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /questionanalysis/lib/statistics/percent_correct_statistic.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | require_once(dirname(__FILE__).'/question_statistic.interface.php'); 18 | 19 | /** 20 | * Questions-statistic interface 21 | * 22 | * This interface defines the methods required for pluggable statistics that may be added to the question analysis. 23 | * 24 | * This module was created as a collaborative effort between Middlebury College 25 | * and Remote Learner. 26 | * 27 | * @package mod_adaptivequiz 28 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | class adaptivequiz_percent_correct_statistic implements adaptivequiz_question_statistic { 32 | 33 | /** 34 | * Answer a display-name for this statistic. 35 | * 36 | * @return string 37 | */ 38 | public function get_display_name () { 39 | return get_string('percent_correct_display_name', 'adaptivequiz'); 40 | } 41 | 42 | /** 43 | * Calculate this statistic for a question's results 44 | * 45 | * @param adaptivequiz_question_analyser $analyser 46 | * @return adaptivequiz_question_statistic_result 47 | */ 48 | public function calculate (adaptivequiz_question_analyser $analyser) { 49 | $correct = 0; 50 | $total = 0; 51 | foreach ($analyser->get_results() as $result) { 52 | $total++; 53 | if ($result->correct) { 54 | $correct++; 55 | } 56 | } 57 | if ($total) { 58 | return new adaptivequiz_percent_correct_statistic_result ($correct / $total); 59 | } else { 60 | return new adaptivequiz_percent_correct_statistic_result (0); 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Questions-statistic-result interface 67 | * 68 | * This interface defines the methods required for pluggable statistic-results that may be added to the question analysis. 69 | * 70 | * This module was created as a collaborative effort between Middlebury College 71 | * and Remote Learner. 72 | * 73 | * @package mod_adaptivequiz 74 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 75 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 76 | */ 77 | class adaptivequiz_percent_correct_statistic_result implements adaptivequiz_question_statistic_result { 78 | 79 | /** @var float $fraction */ 80 | protected $fraction = null; 81 | 82 | /** 83 | * Constructor 84 | * 85 | * @param float $fraction 86 | * @return void 87 | */ 88 | public function __construct ($fraction) { 89 | $this->fraction = $fraction; 90 | } 91 | 92 | /** 93 | * A sortable version of the result. 94 | * 95 | * @return mixed string or numeric 96 | */ 97 | public function sortable () { 98 | return $this->fraction; 99 | } 100 | 101 | /** 102 | * A printable version of the result. 103 | * 104 | * @param numeric $result 105 | * @return mixed string or numeric 106 | */ 107 | public function printable () { 108 | return round($this->fraction * 100).'%'; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /questionanalysis/lib/statistics/question_statistic.interface.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Questions-statistic interface 19 | * 20 | * This interface defines the methods required for pluggable statistics that may be added to the question analysis. 21 | * 22 | * This module was created as a collaborative effort between Middlebury College 23 | * and Remote Learner. 24 | * 25 | * @package mod_adaptivequiz 26 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 27 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 28 | */ 29 | interface adaptivequiz_question_statistic { 30 | 31 | /** 32 | * Answer a display-name for this statistic. 33 | * 34 | * @return string 35 | */ 36 | public function get_display_name (); 37 | 38 | /** 39 | * Calculate this statistic for a question's results 40 | * 41 | * @param adaptivequiz_question_analyser $analyser 42 | * @return adaptivequiz_question_statistic_result 43 | */ 44 | public function calculate (adaptivequiz_question_analyser $analyser); 45 | } 46 | 47 | /** 48 | * Questions-statistic-result interface 49 | * 50 | * This interface defines the methods required for pluggable statistic-results that may be added to the question analysis. 51 | * 52 | * This module was created as a collaborative effort between Middlebury College 53 | * and Remote Learner. 54 | * 55 | * @package mod_adaptivequiz 56 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 57 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 58 | */ 59 | interface adaptivequiz_question_statistic_result { 60 | 61 | /** 62 | * A sortable version of the result. 63 | * 64 | * @return mixed string or numeric 65 | */ 66 | public function sortable (); 67 | 68 | /** 69 | * A printable version of the result. 70 | * 71 | * @param numeric $result 72 | * @return mixed string or numeric 73 | */ 74 | public function printable (); 75 | } 76 | -------------------------------------------------------------------------------- /questionanalysis/lib/statistics/times_used_statistic.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | require_once(dirname(__FILE__).'/question_statistic.interface.php'); 18 | 19 | /** 20 | * Questions-statistic interface 21 | * 22 | * This interface defines the methods required for pluggable statistics that may be added to the question analysis. 23 | * 24 | * This module was created as a collaborative effort between Middlebury College 25 | * and Remote Learner. 26 | * 27 | * @package mod_adaptivequiz 28 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | class adaptivequiz_times_used_statistic implements adaptivequiz_question_statistic { 32 | 33 | /** 34 | * Answer a display-name for this statistic. 35 | * 36 | * @return string 37 | */ 38 | public function get_display_name () { 39 | return get_string('times_used_display_name', 'adaptivequiz'); 40 | } 41 | 42 | /** 43 | * Calculate this statistic for a question's results 44 | * 45 | * @param adaptivequiz_question_analyser $analyser 46 | * @return adaptivequiz_question_statistic_result 47 | */ 48 | public function calculate (adaptivequiz_question_analyser $analyser) { 49 | return new adaptivequiz_times_used_statistic_result (count($analyser->get_results())); 50 | } 51 | } 52 | 53 | /** 54 | * Questions-statistic-result interface 55 | * 56 | * This interface defines the methods required for pluggable statistic-results that may be added to the question analysis. 57 | * 58 | * This module was created as a collaborative effort between Middlebury College 59 | * and Remote Learner. 60 | * 61 | * @package mod_adaptivequiz 62 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 63 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 64 | */ 65 | class adaptivequiz_times_used_statistic_result implements adaptivequiz_question_statistic_result { 66 | 67 | /** @var int $count */ 68 | protected $count = null; 69 | 70 | /** 71 | * Constructor 72 | * 73 | * @param int $count 74 | * @return void 75 | */ 76 | public function __construct ($count) { 77 | $this->count = $count; 78 | } 79 | 80 | /** 81 | * A sortable version of the result. 82 | * 83 | * @return mixed string or numeric 84 | */ 85 | public function sortable () { 86 | return $this->count; 87 | } 88 | 89 | /** 90 | * A printable version of the result. 91 | * 92 | * @param numeric $result 93 | * @return mixed string or numeric 94 | */ 95 | public function printable () { 96 | return $this->count; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /questionanalysis/overview.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz view report script 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../../config.php'); 29 | require_once($CFG->dirroot.'/lib/grouplib.php'); 30 | require_once(dirname(__FILE__).'/../locallib.php'); 31 | require_once(dirname(__FILE__).'/lib/quiz_analyser.class.php'); 32 | require_once(dirname(__FILE__).'/renderer.php'); 33 | 34 | require_once(dirname(__FILE__).'/lib/statistics/times_used_statistic.class.php'); 35 | require_once(dirname(__FILE__).'/lib/statistics/percent_correct_statistic.class.php'); 36 | require_once(dirname(__FILE__).'/lib/statistics/discrimination_statistic.class.php'); 37 | 38 | $id = required_param('cmid', PARAM_INT); 39 | $sortdir = optional_param('sortdir', 'DESC', PARAM_ALPHA); 40 | $sort = optional_param('sort', 'times_used', PARAM_ALPHANUMEXT); 41 | $page = optional_param('page', 0, PARAM_INT); 42 | 43 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 44 | print_error('invalidcoursemodule'); 45 | } 46 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 47 | print_error("coursemisconf"); 48 | } 49 | 50 | require_login($course, true, $cm); 51 | $context = context_module::instance($cm->id); 52 | 53 | require_capability('mod/adaptivequiz:viewreport', $context); 54 | 55 | $adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*'); 56 | $PAGE->set_url('/mod/adaptivequiz/questionanalysis/overview.php', array('cmid' => $cm->id)); 57 | $PAGE->set_title(format_string($adaptivequiz->name)); 58 | $PAGE->set_heading(format_string($course->fullname)); 59 | $PAGE->set_context($context); 60 | $output = $PAGE->get_renderer('mod_adaptivequiz', 'questions'); 61 | 62 | 63 | $quizanalyzer = new adaptivequiz_quiz_analyser(); 64 | $quizanalyzer->load_attempts($cm->instance); 65 | $quizanalyzer->add_statistic('times_used', new adaptivequiz_times_used_statistic()); 66 | $quizanalyzer->add_statistic('percent_correct', new adaptivequiz_percent_correct_statistic()); 67 | $quizanalyzer->add_statistic('discrimination', new adaptivequiz_discrimination_statistic()); 68 | 69 | $headers = $quizanalyzer->get_header(); 70 | $records = $quizanalyzer->get_records($sort, $sortdir); 71 | $recordscount = count($records); 72 | $records = array_slice($records, $page * ADAPTIVEQUIZ_REC_PER_PAGE, ADAPTIVEQUIZ_REC_PER_PAGE); 73 | 74 | // Merge the question id and names into links. 75 | unset($headers['id']); 76 | foreach ($records as &$record) { 77 | $id = array_shift($record); 78 | $url = new moodle_url('/mod/adaptivequiz/questionanalysis/single.php', 79 | array('cmid' => $cm->id, 'qid' => $id, 'sort' => $sort, 'sortdir' => $sortdir, 'page' => $page)); 80 | $record[0] = html_writer::link($url, $record[0]); 81 | } 82 | 83 | 84 | /* print header information */ 85 | $header = $output->print_header(); 86 | $title = $output->heading(get_string('questions_report', 'adaptivequiz')); 87 | /* Output attempts table */ 88 | $reporttable = $output->get_report_table($headers, $records, $cm, '/mod/adaptivequiz/questionanalysis/overview.php', $sort, 89 | $sortdir); 90 | /* Output paging bar */ 91 | $pagingbar = $output->print_paging_bar($recordscount, $page, ADAPTIVEQUIZ_REC_PER_PAGE, $cm, 92 | '/mod/adaptivequiz/questionanalysis/overview.php', $sort, $sortdir); 93 | /* Output footer information */ 94 | $footer = $output->print_footer(); 95 | 96 | echo $header; 97 | echo $pagingbar; 98 | echo $title; 99 | echo $reporttable; 100 | echo $pagingbar; 101 | echo $footer; 102 | -------------------------------------------------------------------------------- /questionanalysis/renderer.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz renderer class 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once($CFG->dirroot.'/mod/adaptivequiz/requiredpassword.class.php'); 29 | 30 | class mod_adaptivequiz_questions_renderer extends plugin_renderer_base { 31 | /** @var string $sortdir the sorting direction being used */ 32 | protected $sortdir = ''; 33 | /** @var moodle_url $sorturl the current base url used for keeping the table sorted */ 34 | protected $sorturl = ''; 35 | /** @var int $groupid variable used to reference the groupid that is currently being used to filter by */ 36 | public $groupid = 0; 37 | /** @var array options that should be used for opening the secure popup. */ 38 | protected static $popupoptions = array( 39 | 'left' => 0, 40 | 'top' => 0, 41 | 'fullscreen' => true, 42 | 'scrollbars' => false, 43 | 'resizeable' => false, 44 | 'directories' => false, 45 | 'toolbar' => false, 46 | 'titlebar' => false, 47 | 'location' => false, 48 | 'status' => false, 49 | 'menubar' => false 50 | ); 51 | 52 | /** 53 | * This function returns page header information to be printed to the page 54 | * @return string HTML markup for header inforation 55 | */ 56 | public function print_header() { 57 | return $this->header(); 58 | } 59 | 60 | /** 61 | * This function returns page footer information to be printed to the page 62 | * @return string HTML markup for footer inforation 63 | */ 64 | public function print_footer() { 65 | return $this->footer(); 66 | } 67 | 68 | /** 69 | * This function generates the HTML required to display the initial reports table 70 | * @param array $records attempt records from adaptivequiz_attempt table 71 | * @param stdClass $cm course module object set to the instance of the activity 72 | * @param string $sort the column the the table is to be sorted by 73 | * @param string $sortdir the direction of the sort 74 | * @return string HTML markup 75 | */ 76 | public function get_report_table($headers, $records, $cm, $baseurl, $sort, $sortdir) { 77 | $table = new html_table(); 78 | $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter'; 79 | $table->head = $this->format_report_table_headers($headers, $cm, $baseurl, $sort, $sortdir); 80 | $table->align = array('center', 'center', 'center'); 81 | $table->size = array('', '', ''); 82 | 83 | $table->data = $records; 84 | return html_writer::table($table); 85 | } 86 | 87 | /** 88 | * This function creates the table header links that will be used to allow instructor to sort the data 89 | * @param stdClass $cm a course module object set to the instance of the activity 90 | * @param string $sort the column the the table is to be sorted by 91 | * @param string $sortdir the direction of the sort 92 | * @return array an array of column headers (firstname / lastname, number of attempts, standard error) 93 | */ 94 | public function format_report_table_headers($headers, $cm, $baseurl, $sort, $sortdir) { 95 | global $OUTPUT; 96 | 97 | /* Create header links */ 98 | $contents = array(); 99 | foreach ($headers as $key => $name) { 100 | if ($sort == $key) { 101 | $seperator = ' '; 102 | if ($sortdir == 'DESC') { 103 | $sortdir = 'ASC'; 104 | $imageparam = array('src' => $OUTPUT->image_url('t/up'), 'alt' => ''); 105 | $icon = html_writer::empty_tag('img', $imageparam); 106 | } else { 107 | $sortdir = 'DESC'; 108 | $imageparam = array('src' => $OUTPUT->image_url('t/down'), 'alt' => ''); 109 | $icon = html_writer::empty_tag('img', $imageparam); 110 | } 111 | } else { 112 | $sortdir = 'ASC'; 113 | $seperator = ''; 114 | $icon = ''; 115 | } 116 | 117 | $url = new moodle_url($baseurl, array('cmid' => $cm->id, 'sort' => $key, 'sortdir' => $sortdir)); 118 | 119 | $contents[] = html_writer::link($url, $name.$seperator.$icon); 120 | } 121 | return $contents; 122 | } 123 | 124 | /** 125 | * This function prints paging information 126 | * @param int $totalrecords the total number of records returned 127 | * @param int $page the current page the user is on 128 | * @param int $perpage the number of records displayed on one page 129 | * @return string HTML markup 130 | */ 131 | public function print_paging_bar($totalrecords, $page, $perpage, $cm, $baseurl, $sort, $sortdir) { 132 | global $OUTPUT; 133 | 134 | $url = new moodle_url($baseurl, array('cmid' => $cm->id, 'sort' => $sort, 'sortdir' => $sortdir)); 135 | 136 | $output = ''; 137 | $output .= $OUTPUT->paging_bar($totalrecords, $page, $perpage, $url); 138 | return $output; 139 | } 140 | 141 | /** 142 | * This function generates the HTML required to display the single-question report 143 | * @param array $headers The labels for the report 144 | * @param array $record An attempt record 145 | * @return string HTML markup 146 | */ 147 | public function get_single_question_report($headers, $record) { 148 | $table = new html_table(); 149 | $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter'; 150 | $table->head = array(get_string('statistic', 'adaptivequiz'), get_string('value', 'adaptivequiz')); 151 | $table->align = array('left', 'left'); 152 | $table->size = array('200px', ''); 153 | $table->width = '100%'; 154 | 155 | while ($name = array_shift($headers)) { 156 | $value = array_shift($record); 157 | $table->data[] = array($name, $value); 158 | } 159 | 160 | return html_writer::table($table); 161 | } 162 | 163 | /** 164 | * Generate an HTML view of a single question. 165 | * 166 | * @param $analyzer 167 | * @return string HTML markup 168 | */ 169 | public function get_question_details (adaptivequiz_question_analyser $analyzer, $context) { 170 | // Setup display options. 171 | $options = new question_display_options(); 172 | $options->readonly = true; 173 | $options->flags = question_display_options::HIDDEN; 174 | $options->marks = question_display_options::MAX_ONLY; 175 | $options->rightanswer = question_display_options::VISIBLE; 176 | $options->correctness = question_display_options::VISIBLE; 177 | $options->numpartscorrect = question_display_options::VISIBLE; 178 | 179 | // Init question usage and set default behaviour of usage. 180 | $quba = question_engine::make_questions_usage_by_activity('mod_adaptivequiz', $context); 181 | $quba->set_preferred_behaviour('deferredfeedback'); 182 | $quba->add_question($analyzer->get_question_definition()); 183 | $quba->start_question(1); 184 | $quba->process_action(1, $quba->get_correct_response(1)); 185 | 186 | return $quba->render_question(1, $options); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /questionanalysis/single.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz view report script 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 Middlebury College {@link http://www.middlebury.edu/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../../config.php'); 29 | require_once($CFG->dirroot.'/lib/grouplib.php'); 30 | require_once(dirname(__FILE__).'/../locallib.php'); 31 | require_once(dirname(__FILE__).'/lib/quiz_analyser.class.php'); 32 | require_once(dirname(__FILE__).'/renderer.php'); 33 | 34 | require_once(dirname(__FILE__).'/lib/statistics/times_used_statistic.class.php'); 35 | require_once(dirname(__FILE__).'/lib/statistics/percent_correct_statistic.class.php'); 36 | require_once(dirname(__FILE__).'/lib/statistics/discrimination_statistic.class.php'); 37 | require_once(dirname(__FILE__).'/lib/statistics/answers_statistic.class.php'); 38 | 39 | $id = required_param('cmid', PARAM_INT); 40 | $qid = required_param('qid', PARAM_INT); 41 | $sortdir = optional_param('sortdir', 'DESC', PARAM_ALPHA); 42 | $sort = optional_param('sort', 'times_used', PARAM_ALPHANUMEXT); 43 | $page = optional_param('page', 0, PARAM_INT); 44 | 45 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 46 | print_error('invalidcoursemodule'); 47 | } 48 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 49 | print_error("coursemisconf"); 50 | } 51 | 52 | 53 | require_login($course, true, $cm); 54 | $context = context_module::instance($cm->id); 55 | 56 | require_capability('mod/adaptivequiz:viewreport', $context); 57 | 58 | $adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*'); 59 | 60 | $quizanalyzer = new adaptivequiz_quiz_analyser(); 61 | $quizanalyzer->load_attempts($cm->instance); 62 | $questionanalyzer = $quizanalyzer->get_question_analyzer($qid); 63 | $definition = $questionanalyzer->get_question_definition(); 64 | 65 | $PAGE->set_url('/mod/adaptivequiz/questionanalysis/single.php', array('cmid' => $cm->id, 'qid' => $qid)); 66 | $PAGE->set_title(format_string($definition->name)); 67 | $PAGE->set_heading(format_string($course->fullname)); 68 | $PAGE->set_context($context); 69 | $output = $PAGE->get_renderer('mod_adaptivequiz', 'questions'); 70 | 71 | 72 | $quizanalyzer->add_statistic('times_used', new adaptivequiz_times_used_statistic()); 73 | $quizanalyzer->add_statistic('percent_correct', new adaptivequiz_percent_correct_statistic()); 74 | $quizanalyzer->add_statistic('discrimination', new adaptivequiz_discrimination_statistic()); 75 | $quizanalyzer->add_statistic('answers', new adaptivequiz_answers_statistic()); 76 | 77 | $headers = $quizanalyzer->get_header(); 78 | $record = $quizanalyzer->get_record($qid); 79 | 80 | // Get rid of the question id and name columns. 81 | unset($headers['id'], $headers['name']); 82 | array_shift($record); 83 | array_shift($record); 84 | 85 | /* print header information */ 86 | $header = $output->print_header(); 87 | $title = $output->heading(get_string('question_report', 'adaptivequiz')); 88 | /* return link */ 89 | $url = new moodle_url('/mod/adaptivequiz/questionanalysis/overview.php', 90 | array('cmid' => $cm->id, 'sort' => $sort, 'sortdir' => $sortdir, 'page' => $page)); 91 | $returnlink = html_writer::link($url, get_string('back_to_all_questions', 'adaptivequiz')); 92 | 93 | /* Output attempts table */ 94 | $details = $output->get_question_details($questionanalyzer, $context); 95 | $reporttable = $output->get_single_question_report($headers, $record, $cm, '/mod/adaptivequiz/questionanalysis/overview.php', 96 | $sort, $sortdir); 97 | /* Output footer information */ 98 | $footer = $output->print_footer(); 99 | 100 | echo $header; 101 | echo $returnlink; 102 | echo $title; 103 | echo $details; 104 | echo $reporttable; 105 | echo $footer; 106 | -------------------------------------------------------------------------------- /requiredpassword.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptivequiz required password form 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | require_once($CFG->libdir.'/formslib.php'); 30 | 31 | class mod_adaptivequiz_requiredpassword extends moodleform { 32 | /** @var string $passwordmessage a string containing text for a failed password attempt */ 33 | public $passwordmessage = ''; 34 | /** 35 | * This method is refactored code from the quiz's password rule add_preflight_check_form_fields() method. 36 | * It prints a form for the user to enter a password 37 | */ 38 | protected function definition() { 39 | $mform = $this->_form; 40 | 41 | foreach ($this->_customdata['hidden'] as $name => $value) { 42 | if ($name === 'sesskey') { 43 | continue; 44 | } 45 | if ($name === 'cmid' || $name === 'uniqueid') { 46 | $mform->setType($name, PARAM_INT); 47 | } 48 | $mform->addElement('hidden', $name, $value); 49 | } 50 | 51 | $mform->addElement('header', 'passwordheader', get_string('password')); 52 | $mform->addElement('static', 'passwordmessage', '', get_string('requirepasswordmessage', 'adaptivequiz')); 53 | 54 | $attr = array('style' => 'color:red;', 'class' => 'wrongpassword'); 55 | $html = html_writer::start_tag('div', $attr); 56 | $mform->addElement('html', $html); 57 | $mform->addElement('static', 'message'); 58 | $html = html_writer::end_tag('div'); 59 | $mform->addElement('html', $html); 60 | 61 | // Don't use the 'proper' field name of 'password' since that get's 62 | // Firefox's password auto-complete over-excited. 63 | $mform->addElement('password', 'quizpassword', get_string('enterrequiredpassword', 'adaptivequiz')); 64 | 65 | $this->add_action_buttons(true, get_string('continue')); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /reviewattempt.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz view attempted questions 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../config.php'); 29 | require_once($CFG->dirroot.'/tag/lib.php'); 30 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 31 | 32 | $id = required_param('cmid', PARAM_INT); 33 | $uniqueid = required_param('uniqueid', PARAM_INT); 34 | $userid = required_param('userid', PARAM_INT); 35 | $page = optional_param('page', 0, PARAM_INT); 36 | 37 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 38 | print_error('invalidcoursemodule'); 39 | } 40 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 41 | print_error("coursemisconf"); 42 | } 43 | 44 | require_login($course, true, $cm); 45 | $context = context_module::instance($cm->id); 46 | 47 | require_capability('mod/adaptivequiz:viewreport', $context); 48 | 49 | $param = array('uniqueid' => $uniqueid, 'userid' => $userid, 'activityid' => $cm->instance); 50 | $sql = 'SELECT a.name, a.highestlevel, a.lowestlevel, aa.timecreated, aa.timemodified, aa.attemptstate, aa.attemptstopcriteria, 51 | aa.questionsattempted, aa.difficultysum, aa.standarderror, aa.measure 52 | FROM {adaptivequiz} a 53 | JOIN {adaptivequiz_attempt} aa ON a.id = aa.instance 54 | WHERE aa.uniqueid = :uniqueid 55 | AND aa.userid = :userid 56 | AND a.id = :activityid 57 | ORDER BY a.name ASC'; 58 | $adaptivequiz = $DB->get_record_sql($sql, $param); 59 | 60 | $PAGE->set_url('/mod/adaptivequiz/reviewattempt.php', array('cmid' => $cm->id)); 61 | $PAGE->set_title(format_string($adaptivequiz->name)); 62 | $PAGE->set_heading(format_string($course->fullname)); 63 | $PAGE->set_context($context); 64 | 65 | $output = $PAGE->get_renderer('mod_adaptivequiz'); 66 | 67 | $PAGE->requires->js_init_call('M.mod_adaptivequiz.init_reviewattempt', null, false, $output->adaptivequiz_get_js_module()); 68 | 69 | 70 | /* print header information */ 71 | $header = $output->print_header(); 72 | /* Output footer information */ 73 | $footer = $output->print_footer(); 74 | /* Load question usage by activity object */ 75 | $quba = question_engine::load_questions_usage_by_activity($uniqueid); 76 | /* render pager links */ 77 | $pager = $output->print_questions_for_review_pager($quba, $page, $cm->id, $userid); 78 | /* Render a button on the page */ 79 | $url = new moodle_url('/mod/adaptivequiz/viewattemptreport.php', array('cmid' => $cm->id, 'userid' => $userid)); 80 | $txt = get_string('backtoviewattemptreport', 'adaptivequiz'); 81 | $button = $output->print_form_and_button($url, $txt); 82 | 83 | $user = $DB->get_record('user', array('id' => $userid)); 84 | if (!$user) { 85 | $user = new stdClass(); 86 | $user->firstname = get_string('unknownuser', 'adaptivequiz'); 87 | $user->lastname = '#'.$userid; 88 | } 89 | 90 | echo $header; 91 | 92 | echo html_writer::tag('h2', get_string('attempt_summary', 'adaptivequiz')); 93 | echo $output->get_attempt_summary_listing($adaptivequiz, $user); 94 | 95 | $graphurl = new moodle_url('/mod/adaptivequiz/attemptgraph.php', 96 | array('uniqueid' => $uniqueid, 'cmid' => $cm->id, 'userid' => $userid)); 97 | $params = array('src' => $graphurl, 'class' => 'adaptivequiz-attemptgraph'); 98 | echo html_writer::empty_tag('img', $params); 99 | 100 | echo ' '; 101 | 102 | $graphurl = new moodle_url('/mod/adaptivequiz/answerdistributiongraph.php', 103 | array('uniqueid' => $uniqueid, 'cmid' => $cm->id, 'userid' => $userid)); 104 | $params = array('src' => $graphurl, 'class' => 'adaptivequiz-answerdistributiongraph'); 105 | echo html_writer::empty_tag('img', $params); 106 | 107 | echo html_writer::start_tag('a', array('href' => '#', 'id' => 'adpq_scoring_table_link')); 108 | echo html_writer::start_tag('h2'); 109 | echo html_writer::tag('span', '▼', array('id' => 'adpq_scoring_table_link_icon')); 110 | echo ' '.get_string('scoring_table', 'adaptivequiz'); 111 | echo html_writer::end_tag('h3'); 112 | echo html_writer::end_tag('a'); 113 | echo html_writer::start_tag('div', array('id' => 'adpq_scoring_table')); 114 | echo $output->get_attempt_scoring_table($adaptivequiz, $quba); 115 | echo $output->get_attempt_distribution_table($adaptivequiz, $quba); 116 | echo html_writer::tag('div', '', array('class' => 'clearfix')); 117 | echo html_writer::end_tag('div'); 118 | 119 | echo html_writer::tag('h2', get_string('attempt_questiondetails', 'adaptivequiz')); 120 | echo $pager; 121 | echo $output->print_questions_for_review($quba, $page, $user, $adaptivequiz->timemodified); 122 | echo $button; 123 | echo html_writer::empty_tag('br'); 124 | echo $pager; 125 | echo $footer; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .adaptivequiz-summarylist { 2 | float: left; 3 | } 4 | .adaptivequiz-summarylist dt { 5 | font-weight: bold; 6 | } 7 | .adaptivequiz-attemptgraph { 8 | clear: both; 9 | } 10 | 11 | #adpq_scoring_table table { 12 | float: left; 13 | margin-right: 20px; 14 | } 15 | 16 | #adpq_scoring_table { 17 | clear: both; 18 | } 19 | 20 | .adpq_download { 21 | text-align: center; 22 | margin: 10px; 23 | } 24 | 25 | /* Styles related to the question analysis reporting */ 26 | table.adpq_answers_table thead th.section { 27 | text-align: left; 28 | padding-top: 30px; 29 | } 30 | table.adpq_answers_table thead:first-child th.section { 31 | padding-top: inherit; 32 | } 33 | .adpq_correct { 34 | color: green; 35 | } 36 | .adpq_incorrect { 37 | color: red; 38 | } 39 | .adpq_highlevel .adpq_incorrect { 40 | font-weight: bold; 41 | } 42 | .adpq_lowlevel .adpq_correct { 43 | font-weight: bold; 44 | } 45 | -------------------------------------------------------------------------------- /tests/dummycatalgo.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * A dummy class that extands catalgo class. The purpose of this class is to expose the protected method of return_current_diff_level() 19 | * 20 | * @package mod_adaptivequiz 21 | * @category phpunit 22 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | class mod_adaptivequiz_mock_catalgo extends catalgo { 27 | 28 | /** 29 | * This function calculates the currently difficulty level of the attempt by calling the parent class' method 30 | * @param question_usage_by_activity $quba a question usage by activity set to an attempt id 31 | * @param int $level the starting level of difficulty for the attempt 32 | * @param stdClass $attemptobj an object with the following properties: lowestlevel and highestlevel 33 | * @return int the current level of difficulty 34 | */ 35 | public function return_current_diff_level($quba, $level, $attemptobj) { 36 | return parent::return_current_diff_level($quba, $level, $attemptobj); 37 | } 38 | } -------------------------------------------------------------------------------- /tests/dummyfetchquestion.class.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * A dummy class that extands fetchquestion class. The purpose of this class is to expose the protected method of retrieve_question_categories() 19 | * 20 | * @package mod_adaptivequiz 21 | * @category phpunit 22 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | class mod_adaptivequiz_mock_fetchquestion extends fetchquestion { 26 | /** 27 | * Constructor 28 | */ 29 | public function __construct($adaptivequiz, $level = 1, $minimumlevel, $maximumlevel, $tags = array()) { 30 | parent::__construct($adaptivequiz, $level, $minimumlevel, $maximumlevel, $tags); 31 | } 32 | 33 | /** 34 | * This function retrieves the question categories associated with the activity instance 35 | * @return array whose keys and values are question categoriy ids 36 | */ 37 | public function return_retrieve_question_categories() { 38 | return parent::retrieve_question_categories(); 39 | } 40 | } -------------------------------------------------------------------------------- /tests/fixtures/mod_adaptivequiz.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | id 6 | instance 7 | questioncategory 8 | 9 | 1 10 | 12 11 | 2 12 | 13 | 14 | 2 15 | 12 16 | 4 17 | 18 | 19 | 3 20 | 12 21 | 6 22 | 23 | 24 | 4 25 | 12 26 | 8 27 | 28 | 29 | 5 30 | 12 31 | 10 32 | 33 | 34 | 6 35 | 12 36 | 12 37 | 38 | 39 | 7 40 | 13 41 | 1 42 | 43 |
44 | 45 | id 46 | course 47 | name 48 | intro 49 | attemptfeedback 50 | highestlevel 51 | lowestlevel 52 | minimumquestions 53 | maximumquestions 54 | standarderror 55 | startinglevel 56 | 57 | 1 58 | 1 59 | Test re-using quba 60 | intro asdf 61 | attempt feedback 62 | 100 63 | 1 64 | 15 65 | 20 66 | 1.0 67 | 50 68 | 69 | 70 | 13 71 | 1 72 | Test min_attempts_reached 73 | intro asdf 74 | attempt feedback 75 | 100 76 | 1 77 | 15 78 | 20 79 | 1.0 80 | 50 81 | 82 | 83 | 330 84 | 1 85 | Test re-using quba 86 | intro asdf 87 | attempt feedback 88 | 100 89 | 1 90 | 15 91 | 20 92 | 1.0 93 | 50 94 | 95 |
96 | 97 | id 98 | instance 99 | userid 100 | uniqueid 101 | attemptstate 102 | questionsattempted 103 | standarderror 104 | difficultysum 105 | 106 | 1 107 | 1 108 | 2 109 | 3 110 | inprogress 111 | 0 112 | 1 113 | 1 114 | 115 | 116 | 2 117 | 13 118 | 3 119 | 3 120 | complete 121 | 0 122 | 1 123 | 1 124 | 125 | 126 | 3 127 | 13 128 | 4 129 | 4 130 | complete 131 | 16 132 | 1 133 | 1 134 | 135 | 136 | 5 137 | 330 138 | 2 139 | 330 140 | inprogress 141 | 0 142 | 1 143 | 1 144 | 145 |
146 | 147 | id 148 | contextid 149 | component 150 | preferredbehaviour 151 | 152 | 330 153 | 330 154 | mod_adaptivequiz 155 | immediatefeedback 156 | 157 |
158 | 159 | id 160 | contextlevel 161 | instanceid 162 | path 163 | depth 164 | 165 | 16 166 | 50 167 | 2 168 | /1/3/16 169 | 3 170 | 171 | 172 | 330 173 | 70 174 | 2 175 | /1/3/16/330 176 | 4 177 | 178 |
179 | 180 | id 181 | questionusageid 182 | slot 183 | questionid 184 | maxmark 185 | minfraction 186 | rightanswer 187 | timemodified 188 | 189 | 1 190 | 330 191 | 1 192 | 1 193 | 1.0 194 | 0.0 195 | true 196 | 0 197 | 198 | 199 | 2 200 | 330 201 | 2 202 | 2 203 | 1.0 204 | 0.0 205 | answer 1 (correct) 206 | 0 207 | 208 |
209 | 210 | id 211 | questionattemptid 212 | sequencenumber 213 | state 214 | timecreated 215 | userid 216 | 217 | 1 218 | 1 219 | 0 220 | todo 221 | 0 222 | 2 223 | 224 | 225 | 2 226 | 1 227 | 1 228 | gradedwrong 229 | 1 230 | 2 231 | 232 | 233 | 3 234 | 2 235 | 0 236 | complete 237 | 1 238 | 2 239 | 240 |
241 |
242 | -------------------------------------------------------------------------------- /tests/fixtures/mod_adaptivequiz_catalgo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | id 5 | instance 6 | userid 7 | uniqueid 8 | attemptstate 9 | questionsattempted 10 | standarderror 11 | difficultysum 12 | measure 13 | 14 | 1 15 | 220 16 | 2 17 | 1110 18 | inprogress 19 | 0 20 | 1.2 21 | 99 22 | 2.222 23 | 24 |
25 | 26 | id 27 | course 28 | name 29 | intro 30 | attemptfeedback 31 | highestlevel 32 | lowestlevel 33 | minimumquestions 34 | maximumquestions 35 | standarderror 36 | startinglevel 37 | 38 | 220 39 | 1 40 | Test for retrieve_standard_error() 41 | intro asdf 42 | attempt feedback 43 | 100 44 | 1 45 | 15 46 | 20 47 | 9.9 48 | 50 49 | 50 |
51 |
-------------------------------------------------------------------------------- /tests/generator/lib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive generator file 19 | * 20 | * @package mod_adaptivequiz 21 | * @category phpunit 22 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | class mod_adaptivequiz_generator extends testing_module_generator { 26 | /** 27 | * Create new quiz module instance. 28 | * @param array|stdClass $record 29 | * @param array $options (mostly course_module properties) 30 | * @return stdClass activity record with extra cmid field 31 | */ 32 | public function create_instance($record = null, array $options = null) { 33 | global $CFG; 34 | require_once("$CFG->dirroot/mod/adaptivequiz/locallib.php"); 35 | 36 | $this->instancecount++; 37 | $i = $this->instancecount; 38 | 39 | $record = (object)(array)$record; 40 | $options = (array)$options; 41 | 42 | if (empty($record->course)) { 43 | throw new coding_exception('module generator requires $record->course'); 44 | } 45 | if (isset($options['idnumber'])) { 46 | $record->cmidnumber = $options['idnumber']; 47 | } else { 48 | $record->cmidnumber = ''; 49 | } 50 | 51 | $defaultadaptivequizsettings = array( 52 | 'name' => get_string('pluginname', 'adaptivequiz').' '.$i, 53 | 'intro' => 'Test adaptivequiz '.$i, 54 | 'introformat' => FORMAT_MOODLE, 55 | 'attempts' => 0, 56 | 'password' => '', 57 | 'attemptfeedback' => 'Attempt Feedback', 58 | 'attemptfeedbackformat' => FORMAT_MOODLE, 59 | 'attemptonlast' => 0, 60 | 'highestlevel' => 111, 61 | 'lowestlevel' => 1, 62 | 'minimumquestions' => 1, 63 | 'maximumquestions' => 111, 64 | 'standarderror' => 1.1, 65 | 'startinglevel' => 11, 66 | 'timecreated' => time(), 67 | 'timemodified' => time(), 68 | 'questionpool' => array(11, 22, 33, 44), 69 | ); 70 | 71 | foreach ($defaultadaptivequizsettings as $name => $value) { 72 | if (!isset($record->{$name})) { 73 | $record->{$name} = $value; 74 | } 75 | } 76 | 77 | $record->coursemodule = $this->precreate_course_module($record->course, $options); 78 | $id = adaptivequiz_add_instance($record); 79 | return $this->post_add_instance($id, $record->coursemodule); 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/generator_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive PHPUnit data generator testcase 19 | * 20 | * @package mod 21 | * @subpackage adaptivequiz 22 | * @category phpunit 23 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | /** 30 | * @group mod_adaptivequiz 31 | */ 32 | class mod_adaptivequiz_generator_testcase extends advanced_testcase { 33 | 34 | /** 35 | * Unit test for adaptivequiz generator 36 | */ 37 | public function test_generator() { 38 | global $DB, $SITE; 39 | 40 | $this->resetAfterTest(true); 41 | 42 | $this->assertEquals(0, $DB->count_records('adaptivequiz')); 43 | 44 | /** @var mod_quiz_generator $generator */ 45 | $generator = $this->getDataGenerator()->get_plugin_generator('mod_adaptivequiz'); 46 | $this->assertInstanceOf('mod_adaptivequiz_generator', $generator); 47 | $this->assertEquals('adaptivequiz', $generator->get_modulename()); 48 | 49 | $generator->create_instance(array('course' => $SITE->id)); 50 | $generator->create_instance(array('course' => $SITE->id)); 51 | $adaptivequiz = $generator->create_instance(array('course' => $SITE->id)); 52 | $this->assertEquals(3, $DB->count_records('adaptivequiz')); 53 | 54 | $cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id); 55 | $this->assertEquals($adaptivequiz->id, $cm->instance); 56 | $this->assertEquals('adaptivequiz', $cm->modname); 57 | $this->assertEquals($SITE->id, $cm->course); 58 | 59 | $context = context_module::instance($cm->id); 60 | $this->assertEquals($adaptivequiz->cmid, $context->instanceid); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/lib_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive lib.php PHPUnit tests 19 | * 20 | * @package mod_adaptivequiz 21 | * @category phpunit 22 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | global $CFG; 29 | require_once($CFG->dirroot.'/mod/adaptivequiz/lib.php'); 30 | 31 | /** 32 | * @group mod_adaptivequiz 33 | */ 34 | class mod_adaptivequiz_lib_testcase extends advanced_testcase { 35 | /** 36 | * This functions loads data via the tests/fixtures/mod_adaptivequiz.xml file 37 | * @return void 38 | */ 39 | protected function setup_test_data_xml() { 40 | $this->loadDataSet($this->createXMLDataSet(__DIR__.'/fixtures/mod_adaptivequiz.xml')); 41 | } 42 | 43 | /** 44 | * Provide input data to the parameters of the test_questioncat_association_insert() method. 45 | */ 46 | public function questioncat_association_records() { 47 | $data = array(); 48 | 49 | $adaptivequiz = new stdClass(); 50 | $adaptivequiz->questionpool = array(1, 2, 3, 4); 51 | $data[] = array(1, $adaptivequiz); 52 | 53 | $adaptivequiz = new stdClass(); 54 | $adaptivequiz->questionpool = array(1, 2); 55 | $data[] = array(2, $adaptivequiz); 56 | 57 | $adaptivequiz = new stdClass(); 58 | $adaptivequiz->questionpool = array(1, 2, 4); 59 | $data[] = array(3, $adaptivequiz); 60 | 61 | return $data; 62 | } 63 | 64 | /** 65 | * Test insertion of question category association records 66 | * @dataProvider questioncat_association_records 67 | * @param int $instance: activity instance id 68 | * @param object $adaptivequiz: An object from the form in mod_form.php 69 | * @group adaptivequiz_lib_test 70 | */ 71 | public function test_questioncat_association_insert($instance, stdClass $adaptivequiz) { 72 | global $DB; 73 | 74 | $this->resetAfterTest(true); 75 | 76 | adaptivequiz_add_questcat_association($instance, $adaptivequiz); 77 | 78 | if (1 == $instance) { 79 | $this->assertEquals(4, $DB->count_records('adaptivequiz_question', array('instance' => $instance))); 80 | } 81 | 82 | if (2 == $instance) { 83 | $this->assertEquals(2, $DB->count_records('adaptivequiz_question', array('instance' => $instance))); 84 | } 85 | 86 | if (3 == $instance) { 87 | $this->assertEquals(3, $DB->count_records('adaptivequiz_question', array('instance' => $instance))); 88 | } 89 | } 90 | 91 | /** 92 | * Test update of question category associations records 93 | * @dataProvider questioncat_association_records 94 | * @param int $instance: activity instance id 95 | * @param object $adaptivequiz: An object from the form in mod_form.php 96 | * @group adaptivequiz_lib_test 97 | */ 98 | public function test_questioncat_association_update($instance, stdClass $adaptivequiz) { 99 | global $DB; 100 | 101 | $this->resetAfterTest(true); 102 | 103 | adaptivequiz_add_questcat_association($instance, $adaptivequiz); 104 | 105 | if (1 == $instance) { 106 | $adaptivequizupdate = new stdClass(); 107 | $adaptivequizupdate->questionpool = array(111, 222, 333, 444, 555, 122, 133, 144, 155, 166); 108 | 109 | adaptivequiz_update_questcat_association($instance, $adaptivequizupdate); 110 | $this->assertEquals(10, $DB->count_records('adaptivequiz_question', array('instance' => $instance))); 111 | } 112 | 113 | if (2 == $instance) { 114 | $adaptivequizupdate = new stdClass(); 115 | $adaptivequizupdate->questionpool = array(4); 116 | 117 | adaptivequiz_update_questcat_association($instance, $adaptivequizupdate); 118 | $this->assertEquals(1, $DB->count_records('adaptivequiz_question', array('instance' => $instance))); 119 | } 120 | 121 | if (3 == $instance) { 122 | $adaptivequizupdate = new stdClass(); 123 | $adaptivequizupdate->questionpool = array(4, 10, 20, 30, 40, 100, 333); 124 | 125 | adaptivequiz_update_questcat_association($instance, $adaptivequizupdate); 126 | $this->assertEquals(7, $DB->count_records('adaptivequiz_question', array('instance' => $instance))); 127 | } 128 | } 129 | 130 | /** 131 | * This function tests the removal of an activity instance and all related data 132 | */ 133 | public function test_adaptivequiz_delete_instance() { 134 | global $DB; 135 | 136 | $this->resetAfterTest(true); 137 | $this->setup_test_data_xml(); 138 | 139 | $instance = 330; 140 | adaptivequiz_delete_instance($instance); 141 | 142 | $this->assertEquals(0, $DB->count_records('adaptivequiz', array('id' => $instance))); 143 | $this->assertEquals(0, $DB->count_records('adaptivequiz_question', array('instance' => $instance))); 144 | $this->assertEquals(0, $DB->count_records('adaptivequiz_attempt', array('instance' => $instance))); 145 | $this->assertEquals(0, $DB->count_records('question_usages', array('id' => $instance))); 146 | } 147 | 148 | /** 149 | * This function tests the output from adaptivequiz_print_recent_mod_activity() 150 | */ 151 | public function test_adaptivequiz_print_recent_mod_activity_details_true() { 152 | $this->resetAfterTest(true); 153 | 154 | $dummy = new stdClass(); 155 | $dummy->user = new stdClass(); 156 | $dummy->user->id = 2; 157 | $dummy->user->fullname = 'user-phpunit'; 158 | $dummy->user->alternatename = 'user-phpunit'; 159 | $dummy->user->picture = ''; 160 | $dummy->user->firstname = 'user'; 161 | $dummy->user->middlename = '-'; 162 | $dummy->user->lastname = 'phpunit'; 163 | $dummy->user->imagealt = ''; 164 | $dummy->user->email = 'a@a.com'; 165 | $dummy->user->firstnamephonetic = 'user'; 166 | $dummy->user->lastnamephonetic = 'phpunit'; 167 | $dummy->content = new stdClass(); 168 | $dummy->content->attemptstate = 'inprogress'; 169 | $dummy->content->questionsattempted = '12'; 170 | $dummy->timestamp = 1234; 171 | $dummy->type = 'mod_adaptivequiz'; 172 | $dummy->name = 'my-phpunit-test'; 173 | $dummy->cmid = 99; 174 | 175 | $output = adaptivequiz_print_recent_mod_activity($dummy, 1, true, array('mod_adaptivequiz' => 'adaptivequiz'), true, true); 176 | 177 | $this->assertContains('assertContains('', $output); 179 | $this->assertContains('assertContains('mod/adaptivequiz/view.php?id=99', $output); 181 | $this->assertContains('/user/view.php?id=2', $output); 182 | $this->assertContains('user phpunit', $output); 183 | $this->assertContains('my-phpunit-test', $output); 184 | } 185 | 186 | /** 187 | * This function tests the output from adaptivequiz_print_recent_mod_activity() 188 | */ 189 | public function test_adaptivequiz_print_recent_mod_activity_details_false() { 190 | $this->resetAfterTest(true); 191 | 192 | $dummy = new stdClass(); 193 | $dummy->user = new stdClass(); 194 | $dummy->user->id = 2; 195 | $dummy->user->fullname = 'user-phpunit'; 196 | $dummy->user->alternatename = 'user-phpunit'; 197 | $dummy->user->picture = ''; 198 | $dummy->user->firstname = 'user'; 199 | $dummy->user->middlename = '-'; 200 | $dummy->user->lastname = 'phpunit'; 201 | $dummy->user->imagealt = ''; 202 | $dummy->user->email = 'a@a.com'; 203 | $dummy->user->firstnamephonetic = 'user'; 204 | $dummy->user->lastnamephonetic = 'phpunit'; 205 | $dummy->content = new stdClass(); 206 | $dummy->content->attemptstate = 'inprogress'; 207 | $dummy->content->questionsattempted = '12'; 208 | $dummy->timestamp = 1234; 209 | $dummy->type = 'mod_adaptivequiz'; 210 | $dummy->name = 'my-phpunit-test'; 211 | $dummy->cmid = 99; 212 | 213 | $output = adaptivequiz_print_recent_mod_activity($dummy, 1, false, array('mod_adaptivequiz' => 'adaptivequiz'), true, true); 214 | 215 | $this->assertContains('assertContains('', $output); 217 | $this->assertContains('assertContains('/user/view.php?id=2', $output); 219 | $this->assertContains('user phpunit', $output); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive testing version information. 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | defined('MOODLE_INTERNAL') || die(); 29 | 30 | $plugin->version = 2017011900; 31 | $plugin->release = '1.2.3'; 32 | $plugin->requires = 2014051200; 33 | $plugin->cron = 0; 34 | $plugin->component = 'mod_adaptivequiz'; 35 | $plugin->maturity = MATURITY_STABLE; 36 | -------------------------------------------------------------------------------- /view.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive testing main view page script 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../config.php'); 29 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 30 | 31 | $id = optional_param('id', 0, PARAM_INT); 32 | $n = optional_param('n', 0, PARAM_INT); 33 | 34 | if ($id) { 35 | $cm = get_coursemodule_from_id('adaptivequiz', $id, 0, false, MUST_EXIST); 36 | $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); 37 | $adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*', MUST_EXIST); 38 | } else if ($n) { 39 | $adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $n), '*', MUST_EXIST); 40 | $course = $DB->get_record('course', array('id' => $adaptivequiz->course), '*', MUST_EXIST); 41 | $cm = get_coursemodule_from_instance('adaptivequiz', $adaptivequiz->id, $course->id, false, MUST_EXIST); 42 | } else { 43 | print_error('invalidarguments'); 44 | } 45 | 46 | require_login($course, true, $cm); 47 | $context = context_module::instance($cm->id); 48 | 49 | $event = \mod_adaptivequiz\event\course_module_viewed::create( 50 | array( 51 | 'objectid' => $PAGE->cm->instance, 52 | 'context' => $PAGE->context, 53 | ) 54 | ); 55 | 56 | $event->add_record_snapshot('course', $PAGE->course); 57 | $event->add_record_snapshot($PAGE->cm->modname, $adaptivequiz); 58 | $event->trigger(); 59 | 60 | // Print the page header. 61 | $PAGE->set_url('/mod/adaptivequiz/view.php', array('id' => $cm->id)); 62 | $PAGE->set_title(format_string($adaptivequiz->name)); 63 | $PAGE->set_heading(format_string($course->fullname)); 64 | $PAGE->set_context($context); 65 | 66 | // Output starts here. 67 | echo $OUTPUT->header(); 68 | 69 | if ($adaptivequiz->intro) { // Conditions to show the intro can change to look for own settings or whatever. 70 | echo $OUTPUT->box(format_module_intro('adaptivequiz', $adaptivequiz, $cm->id), 'generalbox mod_introbox', 'newmoduleintro'); 71 | } 72 | 73 | $renderer = $PAGE->get_renderer('mod_adaptivequiz'); 74 | 75 | // Check if the instance exists. 76 | if (has_capability('mod/adaptivequiz:attempt', $context)) { 77 | 78 | // Check if the user has any previous attempts at this activity. 79 | $count = adaptivequiz_count_user_previous_attempts($adaptivequiz->id, $USER->id); 80 | 81 | if (adaptivequiz_allowed_attempt($adaptivequiz->attempts, $count)) { 82 | if (empty($adaptivequiz->browsersecurity)) { 83 | echo $renderer->display_start_attempt_form($cm->id); 84 | } else { 85 | echo $renderer->display_start_attempt_form_scured($cm->id); 86 | } 87 | } else { 88 | echo $OUTPUT->notification(get_string('noattemptsallowed', 'adaptivequiz')); 89 | } 90 | } 91 | 92 | if (has_capability('mod/adaptivequiz:viewreport', $context)) { 93 | echo $renderer->display_view_report_form($cm->id); 94 | echo $renderer->display_question_analysis_form($cm->id); 95 | } 96 | // Finish the page. 97 | echo $OUTPUT->footer(); -------------------------------------------------------------------------------- /viewattemptreport.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz view attempt report script 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../config.php'); 29 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 30 | 31 | $id = required_param('cmid', PARAM_INT); 32 | $userid = required_param('userid', PARAM_INT); 33 | 34 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 35 | print_error('invalidcoursemodule'); 36 | } 37 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 38 | print_error("coursemisconf"); 39 | } 40 | 41 | require_login($course, true, $cm); 42 | $context = context_module::instance($cm->id); 43 | 44 | require_capability('mod/adaptivequiz:viewreport', $context); 45 | 46 | $param = array('instance' => $cm->instance, 'userid' => $userid); 47 | $sql = "SELECT aa.id, aa.userid, aa.uniqueid, aa.attemptstopcriteria, aa.measure, aa.attemptstate, aa.questionsattempted, 48 | aa.timemodified, aa.standarderror AS stderror, a.highestlevel, a.lowestlevel, a.name, aa.timecreated 49 | FROM {adaptivequiz_attempt} aa 50 | JOIN {adaptivequiz} a ON aa.instance = a.id 51 | WHERE aa.instance = :instance 52 | AND aa.userid = :userid 53 | ORDER BY aa.timemodified DESC"; 54 | $records = $DB->get_records_sql($sql, $param); 55 | 56 | // Check if recordset contains records. 57 | if (empty($records)) { 58 | $url = new moodle_url('/mod/adaptivequiz/viewreport.php', array('cmid' => $cm->id)); 59 | notice(get_string('noattemptrecords', 'adaptivequiz'), $url); 60 | } 61 | 62 | $record = current($records); 63 | 64 | $PAGE->set_url('/mod/adaptivequiz/viewattemptreport.php', array('cmid' => $cm->id)); 65 | $PAGE->set_title(format_string($record->name)); 66 | $PAGE->set_heading(format_string($course->fullname)); 67 | $PAGE->set_context($context); 68 | 69 | $output = $PAGE->get_renderer('mod_adaptivequiz'); 70 | 71 | /* print header information */ 72 | $header = $output->print_header(); 73 | /* Output attempts table */ 74 | $user = $DB->get_record('user', array('id' => $userid)); 75 | $reporttable = $output->print_attempt_report_table($records, $cm, $user); 76 | /* OUtput return to main reports page button */ 77 | $url = new moodle_url('/mod/adaptivequiz/viewreport.php', array('cmid' => $cm->id)); 78 | $txt = get_string('backtoviewreport', 'adaptivequiz'); 79 | $button = $output->print_form_and_button($url, $txt); 80 | 81 | /* Output footer information */ 82 | $footer = $output->print_footer(); 83 | 84 | echo $header; 85 | echo $reporttable; 86 | echo $button; 87 | echo $footer; 88 | -------------------------------------------------------------------------------- /viewreport.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Adaptive quiz view report script 19 | * 20 | * This module was created as a collaborative effort between Middlebury College 21 | * and Remote Learner. 22 | * 23 | * @package mod_adaptivequiz 24 | * @copyright 2013 onwards Remote-Learner {@link http://www.remote-learner.ca/} 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | 28 | require_once(dirname(__FILE__).'/../../config.php'); 29 | require_once($CFG->dirroot.'/lib/grouplib.php'); 30 | require_once($CFG->dirroot.'/mod/adaptivequiz/locallib.php'); 31 | 32 | $id = required_param('cmid', PARAM_INT); 33 | $sortdir = optional_param('sortdir', 'ASC', PARAM_ALPHA); 34 | $sort = optional_param('sort', 'lastname', PARAM_ALPHA); 35 | $page = optional_param('page', 0, PARAM_INT); 36 | $groupid = optional_param('group', 0, PARAM_INT); 37 | $download = optional_param('download', '', PARAM_ALPHA); 38 | 39 | if (!$cm = get_coursemodule_from_id('adaptivequiz', $id)) { 40 | print_error('invalidcoursemodule'); 41 | } 42 | if (!$course = $DB->get_record('course', array('id' => $cm->course))) { 43 | print_error("coursemisconf"); 44 | } 45 | 46 | require_login($course, true, $cm); 47 | $context = context_module::instance($cm->id); 48 | 49 | require_capability('mod/adaptivequiz:viewreport', $context); 50 | 51 | $adaptivequiz = $DB->get_record('adaptivequiz', array('id' => $cm->instance), '*'); 52 | $PAGE->set_url('/mod/adaptivequiz/viewreport.php', array('cmid' => $cm->id)); 53 | $PAGE->set_title(format_string($adaptivequiz->name)); 54 | $PAGE->set_heading(format_string($course->fullname)); 55 | $PAGE->set_context($context); 56 | 57 | if ($download == 'csv') { 58 | $output = $PAGE->get_renderer('mod_adaptivequiz', 'csv'); 59 | } else { 60 | $output = $PAGE->get_renderer('mod_adaptivequiz'); 61 | } 62 | 63 | /* Initialized parameter array for sql query */ 64 | $param = array( 65 | 'attemptstate1' => ADAPTIVEQUIZ_ATTEMPT_COMPLETED, 66 | 'attemptstate2' => ADAPTIVEQUIZ_ATTEMPT_COMPLETED, 67 | 'attemptstate3' => ADAPTIVEQUIZ_ATTEMPT_COMPLETED, 68 | 'attemptstate4' => ADAPTIVEQUIZ_ATTEMPT_COMPLETED, 69 | 'instance' => $cm->instance); 70 | 71 | /* Constructo order by clause */ 72 | $orderby = adaptivequiz_construct_view_report_orderby($sort, $sortdir); 73 | 74 | /* Output the groups selector */ 75 | $groupsel = $output->print_groups_selector($cm, $course, $context, $USER->id); 76 | 77 | $groupjoin = ''; 78 | $groupwhere = ''; 79 | 80 | /* Determine the currently active group id */ 81 | if (empty($groupid)) { 82 | $allowedgroups = groups_get_activity_allowed_groups($cm, $USER->id); 83 | $groupid = groups_get_activity_group($cm, true, $allowedgroups); 84 | } 85 | 86 | /* Create the group sql join and where clause */ 87 | if (0 != $groupid) { 88 | $groupjoin = ' INNER JOIN {groups_members} gm ON u.id = gm.userid '; 89 | $groupwhere = ' AND gm.groupid = :groupid '; 90 | $param['groupid'] = $groupid; 91 | } 92 | 93 | /* Retreive a list of attempts made by each user, displaying the sum of attempts and the highest score for each user */ 94 | $sql = "SELECT u.id, u.firstname, u.lastname, u.email, a.highestlevel, a.lowestlevel, 95 | (SELECT COUNT(*) 96 | FROM {adaptivequiz_attempt} caa 97 | WHERE caa.userid = u.id 98 | AND caa.instance = aa.instance 99 | ) AS attempts, 100 | (SELECT maa.measure 101 | FROM {adaptivequiz_attempt} maa 102 | WHERE maa.instance = a.id 103 | AND maa.userid = u.id 104 | AND maa.attemptstate = :attemptstate1 105 | AND maa.standarderror > 0.0 106 | ORDER BY measure DESC 107 | LIMIT 1) AS measure, 108 | (SELECT saa.standarderror 109 | FROM {adaptivequiz_attempt} saa 110 | WHERE saa.instance = a.id 111 | AND saa.userid = u.id 112 | AND saa.attemptstate = :attemptstate2 113 | AND saa.standarderror > 0.0 114 | ORDER BY measure DESC 115 | LIMIT 1) AS stderror, 116 | (SELECT taa.timemodified 117 | FROM {adaptivequiz_attempt} taa 118 | WHERE taa.instance = a.id 119 | AND taa.userid = u.id 120 | AND taa.attemptstate = :attemptstate3 121 | AND taa.standarderror > 0.0 122 | ORDER BY measure DESC 123 | LIMIT 1) AS timemodified, 124 | (SELECT iaa.uniqueid 125 | FROM {adaptivequiz_attempt} iaa 126 | WHERE iaa.instance = a.id 127 | AND iaa.userid = u.id 128 | AND iaa.attemptstate = :attemptstate4 129 | AND iaa.standarderror > 0.0 130 | ORDER BY measure DESC 131 | LIMIT 1) AS uniqueid 132 | FROM {adaptivequiz_attempt} aa 133 | JOIN {user} u ON u.id = aa.userid 134 | JOIN {adaptivequiz} a ON a.id = aa.instance 135 | $groupjoin 136 | WHERE aa.instance = :instance 137 | $groupwhere 138 | GROUP BY u.id, aa.instance, a.id, a.highestlevel, a.lowestlevel 139 | $orderby"; 140 | $startfrom = $page * ADAPTIVEQUIZ_REC_PER_PAGE; 141 | 142 | if ($download) { 143 | $records = $DB->get_records_sql($sql, $param); 144 | } else { 145 | $records = $DB->get_records_sql($sql, $param, $startfrom, ADAPTIVEQUIZ_REC_PER_PAGE); 146 | } 147 | /* Count the total number of records returned */ 148 | $recordscount = $DB->get_records_sql($sql, $param); 149 | 150 | /* Set selected groupid */ 151 | $output->groupid = $groupid; 152 | 153 | /* print header information */ 154 | $header = $output->print_header(); 155 | /* Output attempts table */ 156 | $reporttable = $output->print_report_table($records, $cm, $sort, $sortdir); 157 | /* Output paging bar */ 158 | $pagingbar = $output->print_paging_bar(count($recordscount), $page, ADAPTIVEQUIZ_REC_PER_PAGE); 159 | /* Output footer information */ 160 | $footer = $output->print_footer(); 161 | 162 | echo $header; 163 | echo $groupsel; 164 | echo $pagingbar; 165 | echo $reporttable; 166 | echo $pagingbar; 167 | echo $footer; 168 | --------------------------------------------------------------------------------