In particular the quiz module will perform an extensive change of the quiz tables and this upgrade has not yet been sufficiently tested. You are very strongly urged to backup your database tables before proceeding.
';
+$string['upgradingquizattempts'] = 'Upgrading quiz attempts: quiz {$a->done}/{$a->outof} (Quiz id {$a->info})';
+$string['upgradingveryoldquizattempts'] = 'Upgrading very old quiz attempts: {$a->done}/{$a->outof}';
$string['url'] = 'URL';
$string['usedcategorymoved'] = 'This category has been preserved and moved to the site level because it is a published category still in use by other courses.';
$string['useroverrides'] = 'User overrides';
@@ -858,27 +780,13 @@ $string['validate'] = 'Validate';
$string['viewallanswers'] = 'View {$a} quiz attempts';
$string['viewallreports'] = 'View reports for {$a} attempts';
$string['warningmissingtype'] = ''. $message .'
';
@@ -720,7 +668,6 @@ function quiz_grade_item_update($quiz, $grades=NULL) {
/**
* Delete grade item for given quiz
*
- * @global stdClass
* @param object $quiz object
* @return object quiz
*/
@@ -728,18 +675,8 @@ function quiz_grade_item_delete($quiz) {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
- return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted' => 1));
-}
-
-/**
- * @return the options for calculating the quiz grade from the individual attempt grades.
- */
-function quiz_get_grading_options() {
- return array (
- QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
- QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
- QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
- QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz'));
+ return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0,
+ null, array('deleted' => 1));
}
/**
@@ -747,22 +684,16 @@ function quiz_get_grading_options() {
*
* @todo: deprecated - to be deleted in 2.2
*
- * @param int $quizid
- * @return array
+ * @param int $quizid the quiz id.
+ * @return array of userids.
*/
function quiz_get_participants($quizid) {
global $CFG, $DB;
- //Get users from attempts
- $us_attempts = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
- FROM {user} u,
- {quiz_attempts} a
- WHERE a.quiz = ? and
- u.id = a.userid", array($quizid));
-
- //Return us_attempts array (it contains an array of unique users)
- return $us_attempts;
-
+ return $DB->get_records_sql('
+ SELECT DISTINCT userid, userid
+ JOIN {quiz_attempts} qa
+ WHERE a.quiz = ?', array($quizid));
}
/**
@@ -772,8 +703,6 @@ function quiz_get_participants($quizid) {
* only quiz events belonging to the course specified are checked.
* This function is used, in its new format, by restore_refresh_events()
*
- * @global object
- * @uses QUIZ_MAX_EVENT_LENGTH
* @param int $courseid
* @return bool
*/
@@ -781,11 +710,11 @@ function quiz_refresh_events($courseid = 0) {
global $DB;
if ($courseid == 0) {
- if (! $quizzes = $DB->get_records('quiz')) {
+ if (!$quizzes = $DB->get_records('quiz')) {
return true;
}
} else {
- if (! $quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
+ if (!$quizzes = $DB->get_records('quiz', array('course' => $courseid))) {
return true;
}
}
@@ -857,11 +786,12 @@ function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
$groupmode = groups_get_activity_groupmode($cm, $course);
if (is_null($modinfo->groups)) {
- $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
+ // load all my groups and cache it in modinfo
+ $modinfo->groups = groups_get_user_groups($course->id);
}
$usersgroups = null;
- $aname = format_string($cm->name,true);
+ $aname = format_string($cm->name, true);
foreach ($attempts as $attempt) {
if ($attempt->userid != $USER->id) {
if (!$grader) {
@@ -885,9 +815,9 @@ function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
}
}
- $options = quiz_get_reviewoptions($quiz, $attempt, $context);
+ $options = quiz_get_review_options($quiz, $attempt, $context);
- $tmpactivity = new stdClass;
+ $tmpactivity = new stdClass();
$tmpactivity->type = 'quiz';
$tmpactivity->cmid = $cm->id;
@@ -897,7 +827,7 @@ function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
$tmpactivity->content->attemptid = $attempt->id;
$tmpactivity->content->attempt = $attempt->attempt;
- if (quiz_has_grades($quiz) && $options->scores) {
+ if (quiz_has_grades($quiz) && $options->marks >= question_display_options::MARK_AND_MAX) {
$tmpactivity->content->sumgrades = quiz_format_grade($quiz, $attempt->sumgrades);
$tmpactivity->content->maxgrade = quiz_format_grade($quiz, $quiz->sumgrades);
} else {
@@ -907,16 +837,14 @@ function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
$tmpactivity->user->id = $attempt->userid;
$tmpactivity->user->firstname = $attempt->firstname;
- $tmpactivity->user->lastname = $attempt->lastname;
- $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
- $tmpactivity->user->picture = $attempt->picture;
- $tmpactivity->user->imagealt = $attempt->imagealt;
- $tmpactivity->user->email = $attempt->email;
+ $tmpactivity->user->lastname = $attempt->lastname;
+ $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
+ $tmpactivity->user->picture = $attempt->picture;
+ $tmpactivity->user->imagealt = $attempt->imagealt;
+ $tmpactivity->user->email = $attempt->email;
$activities[$index++] = $tmpactivity;
}
-
- return;
}
function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
@@ -962,21 +890,13 @@ function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames
* Pre-process the quiz options form data, making any necessary adjustments.
* Called by add/update instance in this file.
*
- * @uses QUIZ_REVIEW_OVERALLFEEDBACK
- * @uses QUIZ_REVIEW_CLOSED
- * @uses QUIZ_REVIEW_OPEN
- * @uses QUIZ_REVIEW_IMMEDIATELY
- * @uses QUIZ_REVIEW_GENERALFEEDBACK
- * @uses QUIZ_REVIEW_SOLUTIONS
- * @uses QUIZ_REVIEW_ANSWERS
- * @uses QUIZ_REVIEW_FEEDBACK
- * @uses QUIZ_REVIEW_SCORES
- * @uses QUIZ_REVIEW_RESPONSES
- * @uses QUESTION_ADAPTIVE
* @param object $quiz The variables set on the form.
- * @return string
*/
-function quiz_process_options(&$quiz) {
+function quiz_process_options($quiz) {
+ global $CFG;
+ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+ require_once($CFG->libdir . '/questionlib.php');
+
$quiz->timemodified = time();
// Quiz name.
@@ -1028,131 +948,66 @@ function quiz_process_options(&$quiz) {
// Check there is nothing in the remaining unused fields.
if (!empty($quiz->feedbackboundaries)) {
for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
- if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
+ if (!empty($quiz->feedbackboundaries[$i]) &&
+ trim($quiz->feedbackboundaries[$i]) != '') {
return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
}
}
}
for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
- if (!empty($quiz->feedbacktext[$i]['text']) && trim($quiz->feedbacktext[$i]['text']) != '') {
+ if (!empty($quiz->feedbacktext[$i]['text']) &&
+ trim($quiz->feedbacktext[$i]['text']) != '') {
return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
}
}
- $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
+ // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
+ $quiz->feedbackboundaries[-1] = $quiz->grade + 1;
$quiz->feedbackboundaries[$numboundaries] = 0;
$quiz->feedbackboundarycount = $numboundaries;
}
- // Settings that get combined to go into the optionflags column.
- $quiz->optionflags = 0;
- if (!empty($quiz->adaptive)) {
- $quiz->optionflags |= QUESTION_ADAPTIVE;
- }
+ // Combing the individual settings into the review columns.
+ $quiz->reviewattempt = quiz_review_option_form_to_db($quiz, 'attempt');
+ $quiz->reviewcorrectness = quiz_review_option_form_to_db($quiz, 'correctness');
+ $quiz->reviewmarks = quiz_review_option_form_to_db($quiz, 'marks');
+ $quiz->reviewspecificfeedback = quiz_review_option_form_to_db($quiz, 'specificfeedback');
+ $quiz->reviewgeneralfeedback = quiz_review_option_form_to_db($quiz, 'generalfeedback');
+ $quiz->reviewrightanswer = quiz_review_option_form_to_db($quiz, 'rightanswer');
+ $quiz->reviewoverallfeedback = quiz_review_option_form_to_db($quiz, 'overallfeedback');
+ $quiz->reviewattempt |= mod_quiz_display_options::DURING;
+ $quiz->reviewoverallfeedback &= ~mod_quiz_display_options::DURING;
+}
+
+/**
+ * Helper function for {@link quiz_process_options()}.
+ * @param object $fromform the sumbitted form date.
+ * @param string $field one of the review option field names.
+ */
+function quiz_review_option_form_to_db($fromform, $field) {
+ static $times = array(
+ 'during' => mod_quiz_display_options::DURING,
+ 'immediately' => mod_quiz_display_options::IMMEDIATELY_AFTER,
+ 'open' => mod_quiz_display_options::LATER_WHILE_OPEN,
+ 'closed' => mod_quiz_display_options::AFTER_CLOSE,
+ );
- // Settings that get combined to go into the review column.
$review = 0;
- if (isset($quiz->responsesimmediately)) {
- $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
- unset($quiz->responsesimmediately);
- }
- if (isset($quiz->responsesopen)) {
- $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
- unset($quiz->responsesopen);
- }
- if (isset($quiz->responsesclosed)) {
- $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
- unset($quiz->responsesclosed);
+ foreach ($times as $whenname => $when) {
+ $fieldname = $field . $whenname;
+ if (isset($fromform->$fieldname)) {
+ $review |= $when;
+ unset($fromform->$fieldname);
+ }
}
- if (isset($quiz->scoreimmediately)) {
- $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
- unset($quiz->scoreimmediately);
- }
- if (isset($quiz->scoreopen)) {
- $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
- unset($quiz->scoreopen);
- }
- if (isset($quiz->scoreclosed)) {
- $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
- unset($quiz->scoreclosed);
- }
-
- if (isset($quiz->feedbackimmediately)) {
- $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
- unset($quiz->feedbackimmediately);
- }
- if (isset($quiz->feedbackopen)) {
- $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
- unset($quiz->feedbackopen);
- }
- if (isset($quiz->feedbackclosed)) {
- $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
- unset($quiz->feedbackclosed);
- }
-
- if (isset($quiz->answersimmediately)) {
- $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
- unset($quiz->answersimmediately);
- }
- if (isset($quiz->answersopen)) {
- $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
- unset($quiz->answersopen);
- }
- if (isset($quiz->answersclosed)) {
- $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
- unset($quiz->answersclosed);
- }
-
- if (isset($quiz->solutionsimmediately)) {
- $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY);
- unset($quiz->solutionsimmediately);
- }
- if (isset($quiz->solutionsopen)) {
- $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN);
- unset($quiz->solutionsopen);
- }
- if (isset($quiz->solutionsclosed)) {
- $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED);
- unset($quiz->solutionsclosed);
- }
-
- if (isset($quiz->generalfeedbackimmediately)) {
- $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
- unset($quiz->generalfeedbackimmediately);
- }
- if (isset($quiz->generalfeedbackopen)) {
- $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
- unset($quiz->generalfeedbackopen);
- }
- if (isset($quiz->generalfeedbackclosed)) {
- $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
- unset($quiz->generalfeedbackclosed);
- }
-
- if (isset($quiz->overallfeedbackimmediately)) {
- $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
- unset($quiz->overallfeedbackimmediately);
- }
- if (isset($quiz->overallfeedbackopen)) {
- $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
- unset($quiz->overallfeedbackopen);
- }
- if (isset($quiz->overallfeedbackclosed)) {
- $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
- unset($quiz->overallfeedbackclosed);
- }
-
- $quiz->review = $review;
+ return $review;
}
/**
* This function is called at the end of quiz_add_instance
* and quiz_update_instance, to do the common processing.
*
- * @global object
- * @uses QUIZ_MAX_EVENT_LENGTH
* @param object $quiz the quiz object.
- * @return void|string Void or error message
*/
function quiz_after_add_or_update($quiz) {
global $DB;
@@ -1166,15 +1021,19 @@ function quiz_after_add_or_update($quiz) {
$DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) {
- $feedback = new stdClass;
+ $feedback = new stdClass();
$feedback->quizid = $quiz->id;
$feedback->feedbacktext = $quiz->feedbacktext[$i]['text'];
$feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'];
$feedback->mingrade = $quiz->feedbackboundaries[$i];
$feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
$feedback->id = $DB->insert_record('quiz_feedback', $feedback);
- $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'], $context->id, 'mod_quiz', 'feedback', $feedback->id, array('subdirs'=>false, 'maxfiles'=>-1, 'maxbytes'=>0), $quiz->feedbacktext[$i]['text']);
- $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext, array('id'=>$feedback->id));
+ $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'],
+ $context->id, 'mod_quiz', 'feedback', $feedback->id,
+ array('subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0),
+ $quiz->feedbacktext[$i]['text']);
+ $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext,
+ array('id' => $feedback->id));
}
// Update the events relating to this quiz.
@@ -1182,7 +1041,6 @@ function quiz_after_add_or_update($quiz) {
//update related grade item
quiz_grade_item_update($quiz);
-
}
/**
@@ -1213,9 +1071,8 @@ function quiz_update_events($quiz, $override = null) {
// need to add all the overrides
$overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id));
// as well as the original quiz (empty override)
- $overrides[] = new stdClass;
- }
- else {
+ $overrides[] = new stdClass();
+ } else {
// Just do the one override
$overrides = array($override);
}
@@ -1230,9 +1087,10 @@ function quiz_update_events($quiz, $override = null) {
$addopen = empty($current->id) || !empty($current->timeopen);
$addclose = empty($current->id) || !empty($current->timeclose);
- $event = new stdClass;
+ $event = new stdClass();
$event->description = $quiz->intro;
- $event->courseid = ($userid) ? 0 : $quiz->course; // Events module won't show user events when the courseid is nonzero
+ // Events module won't show user events when the courseid is nonzero
+ $event->courseid = ($userid) ? 0 : $quiz->course;
$event->groupid = $groupid;
$event->userid = $userid;
$event->modulename = 'quiz';
@@ -1244,7 +1102,7 @@ function quiz_update_events($quiz, $override = null) {
// Determine the event name
if ($groupid) {
- $params = new stdClass;
+ $params = new stdClass();
$params->quiz = $quiz->name;
$params->group = groups_get_group_name($groupid);
if ($params->group === false) {
@@ -1252,9 +1110,8 @@ function quiz_update_events($quiz, $override = null) {
continue;
}
$eventname = get_string('overridegroupeventname', 'quiz', $params);
- }
- else if ($userid) {
- $params = new stdClass;
+ } else if ($userid) {
+ $params = new stdClass();
$params->quiz = $quiz->name;
$eventname = get_string('overrideusereventname', 'quiz', $params);
} else {
@@ -1265,8 +1122,7 @@ function quiz_update_events($quiz, $override = null) {
// Single event for the whole quiz.
if ($oldevent = array_shift($oldevents)) {
$event->id = $oldevent->id;
- }
- else {
+ } else {
unset($event->id);
}
$event->name = $eventname;
@@ -1278,8 +1134,7 @@ function quiz_update_events($quiz, $override = null) {
if ($timeopen && $addopen) {
if ($oldevent = array_shift($oldevents)) {
$event->id = $oldevent->id;
- }
- else {
+ } else {
unset($event->id);
}
$event->name = $eventname.' ('.get_string('quizopens', 'quiz').')';
@@ -1289,8 +1144,7 @@ function quiz_update_events($quiz, $override = null) {
if ($timeclose && $addclose) {
if ($oldevent = array_shift($oldevents)) {
$event->id = $oldevent->id;
- }
- else {
+ } else {
unset($event->id);
}
$event->name = $eventname.' ('.get_string('quizcloses', 'quiz').')';
@@ -1320,70 +1174,62 @@ function quiz_get_view_actions() {
* @return array
*/
function quiz_get_post_actions() {
- return array('attempt', 'close attempt', 'preview', 'editquestions', 'delete attempt', 'manualgrade');
+ return array('attempt', 'close attempt', 'preview', 'editquestions',
+ 'delete attempt', 'manualgrade');
}
/**
- * Returns an array of names of quizzes that use this question
- *
- * @param integer $questionid
- * @return array of strings
+ * @param array $questionids of question ids.
+ * @return bool whether any of these questions are used by any instance of this module.
*/
-function quiz_question_list_instances($questionid) {
- global $CFG, $DB;
-
- // TODO MDL-5780: we should also consider other questions that are used by
- // random questions in this quiz, but that is very hard.
-
- $sql = "SELECT q.id, q.name
- FROM {quiz} q
- JOIN {quiz_question_instances} qqi ON q.id = qqi.quiz
- WHERE qqi.question = ?";
-
- if ($instances = $DB->get_records_sql_menu($sql, array($questionid))) {
- return $instances;
- }
- return array();
+function quiz_questions_in_use($questionids) {
+ global $DB, $CFG;
+ require_once($CFG->libdir . '/questionlib.php');
+ list($test, $params) = $DB->get_in_or_equal($questionids);
+ return $DB->record_exists_select('quiz_question_instances',
+ 'question ' . $test, $params) || question_engine::questions_in_use(
+ $questionids, new qubaid_join('{quiz_attempts} quiza',
+ 'quiza.uniqueid', 'quiza.preview = 0'));
}
/**
* Implementation of the function for printing the form elements that control
* whether the course reset functionality affects the quiz.
*
- * @param $mform form passed by reference
+ * @param $mform the course reset form that is being built.
*/
-function quiz_reset_course_form_definition(&$mform) {
+function quiz_reset_course_form_definition($mform) {
$mform->addElement('header', 'quizheader', get_string('modulenameplural', 'quiz'));
- $mform->addElement('advcheckbox', 'reset_quiz_attempts', get_string('removeallquizattempts','quiz'));
+ $mform->addElement('advcheckbox', 'reset_quiz_attempts',
+ get_string('removeallquizattempts', 'quiz'));
}
/**
* Course reset form defaults.
- * @return array
+ * @return array the defaults.
*/
function quiz_reset_course_form_defaults($course) {
- return array('reset_quiz_attempts'=>1);
+ return array('reset_quiz_attempts' => 1);
}
/**
* Removes all grades from gradebook
*
- * @global stdClass
- * @global object
* @param int $courseid
* @param string optional type
*/
function quiz_reset_gradebook($courseid, $type='') {
global $CFG, $DB;
- $sql = "SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
- FROM {quiz} q, {course_modules} cm, {modules} m
- WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=q.id AND q.course=?";
+ $quizzes = $DB->get_records_sql("
+ SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
+ FROM {modules} m
+ JOIN {course_modules} cm ON m.id = cm.module
+ JOIN {quiz} q ON cm.instance = q.id
+ WHERE m.name = 'quiz' AND cm.course = ?", array($courseid));
- if ($quizs = $DB->get_records_sql($sql, array($courseid))) {
- foreach ($quizs as $quiz) {
- quiz_grade_item_update($quiz, 'reset');
- }
+ foreach ($quizzes as $quiz) {
+ quiz_grade_item_update($quiz, 'reset');
}
}
@@ -1394,8 +1240,6 @@ function quiz_reset_gradebook($courseid, $type='') {
*
* Also, move the quiz open and close dates, if the course start date is changing.
*
- * @global stdClass
- * @global object
* @param object $data the data submitted from the reset course.
* @return array status array
*/
@@ -1406,24 +1250,40 @@ function quiz_reset_userdata($data) {
$componentstr = get_string('modulenameplural', 'quiz');
$status = array();
- /// Delete attempts.
+ // Delete attempts.
if (!empty($data->reset_quiz_attempts)) {
- $quizzes = $DB->get_records('quiz', array('course' => $data->courseid));
- foreach ($quizzes as $quiz) {
- quiz_delete_all_attempts($quiz);
- }
+ require_once($CFG->libdir . '/questionlib.php');
- // remove all grades from gradebook
+ question_engine::delete_questions_usage_by_activities(new qubaid_join(
+ '{quiz_attempts} quiza JOIN {quiz} quiz ON quiza.quiz = quiz.id',
+ 'quiza.uniqueid', 'quiz.course = :quizcourseid',
+ array('quizcourseid' => $data->courseid)));
+
+ $DB->delete_records_select('quiz_attempts',
+ 'quiz IN (SELECT id FROM {quiz} WHERE course = ?)', array($data->courseid));
+ $status[] = array(
+ 'component' => $componentstr,
+ 'item' => get_string('attemptsdeleted', 'quiz'),
+ 'error' => false);
+
+ // Remove all grades from gradebook
if (empty($data->reset_gradebook_grades)) {
quiz_reset_gradebook($data->courseid);
}
- $status[] = array('component' => $componentstr, 'item' => get_string('attemptsdeleted', 'quiz'), 'error' => false);
+ $status[] = array(
+ 'component' => $componentstr,
+ 'item' => get_string('attemptsdeleted', 'quiz'),
+ 'error' => false);
}
- /// updating dates - shift may be negative too
+ // Updating dates - shift may be negative too
if ($data->timeshift) {
- shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
- $status[] = array('component' => $componentstr, 'item' => get_string('openclosedatesupdated', 'quiz'), 'error' => false);
+ shift_course_mod_dates('quiz', array('timeopen', 'timeclose'),
+ $data->timeshift, $data->courseid);
+ $status[] = array(
+ 'component' => $componentstr,
+ 'item' => get_string('openclosedatesupdated', 'quiz'),
+ 'error' => false);
}
return $status;
@@ -1433,12 +1293,9 @@ function quiz_reset_userdata($data) {
* Checks whether the current user is allowed to view a file uploaded in a quiz.
* Teachers can view any from their courses, students can only view their own.
*
- * @global object
- * @global object
- * @uses CONTEXT_COURSE
* @param int $attemptuniqueid int attempt id
* @param int $questionid int question id
- * @return boolean to indicate access granted or denied
+ * @return bool to indicate access granted or denied
*/
function quiz_check_file_access($attemptuniqueid, $questionid, $context = null) {
global $USER, $DB, $CFG;
@@ -1471,9 +1328,10 @@ function quiz_check_file_access($attemptuniqueid, $questionid, $context = null)
// access granted if the current user submitted this file
if ($attempt->userid != $USER->id) {
return false;
- // access granted if the current user has permission to grade quizzes in this course
}
- if (!(has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context))) {
+ // access granted if the current user has permission to grade quizzes in this course
+ if (!(has_capability('mod/quiz:viewreports', $context) ||
+ has_capability('mod/quiz:grade', $context))) {
return false;
}
@@ -1482,15 +1340,12 @@ function quiz_check_file_access($attemptuniqueid, $questionid, $context = null)
/**
* Prints quiz summaries on MyMoodle Page
- *
- * @global object
- * @global object
* @param arry $courses
* @param array $htmlarray
*/
function quiz_print_overview($courses, &$htmlarray) {
global $USER, $CFG;
-/// These next 6 Lines are constant in all modules (just change module name)
+ // These next 6 Lines are constant in all modules (just change module name)
if (empty($courses) || !is_array($courses) || count($courses) == 0) {
return array();
}
@@ -1499,43 +1354,50 @@ function quiz_print_overview($courses, &$htmlarray) {
return;
}
-/// Fetch some language strings outside the main loop.
+ // Fetch some language strings outside the main loop.
$strquiz = get_string('modulename', 'quiz');
$strnoattempts = get_string('noattempts', 'quiz');
-/// We want to list quizzes that are currently available, and which have a close date.
-/// This is the same as what the lesson does, and the dabate is in MDL-10568.
+ // We want to list quizzes that are currently available, and which have a close date.
+ // This is the same as what the lesson does, and the dabate is in MDL-10568.
$now = time();
foreach ($quizzes as $quiz) {
if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
- /// Give a link to the quiz, and the deadline.
+ // Give a link to the quiz, and the deadline.
$str = '' .
- '
';
if (empty($htmlarray[$quiz->course]['quiz'])) {
$htmlarray[$quiz->course]['quiz'] = $str;
@@ -1547,12 +1409,14 @@ function quiz_print_overview($courses, &$htmlarray) {
}
/**
- * Return a textual summary of the number of attemtps that have been made at a particular quiz,
- * returns '' if no attemtps have been made yet, unless $returnzero is passed as true.
+ * Return a textual summary of the number of attempts that have been made at a particular quiz,
+ * returns '' if no attempts have been made yet, unless $returnzero is passed as true.
*
* @param object $quiz the quiz object. Only $quiz->id is used at the moment.
- * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
- * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
+ * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
+ * $cm->groupingid fields are used at the moment.
+ * @param bool $returnzero if false (default), when no attempts have been
+ * made '' is returned instead of 'Attempts: 0'.
* @param int $currentgroup if there is a concept of current group where this method is being called
* (e.g. a report) pass it in here. Default 0 which means no current group.
* @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
@@ -1568,7 +1432,8 @@ function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup
$a->group = $DB->count_records_sql('SELECT count(1) FROM ' .
'{quiz_attempts} qa JOIN ' .
'{groups_members} gm ON qa.userid = gm.userid ' .
- 'WHERE quiz = ? AND preview = 0 AND groupid = ?', array($quiz->id, $currentgroup));
+ 'WHERE quiz = ? AND preview = 0 AND groupid = ?',
+ array($quiz->id, $currentgroup));
return get_string('attemptsnumthisgroup', 'quiz', $a);
} else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) {
list($usql, $params) = $DB->get_in_or_equal(array_keys($groups));
@@ -1590,14 +1455,17 @@ function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup
* to the quiz reports.
*
* @param object $quiz the quiz object. Only $quiz->id is used at the moment.
- * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
+ * @param object $cm the cm object. Only $cm->course, $cm->groupmode and
+ * $cm->groupingid fields are used at the moment.
* @param object $context the quiz context.
- * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
+ * @param bool $returnzero if false (default), when no attempts have been made
+ * '' is returned instead of 'Attempts: 0'.
* @param int $currentgroup if there is a concept of current group where this method is being called
* (e.g. a report) pass it in here. Default 0 which means no current group.
* @return string HTML fragment for the link.
*/
-function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false, $currentgroup = 0) {
+function quiz_attempt_summary_link_to_reports($quiz, $cm, $context, $returnzero = false,
+ $currentgroup = 0) {
global $CFG;
$summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
if (!$summary) {
@@ -1630,8 +1498,6 @@ function quiz_supports($feature) {
}
/**
- * @global object
- * @global stdClass
* @return array all other caps used in module
*/
function quiz_get_extra_capabilities() {
@@ -1649,9 +1515,9 @@ function quiz_get_extra_capabilities() {
* available
*
* @param navigation_node $quiznode The quiz node within the global navigation
- * @param stdClass $course The course object returned from the DB
- * @param stdClass $module The module object returned from the DB
- * @param stdClass $cm The course module instance returned from the DB
+ * @param object $course The course object returned from the DB
+ * @param object $module The module object returned from the DB
+ * @param object $cm The course module instance returned from the DB
*/
function quiz_extend_navigation($quiznode, $course, $module, $cm) {
global $CFG;
@@ -1664,17 +1530,21 @@ function quiz_extend_navigation($quiznode, $course, $module, $cm) {
null, null, new pix_icon('i/info', ''));
}
- if (has_capability('mod/quiz:viewreports', $context)) {
+ if (has_any_capability(array('mod/quiz:viewreports', 'mod/quiz:grade'), $context)) {
require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
$reportlist = quiz_report_list($context);
- $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => reset($reportlist)));
- $reportnode = $quiznode->add(get_string('results', 'quiz'), $url, navigation_node::TYPE_SETTING,
+ $url = new moodle_url('/mod/quiz/report.php',
+ array('id' => $cm->id, 'mode' => reset($reportlist)));
+ $reportnode = $quiznode->add(get_string('results', 'quiz'), $url,
+ navigation_node::TYPE_SETTING,
null, null, new pix_icon('i/report', ''));
foreach ($reportlist as $report) {
- $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => $report));
- $reportnode->add(get_string($report, 'quiz_'.$report), $url, navigation_node::TYPE_SETTING,
+ $url = new moodle_url('/mod/quiz/report.php',
+ array('id' => $cm->id, 'mode' => $report));
+ $reportnode->add(get_string($report, 'quiz_'.$report), $url,
+ navigation_node::TYPE_SETTING,
null, 'quiz_report_' . $report, new pix_icon('i/item', ''));
}
}
@@ -1700,9 +1570,11 @@ function quiz_extend_settings_navigation($settings, $quiznode) {
if (has_capability('mod/quiz:manageoverrides', $PAGE->cm->context)) {
$url = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$PAGE->cm->id));
- $quiznode->add(get_string('groupoverrides', 'quiz'), new moodle_url($url, array('mode'=>'group')),
+ $quiznode->add(get_string('groupoverrides', 'quiz'),
+ new moodle_url($url, array('mode'=>'group')),
navigation_node::TYPE_SETTING, null, 'groupoverrides');
- $quiznode->add(get_string('useroverrides', 'quiz'), new moodle_url($url, array('mode'=>'user')),
+ $quiznode->add(get_string('useroverrides', 'quiz'),
+ new moodle_url($url, array('mode'=>'user')),
navigation_node::TYPE_SETTING, null, 'useroverrides');
}
@@ -1714,7 +1586,8 @@ function quiz_extend_settings_navigation($settings, $quiznode) {
}
if (has_capability('mod/quiz:preview', $PAGE->cm->context)) {
- $url = new moodle_url('/mod/quiz/startattempt.php', array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
+ $url = new moodle_url('/mod/quiz/startattempt.php',
+ array('cmid'=>$PAGE->cm->id, 'sesskey'=>sesskey()));
$quiznode->add(get_string('preview', 'quiz'), $url, navigation_node::TYPE_SETTING,
null, 'mod_quiz_preview', new pix_icon('t/preview', ''));
}
@@ -1778,16 +1651,13 @@ function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownloa
* @param bool $forcedownload whether the user must be forced to download the file.
* @return bool false if file not found, does not return if found - justsend the file
*/
-function quiz_question_pluginfile($course, $context, $component,
- $filearea, $uniqueid, $questionid, $args, $forcedownload) {
+function mod_quiz_question_pluginfile($course, $context, $component,
+ $filearea, $qubaid, $slot, $args, $forcedownload) {
global $USER, $CFG;
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
- $attemptobj = quiz_attempt::create_from_unique_id($uniqueid);
+ $attemptobj = quiz_attempt::create_from_usage_id($qubaid);
require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
- $questionids = array($questionid);
- $attemptobj->load_questions($questionids);
- $attemptobj->load_question_states($questionids);
if ($attemptobj->is_own_attempt() && !$attemptobj->is_finished()) {
// In the middle of an attempt.
@@ -1802,7 +1672,7 @@ function quiz_question_pluginfile($course, $context, $component,
$isreviewing = true;
}
- if (!$attemptobj->check_file_access($questionid, $isreviewing, $context->id,
+ if (!$attemptobj->check_file_access($slot, $isreviewing, $context->id,
$component, $filearea, $args, $forcedownload)) {
send_file_not_found();
}
diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php
index b66c4260b70..f9f57c989a8 100644
--- a/mod/quiz/locallib.php
+++ b/mod/quiz/locallib.php
@@ -1,27 +1,18 @@
.
/**
* Library of functions used by the quiz module.
@@ -32,34 +23,32 @@
* the module-indpendent code for handling questions and which in turn
* initialises all the questiontype classes.
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
-}
-/**
- * Include those library functions that are also used by core Moodle or other modules
- */
+defined('MOODLE_INTERNAL') || die();
+
require_once($CFG->dirroot . '/mod/quiz/lib.php');
require_once($CFG->dirroot . '/mod/quiz/accessrules.php');
+require_once($CFG->dirroot . '/mod/quiz/renderer.php');
require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
require_once($CFG->dirroot . '/question/editlib.php');
require_once($CFG->libdir . '/eventslib.php');
require_once($CFG->libdir . '/filelib.php');
-/// Constants ///////////////////////////////////////////////////////////////////
/**#@+
- * Constants to describe the various states a quiz attempt can be in.
+ * Options determining how the grades from individual attempts are combined to give
+ * the overall grade for a user
*/
-define('QUIZ_STATE_DURING', 'during');
-define('QUIZ_STATE_IMMEDIATELY', 'immedately');
-define('QUIZ_STATE_OPEN', 'open');
-define('QUIZ_STATE_CLOSED', 'closed');
-define('QUIZ_STATE_TEACHERACCESS', 'teacheraccess'); // State only relevant if you are in a studenty role.
+define('QUIZ_GRADEHIGHEST', '1');
+define('QUIZ_GRADEAVERAGE', '2');
+define('QUIZ_ATTEMPTFIRST', '3');
+define('QUIZ_ATTEMPTLAST', '4');
/**#@-*/
/**
@@ -78,11 +67,11 @@ define('QUIZ_SHOW_TIME_BEFORE_DEADLINE', '3600');
* NOT written to the database.
*
* @param object $quiz the quiz to create an attempt for.
- * @param integer $attemptnumber the sequence number for the attempt.
+ * @param int $attemptnumber the sequence number for the attempt.
* @param object $lastattempt the previous attempt by this user, if any. Only needed
* if $attemptnumber > 1 and $quiz->attemptonlast is true.
- * @param integer $timenow the time the attempt was started at.
- * @param boolean $ispreview whether this new attempt is a preview.
+ * @param int $timenow the time the attempt was started at.
+ * @param bool $ispreview whether this new attempt is a preview.
*
* @return object the newly created attempt object.
*/
@@ -90,18 +79,19 @@ function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $isp
global $USER;
if ($attemptnumber == 1 || !$quiz->attemptonlast) {
- /// We are not building on last attempt so create a new attempt.
- $attempt = new stdClass;
+ // We are not building on last attempt so create a new attempt.
+ $attempt = new stdClass();
$attempt->quiz = $quiz->id;
$attempt->userid = $USER->id;
$attempt->preview = 0;
if ($quiz->shufflequestions) {
- $attempt->layout = quiz_clean_layout(quiz_repaginate($quiz->questions, $quiz->questionsperpage, true),true);
+ $attempt->layout = quiz_clean_layout(quiz_repaginate(
+ $quiz->questions, $quiz->questionsperpage, true), true);
} else {
- $attempt->layout = quiz_clean_layout($quiz->questions,true);
+ $attempt->layout = quiz_clean_layout($quiz->questions, true);
}
} else {
- /// Build on last attempt.
+ // Build on last attempt.
if (empty($lastattempt)) {
print_error('cannotfindprevattempt', 'quiz');
}
@@ -109,13 +99,11 @@ function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $isp
}
$attempt->attempt = $attemptnumber;
- $attempt->sumgrades = 0.0;
$attempt->timestart = $timenow;
$attempt->timefinish = 0;
$attempt->timemodified = $timenow;
- $attempt->uniqueid = question_new_attempt_uniqueid();
-/// If this is a preview, mark it as such.
+ // If this is a preview, mark it as such.
if ($ispreview) {
$attempt->preview = 1;
}
@@ -124,11 +112,11 @@ function quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $isp
}
/**
- * Returns the unfinished attempt for the given
- * user on the given quiz, if there is one.
+ * Returns an unfinished attempt (if there is one) for the given
+ * user on the given quiz. This function does not return preview attempts.
*
- * @param integer $quizid the id of the quiz.
- * @param integer $userid the id of the user.
+ * @param int $quizid the id of the quiz.
+ * @param int $userid the id of the user.
*
* @return mixed the unfinished attempt if there is one, false if not.
*/
@@ -145,15 +133,19 @@ function quiz_get_user_attempt_unfinished($quizid, $userid) {
* Returns the most recent attempt by a given user on a given quiz.
* May be finished, or may not.
*
- * @param integer $quizid the id of the quiz.
- * @param integer $userid the id of the user.
+ * @param int $quizid the id of the quiz.
+ * @param int $userid the id of the user.
*
* @return mixed the attempt if there is one, false if not.
*/
function quiz_get_latest_attempt_by_user($quizid, $userid) {
global $CFG, $DB;
- $attempt = $DB->get_records_sql('SELECT qa.* FROM {quiz_attempts} qa
- WHERE qa.quiz=? AND qa.userid= ? ORDER BY qa.timestart DESC, qa.id DESC', array($quizid, $userid), 0, 1);
+ $attempt = $DB->get_records_sql('
+ SELECT qa.*
+ FROM {quiz_attempts} qa
+ WHERE qa.quiz = ? AND qa.userid = ?
+ ORDER BY qa.timestart DESC, qa.id DESC',
+ array($quizid, $userid), 0, 1);
if ($attempt) {
return array_shift($attempt);
} else {
@@ -161,31 +153,10 @@ function quiz_get_latest_attempt_by_user($quizid, $userid) {
}
}
-/**
- * Load an attempt by id. You need to use this method instead of $DB->get_record, because
- * of some ancient history to do with the upgrade from Moodle 1.4 to 1.5, See the comment
- * after CREATE TABLE `prefix_quiz_newest_states` in mod/quiz/db/mysql.php.
- *
- * @param integer $attemptid the id of the attempt to load.
- */
-function quiz_load_attempt($attemptid) {
- global $DB;
- $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid));
- if (!$attempt) {
- return false;
- }
-
- if (!$DB->record_exists('question_sessions', array('attemptid' => $attempt->uniqueid))) {
- /// this attempt has not yet been upgraded to the new model
- quiz_upgrade_states($attempt);
- }
-
- return $attempt;
-}
-
/**
* Delete a quiz attempt.
- * @param mixed $attempt an integer attempt id or an attempt object (row of the quiz_attempts table).
+ * @param mixed $attempt an integer attempt id or an attempt object
+ * (row of the quiz_attempts table).
* @param object $quiz the quiz object.
*/
function quiz_delete_attempt($attempt, $quiz) {
@@ -202,16 +173,15 @@ function quiz_delete_attempt($attempt, $quiz) {
return;
}
+ question_engine::delete_questions_usage_by_activity($attempt->uniqueid);
$DB->delete_records('quiz_attempts', array('id' => $attempt->id));
- delete_attempt($attempt->uniqueid);
// Search quiz_attempts for other instances by this user.
// If none, then delete record for this quiz, this user from quiz_grades
// else recalculate best grade
-
$userid = $attempt->userid;
if (!$DB->record_exists('quiz_attempts', array('userid' => $userid, 'quiz' => $quiz->id))) {
- $DB->delete_records('quiz_grades', array('userid' => $userid,'quiz' => $quiz->id));
+ $DB->delete_records('quiz_grades', array('userid' => $userid, 'quiz' => $quiz->id));
} else {
quiz_save_best_grade($quiz, $userid);
}
@@ -223,7 +193,7 @@ function quiz_delete_attempt($attempt, $quiz) {
* Delete all the preview attempts at a quiz, or possibly all the attempts belonging
* to one user.
* @param object $quiz the quiz object.
- * @param integer $userid (optional) if given, only delete the previews belonging to this user.
+ * @param int $userid (optional) if given, only delete the previews belonging to this user.
*/
function quiz_delete_previews($quiz, $userid = null) {
global $DB;
@@ -238,8 +208,8 @@ function quiz_delete_previews($quiz, $userid = null) {
}
/**
- * @param integer $quizid The quiz id.
- * @return boolean whether this quiz has any (non-preview) attempts.
+ * @param int $quizid The quiz id.
+ * @return bool whether this quiz has any (non-preview) attempts.
*/
function quiz_has_attempts($quizid) {
global $DB;
@@ -248,20 +218,6 @@ function quiz_has_attempts($quizid) {
/// Functions to do with quiz layout and pages ////////////////////////////////
-/**
- * Returns a comma separated list of question ids for the current page
- *
- * @param string $layout the string representing the quiz layout. Each page is represented as a
- * comma separated list of question ids and 0 indicating page breaks.
- * So 5,2,0,3,0 means questions 5 and 2 on page 1 and question 3 on page 2
- * @param integer $page the number of the current page.
- * @return string comma separated list of question ids
- */
-function quiz_questions_on_page($layout, $page) {
- $pages = explode(',0', $layout);
- return trim($pages[$page], ',');
-}
-
/**
* Returns a comma separated list of question ids for the quiz
*
@@ -272,34 +228,29 @@ function quiz_questions_on_page($layout, $page) {
* @return string comma separated list of question ids, without page breaks.
*/
function quiz_questions_in_quiz($layout) {
- $layout = preg_replace('/,(0+,)+/', ',', $layout); // Remove page breaks from the middle.
- $layout = preg_replace('/^0+,/', '', $layout); // And from the start.
- $layout = preg_replace('/(^|,)0+$/', '', $layout); // And from the end.
- return $layout;
+ $questions = str_replace(',0', '', quiz_clean_layout($layout, true));
+ if ($questions === '0') {
+ return '';
+ } else {
+ return $questions;
+ }
}
/**
* Returns the number of pages in a quiz layout
*
* @param string $layout The string representing the quiz layout. Always ends in ,0
- * @return integer The number of pages in the quiz.
+ * @return int The number of pages in the quiz.
*/
function quiz_number_of_pages($layout) {
- $count = 0;
- if ($layout !== '') {
- //if the first page is empty, include it, too
- if (strcmp($layout[0], '0') === 0) {
- $count++;
- }
- $count += substr_count($layout, ',0');
- }
- return $count;
+ return substr_count(',' . $layout, ',0');
}
+
/**
* Returns the number of questions in the quiz layout
*
* @param string $layout the string representing the quiz layout.
- * @return integer The number of questions in the quiz.
+ * @return int The number of questions in the quiz.
*/
function quiz_number_of_questions_in_quiz($layout) {
$layout = quiz_questions_in_quiz(quiz_clean_layout($layout));
@@ -310,43 +261,17 @@ function quiz_number_of_questions_in_quiz($layout) {
return $count;
}
-/**
- * Returns the first question number for the current quiz page
- *
- * @param string $quizlayout The string representing the layout for the whole quiz
- * @param string $pagelayout The string representing the layout for the current page
- * @return integer the number of the first question
- */
-function quiz_first_questionnumber($quizlayout, $pagelayout) {
- // this works by finding all the questions from the quizlayout that
- // come before the current page and then adding up their lengths.
- global $CFG, $DB;
- $start = strpos($quizlayout, ','.$pagelayout.',')-2;
- if ($start > 0) {
- $prevlist = substr($quizlayout, 0, $start);
- list($usql, $params) = $DB->get_in_or_equal(explode(',', $prevlist));
- return $DB->get_field_sql("SELECT sum(length)+1 FROM {question}
- WHERE id $usql", $params);
- } else {
- return 1;
- }
-}
-
/**
* Re-paginates the quiz layout
*
* @param string $layout The string representing the quiz layout.
- * @param integer $perpage The number of questions per page
- * @param boolean $shuffle Should the questions be reordered randomly?
+ * @param int $perpage The number of questions per page
+ * @param bool $shuffle Should the questions be reordered randomly?
* @return string the new layout string
*/
function quiz_repaginate($layout, $perpage, $shuffle = false) {
$layout = str_replace(',0', '', $layout); // remove existing page breaks
$questions = explode(',', $layout);
- //remove empty pages from beginning
- while (reset($questions) == '0') {
- array_shift($questions);
- }
if ($shuffle) {
shuffle($questions);
}
@@ -367,10 +292,10 @@ function quiz_repaginate($layout, $perpage, $shuffle = false) {
/**
* Creates an array of maximum grades for a quiz
- * The grades are extracted from the quiz_question_instances table.
*
- * @param integer $quiz The quiz object
- * @return array Array of grades indexed by question id. These are the maximum
+ * The grades are extracted from the quiz_question_instances table.
+ * @param object $quiz The quiz settings.
+ * @return array of grades indexed by question id. These are the maximum
* possible grades that students can achieve for each of the questions.
*/
function quiz_get_all_question_grades($quiz) {
@@ -389,7 +314,7 @@ function quiz_get_all_question_grades($quiz) {
$params = array_merge($params, $question_params);
}
- $instances = $DB->get_records_sql("SELECT question,grade,id
+ $instances = $DB->get_records_sql("SELECT question, grade, id
FROM {quiz_question_instances}
WHERE quiz = ? $wheresql", $params);
@@ -406,46 +331,30 @@ function quiz_get_all_question_grades($quiz) {
return $grades;
}
-/**
- * Update the sumgrades field of the quiz. This needs to be called whenever
- * the grading structure of the quiz is changed. For example if a question is
- * added or removed, or a question weight is changed.
- *
- * @param object $quiz a quiz.
- */
-function quiz_update_sumgrades($quiz) {
- global $DB;
- $grades = quiz_get_all_question_grades($quiz);
- $sumgrades = 0;
- foreach ($grades as $grade) {
- $sumgrades += $grade;
- }
- if (!isset($quiz->sumgrades) || $quiz->sumgrades != $sumgrades) {
- $DB->set_field('quiz', 'sumgrades', $sumgrades, array('id' => $quiz->id));
- $quiz->sumgrades = $sumgrades;
- }
-}
-
/**
* Convert the raw grade stored in $attempt into a grade out of the maximum
* grade for this quiz.
*
* @param float $rawgrade the unadjusted grade, fof example $attempt->sumgrades
- * @param object $quiz the quiz object. Only the fields grade, sumgrades, decimalpoints and questiondecimalpoints are used.
- * @param mixed $round false = don't round, true = round using quiz_format_grade, 'question' = round using quiz_format_question_grade.
- * @return float the rescaled grade.
+ * @param object $quiz the quiz object. Only the fields grade, sumgrades and decimalpoints are used.
+ * @param bool|string $format whether to format the results for display
+ * or 'question' to format a question grade (different number of decimal places.
+ * @return float|string the rescaled grade, or null/the lang string 'notyetgraded'
+ * if the $grade is null.
*/
-function quiz_rescale_grade($rawgrade, $quiz, $round = true) {
- if ($quiz->sumgrades != 0) {
+function quiz_rescale_grade($rawgrade, $quiz, $format = true) {
+ if (is_null($rawgrade)) {
+ $grade = null;
+ } else if ($quiz->sumgrades >= 0.000005) {
$grade = $rawgrade * $quiz->grade / $quiz->sumgrades;
- if ($round === 'question') { // === really necessary here true == 'question' is true in PHP!
- $grade = quiz_format_question_grade($quiz, $grade);
- } else if ($round) {
- $grade = quiz_format_grade($quiz, $grade);
- }
} else {
$grade = 0;
}
+ if ($format === 'question') {
+ $grade = quiz_format_question_grade($quiz, $grade);
+ } else if ($format) {
+ $grade = quiz_format_grade($quiz, $grade);
+ }
return $grade;
}
@@ -454,22 +363,29 @@ function quiz_rescale_grade($rawgrade, $quiz, $round = true) {
* got this grade on this quiz. The feedback is processed ready for diplay.
*
* @param float $grade a grade on this quiz.
- * @param integer $quizid the id of the quiz object.
+ * @param object $quiz the quiz settings.
+ * @param object $context the quiz context.
* @return string the comment that corresponds to this grade (empty string if there is not one.
*/
-function quiz_feedback_for_grade($grade, $quiz, $context, $cm=null) {
+function quiz_feedback_for_grade($grade, $quiz, $context) {
global $DB;
- $feedback = $DB->get_record_select('quiz_feedback', "quizid = ? AND mingrade <= ? AND $grade < maxgrade", array($quiz->id, $grade));
+ if (is_null($grade)) {
+ return '';
+ }
+
+ $feedback = $DB->get_record_select('quiz_feedback',
+ 'quizid = ? AND mingrade <= ? AND ? < maxgrade', array($quiz->id, $grade, $grade));
if (empty($feedback->feedbacktext)) {
return '';
}
// Clean the text, ready for display.
- $formatoptions = new stdClass;
+ $formatoptions = new stdClass();
$formatoptions->noclean = true;
- $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php', $context->id, 'mod_quiz', 'feedback', $feedback->id);
+ $feedbacktext = file_rewrite_pluginfile_urls($feedback->feedbacktext, 'pluginfile.php',
+ $context->id, 'mod_quiz', 'feedback', $feedback->id);
$feedbacktext = format_text($feedbacktext, $feedback->feedbacktextformat, $formatoptions);
return $feedbacktext;
@@ -477,7 +393,7 @@ function quiz_feedback_for_grade($grade, $quiz, $context, $cm=null) {
/**
* @param object $quiz the quiz database row.
- * @return boolean Whether this quiz has any non-blank feedback text.
+ * @return bool Whether this quiz has any non-blank feedback text.
*/
function quiz_has_feedback($quiz) {
global $DB;
@@ -491,16 +407,70 @@ function quiz_has_feedback($quiz) {
return $cache[$quiz->id];
}
+function quiz_no_questions_message($quiz, $cm, $context) {
+ global $OUTPUT;
+
+ $output = '';
+ $output .= $OUTPUT->notification(get_string('noquestions', 'quiz'));
+ if (has_capability('mod/quiz:manage', $context)) {
+ $output .= $OUTPUT->single_button(new moodle_url('/mod/quiz/edit.php',
+ array('cmid' => $cm->id)), get_string('editquiz', 'quiz'), 'get');
+ }
+
+ return $output;
+}
+
/**
- * The quiz grade is the score that student's results are marked out of. When it
+ * Update the sumgrades field of the quiz. This needs to be called whenever
+ * the grading structure of the quiz is changed. For example if a question is
+ * added or removed, or a question weight is changed.
+ *
+ * @param object $quiz a quiz.
+ */
+function quiz_update_sumgrades($quiz) {
+ global $DB;
+ $sql = 'UPDATE {quiz}
+ SET sumgrades = COALESCE((
+ SELECT SUM(grade)
+ FROM {quiz_question_instances}
+ WHERE quiz = {quiz}.id
+ ), 0)
+ WHERE id = ?';
+ $DB->execute($sql, array($quiz->id));
+ $quiz->sumgrades = $DB->get_field('quiz', 'sumgrades', array('id' => $quiz->id));
+ if ($quiz->sumgrades < 0.000005) {
+ quiz_set_grade(0, $quiz);
+ }
+}
+
+function quiz_update_all_attempt_sumgrades($quiz) {
+ global $DB;
+ $dm = new question_engine_data_mapper();
+ $timenow = time();
+
+ $sql = "UPDATE {quiz_attempts}
+ SET
+ timemodified = :timenow,
+ sumgrades = (
+ {$dm->sum_usage_marks_subquery('uniqueid')}
+ )
+ WHERE quiz = :quizid AND timefinish <> 0";
+ $DB->execute($sql, array('timenow' => $timenow, 'quizid' => $quiz->id));
+}
+
+/**
+ * The quiz grade is the maximum that student's results are marked out of. When it
* changes, the corresponding data in quiz_grades and quiz_feedback needs to be
- * rescaled.
+ * rescaled. After calling this function, you probably need to call
+ * quiz_update_all_attempt_sumgrades, quiz_update_all_final_grades and
+ * quiz_update_grades.
*
* @param float $newgrade the new maximum grade for the quiz.
- * @param object $quiz the quiz we are updating. Passed by reference so its grade field can be updated too.
- * @return boolean indicating success or failure. TODO: MDL-20625
+ * @param object $quiz the quiz we are updating. Passed by reference so its
+ * grade field can be updated too.
+ * @return bool indicating success or failure.
*/
-function quiz_set_grade($newgrade, &$quiz) {
+function quiz_set_grade($newgrade, $quiz) {
global $DB;
// This is potentially expensive, so only do it if necessary.
if (abs($quiz->grade - $newgrade) < 1e-7) {
@@ -511,55 +481,49 @@ function quiz_set_grade($newgrade, &$quiz) {
// Use a transaction, so that on those databases that support it, this is safer.
$transaction = $DB->start_delegated_transaction();
- try {
- // Update the quiz table.
- $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance));
+ // Update the quiz table.
+ $DB->set_field('quiz', 'grade', $newgrade, array('id' => $quiz->instance));
- // Rescaling the other data is only possible if the old grade was non-zero.
- if ($quiz->grade > 1e-7) {
- global $CFG;
+ // Rescaling the other data is only possible if the old grade was non-zero.
+ if ($quiz->grade > 1e-7) {
+ global $CFG;
- $factor = $newgrade/$quiz->grade;
- $quiz->grade = $newgrade;
+ $factor = $newgrade/$quiz->grade;
+ $quiz->grade = $newgrade;
- // Update the quiz_grades table.
- $timemodified = time();
- $DB->execute("
- UPDATE {quiz_grades}
- SET grade = ? * grade, timemodified = ?
- WHERE quiz = ?
- ", array($factor, $timemodified, $quiz->id));
+ // Update the quiz_grades table.
+ $timemodified = time();
+ $DB->execute("
+ UPDATE {quiz_grades}
+ SET grade = ? * grade, timemodified = ?
+ WHERE quiz = ?
+ ", array($factor, $timemodified, $quiz->id));
- // Update the quiz_feedback table.
- $DB->execute("
- UPDATE {quiz_feedback}
- SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
- WHERE quizid = ?
- ", array($factor, $factor, $quiz->id));
- }
-
- // update grade item and send all grades to gradebook
- quiz_grade_item_update($quiz);
- quiz_update_grades($quiz);
-
- $transaction->allow_commit();
- return true;
-
- } catch (Exception $e) {
- //TODO: MDL-20625 this part was returning false, but now throws exception
- $transaction->rollback($e);
+ // Update the quiz_feedback table.
+ $DB->execute("
+ UPDATE {quiz_feedback}
+ SET mingrade = ? * mingrade, maxgrade = ? * maxgrade
+ WHERE quizid = ?
+ ", array($factor, $factor, $quiz->id));
}
+
+ // update grade item and send all grades to gradebook
+ quiz_grade_item_update($quiz);
+ quiz_update_grades($quiz);
+
+ $transaction->allow_commit();
+ return true;
}
/**
* Save the overall grade for a user at a quiz in the quiz_grades table
*
* @param object $quiz The quiz for which the best grade is to be calculated and then saved.
- * @param integer $userid The userid to calculate the grade for. Defaults to the current user.
+ * @param int $userid The userid to calculate the grade for. Defaults to the current user.
* @param array $attempts The attempts of this user. Useful if you are
* looping through many users. Attempts can be fetched in one master query to
* avoid repeated querying.
- * @return boolean Indicates success or failure.
+ * @return bool Indicates success or failure.
*/
function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
global $DB;
@@ -569,12 +533,9 @@ function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
$userid = $USER->id;
}
- if (!$attempts){
+ if (!$attempts) {
// Get all the attempts made by the user
- if (!$attempts = quiz_get_user_attempts($quiz->id, $userid)) {
- echo $OUTPUT->notification('Could not find any user attempts');
- return false;
- }
+ $attempts = quiz_get_user_attempts($quiz->id, $userid);
}
// Calculate the best grade
@@ -582,10 +543,15 @@ function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
$bestgrade = quiz_rescale_grade($bestgrade, $quiz, false);
// Save the best grade in the database
- if ($grade = $DB->get_record('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid))) {
+ if (is_null($bestgrade)) {
+ $DB->delete_records('quiz_grades', array('quiz' => $quiz->id, 'userid' => $userid));
+
+ } else if ($grade = $DB->get_record('quiz_grades',
+ array('quiz' => $quiz->id, 'userid' => $userid))) {
$grade->grade = $bestgrade;
$grade->timemodified = time();
$DB->update_record('quiz_grades', $grade);
+
} else {
$grade->quiz = $quiz->id;
$grade->userid = $userid;
@@ -595,7 +561,6 @@ function quiz_save_best_grade($quiz, $userid = null, $attempts = array()) {
}
quiz_update_grades($quiz, $userid);
- return true;
}
/**
@@ -613,7 +578,7 @@ function quiz_calculate_best_grade($quiz, $attempts) {
foreach ($attempts as $attempt) {
return $attempt->sumgrades;
}
- break;
+ return $final;
case QUIZ_ATTEMPTLAST:
foreach ($attempts as $attempt) {
@@ -625,14 +590,19 @@ function quiz_calculate_best_grade($quiz, $attempts) {
$sum = 0;
$count = 0;
foreach ($attempts as $attempt) {
- $sum += $attempt->sumgrades;
- $count++;
+ if (!is_null($attempt->sumgrades)) {
+ $sum += $attempt->sumgrades;
+ $count++;
+ }
}
- return (float)$sum/$count;
+ if ($count == 0) {
+ return null;
+ }
+ return $sum / $count;
default:
case QUIZ_GRADEHIGHEST:
- $max = 0;
+ $max = null;
foreach ($attempts as $attempt) {
if ($attempt->sumgrades > $max) {
$max = $attempt->sumgrades;
@@ -642,6 +612,147 @@ function quiz_calculate_best_grade($quiz, $attempts) {
}
}
+/**
+ * Update the final grade at this quiz for all students.
+ *
+ * This function is equivalent to calling quiz_save_best_grade for all
+ * users, but much more efficient.
+ *
+ * @param object $quiz the quiz settings.
+ */
+function quiz_update_all_final_grades($quiz) {
+ global $DB;
+
+ if (!$quiz->sumgrades) {
+ return;
+ }
+
+ $param = array('iquizid' => $quiz->id);
+ $firstlastattemptjoin = "JOIN (
+ SELECT
+ iquiza.userid,
+ MIN(attempt) AS firstattempt,
+ MAX(attempt) AS lastattempt
+
+ FROM {quiz_attempts iquiza}
+
+ WHERE
+ iquiza.timefinish <> 0 AND
+ iquiza.preview = 0 AND
+ iquiza.quiz = :iquizid
+
+ GROUP BY iquiza.userid
+ ) first_last_attempts ON first_last_attempts.userid = quiza.userid";
+
+ switch ($quiz->grademethod) {
+ case QUIZ_ATTEMPTFIRST:
+ // Becuase of the where clause, there will only be one row, but we
+ // must still use an aggregate function.
+ $select = 'MAX(quiza.sumgrades)';
+ $join = $firstlastattemptjoin;
+ $where = 'quiza.attempt = first_last_attempts.firstattempt AND';
+ break;
+
+ case QUIZ_ATTEMPTLAST:
+ // Becuase of the where clause, there will only be one row, but we
+ // must still use an aggregate function.
+ $select = 'MAX(quiza.sumgrades)';
+ $join = $firstlastattemptjoin;
+ $where = 'quiza.attempt = first_last_attempts.lastattempt AND';
+ break;
+
+ case QUIZ_GRADEAVERAGE:
+ $select = 'AVG(quiza.sumgrades)';
+ $join = '';
+ $where = '';
+ break;
+
+ default:
+ case QUIZ_GRADEHIGHEST:
+ $select = 'MAX(quiza.sumgrades)';
+ $join = '';
+ $where = '';
+ break;
+ }
+
+ if ($quiz->sumgrades >= 0.000005) {
+ $finalgrade = $select . ' * ' . ($quiz->grade / $quiz->sumgrades);
+ } else {
+ $finalgrade = '0';
+ }
+ $param['quizid'] = $quiz->id;
+ $param['quizid2'] = $quiz->id;
+ $param['quizid3'] = $quiz->id;
+ $param['quizid4'] = $quiz->id;
+ $finalgradesubquery = "
+ SELECT quiza.userid, $finalgrade AS newgrade
+ FROM {quiz_attempts} quiza
+ $join
+ WHERE
+ $where
+ quiza.timefinish <> 0 AND
+ quiza.preview = 0 AND
+ quiza.quiz = :quizid3
+ GROUP BY quiza.userid";
+
+ $changedgrades = $DB->get_records_sql("
+ SELECT users.userid, qg.id, qg.grade, newgrades.newgrade
+
+ FROM (
+ SELECT userid
+ FROM {quiz_grades} qg
+ WHERE quiz = :quizid
+ UNION
+ SELECT DISTINCT userid
+ FROM {quiz_attempts} quiza2
+ WHERE
+ quiza2.timefinish <> 0 AND
+ quiza2.preview = 0 AND
+ quiza2.quiz = :quizid2
+ ) users
+
+ LEFT JOIN {quiz_grades} qg ON qg.userid = users.userid AND qg.quiz = :quizid4
+
+ LEFT JOIN (
+ $finalgradesubquery
+ ) newgrades ON newgrades.userid = users.userid
+
+ WHERE
+ ABS(newgrades.newgrade - qg.grade) > 0.000005 OR
+ (newgrades.newgrade IS NULL) <> (qg.grade IS NULL)",
+ $param);
+
+ $timenow = time();
+ $todelete = array();
+ foreach ($changedgrades as $changedgrade) {
+
+ if (is_null($changedgrade->newgrade)) {
+ $todelete[] = $changedgrade->userid;
+
+ } else if (is_null($changedgrade->grade)) {
+ $toinsert = new stdClass();
+ $toinsert->quiz = $quiz->id;
+ $toinsert->userid = $changedgrade->userid;
+ $toinsert->timemodified = $timenow;
+ $toinsert->grade = $changedgrade->newgrade;
+ $DB->insert_record('quiz_grades', $toinsert);
+
+ } else {
+ $toupdate = new stdClass();
+ $toupdate->id = $changedgrade->id;
+ $toupdate->grade = $changedgrade->newgrade;
+ $toupdate->timemodified = $timenow;
+ $DB->update_record('quiz_grades', $toupdate);
+ }
+ }
+
+ if (!empty($todelete)) {
+ list($test, $params) = $DB->get_in_or_equal($todelete);
+ $DB->delete_records_select('quiz_grades', 'quiz = ? AND userid ' . $test,
+ array_merge(array($quiz->id), $params));
+ }
+}
+
/**
* Return the attempt with the best grade for a quiz
*
@@ -682,7 +793,20 @@ function quiz_calculate_best_attempt($quiz, $attempts) {
}
/**
- * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE, QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
+ * @return the options for calculating the quiz grade from the individual attempt grades.
+ */
+function quiz_get_grading_options() {
+ return array(
+ QUIZ_GRADEHIGHEST => get_string('gradehighest', 'quiz'),
+ QUIZ_GRADEAVERAGE => get_string('gradeaverage', 'quiz'),
+ QUIZ_ATTEMPTFIRST => get_string('attemptfirst', 'quiz'),
+ QUIZ_ATTEMPTLAST => get_string('attemptlast', 'quiz')
+ );
+}
+
+/**
+ * @param int $option one of the values QUIZ_GRADEHIGHEST, QUIZ_GRADEAVERAGE,
+ * QUIZ_ATTEMPTFIRST or QUIZ_ATTEMPTLAST.
* @return the lang string for that option.
*/
function quiz_get_grading_option_name($option) {
@@ -692,61 +816,9 @@ function quiz_get_grading_option_name($option) {
/// Other quiz functions ////////////////////////////////////////////////////
-/**
- * Parse field names used for the replace options on question edit forms
- */
-function quiz_parse_fieldname($name, $nameprefix='question') {
- $reg = array();
- if (preg_match("/$nameprefix(\\d+)(\w+)/", $name, $reg)) {
- return array('mode' => $reg[2], 'id' => (int)$reg[1]);
- } else {
- return false;
- }
-}
-
-/**
- * Upgrade states for an attempt to Moodle 1.5 model
- *
- * Any state that does not yet have its timestamp set to nonzero has not yet been upgraded from Moodle 1.4
- * The reason these are still around is that for large sites it would have taken too long to
- * upgrade all states at once. This function sets the timestamp field and creates an entry in the
- * question_sessions table.
- * @param object $attempt The attempt whose states need upgrading
- */
-function quiz_upgrade_states($attempt) {
- global $DB;
- global $CFG;
- // The old quiz model only allowed a single response per quiz attempt so that there will be
- // only one state record per question for this attempt.
-
- // We set the timestamp of all states to the timemodified field of the attempt.
- $DB->execute("UPDATE {question_states} SET timestamp = ? WHERE attempt = ?", array($attempt->timemodified, $attempt->uniqueid));
-
- // For each state we create an entry in the question_sessions table, with both newest and
- // newgraded pointing to this state.
- // Actually we only do this for states whose question is actually listed in $attempt->layout.
- // We do not do it for states associated to wrapped questions like for example the questions
- // used by a RANDOM question
- $session = new stdClass;
- $session->attemptid = $attempt->uniqueid;
- $questionlist = quiz_questions_in_quiz($attempt->layout);
- $params = array($attempt->uniqueid);
- list($usql, $question_params) = $DB->get_in_or_equal(explode(',',$questionlist));
- $params = array_merge($params, $question_params);
-
- if ($questionlist and $states = $DB->get_records_select('question_states', "attempt = ? AND question $usql", $params)) {
- foreach ($states as $state) {
- $session->newgraded = $state->id;
- $session->newest = $state->id;
- $session->questionid = $state->question;
- $DB->insert_record('question_sessions', $session, false);
- }
- }
-}
-
/**
* @param object $quiz the quiz.
- * @param integer $cmid the course_module object for this quiz.
+ * @param int $cmid the course_module object for this quiz.
* @param object $question the question.
* @param string $returnurl url to return to after action is done.
* @return string html for a number of icons linked to action pages for a
@@ -759,11 +831,12 @@ function quiz_question_action_icons($quiz, $cmid, $question, $returnurl) {
}
/**
- * @param integer $cmid the course_module.id for this quiz.
+ * @param int $cmid the course_module.id for this quiz.
* @param object $question the question.
* @param string $returnurl url to return to after action is done.
* @param string $contentbeforeicon some HTML content to be added inside the link, before the icon.
- * @return the HTML for an edit icon, view icon, or nothing for a question (depending on permissions).
+ * @return the HTML for an edit icon, view icon, or nothing for a question
+ * (depending on permissions).
*/
function quiz_question_edit_button($cmid, $question, $returnurl, $contentaftericon = '') {
global $CFG, $OUTPUT;
@@ -771,24 +844,29 @@ function quiz_question_edit_button($cmid, $question, $returnurl, $contentafteric
// Minor efficiency saving. Only get strings once, even if there are a lot of icons on one page.
static $stredit = null;
static $strview = null;
- if ($stredit === null){
+ if ($stredit === null) {
$stredit = get_string('edit');
$strview = get_string('view');
}
// What sort of icon should we show?
$action = '';
- if (question_has_capability_on($question, 'edit', $question->category) ||
- question_has_capability_on($question, 'move', $question->category)) {
+ if (!empty($question->id) &&
+ (question_has_capability_on($question, 'edit', $question->category) ||
+ question_has_capability_on($question, 'move', $question->category))) {
$action = $stredit;
$icon = '/t/edit';
- } else if (question_has_capability_on($question, 'view', $question->category)) {
+ } else if (!empty($question->id) &&
+ question_has_capability_on($question, 'view', $question->category)) {
$action = $strview;
$icon = '/i/info';
}
// Build the icon.
if ($action) {
+ if ($returnurl instanceof moodle_url) {
+ $returnurl = str_replace($CFG->wwwroot, '', $returnurl->out(false));
+ }
$questionparams = array('returnurl' => $returnurl, 'cmid' => $cmid, 'id' => $question->id);
$questionurl = new moodle_url("$CFG->wwwroot/question/question.php", $questionparams);
return '
pix_icon('t/preview', $strpreviewquestion);
- $link = new moodle_url($CFG->wwwroot."/question/preview.php?id=$question->id&quizid=$quiz->id");
- parse_str(QUESTION_PREVIEW_POPUP_OPTIONS, $options);
- $action = new popup_action('click', $link, 'questionpreview', $options);
+ $action = new popup_action('click', $url, 'questionpreview',
+ question_preview_popup_params());
- return $OUTPUT->action_link($link, $image, $action, array('title' => $strpreviewquestion));
+ return $OUTPUT->action_link($url, $image, $action, array('title' => $strpreviewquestion));
}
/**
* @param object $attempt the attempt.
* @param object $context the quiz context.
- * @return integer whether flags should be shown/editable to the current user for this attempt.
+ * @return int whether flags should be shown/editable to the current user for this attempt.
*/
function quiz_get_flag_option($attempt, $context) {
global $USER;
- static $flagmode = null;
- if (is_null($flagmode)) {
- if (!has_capability('moodle/question:flag', $context)) {
- $flagmode = QUESTION_FLAGSHIDDEN;
- } else if ($attempt->userid == $USER->id) {
- $flagmode = QUESTION_FLAGSEDITABLE;
- } else {
- $flagmode = QUESTION_FLAGSSHOWN;
- }
+ if (!has_capability('moodle/question:flag', $context)) {
+ return question_display_options::HIDDEN;
+ } else if ($attempt->userid == $USER->id) {
+ return question_display_options::EDITABLE;
+ } else {
+ return question_display_options::VISIBLE;
}
- return $flagmode;
}
/**
- * Determine render options
- *
- * @param int $reviewoptions
- * @param object $state
+ * Work out what state this quiz attempt is in.
+ * @param object $quiz the quiz settings
+ * @param object $attempt the quiz_attempt database row.
+ * @return int one of the mod_quiz_display_options::DURING,
+ * IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
*/
-function quiz_get_renderoptions($quiz, $attempt, $context, $state) {
- $reviewoptions = $quiz->review;
- $options = new stdClass;
-
- $options->flags = quiz_get_flag_option($attempt, $context);
-
- // Show the question in readonly (review) mode if the question is in
- // the closed state
- $options->readonly = question_state_is_closed($state);
-
- // Show feedback once the question has been graded (if allowed by the quiz)
- $options->feedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
-
- // Show correct responses in readonly mode if the quiz allows it
- $options->correct_responses = $options->readonly && ($reviewoptions & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
-
- // Show general feedback if the question has been graded and the quiz allows it.
- $options->generalfeedback = question_state_is_graded($state) && ($reviewoptions & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
-
- // Show overallfeedback once the attempt is over.
- $options->overallfeedback = false;
-
- // Always show responses and scores
- $options->responses = true;
- $options->scores = true;
- $options->quizstate = QUIZ_STATE_DURING;
- $options->history = false;
-
- return $options;
+function quiz_attempt_state($quiz, $attempt) {
+ if ($attempt->timefinish == 0) {
+ return mod_quiz_display_options::DURING;
+ } else if (time() < $attempt->timefinish + 120) {
+ return mod_quiz_display_options::IMMEDIATELY_AFTER;
+ } else if (!$quiz->timeclose || time() < $quiz->timeclose) {
+ return mod_quiz_display_options::LATER_WHILE_OPEN;
+ } else {
+ return mod_quiz_display_options::AFTER_CLOSE;
+ }
}
/**
- * Determine review options
+ * The the appropraite mod_quiz_display_options object for this attempt at this
+ * quiz right now.
*
* @param object $quiz the quiz instance.
* @param object $attempt the attempt in question.
- * @param $context the quiz module context.
+ * @param $context the quiz context.
*
- * @return object an object with boolean fields responses, scores, feedback,
- * correct_responses, solutions and general feedback
+ * @return mod_quiz_display_options
*/
-function quiz_get_reviewoptions($quiz, $attempt, $context) {
- global $USER;
+function quiz_get_review_options($quiz, $attempt, $context) {
+ $options = mod_quiz_display_options::make_from_quiz($quiz, quiz_attempt_state($quiz, $attempt));
- $options = new stdClass;
$options->readonly = true;
-
$options->flags = quiz_get_flag_option($attempt, $context);
-
- // Provide the links to the question review and comment script
if (!empty($attempt->id)) {
- $options->questionreviewlink = '/mod/quiz/reviewquestion.php?attempt=' . $attempt->id;
+ $options->questionreviewlink = new moodle_url('/mod/quiz/reviewquestion.php',
+ array('attempt' => $attempt->id));
}
// Show a link to the comment box only for closed attempts
- if (!empty($attempt->id) && $attempt->timefinish &&
- has_capability('mod/quiz:grade', $context)) {
- $options->questioncommentlink = new moodle_url('/mod/quiz/comment.php', array('attempt' => $attempt->id));
+ if (!empty($attempt->id) && $attempt->timefinish && !$attempt->preview &&
+ !is_null($context) && has_capability('mod/quiz:grade', $context)) {
+ $options->manualcomment = question_display_options::VISIBLE;
+ $options->manualcommentlink = new moodle_url('/mod/quiz/comment.php',
+ array('attempt' => $attempt->id));
}
- // Whether to display a response history.
- $canviewreports = has_capability('mod/quiz:viewreports', $context);
- $options->history = ($canviewreports && !$attempt->preview) ? 'all' : 'graded';
-
- if ($canviewreports && has_capability('moodle/grade:viewhidden', $context) && !$attempt->preview) {
+ if (!is_null($context) && !$attempt->preview &&
+ has_capability('mod/quiz:viewreports', $context) &&
+ has_capability('moodle/grade:viewhidden', $context)) {
// People who can see reports and hidden grades should be shown everything,
// except during preview when teachers want to see what students see.
- $options->responses = true;
- $options->scores = true;
- $options->feedback = true;
- $options->correct_responses = true;
- $options->solutions = false;
- $options->generalfeedback = true;
- $options->overallfeedback = true;
- $options->quizstate = QUIZ_STATE_TEACHERACCESS;
- } else {
- // Work out the state of the attempt ...
- if (((time() - $attempt->timefinish) < 120) || $attempt->timefinish==0) {
- $quiz_state_mask = QUIZ_REVIEW_IMMEDIATELY;
- $options->quizstate = QUIZ_STATE_IMMEDIATELY;
- } else if (!$quiz->timeclose or time() < $quiz->timeclose) {
- $quiz_state_mask = QUIZ_REVIEW_OPEN;
- $options->quizstate = QUIZ_STATE_OPEN;
- } else {
- $quiz_state_mask = QUIZ_REVIEW_CLOSED;
- $options->quizstate = QUIZ_STATE_CLOSED;
- }
+ $options->attempt = question_display_options::VISIBLE;
+ $options->correctness = question_display_options::VISIBLE;
+ $options->marks = question_display_options::MARK_AND_MAX;
+ $options->feedback = question_display_options::VISIBLE;
+ $options->numpartscorrect = question_display_options::VISIBLE;
+ $options->generalfeedback = question_display_options::VISIBLE;
+ $options->rightanswer = question_display_options::VISIBLE;
+ $options->overallfeedback = question_display_options::VISIBLE;
+ $options->history = question_display_options::VISIBLE;
- // ... and hence extract the appropriate review options.
- $options->responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_RESPONSES) ? 1 : 0;
- $options->scores = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SCORES) ? 1 : 0;
- $options->feedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_FEEDBACK) ? 1 : 0;
- $options->correct_responses = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_ANSWERS) ? 1 : 0;
- $options->solutions = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_SOLUTIONS) ? 1 : 0;
- $options->generalfeedback = ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_GENERALFEEDBACK) ? 1 : 0;
- $options->overallfeedback = $attempt->timefinish && ($quiz->review & $quiz_state_mask & QUIZ_REVIEW_OVERALLFEEDBACK);
}
return $options;
@@ -964,7 +1012,7 @@ function quiz_get_reviewoptions($quiz, $attempt, $context) {
/**
* Combines the review options from a number of different quiz attempts.
- * Returns an array of two ojects, so he suggested way of calling this
+ * Returns an array of two ojects, so the suggested way of calling this
* funciton is:
* list($someoptions, $alloptions) = quiz_get_combined_reviewoptions(...)
*
@@ -977,30 +1025,103 @@ function quiz_get_reviewoptions($quiz, $attempt, $context) {
* at least one of the attempts, the other showing which options are true
* for all attempts.
*/
-function quiz_get_combined_reviewoptions($quiz, $attempts, $context) {
- $fields = array('readonly', 'scores', 'feedback', 'correct_responses', 'solutions', 'generalfeedback', 'overallfeedback');
- $someoptions = new stdClass;
- $alloptions = new stdClass;
+function quiz_get_combined_reviewoptions($quiz, $attempts) {
+ $fields = array('feedback', 'generalfeedback', 'rightanswer', 'overallfeedback');
+ $someoptions = new stdClass();
+ $alloptions = new stdClass();
foreach ($fields as $field) {
$someoptions->$field = false;
$alloptions->$field = true;
}
+ $someoptions->marks = question_display_options::HIDDEN;
+ $alloptions->marks = question_display_options::MARK_AND_MAX;
+
foreach ($attempts as $attempt) {
- $attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
+ $attemptoptions = mod_quiz_display_options::make_from_quiz($quiz,
+ quiz_attempt_state($quiz, $attempt));
foreach ($fields as $field) {
$someoptions->$field = $someoptions->$field || $attemptoptions->$field;
$alloptions->$field = $alloptions->$field && $attemptoptions->$field;
}
+ $someoptions->marks = max($someoptions->marks, $attemptoptions->marks);
+ $alloptions->marks = min($alloptions->marks, $attemptoptions->marks);
}
return array($someoptions, $alloptions);
}
+/**
+ * Clean the question layout from various possible anomalies:
+ * - Remove consecutive ","'s
+ * - Remove duplicate question id's
+ * - Remove extra "," from beginning and end
+ * - Finally, add a ",0" in the end if there is none
+ *
+ * @param $string $layout the quiz layout to clean up, usually from $quiz->questions.
+ * @param bool $removeemptypages If true, remove empty pages from the quiz. False by default.
+ * @return $string the cleaned-up layout
+ */
+function quiz_clean_layout($layout, $removeemptypages = false) {
+ // Remove repeated ','s. This can happen when a restore fails to find the right
+ // id to relink to.
+ $layout = preg_replace('/,{2,}/', ',', trim($layout, ','));
+
+ // Remove duplicate question ids
+ $layout = explode(',', $layout);
+ $cleanerlayout = array();
+ $seen = array();
+ foreach ($layout as $item) {
+ if ($item == 0) {
+ $cleanerlayout[] = '0';
+ } else if (!in_array($item, $seen)) {
+ $cleanerlayout[] = $item;
+ $seen[] = $item;
+ }
+ }
+
+ if ($removeemptypages) {
+ // Avoid duplicate page breaks
+ $layout = $cleanerlayout;
+ $cleanerlayout = array();
+ $stripfollowingbreaks = true; // Ensure breaks are stripped from the start.
+ foreach ($layout as $item) {
+ if ($stripfollowingbreaks && $item == 0) {
+ continue;
+ }
+ $cleanerlayout[] = $item;
+ $stripfollowingbreaks = $item == 0;
+ }
+ }
+
+ // Add a page break at the end if there is none
+ if (end($cleanerlayout) !== '0') {
+ $cleanerlayout[] = '0';
+ }
+
+ return implode(',', $cleanerlayout);
+}
+
+/**
+ * Get the slot for a question with a particular id.
+ * @param object $quiz the quiz settings.
+ * @param int $questionid the of a question in the quiz.
+ * @return int the corresponding slot. Null if the question is not in the quiz.
+ */
+function quiz_get_slot_for_question($quiz, $questionid) {
+ $questionids = quiz_questions_in_quiz($quiz->questions);
+ foreach (explode(',', $questionids) as $key => $id) {
+ if ($id == $questionid) {
+ return $key + 1;
+ }
+ }
+ return null;
+}
+
/// FUNCTIONS FOR SENDING NOTIFICATION EMAILS ///////////////////////////////
/**
* Sends confirmation email to the student taking the course
*
- * @param stdClass $a associative array of replaceable fields for the templates
+ * @param object $a associative array of replaceable fields for the templates
*
* @return bool
*/
@@ -1041,7 +1162,7 @@ function quiz_send_confirmation($a) {
* Sends notification messages to the interested parties that assign the role capability
*
* @param object $recipient user object of the intended recipient
- * @param stdClass $a associative array of replaceable fields for the templates
+ * @param object $a associative array of replaceable fields for the templates
*
* @return bool
*/
@@ -1052,7 +1173,6 @@ function quiz_send_notification($recipient, $a) {
// recipient info for template
$a->username = fullname($recipient);
$a->userusername = $recipient->username;
- //$a->userusername = $recipient->username;
// fetch the subject and body from strings
$subject = get_string('emailnotifysubject', 'quiz', $a);
@@ -1105,7 +1225,7 @@ function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm)
// check for confirmation required
$sendconfirm = false;
$notifyexcludeusers = '';
- if (has_capability('mod/quiz:emailconfirmsubmission', $context, NULL, false)) {
+ if (has_capability('mod/quiz:emailconfirmsubmission', $context, null, false)) {
// exclude from notify emails later
$notifyexcludeusers = $USER->id;
// send the email
@@ -1113,7 +1233,8 @@ function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm)
}
// check for notifications required
- $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.lang, u.timezone, u.mailformat, u.maildisplay';
+ $notifyfields = 'u.id, u.username, u.firstname, u.lastname, u.email, u.lang, ' .
+ 'u.timezone, u.mailformat, u.maildisplay';
$groups = groups_get_all_groups($course->id, $USER->id);
if (is_array($groups) && count($groups) > 0) {
$groups = array_keys($groups);
@@ -1130,16 +1251,18 @@ function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm)
// if something to send, then build $a
if (! empty($userstonotify) or $sendconfirm) {
- $a = new stdClass;
+ $a = new stdClass();
// course info
$a->coursename = $course->fullname;
$a->courseshortname = $course->shortname;
// quiz info
$a->quizname = $quiz->name;
$a->quizreporturl = $CFG->wwwroot . '/mod/quiz/report.php?id=' . $cm->id;
- $a->quizreportlink = '' . format_string($quiz->name) . ' report';
+ $a->quizreportlink = '
' .
+ format_string($quiz->name) . ' report';
$a->quizreviewurl = $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attempt->id;
- $a->quizreviewlink = '
' . format_string($quiz->name) . ' review';
+ $a->quizreviewlink = '
' .
+ format_string($quiz->name) . ' review';
$a->quizurl = $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
$a->quizlink = '
' . format_string($quiz->name) . '';
// attempt info
@@ -1181,82 +1304,19 @@ function quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm)
// log errors sending emails if any
if (! empty($emailresult['fail'])) {
- debugging('quiz_send_notification_emails:: '.$emailresult['fail'].' email(s) failed to be sent.', DEBUG_DEVELOPER);
+ debugging('quiz_send_notification_emails:: ' . $emailresult['fail'] .
+ ' email(s) failed to be sent.', DEBUG_DEVELOPER);
}
// return the number of successfully sent emails
return $emailresult['good'];
}
-/**
- * Clean the question layout from various possible anomalies:
- * - Remove consecutive ","'s
- * - Remove duplicate question id's
- * - Remove extra "," from beginning and end
- * - Finally, add a ",0" in the end if there is none
- *
- * @param $string $layout the quiz layout to clean up, usually from $quiz->questions.
- * @param boolean $removeemptypages If true, remove empty pages from the quiz. False by default.
- * @return $string the cleaned-up layout
- */
-function quiz_clean_layout($layout, $removeemptypages = false){
- // Remove duplicate "," (or triple, or...)
- $layout = preg_replace('/,{2,}/', ',', trim($layout, ','));
-
- // Remove duplicate question ids
- $layout = explode(',', $layout);
- $cleanerlayout = array();
- $seen = array();
- foreach ($layout as $item) {
- if ($item == 0) {
- $cleanerlayout[] = '0';
- } else if (!in_array($item, $seen)) {
- $cleanerlayout[] = $item;
- $seen[] = $item;
- }
- }
-
- if ($removeemptypages) {
- // Avoid duplicate page breaks
- $layout = $cleanerlayout;
- $cleanerlayout = array();
- $stripfollowingbreaks = true; // Ensure breaks are stripped from the start.
- foreach ($layout as $item) {
- if ($stripfollowingbreaks && $item == 0) {
- continue;
- }
- $cleanerlayout[] = $item;
- $stripfollowingbreaks = $item == 0;
- }
- }
-
- // Add a page break at the end if there is none
- if (end($cleanerlayout) !== '0') {
- $cleanerlayout[] = '0';
- }
-
- return implode(',', $cleanerlayout);
-}
-/**
- * Print a quiz error message. This is a thin wrapper around print_error, for convinience.
- *
- * @param mixed $quiz either the quiz object, or the interger quiz id.
- * @param string $errorcode the name of the string from quiz.php to print.
- * @param object $a any extra data required by the error string.
- */
-function quiz_error($quiz, $errorcode, $a = null) {
- global $CFG;
- if (is_object($quiz)) {
- $quiz = $quiz->id;
- }
- print_error($errorcode, 'quiz', $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz, $a);
-}
-
/**
* Checks if browser is safe browser
*
* @return true, if browser is safe browser else false
-*/
+ */
function quiz_check_safe_browser() {
return strpos($_SERVER['HTTP_USER_AGENT'], "SEB") !== false;
}
@@ -1266,7 +1326,8 @@ function quiz_get_js_module() {
return array(
'name' => 'mod_quiz',
'fullpath' => '/mod/quiz/module.js',
- 'requires' => array('base', 'dom', 'event-delegate', 'event-key', 'core_question_engine'),
+ 'requires' => array('base', 'dom', 'event-delegate', 'event-key',
+ 'core_question_engine'),
'strings' => array(
array('timesup', 'quiz'),
array('functiondisabledbysecuremode', 'quiz'),
@@ -1274,3 +1335,97 @@ function quiz_get_js_module() {
),
);
}
+
+
+/**
+ * An extension of question_display_options that includes the extra options used
+ * by the quiz.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_quiz_display_options extends question_display_options {
+ /**#@+
+ * @var integer bits used to indicate various times in relation to a
+ * quiz attempt.
+ */
+ const DURING = 0x10000;
+ const IMMEDIATELY_AFTER = 0x01000;
+ const LATER_WHILE_OPEN = 0x00100;
+ const AFTER_CLOSE = 0x00010;
+ /**#@-*/
+
+ /**
+ * @var boolean if this is false, then the student is not allowed to review
+ * anything about the attempt.
+ */
+ public $attempt = true;
+
+ /**
+ * @var boolean if this is false, then the student is not allowed to review
+ * anything about the attempt.
+ */
+ public $overallfeedback = self::VISIBLE;
+
+ /**
+ * Set up the various options from the quiz settings, and a time constant.
+ * @param object $quiz the quiz settings.
+ * @param int $one of the {@link DURING}, {@link IMMEDIATELY_AFTER},
+ * {@link LATER_WHILE_OPEN} or {@link AFTER_CLOSE} constants.
+ * @return mod_quiz_display_options set up appropriately.
+ */
+ public static function make_from_quiz($quiz, $when) {
+ $options = new self();
+
+ $options->attempt = self::extract($quiz->reviewattempt, $when, true, false);
+ $options->correctness = self::extract($quiz->reviewcorrectness, $when);
+ $options->marks = self::extract($quiz->reviewmarks, $when,
+ self::MARK_AND_MAX, self::MAX_ONLY);
+ $options->feedback = self::extract($quiz->reviewspecificfeedback, $when);
+ $options->generalfeedback = self::extract($quiz->reviewgeneralfeedback, $when);
+ $options->rightanswer = self::extract($quiz->reviewrightanswer, $when);
+ $options->overallfeedback = self::extract($quiz->reviewoverallfeedback, $when);
+
+ $options->numpartscorrect = $options->feedback;
+
+ if ($quiz->questiondecimalpoints != -1) {
+ $options->markdp = $quiz->questiondecimalpoints;
+ } else {
+ $options->markdp = $quiz->decimalpoints;
+ }
+
+ return $options;
+ }
+
+ protected static function extract($bitmask, $bit,
+ $whenset = self::VISIBLE, $whennotset = self::HIDDEN) {
+ if ($bitmask & $bit) {
+ return $whenset;
+ } else {
+ return $whennotset;
+ }
+ }
+}
+
+
+/**
+ * A {@link qubaid_condition} for finding all the question usages belonging to
+ * a particular quiz.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qubaids_for_quiz extends qubaid_join {
+ public function __construct($quizid, $includepreviews = true, $onlyfinished = false) {
+ $where = 'quiza.quiz = :quizaquiz';
+ if (!$includepreviews) {
+ $where .= ' AND preview = 0';
+ }
+ if ($onlyfinished) {
+ $where .= ' AND timefinish <> 0';
+ }
+
+ parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where,
+ array('quizaquiz' => $quizid));
+ }
+}
diff --git a/mod/quiz/mod_form.php b/mod/quiz/mod_form.php
index 0da3ec6db03..f05302eed8b 100644
--- a/mod/quiz/mod_form.php
+++ b/mod/quiz/mod_form.php
@@ -1,54 +1,67 @@
.
-///////////////////////////////////////////////////////////////////////////
-// //
-// NOTICE OF COPYRIGHT //
-// //
-// Moodle - Modular Object-Oriented Dynamic Learning Environment //
-// http://moodle.org //
-// //
-// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
-// //
-// This program is free software; you can redistribute it and/or modify //
-// it under the terms of the GNU General Public License as published by //
-// the Free Software Foundation; either version 2 of the License, or //
-// (at your option) any later version. //
-// //
-// This program is distributed in the hope that it will be useful, //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
-// GNU General Public License for more details: //
-// //
-// http://www.gnu.org/copyleft/gpl.html //
-// //
-///////////////////////////////////////////////////////////////////////////
+/**
+ * Defines the quiz module ettings form.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2006 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
-}
+
+defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/course/moodleform_mod.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
/**
* Settings form for the quiz module.
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @copyright 2006 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_mod_form extends moodleform_mod {
- var $_feedbacks;
+ protected $_feedbacks;
+ protected static $reviewfields = array(); // Initialised in the constructor.
- function definition() {
+ public function __construct($current, $section, $cm, $course) {
+ self::$reviewfields = array(
+ 'attempt' => get_string('theattempt', 'quiz'),
+ 'correctness' => get_string('whethercorrect', 'question'),
+ 'marks' => get_string('marks', 'question'),
+ 'specificfeedback' => get_string('specificfeedback', 'question'),
+ 'generalfeedback' => get_string('generalfeedback', 'question'),
+ 'rightanswer' => get_string('rightanswer', 'question'),
+ 'overallfeedback' => get_string('overallfeedback', 'quiz'),
+ );
+ parent::__construct($current, $section, $cm, $course);
+ }
+ protected function definition() {
global $COURSE, $CFG, $DB, $PAGE;
$quizconfig = get_config('quiz');
- $mform =& $this->_form;
+ $mform = $this->_form;
-//-------------------------------------------------------------------------------
+ //-------------------------------------------------------------------------------
$mform->addElement('header', 'general', get_string('general', 'form'));
- /// Name.
+ // Name.
$mform->addElement('text', 'name', get_string('name'), array('size'=>'64'));
if (!empty($CFG->formatstringstriptags)) {
$mform->setType('name', PARAM_TEXT);
@@ -57,45 +70,56 @@ class mod_quiz_mod_form extends moodleform_mod {
}
$mform->addRule('name', null, 'required', null, 'client');
- /// Introduction.
+ // Introduction.
$this->add_intro_editor(false, get_string('introduction', 'quiz'));
- /// Open and close dates.
- $mform->addElement('date_time_selector', 'timeopen', get_string('quizopen', 'quiz'), array('optional' => true));
- $mform->addElement('date_time_selector', 'timeclose', get_string('quizclose', 'quiz'), array('optional' => true));
+ // Open and close dates.
+ $mform->addElement('date_time_selector', 'timeopen', get_string('quizopen', 'quiz'),
+ array('optional' => true, 'step' => 1));
+ $mform->addHelpButton('timeopen', 'quizopenclose', 'quiz');
- /// Time limit.
- $mform->addElement('duration', 'timelimit', get_string('timelimit', 'quiz'), array('optional' => true));
+ $mform->addElement('date_time_selector', 'timeclose', get_string('quizclose', 'quiz'),
+ array('optional' => true, 'step' => 1));
+
+ // Time limit.
+ $mform->addElement('duration', 'timelimit', get_string('timelimit', 'quiz'),
+ array('optional' => true));
$mform->addHelpButton('timelimit', 'timelimit', 'quiz');
$mform->setAdvanced('timelimit', $quizconfig->timelimit_adv);
$mform->setDefault('timelimit', $quizconfig->timelimit);
- /// Number of attempts.
+ // Number of attempts.
$attemptoptions = array('0' => get_string('unlimited'));
for ($i = 1; $i <= QUIZ_MAX_ATTEMPT_OPTION; $i++) {
$attemptoptions[$i] = $i;
}
- $mform->addElement('select', 'attempts', get_string('attemptsallowed', 'quiz'), $attemptoptions);
+ $mform->addElement('select', 'attempts', get_string('attemptsallowed', 'quiz'),
+ $attemptoptions);
$mform->setAdvanced('attempts', $quizconfig->attempts_adv);
$mform->setDefault('attempts', $quizconfig->attempts);
- /// Grading method.
- $mform->addElement('select', 'grademethod', get_string('grademethod', 'quiz'), quiz_get_grading_options());
+ // Grading method.
+ $mform->addElement('select', 'grademethod', get_string('grademethod', 'quiz'),
+ quiz_get_grading_options());
$mform->addHelpButton('grademethod', 'grademethod', 'quiz');
$mform->setAdvanced('grademethod', $quizconfig->grademethod_adv);
$mform->setDefault('grademethod', $quizconfig->grademethod);
$mform->disabledIf('grademethod', 'attempts', 'eq', 1);
-//-------------------------------------------------------------------------------
+ //-------------------------------------------------------------------------------
$mform->addElement('header', 'layouthdr', get_string('layout', 'quiz'));
- /// Shuffle questions.
- $shuffleoptions = array(0 => get_string('asshownoneditscreen', 'quiz'), 1 => get_string('shuffledrandomly', 'quiz'));
- $mform->addElement('select', 'shufflequestions', get_string('questionorder', 'quiz'), $shuffleoptions, array('id' => 'id_shufflequestions'));
+ // Shuffle questions.
+ $shuffleoptions = array(
+ 0 => get_string('asshownoneditscreen', 'quiz'),
+ 1 => get_string('shuffledrandomly', 'quiz')
+ );
+ $mform->addElement('select', 'shufflequestions', get_string('questionorder', 'quiz'),
+ $shuffleoptions, array('id' => 'id_shufflequestions'));
$mform->setAdvanced('shufflequestions', $quizconfig->shufflequestions_adv);
$mform->setDefault('shufflequestions', $quizconfig->shufflequestions);
- /// Questions per page.
+ // Questions per page.
$pageoptions = array();
$pageoptions[0] = get_string('neverallononepage', 'quiz');
$pageoptions[1] = get_string('everyquestion', 'quiz');
@@ -104,127 +128,109 @@ class mod_quiz_mod_form extends moodleform_mod {
}
$pagegroup = array();
- $pagegroup[] = &$mform->createElement('select', 'questionsperpage', get_string('newpage', 'quiz'), $pageoptions, array('id' => 'id_questionsperpage'));
+ $pagegroup[] = $mform->createElement('select', 'questionsperpage',
+ get_string('newpage', 'quiz'), $pageoptions, array('id' => 'id_questionsperpage'));
$mform->setDefault('questionsperpage', $quizconfig->questionsperpage);
if (!empty($this->_cm)) {
- $pagegroup[] = &$mform->createElement('checkbox', 'repaginatenow', '', get_string('repaginatenow', 'quiz'), array('id' => 'id_repaginatenow'));
+ $pagegroup[] = $mform->createElement('checkbox', 'repaginatenow', '',
+ get_string('repaginatenow', 'quiz'), array('id' => 'id_repaginatenow'));
$mform->disabledIf('repaginatenow', 'shufflequestions', 'eq', 1);
$PAGE->requires->yui2_lib('event');
$PAGE->requires->js('/mod/quiz/edit.js');
+ $PAGE->requires->js_init_call('quiz_settings_init');
}
- $mform->addGroup($pagegroup, 'questionsperpagegrp', get_string('newpage', 'quiz'), null, false);
+ $mform->addGroup($pagegroup, 'questionsperpagegrp',
+ get_string('newpage', 'quiz'), null, false);
$mform->addHelpButton('questionsperpagegrp', 'newpage', 'quiz');
$mform->setAdvanced('questionsperpagegrp', $quizconfig->questionsperpage_adv);
-//-------------------------------------------------------------------------------
+ //-------------------------------------------------------------------------------
$mform->addElement('header', 'interactionhdr', get_string('questionbehaviour', 'quiz'));
- /// Shuffle within questions.
+ // Shuffle within questions.
$mform->addElement('selectyesno', 'shuffleanswers', get_string('shufflewithin', 'quiz'));
$mform->addHelpButton('shuffleanswers', 'shufflewithin', 'quiz');
$mform->setAdvanced('shuffleanswers', $quizconfig->shuffleanswers_adv);
$mform->setDefault('shuffleanswers', $quizconfig->shuffleanswers);
- /// Adaptive mode.
- $mform->addElement('selectyesno', 'adaptive', get_string('adaptive', 'quiz'));
- $mform->addHelpButton('adaptive', 'adaptive', 'quiz');
- $mform->setAdvanced('adaptive', $quizconfig->optionflags_adv);
- $mform->setDefault('adaptive', $quizconfig->optionflags & QUESTION_ADAPTIVE);
+ // How questions behave (question behaviour).
+ if (!empty($this->current->preferredbehaviour)) {
+ $currentbehaviour = $this->current->preferredbehaviour;
+ } else {
+ $currentbehaviour = '';
+ }
+ $behaviours = question_engine::get_behaviour_options($currentbehaviour);
+ $mform->addElement('select', 'preferredbehaviour',
+ get_string('howquestionsbehave', 'question'), $behaviours);
+ $mform->addHelpButton('preferredbehaviour', 'howquestionsbehave', 'question');
+ $mform->setDefault('preferredbehaviour', $quizconfig->preferredbehaviour);
- /// Apply penalties.
- $mform->addElement('selectyesno', 'penaltyscheme', get_string('penaltyscheme', 'quiz'));
- $mform->addHelpButton('penaltyscheme', 'penaltyscheme', 'quiz');
- $mform->setAdvanced('penaltyscheme', $quizconfig->penaltyscheme_adv);
- $mform->setDefault('penaltyscheme', $quizconfig->penaltyscheme);
- $mform->disabledIf('penaltyscheme', 'adaptive', 'neq', 1);
-
- /// Each attempt builds on last.
- $mform->addElement('selectyesno', 'attemptonlast', get_string('eachattemptbuildsonthelast', 'quiz'));
+ // Each attempt builds on last.
+ $mform->addElement('selectyesno', 'attemptonlast',
+ get_string('eachattemptbuildsonthelast', 'quiz'));
$mform->addHelpButton('attemptonlast', 'eachattemptbuildsonthelast', 'quiz');
$mform->setAdvanced('attemptonlast', $quizconfig->attemptonlast_adv);
$mform->setDefault('attemptonlast', $quizconfig->attemptonlast);
$mform->disabledIf('attemptonlast', 'attempts', 'eq', 1);
-//-------------------------------------------------------------------------------
- $mform->addElement('header', 'reviewoptionshdr', get_string('reviewoptionsheading', 'quiz'));
+ //-------------------------------------------------------------------------------
+ $mform->addElement('header', 'reviewoptionshdr',
+ get_string('reviewoptionsheading', 'quiz'));
$mform->addHelpButton('reviewoptionshdr', 'reviewoptionsheading', 'quiz');
- $mform->setAdvanced('reviewoptionshdr', $quizconfig->review_adv);
- /// Review options.
- $immediatelyoptionsgrp=array();
- $immediatelyoptionsgrp[] = &$mform->createElement('checkbox', 'responsesimmediately', '', get_string('responses', 'quiz'));
- $immediatelyoptionsgrp[] = &$mform->createElement('checkbox', 'answersimmediately', '', get_string('answers', 'quiz'));
- $immediatelyoptionsgrp[] = &$mform->createElement('checkbox', 'feedbackimmediately', '', get_string('feedback', 'quiz'));
- $immediatelyoptionsgrp[] = &$mform->createElement('checkbox', 'generalfeedbackimmediately', '', get_string('generalfeedback', 'quiz'));
- $immediatelyoptionsgrp[] = &$mform->createElement('checkbox', 'scoreimmediately', '', get_string('scores', 'quiz'));
- $immediatelyoptionsgrp[] = &$mform->createElement('checkbox', 'overallfeedbackimmediately', '', get_string('overallfeedback', 'quiz'));
- $mform->addGroup($immediatelyoptionsgrp, 'immediatelyoptionsgrp', get_string('reviewimmediately', 'quiz'), null, false);
- $mform->setDefault('responsesimmediately', $quizconfig->review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
- $mform->setDefault('answersimmediately', $quizconfig->review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
- $mform->setDefault('feedbackimmediately', $quizconfig->review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
- $mform->setDefault('generalfeedbackimmediately', $quizconfig->review & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
- $mform->setDefault('scoreimmediately', $quizconfig->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
- $mform->setDefault('overallfeedbackimmediately', $quizconfig->review & QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
+ // Review options.
+ $this->add_review_options_group($mform, $quizconfig, 'during',
+ mod_quiz_display_options::DURING);
+ $this->add_review_options_group($mform, $quizconfig, 'immediately',
+ mod_quiz_display_options::IMMEDIATELY_AFTER);
+ $this->add_review_options_group($mform, $quizconfig, 'open',
+ mod_quiz_display_options::LATER_WHILE_OPEN);
+ $this->add_review_options_group($mform, $quizconfig, 'closed',
+ mod_quiz_display_options::AFTER_CLOSE);
- $openoptionsgrp=array();
- $openoptionsgrp[] = &$mform->createElement('checkbox', 'responsesopen', '', get_string('responses', 'quiz'));
- $openoptionsgrp[] = &$mform->createElement('checkbox', 'answersopen', '', get_string('answers', 'quiz'));
- $openoptionsgrp[] = &$mform->createElement('checkbox', 'feedbackopen', '', get_string('feedback', 'quiz'));
- $openoptionsgrp[] = &$mform->createElement('checkbox', 'generalfeedbackopen', '', get_string('generalfeedback', 'quiz'));
- $openoptionsgrp[] = &$mform->createElement('checkbox', 'scoreopen', '', get_string('scores', 'quiz'));
- $openoptionsgrp[] = &$mform->createElement('checkbox', 'overallfeedbackopen', '', get_string('overallfeedback', 'quiz'));
- $mform->addGroup($openoptionsgrp, 'openoptionsgrp', get_string('reviewopen', 'quiz'), array(' '), false);
- $mform->setDefault('responsesopen', $quizconfig->review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
- $mform->setDefault('answersopen', $quizconfig->review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
- $mform->setDefault('feedbackopen', $quizconfig->review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
- $mform->setDefault('generalfeedbackopen', $quizconfig->review & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
- $mform->setDefault('scoreopen', $quizconfig->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
- $mform->setDefault('overallfeedbackopen', $quizconfig->review & QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
+ foreach ($behaviours as $behaviour => $notused) {
+ $unusedoptions = question_engine::get_behaviour_unused_display_options($behaviour);
+ foreach ($unusedoptions as $unusedoption) {
+ $mform->disabledIf($unusedoption . 'during', 'preferredbehaviour',
+ 'eq', $behaviour);
+ }
+ }
+ $mform->disabledIf('attemptduring', 'preferredbehaviour',
+ 'neq', 'wontmatch');
+ $mform->disabledIf('overallfeedbackduring', 'preferredbehaviour',
+ 'neq', 'wontmatch');
- $closedoptionsgrp=array();
- $closedoptionsgrp[] = &$mform->createElement('checkbox', 'responsesclosed', '', get_string('responses', 'quiz'));
- $closedoptionsgrp[] = &$mform->createElement('checkbox', 'answersclosed', '', get_string('answers', 'quiz'));
- $closedoptionsgrp[] = &$mform->createElement('checkbox', 'feedbackclosed', '', get_string('feedback', 'quiz'));
- $closedoptionsgrp[] = &$mform->createElement('checkbox', 'generalfeedbackclosed', '', get_string('generalfeedback', 'quiz'));
- $closedoptionsgrp[] = &$mform->createElement('checkbox', 'scoreclosed', '', get_string('scores', 'quiz'));
- $closedoptionsgrp[] = &$mform->createElement('checkbox', 'overallfeedbackclosed', '', get_string('overallfeedback', 'quiz'));
- $mform->addGroup($closedoptionsgrp, 'closedoptionsgrp', get_string('reviewclosed', 'quiz'), array(' '), false);
- $mform->setDefault('responsesclosed', $quizconfig->review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
- $mform->setDefault('answersclosed', $quizconfig->review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
- $mform->setDefault('feedbackclosed', $quizconfig->review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
- $mform->setDefault('generalfeedbackclosed', $quizconfig->review & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
- $mform->setDefault('scoreclosed', $quizconfig->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
- $mform->setDefault('overallfeedbackclosed', $quizconfig->review & QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
- $mform->disabledIf('closedoptionsgrp', 'timeclose[enabled]');
-
-//-------------------------------------------------------------------------------
+ //-------------------------------------------------------------------------------
$mform->addElement('header', 'display', get_string('display', 'form'));
- /// Show user picture.
- $mform->addElement('selectyesno', 'showuserpicture', get_string('showuserpicture', 'quiz'));
+ // Show user picture.
+ $mform->addElement('selectyesno', 'showuserpicture',
+ get_string('showuserpicture', 'quiz'));
$mform->addHelpButton('showuserpicture', 'showuserpicture', 'quiz');
$mform->setAdvanced('showuserpicture', $quizconfig->showuserpicture_adv);
$mform->setDefault('showuserpicture', $quizconfig->showuserpicture);
- /// Overall decimal points.
+ // Overall decimal points.
$options = array();
for ($i = 0; $i <= QUIZ_MAX_DECIMAL_OPTION; $i++) {
$options[$i] = $i;
}
- $mform->addElement('select', 'decimalpoints', get_string('decimalplaces', 'quiz'), $options);
+ $mform->addElement('select', 'decimalpoints', get_string('decimalplaces', 'quiz'),
+ $options);
$mform->addHelpButton('decimalpoints', 'decimalplaces', 'quiz');
$mform->setAdvanced('decimalpoints', $quizconfig->decimalpoints_adv);
$mform->setDefault('decimalpoints', $quizconfig->decimalpoints);
- /// Question decimal points.
+ // Question decimal points.
$options = array(-1 => get_string('sameasoverall', 'quiz'));
for ($i = 0; $i <= QUIZ_MAX_Q_DECIMAL_OPTION; $i++) {
$options[$i] = $i;
}
- $mform->addElement('select', 'questiondecimalpoints', get_string('decimalplacesquestion', 'quiz'), $options);
- $mform->addHelpButton('questiondecimalpoints', 'decimalplacesquestion','quiz');
+ $mform->addElement('select', 'questiondecimalpoints',
+ get_string('decimalplacesquestion', 'quiz'), $options);
+ $mform->addHelpButton('questiondecimalpoints', 'decimalplacesquestion', 'quiz');
$mform->setAdvanced('questiondecimalpoints', $quizconfig->questiondecimalpoints_adv);
$mform->setDefault('questiondecimalpoints', $quizconfig->questiondecimalpoints);
@@ -234,38 +240,40 @@ class mod_quiz_mod_form extends moodleform_mod {
$mform->setAdvanced('showblocks', $quizconfig->showblocks_adv);
$mform->setDefault('showblocks', $quizconfig->showblocks);
-//-------------------------------------------------------------------------------
+ //-------------------------------------------------------------------------------
$mform->addElement('header', 'security', get_string('extraattemptrestrictions', 'quiz'));
- /// Enforced time delay between quiz attempts.
+ // Enforced time delay between quiz attempts.
$mform->addElement('passwordunmask', 'quizpassword', get_string('requirepassword', 'quiz'));
$mform->setType('quizpassword', PARAM_TEXT);
$mform->addHelpButton('quizpassword', 'requirepassword', 'quiz');
$mform->setAdvanced('quizpassword', $quizconfig->password_adv);
$mform->setDefault('quizpassword', $quizconfig->password);
- /// IP address.
+ // IP address.
$mform->addElement('text', 'subnet', get_string('requiresubnet', 'quiz'));
$mform->setType('subnet', PARAM_TEXT);
$mform->addHelpButton('subnet', 'requiresubnet', 'quiz');
$mform->setAdvanced('subnet', $quizconfig->subnet_adv);
$mform->setDefault('subnet', $quizconfig->subnet);
- /// Enforced time delay between quiz attempts.
- $mform->addElement('duration', 'delay1', get_string('delay1st2nd', 'quiz'), array('optional' => true));
+ // Enforced time delay between quiz attempts.
+ $mform->addElement('duration', 'delay1', get_string('delay1st2nd', 'quiz'),
+ array('optional' => true));
$mform->addHelpButton('delay1', 'delay1st2nd', 'quiz');
$mform->setAdvanced('delay1', $quizconfig->delay1_adv);
$mform->setDefault('delay1', $quizconfig->delay1);
$mform->disabledIf('delay1', 'attempts', 'eq', 1);
- $mform->addElement('duration', 'delay2', get_string('delaylater', 'quiz'), array('optional' => true));
+ $mform->addElement('duration', 'delay2', get_string('delaylater', 'quiz'),
+ array('optional' => true));
$mform->addHelpButton('delay2', 'delaylater', 'quiz');
$mform->setAdvanced('delay2', $quizconfig->delay2_adv);
$mform->setDefault('delay2', $quizconfig->delay2);
$mform->disabledIf('delay2', 'attempts', 'eq', 1);
$mform->disabledIf('delay2', 'attempts', 'eq', 2);
- /// 'Secure' window.
+ // 'Secure' window.
$options = array(
0 => get_string('none', 'quiz'),
1 => get_string('popupwithjavascriptsupport', 'quiz'));
@@ -277,47 +285,57 @@ class mod_quiz_mod_form extends moodleform_mod {
$mform->setAdvanced('popup', $quizconfig->popup_adv);
$mform->setDefault('popup', $quizconfig->popup);
-//-------------------------------------------------------------------------------
+ //-------------------------------------------------------------------------------
$mform->addElement('header', 'overallfeedbackhdr', get_string('overallfeedback', 'quiz'));
$mform->addHelpButton('overallfeedbackhdr', 'overallfeedback', 'quiz');
$mform->addElement('hidden', 'grade', $quizconfig->maximumgrade);
$mform->setType('grade', PARAM_RAW);
- if (empty($this->_cm)) {
- $needwarning = $quizconfig->maximumgrade == 0;
+
+ if (isset($this->current->grade)) {
+ $needwarning = $this->current->grade === 0;
} else {
- $quizgrade = $DB->get_field('quiz', 'grade', array('id' => $this->_instance));
- $needwarning = $quizgrade == 0;
+ $needwarning = $quizconfig->maximumgrade == 0;
}
if ($needwarning) {
- $mform->addElement('static', 'nogradewarning', '', get_string('nogradewarning', 'quiz'));
+ $mform->addElement('static', 'nogradewarning', '',
+ get_string('nogradewarning', 'quiz'));
}
- $mform->addElement('static', 'gradeboundarystatic1', get_string('gradeboundary', 'quiz'), '100%');
+ $mform->addElement('static', 'gradeboundarystatic1',
+ get_string('gradeboundary', 'quiz'), '100%');
$repeatarray = array();
- $repeatarray[] = &MoodleQuickForm::createElement('editor', 'feedbacktext', get_string('feedback', 'quiz'), null, array('maxfiles'=>EDITOR_UNLIMITED_FILES, 'noclean'=>true, 'context'=>$this->context));
- $mform->setType('feedbacktext', PARAM_RAW);
- $repeatarray[] = &MoodleQuickForm::createElement('text', 'feedbackboundaries', get_string('gradeboundary', 'quiz'), array('size' => 10));
- $mform->setType('feedbackboundaries', PARAM_NOTAGS);
+ $repeatedoptions = array();
+ $repeatarray[] = MoodleQuickForm::createElement('editor', 'feedbacktext',
+ get_string('feedback', 'quiz'), null, array('maxfiles' => EDITOR_UNLIMITED_FILES,
+ 'noclean' => true, 'context' => $this->context));
+ $repeatarray[] = MoodleQuickForm::createElement('text', 'feedbackboundaries',
+ get_string('gradeboundary', 'quiz'), array('size' => 10));
+ $repeatedoptions['feedbacktext']['type'] = PARAM_RAW;
+ $repeatedoptions['feedbackboundaries']['type'] = PARAM_RAW;
if (!empty($this->_instance)) {
- $this->_feedbacks = $DB->get_records('quiz_feedback', array('quizid'=>$this->_instance), 'mingrade DESC');
+ $this->_feedbacks = $DB->get_records('quiz_feedback',
+ array('quizid' => $this->_instance), 'mingrade DESC');
} else {
$this->_feedbacks = array();
}
$numfeedbacks = max(count($this->_feedbacks) * 1.5, 5);
- $nextel=$this->repeat_elements($repeatarray, $numfeedbacks - 1,
- array(), 'boundary_repeats', 'boundary_add_fields', 3,
+ $nextel = $this->repeat_elements($repeatarray, $numfeedbacks - 1,
+ $repeatedoptions, 'boundary_repeats', 'boundary_add_fields', 3,
get_string('addmoreoverallfeedbacks', 'quiz'), true);
// Put some extra elements in before the button
- $insertEl = &MoodleQuickForm::createElement('editor', "feedbacktext[$nextel]", get_string('feedback', 'quiz'), null, array('maxfiles'=>EDITOR_UNLIMITED_FILES, 'noclean'=>true, 'context'=>$this->context));
- $mform->insertElementBefore($insertEl, 'boundary_add_fields');
-
- $insertEl = &MoodleQuickForm::createElement('static', 'gradeboundarystatic2', get_string('gradeboundary', 'quiz'), '0%');
- $mform->insertElementBefore($insertEl, 'boundary_add_fields');
+ $mform->insertElementBefore(MoodleQuickForm::createElement('editor',
+ "feedbacktext[$nextel]", get_string('feedback', 'quiz'), null,
+ array('maxfiles' => EDITOR_UNLIMITED_FILES, 'noclean' => true,
+ 'context' => $this->context)),
+ 'boundary_add_fields');
+ $mform->insertElementBefore(MoodleQuickForm::createElement('static',
+ 'gradeboundarystatic2', get_string('gradeboundary', 'quiz'), '0%'),
+ 'boundary_add_fields');
// Add the disabledif rules. We cannot do this using the $repeatoptions parameter to
// repeat_elements becuase we don't want to dissable the first feedbacktext.
@@ -326,24 +344,57 @@ class mod_quiz_mod_form extends moodleform_mod {
$mform->disabledIf('feedbacktext[' . ($i + 1) . ']', 'grade', 'eq', 0);
}
-//-------------------------------------------------------------------------------
+ //-------------------------------------------------------------------------------
$this->standard_coursemodule_elements();
-//-------------------------------------------------------------------------------
+ //-------------------------------------------------------------------------------
// buttons
$this->add_action_buttons();
}
- function data_preprocessing(&$default_values){
- if (isset($default_values['grade'])) {
- $default_values['grade'] = $default_values['grade'] + 0; // Convert to a real number, so we don't get 0.0000.
+ protected function add_review_options_group($mform, $quizconfig, $whenname, $when) {
+ $group = array();
+ foreach (self::$reviewfields as $field => $label) {
+ $group[] = $mform->createElement('checkbox', $field . $whenname, '', $label);
+ }
+ $mform->addGroup($group, $whenname . 'optionsgrp',
+ get_string('review' . $whenname, 'quiz'), null, false);
+
+ foreach (self::$reviewfields as $field => $notused) {
+ $cfgfield = 'review' . $field;
+ if ($quizconfig->$cfgfield & $when) {
+ $mform->setDefault($field . $whenname, 1);
+ } else {
+ $mform->setDefault($field . $whenname, 0);
+ }
+ }
+
+ $mform->disabledIf('correctness' . $whenname, 'attempt' . $whenname);
+ $mform->disabledIf('specificfeedback' . $whenname, 'attempt' . $whenname);
+ $mform->disabledIf('generalfeedback' . $whenname, 'attempt' . $whenname);
+ $mform->disabledIf('rightanswer' . $whenname, 'attempt' . $whenname);
+ }
+
+ protected function preprocessing_review_settings(&$toform, $whenname, $when) {
+ foreach (self::$reviewfields as $field => $notused) {
+ $fieldname = 'review' . $field;
+ if (array_key_exists($fieldname, $toform)) {
+ $toform[$field . $whenname] = $toform[$fieldname] & $when;
+ }
+ }
+ }
+
+ public function data_preprocessing(&$toform) {
+ if (isset($toform['grade'])) {
+ // Convert to a real number, so we don't get 0.0000.
+ $toform['grade'] = $toform['grade'] + 0;
}
if (count($this->_feedbacks)) {
$key = 0;
- foreach ($this->_feedbacks as $feedback){
+ foreach ($this->_feedbacks as $feedback) {
$draftid = file_get_submitted_draft_itemid('feedbacktext['.$key.']');
- $default_values['feedbacktext['.$key.']']['text'] = file_prepare_draft_area(
+ $toform['feedbacktext['.$key.']']['text'] = file_prepare_draft_area(
$draftid, // draftid
$this->context->id, // context
'mod_quiz', // component
@@ -352,10 +403,10 @@ class mod_quiz_mod_form extends moodleform_mod {
null,
$feedback->feedbacktext // text
);
- $default_values['feedbacktext['.$key.']']['format'] = $feedback->feedbacktextformat;
- $default_values['feedbacktext['.$key.']']['itemid'] = $draftid;
+ $toform['feedbacktext['.$key.']']['format'] = $feedback->feedbacktextformat;
+ $toform['feedbacktext['.$key.']']['itemid'] = $draftid;
- if ($default_values['grade'] == 0) {
+ if ($toform['grade'] == 0) {
// When a quiz is un-graded, there can only be one lot of
// feedback. If the quiz previously had a maximum grade and
// several lots of feedback, we must now avoid putting text
@@ -365,60 +416,42 @@ class mod_quiz_mod_form extends moodleform_mod {
}
if ($feedback->mingrade > 0) {
- $default_values['feedbackboundaries['.$key.']'] = (100.0 * $feedback->mingrade / $default_values['grade']) . '%';
+ $toform['feedbackboundaries['.$key.']'] =
+ (100.0 * $feedback->mingrade / $toform['grade']) . '%';
}
$key++;
}
}
- if (isset($default_values['timelimit'])) {
- $default_values['timelimitenable'] = $default_values['timelimit'] > 0;
+ if (isset($toform['timelimit'])) {
+ $toform['timelimitenable'] = $toform['timelimit'] > 0;
}
- if (isset($default_values['review'])){
- $review = (int)$default_values['review'];
- unset($default_values['review']);
+ $this->preprocessing_review_settings($toform, 'during',
+ mod_quiz_display_options::DURING);
+ $this->preprocessing_review_settings($toform, 'immediately',
+ mod_quiz_display_options::IMMEDIATELY_AFTER);
+ $this->preprocessing_review_settings($toform, 'open',
+ mod_quiz_display_options::LATER_WHILE_OPEN);
+ $this->preprocessing_review_settings($toform, 'closed',
+ mod_quiz_display_options::AFTER_CLOSE);
+ $toform['attemptduring'] = true;
+ $toform['overallfeedbackduring'] = false;
- $default_values['responsesimmediately'] = $review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY;
- $default_values['answersimmediately'] = $review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY;
- $default_values['feedbackimmediately'] = $review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY;
- $default_values['generalfeedbackimmediately'] = $review & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY;
- $default_values['scoreimmediately'] = $review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY;
- $default_values['overallfeedbackimmediately'] = $review & QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY;
-
- $default_values['responsesopen'] = $review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN;
- $default_values['answersopen'] = $review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN;
- $default_values['feedbackopen'] = $review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN;
- $default_values['generalfeedbackopen'] = $review & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN;
- $default_values['scoreopen'] = $review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN;
- $default_values['overallfeedbackopen'] = $review & QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN;
-
- $default_values['responsesclosed'] = $review & QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED;
- $default_values['answersclosed'] = $review & QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED;
- $default_values['feedbackclosed'] = $review & QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED;
- $default_values['generalfeedbackclosed'] = $review & QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED;
- $default_values['scoreclosed'] = $review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED;
- $default_values['overallfeedbackclosed'] = $review & QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED;
- }
-
- if (isset($default_values['optionflags'])){
- $default_values['adaptive'] = $default_values['optionflags'] & QUESTION_ADAPTIVE;
- unset($default_values['optionflags']);
- }
-
- // Password field - different in form to stop browsers that remember passwords
- // getting confused.
- if (isset($default_values['password'])) {
- $default_values['quizpassword'] = $default_values['password'];
- unset($default_values['password']);
+ // Password field - different in form to stop browsers that remember
+ // passwords from getting confused.
+ if (isset($toform['password'])) {
+ $toform['quizpassword'] = $toform['password'];
+ unset($toform['password']);
}
}
- function validation($data, $files) {
+ public function validation($data, $files) {
$errors = parent::validation($data, $files);
// Check open and close times are consistent.
- if ($data['timeopen'] != 0 && $data['timeclose'] != 0 && $data['timeclose'] < $data['timeopen']) {
+ if ($data['timeopen'] != 0 && $data['timeclose'] != 0 &&
+ $data['timeclose'] < $data['timeopen']) {
$errors['timeclose'] = get_string('closebeforeopen', 'quiz');
}
@@ -431,14 +464,18 @@ class mod_quiz_mod_form extends moodleform_mod {
if (is_numeric($boundary)) {
$boundary = $boundary * $data['grade'] / 100.0;
} else {
- $errors["feedbackboundaries[$i]"] = get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
+ $errors["feedbackboundaries[$i]"] =
+ get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
}
}
if (is_numeric($boundary) && $boundary <= 0 || $boundary >= $data['grade'] ) {
- $errors["feedbackboundaries[$i]"] = get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
+ $errors["feedbackboundaries[$i]"] =
+ get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
}
- if (is_numeric($boundary) && $i > 0 && $boundary >= $data['feedbackboundaries'][$i - 1]) {
- $errors["feedbackboundaries[$i]"] = get_string('feedbackerrororder', 'quiz', $i + 1);
+ if (is_numeric($boundary) && $i > 0 &&
+ $boundary >= $data['feedbackboundaries'][$i - 1]) {
+ $errors["feedbackboundaries[$i]"] =
+ get_string('feedbackerrororder', 'quiz', $i + 1);
}
$data['feedbackboundaries'][$i] = $boundary;
$i += 1;
@@ -448,19 +485,21 @@ class mod_quiz_mod_form extends moodleform_mod {
// Check there is nothing in the remaining unused fields.
if (!empty($data['feedbackboundaries'])) {
for ($i = $numboundaries; $i < count($data['feedbackboundaries']); $i += 1) {
- if (!empty($data['feedbackboundaries'][$i] ) && trim($data['feedbackboundaries'][$i] ) != '') {
- $errors["feedbackboundaries[$i]"] = get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
+ if (!empty($data['feedbackboundaries'][$i] ) &&
+ trim($data['feedbackboundaries'][$i] ) != '') {
+ $errors["feedbackboundaries[$i]"] =
+ get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
}
}
}
for ($i = $numboundaries + 1; $i < count($data['feedbacktext']); $i += 1) {
- if (!empty($data['feedbacktext'][$i]['text']) && trim($data['feedbacktext'][$i]['text'] ) != '') {
- $errors["feedbacktext[$i]"] = get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
+ if (!empty($data['feedbacktext'][$i]['text']) &&
+ trim($data['feedbacktext'][$i]['text'] ) != '') {
+ $errors["feedbacktext[$i]"] =
+ get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
}
}
return $errors;
}
-
}
-
diff --git a/mod/quiz/module.js b/mod/quiz/module.js
index 6d3b4757a01..7c11547ef85 100644
--- a/mod/quiz/module.js
+++ b/mod/quiz/module.js
@@ -1,7 +1,25 @@
-/*
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see
.
+
+/**
* JavaScript library for the quiz module.
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package mod
+ * @subpackage quiz
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
M.mod_quiz = M.mod_quiz || {};
@@ -131,7 +149,7 @@ M.mod_quiz.nav.init = function(Y) {
} else {
pageno = 0;
}
- Y.one('#nextpagehiddeninput').set('value', pageno);
+ Y.one('#followingpage').set('value', pageno);
var questionidmatch = this.get('href').match(/#q(\d+)/);
if (questionidmatch) {
@@ -145,7 +163,7 @@ M.mod_quiz.nav.init = function(Y) {
if (Y.one('a.endtestlink')) {
Y.on('click', function(e) {
e.preventDefault(e);
- Y.one('#nextpagehiddeninput').set('value', -1);
+ Y.one('#followingpage').set('value', -1);
Y.one('#responseform').submit();
}, 'a.endtestlink');
}
@@ -157,7 +175,7 @@ M.mod_quiz.nav.init = function(Y) {
M.mod_quiz.secure_window = {
init: function(Y) {
- if (window.location.href.substring(0,4) == 'file') {
+ if (window.location.href.substring(0, 4) == 'file') {
window.location = 'about:blank';
}
Y.delegate('contextmenu', M.mod_quiz.secure_window.prevent, document.body, '*');
@@ -198,6 +216,12 @@ M.mod_quiz.secure_window = {
e.halt();
},
+ init_close_button: function(Y, url) {
+ Y.on('click', function(e) {
+ M.mod_quiz.secure_window.close(url, 0)
+ }, '#secureclosebutton');
+ },
+
close: function(url, delay) {
setTimeout(function() {
if (window.opener) {
diff --git a/mod/quiz/override_form.php b/mod/quiz/override_form.php
index 00c3fc72826..c5e12f4dca2 100644
--- a/mod/quiz/override_form.php
+++ b/mod/quiz/override_form.php
@@ -1,5 +1,4 @@
.
-
/**
* Settings form for overrides in the quiz module.
*
- * @package mod_quiz
- * @copyright 2010 Matt Petro
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2010 Matt Petro
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
-}
-require_once $CFG->libdir.'/formslib.php';
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/formslib.php');
+
+
+/**
+ * Form for editing settings overrides.
+ *
+ * @copyright 2010 Matt Petro
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class quiz_override_form extends moodleform {
protected $cm; // course module object
@@ -39,7 +44,7 @@ class quiz_override_form extends moodleform {
protected $groupid; // groupid, if provided
protected $userid; // userid, if provided
- public function quiz_override_form($submiturl, $cm, $quiz, $context, $groupmode, $override) {
+ public function __construct($submiturl, $cm, $quiz, $context, $groupmode, $override) {
$this->cm = $cm;
$this->quiz = $quiz;
@@ -48,11 +53,11 @@ class quiz_override_form extends moodleform {
$this->groupid = empty($override->groupid) ? 0 : $override->groupid;
$this->userid = empty($override->userid) ? 0 : $override->userid;
- parent::moodleform($submiturl, null, 'post');
+ parent::__construct($submiturl, null, 'post');
}
- public function definition() {
+ protected function definition() {
global $CFG, $USER, $DB;
$cm = $this->cm;
@@ -66,7 +71,8 @@ class quiz_override_form extends moodleform {
// There is already a groupid, so freeze the selector
$groupchoices = array();
$groupchoices[$this->groupid] = groups_get_group_name($this->groupid);
- $mform->addElement('select', 'groupid', get_string('overridegroup', 'quiz'), $groupchoices);
+ $mform->addElement('select', 'groupid',
+ get_string('overridegroup', 'quiz'), $groupchoices);
$mform->freeze('groupid');
} else {
// Prepare the list of groups
@@ -87,7 +93,8 @@ class quiz_override_form extends moodleform {
$groupchoices[0] = get_string('none');
}
- $mform->addElement('select', 'groupid', get_string('overridegroup', 'quiz'), $groupchoices);
+ $mform->addElement('select', 'groupid',
+ get_string('overridegroup', 'quiz'), $groupchoices);
$mform->addRule('groupid', get_string('required'), 'required', null, 'client');
}
} else {
@@ -97,7 +104,8 @@ class quiz_override_form extends moodleform {
$user = $DB->get_record('user', array('id'=>$this->userid));
$userchoices = array();
$userchoices[$this->userid] = fullname($user);
- $mform->addElement('select', 'userid', get_string('overrideuser', 'quiz'), $userchoices);
+ $mform->addElement('select', 'userid',
+ get_string('overrideuser', 'quiz'), $userchoices);
$mform->freeze('userid');
} else {
// Prepare the list of users
@@ -105,15 +113,16 @@ class quiz_override_form extends moodleform {
if (!empty($CFG->enablegroupmembersonly) && $cm->groupmembersonly) {
// only users from the grouping
$groups = groups_get_all_groups($cm->course, 0, $cm->groupingid);
- if (empty($groups)) {
- // empty grouping
- } else {
- $users = get_users_by_capability($this->context, 'mod/quiz:attempt', 'u.id,u.firstname,u.lastname,u.email' ,
- 'firstname ASC, lastname ASC', '', '', array_keys($groups), '', false, true);
+ if (!empty($groups)) {
+ $users = get_users_by_capability($this->context, 'mod/quiz:attempt',
+ 'u.id, u.firstname, u.lastname, u.email',
+ 'firstname ASC, lastname ASC', '', '', array_keys($groups),
+ '', false, true);
}
} else {
- $users = get_users_by_capability($this->context, 'mod/quiz:attempt', 'u.id,u.firstname,u.lastname,u.email' ,
- 'firstname ASC, lastname ASC', '', '', '', '', false, true);
+ $users = get_users_by_capability($this->context, 'mod/quiz:attempt',
+ 'u.id, u.firstname, u.lastname, u.email' ,
+ 'firstname ASC, lastname ASC', '', '', '', '', false, true);
}
if (empty($users)) {
// generate an error
@@ -122,8 +131,9 @@ class quiz_override_form extends moodleform {
}
$userchoices = array();
- foreach ($users as $id=>$user) {
- if (empty($invalidusers[$id]) || (!empty($override) && $id == $override->userid)) {
+ foreach ($users as $id => $user) {
+ if (empty($invalidusers[$id]) || (!empty($override) &&
+ $id == $override->userid)) {
$userchoices[$id] = fullname($user) . ', ' . $user->email;
}
}
@@ -132,7 +142,8 @@ class quiz_override_form extends moodleform {
if (count($userchoices) == 0) {
$userchoices[0] = get_string('none');
}
- $mform->addElement('searchableselector', 'userid', get_string('overrideuser', 'quiz'), $userchoices);
+ $mform->addElement('searchableselector', 'userid',
+ get_string('overrideuser', 'quiz'), $userchoices);
$mform->addRule('userid', get_string('required'), 'required', null, 'client');
}
}
@@ -146,14 +157,17 @@ class quiz_override_form extends moodleform {
$mform->setDefault('password', $this->quiz->password);
// Open and close dates.
- $mform->addElement('date_time_selector', 'timeopen', get_string('quizopen', 'quiz'), array('optional' => true));
+ $mform->addElement('date_time_selector', 'timeopen',
+ get_string('quizopen', 'quiz'), array('optional' => true));
$mform->setDefault('timeopen', $this->quiz->timeopen);
- $mform->addElement('date_time_selector', 'timeclose', get_string('quizclose', 'quiz'), array('optional' => true));
+ $mform->addElement('date_time_selector', 'timeclose',
+ get_string('quizclose', 'quiz'), array('optional' => true));
$mform->setDefault('timeclose', $this->quiz->timeclose);
// Time limit.
- $mform->addElement('duration', 'timelimit', get_string('timelimit', 'quiz'), array('optional' => true));
+ $mform->addElement('duration', 'timelimit',
+ get_string('timelimit', 'quiz'), array('optional' => true));
$mform->addHelpButton('timelimit', 'timelimit', 'quiz');
$mform->setDefault('timelimit', $this->quiz->timelimit);
@@ -162,15 +176,19 @@ class quiz_override_form extends moodleform {
for ($i = 1; $i <= QUIZ_MAX_ATTEMPT_OPTION; $i++) {
$attemptoptions[$i] = $i;
}
- $mform->addElement('select', 'attempts', get_string('attemptsallowed', 'quiz'), $attemptoptions);
+ $mform->addElement('select', 'attempts',
+ get_string('attemptsallowed', 'quiz'), $attemptoptions);
$mform->setDefault('attempts', $this->quiz->attempts);
// Submit buttons
- $mform->addElement('submit', 'resetbutton', get_string('reverttodefaults','quiz'));
+ $mform->addElement('submit', 'resetbutton',
+ get_string('reverttodefaults', 'quiz'));
$buttonarray = array();
- $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('save', 'quiz'));
- $buttonarray[] = $mform->createElement('submit', 'againbutton', get_string('saveoverrideandstay', 'quiz'));
+ $buttonarray[] = $mform->createElement('submit', 'submitbutton',
+ get_string('save', 'quiz'));
+ $buttonarray[] = $mform->createElement('submit', 'againbutton',
+ get_string('saveoverrideandstay', 'quiz'));
$buttonarray[] = $mform->createElement('cancel');
$mform->addGroup($buttonarray, 'buttonbar', '', array(' '), false);
@@ -207,7 +225,7 @@ class quiz_override_form extends moodleform {
// Ensure that at least one quiz setting was changed
$changed = false;
- $keys = array('timeopen','timeclose', 'timelimit', 'attempts', 'password');
+ $keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password');
foreach ($keys as $key) {
if ($data[$key] != $quiz->{$key}) {
$changed = true;
diff --git a/mod/quiz/overridedelete.php b/mod/quiz/overridedelete.php
index 5170787d706..188704832cd 100644
--- a/mod/quiz/overridedelete.php
+++ b/mod/quiz/overridedelete.php
@@ -1,5 +1,4 @@
.
-
/**
* This page handles deleting quiz overrides
*
- * @package mod_quiz
- * @copyright 2010 Matt Petro
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2010 Matt Petro
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot.'/mod/quiz/lib.php');
require_once($CFG->dirroot.'/mod/quiz/locallib.php');
@@ -85,11 +85,11 @@ $PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
if ($override->groupid) {
- $group = $DB->get_record('groups', array('id' => $override->groupid), 'id,name');
+ $group = $DB->get_record('groups', array('id' => $override->groupid), 'id, name');
$confirmstr = get_string("overridedeletegroupsure", "quiz", $group->name);
-}
-else {
- $user = $DB->get_record('user', array('id' => $override->userid), 'id,firstname,lastname');
+} else {
+ $user = $DB->get_record('user', array('id' => $override->userid),
+ 'id, firstname, lastname');
$confirmstr = get_string("overridedeleteusersure", "quiz", fullname($user));
}
diff --git a/mod/quiz/overrideedit.php b/mod/quiz/overrideedit.php
index d340435157a..8461f4d499d 100644
--- a/mod/quiz/overrideedit.php
+++ b/mod/quiz/overrideedit.php
@@ -1,5 +1,4 @@
.
-
/**
* This page handles editing and creation of quiz overrides
*
- * @package mod_quiz
- * @copyright 2010 Matt Petro
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2010 Matt Petro
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot.'/mod/quiz/lib.php');
require_once($CFG->dirroot.'/mod/quiz/locallib.php');
require_once($CFG->dirroot.'/mod/quiz/override_form.php');
-$cmid = optional_param('cmid', 0, PARAM_INT); // course module ID, if new override
-$overrideid = optional_param('id', 0, PARAM_INT); // override ID, if editing existing override
-$action = optional_param('action', null, PARAM_ALPHA); // if creating new override, one of 'adduser','addgroup', or 'duplicate'
-$reset = optional_param('reset', false, PARAM_BOOL); // reset form to defaults
+$cmid = optional_param('cmid', 0, PARAM_INT);
+$overrideid = optional_param('id', 0, PARAM_INT);
+$action = optional_param('action', null, PARAM_ALPHA);
+$reset = optional_param('reset', false, PARAM_BOOL);
$override = null;
if ($overrideid) {
@@ -83,28 +83,29 @@ require_capability('mod/quiz:manageoverrides', $context);
if ($overrideid) {
// editing override
$data = clone $override;
-}
-else {
+} else {
// new override
$data = new stdClass();
}
// merge quiz defaults with data
-$keys = array('timeopen','timeclose', 'timelimit', 'attempts', 'password');
+$keys = array('timeopen', 'timeclose', 'timelimit', 'attempts', 'password');
foreach ($keys as $key) {
if (!isset($data->{$key}) || $reset) {
$data->{$key} = $quiz->{$key};
}
}
-// If we are duplicating an override, then clear the user/group and override id since they will change
+// If we are duplicating an override, then clear the user/group and override id
+// since they will change.
if ($action === 'duplicate') {
$override->id = null;
$override->userid = null;
$override->groupid = null;
}
-$groupmode = !empty($data->groupid) || ($action === 'addgroup' && empty($overrideid)); // true if group-based override
+// true if group-based override
+$groupmode = !empty($data->groupid) || ($action === 'addgroup' && empty($overrideid));
$overridelisturl = new moodle_url('/mod/quiz/overrides.php', array('cmid'=>$cm->id));
if (!$groupmode) {
@@ -150,7 +151,7 @@ if ($mform->is_cancelled()) {
if ($oldoverride = $DB->get_record('quiz_overrides', $conditions)) {
// There is an old override, so we merge any new settings on top of
// the older override
- foreach ($keys as $key) {
+ foreach ($keys as $key) {
if (is_null($fromform->{$key})) {
$fromform->{$key} = $oldoverride->{$key};
}
@@ -163,8 +164,7 @@ if ($mform->is_cancelled()) {
if (!empty($override->id)) {
$fromform->id = $override->id;
$DB->update_record('quiz_overrides', $fromform);
- }
- else {
+ } else {
unset($fromform->id);
$fromform->id = $DB->insert_record('quiz_overrides', $fromform);
}
diff --git a/mod/quiz/overrides.php b/mod/quiz/overrides.php
index 5f5c6da6e20..6c2147415be 100644
--- a/mod/quiz/overrides.php
+++ b/mod/quiz/overrides.php
@@ -1,5 +1,4 @@
.
-
/**
* This page handles listing of quiz overrides
*
- * @package mod_quiz
- * @copyright 2010 Matt Petro
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2010 Matt Petro
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot.'/mod/quiz/lib.php');
require_once($CFG->dirroot.'/mod/quiz/locallib.php');
@@ -95,8 +95,7 @@ if ($groupmode) {
ON o.groupid = g.id
WHERE o.quiz = ?
ORDER BY g.name';
-}
-else {
+} else {
$colname = get_string('user');
$sql = 'SELECT o.*, u.firstname, u.lastname
FROM {quiz_overrides} o JOIN {user} u
@@ -110,8 +109,8 @@ $overrides = $DB->get_records_sql($sql, $params);
// Initialise table
$table = new html_table();
-$table->headspan = array(1,2,1);
-$table->colclasses = array('colname','colsetting','colvalue','colaction');
+$table->headspan = array(1, 2, 1);
+$table->colclasses = array('colname', 'colsetting', 'colvalue', 'colaction');
$table->head = array(
$colname,
get_string('overrides', 'quiz'),
@@ -137,7 +136,8 @@ foreach ($overrides as $override) {
if (!has_capability('mod/quiz:attempt', $context, $override->userid)) {
// user not allowed to take the quiz
$active = false;
- } else if (!empty($CFG->enablegroupmembersonly) && $cm->groupmembersonly && !groups_has_membership($cm, $override->userid)) {
+ } else if (!empty($CFG->enablegroupmembersonly) && $cm->groupmembersonly &&
+ !groups_has_membership($cm, $override->userid)) {
// user does not belong to the current grouping
$active = false;
}
@@ -146,31 +146,36 @@ foreach ($overrides as $override) {
// Format timeopen
if (isset($override->timeopen)) {
$fields[] = get_string('quizopens', 'quiz');
- $values[] = ($override->timeopen > 0)? userdate($override->timeopen) : get_string('noopen', 'quiz');
+ $values[] = $override->timeopen > 0 ?
+ userdate($override->timeopen) : get_string('noopen', 'quiz');
}
// Format timeclose
if (isset($override->timeclose)) {
$fields[] = get_string('quizcloses', 'quiz');
- $values[] = ($override->timeclose > 0)? userdate($override->timeclose) : get_string('noclose', 'quiz');
+ $values[] = $override->timeclose > 0 ?
+ userdate($override->timeclose) : get_string('noclose', 'quiz');
}
// Format timelimit
if (isset($override->timelimit)) {
$fields[] = get_string('timelimit', 'quiz');
- $values[] = ($override->timelimit > 0)? format_time($override->timelimit) : get_string('none', 'quiz');
+ $values[] = $override->timelimit > 0 ?
+ format_time($override->timelimit) : get_string('none', 'quiz');
}
// Format number of attempts
if (isset($override->attempts)) {
$fields[] = get_string('attempts', 'quiz');
- $values[] = ($override->attempts > 0)? $override->attempts : get_string('unlimited');
+ $values[] = $override->attempts > 0 ?
+ $override->attempts : get_string('unlimited');
}
// Format password
if (isset($override->password)) {
$fields[] = get_string('requirepassword', 'quiz');
- $values[] = ($override->password !== '')? get_string('enabled', 'quiz') : get_string('none', 'quiz');
+ $values[] = $override->password !== '' ?
+ get_string('enabled', 'quiz') : get_string('none', 'quiz');
}
// Icons:
@@ -181,22 +186,28 @@ foreach ($overrides as $override) {
// edit
$editurlstr = $overrideediturl->out(true, array('id' => $override->id));
$iconstr = '
' .
- ' ';
+ '
';
// duplicate
- $copyurlstr = $overrideediturl->out(true, array('id' => $override->id, 'action' => 'duplicate'));
+ $copyurlstr = $overrideediturl->out(true,
+ array('id' => $override->id, 'action' => 'duplicate'));
$iconstr .= '
' .
- ' ';
+ '
';
}
// delete
- $deleteurlstr = $overridedeleteurl->out(true, array('id' => $override->id, 'sesskey' => sesskey()));
+ $deleteurlstr = $overridedeleteurl->out(true,
+ array('id' => $override->id, 'sesskey' => sesskey()));
$iconstr .= '
' .
- ' ';
+ '
';
if ($groupmode) {
- $usergroupstr = '
' . $override->name . '';
- }
- else {
- $usergroupstr = '
' . fullname($override) . '';
+ $usergroupstr = '
' . $override->name . '';
+ } else {
+ $usergroupstr = '
' . fullname($override) . '';
}
$class = '';
@@ -250,8 +261,9 @@ if ($groupmode) {
echo $OUTPUT->notification(get_string('groupsnone', 'quiz'), 'error');
$options['disabled'] = true;
}
- echo $OUTPUT->single_button($overrideediturl->out(true, array('action' => 'addgroup', 'cmid' => $cm->id)),
- get_string('addnewgroupoverride', 'quiz'), 'post', $options);
+ echo $OUTPUT->single_button($overrideediturl->out(true,
+ array('action' => 'addgroup', 'cmid' => $cm->id)),
+ get_string('addnewgroupoverride', 'quiz'), 'post', $options);
} else {
$users = array();
// See if there are any students in the quiz
@@ -259,12 +271,12 @@ if ($groupmode) {
// restrict to grouping
$limitgroups = groups_get_all_groups($cm->course, 0, $cm->groupingid);
if (!empty($limitgroups)) {
- $users = get_users_by_capability($context, 'mod/quiz:attempt', 'u.id', '', '', 1, array_keys($limitgroups)); // Limit to one user for speed
- } else {
- // empty grouping
+ $users = get_users_by_capability($context, 'mod/quiz:attempt', 'u.id',
+ '', '', 1, array_keys($limitgroups)); // Limit to one user for speed
}
} else {
- $users = get_users_by_capability($context, 'mod/quiz:attempt', 'u.id'); // Limit to one user for speed
+ // Limit to one user for speed.
+ $users = get_users_by_capability($context, 'mod/quiz:attempt', 'u.id');
}
if (empty($users)) {
@@ -272,8 +284,9 @@ if ($groupmode) {
echo $OUTPUT->notification(get_string('usersnone', 'quiz'), 'error');
$options['disabled'] = true;
}
- echo $OUTPUT->single_button($overrideediturl->out(true, array('action' => 'adduser', 'cmid' => $cm->id)),
- get_string('addnewuseroverride', 'quiz'), 'post', $options);
+ echo $OUTPUT->single_button($overrideediturl->out(true,
+ array('action' => 'adduser', 'cmid' => $cm->id)),
+ get_string('addnewuseroverride', 'quiz'), 'post', $options);
}
echo html_writer::end_tag('div');
echo html_writer::end_tag('div');
diff --git a/mod/quiz/pix/icon.gif b/mod/quiz/pix/icon.gif
deleted file mode 100644
index 47b7040d2b1..00000000000
Binary files a/mod/quiz/pix/icon.gif and /dev/null differ
diff --git a/mod/quiz/pix/icon.png b/mod/quiz/pix/icon.png
new file mode 100644
index 00000000000..dcaf4565e4e
Binary files /dev/null and b/mod/quiz/pix/icon.png differ
diff --git a/mod/quiz/pix/navflagged.png b/mod/quiz/pix/navflagged.png
new file mode 100644
index 00000000000..e1e10b7fbb8
Binary files /dev/null and b/mod/quiz/pix/navflagged.png differ
diff --git a/mod/quiz/processattempt.php b/mod/quiz/processattempt.php
index d2f6f8b22ac..7d74e19432c 100644
--- a/mod/quiz/processattempt.php
+++ b/mod/quiz/processattempt.php
@@ -1,4 +1,19 @@
.
+
/**
* This page deals with processing responses during an attempt at a quiz.
*
@@ -8,194 +23,96 @@
*
* This code used to be near the top of attempt.php, if you are looking for CVS history.
*
- * @author Tim Hunt.
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2009 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
-/// Remember the current time as the time any responses were submitted
-/// (so as to make sure students don't get penalized for slow processing on this page)
+// Remember the current time as the time any responses were submitted
+// (so as to make sure students don't get penalized for slow processing on this page)
$timenow = time();
-/// Get submitted parameters.
+// Get submitted parameters.
$attemptid = required_param('attempt', PARAM_INT);
+$next = optional_param('next', false, PARAM_BOOL);
+$thispage = optional_param('thispage', 0, PARAM_INT);
$nextpage = optional_param('nextpage', 0, PARAM_INT);
-$submittedquestionids = required_param('questionids', PARAM_SEQUENCE);
$finishattempt = optional_param('finishattempt', 0, PARAM_BOOL);
$timeup = optional_param('timeup', 0, PARAM_BOOL); // True if form was submitted by timer.
+$scrollpos = optional_param('scrollpos', '', PARAM_RAW);
+$transaction = $DB->start_delegated_transaction();
$attemptobj = quiz_attempt::create($attemptid);
-/// Set $nexturl now. It will be updated if a particular question was sumbitted in
-/// adaptive mode.
-if ($nextpage == -1) {
+// Set $nexturl now.
+if ($next) {
+ $page = $nextpage;
+} else {
+ $page = $thispage;
+}
+if ($page == -1) {
$nexturl = $attemptobj->summary_url();
} else {
- $nexturl = $attemptobj->attempt_url(0, $nextpage);
+ $nexturl = $attemptobj->attempt_url(0, $page);
+ if ($scrollpos !== '') {
+ $nexturl->param('scrollpos', $scrollpos);
+ }
}
-/// We treat automatically closed attempts just like normally closed attempts
+// We treat automatically closed attempts just like normally closed attempts
if ($timeup) {
$finishattempt = 1;
}
-/// Check login.
-require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
+// Check login.
+require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
require_sesskey();
-/// Check that this attempt belongs to this user.
+// Check that this attempt belongs to this user.
if ($attemptobj->get_userid() != $USER->id) {
- quiz_error($attemptobj->get_quiz(), 'notyourattempt');
+ throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'notyourattempt');
}
-/// Check capabilities.
+// Check capabilities.
if (!$attemptobj->is_preview_user()) {
$attemptobj->require_capability('mod/quiz:attempt');
}
-/// If the attempt is already closed, send them to the review page.
+// If the attempt is already closed, send them to the review page.
if ($attemptobj->is_finished()) {
- quiz_error($attemptobj->get_quiz(), 'attemptalreadyclosed');
+ throw new moodle_quiz_exception($attemptobj->get_quizobj(),
+ 'attemptalreadyclosed', null, $attemptobj->review_url());
}
-/// Don't log - we will end with a redirect to a page that is logged.
+// Don't log - we will end with a redirect to a page that is logged.
-/// Get the list of questions needed by this page.
-if (!empty($submittedquestionids)) {
- $submittedquestionids = explode(',', $submittedquestionids);
-} else {
- $submittedquestionids = array();
-}
-if ($finishattempt) {
- $questionids = $attemptobj->get_question_ids();
-} else {
- $questionids = $submittedquestionids;
-}
-
-/// Load those questions we need, and just the submitted states for now.
-$attemptobj->load_questions($questionids);
-if (!empty($submittedquestionids)) {
- $attemptobj->load_question_states($submittedquestionids);
-}
-
-/// Process the responses /////////////////////////////////////////////////
-if (!$responses = data_submitted()) {
- quiz_error($attemptobj->get_quiz(), 'nodatasubmitted');
-}
-
-/// Set the default event. This can be overruled by individual buttons.
-if ($finishattempt) {
- $event = QUESTION_EVENTCLOSE;
-} else {
- $event = QUESTION_EVENTSAVE;
-}
-
-/// Unset any variables we know are not responses
-unset($responses->id);
-unset($responses->q);
-unset($responses->oldpage);
-unset($responses->newpage);
-unset($responses->review);
-unset($responses->questionids);
-unset($responses->finishattempt); // same as $finishattempt
-unset($responses->forcenewattempt);
-
-/// Extract the responses. $actions will be an array indexed by the questions ids.
-$actions = question_extract_responses($attemptobj->get_questions($submittedquestionids),
- $responses, $event);
-
-/// Process each question in turn
-$success = true;
-$attempt = $attemptobj->get_attempt();
-foreach($submittedquestionids as $id) {
- if (!isset($actions[$id])) {
- $actions[$id]->responses = array('' => '');
- $actions[$id]->event = QUESTION_EVENTOPEN;
- }
- $actions[$id]->timestamp = $timenow;
-
-/// If a particular question was submitted, update the nexturl to go back to that question.
- if ($actions[$id]->event == QUESTION_EVENTSUBMIT) {
- $nexturl = $attemptobj->attempt_url($id);
- }
-
- $state = $attemptobj->get_question_state($id);
- if (question_process_responses($attemptobj->get_question($id),
- $state, $actions[$id], $attemptobj->get_quiz(), $attempt)) {
- save_question_session($attemptobj->get_question($id), $state);
- } else {
- $success = false;
- }
-}
-
-if (!$success) {
- print_error('errorprocessingresponses', 'question', $attemptobj->attempt_url(0, $page));
-}
-
-/// If we do not have to finish the attempts (if we are only processing responses)
-/// save the attempt and redirect to the next page.
if (!$finishattempt) {
- $attempt->timemodified = $timenow;
- $DB->update_record('quiz_attempts', $attempt);
-
+ // Just process the responses for this page and go to the next page.
+ try {
+ $attemptobj->process_all_actions($timenow);
+ } catch (question_out_of_sequence_exception $e) {
+ print_error('submissionoutofsequencefriendlymessage', 'question',
+ $attemptobj->attempt_url(0, $thispage));
+ }
+ $transaction->allow_commit();
redirect($nexturl);
}
-/// We have been asked to finish attempt, so do that //////////////////////
+// Otherwise, we have been asked to finish attempt, so do that.
-/// Now load the state of every question, reloading the ones we messed around
-/// with above.
-$attemptobj->preload_question_states();
-$attemptobj->load_question_states();
-
-/// Move each question to the closed state.
-$success = true;
-$attempt = $attemptobj->get_attempt();
-foreach ($attemptobj->get_questions() as $id => $question) {
- $state = $attemptobj->get_question_state($id);
- $action = new stdClass;
- $action->event = QUESTION_EVENTCLOSE;
- $action->responses = $state->responses;
- $action->responses['_flagged'] = $state->flagged;
- $action->timestamp = $state->timestamp;
- if (question_process_responses($attemptobj->get_question($id),
- $state, $action, $attemptobj->get_quiz(), $attempt)) {
- save_question_session($attemptobj->get_question($id), $state);
- } else {
- $success = false;
- }
-}
-
-if (!$success) {
- print_error('errorprocessingresponses', 'question', $attemptobj->attempt_url(0, $page));
-}
-
-/// Log the end of this attempt.
+// Log the end of this attempt.
add_to_log($attemptobj->get_courseid(), 'quiz', 'close attempt',
'review.php?attempt=' . $attemptobj->get_attemptid(),
$attemptobj->get_quizid(), $attemptobj->get_cmid());
-/// Update the quiz attempt record.
-$attempt->timemodified = $timenow;
-$attempt->timefinish = $timenow;
-$DB->update_record('quiz_attempts', $attempt);
+// Update the quiz attempt record.
+$attemptobj->finish_attempt($timenow);
-if (!$attempt->preview) {
-/// Record this user's best grade (if this is not a preview).
- quiz_save_best_grade($attemptobj->get_quiz());
-
-/// Send any notification emails (if this is not a preview).
- $attemptobj->quiz_send_notification_emails();
-}
-
-/// Clear the password check flag in the session.
-$accessmanager = $attemptobj->get_access_manager($timenow);
-$accessmanager->clear_password_access();
-
-/// Trigger event
+// Trigger event
$eventdata = new stdClass();
$eventdata->component = 'mod_quiz';
$eventdata->course = $attemptobj->get_courseid();
@@ -205,5 +122,10 @@ $eventdata->user = $USER;
$eventdata->attempt = $attemptobj->get_attemptid();
events_trigger('quiz_attempt_processed', $eventdata);
-/// Send the user to the review page.
+// Clear the password check flag in the session.
+$accessmanager = $attemptobj->get_access_manager($timenow);
+$accessmanager->clear_password_access();
+
+// Send the user to the review page.
+$transaction->allow_commit();
redirect($attemptobj->review_url());
diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php
new file mode 100644
index 00000000000..e957156bbc7
--- /dev/null
+++ b/mod/quiz/renderer.php
@@ -0,0 +1,1000 @@
+.
+
+/**
+ * Defines the renderer for the quiz module.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * The renderer for the quiz module.
+ *
+ * @copyright 2011 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_quiz_renderer extends plugin_renderer_base {
+ /**
+ * Builds the review page
+ *
+ * @param quiz_attempt $attemptobj an instance of quiz_attempt.
+ * @param array $slots an array of intgers relating to questions.
+ * @param int $page the current page number
+ * @param bool $showall whether to show entire attempt on one page.
+ * @param bool $lastpage if true the current page is the last page.
+ * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options.
+ * @param array $summarydata contains all table data
+ * @return $output containing html data.
+ */
+ public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall,
+ $lastpage, mod_quiz_display_options $displayoptions,
+ $summarydata) {
+
+ $output = '';
+ $output .= $this->header();
+ $output .= $this->review_summary_table($summarydata, $page);
+ $output .= $this->review_form($page, $showall, $displayoptions,
+ $this->questions($attemptobj, true, $slots, $page, $showall, $displayoptions),
+ $attemptobj, $showall);
+
+ $output .= $this->review_next_navigation($attemptobj, $page, $lastpage);
+ $output .= $this->footer();
+ return $output;
+ }
+
+ /**
+ * Filters the summarydata array.
+ *
+ * @param array $summarydata contains row data for table
+ * @param int $page the current page number
+ * @return $summarydata containing filtered row data
+ */
+ protected function filter_summary_table($summarydata, $page) {
+ if ($page == 0) {
+ return $summarydata;
+ }
+
+ // Only show some of summary table on subsequent pages.
+ foreach ($summarydata as $key => $rowdata) {
+ if (!in_array($key, array('user', 'attemptlist'))) {
+ unset($summarydata[$key]);
+ }
+ }
+
+ return $summarydata;
+ }
+
+ /**
+ * Outputs the table containing data from summary data array
+ *
+ * @param array $summarydata contains row data for table
+ * @param int $page contains the current page number
+ */
+ public function review_summary_table($summarydata, $page) {
+ $summarydata = $this->filter_summary_table($summarydata,
+ $page);
+ if (empty($summarydata)) {
+ return '';
+ }
+
+ $output = '';
+ $output .= html_writer::start_tag('table', array(
+ 'class' => 'generaltable generalbox quizreviewsummary'));
+ $output .= html_writer::start_tag('tbody');
+ foreach ($summarydata as $rowdata) {
+ if ($rowdata['title'] instanceof renderable) {
+ $title = $this->render($rowdata['title']);
+ } else {
+ $title = $rowdata['title'];
+ }
+
+ if ($rowdata['content'] instanceof renderable) {
+ $content = $this->render($rowdata['content']);
+ } else {
+ $content = $rowdata['content'];
+ }
+
+ $output .= html_writer::tag('tr',
+ html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) .
+ html_writer::tag('td', $content, array('class' => 'cell'))
+ );
+ }
+
+ $output .= html_writer::end_tag('tbody');
+ $output .= html_writer::end_tag('table');
+ return $output;
+ }
+
+ /**
+ * Renders each question
+ *
+ * @param quiz_attempt $attemptobj instance of quiz_attempt
+ * @param bool $reviewing
+ * @param array $slots array of intgers relating to questions
+ * @param int $page current page number
+ * @param bool $showall if true shows attempt on single page
+ * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options
+ */
+ public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall,
+ mod_quiz_display_options $displayoptions) {
+ $output = '';
+ foreach ($slots as $slot) {
+ $output .= $attemptobj->render_question($slot, $reviewing,
+ $attemptobj->review_url($slot, $page, $showall));
+ }
+ return $output;
+ }
+
+ /**
+ * Renders the main bit of the review page.
+ *
+ * @param array $summarydata contain row data for table
+ * @param int $page current page number
+ * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options
+ * @param $content contains each question
+ * @param quiz_attempt $attemptobj instance of quiz_attempt
+ * @param bool $showall if true display attempt on one page
+ */
+ public function review_form($summarydata, $page, $displayoptions, $content, $attemptobj,
+ $showall) {
+ if ($displayoptions->flags != question_display_options::EDITABLE) {
+ return $content;
+ }
+
+ $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false,
+ quiz_get_js_module());
+
+ $output = '';
+ $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(0,
+ $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform'));
+ $output .= html_writer::start_tag('div');
+ $output .= $content;
+ $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
+ 'value' => sesskey()));
+ $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
+ $output .= html_writer::empty_tag('input', array('type' => 'submit',
+ 'class' => 'questionflagsavebutton', 'name' => 'savingflags',
+ 'value' => get_string('saveflags', 'question')));
+ $output .= html_writer::end_tag('div');
+ $output .= html_writer::end_tag('div');
+ $output .= html_writer::end_tag('form');
+
+ return $output;
+ }
+
+ /**
+ * Returns either a liink or button
+ *
+ * @param $url contains a url for the review link
+ */
+ public function finish_review_link($url) {
+ if ($this->page->pagelayout == 'popup') {
+ // In a 'secure' popup window.
+ $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button',
+ array($url), quiz_get_js_module());
+ return html_writer::empty_tag('input', array('type' => 'button',
+ 'value' => get_string('finishreview', 'quiz'),
+ 'id' => 'secureclosebutton'));
+ } else {
+ return html_writer::link($url, get_string('finishreview', 'quiz'));
+ }
+ }
+
+ /**
+ * Creates a next page arrow or the finishing link
+ *
+ * @param quiz_attempt $attemptobj instance of quiz_attempt
+ * @param int $page the current page
+ * @param bool $lastpage if true current page is the last page
+ */
+ public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage) {
+ if ($lastpage) {
+ $nav = $this->finish_review_link($attemptobj->view_url());
+ } else {
+ $nav = link_arrow_right(get_string('next'), $attemptobj->review_url(0, $page + 1));
+ }
+ return html_writer::tag('div', $nav, array('class' => 'submitbtns'));
+ }
+
+ /**
+ * Return the HTML of the quiz timer.
+ * @return string HTML content.
+ */
+ public function countdown_timer() {
+ return html_writer::tag('div', get_string('timeleft', 'quiz') .
+ html_writer::tag('span', '', array('id' => 'quiz-time-left')),
+ array('id' => 'quiz-timer'));
+ }
+
+ /**
+ * Create a preview link
+ *
+ * @param $url contains a url to the given page
+ */
+ public function restart_preview_button($url) {
+ return $this->single_button($url, get_string('startnewpreview', 'quiz'));
+ }
+
+ /**
+ * Outputs the navigation block panel
+ *
+ * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base
+ */
+ public function navigation_panel(quiz_nav_panel_base $panel) {
+
+ $output = '';
+ $userpicture = $panel->user_picture();
+ if ($userpicture) {
+ $output .= html_writer::tag('div', $this->render($userpicture),
+ array('id' => 'user-picture', 'class' => 'clearfix'));
+ }
+ $output .= $panel->render_before_button_bits($this);
+
+ $output = html_writer::start_tag('div', array('class' => 'qn_buttons'));
+ foreach ($panel->get_question_buttons() as $button) {
+ $output .= $this->render($button);
+ }
+ $output .= html_writer::end_tag('div');
+
+ $output .= html_writer::tag('div', $panel->render_end_bits($this),
+ array('class' => 'othernav'));
+
+ $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false,
+ quiz_get_js_module());
+
+ return $output;
+ }
+
+ /**
+ * Returns the quizzes navigation button
+ *
+ * @param quiz_nav_question_button $button
+ */
+ protected function render_quiz_nav_question_button(quiz_nav_question_button $button) {
+ $classes = array('qnbutton', $button->stateclass);
+ $attributes = array();
+
+ if ($button->currentpage) {
+ $classes[] = 'thispage';
+ $attributes[] = get_string('onthispage', 'quiz');
+ }
+
+ $attributes[] = $button->statestring;
+
+ // Flagged?
+ if ($button->flagged) {
+ $classes[] = 'flagged';
+ $flaglabel = get_string('flagged', 'question');
+ } else {
+ $flaglabel = '';
+ }
+ $attributes[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate'));
+
+ if (is_numeric($button->number)) {
+ $qnostring = 'questionnonav';
+ } else {
+ $qnostring = 'questionnonavinfo';
+ }
+
+ $a = new stdClass();
+ $a->number = $button->number;
+ $a->attributes = implode(' ', $attributes);
+
+ return html_writer::link($button->url,
+ html_writer::tag('span', '', array('class' => 'thispageholder')) .
+ html_writer::tag('span', '', array('class' => 'trafficlight')) .
+ get_string($qnostring, 'quiz', $a),
+ array('class' => implode(' ', $classes), 'id' => $button->id,
+ 'title' => $button->statestring));
+ }
+
+ /**
+ * outputs the link the other attempts.
+ *
+ * @param mod_quiz_links_to_other_attempts $links
+ */
+ protected function render_mod_quiz_links_to_other_attempts(
+ mod_quiz_links_to_other_attempts $links) {
+ $attemptlinks = array();
+ foreach ($links->links as $attempt => $url) {
+ if ($url) {
+ $attemptlinks[] = html_writer::link($url, $attempt);
+ } else {
+ $attemptlinks[] = html_writer::tag('strong', $attempt);
+ }
+ }
+ return implode(', ', $attemptlinks);
+ }
+
+ /**
+ * Attempt Page
+ *
+ * @param quiz_attempt $attemptobj Instance of quiz_attempt
+ * @param int $page Current page number
+ * @param quiz_access_manager $accessmanager Instance of quiz_access_manager
+ * @param array $messages An array of messages
+ * @param array $slots Contains an array of integers that relate to questions
+ * @param int $id The ID of an attempt
+ * @param int $nextpage The number of the next page
+ */
+ public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id,
+ $nextpage) {
+ $output = '';
+ $output .= $this->quiz_notices($messages);
+ $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage);
+ return $output;
+ }
+
+ /**
+ * Returns any notices.
+ *
+ * @param array $messages
+ */
+ public function quiz_notices($messages) {
+ if (!$messages) {
+ return '';
+ }
+ return $this->box($this->heading(get_string('accessnoticesheader', 'quiz'), 3) .
+ $this->access_messages($messages), 'quizaccessnotices');
+ }
+
+ /**
+ * Ouputs the form for making an attempt
+ *
+ * @param quiz_attempt $attemptobj
+ * @param int $page Current page number
+ * @param array $slots Array of integers relating to questions
+ * @param int $id ID of the attempt
+ * @param int $nextpage Next page number
+ */
+ public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) {
+ $output = '';
+
+ //Start Form
+ $output .= html_writer::start_tag('form',
+ array('action' => $attemptobj->processattempt_url(), 'method' => 'post',
+ 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
+ 'id' => 'responseform'));
+ $output .= html_writer::start_tag('div');
+
+ // Print all the questions
+ foreach ($slots as $slot) {
+ $output .= $attemptobj->render_question($slot, false, $attemptobj->attempt_url($id,
+ $page));
+ }
+
+ $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
+ $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
+ 'value' => get_string('next')));
+ $output .= html_writer::end_tag('div');
+
+ // Some hidden fields to trach what is going on.
+ $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt',
+ 'value' => $attemptobj->get_attemptid()));
+ $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage',
+ 'value' => $page, 'id' => 'followingpage'));
+ $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage',
+ 'value' => $nextpage));
+ $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup',
+ 'value' => '0', 'id' => 'timeup'));
+ $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
+ 'value' => sesskey()));
+ $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos',
+ 'value' => '', 'id' => 'scrollpos'));
+
+ // Add a hidden field with questionids. Do this at the end of the form, so
+ // if you navigate before the form has finished loading, it does not wipe all
+ // the student's answers.
+ $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots',
+ 'value' => implode(',', $slots)));
+
+ //Finish form
+ $output .= html_writer::end_tag('div');
+ $output .= html_writer::end_tag('form');
+
+ return $output;
+ }
+
+ /**
+ * Print each message in an array, surrounded by <p>, </p> tags.
+ *
+ * @param array $messages the array of message strings.
+ * @param bool $return if true, return a string, instead of outputting.
+ *
+ * @return mixed, if $return is true, return the string that would have been output, otherwise
+ * return null.
+ */
+ public function access_messages($messages) {
+ $output = '';
+ foreach ($messages as $message) {
+ $output .= html_writer::tag('p', $message) . "\n";
+ }
+ return $output;
+ }
+
+ /*
+ * Summary Page
+ */
+ /**
+ * Create the summary page
+ *
+ * @param quiz_attempt $attemptobj
+ * @param mod_quiz_display_options $displayoptions
+ */
+ public function summary_page($attemptobj, $displayoptions) {
+ $output = '';
+ $output .= $this->summary_table($attemptobj, $displayoptions);
+ $output .= $this->summary_page_controls($attemptobj);
+ return $output;
+ }
+
+ /**
+ * Generates the table of summarydata
+ *
+ * @param quiz_attempt $attemptobj
+ * @param mod_quiz_display_options $displayoptions
+ */
+ public function summary_table($attemptobj, $displayoptions) {
+ // Prepare the summary table header
+ $table = new html_table();
+ $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter';
+ $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz'));
+ $table->align = array('left', 'left');
+ $table->size = array('', '');
+ $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX;
+ if ($markscolumn) {
+ $table->head[] = get_string('marks', 'quiz');
+ $table->align[] = 'left';
+ $table->size[] = '';
+ }
+ $table->data = array();
+
+ // Get the summary info for each question.
+ $slots = $attemptobj->get_slots();
+ foreach ($slots as $slot) {
+ if (!$attemptobj->is_real_question($slot)) {
+ continue;
+ }
+ $flag = '';
+ if ($attemptobj->is_question_flagged($slot)) {
+ $flag = html_writer::empty_tag('img', array('src' => $this->pix_url('i/flagged'),
+ 'alt' => get_string('flagged', 'question'), 'class' => 'questionflag'));
+ }
+ $row = array(html_writer::link($attemptobj->attempt_url($slot),
+ $attemptobj->get_question_number($slot) . $flag),
+ $attemptobj->get_question_status($slot, $displayoptions->correctness));
+ if ($markscolumn) {
+ $row[] = $attemptobj->get_question_mark($slot);
+ }
+ $table->data[] = $row;
+ $table->rowclasses[] = $attemptobj->get_question_state_class(
+ $slot, $displayoptions->correctness);
+ }
+
+ // Print the summary table.
+ $output = html_writer::table($table);
+
+ return $output;
+ }
+
+ /**
+ * Creates any controls a the page should have.
+ *
+ * @param quiz_attempt $attemptobj
+ */
+ public function summary_page_controls($attemptobj) {
+ $output = '';
+ // countdown timer
+ $output .= $this->countdown_timer();
+
+ // Finish attempt button.
+ $output .= $this->container_start('submitbtns mdl-align');
+ $options = array(
+ 'attempt' => $attemptobj->get_attemptid(),
+ 'finishattempt' => 1,
+ 'timeup' => 0,
+ 'slots' => '',
+ 'sesskey' => sesskey(),
+ );
+
+ $button = new single_button(
+ new moodle_url($attemptobj->processattempt_url(), $options),
+ get_string('submitallandfinish', 'quiz'));
+ $button->id = 'responseform';
+ $button->add_confirm_action(get_string('confirmclose', 'quiz'));
+
+ $output .= $this->container_start('controls');
+ $output .= $this->render($button);
+ $output .= $this->container_end();
+ $output .= $this->container_end();
+
+ return $output;
+ }
+
+ /*
+ * View Page
+ */
+ /**
+ * Generates the view page
+ *
+ * @param int $course The id of the course
+ * @param array $quiz Array conting quiz data
+ * @param int $cm Course Module ID
+ * @param int $context The page context ID
+ * @param array $infomessages information about this quiz
+ * @param mod_quiz_view_object $viewobj
+ * @param string $buttontext text for the start/continue attempt button, if
+ * it should be shown.
+ * @param array $infomessages further information about why the student cannot
+ * attempt this quiz now, if appicable this quiz
+ */
+ public function view_page($course, $quiz, $cm, $context, $infomessages, $viewobj,
+ $buttontext, $preventmessages) {
+ $output = '';
+ $output .= $this->view_information($course, $quiz, $cm, $context, $infomessages);
+ $output .= $this->view_table($quiz, $context, $viewobj);
+ $output .= $this->view_best_score($viewobj);
+ $output .= $this->view_result_info($quiz, $context, $cm, $viewobj);
+ $output .= $this->view_attempt_button($course, $quiz, $cm, $context, $viewobj,
+ $buttontext, $preventmessages);
+ return $output;
+ }
+
+ /**
+ * Outputs an error message for any guests accessing the quiz
+ *
+ * @param int $course The course ID
+ * @param array $quiz Array contingin quiz data
+ * @param int $cm Course Module ID
+ * @param int $context The page contect ID
+ * @param array $messages Array containing any messages
+ */
+ public function view_page_guest($course, $quiz, $cm, $context, $messages) {
+ $output = '';
+ $output .= $this->view_information($course, $quiz, $cm, $context, $messages);
+ $guestno = html_writer::tag('p', get_string('guestsno', 'quiz'));
+ $liketologin = html_writer::tag('p', get_string('liketologin'));
+ $output .= $this->confirm($guestno.'\n\n'.$liketologin.'\n', get_login_url(),
+ get_referer(false));
+ return $output;
+ }
+
+ /**
+ * Outputs and error message for anyone who is not enrolle don the course
+ *
+ * @param int $course The course ID
+ * @param array $quiz Array contingin quiz data
+ * @param int $cm Course Module ID
+ * @param int $context The page contect ID
+ * @param array $messages Array containing any messages
+ */
+ public function view_page_notenrolled($course, $quiz, $cm, $context, $messages) {
+ global $CFG;
+ $output = '';
+ $output .= $this->view_information($course, $quiz, $cm, $context, $messages);
+ $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz'));
+ $button = html_writer::tag('p',
+ $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id));
+ $output .= $this->box($youneedtoenrol.'\n\n'.$button.'\n', 'generalbox', 'notice');
+ return $output;
+ }
+
+ /**
+ * Output the page information
+ *
+ * @param int $course The course ID
+ * @param array $quiz Array contingin quiz data
+ * @param int $cm Course Module ID
+ * @param int $context The page contect ID
+ * @param array $messages Array containing any messages
+ */
+ public function view_information($course, $quiz, $cm, $context, $messages) {
+ global $CFG;
+ $output = '';
+ // Print quiz name and description
+ $output .= $this->heading(format_string($quiz->name));
+ if (trim(strip_tags($quiz->intro))) {
+ $output .= $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox',
+ 'intro');
+ }
+
+ $output .= $this->box($this->access_messages($messages), 'quizinfo');
+
+ // Show number of attempts summary to those who can view reports.
+ if (has_capability('mod/quiz:viewreports', $context)) {
+ if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm,
+ $context)) {
+ $output .= html_writer::tag('div', $strattemptnum,
+ array('class' => 'quizattemptcounts'));
+ }
+ }
+ return $output;
+ }
+
+ /**
+ * Generates the table heading.
+ */
+ public function view_table_heading() {
+ return $this->heading(get_string('summaryofattempts', 'quiz'));
+ }
+
+ /**
+ * Generates the table of data
+ *
+ * @param array $quiz Array contining quiz data
+ * @param int $context The page context ID
+ * @param mod_quiz_view_object $viewobj
+ */
+ public function view_table($quiz, $context, $viewobj) {
+ $output = '';
+ if (!$viewobj->attempts) {
+ return $output;
+ }
+ $output .= $this->view_table_heading();
+
+ // Prepare table header
+ $table = new html_table();
+ $table->attributes['class'] = 'generaltable quizattemptsummary';
+ $table->head = array();
+ $table->align = array();
+ $table->size = array();
+ if ($viewobj->attemptcolumn) {
+ $table->head[] = get_string('attemptnumber', 'quiz');
+ $table->align[] = 'center';
+ $table->size[] = '';
+ }
+ $table->head[] = get_string('timecompleted', 'quiz');
+ $table->align[] = 'left';
+ $table->size[] = '';
+ if ($viewobj->markcolumn) {
+ $table->head[] = get_string('marks', 'quiz') . ' / ' .
+ quiz_format_grade($quiz, $quiz->sumgrades);
+ $table->align[] = 'center';
+ $table->size[] = '';
+ }
+ if ($viewobj->gradecolumn) {
+ $table->head[] = get_string('grade') . ' / ' .
+ quiz_format_grade($quiz, $quiz->grade);
+ $table->align[] = 'center';
+ $table->size[] = '';
+ }
+ if ($viewobj->canreviewmine) {
+ $table->head[] = get_string('review', 'quiz');
+ $table->align[] = 'center';
+ $table->size[] = '';
+ }
+ if ($viewobj->feedbackcolumn) {
+ $table->head[] = get_string('feedback', 'quiz');
+ $table->align[] = 'left';
+ $table->size[] = '';
+ }
+ if (isset($quiz->showtimetaken)) {
+ $table->head[] = get_string('timetaken', 'quiz');
+ $table->align[] = 'left';
+ $table->size[] = '';
+ }
+
+ // One row for each attempt
+ foreach ($viewobj->attempts as $attempt) {
+ $attemptoptions = quiz_get_review_options($quiz, $attempt, $context);
+ $row = array();
+
+ // Add the attempt number, making it a link, if appropriate.
+ if ($viewobj->attemptcolumn) {
+ if ($attempt->preview) {
+ $row[] = get_string('preview', 'quiz');
+ } else {
+ $row[] = $attempt->attempt;
+ }
+ }
+
+ // prepare strings for time taken and date completed
+ $timetaken = '';
+ $datecompleted = '';
+ if ($attempt->timefinish > 0) {
+ // attempt has finished
+ $timetaken = format_time($attempt->timefinish - $attempt->timestart);
+ $datecompleted = userdate($attempt->timefinish);
+ } else if (!$quiz->timeclose || $viewobj->timenow < $quiz->timeclose) {
+ // The attempt is still in progress.
+ $timetaken = format_time($viewobj->timenow - $attempt->timestart);
+ $datecompleted = get_string('inprogress', 'quiz');
+ } else {
+ $timetaken = format_time($quiz->timeclose - $attempt->timestart);
+ $datecompleted = userdate($quiz->timeclose);
+ }
+ $row[] = $datecompleted;
+
+ if ($viewobj->markcolumn && $attempt->timefinish > 0) {
+ if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX) {
+ $row[] = quiz_format_grade($quiz, $attempt->sumgrades);
+ } else {
+ $row[] = '';
+ }
+ }
+
+ // Ouside the if because we may be showing feedback but not grades.
+ $attemptgrade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
+
+ if ($viewobj->gradecolumn) {
+ if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
+ $attempt->timefinish > 0) {
+ $formattedgrade = quiz_format_grade($quiz, $attemptgrade);
+ // highlight the highest grade if appropriate
+ if ($viewobj->overallstats && !$attempt->preview
+ && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade)
+ && $attemptgrade == $viewobj->mygrade
+ && $quiz->grademethod == QUIZ_GRADEHIGHEST) {
+ $table->rowclasses[$attempt->attempt] = 'bestrow';
+ }
+
+ $row[] = $formattedgrade;
+ } else {
+ $row[] = '';
+ }
+ }
+
+ if ($viewobj->canreviewmine) {
+ $row[] = $viewobj->accessmanager->make_review_link($attempt,
+ $viewobj->canpreview, $attemptoptions);
+ }
+
+ if ($viewobj->feedbackcolumn && $attempt->timefinish > 0) {
+ if ($attemptoptions->overallfeedback) {
+ $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context, $cm);
+ } else {
+ $row[] = '';
+ }
+ }
+
+ if (isset($quiz->showtimetaken)) {
+ $row[] = $timetaken;
+ }
+
+ if ($attempt->preview) {
+ $table->data['preview'] = $row;
+ } else {
+ $table->data[$attempt->attempt] = $row;
+ }
+ } // End of loop over attempts.
+ $output .= html_writer::table($table);
+
+ return $output;
+ }
+
+ /**
+ * Prints the students best score
+ *
+ * @param mod_quiz_view_object $viewobj
+ */
+ public function view_best_score($viewobj) {
+ $output = '';
+ // Print information about the student's best score for this quiz if possible.
+ if (!$viewobj->moreattempts) {
+ $output .= $this->heading(get_string('nomoreattempts', 'quiz'));
+ }
+ return $output;
+ }
+
+ /**
+ * Generates data pertaining to quiz results
+ *
+ * @param array $quiz Array containing quiz data
+ * @param int $context The page context ID
+ * @param int $cm The Course Module Id
+ * @param mod_quiz_view_object $viewobj
+ */
+ public function view_result_info($quiz, $context, $cm, $viewobj) {
+ $output = '';
+ if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) {
+ return $output;
+ }
+ $resultinfo = '';
+
+ if ($viewobj->overallstats) {
+ if ($viewobj->moreattempts) {
+ $a = new stdClass();
+ $a->method = quiz_get_grading_option_name($quiz->grademethod);
+ $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade);
+ $a->quizgrade = quiz_format_grade($quiz, $quiz->grade);
+ $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 2, 'main');
+ } else {
+ $a = new stdClass();
+ $a->grade = quiz_format_grade($quiz, $viewobj->mygrade);
+ $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
+ $a = get_string('outofshort', 'quiz', $a);
+ $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 2,
+ 'main');
+ }
+ }
+
+ if ($viewobj->mygradeoverridden) {
+
+ $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'),
+ array('class' => 'overriddennotice')).'\n';
+ }
+ if ($viewobj->gradebookfeedback) {
+ $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3, 'main');
+ $resultinfo .= '
'.$viewobj->gradebookfeedback.
+ "
\n";
+ }
+ if ($viewobj->feedbackcolumn) {
+ $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3, 'main');
+ $resultinfo .= html_writer::tag('p',
+ quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context, $cm),
+ array('class' => 'quizgradefeedback')).'\n';
+ }
+
+ if ($resultinfo) {
+ $output .= $this->box($resultinfo, 'generalbox', 'feedback');
+ }
+ return $output;
+ }
+
+ /**
+ * Generates the view attempt button
+ *
+ * @param int $course The course ID
+ * @param array $quiz Array containging quiz date
+ * @param int $cm The Course Module ID
+ * @param int $context The page Context ID
+ * @param mod_quiz_view_object $viewobj
+ * @param string $buttontext
+ */
+ public function view_attempt_button($course, $quiz, $cm, $context, $viewobj,
+ $buttontext, $preventmessages) {
+ $output = '';
+ // Determine if we should be showing a start/continue attempt button,
+ // or a button to go back to the course page.
+ $output .= $this->box_start('quizattempt');
+
+ // Now actually print the appropriate button.
+ if (!quiz_clean_layout($quiz->questions, true)) {
+ $output .= quiz_no_questions_message($quiz, $cm, $context);
+ }
+
+ if ($preventmessages) {
+ $output .= $this->access_messages($preventmessages);
+ }
+
+ if ($buttontext) {
+ $output .= $viewobj->accessmanager->print_start_attempt_button($viewobj->canpreview,
+ $buttontext, $viewobj->unfinished);
+ } else if ($buttontext === '') {
+ $output .= $this->single_button(new moodle_url('/course/view.php',
+ array('id' => $course->id)), get_string('backtocourse', 'quiz'), 'get',
+ array('class' => 'continuebutton'));
+ }
+ $output .= $this->box_end();
+
+ return $output;
+ }
+
+ /**
+ * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
+ * to the quiz reports.
+ *
+ * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
+ * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid
+ * fields are used at the moment.
+ * @param object $context the quiz context.
+ * @param bool $returnzero if false (default), when no attempts have been made '' is returned
+ * instead of 'Attempts: 0'.
+ * @param int $currentgroup if there is a concept of current group where this method is being
+ * called
+ * (e.g. a report) pass it in here. Default 0 which means no current group.
+ * @return string HTML fragment for the link.
+ */
+ public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context,
+ $returnzero = false, $currentgroup = 0) {
+ global $CFG;
+ $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
+ if (!$summary) {
+ return '';
+ }
+
+ require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
+ $url = new moodle_url('/mod/quiz/report.php', array(
+ 'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
+ return html_writer::link($url, $summary);
+ }
+}
+
+class mod_quiz_links_to_other_attempts implements renderable {
+ /**
+ * @var array string attempt number => url, or null for the current attempt.
+ */
+ public $links = array();
+}
+
+class mod_quiz_view_object {
+ /**
+ * @var array $attempt contains all the user's attempts at this quiz.
+ */
+ public $attempts;
+ /**
+ * @var object $accessmanager contains various access rules.
+ */
+ public $accessmanager;
+ /**
+ * @var int $canattempt determins capability for attempting a quiz.
+ */
+ public $canattempt;
+ /**
+ * @var int $canpreview determins capability for previewing a quiz.
+ */
+ public $canpreview;
+ /**
+ * @var int $canreviewmine determins capability for reviwing own quiz.
+ */
+ public $canreviewmine;
+ /**
+ * @var int $attemptcolumn contains the number of attempts done.
+ */
+ public $attemptcolumn;
+ /**
+ * @var int $gradecolumn contains the grades of any attempts.
+ */
+ public $gradecolumn;
+ /**
+ * @var int $markcolumn contains the marks of any attempt.
+ */
+ public $markcolumn;
+ /**
+ * @var int $overallstats contains all marks for any attempt.
+ */
+ public $overallstats;
+ /**
+ * @var string $feedbackcolumn contains any feedback for and attempt.
+ */
+ public $feedbackcolumn;
+ /**
+ * @var string $timenow contains a timestamp in string format.
+ */
+ public $timenow;
+ /**
+ * @var int $numattempts contains the total number of attempts.
+ */
+ public $numattempts;
+ /**
+ * @var int $mygrade contains the current users final grade for a quiz.
+ */
+ public $mygrade;
+ /**
+ * @var int $moreattempts total attempts left.
+ */
+ public $moreattempts;
+ /**
+ * @var int $mygradeoverridden contains an overriden grade.
+ */
+ public $mygradeoverridden;
+ /**
+ * @var string $gradebookfeedback contains any feedback for a gradebook.
+ */
+ public $gradebookfeedback;
+ /**
+ * @var int $unfinished contains 1 if an attempt is unfinished.
+ */
+ public $unfinished;
+ /**
+ * @var int $lastfinishedattempt contains a pointer to the last attempt in the attempts array.
+ */
+ public $lastfinishedattempt;
+}
\ No newline at end of file
diff --git a/mod/quiz/report.php b/mod/quiz/report.php
index 710a462e1f7..4ebb4c2aa6d 100644
--- a/mod/quiz/report.php
+++ b/mod/quiz/report.php
@@ -1,98 +1,103 @@
.
-// This script uses installed report plugins to print quiz reports
+/**
+ * This script controls the display of the quiz reports.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
- require_once('../../config.php');
- require_once($CFG->dirroot.'/mod/quiz/locallib.php');
- require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
- $id = optional_param('id',0,PARAM_INT); // Course Module ID, or
- $q = optional_param('q',0,PARAM_INT); // quiz ID
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
+require_once($CFG->dirroot . '/mod/quiz/report/default.php');
- $mode = optional_param('mode', '', PARAM_ALPHA); // Report mode
+$id = optional_param('id', 0, PARAM_INT);
+$q = optional_param('q', 0, PARAM_INT);
+$mode = optional_param('mode', '', PARAM_ALPHA);
- if ($id) {
- if (! $cm = get_coursemodule_from_id('quiz', $id)) {
- print_error('invalidcoursemodule');
- }
-
- if (! $course = $DB->get_record('course', array('id' => $cm->course))) {
- print_error('coursemisconf');
- }
-
- if (! $quiz = $DB->get_record('quiz', array('id' => $cm->instance))) {
- print_error('invalidcoursemodule');
- }
-
- } else {
- if (! $quiz = $DB->get_record('quiz', array('id' => $q))) {
- print_error('invalidquizid', 'quiz');
- }
- if (! $course = $DB->get_record('course', array('id' => $quiz->course))) {
- print_error('invalidcourseid');
- }
- if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
- print_error('invalidcoursemodule');
- }
+if ($id) {
+ if (!$cm = get_coursemodule_from_id('quiz', $id)) {
+ print_error('invalidcoursemodule');
+ }
+ if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
+ print_error('coursemisconf');
+ }
+ if (!$quiz = $DB->get_record('quiz', array('id' => $cm->instance))) {
+ print_error('invalidcoursemodule');
}
- $url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id));
- if ($mode !== '') {
- $url->param('mode', $mode);
+} else {
+ if (!$quiz = $DB->get_record('quiz', array('id' => $q))) {
+ print_error('invalidquizid', 'quiz');
}
- $PAGE->set_url($url);
-
- require_login($course, false, $cm);
- $context = get_context_instance(CONTEXT_MODULE, $cm->id);
- $PAGE->set_pagelayout('report');
-
- $reportlist = quiz_report_list($context);
- if (count($reportlist)==0){
- print_error('erroraccessingreport', 'quiz');
+ if (!$course = $DB->get_record('course', array('id' => $quiz->course))) {
+ print_error('invalidcourseid');
}
- if ($mode == '') {
- // Default to first accessible report and redirect.
- $url->param('mode', reset($reportlist));
- redirect($url);
- } else if (!in_array($mode, $reportlist)){
- print_error('erroraccessingreport', 'quiz');
+ if (!$cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
+ print_error('invalidcoursemodule');
}
+}
- // if no questions have been set up yet redirect to edit.php
- if (!$quiz->questions and has_capability('mod/quiz:manage', $context)) {
- redirect('edit.php?cmid=' . $cm->id);
- }
+$url = new moodle_url('/mod/quiz/report.php', array('id' => $cm->id));
+if ($mode !== '') {
+ $url->param('mode', $mode);
+}
+$PAGE->set_url($url);
- // Upgrade any attempts that have not yet been upgraded to the
- // Moodle 1.5 model (they will not yet have the timestamp set)
- if ($attempts = $DB->get_records_sql("SELECT a.*".
- " FROM {quiz_attempts} a, {question_states} s".
- " WHERE a.quiz = ? AND s.attempt = a.uniqueid AND s.timestamp = 0", array($quiz->id))) {
- foreach ($attempts as $attempt) {
- quiz_upgrade_states($attempt);
- }
- }
+require_login($course, false, $cm);
+$context = get_context_instance(CONTEXT_MODULE, $cm->id);
+$PAGE->set_pagelayout('report');
- add_to_log($course->id, "quiz", "report", "report.php?id=$cm->id", "$quiz->id", "$cm->id");
+$reportlist = quiz_report_list($context);
+if (empty($reportlist)) {
+ print_error('erroraccessingreport', 'quiz');
+}
-/// Open the selected quiz report and display it
+// Validate the requested report name.
+if ($mode == '') {
+ // Default to first accessible report and redirect.
+ $url->param('mode', reset($reportlist));
+ redirect($url);
+} else if (!in_array($mode, $reportlist)) {
+ print_error('erroraccessingreport', 'quiz');
+}
+if (!is_readable("report/$mode/report.php")) {
+ print_error('reportnotfound', 'quiz', '', $mode);
+}
- if (!is_readable("report/$mode/report.php")) {
- print_error('reportnotfound', 'quiz', '', $mode);
- }
+add_to_log($course->id, 'quiz', 'report', 'report.php?id=' . $cm->id . '&mode=' . $mode,
+ $quiz->id, $cm->id);
- include("report/default.php"); // Parent class
- include("report/$mode/report.php");
-
- $reportclassname = "quiz_{$mode}_report";
- $report = new $reportclassname();
-
- if (!$report->display($quiz, $cm, $course)) { // Run the report!
- print_error("preprocesserror", 'quiz');
- }
-
-/// Print footer
-
- echo $OUTPUT->footer();
+// Open the selected quiz report and display it
+$file = $CFG->dirroot . '/mod/quiz/report/' . $mode . '/report.php';
+if (is_readable($file)) {
+ include_once($file);
+}
+$reportclassname = 'quiz_' . $mode . '_report';
+if (!class_exists($reportclassname)) {
+ print_error('preprocesserror', 'quiz');
+}
+$report = new $reportclassname();
+$report->display($quiz, $cm, $course);
+// Print footer
+echo $OUTPUT->footer();
diff --git a/mod/quiz/report/attemptsreport.php b/mod/quiz/report/attemptsreport.php
new file mode 100644
index 00000000000..1df5d9b2c9d
--- /dev/null
+++ b/mod/quiz/report/attemptsreport.php
@@ -0,0 +1,678 @@
+.
+
+/**
+ * The file defines some subclasses that can be used when you are building
+ * a report like the overview or responses report, that basically has one
+ * row per attempt.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/tablelib.php');
+
+
+/**
+ * Base class for quiz reports that are basically a table with one row for each attempt.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class quiz_attempt_report extends quiz_default_report {
+ /** @var object the quiz context. */
+ protected $context;
+
+ /** @var boolean caches the results of {@link should_show_grades()}. */
+ protected $showgrades = null;
+
+ /**
+ * Should the grades be displayed in this report. That depends on the quiz
+ * display options, and whether the quiz is graded.
+ * @param object $quiz the quiz settings.
+ * @return bool
+ */
+ protected function should_show_grades($quiz) {
+ if (!is_null($this->showgrades)) {
+ return $this->showgrades;
+ }
+
+ if ($quiz->timeclose && time() > $quiz->timeclose) {
+ $when = mod_quiz_display_options::AFTER_CLOSE;
+ } else {
+ $when = mod_quiz_display_options::LATER_WHILE_OPEN;
+ }
+ $reviewoptions = mod_quiz_display_options::make_from_quiz($quiz, $when);
+
+ $this->showgrades = quiz_has_grades($quiz) &&
+ ($reviewoptions->marks >= question_display_options::MARK_AND_MAX ||
+ has_capability('moodle/grade:viewhidden', $this->context));
+
+ return $this->showgrades;
+ }
+
+ /**
+ * Get information about which students to show in the report.
+ * @param object $cm the coures module.
+ * @return an array with four elements:
+ * 0 => integer the current group id (0 for none).
+ * 1 => array ids of all the students in this course.
+ * 2 => array ids of all the students in the current group.
+ * 3 => array ids of all the students to show in the report. Will be the
+ * same as either element 1 or 2.
+ */
+ protected function load_relevant_students($cm) {
+ $currentgroup = groups_get_activity_group($cm, true);
+
+ if (!$students = get_users_by_capability($this->context,
+ array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
+ 'u.id, 1', '', '', '', '', '', false)) {
+ $students = array();
+ } else {
+ $students = array_keys($students);
+ }
+
+ if (empty($currentgroup)) {
+ return array($currentgroup, $students, array(), $students);
+ }
+
+ // We have a currently selected group.
+ if (!$groupstudents = get_users_by_capability($this->context,
+ array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
+ 'u.id, 1', '', '', '', $currentgroup, '', false)) {
+ $groupstudents = array();
+ } else {
+ $groupstudents = array_keys($groupstudents);
+ }
+
+ return array($currentgroup, $students, $groupstudents, $groupstudents);
+ }
+
+ /**
+ * Alters $attemptsmode and $pagesize if the current values are inappropriate.
+ * @param int $attemptsmode what sort of attempts to display (may be updated)
+ * @param int $pagesize number of records to display per page (may be updated)
+ * @param object $course the course settings.
+ * @param int $currentgroup the currently selected group. 0 for none.
+ */
+ protected function validate_common_options(&$attemptsmode, &$pagesize, $course, $currentgroup) {
+ if ($currentgroup) {
+ //default for when a group is selected
+ if ($attemptsmode === null || $attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) {
+ $attemptsmode = QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH;
+ }
+ } else if (!$currentgroup && $course->id == SITEID) {
+ //force report on front page to show all, unless a group is selected.
+ $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
+ } else if ($attemptsmode === null) {
+ //default
+ $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
+ }
+
+ if ($pagesize < 1) {
+ $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE;
+ }
+ }
+
+ /**
+ * Contruct all the parts of the main database query.
+ * @param object $quiz the quiz settings.
+ * @param string $qmsubselect SQL fragment from {@link quiz_report_qm_filter_select()}.
+ * @param bool $qmfilter whether to show all, or only the final grade attempt.
+ * @param int $attemptsmode which attempts to show.
+ * One of the QUIZ_REPORT_ATTEMPTS_... constants.
+ * @param array $reportstudents list if userids of users to include in the report.
+ * @return array with 4 elements ($fields, $from, $where, $params) that can be used to
+ * build the actual database query.
+ */
+ protected function base_sql($quiz, $qmsubselect, $qmfilter, $attemptsmode, $reportstudents) {
+ global $DB;
+
+ $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,';
+
+ if ($qmsubselect) {
+ $fields .= "\n(CASE WHEN $qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,";
+ }
+
+ $fields .= '
+ quiza.uniqueid AS usageid,
+ quiza.id AS attempt,
+ u.id AS userid,
+ u.idnumber,
+ u.firstname,
+ u.lastname,
+ u.picture,
+ u.imagealt,
+ u.institution,
+ u.department,
+ u.email,
+ quiza.sumgrades,
+ quiza.timefinish,
+ quiza.timestart,
+ CASE WHEN quiza.timefinish = 0 THEN null
+ WHEN quiza.timefinish > quiza.timestart THEN quiza.timefinish - quiza.timestart
+ ELSE 0 END AS duration';
+ // To explain that last bit, in MySQL, qa.timestart and qa.timefinish
+ // are unsigned. Since MySQL 5.5.5, when they introduced strict mode,
+ // subtracting a larger unsigned int from a smaller one gave an error.
+ // Therefore, we avoid doing that. timefinish can be non-zero and less
+ // than timestart when you have two load-balanced servers with very
+ // badly synchronised clocks, and a student does a really quick attempt.';
+
+ // This part is the same for all cases - join users and quiz_attempts tables
+ $from = "\n{user} u";
+ $from .= "\nLEFT JOIN {quiz_attempts} quiza ON
+ quiza.userid = u.id AND quiza.quiz = :quizid";
+ $params = array('quizid' => $quiz->id);
+
+ if ($qmsubselect && $qmfilter) {
+ $from .= " AND $qmsubselect";
+ }
+ switch ($attemptsmode) {
+ case QUIZ_REPORT_ATTEMPTS_ALL:
+ // Show all attempts, including students who are no longer in the course
+ $where = 'quiza.id IS NOT NULL AND quiza.preview = 0';
+ break;
+ case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH:
+ // Show only students with attempts
+ list($usql, $uparams) = $DB->get_in_or_equal(
+ $reportstudents, SQL_PARAMS_NAMED, 'u');
+ $params += $uparams;
+ $where = "u.id $usql AND quiza.preview = 0 AND quiza.id IS NOT NULL";
+ break;
+ case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
+ // Show only students without attempts
+ list($usql, $uparams) = $DB->get_in_or_equal(
+ $reportstudents, SQL_PARAMS_NAMED, 'u');
+ $params += $uparams;
+ $where = "u.id $usql AND quiza.id IS NULL";
+ break;
+ case QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS:
+ // Show all students with or without attempts
+ list($usql, $uparams) = $DB->get_in_or_equal(
+ $reportstudents, SQL_PARAMS_NAMED, 'u');
+ $params += $uparams;
+ $where = "u.id $usql AND (quiza.preview = 0 OR quiza.preview IS NULL)";
+ break;
+ }
+
+ return array($fields, $from, $where, $params);
+ }
+
+ /**
+ * Add all the user-related columns to the $columns and $headers arrays.
+ * @param table_sql $table the table being constructed.
+ * @param array $columns the list of columns. Added to.
+ * @param array $headers the columns headings. Added to.
+ */
+ protected function add_user_columns($table, &$columns, &$headers) {
+ global $CFG;
+ if (!$table->is_downloading() && $CFG->grade_report_showuserimage) {
+ $columns[] = 'picture';
+ $headers[] = '';
+ }
+ if (!$table->is_downloading()) {
+ $columns[] = 'fullname';
+ $headers[] = get_string('name');
+ } else {
+ $columns[] = 'lastname';
+ $headers[] = get_string('lastname');
+ $columns[] = 'firstname';
+ $headers[] = get_string('firstname');
+ }
+
+ if ($CFG->grade_report_showuseridnumber) {
+ $columns[] = 'idnumber';
+ $headers[] = get_string('idnumber');
+ }
+
+ if ($table->is_downloading()) {
+ $columns[] = 'institution';
+ $headers[] = get_string('institution');
+
+ $columns[] = 'department';
+ $headers[] = get_string('department');
+
+ $columns[] = 'email';
+ $headers[] = get_string('email');
+ }
+ }
+
+ /**
+ * Set the display options for the user-related columns in the table.
+ * @param table_sql $table the table being constructed.
+ */
+ protected function configure_user_columns($table) {
+ $table->column_suppress('picture');
+ $table->column_suppress('fullname');
+ $table->column_suppress('idnumber');
+
+ $table->column_class('picture', 'picture');
+ $table->column_class('lastname', 'bold');
+ $table->column_class('firstname', 'bold');
+ $table->column_class('fullname', 'bold');
+ }
+
+ /**
+ * Add all the time-related columns to the $columns and $headers arrays.
+ * @param array $columns the list of columns. Added to.
+ * @param array $headers the columns headings. Added to.
+ */
+ protected function add_time_columns(&$columns, &$headers) {
+ $columns[] = 'timestart';
+ $headers[] = get_string('startedon', 'quiz');
+
+ $columns[] = 'timefinish';
+ $headers[] = get_string('timecompleted', 'quiz');
+
+ $columns[] = 'duration';
+ $headers[] = get_string('attemptduration', 'quiz');
+ }
+
+ /**
+ * Add all the grade and feedback columns, if applicable, to the $columns
+ * and $headers arrays.
+ * @param object $quiz the quiz settings.
+ * @param array $columns the list of columns. Added to.
+ * @param array $headers the columns headings. Added to.
+ */
+ protected function add_grade_columns($quiz, &$columns, &$headers) {
+ if ($this->should_show_grades($quiz)) {
+ $columns[] = 'sumgrades';
+ $headers[] = get_string('grade', 'quiz') . '/' .
+ quiz_format_grade($quiz, $quiz->grade);
+ }
+
+ if (quiz_has_feedback($quiz)) {
+ $columns[] = 'feedbacktext';
+ $headers[] = get_string('feedback', 'quiz');
+ }
+ }
+
+ /**
+ * Set up the table.
+ * @param table_sql $table the table being constructed.
+ * @param array $columns the list of columns.
+ * @param array $headers the columns headings.
+ * @param moodle_url $reporturl the URL of this report.
+ * @param array $displayoptions the display options.
+ * @param bool $collapsible whether to allow columns in the report to be collapsed.
+ */
+ protected function set_up_table_columns($table, $columns, $headers, $reporturl,
+ $displayoptions, $collapsible) {
+ $table->define_columns($columns);
+ $table->define_headers($headers);
+ $table->sortable(true, 'uniqueid');
+
+ $table->define_baseurl($reporturl->out(false, $displayoptions));
+
+ $this->configure_user_columns($table);
+
+ $table->no_sorting('feedbacktext');
+ $table->column_class('sumgrades', 'bold');
+
+ $table->set_attribute('id', 'attempts');
+
+ $table->collapsible($collapsible);
+ }
+
+ /**
+ * Delete the quiz attempts
+ * @param object $quiz the quiz settings. Attempts that don't belong to
+ * this quiz are not deleted.
+ * @param object $cm the course_module object.
+ * @param array $attemptids the list of attempt ids to delete.
+ * @param array $allowed This list of userids that are visible in the report.
+ * Users can only delete attempts that they are allowed to see in the report.
+ * Empty means all users.
+ */
+ protected function delete_selected_attempts($quiz, $cm, $attemptids, $allowed) {
+ global $DB;
+
+ foreach ($attemptids as $attemptid) {
+ $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid));
+ if (!$attempt || $attempt->quiz != $quiz->id || $attempt->preview != 0) {
+ // Ensure the attempt exists, and belongs to this quiz. If not skip.
+ continue;
+ }
+ if ($allowed && !array_key_exists($attempt->userid, $allowed)) {
+ // Ensure the attempt belongs to a student included in the report. If not skip.
+ continue;
+ }
+ add_to_log($quiz->course, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id,
+ $attemptid, $cm->id);
+ quiz_delete_attempt($attempt, $quiz);
+ }
+ }
+}
+
+/**
+ * Base class for the table used by {@link quiz_attempt_report}s.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class quiz_attempt_report_table extends table_sql {
+ public $useridfield = 'userid';
+
+ /** @var moodle_url the URL of this report. */
+ protected $reporturl;
+
+ /** @var array the display options. */
+ protected $displayoptions;
+
+ /**
+ * @var array information about the latest step of each question.
+ * Loaded by {@link load_question_latest_steps()}, if applicable.
+ */
+ protected $lateststeps = null;
+
+ protected $quiz;
+ protected $context;
+ protected $qmsubselect;
+ protected $groupstudents;
+ protected $students;
+ protected $questions;
+ protected $candelete;
+
+ public function __construct($uniqueid, $quiz, $context, $qmsubselect, $groupstudents,
+ $students, $questions, $candelete, $reporturl, $displayoptions) {
+ parent::__construct($uniqueid);
+ $this->quiz = $quiz;
+ $this->context = $context;
+ $this->qmsubselect = $qmsubselect;
+ $this->groupstudents = $groupstudents;
+ $this->students = $students;
+ $this->questions = $questions;
+ $this->candelete = $candelete;
+ $this->reporturl = $reporturl;
+ $this->displayoptions = $displayoptions;
+ }
+
+ public function col_checkbox($attempt) {
+ if ($attempt->attempt) {
+ return '
';
+ } else {
+ return '';
+ }
+ }
+
+ public function col_picture($attempt) {
+ global $COURSE, $OUTPUT;
+ $user = new stdClass();
+ $user->id = $attempt->userid;
+ $user->lastname = $attempt->lastname;
+ $user->firstname = $attempt->firstname;
+ $user->imagealt = $attempt->imagealt;
+ $user->picture = $attempt->picture;
+ $user->email = $attempt->email;
+ return $OUTPUT->user_picture($user);
+ }
+
+ public function col_fullname($attempt) {
+ $html = parent::col_fullname($attempt);
+ if ($this->is_downloading()) {
+ return $html;
+ }
+
+ return $html . html_writer::empty_tag('br') . html_writer::link(
+ new moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->attempt)),
+ get_string('reviewattempt', 'quiz'), array('class' => 'reviewlink'));
+ }
+
+ public function col_timestart($attempt) {
+ if ($attempt->attempt) {
+ return userdate($attempt->timestart, $this->strtimeformat);
+ } else {
+ return '-';
+ }
+ }
+
+ public function col_timefinish($attempt) {
+ if ($attempt->attempt && $attempt->timefinish) {
+ return userdate($attempt->timefinish, $this->strtimeformat);
+ } else {
+ return '-';
+ }
+ }
+
+ public function col_duration($attempt) {
+ if ($attempt->timefinish) {
+ return format_time($attempt->timefinish - $attempt->timestart);
+ } else if ($attempt->timestart) {
+ return get_string('unfinished', 'quiz');
+ } else {
+ return '-';
+ }
+ }
+
+ public function col_feedbacktext($attempt) {
+ if (!$attempt->timefinish) {
+ return '-';
+ }
+
+ if (!$this->is_downloading()) {
+ return quiz_report_feedback_for_grade(
+ quiz_rescale_grade($attempt->sumgrades, $this->quiz, false),
+ $this->quiz->id, $this->context);
+ } else {
+ return strip_tags(quiz_report_feedback_for_grade(
+ quiz_rescale_grade($attempt->sumgrades, $this->quiz, false),
+ $this->quiz->id));
+ }
+ }
+
+ public function get_row_class($attempt) {
+ if ($this->qmsubselect && $attempt->gradedattempt) {
+ return 'gradedattempt';
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Make a link to review an individual question in a popup window.
+ *
+ * @param string $data HTML fragment. The text to make into the link.
+ * @param object $attempt data for the row of the table being output.
+ * @param int $slot the number used to identify this question within this usage.
+ */
+ public function make_review_link($data, $attempt, $slot) {
+ global $OUTPUT;
+
+ $stepdata = $this->lateststeps[$attempt->usageid][$slot];
+ $state = question_state::get($stepdata->state);
+
+ $flag = '';
+ if ($stepdata->flagged) {
+ $flag = ' ' . $OUTPUT->pix_icon('i/flagged', get_string('flagged', 'question'),
+ 'moodle', array('class' => 'questionflag'));
+ }
+
+ $feedbackimg = '';
+ if ($state->is_finished() && $state != question_state::$needsgrading) {
+ $feedbackimg = ' ' . $this->icon_for_fraction($stepdata->fraction);
+ }
+
+ $output = html_writer::tag('span', html_writer::tag('span',
+ $data . $feedbackimg . $flag,
+ array('class' => $state->get_state_class(true))), array('class' => 'que'));
+
+ $url = new moodle_url('/mod/quiz/reviewquestion.php',
+ array('attempt' => $attempt->attempt, 'slot' => $slot));
+ $output = $OUTPUT->action_link($url, $output,
+ new popup_action('click', $url, 'reviewquestion',
+ array('height' => 450, 'width' => 650)),
+ array('title' => get_string('reviewresponse', 'quiz')));
+
+ return $output;
+ }
+
+ /**
+ * Return an appropriate icon (green tick, red cross, etc.) for a grade.
+ * @param float $fraction grade on a scale 0..1.
+ * @return string html fragment.
+ */
+ protected function icon_for_fraction($fraction) {
+ global $OUTPUT;
+
+ $state = question_state::graded_state_for_fraction($fraction);
+ if ($state == question_state::$gradedright) {
+ $icon = 'i/tick_green_big';
+ } else if ($state == question_state::$gradedpartial) {
+ $icon = 'i/tick_amber_big';
+ } else {
+ $icon = 'i/cross_red_big';
+ }
+
+ return $OUTPUT->pix_icon($icon, get_string($state->get_feedback_class(), 'question'),
+ 'moodle', array('class' => 'icon'));
+ }
+
+ /**
+ * Load information about the latest state of selected questions in selected attempts.
+ *
+ * The results are returned as an two dimensional array $qubaid => $slot => $dataobject
+ *
+ * @param qubaid_condition $qubaids used to restrict which usages are included
+ * in the query. See {@link qubaid_condition}.
+ * @param array $slots A list of slots for the questions you want to konw about.
+ * @return array of records. See the SQL in this function to see the fields available.
+ */
+ protected function load_question_latest_steps(qubaid_condition $qubaids) {
+ $dm = new question_engine_data_mapper();
+ $latesstepdata = $dm->load_questions_usages_latest_steps(
+ $qubaids, array_keys($this->questions));
+
+ $lateststeps = array();
+ foreach ($latesstepdata as $step) {
+ $lateststeps[$step->questionusageid][$step->slot] = $step;
+ }
+
+ return $lateststeps;
+ }
+
+ /**
+ * @return bool should {@link query_db()} call {@link load_question_latest_steps}?
+ */
+ protected function requires_latest_steps_loaded() {
+ return false;
+ }
+
+ /**
+ * Is this a column that depends on joining to the latest state information?
+ * If so, return the corresponding slot. If not, return false.
+ * @param string $column a column name
+ * @return int false if no, else a slot.
+ */
+ protected function is_latest_step_column($column) {
+ return false;
+ }
+
+ /**
+ * Get any fields that might be needed when sorting on date for a particular slot.
+ * @param int $slot the slot for the column we want.
+ * @param string $alias the table alias for latest state information relating to that slot.
+ */
+ protected function get_required_latest_state_fields($slot, $alias) {
+ return '';
+ }
+
+ /**
+ * Add the information about the latest state of the question with slot
+ * $slot to the query.
+ *
+ * The extra information is added as a join to a
+ * 'table' with alias qa$slot, with columns that are a union of
+ * the columns of the question_attempts and question_attempts_states tables.
+ *
+ * @param int $slot the question to add information for.
+ */
+ protected function add_latest_state_join($slot) {
+ $alias = 'qa' . $slot;
+
+ $fields = $this->get_required_latest_state_fields($slot, $alias);
+ if (!$fields) {
+ return;
+ }
+
+ $dm = new question_engine_data_mapper();
+ $inlineview = $dm->question_attempt_latest_state_view($alias);
+
+ $this->sql->fields .= ",\n$fields";
+ $this->sql->from .= "\nLEFT JOIN $inlineview ON " .
+ "$alias.questionusageid = quiza.uniqueid AND $alias.slot = :{$alias}slot";
+ $this->sql->params[$alias . 'slot'] = $slot;
+ }
+
+ /**
+ * Get an appropriate qubaid_condition for loading more data about the
+ * attempts we are displaying.
+ * @return qubaid_condition
+ */
+ protected function get_qubaids_condition() {
+ if (is_null($this->rawdata)) {
+ throw new coding_exception(
+ 'Cannot call get_qubaids_condition until the main data has been loaded.');
+ }
+
+ if ($this->is_downloading()) {
+ // We want usages for all attempts.
+ return new qubaid_join($this->sql->from, 'quiza.uniqueid',
+ $this->sql->where, $this->sql->params);
+ }
+
+ $qubaids = array();
+ foreach ($this->rawdata as $attempt) {
+ if ($attempt->usageid > 0) {
+ $qubaids[] = $attempt->usageid;
+ }
+ }
+
+ return new qubaid_list($qubaids);
+ }
+
+ public function query_db($pagesize, $useinitialsbar = true) {
+ $doneslots = array();
+ foreach ($this->get_sort_columns() as $column => $notused) {
+ $slot = $this->is_latest_step_column($column);
+ if ($slot && !in_array($slot, $doneslots)) {
+ $this->add_latest_state_join($slot);
+ $doneslots[] = $slot;
+ }
+ }
+
+ parent::query_db($pagesize, $useinitialsbar);
+
+ if ($this->requires_latest_steps_loaded()) {
+ $qubaids = $this->get_qubaids_condition();
+ $this->lateststeps = $this->load_question_latest_steps($qubaids);
+ }
+ }
+
+ public function get_sort_columns() {
+ // Add attemptid as a final tie-break to the sort. This ensures that
+ // Attempts by the same student appear in order when just sorting by name.
+ $sortcolumns = parent::get_sort_columns();
+ $sortcolumns['quiza.id'] = SORT_ASC;
+ return $sortcolumns;
+ }
+}
diff --git a/mod/quiz/report/default.php b/mod/quiz/report/default.php
index 465e1df6b01..ad80e2ea5f9 100644
--- a/mod/quiz/report/default.php
+++ b/mod/quiz/report/default.php
@@ -1,37 +1,62 @@
.
-////////////////////////////////////////////////////////////////////
-/// Default class for report plugins
-///
-/// Doesn't do anything on it's own -- it needs to be extended.
-/// This class displays quiz reports. Because it is called from
-/// within /mod/quiz/report.php you can assume that the page header
-/// and footer are taken care of.
-///
-/// This file can refer to itself as report.php to pass variables
-/// to itself - all these will also be globally available. You must
-/// pass "id=$cm->id" or q=$quiz->id", and "mode=reportname".
-////////////////////////////////////////////////////////////////////
+/**
+ * Base class for quiz report plugins.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
-// Included by ../report.php
-class quiz_default_report {
+defined('MOODLE_INTERNAL') || die();
- function display($cm, $course, $quiz) { /// This function just displays the report
- return true;
- }
- function print_header_and_tabs($cm, $course, $quiz, $reportmode="overview") {
- global $CFG, $PAGE, $OUTPUT;
- /// Define some strings
- $strquizzes = get_string("modulenameplural", "quiz");
- $strquiz = get_string("modulename", "quiz");
- /// Print the page header
+/**
+ * Base class for quiz report plugins.
+ *
+ * Doesn't do anything on it's own -- it needs to be extended.
+ * This class displays quiz reports. Because it is called from
+ * within /mod/quiz/report.php you can assume that the page header
+ * and footer are taken care of.
+ *
+ * This file can refer to itself as report.php to pass variables
+ * to itself - all these will also be globally available. You must
+ * pass "id=$cm->id" or q=$quiz->id", and "mode=reportname".
+ *
+ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class quiz_default_report {
+ /**
+ * Override this function to displays the report.
+ * @param $cm the course-module for this quiz.
+ * @param $course the coures we are in.
+ * @param $quiz this quiz.
+ */
+ public abstract function display($cm, $course, $quiz);
+
+ public function print_header_and_tabs($cm, $course, $quiz, $reportmode = 'overview') {
+ global $PAGE, $OUTPUT;
+
+ // Print the page header
$PAGE->set_title(format_string($quiz->name));
$PAGE->set_heading($course->fullname);
echo $OUTPUT->header();
- $course_context = get_context_instance(CONTEXT_COURSE, $course->id);
}
}
-
-
diff --git a/mod/quiz/report/grading/db/access.php b/mod/quiz/report/grading/db/access.php
new file mode 100644
index 00000000000..e3fdd609d69
--- /dev/null
+++ b/mod/quiz/report/grading/db/access.php
@@ -0,0 +1,50 @@
+.
+
+/**
+ * Capability definitions for the quiz manual grading report.
+ *
+ * @package quiz
+ * @subpackage grading
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+ // Is the user allowed to see the student's real names while grading?
+ 'quiz/grading:viewstudentnames' => array(
+ 'captype' => 'read',
+ 'contextlevel' => CONTEXT_MODULE,
+ 'legacy' => array(
+ 'teacher' => CAP_ALLOW,
+ 'editingteacher' => CAP_ALLOW
+ ),
+ 'clonepermissionsfrom' => 'mod/quiz:viewreports'
+ ),
+
+ // Is the user allowed to see the student's idnumber while grading?
+ 'quiz/grading:viewidnumber' => array(
+ 'captype' => 'read',
+ 'contextlevel' => CONTEXT_MODULE,
+ 'legacy' => array(
+ 'teacher' => CAP_ALLOW,
+ 'editingteacher' => CAP_ALLOW
+ ),
+ 'clonepermissionsfrom' => 'mod/quiz:viewreports'
+ )
+);
diff --git a/mod/quiz/report/grading/gradingsettings_form.php b/mod/quiz/report/grading/gradingsettings_form.php
new file mode 100644
index 00000000000..5df0aa3a348
--- /dev/null
+++ b/mod/quiz/report/grading/gradingsettings_form.php
@@ -0,0 +1,97 @@
+.
+
+/**
+ * This file defines the setting form for the quiz grading report.
+ *
+ * @package quiz
+ * @subpackage grading
+ * @copyright 2010 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+
+/**
+ * Quiz grading report settings form.
+ *
+ * @copyright 2010 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_grading_settings extends moodleform {
+ protected $includeauto;
+ protected $hidden = array();
+ protected $counts;
+ protected $shownames;
+ protected $showidnumbers;
+
+ public function __construct($hidden, $counts, $shownames, $showidnumbers) {
+ global $CFG;
+ $this->includeauto = !empty($hidden['includeauto']);
+ $this->hidden = $hidden;
+ $this->counts = $counts;
+ $this->shownames = $shownames;
+ $this->showidnumbers = $showidnumbers;
+ parent::__construct($CFG->wwwroot . '/mod/quiz/report.php', null, 'get');
+ }
+
+ protected function definition() {
+ $mform =& $this->_form;
+
+ $mform->addElement('header', 'options', get_string('options', 'quiz_grading'));
+
+ $gradeoptions = array();
+ foreach (array('needsgrading', 'manuallygraded', 'autograded', 'all') as $type) {
+ if (empty($this->counts->$type)) {
+ continue;
+ }
+ if ($type == 'autograded' && !$this->includeauto) {
+ continue;
+ }
+ $gradeoptions[$type] = get_string('gradeattempts' . $type, 'quiz_grading',
+ $this->counts->$type);
+ }
+ $mform->addElement('select', 'grade', get_string('attemptstograde', 'quiz_grading'),
+ $gradeoptions);
+
+ $mform->addElement('text', 'pagesize', get_string('questionsperpage', 'quiz_grading'),
+ array('size' => 3));
+ $mform->setType('pagesize', PARAM_INT);
+
+ $orderoptions = array(
+ 'random' => get_string('randomly', 'quiz_grading'),
+ 'date' => get_string('bydate', 'quiz_grading'),
+ );
+ if ($this->shownames) {
+ $orderoptions['student'] = get_string('bystudentname', 'quiz_grading');
+ }
+ if ($this->showidnumbers) {
+ $orderoptions['idnumber'] = get_string('bystudentidnumber', 'quiz_grading');
+ }
+ $mform->addElement('select', 'order', get_string('orderattempts', 'quiz_grading'),
+ $orderoptions);
+
+ foreach ($this->hidden as $name => $value) {
+ $mform->addElement('hidden', $name, $value);
+ }
+
+ $mform->addElement('submit', 'submitbutton', get_string('changeoptions', 'quiz_grading'));
+ }
+}
diff --git a/mod/quiz/report/grading/lang/en/quiz_grading.php b/mod/quiz/report/grading/lang/en/quiz_grading.php
index 25a91403574..676d99e77b2 100644
--- a/mod/quiz/report/grading/lang/en/quiz_grading.php
+++ b/mod/quiz/report/grading/lang/en/quiz_grading.php
@@ -1,5 +1,4 @@
attempt} for {$a->fullname}';
+$string['gradingattemptwithidnumber'] = 'Attempt number {$a->attempt} for {$a->fullname} ({$a->idnumber})';
+$string['gradingattemptsxtoyofz'] = 'Grading attempts {$a->from} to {$a->to} of {$a->of}';
$string['gradingnextungraded'] = 'Next {$a} ungraded attempts';
$string['gradingnotallowed'] = 'You do not have permission to manually grade responses in this quiz';
+$string['gradingquestionx'] = 'Grading question {$a->number}: {$a->questionname}';
$string['gradingreport'] = 'Manual grading report';
-$string['gradingungraded'] = '{$a} ungraded attempts';
$string['gradinguser'] = 'Attempts for {$a}';
-$string['invalidattemptid'] = 'No such attempt ID exists';
-$string['invalidquestionid'] = 'Gradeable question with id {$a} not found';
+$string['gradingungraded'] = '{$a} ungraded attempts';
+$string['hideautomaticallygraded'] = 'Hide questions that have been graded automatically';
+$string['inprogress'] = 'In progress';
+$string['noquestionsfound'] = 'No manually graded questions found';
+$string['options'] = 'Options';
+$string['orderattempts'] = 'Order attempts';
+$string['qno'] = 'Q #';
+$string['questionname'] = 'Question name';
+$string['questionsperpage'] = 'Questions per page';
+$string['questionsthatneedgrading'] = 'Questions that need grading';
$string['questiontitle'] = 'Question {$a->number} : "{$a->name}" ({$a->openspan}{$a->gradedattempts}{$a->closespan} / {$a->totalattempts} attempts {$a->openspan}graded{$a->closespan}).';
+$string['randomly'] = 'Randomly';
+$string['saveandnext'] = 'Save and go to next page';
+$string['showstudentnames'] = 'Show student names';
+$string['tograde'] = 'To grade';
+$string['total'] = 'Total';
+$string['unknownquestion'] = 'Unknown question';
+$string['updategrade'] = 'update grades';
diff --git a/mod/quiz/report/grading/report.php b/mod/quiz/report/grading/report.php
index 4cf45499e32..44c420f56e1 100644
--- a/mod/quiz/report/grading/report.php
+++ b/mod/quiz/report/grading/report.php
@@ -1,471 +1,554 @@
.
+
/**
- * Quiz report to help teachers manually grade quiz questions that need it.
+ * This file defines the quiz manual grading report class.
*
- * @package quiz
- * @subpackage reports
+ * @package quiz
+ * @subpackage grading
+ * @copyright 2006 Gustav Delius
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-// Flow of the file:
-// Get variables, run essential queries
-// Check for post data submitted. If exists, then process data (the data is the grades and comments for essay questions)
-// Check for userid, attemptid, or gradeall and for questionid. If found, print out the appropriate essay question attempts
-// Switch:
-// first case: print out all essay questions in quiz and the number of ungraded attempts
-// second case: print out all users and their attempts for a specific essay question
-require_once($CFG->dirroot . "/mod/quiz/editlib.php");
-require_once($CFG->libdir . '/tablelib.php');
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/quiz/report/grading/gradingsettings_form.php');
+
/**
- * Quiz report to help teachers manually grade quiz questions that need it.
+ * Quiz report to help teachers manually grade questions that need it.
*
- * @package quiz
- * @subpackage reports
+ * This report basically provides two screens:
+ * - List question that might need manual grading (or optionally all questions).
+ * - Provide an efficient UI to grade all attempts at a particular question.
+ *
+ * @copyright 2006 Gustav Delius
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_grading_report extends quiz_default_report {
- /**
- * Displays the report.
- */
- function display($quiz, $cm, $course) {
- global $CFG, $QTYPES, $DB, $OUTPUT, $PAGE;
+ const DEFAULT_PAGE_SIZE = 5;
+ const DEFAULT_ORDER = 'random';
- $viewoptions = array('mode'=>'grading', 'q'=>$quiz->id);
+ protected $viewoptions = array();
+ protected $questions;
+ protected $currentgroup;
+ protected $users;
+ protected $cm;
+ protected $quiz;
+ protected $context;
- if ($questionid = optional_param('questionid', 0, PARAM_INT)){
- $viewoptions += array('questionid'=>$questionid);
- }
-
- // grade question specific parameters
- if ($userid = optional_param('userid', 0, PARAM_INT)){
- $viewoptions += array('userid'=>$userid);
- }
- if ($attemptid = optional_param('attemptid', 0, PARAM_INT)){
- $viewoptions += array('attemptid'=>$attemptid);
- }
- if ($gradeall = optional_param('gradeall', 0, PARAM_INT)){
- $viewoptions += array('gradeall'=> $gradeall);
- }
- if ($gradeungraded = optional_param('gradeungraded', 0, PARAM_INT)){
- $viewoptions += array('gradeungraded'=> $gradeungraded);
- }
- if ($gradenextungraded = optional_param('gradenextungraded', 0, PARAM_INT)){
- $viewoptions += array('gradenextungraded'=> $gradenextungraded);
- }
+ public function display($quiz, $cm, $course) {
+ global $CFG, $DB, $PAGE;
+ $this->quiz = $quiz;
$this->cm = $cm;
+ $this->course = $course;
- $this->print_header_and_tabs($cm, $course, $quiz, $reportmode="grading");
+ // Get the URL options.
+ $slot = optional_param('slot', null, PARAM_INT);
+ $questionid = optional_param('qid', null, PARAM_INT);
+ $grade = optional_param('grade', null, PARAM_ALPHA);
+
+ $includeauto = optional_param('includeauto', false, PARAM_BOOL);
+ if (!in_array($grade, array('all', 'needsgrading', 'autograded', 'manuallygraded'))) {
+ $grade = null;
+ }
+ $pagesize = optional_param('pagesize', self::DEFAULT_PAGE_SIZE, PARAM_INT);
+ $page = optional_param('page', 0, PARAM_INT);
+ $order = optional_param('order', self::DEFAULT_ORDER, PARAM_ALPHA);
+
+ // Assemble the options requried to reload this page.
+ $optparams = array('includeauto', 'page');
+ foreach ($optparams as $param) {
+ if ($$param) {
+ $this->viewoptions[$param] = $$param;
+ }
+ }
+ if ($pagesize != self::DEFAULT_PAGE_SIZE) {
+ $this->viewoptions['pagesize'] = $pagesize;
+ }
+ if ($order != self::DEFAULT_ORDER) {
+ $this->viewoptions['order'] = $order;
+ }
// Check permissions
$this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
- if (!has_capability('mod/quiz:grade', $this->context)) {
- echo $OUTPUT->notification(get_string('gradingnotallowed', 'quiz_grading'));
- return true;
+ require_capability('mod/quiz:grade', $this->context);
+ $shownames = has_capability('quiz/grading:viewstudentnames', $this->context);
+ $showidnumbers = has_capability('quiz/grading:viewidnumber', $this->context);
+
+ // Validate order.
+ if (!in_array($order, array('random', 'date', 'student', 'idnumber'))) {
+ $order = self::DEFAULT_ORDER;
+ } else if (!$shownames && $order == 'student') {
+ $order = self::DEFAULT_ORDER;
+ } else if (!$showidnumbers && $order == 'idnumber') {
+ $order = self::DEFAULT_ORDER;
+ }
+ if ($order == 'random') {
+ $page = 0;
}
- $gradeableqs = quiz_report_load_questions($quiz);
- $questionsinuse = implode(',', array_keys($gradeableqs));
- foreach ($gradeableqs as $qid => $question){
- if (!$QTYPES[$question->qtype]->is_question_manual_graded($question, $questionsinuse)){
- unset($gradeableqs[$qid]);
- }
+ // Get the list of questions in this quiz.
+ $this->questions = quiz_report_get_significant_questions($quiz);
+ if ($slot && !array_key_exists($slot, $this->questions)) {
+ throw new moodle_exception('unknownquestion', 'quiz_grading');
}
- if (empty($gradeableqs)) {
- echo $OUTPUT->heading(get_string('noessayquestionsfound', 'quiz'));
- return true;
- } else if (count($gradeableqs)==1){
- $questionid = array_shift(array_keys($gradeableqs));
+ // Process any submitted data.
+ if ($data = data_submitted() && confirm_sesskey() && $this->validate_submitted_marks()) {
+ $this->process_submitted_data();
+
+ redirect($this->grade_question_url($slot, $questionid, $grade, $page + 1));
}
- $currentgroup = groups_get_activity_group($this->cm, true);
- $this->users = get_users_by_capability($this->context, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'','','','',$currentgroup,'',false);
+ // Get the group, and the list of significant users.
+ $this->currentgroup = groups_get_activity_group($this->cm, true);
+ $this->users = get_users_by_capability($this->context,
+ array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), '', '', '', '',
+ $this->currentgroup, '', false);
- if (!empty($questionid)) {
- if (!isset($gradeableqs[$questionid])){
- print_error('invalidquestionid', 'quiz_grading', '', $questionid);
- } else {
- $question =& $gradeableqs[$questionid];
- }
+ // Start output.
+ $this->print_header_and_tabs($cm, $course, $quiz, 'grading');
- // Some of the questions code is optimised to work with several questions
- // at once so it wants the question to be in an array. The array key
- // must be the question id.
- $key = $question->id;
- $questions[$key] = &$question;
+ // What sort of page to display?
+ if (!quiz_questions_in_quiz($quiz->questions)) {
+ echo quiz_no_questions_message($quiz, $cm, $this->context);
- // We need to add additional questiontype specific information to
- // the question objects.
- if (!get_question_options($questions)) {
- print_error('cannotloadquestioninfo', 'quiz_grading');
- }
- // This will have extended the question object so that it now holds
- // all the information about the questions that may be needed later.
- }
+ } else if (!$slot) {
+ $this->display_index($includeauto);
- add_to_log($course->id, "quiz", "manualgrading", "report.php?mode=grading&q=$quiz->id", "$quiz->id", "$cm->id");
-
- if ($data = data_submitted()) { // post data submitted, process it
- if (confirm_sesskey() && $this->users){
-
- // now go through all of the responses and save them.
- $allok = true;
- foreach($data->manualgrades as $uniqueid => $response) {
- // get our attempt
- $uniqueid = clean_param($uniqueid, PARAM_INT);
- list($usql, $params) = $DB->get_in_or_equal(array_keys($this->users));
-
- if (!$attempt = $DB->get_record_sql("SELECT * FROM {quiz_attempts} " .
- "WHERE uniqueid = ? AND " .
- "userid $usql AND " .
- "quiz=?", array_merge(array($uniqueid), $params, array($quiz->id)))){
- print_error('invalidattemptid', 'quiz_grading');
- }
-
- // Load the state for this attempt (The questions array was created earlier)
- $states = get_question_states($questions, $quiz, $attempt);
- // The $states array is indexed by question id but because we are dealing
- // with only one question there is only one entry in this array
- $state = &$states[$question->id];
-
- // the following will update the state and attempt
- $error = question_process_comment($question, $state, $attempt,
- $response['comment'], FORMAT_HTML, $response['grade']);
- if (is_string($error)) {
- echo $OUTPUT->notification($error);
- $allok = false;
- } else if ($state->changed) {
- // If the state has changed save it and update the quiz grade
- save_question_session($question, $state);
- quiz_save_best_grade($quiz, $attempt->userid);
- }
- }
-
- if ($allok) {
- echo $OUTPUT->notification(get_string('changessaved', 'quiz'), 'notifysuccess');
- } else {
- echo $OUTPUT->notification(get_string('changessavedwitherrors', 'quiz'), 'notifysuccess');
- }
- }
- }
- $this->viewurl = new moodle_url('/mod/quiz/report.php', $viewoptions);
- /// find out current groups mode
-
- if ($groupmode = groups_get_activity_groupmode($this->cm)) { // Groups are being used
- groups_print_activity_menu($this->cm, $this->viewurl->out(true, array('userid'=>0, 'attemptid'=>0)));
- }
-
- if(empty($this->users)) {
- if ($currentgroup){
- echo $OUTPUT->notification(get_string('nostudentsingroup'));
- } else {
- echo $OUTPUT->notification(get_string('nostudentsyet'));
- }
- return true;
- }
- $qattempts = quiz_get_total_qas_graded_and_ungraded($quiz, array_keys($gradeableqs), array_keys($this->users));
- if(empty($qattempts)) {
- echo $OUTPUT->notification(get_string('noattemptstoshow', 'quiz'));
- return true;
- }
- $qmenu = array();
- foreach ($gradeableqs as $qid => $questionformenu){
- $a= new stdClass();
- $a->number = $gradeableqs[$qid]->number;
- $a->name = $gradeableqs[$qid]->name;
- $a->gradedattempts =$qattempts[$qid]->gradedattempts;
- $a->totalattempts =$qattempts[$qid]->totalattempts;
- $a->openspan ='';
- $a->closespan ='';
- $qmenu[$qid]= get_string('questiontitle', 'quiz_grading', $a);
- }
- if (count($gradeableqs)!=1){
- $qurl = fullclone($this->viewurl);
- $qurl->remove_params('questionid', 'attemptid', 'gradeall', 'gradeungraded', 'gradenextungraded');
- $menu = $OUTPUT->single_select($qurl, 'questionid', $qmenu, $questionid, array(''=>'choosedots'), 'questionid');
- echo '
'.$menu.'
';
- }
- if (!$questionid){
- return true;
- }
- $a= new stdClass();
- $a->number = $question->number;
- $a->name = $question->name;
- $a->gradedattempts =$qattempts[$question->id]->gradedattempts;
- $a->totalattempts =$qattempts[$question->id]->totalattempts;
- $a->openspan ='
';
- $a->closespan ='';
- echo $OUTPUT->heading(get_string('questiontitle', 'quiz_grading', $a));
-
- // our 2 different views
-
- // the first view allows a user to select a question and
- // displays the users who have answered the essay question
- // and all of their attempts at answering the question
-
- // the second prints selected attempt answer(s) with a comment
- // and grade form underneath them
-
- $ungraded = $qattempts[$questionid]->totalattempts- $qattempts[$questionid]->gradedattempts;
- if ($gradenextungraded ||$gradeungraded || $gradeall || $userid || $attemptid){
- $this->print_questions_and_form($quiz, $question, $userid, $attemptid, $gradeungraded, $gradenextungraded, $ungraded);
} else {
- $this->view_question($quiz, $question, $qattempts[$questionid]->totalattempts, $ungraded);
+ $this->display_grading_interface($slot, $questionid, $grade,
+ $pagesize, $page, $shownames, $showidnumbers, $order);
}
return true;
}
- /**
- * Prints a table with users and their attempts
- *
- * @return void
- * @todo Add current grade to the table
- * Finnish documenting
- **/
- function view_question($quiz, $question, $totalattempts, $ungraded) {
- global $CFG, $DB, $OUTPUT;
+ protected function get_qubaids_condition() {
+ global $DB;
- $usercount = count($this->users);
+ $where = "quiza.quiz = :mangrquizid AND
+ quiza.preview = 0 AND
+ quiza.timefinish <> 0";
+ $params = array('mangrquizid' => $this->cm->instance);
- // set up table
- $tablecolumns = array('picture', 'fullname', 'timefinish', 'grade');
- $tableheaders = array('', get_string('name'), get_string("completedon", "quiz"), '');
-
- $table = new flexible_table('mod-quiz-report-grading');
-
- $table->define_columns($tablecolumns);
- $table->define_headers($tableheaders);
- $table->define_baseurl($this->viewurl->out());
-
- $table->sortable(true);
- $table->initialbars($usercount>20); // will show initialbars if there are more than 20 users
- $table->pageable(true);
- $table->collapsible(true);
-
- $table->column_suppress('fullname');
- $table->column_suppress('picture');
- $table->column_suppress('grade');
-
- $table->column_class('picture', 'picture');
-
- // attributes in the table tag
- $table->set_attribute('cellspacing', '0');
- $table->set_attribute('id', 'attempts');
- $table->set_attribute('class', 'generaltable generalbox');
- $table->set_attribute('align', 'center');
- //$table->set_attribute('width', '50%');
-
- // get it ready!
- $table->setup();
-
- list($select, $from, $where, $params) = $this->attempts_sql($quiz->id, true, $question->id);
-
- list($twhere, $tparams) = $table->get_sql_where();
- if ($twhere) {
- $where .= ' AND '.$twhere; //initial bar
- $params = array_merge($params, $tparams);
+ if ($this->currentgroup) {
+ list($usql, $uparam) = $DB->get_in_or_equal(array_keys($this->users),
+ SQL_PARAMS_NAMED, 'mangru');
+ $where .= ' AND quiza.userid ' . $usql;
+ $params += $uparam;
}
- // sorting of the table
- if ($sort = $table->get_sql_sort()) {
- $sort = 'ORDER BY '.$sort; // seems like I would need to have u. or qa. infront of the ORDER BY attribues... but seems to work..
- } else {
- // my default sort rule
- $sort = 'ORDER BY u.firstname, u.lastname, qa.timefinish ASC';
- }
-
- // set up the pagesize
- $table->pagesize(QUIZ_REPORT_DEFAULT_PAGE_SIZE, $totalattempts);
-
- // get the attempts and process them
- echo '
';
- if ($attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, $table->get_page_start(), $table->get_page_size())) {
- // grade all link
- $links = "
id&questionid=$question->id\">".get_string('gradeall', 'quiz_grading', $totalattempts).'';
- if ($ungraded>0){
- $links .="
id&questionid=$question->id\">".get_string('gradeungraded', 'quiz_grading', $ungraded).'';
- if ($ungraded>QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE){
- $links .="
id&questionid=$question->id\">".get_string('gradenextungraded', 'quiz_grading', QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE).'';
- }
- }
- $table->add_data_keyed(array('grade'=> $links));
- $table->add_separator();
- foreach($attempts as $attempt) {
-
- $user = clone($attempt);
- $user->id = $user->userid;
- $picture = $OUTPUT->user_picture($user, array('courseid'=>$quiz->course));
-
- // link to student profile
- $userlink = "
wwwroot/user/view.php?id=$attempt->userid&course=$quiz->course\">".
- fullname($attempt, true).'';
-
- $gradedclass = question_state_is_graded($attempt)?' class="highlightgraded" ':'';
- $gradedstring = question_state_is_graded($attempt)?(' '.get_string('graded','quiz_grading')):'';
-
- // link for the attempt
- $attemptlink = "
id&questionid=$question->id&attemptid=$attempt->attemptid\">".
- userdate($attempt->timefinish, get_string('strftimedatetime')).
- $gradedstring.'';
-
- // grade all attempts for this user
- $gradelink = "
id&questionid=$question->id&userid=$attempt->userid\">".
- get_string('grade').'';
-
- $table->add_data( array($picture, $userlink, $attemptlink, $gradelink) );
- }
- $table->add_separator();
- $table->add_data_keyed(array('grade'=> $links));
- }
- // print everything here
- $table->print_html();
- echo '
';
+ return new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
}
+ protected function load_attempts_by_usage_ids($qubaids) {
+ global $DB;
+
+ list($asql, $params) = $DB->get_in_or_equal($qubaids);
+ $params[] = $this->quiz->id;
+
+ $attemptsbyid = $DB->get_records_sql("
+ SELECT quiza.*, u.firstname, u.lastname, u.idnumber
+ FROM {quiz_attempts} quiza
+ JOIN {user} u ON u.id = quiza.userid
+ WHERE quiza.uniqueid $asql AND quiza.timefinish <> 0 AND quiza.quiz = ?",
+ $params);
+
+ $attempts = array();
+ foreach ($attemptsbyid as $attempt) {
+ $attempts[$attempt->uniqueid] = $attempt;
+ }
+ return $attempts;
+ }
/**
- * Prints questions with comment and grade form underneath each question
- *
- * @return void
- * @todo Finish documenting this function
- **/
- function print_questions_and_form($quiz, $question, $userid, $attemptid, $gradeungraded, $gradenextungraded, $ungraded) {
- global $CFG, $DB, $OUTPUT;
+ * Get the URL of the front page of the report that lists all the questions.
+ * @param $includeauto if not given, use the current setting, otherwise,
+ * force a paricular value of includeauto in the URL.
+ * @return string the URL.
+ */
+ protected function base_url() {
+ return new moodle_url('/mod/quiz/report.php',
+ array('id' => $this->cm->id, 'mode' => 'grading'));
+ }
- $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
+ /**
+ * Get the URL of the front page of the report that lists all the questions.
+ * @param $includeauto if not given, use the current setting, otherwise,
+ * force a paricular value of includeauto in the URL.
+ * @return string the URL.
+ */
+ protected function list_questions_url($includeauto = null) {
+ $url = $this->base_url();
- $questions[$question->id] = &$question;
+ $url->params($this->viewoptions);
+
+ if (!is_null($includeauto)) {
+ $url->param('includeauto', $includeauto);
+ }
+
+ return $url;
+ }
+
+ /**
+ * @param int $slot
+ * @param int $questionid
+ * @param string $grade
+ * @param mixed $page = true, link to current page. false = omit page.
+ * number = link to specific page.
+ */
+ protected function grade_question_url($slot, $questionid, $grade, $page = true) {
+ $url = $this->base_url();
+ $url->params(array('slot' => $slot, 'qid' => $questionid, 'grade' => $grade));
+ $url->params($this->viewoptions);
+
+ $options = $this->viewoptions;
+ if (!$page) {
+ $url->remove_params('page');
+ } else if (is_integer($page)) {
+ $url->param('page', $page);
+ }
+
+ return $url;
+ }
+
+ protected function format_count_for_table($counts, $type, $gradestring) {
+ $result = $counts->$type;
+ if ($counts->$type > 0) {
+ $result .= ' ' . html_writer::link($this->grade_question_url(
+ $counts->slot, $counts->questionid, $type),
+ get_string($gradestring, 'quiz_grading'),
+ array('class' => 'gradetheselink'));
+ }
+ return $result;
+ }
+
+ protected function display_index($includeauto) {
+ global $OUTPUT;
+
+ if ($groupmode = groups_get_activity_groupmode($this->cm)) {
+ // Groups are being used
+ groups_print_activity_menu($this->cm, $this->list_questions_url());
+ }
+
+ echo $OUTPUT->heading(get_string('questionsthatneedgrading', 'quiz_grading'));
+ if ($includeauto) {
+ $linktext = get_string('hideautomaticallygraded', 'quiz_grading');
+ } else {
+ $linktext = get_string('alsoshowautomaticallygraded', 'quiz_grading');
+ }
+ echo html_writer::tag('p', html_writer::link($this->list_questions_url(!$includeauto),
+ $linktext), array('class' => 'toggleincludeauto'));
+
+ $statecounts = $this->get_question_state_summary(array_keys($this->questions));
+
+ $data = array();
+ foreach ($statecounts as $counts) {
+ if ($counts->all == 0) {
+ continue;
+ }
+ if (!$includeauto && $counts->needsgrading == 0 && $counts->manuallygraded == 0) {
+ continue;
+ }
+
+ $row = array();
+
+ $row[] = $this->questions[$counts->slot]->number;
+
+ $row[] = format_string($counts->name);
+
+ $row[] = $this->format_count_for_table($counts, 'needsgrading', 'grade');
+
+ $row[] = $this->format_count_for_table($counts, 'manuallygraded', 'updategrade');
+
+ if ($includeauto) {
+ $row[] = $this->format_count_for_table($counts, 'autograded', 'updategrade');
+ }
+
+ $row[] = $this->format_count_for_table($counts, 'all', 'gradeall');
+
+ $data[] = $row;
+ }
+
+ if (empty($data)) {
+ echo $OUTPUT->heading(get_string('noquestionsfound', 'quiz_grading'));
+ return;
+ }
+
+ $table = new html_table();
+ $table->class = 'generaltable';
+ $table->id = 'questionstograde';
+
+ $table->head[] = get_string('qno', 'quiz_grading');
+ $table->head[] = get_string('questionname', 'quiz_grading');
+ $table->head[] = get_string('tograde', 'quiz_grading');
+ $table->head[] = get_string('alreadygraded', 'quiz_grading');
+ if ($includeauto) {
+ $table->head[] = get_string('automaticallygraded', 'quiz_grading');
+ }
+ $table->head[] = get_string('total', 'quiz_grading');
+
+ $table->data = $data;
+ echo html_writer::table($table);
+ }
+
+ protected function display_grading_interface($slot, $questionid, $grade,
+ $pagesize, $page, $shownames, $showidnumbers, $order) {
+ global $OUTPUT;
+
+ // Make sure there is something to do.
+ $statecounts = $this->get_question_state_summary(array($slot));
+
+ $counts = null;
+ foreach ($statecounts as $record) {
+ if ($record->questionid == $questionid) {
+ $counts = $record;
+ break;
+ }
+ }
+
+ // If not, redirect back to the list.
+ if (!$counts || $counts->$grade == 0) {
+ redirect($this->list_questions_url(), get_string('alldoneredirecting', 'quiz_grading'));
+ }
+
+ if ($pagesize * $page >= $counts->$grade) {
+ $page = 0;
+ }
+
+ list($qubaids, $count) = $this->get_usage_ids_where_question_in_state(
+ $grade, $slot, $questionid, $order, $page, $pagesize);
+ $attempts = $this->load_attempts_by_usage_ids($qubaids);
+
+ // Prepare the form.
+ $hidden = array(
+ 'id' => $this->cm->id,
+ 'mode' => 'grading',
+ 'slot' => $slot,
+ 'qid' => $questionid,
+ 'page' => $page,
+ );
+ if (array_key_exists('includeauto', $this->viewoptions)) {
+ $hidden['includeauto'] = $this->viewoptions['includeauto'];
+ }
+ $mform = new quiz_grading_settings($hidden, $counts, $shownames, $showidnumbers);
+
+ // Tell the form the current settings.
+ $settings = new stdClass();
+ $settings->grade = $grade;
+ $settings->pagesize = $pagesize;
+ $settings->order = $order;
+ $mform->set_data($settings);
+
+ // Print the heading and form.
+ echo question_engine::initialise_js();
+
+ $a = new stdClass();
+ $a->number = $this->questions[$slot]->number;
+ $a->questionname = format_string($counts->name);
+ echo $OUTPUT->heading(get_string('gradingquestionx', 'quiz_grading', $a));
+ echo html_writer::tag('p', html_writer::link($this->list_questions_url(),
+ get_string('backtothelistofquestions', 'quiz_grading')),
+ array('class' => 'mdl-align'));
+
+ $mform->display();
+
+ // Paging info.
+ $a = new stdClass();
+ $a->from = $page * $pagesize + 1;
+ $a->to = min(($page + 1) * $pagesize, $count);
+ $a->of = $count;
+ echo $OUTPUT->heading(get_string('gradingattemptsxtoyofz', 'quiz_grading', $a), 3);
+
+ if ($count > $pagesize && $order != 'random') {
+ echo $OUTPUT->paging_bar($count, $page, $pagesize,
+ $this->grade_question_url($slot, $questionid, $grade, false));
+ }
+
+ // Display the form with one section for each attempt.
$usehtmleditor = can_use_html_editor();
+ $sesskey = sesskey();
+ $qubaidlist = implode(',', $qubaids);
+ echo html_writer::start_tag('form', array('method' => 'post',
+ 'action' => $this->grade_question_url($slot, $questionid, $grade, $page),
+ 'class' => 'mform', 'id' => 'manualgradingform')) .
+ html_writer::start_tag('div') .
+ html_writer::input_hidden_params(new moodle_url('', array(
+ 'qubaids' => $qubaidlist, 'slots' => $slot, 'sesskey' => $sesskey)));
- list($select, $from, $where, $params) = $this->attempts_sql($quiz->id, false, $question->id, $userid, $attemptid, $gradeungraded, $gradenextungraded);
+ foreach ($qubaids as $qubaid) {
+ $attempt = $attempts[$qubaid];
+ $quba = question_engine::load_questions_usage_by_activity($qubaid);
+ $displayoptions = quiz_get_review_options($this->quiz, $attempt, $this->context);
+ $displayoptions->hide_all_feedback();
+ $displayoptions->history = question_display_options::HIDDEN;
+ $displayoptions->manualcomment = question_display_options::EDITABLE;
- $sort = 'ORDER BY u.firstname, u.lastname, qa.attempt ASC';
-
- if ($gradenextungraded){
- $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, 0, QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE);
- } else {
- $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params);
- }
- if ($attempts){
- $firstattempt = current($attempts);
- $fullname = fullname($firstattempt);
- if ($gradeungraded) { // getting all ungraded attempts
- echo $OUTPUT->heading(get_string('gradingungraded','quiz_grading', $ungraded), 3);
- } else if ($gradenextungraded) { // getting next ungraded attempts
- echo $OUTPUT->heading(get_string('gradingnextungraded','quiz_grading', QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE), 3);
- } else if ($userid){
- echo $OUTPUT->heading(get_string('gradinguser','quiz_grading', $fullname), 3);
- } else if ($attemptid){
- $a = new stdClass();
- $a->fullname = $fullname;
- $a->attempt = $firstattempt->attempt;
- echo $OUTPUT->heading(get_string('gradingattempt','quiz_grading', $a), 3);
- } else {
- echo $OUTPUT->heading(get_string('gradingall','quiz_grading', count($attempts)), 3);
+ $heading = $this->get_question_heading($attempt, $shownames, $showidnumbers);
+ if ($heading) {
+ echo $OUTPUT->heading($heading, 4);
}
+ echo $quba->render_question($slot, $displayoptions, $this->questions[$slot]->number);
+ }
- // Display the form with one part for each selected attempt
+ echo html_writer::tag('div', html_writer::empty_tag('input', array(
+ 'type' => 'submit', 'value' => get_string('saveandnext', 'quiz_grading'))),
+ array('class' => 'mdl-align')) .
+ html_writer::end_tag('div') . html_writer::end_tag('form');
+ }
- echo '
';
+ }
+
+ return true;
+ }
+
+ protected function process_submitted_data() {
+ global $DB;
+
+ $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
+ if (!$qubaids) {
+ return;
+ }
+
+ $qubaids = clean_param(explode(',', $qubaids), PARAM_INT);
+ $attempts = $this->load_attempts_by_usage_ids($qubaids);
+
+ $transaction = $DB->start_delegated_transaction();
+ foreach ($qubaids as $qubaid) {
+ $attempt = $attempts[$qubaid];
+ $quba = question_engine::load_questions_usage_by_activity($qubaid);
+ $attemptobj = new quiz_attempt($attempt, $this->quiz, $this->cm, $this->course);
+ $attemptobj->process_all_actions(time());
+ }
+ $transaction->allow_commit();
+ }
+
+ /**
+ * Load information about the number of attempts at various questions in each
+ * summarystate.
+ *
+ * The results are returned as an two dimensional array $qubaid => $slot => $dataobject
+ *
+ * @param array $slots A list of slots for the questions you want to konw about.
+ * @return array The array keys are slot,qestionid. The values are objects with
+ * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
+ * $manuallygraded and $all.
+ */
+ protected function get_question_state_summary($slots) {
+ $dm = new question_engine_data_mapper();
+ return $dm->load_questions_usages_question_state_summary(
+ $this->get_qubaids_condition(), $slots);
+ }
+
+ /**
+ * Get a list of usage ids where the question with slot $slot, and optionally
+ * also with question id $questionid, is in summary state $summarystate. Also
+ * return the total count of such states.
+ *
+ * Only a subset of the ids can be returned by using $orderby, $limitfrom and
+ * $limitnum. A special value 'random' can be passed as $orderby, in which case
+ * $limitfrom is ignored.
+ *
+ * @param int $slot The slot for the questions you want to konw about.
+ * @param int $questionid (optional) Only return attempts that were of this specific question.
+ * @param string $summarystate 'all', 'needsgrading', 'autograded' or 'manuallygraded'.
+ * @param string $orderby 'random', 'date', 'student' or 'idnumber'.
+ * @param int $page implements paging of the results.
+ * Ignored if $orderby = random or $pagesize is null.
+ * @param int $pagesize implements paging of the results. null = all.
+ */
+ protected function get_usage_ids_where_question_in_state($summarystate, $slot,
+ $questionid = null, $orderby = 'random', $page = 0, $pagesize = null) {
+ global $CFG;
+ $dm = new question_engine_data_mapper();
+
+ if ($pagesize && $orderby != 'random') {
+ $limitfrom = $page * $pagesize;
} else {
- echo $OUTPUT->notification(get_string('noattemptstoshow', 'quiz'));
+ $limitfrom = 0;
}
- }
- function attempts_sql($quizid, $wantstateevent=false, $questionid=0, $userid=0, $attemptid=0, $gradeungraded=0, $gradenextungraded=0){
- global $CFG, $DB;
- // this sql joins the attempts table and the user table
- $select = 'SELECT qa.id AS attemptid, qa.uniqueid, qa.attempt, qa.timefinish, qa.preview,
- u.id AS userid, u.firstname, u.lastname, u.picture, u.imagealt, u.email ';
- if ($wantstateevent && $questionid){
- $select .= ', qs.event ';
- }
- $from = 'FROM {user} u, ' .
- '{quiz_attempts} qa ';
+ $qubaids = $this->get_qubaids_condition();
+
$params = array();
-
- $from .= "LEFT JOIN {question_sessions} qns " .
- "ON (qns.attemptid = qa.uniqueid AND qns.questionid = ?) ";
- $params[] = $questionid;
- $from .= "LEFT JOIN {question_states} qs " .
- "ON (qs.id = qns.newest AND qs.question = ?) ";
- $params[] = $questionid;
-
- list($usql, $u_params) = $DB->get_in_or_equal(array_keys($this->users));
- if ($gradenextungraded || $gradeungraded) { // get ungraded attempts
- $where = "WHERE u.id $usql AND qs.event NOT IN (".QUESTION_EVENTS_GRADED.")";
- $params = array_merge($params, $u_params);
- } else if ($userid) { // get all the attempts for a specific user
- $where = 'WHERE u.id=?';
- $params[] = $userid;
- } else if ($attemptid) { // get a specific attempt
- $where = 'WHERE qa.id=? ';
- $params[] = $attemptid;
- } else { // get all user attempts
- $where = "WHERE u.id $usql ";
- $params = array_merge($params, $u_params);
+ if ($orderby == 'date') {
+ list($statetest, $params) = $dm->in_summary_state_test(
+ 'manuallygraded', false, 'mangrstate');
+ $orderby = "(
+ SELECT MAX(sortqas.timecreated)
+ FROM {question_attempt_steps} sortqas
+ WHERE sortqas.questionattemptid = qa.id
+ AND sortqas.state $statetest
+ )";
+ } else if ($orderby == 'student' || $orderby == 'idnumber') {
+ $qubaids->from .= " JOIN {user} u ON quiza.userid = u.id ";
+ if ($orderby == 'student') {
+ $orderby = sql_fullname('u.firstname', 'u.lastname');
+ }
}
- $where .= ' AND qs.event IN ('.QUESTION_EVENTS_CLOSED_OR_GRADED.')';
-
- $where .= ' AND u.id = qa.userid AND qa.quiz = ?';
- $params[] = $quizid;
- // ignore previews
- $where .= ' AND preview = 0 ';
-
- $where .= ' AND qa.timefinish != 0 ';
-
- return array($select, $from, $where, $params);
+ return $dm->load_questions_usages_where_question_in_state($qubaids, $summarystate,
+ $slot, $questionid, $orderby, $params, $limitfrom, $pagesize);
}
-
}
-
-
diff --git a/mod/quiz/report/grading/styles.css b/mod/quiz/report/grading/styles.css
new file mode 100644
index 00000000000..9b21bc128c8
--- /dev/null
+++ b/mod/quiz/report/grading/styles.css
@@ -0,0 +1,4 @@
+#page-mod-quiz-report #manualgradingform {width: 100%;}
+#page-mod-quiz-report #manualgradingform.mform br {clear: none;}
+#page-mod-quiz-report #manualgradingform.mform .clearfix:after {clear: none;}
+#page-mod-quiz-report #manualgradingform .que {margin-bottom: 0.7em;}
diff --git a/mod/quiz/report/grading/version.php b/mod/quiz/report/grading/version.php
new file mode 100644
index 00000000000..469bb49c34b
--- /dev/null
+++ b/mod/quiz/report/grading/version.php
@@ -0,0 +1,29 @@
+.
+
+/**
+ * Quiz grading report version information.
+ *
+ * @package quiz
+ * @subpackage grading
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2011051200;
+$plugin->requires = 2011060313;
diff --git a/mod/quiz/report/overview/db/install.xml b/mod/quiz/report/overview/db/install.xml
index 347b9345eae..fdfbcf4af45 100644
--- a/mod/quiz/report/overview/db/install.xml
+++ b/mod/quiz/report/overview/db/install.xml
@@ -4,14 +4,14 @@
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
-
+
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/mod/quiz/report/overview/db/upgrade.php b/mod/quiz/report/overview/db/upgrade.php
index 9b7bd6c2be6..3f490839cdf 100644
--- a/mod/quiz/report/overview/db/upgrade.php
+++ b/mod/quiz/report/overview/db/upgrade.php
@@ -1,39 +1,227 @@
.
+/**
+ * Quiz overview report upgrade script.
+ *
+ * @package quiz
+ * @subpackage overview
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Quiz overview report upgrade function.
+ * @param number $oldversion
+ */
function xmldb_quiz_overview_upgrade($oldversion) {
global $CFG, $DB;
$dbman = $DB->get_manager();
-//===== 1.9.0 upgrade line ======//
+ //===== 1.9.0 upgrade line ======//
if ($oldversion < 2009091400) {
- /// Define table quiz_question_regrade to be created
+ // Define table quiz_question_regrade to be created
$table = new xmldb_table('quiz_question_regrade');
- /// Adding fields to table quiz_question_regrade
- $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
- $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
- $table->add_field('attemptid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
- $table->add_field('newgrade', XMLDB_TYPE_NUMBER, '12, 7', null, XMLDB_NOTNULL, null, null);
- $table->add_field('oldgrade', XMLDB_TYPE_NUMBER, '12, 7', null, XMLDB_NOTNULL, null, null);
- $table->add_field('regraded', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
- $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+ // Adding fields to table quiz_question_regrade
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null);
+ $table->add_field('attemptid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null);
+ $table->add_field('newgrade', XMLDB_TYPE_NUMBER, '12, 7', null,
+ XMLDB_NOTNULL, null, null);
+ $table->add_field('oldgrade', XMLDB_TYPE_NUMBER, '12, 7', null,
+ XMLDB_NOTNULL, null, null);
+ $table->add_field('regraded', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null);
+ $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null);
- /// Adding keys to table quiz_question_regrade
+ // Adding keys to table quiz_question_regrade
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
- /// Conditionally launch create table for quiz_question_regrade
+ // Conditionally launch create table for quiz_question_regrade
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
- /// overview savepoint reached
- upgrade_plugin_savepoint(true, 2009091400, 'quizreport', 'overview');
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2009091400, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010040600) {
+
+ // Wipe the quiz_question_regrade before we changes its structure. The data
+ // It contains is not important long-term, and it is almost impossible to upgrade.
+ $DB->delete_records('quiz_question_regrade');
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010040600, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010040601) {
+
+ // Rename field attemptid on table quiz_question_regrade to questionusageid
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('attemptid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null, 'id');
+
+ // Launch rename field questionusageid
+ $dbman->rename_field($table, $field, 'questionusageid');
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010040601, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010040602) {
+
+ // Define field slot to be added to quiz_question_regrade
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('slot', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null, 'questionusageid');
+
+ // Conditionally launch add field slot
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010040602, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010040603) {
+
+ // Define field questionid to be dropped from quiz_question_regrade
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('questionid');
+
+ // Conditionally launch drop field questionusageid
+ if ($dbman->field_exists($table, $field)) {
+ $dbman->drop_field($table, $field);
+ }
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010040603, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010040604) {
+
+ // Rename field newgrade on table quiz_question_regrade to newfraction
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('newgrade', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'slot');
+
+ // Launch rename field newfraction
+ $dbman->rename_field($table, $field, 'newfraction');
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010040604, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010040605) {
+
+ // Rename field oldgrade on table quiz_question_regrade to oldfraction
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('oldgrade', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'slot');
+
+ // Launch rename field newfraction
+ $dbman->rename_field($table, $field, 'oldfraction');
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010040605, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010040606) {
+
+ // Changing precision of field newfraction on table quiz_question_regrade to (12, 7)
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('newfraction', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'slot');
+
+ // Launch change of precision for field newfraction
+ $dbman->change_field_precision($table, $field);
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010040606, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010040607) {
+
+ // Changing precision of field oldfraction on table quiz_question_regrade to (12, 7)
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('oldfraction', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'slot');
+
+ // Launch change of precision for field newfraction
+ $dbman->change_field_precision($table, $field);
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010040607, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010082700) {
+
+ // Changing nullability of field newfraction on table quiz_question_regrade to null
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('newfraction', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'slot');
+
+ // Launch change of nullability for field newfraction
+ $dbman->change_field_notnull($table, $field);
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010082700, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2010082701) {
+
+ // Changing nullability of field oldfraction on table quiz_question_regrade to null
+ $table = new xmldb_table('quiz_question_regrade');
+ $field = new xmldb_field('oldfraction', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'slot');
+
+ // Launch change of nullability for field newfraction
+ $dbman->change_field_notnull($table, $field);
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2010082701, 'quiz', 'overview');
+ }
+
+ if ($oldversion < 2011021600) {
+
+ // Define table quiz_question_regrade to be renamed to quiz_overview_regrades
+ // so that it follows the Moodle coding guidelines.
+ $table = new xmldb_table('quiz_question_regrade');
+
+ // Launch rename table for quiz_question_regrade
+ $dbman->rename_table($table, 'quiz_overview_regrades');
+
+ // overview savepoint reached
+ upgrade_plugin_savepoint(true, 2011021600, 'quiz', 'overview');
}
return true;
}
-
-
diff --git a/mod/quiz/report/overview/lang/en/quiz_overview.php b/mod/quiz/report/overview/lang/en/quiz_overview.php
index acb9bd9b358..21a90ebf4e8 100644
--- a/mod/quiz/report/overview/lang/en/quiz_overview.php
+++ b/mod/quiz/report/overview/lang/en/quiz_overview.php
@@ -1,5 +1,4 @@
groupna
$string['regradeallgroup'] = 'Full regrade for group \'{$a->groupname}\'';
$string['regradeheader'] = 'Regrading';
$string['regradeselected'] = 'Regrade selected attempts';
-$string['requiresgrading'] = 'Requires grading';
-$string['show'] = 'Include';
-$string['showattempts'] = 'Include attempts';
-$string['showdetailedmarks'] = 'Marks for each question';
+$string['show'] = 'Show / download';
+$string['showattempts'] = 'Only show / download attempts';
+$string['showdetailedmarks'] = 'Show / download marks for each question';
$string['showinggraded'] = 'Showing only the attempt graded for each user.';
$string['showinggradedandungraded'] = 'Showing graded and ungraded attempts for each user. The one attempt for each user that is graded is highlighted. The grading method for this quiz is {$a}.';
$string['studentingroup'] = '\'{$a->coursestudent}\' in group \'{$a->groupname}\'';
diff --git a/mod/quiz/report/overview/overview_table.php b/mod/quiz/report/overview/overview_table.php
index 3dabcf5898d..0491ef9a3a8 100644
--- a/mod/quiz/report/overview/overview_table.php
+++ b/mod/quiz/report/overview/overview_table.php
@@ -1,172 +1,194 @@
.
-class quiz_report_overview_table extends table_sql {
+/**
+ * This file defines the quiz grades table.
+ *
+ * @package quiz
+ * @subpackage overview
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
- var $useridfield = 'userid';
- var $candelete;
- var $reporturl;
- var $displayoptions;
- var $regradedqs = array();
+defined('MOODLE_INTERNAL') || die();
- function quiz_report_overview_table($quiz , $qmsubselect, $groupstudents,
- $students, $detailedmarks, $questions, $candelete, $reporturl, $displayoptions, $context){
- parent::__construct('mod-quiz-report-overview-report');
- $this->quiz = $quiz;
- $this->qmsubselect = $qmsubselect;
- $this->groupstudents = $groupstudents;
- $this->students = $students;
+
+/**
+ * This is a table subclass for displaying the quiz grades report.
+ *
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_report_overview_table extends quiz_attempt_report_table {
+
+ protected $candelete;
+ protected $regradedqs = array();
+
+ public function __construct($quiz, $context, $qmsubselect, $groupstudents,
+ $students, $detailedmarks, $questions, $candelete, $reporturl, $displayoptions) {
+ parent::__construct('mod-quiz-report-overview-report', $quiz , $context,
+ $qmsubselect, $groupstudents, $students, $questions, $candelete,
+ $reporturl, $displayoptions);
$this->detailedmarks = $detailedmarks;
- $this->questions = $questions;
- $this->candelete = $candelete;
- $this->reporturl = $reporturl;
- $this->displayoptions = $displayoptions;
- $this->context = $context;
}
- function build_table(){
- global $CFG, $DB;
+
+ public function build_table() {
+ global $DB;
+
if ($this->rawdata) {
- // Define some things we need later to process raw data from db.
$this->strtimeformat = str_replace(',', '', get_string('strftimedatetime'));
parent::build_table();
+
//end of adding data from attempts data to table / download
//now add averages at bottom of table :
$params = array($this->quiz->id);
- $averagesql = "SELECT AVG(qg.grade) AS grade " .
- "FROM {quiz_grades} qg " .
- "WHERE quiz=?";
+ $averagesql = '
+ SELECT AVG(qg.grade) AS grade, COUNT(qg.grade) AS numaveraged
+ FROM {quiz_grades} qg
+ WHERE quiz = ?';
$this->add_separator();
- if ($this->is_downloading()){
+ if ($this->is_downloading()) {
$namekey = 'lastname';
} else {
$namekey = 'fullname';
}
- if ($this->groupstudents){
- list($g_usql, $g_params) = $DB->get_in_or_equal($this->groupstudents);
-
- $groupaveragesql = $averagesql." AND qg.userid $g_usql";
- $groupaverage = $DB->get_record_sql($groupaveragesql, array_merge($params, $g_params));
- $groupaveragerow = array($namekey => get_string('groupavg', 'grades'),
- 'sumgrades' => quiz_format_grade($this->quiz, $groupaverage->grade),
- 'feedbacktext'=> strip_tags(quiz_report_feedback_for_grade($groupaverage->grade, $this->quiz->id, $this->context)));
- if($this->detailedmarks && ($this->qmsubselect || $this->quiz->attempts == 1)) {
- $avggradebyq = quiz_get_average_grade_for_questions($this->quiz, $this->groupstudents);
- $groupaveragerow += quiz_format_average_grade_for_questions($avggradebyq, $this->questions, $this->quiz, $this->is_downloading());
+ if ($this->groupstudents) {
+ list($usql, $uparams) = $DB->get_in_or_equal($this->groupstudents);
+ $record = $DB->get_record_sql($averagesql . ' AND qg.userid ' . $usql,
+ array_merge($params, $uparams));
+ $groupaveragerow = array(
+ $namekey => get_string('groupavg', 'grades'),
+ 'sumgrades' => $this->format_average($record),
+ 'feedbacktext'=> strip_tags(quiz_report_feedback_for_grade(
+ $record->grade, $this->quiz->id)));
+ if ($this->detailedmarks && ($this->quiz->attempts == 1 || $this->qmsubselect)) {
+ $avggradebyq = $this->load_average_question_grades($this->groupstudents);
+ $groupaveragerow += $this->format_average_grade_for_questions($avggradebyq);
}
$this->add_data_keyed($groupaveragerow);
}
if ($this->students) {
- list($s_usql, $s_params) = $DB->get_in_or_equal($this->students);
- $overallaverage = $DB->get_record_sql($averagesql." AND qg.userid $s_usql", array_merge($params, $s_params));
- $overallaveragerow = array($namekey => get_string('overallaverage', 'grades'),
- 'sumgrades' => quiz_format_grade($this->quiz, $overallaverage->grade),
- 'feedbacktext'=> strip_tags(quiz_report_feedback_for_grade($overallaverage->grade, $this->quiz->id, $this->context)));
- if($this->detailedmarks && ($this->qmsubselect || $this->quiz->attempts == 1)) {
- $avggradebyq = quiz_get_average_grade_for_questions($this->quiz, $this->students);
- $overallaveragerow += quiz_format_average_grade_for_questions($avggradebyq, $this->questions, $this->quiz, $this->is_downloading());
+ list($usql, $uparams) = $DB->get_in_or_equal($this->students);
+ $record = $DB->get_record_sql($averagesql . ' AND qg.userid ' . $usql,
+ array_merge($params, $uparams));
+ $overallaveragerow = array(
+ $namekey => get_string('overallaverage', 'grades'),
+ 'sumgrades' => $this->format_average($record),
+ 'feedbacktext'=> strip_tags(quiz_report_feedback_for_grade(
+ $record->grade, $this->quiz->id, $this->context)));
+ if ($this->detailedmarks && ($this->quiz->attempts == 1 || $this->qmsubselect)) {
+ $avggradebyq = $this->load_average_question_grades($this->students);
+ $overallaveragerow += $this->format_average_grade_for_questions($avggradebyq);
}
$this->add_data_keyed($overallaveragerow);
}
}
}
- function wrap_html_start(){
- if (!$this->is_downloading()) {
- if ($this->candelete) {
- // Start form
- $url = new moodle_url($this->reporturl, $this->displayoptions);
- echo '';
+ protected function format_average_grade_for_questions($gradeaverages) {
+ $row = array();
+ if (!$gradeaverages) {
+ $gradeaverages = array();
+ }
+ foreach ($this->questions as $question) {
+ if (isset($gradeaverages[$question->slot]) && $question->maxmark > 0) {
+ $record = $gradeaverages[$question->slot];
+ $record->grade = quiz_rescale_grade(
+ $record->averagefraction * $question->maxmark, $this->quiz, false);
+ } else {
+ $record = new stdClass();
+ $record->grade = null;
+ $record->numaveraged = null;
}
+ $row['qsgrade' . $question->slot] = $this->format_average($record, true);
}
+ return $row;
}
-
- function col_checkbox($attempt){
- if ($attempt->attempt){
- return '';
+ /**
+ * Format an entry in an average row.
+ * @param object $record with fields grade and numaveraged
+ */
+ protected function format_average($record, $question = false) {
+ if (is_null($record->grade)) {
+ $average = '-';
+ } else if ($question) {
+ $average = quiz_format_question_grade($this->quiz, $record->grade);
} else {
- return '';
- }
- }
-
- function col_picture($attempt){
- global $COURSE, $OUTPUT;
- $user = new stdClass();
- $user->id = $attempt->userid;
- $user->lastname = $attempt->lastname;
- $user->firstname = $attempt->firstname;
- $user->imagealt = $attempt->imagealt;
- $user->picture = $attempt->picture;
- $user->email = $attempt->email;
- return $OUTPUT->user_picture($user);
- }
-
- function col_fullname($attempt){
- $html = parent::col_fullname($attempt);
- if ($this->is_downloading()) {
- return $html;
+ $average = quiz_format_grade($this->quiz, $record->grade);
}
- return $html . '
'.get_string('reviewattempt', 'quiz').'';
- }
-
- function col_timestart($attempt){
- if ($attempt->attempt) {
- return userdate($attempt->timestart, $this->strtimeformat);
+ if ($this->download) {
+ return $average;
+ } else if (is_null($record->numaveraged)) {
+ return html_writer::tag('span', html_writer::tag('span',
+ $average, array('class' => 'average')), array('class' => 'avgcell'));
} else {
- return '-';
- }
- }
- function col_timefinish($attempt){
- if ($attempt->attempt && $attempt->timefinish) {
- return userdate($attempt->timefinish, $this->strtimeformat);
- } else {
- return '-';
+ return html_writer::tag('span', html_writer::tag('span',
+ $average, array('class' => 'average')) . ' ' . html_writer::tag('span',
+ '(' . $record->numaveraged . ')', array('class' => 'count')),
+ array('class' => 'avgcell'));
}
}
- function col_duration($attempt){
- if ($attempt->timefinish) {
- return format_time($attempt->timefinish - $attempt->timestart);
- } elseif ($attempt->timestart) {
- return get_string('unfinished', 'quiz');
- } else {
- return '-';
+ public function wrap_html_start() {
+ if ($this->is_downloading() || !$this->candelete) {
+ return;
}
+
+ // Start form
+ $url = new moodle_url($this->reporturl, $this->displayoptions +
+ array('sesskey' => sesskey()));
+ echo '';
+ }
+
+ public function col_sumgrades($attempt) {
if (!$attempt->timefinish) {
return '-';
}
@@ -176,29 +198,30 @@ class quiz_report_overview_table extends table_sql {
return $grade;
}
- if (isset($this->regradedqs[$attempt->attemptuniqueid])){
+ if (isset($this->regradedqs[$attempt->usageid])) {
$newsumgrade = 0;
$oldsumgrade = 0;
- foreach ($this->questions as $question){
- if (isset($this->regradedqs[$attempt->attemptuniqueid][$question->id])){
- $newsumgrade += $this->regradedqs[$attempt->attemptuniqueid][$question->id]->newgrade;
- $oldsumgrade += $this->regradedqs[$attempt->attemptuniqueid][$question->id]->oldgrade;
+ foreach ($this->questions as $question) {
+ if (isset($this->regradedqs[$attempt->usageid][$question->slot])) {
+ $newsumgrade += $this->regradedqs[$attempt->usageid]
+ [$question->slot]->newfraction * $question->maxmark;
+ $oldsumgrade += $this->regradedqs[$attempt->usageid]
+ [$question->slot]->oldfraction * $question->maxmark;
} else {
- $newsumgrade += $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$question->id]->grade;
- $oldsumgrade += $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$question->id]->grade;
+ $newsumgrade += $this->lateststeps[$attempt->usageid]
+ [$question->slot]->fraction * $question->maxmark;
+ $oldsumgrade += $this->lateststeps[$attempt->usageid]
+ [$question->slot]->fraction * $question->maxmark;
}
}
$newsumgrade = quiz_rescale_grade($newsumgrade, $this->quiz);
$oldsumgrade = quiz_rescale_grade($oldsumgrade, $this->quiz);
- $grade = "$oldsumgrade
$newsumgrade";
+ $grade = html_writer::tag('del', $oldsumgrade) . '/' .
+ html_writer::empty_tag('br') . $newsumgrade;
}
-
- $gradehtml = ''.$grade.'';
- if ($this->qmsubselect && $attempt->gradedattempt){
- $gradehtml = ''.$gradehtml.'
';
- }
- return $gradehtml;
+ return html_writer::link(new moodle_url('/mod/quiz/review.php',
+ array('attempt' => $attempt->attempt)), $grade,
+ array('title' => get_string('reviewattempt', 'quiz')));
}
/**
@@ -208,77 +231,53 @@ class quiz_report_overview_table extends table_sql {
* and what they are called.
* @return string the contents of the cell.
*/
- function other_cols($colname, $attempt){
- global $OUTPUT;
-
- if (preg_match('/^qsgrade([0-9]+)$/', $colname, $matches)){
- $questionid = $matches[1];
- $question = $this->questions[$questionid];
- if (isset($this->gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid])){
- $stateforqinattempt = $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid];
- } else {
- $stateforqinattempt = false;
- }
- if ($stateforqinattempt && question_state_is_graded($stateforqinattempt)) {
- $grade = quiz_rescale_grade($stateforqinattempt->grade, $this->quiz, 'question');
- if (!$this->is_downloading()) {
- if (isset($this->regradedqs[$attempt->attemptuniqueid][$questionid])){
- $gradefromdb = $grade;
- $newgrade = quiz_rescale_grade($this->regradedqs[$attempt->attemptuniqueid][$questionid]->newgrade, $this->quiz, 'question');
- $oldgrade = quiz_rescale_grade($this->regradedqs[$attempt->attemptuniqueid][$questionid]->oldgrade, $this->quiz, 'question');
-
- $grade = ''.$oldgrade.'
'.
- $newgrade;
- }
-
- $link = new moodle_url("/mod/quiz/reviewquestion.php?attempt=$attempt->attempt&question=$question->id");
- $action = new popup_action('click', $link, 'reviewquestion', array('height' => 450, 'width' => 650));
- $linktopopup = $OUTPUT->action_link($link, $grade, $action, array('title'=>get_string('reviewresponsetoq', 'quiz', $question->formattedname)));
-
- if (($this->questions[$questionid]->maxgrade != 0)){
- $fractionofgrade = $stateforqinattempt->grade
- / $this->questions[$questionid]->maxgrade;
- $qclass = question_get_feedback_class($fractionofgrade);
- $feedbackimg = question_get_feedback_image($fractionofgrade);
- $questionclass = "que";
- return "".$linktopopup."$feedbackimg";
- } else {
- return $linktopopup;
- }
-
- } else {
- return $grade;
- }
- } else if ($stateforqinattempt && question_state_is_closed($stateforqinattempt)) {
- $text = get_string('requiresgrading', 'quiz_overview');
- if (!$this->is_downloading()) {
- $link = new moodle_url("/mod/quiz/reviewquestion.php?attempt=$attempt->attempt&question=$question->id");
- $action = new popup_action('click', $link, 'reviewquestion', array('height' => 450, 'width' => 650));
- return $OUTPUT->action_link($link, $text, $action, array('title'=>get_string('reviewresponsetoq', 'quiz', $question->formattedname)));
- } else {
- return $text;
- }
- } else {
- return '--';
- }
- } else {
- return NULL;
+ public function other_cols($colname, $attempt) {
+ if (!preg_match('/^qsgrade(\d+)$/', $colname, $matches)) {
+ return null;
}
- }
-
- function col_feedbacktext($attempt){
- if ($attempt->timefinish) {
- if (!$this->is_downloading()) {
- return quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz, false), $this->quiz->id, $this->context);
- } else {
- return strip_tags(quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz, false), $this->quiz->id, $this->context));
- }
- } else {
+ $slot = $matches[1];
+ $question = $this->questions[$slot];
+ if (!isset($this->lateststeps[$attempt->usageid][$slot])) {
return '-';
}
+ $stepdata = $this->lateststeps[$attempt->usageid][$slot];
+ $state = question_state::get($stepdata->state);
+
+ if ($question->maxmark == 0) {
+ $grade = '-';
+ } else if (is_null($stepdata->fraction)) {
+ if ($state == question_state::$needsgrading) {
+ $grade = get_string('requiresgrading', 'question');
+ } else {
+ $grade = '-';
+ }
+ } else {
+ $grade = quiz_rescale_grade(
+ $stepdata->fraction * $question->maxmark, $this->quiz, 'question');
+ }
+
+ if ($this->is_downloading()) {
+ return $grade;
+ }
+
+ if (isset($this->regradedqs[$attempt->usageid][$slot])) {
+ $gradefromdb = $grade;
+ $newgrade = quiz_rescale_grade(
+ $this->regradedqs[$attempt->usageid][$slot]->newfraction * $question->maxmark,
+ $this->quiz, 'question');
+ $oldgrade = quiz_rescale_grade(
+ $this->regradedqs[$attempt->usageid][$slot]->oldfraction * $question->maxmark,
+ $this->quiz, 'question');
+
+ $grade = html_writer::tag('del', $oldgrade) . '/' .
+ html_writer::empty_tag('br') . $newgrade;
+ }
+
+ return $this->make_review_link($grade, $attempt, $slot);
}
- function col_regraded($attempt){
+
+ public function col_regraded($attempt) {
if ($attempt->regraded == '') {
return '';
} else if ($attempt->regraded == 0) {
@@ -287,59 +286,64 @@ class quiz_report_overview_table extends table_sql {
return get_string('done', 'quiz_overview');
}
}
- function query_db($pagesize, $useinitialsbar=true){
- // Add table joins so we can sort by question grade
- // unfortunately can't join all tables necessary to fetch all grades
- // to get the state for one question per attempt row we must join two tables
- // and there is a limit to how many joins you can have in one query. In MySQL it
- // is 61. This means that when having more than 29 questions the query will fail.
- // So we join just the tables needed to sort the attempts.
- if($sort = $this->get_sql_sort()) {
- if ($this->detailedmarks) {
- $this->sql->from .= ' ';
- $sortparts = explode(',', $sort);
- $matches = array();
- foreach($sortparts as $sortpart) {
- $sortpart = trim($sortpart);
- if (preg_match('/^qsgrade([0-9]+)/', $sortpart, $matches)){
- $qid = intval($matches[1]);
- $this->sql->fields .= ", qs$qid.grade AS qsgrade$qid, qs$qid.event AS qsevent$qid, qs$qid.id AS qsid$qid";
- $this->sql->from .= "LEFT JOIN {question_sessions} qns$qid ON qns$qid.attemptid = qa.uniqueid AND qns$qid.questionid = :qid$qid ";
- $this->sql->from .= "LEFT JOIN {question_states} qs$qid ON qs$qid.id = qns$qid.newgraded ";
- $this->sql->params['qid'.$qid] = $qid;
- }
- }
- } else {
- //unset any sort columns that sort on question grade as the
- //grades are not being fetched as fields
- $sess = &$this->sess;
- foreach($sess->sortby as $column => $order) {
- if (preg_match('/^qsgrade([0-9]+)/', trim($column))){
- unset($sess->sortby[$column]);
- }
- }
- }
+
+ protected function requires_latest_steps_loaded() {
+ return $this->detailedmarks;
+ }
+
+ protected function is_latest_step_column($column) {
+ if (preg_match('/^qsgrade([0-9]+)/', $column, $matches)) {
+ return $matches[1];
}
+ return false;
+ }
+
+ protected function get_required_latest_state_fields($slot, $alias) {
+ return "$alias.fraction * $alias.maxmark AS qsgrade$slot";
+ }
+
+ public function query_db($pagesize, $useinitialsbar = true) {
parent::query_db($pagesize, $useinitialsbar);
- //get all the attempt ids we want to display on this page
- //or to export for download.
- if (!$this->is_downloading()) {
- $attemptids = array();
- foreach ($this->rawdata as $attempt){
- if ($attempt->attemptuniqueid > 0){
- $attemptids[] = $attempt->attemptuniqueid;
- }
- }
- $this->gradedstatesbyattempt = quiz_get_newgraded_states($attemptids, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt');
- if (has_capability('mod/quiz:regrade', $this->context)){
- $this->regradedqs = quiz_get_regraded_qs($attemptids);
- }
- } else {
- $this->gradedstatesbyattempt = quiz_get_newgraded_states($this->sql, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt');
- if (has_capability('mod/quiz:regrade', $this->context)){
- $this->regradedqs = quiz_get_regraded_qs($this->sql);
- }
+
+ if ($this->detailedmarks && has_capability('mod/quiz:regrade', $this->context)) {
+ $this->regradedqs = $this->get_regraded_questions();
}
}
-}
+ /**
+ * Load the average grade for each question, averaged over particular users.
+ * @param array $userids the user ids to average over.
+ */
+ protected function load_average_question_grades($userids) {
+ global $DB;
+
+ $qmfilter = '';
+ if ($this->quiz->attempts != 1) {
+ $qmfilter = '(' . quiz_report_qm_filter_select($this->quiz, 'quiza') . ') AND ';
+ }
+
+ list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
+ $params['quizid'] = $this->quiz->id;
+ $qubaids = new qubaid_join(
+ '{quiz_attempts} quiza',
+ 'quiza.uniqueid',
+ "quiza.userid $usql AND quiza.quiz = :quizid",
+ $params);
+
+ $dm = new question_engine_data_mapper();
+ return $dm->load_average_marks($qubaids, array_keys($this->questions));
+ }
+
+ /**
+ * Get all the questions in all the attempts being displayed that need regrading.
+ * @return array A two dimensional array $questionusageid => $slot => $regradeinfo.
+ */
+ protected function get_regraded_questions() {
+ global $DB;
+
+ $qubaids = $this->get_qubaids_condition();
+ $regradedqs = $DB->get_records_select('quiz_overview_regrades',
+ 'questionusageid ' . $qubaids->usage_id_in(), $qubaids->usage_id_in_params());
+ return quiz_report_index_by_keys($regradedqs, array('questionusageid', 'slot'));
+ }
+}
diff --git a/mod/quiz/report/overview/overviewgraph.php b/mod/quiz/report/overview/overviewgraph.php
index 9fb13b66abb..4efa315014b 100644
--- a/mod/quiz/report/overview/overviewgraph.php
+++ b/mod/quiz/report/overview/overviewgraph.php
@@ -1,36 +1,65 @@
dirroot."/lib/graphlib.php";
-include $CFG->dirroot."/mod/quiz/report/reportlib.php";
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * This file renders the quiz overview graph.
+ *
+ * @package quiz
+ * @subpackage overview
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+require_once(dirname(__FILE__) . '/../../../../config.php');
+require_once($CFG->libdir . '/graphlib.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
+
$quizid = required_param('id', PARAM_INT);
$groupid = optional_param('groupid', 0, PARAM_INT);
$quiz = $DB->get_record('quiz', array('id' => $quizid));
$course = $DB->get_record('course', array('id' => $quiz->course));
$cm = get_coursemodule_from_instance('quiz', $quizid);
-require_login($course, true, $cm);
+
+require_login($course, false, $cm);
$modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
-if ($groupid && $groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
- $groups = groups_get_activity_allowed_groups($cm);
- if (array_key_exists($groupid, $groups)){
- $group = $groups[$groupid];
- if (!$groupusers = get_users_by_capability($modcontext, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'','','','',$group->id,'',false)){
- print_error('nostudentsingroup');
- } else {
- $groupusers = array_keys($groupusers);
- }
- } else {
- print_error('errorinvalidgroup', 'group', null, $groupid);
- }
-} else {
- $groups = false;
- $group = false;
- $groupusers = array();
-}
require_capability('mod/quiz:viewreports', $modcontext);
-$line = new graph(800,600);
-$line->parameter['title'] = '';
+if ($groupid && $groupmode = groups_get_activity_groupmode($cm)) {
+ // Groups are being used
+ $groups = groups_get_activity_allowed_groups($cm);
+ if (!array_key_exists($groupid, $groups)) {
+ print_error('errorinvalidgroup', 'group', null, $groupid);
+ }
+ $group = $groups[$groupid];
+ $groupusers = get_users_by_capability($modcontext,
+ array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
+ '', '', '', '', $group->id, '', false);
+ if (!$groupusers) {
+ print_error('nostudentsingroup');
+ }
+ $groupusers = array_keys($groupusers);
+} else {
+ $groupusers = array();
+}
+
+$line = new graph(800, 600);
+$line->parameter['title'] = '';
$line->parameter['y_label_left'] = get_string('participants');
$line->parameter['x_label'] = get_string('grade');
$line->parameter['y_label_angle'] = 90;
@@ -41,59 +70,59 @@ $line->parameter['x_axis_angle'] = 60;
$line->y_tick_labels = null;
$line->offset_relation = null;
-$line->parameter['bar_size'] = 1; // will make size > 1 to get overlap effect when showing groups
-$line->parameter['bar_spacing'] = 10; // don't forget to increase spacing so that graph doesn't become one big block of colour
+// will make size > 1 to get overlap effect when showing groups
+$line->parameter['bar_size'] = 1;
+// don't forget to increase spacing so that graph doesn't become one big block of colour
+$line->parameter['bar_spacing'] = 10;
//pick a sensible number of bands depending on quiz maximum grade.
$bands = $quiz->grade;
-while ($bands > 20 || $bands <= 10){
- if ($bands > 50){
- $bands = $bands /5;
+while ($bands > 20 || $bands <= 10) {
+ if ($bands > 50) {
+ $bands /= 5;
} else if ($bands > 20) {
- $bands = $bands /2;
+ $bands /= 2;
}
- if ($bands < 4){
- $bands = $bands * 5;
- } else if ($bands <= 10){
- $bands = $bands * 2;
+ if ($bands < 4) {
+ $bands *= 5;
+ } else if ($bands <= 10) {
+ $bands *= 2;
}
}
-$bandwidth = $quiz->grade / $bands;
$bands = ceil($bands);
+$bandwidth = $quiz->grade / $bands;
$bandlabels = array();
-for ($i=0;$i < $quiz->grade;$i += $bandwidth){
- $label = quiz_format_grade($quiz, $i).' - ';
- if ($quiz->grade > $i+$bandwidth){
- $label .= quiz_format_grade($quiz, $i+$bandwidth);
- } else {
- $label .= quiz_format_grade($quiz, $quiz->grade);
- }
- $bandlabels[] = $label;
+for ($i = 1; $i <= $bands; $i++) {
+ $bandlabels[] = quiz_format_grade($quiz, ($i - 1) * $bandwidth) . ' - ' .
+ quiz_format_grade($quiz, $i * $bandwidth);
}
-$line->x_data = $bandlabels;
+$line->x_data = $bandlabels;
-$line->y_format['allusers'] =
- array('colour' => 'red', 'bar' => 'fill', 'shadow_offset' => 1, 'legend' => get_string('allparticipants'));
+$line->y_format['allusers'] = array(
+ 'colour' => 'red',
+ 'bar' => 'fill',
+ 'shadow_offset' => 1,
+ 'legend' => get_string('allparticipants')
+);
$line->y_data['allusers'] = quiz_report_grade_bands($bandwidth, $bands, $quizid, $groupusers);
$line->y_order = array('allusers');
+$ymax = max($line->y_data['allusers']);
$line->parameter['y_min_left'] = 0; // start at 0
-$line->parameter['y_max_left'] = max($line->y_data['allusers']);
+$line->parameter['y_max_left'] = $ymax;
$line->parameter['y_decimal_left'] = 0; // 2 decimal places for y axis.
-
//pick a sensible number of gridlines depending on max value on graph.
-$gridlines = max($line->y_data['allusers']);
-while ($gridlines >= 10){
- if ($gridlines >= 50){
- $gridlines = $gridlines /5;
+$gridlines = $ymax;
+while ($gridlines >= 10) {
+ if ($gridlines >= 50) {
+ $gridlines /= 5;
} else {
- $gridlines = $gridlines /2;
+ $gridlines /= 2;
}
}
-$line->parameter['y_axis_gridlines'] = $gridlines+1;
+$line->parameter['y_axis_gridlines'] = $gridlines + 1;
$line->draw();
-
diff --git a/mod/quiz/report/overview/overviewsettings_form.php b/mod/quiz/report/overview/overviewsettings_form.php
index b1630818ac7..1de70d039c6 100644
--- a/mod/quiz/report/overview/overviewsettings_form.php
+++ b/mod/quiz/report/overview/overviewsettings_form.php
@@ -1,57 +1,104 @@
libdir/formslib.php";
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * This file defines the setting form for the quiz overview report.
+ *
+ * @package quiz
+ * @subpackage overview
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+
+/**
+ * Quiz overview report settings form.
+ *
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class mod_quiz_report_overview_settings extends moodleform {
- function definition() {
- global $COURSE;
- $mform =& $this->_form;
-//-------------------------------------------------------------------------------
- $mform->addElement('header', 'preferencespage', get_string('preferencespage', 'quiz_overview'));
+ protected function definition() {
+ $mform = $this->_form;
- if (!$this->_customdata['currentgroup']){
+ $mform->addElement('header', 'preferencespage',
+ get_string('preferencespage', 'quiz_overview'));
+
+ if (!$this->_customdata['currentgroup']) {
$studentsstring = get_string('participants');
} else {
$a = new stdClass();
$a->coursestudent = get_string('participants');
$a->groupname = groups_get_group_name($this->_customdata['currentgroup']);
- if (20 < strlen($a->groupname)){
- $studentsstring = get_string('studentingrouplong', 'quiz_overview', $a);
+ if (20 < strlen($a->groupname)) {
+ $studentsstring = get_string('studentingrouplong', 'quiz_overview', $a);
} else {
- $studentsstring = get_string('studentingroup', 'quiz_overview', $a);
+ $studentsstring = get_string('studentingroup', 'quiz_overview', $a);
}
}
$options = array();
- if (!$this->_customdata['currentgroup']){
- $options[QUIZ_REPORT_ATTEMPTS_ALL] = get_string('optallattempts','quiz_overview');
+ if (!$this->_customdata['currentgroup']) {
+ $options[QUIZ_REPORT_ATTEMPTS_ALL] = get_string('optallattempts', 'quiz_overview');
}
- if ($this->_customdata['currentgroup'] || $COURSE->id != SITEID) {
- $options[QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS] = get_string('optallstudents','quiz_overview', $studentsstring);
+ if ($this->_customdata['currentgroup'] ||
+ !is_inside_frontpage($this->_customdata['context'])) {
+ $options[QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS] =
+ get_string('optallstudents', 'quiz_overview', $studentsstring);
$options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH] =
- get_string('optattemptsonly','quiz_overview', $studentsstring);
- $options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO] = get_string('optnoattemptsonly', 'quiz_overview', $studentsstring);
+ get_string('optattemptsonly', 'quiz_overview', $studentsstring);
+ $options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO] =
+ get_string('optnoattemptsonly', 'quiz_overview', $studentsstring);
}
$mform->addElement('select', 'attemptsmode', get_string('show', 'quiz_overview'), $options);
$showattemptsgrp = array();
- if ($this->_customdata['qmsubselect']){
- $gm = ''.quiz_get_grading_option_name($this->_customdata['quiz']->grademethod).'';
- $showattemptsgrp[] =& $mform->createElement('advcheckbox', 'qmfilter', get_string('showattempts', 'quiz_overview'), get_string('optonlygradedattempts', 'quiz_overview', $gm), null, array(0,1));
+ if ($this->_customdata['qmsubselect']) {
+ $gm = '' .
+ quiz_get_grading_option_name($this->_customdata['quiz']->grademethod) .
+ '';
+ $showattemptsgrp[] = $mform->createElement('advcheckbox', 'qmfilter',
+ get_string('showattempts', 'quiz_overview'),
+ get_string('optonlygradedattempts', 'quiz_overview', $gm), null, array(0, 1));
}
- if (has_capability('mod/quiz:regrade', $this->_customdata['context'])){
- $showattemptsgrp[] =& $mform->createElement('advcheckbox', 'regradefilter', get_string('showattempts', 'quiz_overview'), get_string('optonlyregradedattempts', 'quiz_overview'), null, array(0,1));
+ if (has_capability('mod/quiz:regrade', $this->_customdata['context'])) {
+ $showattemptsgrp[] = $mform->createElement('advcheckbox', 'regradefilter',
+ get_string('showattempts', 'quiz_overview'),
+ get_string('optonlyregradedattempts', 'quiz_overview'), null, array(0, 1));
}
- if ($showattemptsgrp){
- $mform->addGroup($showattemptsgrp, null, get_string('showattempts', 'quiz_overview'), '
', false);
+ if ($showattemptsgrp) {
+ $mform->addGroup($showattemptsgrp, null,
+ get_string('showattempts', 'quiz_overview'), '
', false);
}
-//-------------------------------------------------------------------------------
- $mform->addElement('header', 'preferencesuser', get_string('preferencesuser', 'quiz_overview'));
+
+ $mform->addElement('header', 'preferencesuser',
+ get_string('preferencesuser', 'quiz_overview'));
$mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz_overview'));
$mform->setType('pagesize', PARAM_INT);
- $mform->addElement('selectyesno', 'detailedmarks', get_string('showdetailedmarks', 'quiz_overview'));
+ $mform->addElement('selectyesno', 'detailedmarks',
+ get_string('showdetailedmarks', 'quiz_overview'));
- $mform->addElement('submit', 'submitbutton', get_string('preferencessave', 'quiz_overview'));
+ $mform->addElement('submit', 'submitbutton',
+ get_string('preferencessave', 'quiz_overview'));
}
}
-
diff --git a/mod/quiz/report/overview/report.php b/mod/quiz/report/overview/report.php
index 386c052d33e..c36f4a5966b 100644
--- a/mod/quiz/report/overview/report.php
+++ b/mod/quiz/report/overview/report.php
@@ -1,58 +1,53 @@
.
+
/**
- * This script lists student attempts
+ * This file defines the quiz overview report class.
*
- * @author Martin Dougiamas, Tim Hunt and others.
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package quiz
+ * @subpackage overview
+ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-require_once($CFG->libdir.'/tablelib.php');
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/mod/quiz/report/attemptsreport.php');
require_once($CFG->dirroot.'/mod/quiz/report/overview/overviewsettings_form.php');
require_once($CFG->dirroot.'/mod/quiz/report/overview/overview_table.php');
-class quiz_overview_report extends quiz_default_report {
- /**
- * Display the report.
- */
- function display($quiz, $cm, $course) {
+/**
+ * Quiz report subclass for the overview (grades) report.
+ *
+ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_overview_report extends quiz_attempt_report {
+
+ public function display($quiz, $cm, $course) {
global $CFG, $COURSE, $DB, $OUTPUT;
$this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
- // Work out some display options - whether there is feedback, and whether scores should be shown.
- $hasfeedback = quiz_has_feedback($quiz);
- $fakeattempt = new stdClass();
- $fakeattempt->preview = false;
- $fakeattempt->timefinish = $quiz->timeopen;
- $fakeattempt->userid = 0;
- $reviewoptions = quiz_get_reviewoptions($quiz, $fakeattempt, $this->context);
- $showgrades = quiz_has_grades($quiz) && $reviewoptions->scores;
-
$download = optional_param('download', '', PARAM_ALPHA);
- /// find out current groups mode
- $currentgroup = groups_get_activity_group($cm, true);
- if (!$students = get_users_by_capability($this->context, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'u.id,1','','','','','',false)) {
- $students = array();
- } else {
- $students = array_keys($students);
- }
-
- if (empty($currentgroup)) {
- // all users who can attempt quizzes
- $allowed = $students;
- $groupstudents = array();
- } else {
- // all users who can attempt quizzes and who are in the currently selected group
- if (!$groupstudents = get_users_by_capability($this->context, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'u.id,1','','','',$currentgroup,'',false)) {
- $groupstudents = array();
- } else {
- $groupstudents = array_keys($groupstudents);
- }
- $allowed = $groupstudents;
- }
+ list($currentgroup, $students, $groupstudents, $allowed) =
+ $this->load_relevant_students($cm);
$pageoptions = array();
$pageoptions['id'] = $cm->id;
@@ -61,26 +56,26 @@ class quiz_overview_report extends quiz_default_report {
$reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
$qmsubselect = quiz_report_qm_filter_select($quiz);
- $mform = new mod_quiz_report_overview_settings($reporturl, array('qmsubselect'=> $qmsubselect, 'quiz'=>$quiz,
- 'currentgroup'=>$currentgroup, 'context'=>$this->context));
+ $mform = new mod_quiz_report_overview_settings($reporturl,
+ array('qmsubselect' => $qmsubselect, 'quiz' => $quiz,
+ 'currentgroup' => $currentgroup, 'context' => $this->context));
+
if ($fromform = $mform->get_data()) {
$regradeall = false;
$regradealldry = false;
$regradealldrydo = false;
$attemptsmode = $fromform->attemptsmode;
if ($qmsubselect) {
- //control is not on the form if
- //the grading method is not set
- //to grade one attempt per user eg. for average attempt grade.
$qmfilter = $fromform->qmfilter;
} else {
$qmfilter = 0;
}
- $regradefilter = $fromform->regradefilter;
+ $regradefilter = !empty($fromform->regradefilter);
set_user_preference('quiz_report_overview_detailedmarks', $fromform->detailedmarks);
set_user_preference('quiz_report_pagesize', $fromform->pagesize);
$detailedmarks = $fromform->detailedmarks;
$pagesize = $fromform->pagesize;
+
} else {
$regradeall = optional_param('regradeall', 0, PARAM_BOOL);
$regradealldry = optional_param('regradealldry', 0, PARAM_BOOL);
@@ -92,82 +87,91 @@ class quiz_overview_report extends quiz_default_report {
$qmfilter = 0;
}
$regradefilter = optional_param('regradefilter', 0, PARAM_INT);
-
$detailedmarks = get_user_preferences('quiz_report_overview_detailedmarks', 1);
$pagesize = get_user_preferences('quiz_report_pagesize', 0);
}
- if ($currentgroup) {
- //default for when a group is selected
- if ($attemptsmode === null || $attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) {
- $attemptsmode = QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH;
- }
- } else if (!$currentgroup && $course->id == SITEID) {
- //force report on front page to show all, unless a group is selected.
- $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
- } else if ($attemptsmode === null) {
- //default
- $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
- }
- if (!$reviewoptions->scores) {
- $detailedmarks = 0;
- }
- if ($pagesize < 1) {
- $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE;
- }
- // We only want to show the checkbox to delete attempts
- // if the user has permissions and if the report mode is showing attempts.
- $candelete = has_capability('mod/quiz:deleteattempts', $this->context)
- && ($attemptsmode != QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
+ $this->validate_common_options($attemptsmode, $pagesize, $course, $currentgroup);
$displayoptions = array();
$displayoptions['attemptsmode'] = $attemptsmode;
$displayoptions['qmfilter'] = $qmfilter;
$displayoptions['regradefilter'] = $regradefilter;
+ $mform->set_data($displayoptions +
+ array('detailedmarks' => $detailedmarks, 'pagesize' => $pagesize));
+
+ if (!$this->should_show_grades($quiz)) {
+ $detailedmarks = 0;
+ }
+
+ // We only want to show the checkbox to delete attempts
+ // if the user has permissions and if the report mode is showing attempts.
+ $candelete = has_capability('mod/quiz:deleteattempts', $this->context)
+ && ($attemptsmode != QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
+
if ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) {
+ // This option is only available to users who can access all groups in
+ // groups mode, so setting allowed to empty (which means all quiz attempts
+ // are accessible, is not a security porblem.
$allowed = array();
}
+ // Load the required questions.
+ if ($detailedmarks) {
+ $questions = quiz_report_get_significant_questions($quiz);
+ } else {
+ $questions = array();
+ }
+
+ $table = new quiz_report_overview_table($quiz, $this->context, $qmsubselect,
+ $groupstudents, $students, $detailedmarks, $questions, $candelete,
+ $reporturl, $displayoptions);
+ $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
+ $course->shortname, $quiz->name);
+ $table->is_downloading($download, $filename,
+ $COURSE->shortname . ' ' . format_string($quiz->name, true));
+ if ($table->is_downloading()) {
+ raise_memory_limit(MEMORY_EXTRA);
+ }
+
+ // Process actions.
if (empty($currentgroup) || $groupstudents) {
if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) {
if ($attemptids = optional_param('attemptid', array(), PARAM_INT)) {
require_capability('mod/quiz:deleteattempts', $this->context);
- $this->delete_selected_attempts($quiz, $cm, $attemptids, $allowed, $groupstudents);
+ $this->delete_selected_attempts($quiz, $cm, $attemptids, $allowed);
redirect($reporturl->out(false, $displayoptions));
}
+
} else if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
if ($attemptids = optional_param('attemptid', array(), PARAM_INT)) {
- $this->regrade_selected_attempts($quiz, $attemptids, $groupstudents);
+ require_capability('mod/quiz:regrade', $this->context);
+ $this->regrade_attempts($quiz, false, $groupstudents, $attemptids);
redirect($reporturl->out(false, $displayoptions));
}
}
}
- //work out the sql for this table.
- if ($detailedmarks) {
- $questions = quiz_report_load_questions($quiz);
- } else {
- $questions = array();
- }
- $table = new quiz_report_overview_table($quiz , $qmsubselect, $groupstudents,
- $students, $detailedmarks, $questions, $candelete, $reporturl,
- $displayoptions, $this->context);
- $table->is_downloading($download, get_string('reportoverview','quiz'),
- "$COURSE->shortname ".format_string($quiz->name,true));
- if (!$table->is_downloading()) {
- // Only print headers if not asked to download data
- $this->print_header_and_tabs($cm, $course, $quiz, "overview");
+ if ($regradeall && confirm_sesskey()) {
+ require_capability('mod/quiz:regrade', $this->context);
+ $this->regrade_attempts($quiz, false, $groupstudents);
+ redirect($reporturl->out(false, $displayoptions), '', 5);
+
+ } else if ($regradealldry && confirm_sesskey()) {
+ require_capability('mod/quiz:regrade', $this->context);
+ $this->regrade_attempts($quiz, true, $groupstudents);
+ redirect($reporturl->out(false, $displayoptions), '', 5);
+
+ } else if ($regradealldrydo && confirm_sesskey()) {
+ require_capability('mod/quiz:regrade', $this->context);
+ $this->regrade_attempts_needing_it($quiz, $groupstudents);
+ redirect($reporturl->out(false, $displayoptions), '', 5);
}
- if ($regradeall && confirm_sesskey()) {
- $this->regrade_all(false, $quiz, $groupstudents);
- } else if ($regradealldry && confirm_sesskey()) {
- $this->regrade_all(true, $quiz, $groupstudents);
- } else if ($regradealldrydo && confirm_sesskey()) {
- $this->regrade_all_needed($quiz, $groupstudents);
- }
- if ($regradeall || $regradealldry || $regradealldrydo) {
- redirect($reporturl->out(false, $displayoptions), '', 5);
+ // Start output.
+ if (!$table->is_downloading()) {
+ // Only print headers if not asked to download data
+ $this->print_header_and_tabs($cm, $course, $quiz, 'overview');
}
if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
@@ -176,510 +180,402 @@ class quiz_overview_report extends quiz_default_report {
}
}
- $nostudents = false;
- if (!$students) {
- if (!$table->is_downloading()) {
- echo $OUTPUT->notification(get_string('nostudentsyet'));
- }
- $nostudents = true;
- } else if ($currentgroup && !$groupstudents) {
- if (!$table->is_downloading()) {
- echo $OUTPUT->notification(get_string('nostudentsingroup'));
- }
- $nostudents = true;
- }
- if (!$table->is_downloading()) {
- // Print display options
- $mform->set_data($displayoptions +compact('detailedmarks', 'pagesize'));
- $mform->display();
-
- // Print information on the number of existing attempts
+ // Print information on the number of existing attempts
+ if (!$table->is_downloading()) { //do not print notices when downloading
if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
echo '' . $strattemptnum . '
';
}
}
- if (!$nostudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL)) {
+ $hasquestions = quiz_questions_in_quiz($quiz->questions);
+ if (!$table->is_downloading()) {
+ if (!$hasquestions) {
+ echo quiz_no_questions_message($quiz, $cm, $this->context);
+ } else if (!$students) {
+ echo $OUTPUT->notification(get_string('nostudentsyet'));
+ } else if ($currentgroup && !$groupstudents) {
+ echo $OUTPUT->notification(get_string('nostudentsingroup'));
+ }
+ // Print display options
+ $mform->display();
+ }
+
+ $hasstudents = $students && (!$currentgroup || $groupstudents);
+ if ($hasquestions && ($hasstudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL))) {
// Construct the SQL
- $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(qa.attempt, 0)') . ' AS uniqueid,';
+ $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') .
+ ' AS uniqueid, ';
if ($qmsubselect) {
- $fields .= "\n(CASE WHEN $qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,";
+ $fields .=
+ "(CASE " .
+ " WHEN $qmsubselect THEN 1" .
+ " ELSE 0 " .
+ "END) AS gradedattempt, ";
}
- $fields .= '
- qa.uniqueid AS attemptuniqueid,
- qa.id AS attempt,
- u.id AS userid,
- u.idnumber,
- u.firstname,
- u.lastname,
- u.picture,
- u.imagealt,
- u.email,
- qa.sumgrades,
- qa.timefinish,
- qa.timestart,
- CASE WHEN qa.timefinish = 0 THEN null
- WHEN qa.timefinish > qa.timestart THEN qa.timefinish - qa.timestart
- ELSE 0 END AS duration';
- // To explain that last bit, in MySQL, qa.timestart and qa.timefinish
- // are unsigned. Since MySQL 5.5.5, when they introduced strict mode,
- // subtracting a larger unsigned int from a smaller one gave an error.
- // Therefore, we avoid doing that. timefinish can be non-zero and less
- // than timestart when you have two load-balanced servers with very
- // badly synchronised clocks, and a student does a really quick attempt.
-
- // This part is the same for all cases - join users and quiz_attempts tables
- $from = '{user} u ';
- $from .= 'LEFT JOIN {quiz_attempts} qa ON qa.userid = u.id AND qa.quiz = :quizid';
- $params = array('quizid' => $quiz->id);
-
- if ($qmsubselect && $qmfilter) {
- $from .= ' AND '.$qmsubselect;
- }
- switch ($attemptsmode) {
- case QUIZ_REPORT_ATTEMPTS_ALL:
- // Show all attempts, including students who are no longer in the course
- $where = 'qa.id IS NOT NULL AND qa.preview = 0';
- break;
- case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH:
- // Show only students with attempts
- list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u');
- $params += $allowed_params;
- $where = "u.id $allowed_usql AND qa.preview = 0 AND qa.id IS NOT NULL";
- break;
- case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
- // Show only students without attempts
- list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u');
- $params += $allowed_params;
- $where = "u.id $allowed_usql AND qa.id IS NULL";
- break;
- case QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS:
- // Show all students with or without attempts
- list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u');
- $params += $allowed_params;
- $where = "u.id $allowed_usql AND (qa.preview = 0 OR qa.preview IS NULL)";
- break;
- }
+ list($fields, $from, $where, $params) =
+ $this->base_sql($quiz, $qmsubselect, $qmfilter, $attemptsmode, $allowed);
$table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
- $sqlobject = new stdClass();
- $sqlobject->from = $from;
- $sqlobject->where = $where;
- $sqlobject->params = $params;
- //test to see if there are any regraded attempts to be listed.
- if (quiz_get_regraded_qs($sqlobject, 0, 1)) {
- $regradedattempts = true;
- } else {
- $regradedattempts = false;
- }
- $fields .= ', COALESCE((SELECT MAX(qqr.regraded) FROM {quiz_question_regrade} qqr WHERE qqr.attemptid = qa.uniqueid),-1) AS regraded';
+ // Test to see if there are any regraded attempts to be listed.
+ $fields .= ", COALESCE((
+ SELECT MAX(qqr.regraded)
+ FROM {quiz_overview_regrades} qqr
+ WHERE qqr.questionusageid = quiza.uniqueid
+ ), -1) AS regraded";
if ($regradefilter) {
- $where .= ' AND COALESCE((SELECT MAX(qqr.regraded) FROM {quiz_question_regrade} qqr WHERE qqr.attemptid = qa.uniqueid),-1) !=\'-1\'';
+ $where .= " AND COALESCE((
+ SELECT MAX(qqr.regraded)
+ FROM {quiz_overview_regrades} qqr
+ WHERE qqr.questionusageid = quiza.uniqueid
+ ), -1) <> -1";
}
$table->set_sql($fields, $from, $where, $params);
- // Define table columns
- $columns = array();
- $headers = array();
- if (!$table->is_downloading()) { //do not print notices when downloading
- //regrade buttons
+ if (!$table->is_downloading()) {
+ // Regrade buttons
if (has_capability('mod/quiz:regrade', $this->context)) {
- $countregradeneeded = $this->count_regrade_all_needed($quiz, $groupstudents);
+ $regradesneeded = $this->count_question_attempts_needing_regrade(
+ $quiz, $groupstudents);
if ($currentgroup) {
$a= new stdClass();
$a->groupname = groups_get_group_name($currentgroup);
$a->coursestudents = get_string('participants');
- $a->countregradeneeded = $countregradeneeded;
- $regradealldrydolabel = get_string('regradealldrydogroup', 'quiz_overview', $a);
- $regradealldrylabel = get_string('regradealldrygroup', 'quiz_overview', $a);
- $regradealllabel = get_string('regradeallgroup', 'quiz_overview', $a);
+ $a->countregradeneeded = $regradesneeded;
+ $regradealldrydolabel =
+ get_string('regradealldrydogroup', 'quiz_overview', $a);
+ $regradealldrylabel =
+ get_string('regradealldrygroup', 'quiz_overview', $a);
+ $regradealllabel =
+ get_string('regradeallgroup', 'quiz_overview', $a);
} else {
- $regradealldrydolabel = get_string('regradealldrydo', 'quiz_overview', $countregradeneeded);
- $regradealldrylabel = get_string('regradealldry', 'quiz_overview');
- $regradealllabel = get_string('regradeall', 'quiz_overview');
+ $regradealldrydolabel =
+ get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
+ $regradealldrylabel =
+ get_string('regradealldry', 'quiz_overview');
+ $regradealllabel =
+ get_string('regradeall', 'quiz_overview');
}
- $displayurl = new moodle_url($reporturl, $displayoptions);
+ $displayurl = new moodle_url($reporturl,
+ $displayoptions + array('sesskey' => sesskey()));
echo '';
}
// Print information on the grading method
- if ($strattempthighlight = quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter)) {
+ if ($strattempthighlight = quiz_report_highlighting_grading_method(
+ $quiz, $qmsubselect, $qmfilter)) {
echo '' . $strattempthighlight . '
';
}
}
+ // Define table columns
+ $columns = array();
+ $headers = array();
+
if (!$table->is_downloading() && $candelete) {
- $columns[]= 'checkbox';
- $headers[]= NULL;
+ $columns[] = 'checkbox';
+ $headers[] = null;
}
- if (!$table->is_downloading() && $CFG->grade_report_showuserimage) {
- $columns[]= 'picture';
- $headers[]= '';
- }
- if (!$table->is_downloading()) {
- $columns[]= 'fullname';
- $headers[]= get_string('name');
- } else {
- $columns[]= 'lastname';
- $headers[]= get_string('lastname');
- $columns[]= 'firstname';
- $headers[]= get_string('firstname');
- }
+ $this->add_user_columns($table, $columns, $headers);
- if ($CFG->grade_report_showuseridnumber) {
- $columns[]= 'idnumber';
- $headers[]= get_string('idnumber');
- }
-
- $columns[]= 'timestart';
- $headers[]= get_string('startedon', 'quiz');
-
- $columns[]= 'timefinish';
- $headers[]= get_string('timecompleted','quiz');
-
- $columns[]= 'duration';
- $headers[]= get_string('attemptduration', 'quiz');
+ $this->add_time_columns($columns, $headers);
if ($detailedmarks) {
- foreach ($questions as $id => $question) {
+ foreach ($questions as $slot => $question) {
// Ignore questions of zero length
- $columns[] = 'qsgrade'.$id;
- $header = '#'.$question->number;
+ $columns[] = 'qsgrade' . $slot;
+ $header = get_string('qbrief', 'quiz', $question->number);
if (!$table->is_downloading()) {
- $header .='
';
+ $header .= '
';
} else {
- $header .=' ';
+ $header .= ' ';
}
- $header .='--/'.quiz_rescale_grade($question->maxgrade, $quiz, 'question');
+ $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
$headers[] = $header;
- $question->formattedname = strip_tags(format_string($question->name));
}
}
- if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) && $regradedattempts) {
+
+ if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
+ $this->has_regraded_questions($from, $where, $params)) {
$columns[] = 'regraded';
$headers[] = get_string('regrade', 'quiz_overview');
}
- if ($showgrades) {
- $columns[] = 'sumgrades';
- $headers[] = get_string('grade', 'quiz').'/'.quiz_format_grade($quiz, $quiz->grade);
- }
- if ($hasfeedback) {
- $columns[] = 'feedbacktext';
- $headers[] = get_string('feedback', 'quiz');
- }
+ $this->add_grade_columns($quiz, $columns, $headers);
- $table->define_columns($columns);
- $table->define_headers($headers);
- $table->sortable(true, 'uniqueid');
-
- // Set up the table
- $table->define_baseurl($reporturl->out(true, $displayoptions));
-
- $table->collapsible(true);
-
- $table->no_sorting('feedbacktext');
-
- $table->column_class('picture', 'picture');
- $table->column_class('lastname', 'bold');
- $table->column_class('firstname', 'bold');
- $table->column_class('fullname', 'bold');
- $table->column_class('sumgrades', 'bold');
-
- $table->set_attribute('id', 'attempts');
+ $this->set_up_table_columns(
+ $table, $columns, $headers, $reporturl, $displayoptions, false);
+ $table->set_attribute('class', 'generaltable generalbox grades');
$table->out($pagesize, true);
}
- if (!$table->is_downloading() && $showgrades) {
+
+ if (!$table->is_downloading() && $this->should_show_grades($quiz)) {
if ($currentgroup && $groupstudents) {
list($usql, $params) = $DB->get_in_or_equal($groupstudents);
$params[] = $quiz->id;
- if ($DB->record_exists_select('quiz_grades', "userid $usql AND quiz = ?", $params)) {
- $imageurl = "{$CFG->wwwroot}/mod/quiz/report/overview/overviewgraph.php?id={$quiz->id}&groupid=$currentgroup";
- $graphname = get_string('overviewreportgraphgroup', 'quiz_overview', groups_get_group_name($currentgroup));
+ if ($DB->record_exists_select('quiz_grades', "userid $usql AND quiz = ?",
+ $params)) {
+ $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
+ array('id' => $quiz->id, 'groupid' => $currentgroup));
+ $graphname = get_string('overviewreportgraphgroup', 'quiz_overview',
+ groups_get_group_name($currentgroup));
echo $OUTPUT->heading($graphname);
- echo '';
+ echo html_writer::tag('div', html_writer::empty_tag('img',
+ array('src' => $imageurl, 'alt' => $graphname)),
+ array('class' => 'graph'));
}
}
+
if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
$graphname = get_string('overviewreportgraph', 'quiz_overview');
- $imageurl = $CFG->wwwroot.'/mod/quiz/report/overview/overviewgraph.php?id='.$quiz->id;
+ $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
+ array('id' => $quiz->id));
echo $OUTPUT->heading($graphname);
- echo '';
+ echo html_writer::tag('div', html_writer::empty_tag('img',
+ array('src' => $imageurl, 'alt' => $graphname)),
+ array('class' => 'graph'));
}
}
return true;
}
+
/**
- * @param bool changedb whether to change contents of state and grades
- * tables.
+ * Regrade a particular quiz attempt. Either for real ($dryrun = false), or
+ * as a pretend regrade to see which fractions would change. The outcome is
+ * stored in the quiz_overview_regrades table.
+ *
+ * Note, $attempt is not upgraded in the database. The caller needs to do that.
+ * However, $attempt->sumgrades is updated, if this is not a dry run.
+ *
+ * @param object $attempt the quiz attempt to regrade.
+ * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
+ * @param array $slots if null, regrade all questions, otherwise, just regrade
+ * the quetsions with those slots.
*/
- function regrade_all($dry, $quiz, $groupstudents) {
- global $DB, $OUTPUT;
- if (!has_capability('mod/quiz:regrade', $this->context)) {
- echo $OUTPUT->notification(get_string('regradenotallowed', 'quiz'));
- return true;
- }
- // Fetch all attempts
- if ($groupstudents) {
- list($usql, $params) = $DB->get_in_or_equal($groupstudents);
- $select = "userid $usql AND ";
- } else {
- $select = '';
- $params = array();
- }
- $select .= "quiz = ? AND preview = 0";
- $params[] = $quiz->id;
- if (!$attempts = $DB->get_records_select('quiz_attempts', $select, $params)) {
- echo $OUTPUT->heading(get_string('noattempts', 'quiz'));
- return true;
- }
-
- $this->clear_regrade_table($quiz, $groupstudents);
-
- // Fetch all questions
- $questions = question_load_questions(explode(',',quiz_questions_in_quiz($quiz->questions)), 'qqi.grade AS maxgrade, qqi.id AS instance',
- '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question');
-
- // Print heading
- echo $OUTPUT->heading(get_string('regradingquiz', 'quiz', format_string($quiz->name)));
- $qstodo = count($questions);
- $qsdone = 0;
- if ($qstodo > 1) {
- $qpb = new progress_bar('qregradingbar', 500, true);
- $qpb->update($qsdone, $qstodo, "Question $qsdone of $qstodo");
- }
- $apb = new progress_bar('aregradingbar', 500, true);
-
- // Loop through all questions and all attempts and regrade while printing progress info
- $attemptstodo = count($attempts);
- foreach ($questions as $question) {
- $attemptsdone = 0;
- $apb->restart();
- echo ''.get_string('regradingquestion', 'quiz', $question->name).'
';
- @flush();@ob_flush();
- foreach ($attempts as $attempt) {
- set_time_limit(30);
- $changed = regrade_question_in_attempt($question, $attempt, $quiz, true, $dry);
-
- $attemptsdone++;
- $a = new stdClass();
- $a->done = $attemptsdone;
- $a->todo = $attemptstodo;
- $apb->update($attemptsdone, $attemptstodo, get_string('attemptprogress', 'quiz_overview', $a));
- }
- $qsdone++;
- if (isset($qpb)) {
- $a = new stdClass();
- $a->done = $qsdone;
- $a->todo = $qstodo;
- $qpb->update($qsdone, $qstodo, get_string('qprogress', 'quiz_overview', $a));
- }
- // the following makes sure that the output is sent immediately.
- @flush();@ob_flush();
- }
-
- if (!$dry) {
- $this->check_overall_grades($quiz, $groupstudents);
- }
- }
- function count_regrade_all_needed($quiz, $groupstudents) {
+ protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
global $DB;
- // Fetch all attempts that need regrading
- if ($groupstudents) {
- list($usql, $params) = $DB->get_in_or_equal($groupstudents);
- $where = "qa.userid $usql AND ";
- } else {
- $where = '';
- $params = array();
+
+ $transaction = $DB->start_delegated_transaction();
+
+ $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
+
+ if (is_null($slots)) {
+ $slots = $quba->get_slots();
}
- $where .= "qa.quiz = ? AND qa.preview = 0 AND qa.uniqueid = qqr.attemptid AND qqr.regraded = 0";
- $params[] = $quiz->id;
- return $DB->get_field_sql('SELECT COUNT(1) FROM {quiz_attempts} qa, {quiz_question_regrade} qqr WHERE '. $where, $params);
+
+ $finished = $attempt->timefinish > 0;
+ foreach ($slots as $slot) {
+ $qqr = new stdClass();
+ $qqr->oldfraction = $quba->get_question_fraction($slot);
+
+ $quba->regrade_question($slot, $finished);
+
+ $qqr->newfraction = $quba->get_question_fraction($slot);
+
+ if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
+ $qqr->questionusageid = $quba->get_id();
+ $qqr->slot = $slot;
+ $qqr->regraded = empty($dryrun);
+ $qqr->timemodified = time();
+ $DB->insert_record('quiz_overview_regrades', $qqr, false);
+ }
+ }
+
+ if (!$dryrun) {
+ question_engine::save_questions_usage_by_activity($quba);
+ }
+
+ $transaction->allow_commit();
}
- function regrade_all_needed($quiz, $groupstudents) {
- global $DB, $OUTPUT;
- if (!has_capability('mod/quiz:regrade', $this->context)) {
- echo $OUTPUT->notification(get_string('regradenotallowed', 'quiz'));
+
+ /**
+ * Regrade attempts for this quiz, exactly which attempts are regraded is
+ * controlled by the parameters.
+ * @param object $quiz the quiz settings.
+ * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
+ * @param array $groupstudents blank for all attempts, otherwise regrade attempts
+ * for these users.
+ * @param array $attemptids blank for all attempts, otherwise only regrade
+ * attempts whose id is in this list.
+ */
+ protected function regrade_attempts($quiz, $dryrun = false,
+ $groupstudents = array(), $attemptids = array()) {
+ global $DB;
+
+ $where = "quiz = ? AND preview = 0";
+ $params = array($quiz->id);
+
+ if ($groupstudents) {
+ list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
+ $where .= " AND userid $usql";
+ $params = array_merge($params, $uparams);
+ }
+
+ if ($attemptids) {
+ list($asql, $aparams) = $DB->get_in_or_equal($attemptids);
+ $where .= " AND id $asql";
+ $params = array_merge($params, $aparams);
+ }
+
+ $attempts = $DB->get_records_select('quiz_attempts', $where, $params);
+ if (!$attempts) {
return;
}
- // Fetch all attempts that need regrading
- if ($groupstudents) {
- list($usql, $params) = $DB->get_in_or_equal($groupstudents);
- $where = "qa.userid $usql AND ";
- } else {
- $where = '';
- $params = array();
- }
- $where .= "qa.quiz = ? AND qa.preview = 0 AND qa.uniqueid = qqr.attemptid AND qqr.regraded = 0";
- $params[] = $quiz->id;
- if (!$attempts = $DB->get_records_sql('SELECT qa.*, qqr.questionid FROM {quiz_attempts} qa, {quiz_question_regrade} qqr WHERE '. $where, $params)) {
- echo $OUTPUT->heading(get_string('noattemptstoregrade', 'quiz_overview'));
- return true;
- }
+
$this->clear_regrade_table($quiz, $groupstudents);
- // Fetch all questions
- $questions = question_load_questions(explode(',',quiz_questions_in_quiz($quiz->questions)), 'qqi.grade AS maxgrade, qqi.id AS instance',
- '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question');
- // Print heading
- echo $OUTPUT->heading(get_string('regradingquiz', 'quiz', format_string($quiz->name)));
-
- $apb = new progress_bar('aregradingbar', 500, true);
-
- // Loop through all questions and all attempts and regrade while printing progress info
- $attemptstodo = count($attempts);
- $attemptsdone = 0;
- @flush();@ob_flush();
- $attemptschanged = array();
foreach ($attempts as $attempt) {
- $question = $questions[$attempt->questionid];
- $changed = regrade_question_in_attempt($question, $attempt, $quiz, true);
- if ($changed) {
- $attemptschanged[] = $attempt->uniqueid;
- $usersschanged[] = $attempt->userid;
- }
- if (!empty($apb)) {
- $attemptsdone++;
- $a = new stdClass();
- $a->done = $attemptsdone;
- $a->todo = $attemptstodo;
- $apb->update($attemptsdone, $attemptstodo, get_string('attemptprogress', 'quiz_overview', $a));
- }
+ set_time_limit(30);
+ $this->regrade_attempt($attempt, $dryrun);
+ }
+
+ if (!$dryrun) {
+ $this->update_overall_grades($quiz);
}
- $this->check_overall_grades($quiz, array(), $attemptschanged);
}
- function clear_regrade_table($quiz, $groupstudents) {
+ /**
+ * Regrade those questions in those attempts that are marked as needing regrading
+ * in the quiz_overview_regrades table.
+ * @param object $quiz the quiz settings.
+ * @param array $groupstudents blank for all attempts, otherwise regrade attempts
+ * for these users.
+ */
+ protected function regrade_attempts_needing_it($quiz, $groupstudents) {
global $DB;
+
+ $where = "quiza.quiz = ? AND quiza.preview = 0 AND qqr.regraded = 0";
+ $params = array($quiz->id);
+
// Fetch all attempts that need regrading
+ if ($groupstudents) {
+ list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
+ $where .= " AND quiza.userid $usql";
+ $params += $uparams;
+ }
+
+ $toregrade = $DB->get_records_sql("
+ SELECT quiza.uniqueid, qqr.slot
+ FROM {quiz_attempts} quiza
+ JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid
+ WHERE $where", $params);
+
+ if (!$toregrade) {
+ return;
+ }
+
+ $attemptquestions = array();
+ foreach ($toregrade as $row) {
+ $attemptquestions[$row->uniqueid][] = $row->slot;
+ }
+ $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
+ array_keys($attemptquestions));
+
+ $this->clear_regrade_table($quiz, $groupstudents);
+
+ foreach ($attempts as $attempt) {
+ set_time_limit(30);
+ $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
+ }
+
+ $this->update_overall_grades($quiz);
+ }
+
+ /**
+ * Count the number of attempts in need of a regrade.
+ * @param object $quiz the quiz settings.
+ * @param array $groupstudents user ids. If this is given, only data relating
+ * to these users is cleared.
+ */
+ protected function count_question_attempts_needing_regrade($quiz, $groupstudents) {
+ global $DB;
+
+ $usertest = '';
+ $params = array();
+ if ($groupstudents) {
+ list($usql, $params) = get_in_or_equal($groupstudents);
+ $usertest = "quiza.userid $usql AND ";
+ }
+
+ $params[] = $quiz->id;
+ $sql = "SELECT COUNT(DISTINCT quiza.id)
+ FROM {quiz_attempts} quiza
+ JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
+ WHERE
+ $usertest
+ quiza.quiz = ? AND
+ quiza.preview = 0 AND
+ qqr.regraded = 0";
+ return $DB->count_records_sql($sql, $params);
+ }
+
+ /**
+ * Are there any pending regrades in the table we are going to show?
+ * @param string $from tables used by the main query.
+ * @param string $where where clause used by the main query.
+ * @param array $params required by the SQL.
+ * @return bool whether there are pending regrades.
+ */
+ protected function has_regraded_questions($from, $where, $params) {
+ global $DB;
+ $qubaids = new qubaid_join($from, 'uniqueid', $where, $params);
+ return $DB->record_exists_select('quiz_overview_regrades',
+ 'questionusageid ' . $qubaids->usage_id_in(),
+ $qubaids->usage_id_in_params());
+ }
+
+ /**
+ * Remove all information about pending/complete regrades from the database.
+ * @param object $quiz the quiz settings.
+ * @param array $groupstudents user ids. If this is given, only data relating
+ * to these users is cleared.
+ */
+ protected function clear_regrade_table($quiz, $groupstudents) {
+ global $DB;
+
+ // Fetch all attempts that need regrading
+ $where = '';
+ $params = array();
if ($groupstudents) {
list($usql, $params) = $DB->get_in_or_equal($groupstudents);
$where = "userid $usql AND ";
- } else {
- $usql = '';
- $where = '';
- $params = array();
}
+
$params[] = $quiz->id;
- $delsql = 'DELETE FROM {quiz_question_regrade} WHERE attemptid IN
- (SELECT uniqueid FROM {quiz_attempts} WHERE ' . $where . ' quiz = ?)';
- if (!$DB->execute($delsql, $params)) {
- print_error('err_failedtodeleteregrades', 'quiz_overview');
- }
+ $DB->delete_records_select('quiz_overview_regrades',
+ "questionusageid IN (
+ SELECT uniqueid
+ FROM {quiz_attempts}
+ WHERE $where quiz = ?
+ )", $params);
}
- function check_overall_grades($quiz, $userids=array(), $attemptids=array()) {
- global $DB;
- //recalculate $attempt->sumgrade
- //already updated in regrade_question_in_attempt
- $sql = "UPDATE {quiz_attempts} SET sumgrades= " .
- "COALESCE((SELECT SUM(qs.grade) FROM {question_sessions} qns, {question_states} qs " .
- "WHERE qns.newgraded = qs.id AND qns.attemptid = {quiz_attempts}.uniqueid ), 0) WHERE ";
- $attemptsql='';
- if (!$attemptids) {
- if ($userids) {
- list($usql, $params) = $DB->get_in_or_equal($userids);
- $attemptsql .= "{quiz_attempts}.userid $usql AND ";
- } else {
- $params = array();
- }
- $attemptsql .= "{quiz_attempts}.quiz =? AND preview = 0";
- $params[] = $quiz->id;
- } else {
- list($asql, $params) = $DB->get_in_or_equal($attemptids);
- $attemptsql .= "{quiz_attempts}.uniqueid $asql";
- }
- $sql .= $attemptsql;
- if (!$DB->execute($sql, $params)) {
- print_error('err_failedtorecalculateattemptgrades', 'quiz_overview');
- }
-
- // Update the overall quiz grades
- if ($attemptids) {
- //make sure we fetch all attempts for users to calculate grade.
- //not just those that have changed.
- $sql = "SELECT qa2.* FROM {quiz_attempts} qa2 WHERE " .
- "qa2.userid IN (SELECT DISTINCT userid FROM {quiz_attempts} WHERE $attemptsql) " .
- "AND qa2.timefinish > 0";
- } else {
- $sql = "SELECT * FROM {quiz_attempts} WHERE $attemptsql AND timefinish > 0";
- }
- if ($attempts = $DB->get_records_sql($sql, $params)) {
- $attemptsbyuser = quiz_report_index_by_keys($attempts, array('userid', 'id'));
- foreach($attemptsbyuser as $userid => $attemptsforuser) {
- quiz_save_best_grade($quiz, $userid, $attemptsforuser);
- }
- }
- }
-
- function delete_selected_attempts($quiz, $cm, $attemptids, $allowed, $groupstudents) {
- global $DB, $COURSE;
- foreach($attemptids as $attemptid) {
- $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid));
- if (!$attempt || $attempt->quiz != $quiz->id || $attempt->preview != 0) {
- // Ensure the attempt exists, and belongs to this quiz. If not skip.
- continue;
- }
- if ($allowed && !array_key_exists($attempt->userid, $allowed)) {
- // Ensure the attempt belongs to a student included in the report. If not skip.
- continue;
- }
- if ($groupstudents && !array_key_exists($attempt->userid, $groupstudents)) {
- // Additional check in groups mode.
- continue;
- }
- add_to_log($COURSE->id, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id,
- $attemptid, $cm->id);
- quiz_delete_attempt($attempt, $quiz);
- }
- }
-
- function regrade_selected_attempts($quiz, $attemptids, $groupstudents) {
- global $DB;
- require_capability('mod/quiz:regrade', $this->context);
- if ($groupstudents) {
- list($usql, $params) = $DB->get_in_or_equal($groupstudents);
- $where = "qa.userid $usql AND ";
- } else {
- $params = array();
- $where = '';
- }
- list($asql, $aparams) = $DB->get_in_or_equal($attemptids);
- $where = "qa.id $asql AND ";
- $params = array_merge($params, $aparams);
-
- $where .= "qa.quiz = ? AND qa.preview = 0";
- $params[] = $quiz->id;
- if (!$attempts = $DB->get_records_sql('SELECT qa.* FROM {quiz_attempts} qa WHERE '. $where, $params)) {
- print_error('noattemptstoregrade', 'quiz_overview');
- }
-
- // Fetch all questions
- $questions = question_load_questions(explode(',',quiz_questions_in_quiz($quiz->questions)), 'qqi.grade AS maxgrade, qqi.id AS instance',
- '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question');
- $updateoverallgrades = array();
- foreach($attempts as $attempt) {
- foreach ($questions as $question) {
- $changed = regrade_question_in_attempt($question, $attempt, $quiz, true);
- }
- $updateoverallgrades[] = $attempt->uniqueid;
- }
- $this->check_overall_grades($quiz, array(), $updateoverallgrades);
+ /**
+ * Update the final grades for all attempts. This method is used following
+ * a regrade.
+ * @param object $quiz the quiz settings.
+ * @param array $userids only update scores for these userids.
+ * @param array $attemptids attemptids only update scores for these attempt ids.
+ */
+ protected function update_overall_grades($quiz) {
+ quiz_update_all_attempt_sumgrades($quiz);
+ quiz_update_all_final_grades($quiz);
+ quiz_update_grades($quiz);
}
}
diff --git a/mod/quiz/report/overview/version.php b/mod/quiz/report/overview/version.php
index 61cc45d9f6b..90d55f3af5e 100644
--- a/mod/quiz/report/overview/version.php
+++ b/mod/quiz/report/overview/version.php
@@ -1,10 +1,29 @@
.
-////////////////////////////////////////////////////////////////////////////////
-// Code fragment to define the version of quiz overview report
-// This fragment is called by moodle_needs_upgrading() and /admin/index.php
-////////////////////////////////////////////////////////////////////////////////
-
-$plugin->version = 2009091400; // The (date) version of this module
+/**
+ * Quiz overview report version information.
+ *
+ * @package quiz
+ * @subpackage overview
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+$plugin->version = 2011051200;
+$plugin->requires = 2011060313;
diff --git a/mod/quiz/report/reportlib.php b/mod/quiz/report/reportlib.php
index b016a2af21a..14a4be88bb0 100644
--- a/mod/quiz/report/reportlib.php
+++ b/mod/quiz/report/reportlib.php
@@ -1,5 +1,33 @@
.
+
+/**
+ * Helper functions for the quiz reports.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
require_once($CFG->dirroot . '/mod/quiz/lib.php');
+require_once($CFG->libdir . '/filelib.php');
define('QUIZ_REPORT_DEFAULT_PAGE_SIZE', 30);
define('QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE', 10);
@@ -8,44 +36,7 @@ define('QUIZ_REPORT_ATTEMPTS_ALL', 0);
define('QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO', 1);
define('QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH', 2);
define('QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS', 3);
-/**
- * Get newest graded state or newest state for a number of attempts. Pass in the
- * uniqueid field from quiz_attempt table not the id. Use question_state_is_graded
- * function to check that the question is actually graded.
- * @param array attemptidssql either an array of attemptids with numerical keys
- * or an object with properties from, where and params.
- * @param boolean idxattemptq true if a multidimensional array should be
- * constructed with keys indexing array first by attempt and then by question
- * id.
- */
-function quiz_get_newgraded_states($attemptidssql, $idxattemptq = true, $fields='qs.*'){
- global $CFG, $DB;
- if ($attemptidssql && is_array($attemptidssql)){
- list($usql, $params) = $DB->get_in_or_equal($attemptidssql);
- $gradedstatesql = "SELECT $fields FROM " .
- "{question_sessions} qns, " .
- "{question_states} qs " .
- "WHERE qns.attemptid $usql AND " .
- "qns.newest = qs.id";
- $gradedstates = $DB->get_records_sql($gradedstatesql, $params);
- } else if ($attemptidssql && is_object($attemptidssql)){
- $gradedstatesql = "SELECT $fields FROM " .
- $attemptidssql->from.",".
- "{question_sessions} qns, " .
- "{question_states} qs " .
- "WHERE qns.attemptid = qa.uniqueid AND " .
- $attemptidssql->where." AND ".
- "qns.newest = qs.id";
- $gradedstates = $DB->get_records_sql($gradedstatesql, $attemptidssql->params);
- } else {
- return array();
- }
- if ($idxattemptq){
- return quiz_report_index_by_keys($gradedstates, array('attempt', 'question'));
- } else {
- return $gradedstates;
- }
-}
+
/**
* Takes an array of objects and constructs a multidimensional array keyed by
* the keys it finds on the object.
@@ -53,39 +44,40 @@ function quiz_get_newgraded_states($attemptidssql, $idxattemptq = true, $fields=
* including the keys passed as the next param.
* @param array $keys Array of strings with the names of the properties on the
* objects in datum that you want to index the multidimensional array by.
- * @param boolean $keysunique If there is not only one object for each
+ * @param bool $keysunique If there is not only one object for each
* combination of keys you are using you should set $keysunique to true.
* Otherwise all the object will be added to a zero based array. So the array
* returned will have count($keys) + 1 indexs.
* @return array multidimensional array properly indexed.
*/
-function quiz_report_index_by_keys($datum, $keys, $keysunique=true){
- if (!$datum){
- return $datum;
+function quiz_report_index_by_keys($datum, $keys, $keysunique = true) {
+ if (!$datum) {
+ return array();
}
$key = array_shift($keys);
$datumkeyed = array();
- foreach ($datum as $data){
- if ($keys || !$keysunique){
+ foreach ($datum as $data) {
+ if ($keys || !$keysunique) {
$datumkeyed[$data->{$key}][]= $data;
} else {
$datumkeyed[$data->{$key}]= $data;
}
}
- if ($keys){
- foreach ($datumkeyed as $datakey => $datakeyed){
+ if ($keys) {
+ foreach ($datumkeyed as $datakey => $datakeyed) {
$datumkeyed[$datakey] = quiz_report_index_by_keys($datakeyed, $keys, $keysunique);
}
}
return $datumkeyed;
}
-function quiz_report_unindex($datum){
- if (!$datum){
+
+function quiz_report_unindex($datum) {
+ if (!$datum) {
return $datum;
}
$datumunkeyed = array();
- foreach ($datum as $value){
- if (is_array($value)){
+ foreach ($datum as $value) {
+ if (is_array($value)) {
$datumunkeyed = array_merge($datumunkeyed, quiz_report_unindex($value));
} else {
$datumunkeyed[] = $value;
@@ -93,237 +85,192 @@ function quiz_report_unindex($datum){
}
return $datumunkeyed;
}
-function quiz_get_regraded_qs($attemptidssql, $limitfrom=0, $limitnum=0){
- global $CFG, $DB;
- if ($attemptidssql && is_array($attemptidssql)){
- list($asql, $params) = $DB->get_in_or_equal($attemptidssql);
- $regradedqsql = "SELECT qqr.* FROM " .
- "{quiz_question_regrade} qqr " .
- "WHERE qqr.attemptid $asql";
- $regradedqs = $DB->get_records_sql($regradedqsql, $params, $limitfrom, $limitnum);
- } else if ($attemptidssql && is_object($attemptidssql)){
- $regradedqsql = "SELECT qqr.* FROM " .
- $attemptidssql->from.", ".
- "{quiz_question_regrade} qqr " .
- "WHERE qqr.attemptid = qa.uniqueid AND " .
- $attemptidssql->where;
- $regradedqs = $DB->get_records_sql($regradedqsql, $attemptidssql->params, $limitfrom, $limitnum);
- } else {
+
+/**
+ * Get the slots of real questions (not descriptions) in this quiz, in order.
+ * @param object $quiz the quiz.
+ * @return array of slot => $question object with fields
+ * ->slot, ->id, ->maxmark, ->number, ->length.
+ */
+function quiz_report_get_significant_questions($quiz) {
+ global $DB;
+
+ $questionids = quiz_questions_in_quiz($quiz->questions);
+ if (empty($questionids)) {
return array();
}
- return quiz_report_index_by_keys($regradedqs, array('attemptid', 'questionid'));
-}
-function quiz_get_average_grade_for_questions($quiz, $userids){
- global $CFG, $DB;
- $qmfilter = quiz_report_qm_filter_select($quiz);
- list($usql, $params) = $DB->get_in_or_equal($userids);
+
+ list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionids));
$params[] = $quiz->id;
- $questionavgssql = "SELECT qns.questionid, AVG(qs.grade) FROM
- {quiz_attempts} qa
- LEFT JOIN {question_sessions} qns ON (qns.attemptid = qa.uniqueid)
- LEFT JOIN {question_states} qs ON (qns.newgraded = qs.id AND qs.event IN (".QUESTION_EVENTS_GRADED."))
- WHERE " .
- ($qmfilter?$qmfilter.' AND ':'') .
- "qa.userid $usql AND " .
- "qa.quiz = ? ".
- "GROUP BY qns.questionid";
- return $DB->get_records_sql_menu($questionavgssql, $params);
-}
+ $questions = $DB->get_records_sql("
+SELECT
+ q.id,
+ q.length,
+ qqi.grade AS maxmark
-function quiz_get_total_qas_graded_and_ungraded($quiz, $questionids, $userids){
- global $CFG, $DB;
- $params = array($quiz->id);
- list($u_sql, $u_params) = $DB->get_in_or_equal($userids);
- list($q_sql, $q_params) = $DB->get_in_or_equal($questionids);
+FROM {question} q
+JOIN {quiz_question_instances} qqi ON qqi.question = q.id
- $params = array_merge($params, $u_params, $q_params);
- $sql = "SELECT qs.question, COUNT(1) AS totalattempts,
- SUM(CASE WHEN (qs.event IN(".QUESTION_EVENTS_GRADED.")) THEN 1 ELSE 0 END) AS gradedattempts
- FROM
- {quiz_attempts} qa,
- {question_sessions} qns,
- {question_states} qs
- WHERE
- qa.quiz = ? AND
- qa.userid $u_sql AND
- qns.attemptid = qa.uniqueid AND
- qns.newest = qs.id AND
- qs.event IN (".QUESTION_EVENTS_CLOSED_OR_GRADED.") AND
- qs.question $q_sql
- GROUP BY qs.question";
- return $DB->get_records_sql($sql, $params);
-}
+WHERE
+ q.id $usql AND
+ qqi.quiz = ? AND
+ length > 0", $params);
-function quiz_format_average_grade_for_questions($avggradebyq, $questions, $quiz, $download){
- $row = array();
- if (!$avggradebyq){
- $avggradebyq = array();
- }
- foreach(array_keys($questions) as $questionid) {
- if (isset($avggradebyq[$questionid])){
- $grade = $avggradebyq[$questionid];
- $grade = quiz_rescale_grade($grade, $quiz, 'question');
- } else {
- $grade = '--';
- }
- $row['qsgrade'.$questionid] = $grade;
- }
- return $row;
-}
-/**
- * Load the question data necessary in the reports.
- * - Remove description questions.
- * - Order questions in order that they are in the quiz
- * - Add question numbers.
- * - Add grade from quiz_questions_instance
- */
-function quiz_report_load_questions($quiz){
- global $CFG, $DB;
- $questionlist = quiz_questions_in_quiz($quiz->questions);
- //In fact in most cases the id IN $questionlist below is redundant
- //since we are also doing a JOIN on the qqi table. But will leave it in
- //since this double check will probably do no harm.
- list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionlist));
- $params[] = $quiz->id;
- if (!$questions = $DB->get_records_sql("SELECT q.*, qqi.grade AS maxgrade
- FROM {question} q,
- {quiz_question_instances} qqi
- WHERE q.id $usql AND
- qqi.question = q.id AND
- qqi.quiz = ?", $params)) {
- print_error('noquestionsfound', 'quiz');
- }
- //Now we have an array of questions from a quiz we work out there question nos and remove
- //questions with zero length ie. description questions etc.
- //also put questions in order.
+ $qsbyslot = array();
$number = 1;
- $realquestions = array();
- $questionids = explode(',', $questionlist);
- foreach ($questionids as $id) {
- if ($questions[$id]->length) {
- // Ignore questions of zero length
- $realquestions[$id] = $questions[$id];
- $realquestions[$id]->number = $number;
- $number += $questions[$id]->length;
+ foreach (explode(',', $questionids) as $key => $id) {
+ if (!array_key_exists($id, $questions)) {
+ continue;
}
+
+ $slot = $key + 1;
+ $question = $questions[$id];
+ $question->slot = $slot;
+ $question->number = $number;
+
+ $qsbyslot[$slot] = $question;
+
+ $number += $question->length;
}
- return $realquestions;
+
+ return $qsbyslot;
}
+
/**
* Given the quiz grading method return sub select sql to find the id of the
* one attempt that will be graded for each user. Or return
* empty string if all attempts contribute to final grade.
*/
-function quiz_report_qm_filter_select($quiz){
- if ($quiz->attempts == 1) {//only one attempt allowed on this quiz
+function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
+ if ($quiz->attempts == 1) { // Only one attempt allowed on this quiz
return '';
}
- $useridsql = 'qa.userid';
- $quizidsql = 'qa.quiz';
- $qmfilterattempts = true;
- switch ($quiz->grademethod) {
- case QUIZ_GRADEHIGHEST :
- $field1 = 'sumgrades';
- $field2 = 'timestart';
- $aggregator1 = 'MAX';
- $aggregator2 = 'MIN';
- $qmselectpossible = true;
- break;
- case QUIZ_GRADEAVERAGE :
- $qmselectpossible = false;
- break;
- case QUIZ_ATTEMPTFIRST :
- $field1 = 'timestart';
- $field2 = 'id';
- $aggregator1 = 'MIN';
- $aggregator2 = 'MIN';
- $qmselectpossible = true;
- break;
- case QUIZ_ATTEMPTLAST :
- $field1 = 'timestart';
- $field2 = 'id';
- $aggregator1 = 'MAX';
- $aggregator2 = 'MAX';
- $qmselectpossible = true;
- break;
- }
- if ($qmselectpossible){
- $qmselect = "qa.$field1 = (SELECT $aggregator1(qa2.$field1) FROM {quiz_attempts} qa2 WHERE qa2.quiz = $quizidsql AND qa2.userid = $useridsql) AND " .
- "qa.$field2 = (SELECT $aggregator2(qa3.$field2) FROM {quiz_attempts} qa3 WHERE qa3.quiz = $quizidsql AND qa3.userid = $useridsql AND qa3.$field1 = qa.$field1)";
- } else {
- $qmselect = '';
- }
- return $qmselect;
+ switch ($quiz->grademethod) {
+ case QUIZ_GRADEHIGHEST :
+ return "$quizattemptsalias.id = (
+ SELECT MIN(qa2.id)
+ FROM {quiz_attempts} qa2
+ WHERE qa2.quiz = $quizattemptsalias.quiz AND
+ qa2.userid = $quizattemptsalias.userid AND
+ COALESCE(qa2.sumgrades, 0) = (
+ SELECT MAX(COALESCE(qa3.sumgrades, 0))
+ FROM {quiz_attempts} qa3
+ WHERE qa3.quiz = $quizattemptsalias.quiz AND
+ qa3.userid = $quizattemptsalias.userid
+ )
+ )";
+
+ case QUIZ_GRADEAVERAGE :
+ return '';
+
+ case QUIZ_ATTEMPTFIRST :
+ return "$quizattemptsalias.id = (
+ SELECT MIN(qa2.id)
+ FROM {quiz_attempts} qa2
+ WHERE qa2.quiz = $quizattemptsalias.quiz AND
+ qa2.userid = $quizattemptsalias.userid)";
+
+ case QUIZ_ATTEMPTLAST :
+ return "$quizattemptsalias.id = (
+ SELECT MAX(qa2.id)
+ FROM {quiz_attempts} qa2
+ WHERE qa2.quiz = $quizattemptsalias.quiz AND
+ qa2.userid = $quizattemptsalias.userid)";
+ }
}
-function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids=array()){
- global $CFG, $DB;
- if ($userids){
- list($usql, $params) = $DB->get_in_or_equal($userids);
+/**
+ * Get the nuber of students whose score was in a particular band for this quiz.
+ * @param number $bandwidth the width of each band.
+ * @param int $bands the number of bands
+ * @param int $quizid the quiz id.
+ * @param array $userids list of user ids.
+ * @return array band number => number of users with scores in that band.
+ */
+function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids = array()) {
+ global $DB;
+
+ if ($userids) {
+ list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
+ $usql = "qg.userid $usql AND";
} else {
- $usql ='';
+ $usql = '';
$params = array();
}
- $sql = "SELECT
- FLOOR(qg.grade/$bandwidth) AS band,
- COUNT(1) AS num
- FROM
- {quiz_grades} qg, {quiz} q
- WHERE qg.quiz = q.id " .
- ($usql?"AND qg.userid $usql ":'') .
- "AND qg.quiz = ?
- GROUP BY FLOOR(qg.grade/$bandwidth)
- ORDER BY band";
- $params[] = $quizid;
+ $sql = "
+SELECT band, COUNT(1)
+
+FROM (
+ SELECT FLOOR(qg.grade / :bandwidth) AS band
+ FROM {quiz_grades} qg
+ WHERE $usql qg.quiz = :quizid
+) subquery
+
+GROUP BY
+ band
+
+ORDER BY
+ band";
+
+ $params['quizid'] = $quizid;
+ $params['bandwidth'] = $bandwidth;
+
$data = $DB->get_records_sql_menu($sql, $params);
+
//need to create array elements with values 0 at indexes where there is no element
$data = $data + array_fill(0, $bands+1, 0);
ksort($data);
+
//place the maximum (prefect grade) into the last band i.e. make last
//band for example 9 <= g <=10 (where 10 is the perfect grade) rather than
//just 9 <= g <10.
- $data[$bands-1] += $data[$bands];
+ $data[$bands - 1] += $data[$bands];
unset($data[$bands]);
+
return $data;
}
-function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter){
- if ($quiz->attempts == 1) {//only one attempt allowed on this quiz
- return "".get_string('onlyoneattemptallowed', "quiz_overview")."
";
- } else if (!$qmsubselect){
- return "".get_string('allattemptscontributetograde', "quiz_overview")."
";
- } else if ($qmfilter){
- return "".get_string('showinggraded', "quiz_overview")."
";
- }else {
- return "".get_string('showinggradedandungraded', "quiz_overview",
- (''.quiz_get_grading_option_name($quiz->grademethod).''))."
";
+
+function quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter) {
+ if ($quiz->attempts == 1) {
+ return '' . get_string('onlyoneattemptallowed', 'quiz_overview') . '
';
+
+ } else if (!$qmsubselect) {
+ return '' . get_string('allattemptscontributetograde', 'quiz_overview') . '
';
+
+ } else if ($qmfilter) {
+ return '' . get_string('showinggraded', 'quiz_overview') . '
';
+
+ } else {
+ return '' . get_string('showinggradedandungraded', 'quiz_overview',
+ '' . quiz_get_grading_option_name($quiz->grademethod) .
+ '') . '
';
}
}
-
/**
* Get the feedback text for a grade on this quiz. The feedback is
* processed ready for display.
*
* @param float $grade a grade on this quiz.
- * @param integer $quizid the id of the quiz object.
+ * @param int $quizid the id of the quiz object.
* @return string the comment that corresponds to this grade (empty string if there is not one.
*/
function quiz_report_feedback_for_grade($grade, $quizid, $context) {
- global $DB, $CFG;
-
- require_once($CFG->libdir . '/filelib.php');
+ global $DB;
static $feedbackcache = array();
- if (!isset($feedbackcache[$quizid])){
+
+ if (!isset($feedbackcache[$quizid])) {
$feedbackcache[$quizid] = $DB->get_records('quiz_feedback', array('quizid' => $quizid));
}
+
$feedbacks = $feedbackcache[$quizid];
$feedbackid = 0;
$feedbacktext = '';
$feedbacktextformat = FORMAT_MOODLE;
foreach ($feedbacks as $feedback) {
- if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade){
+ if ($feedback->mingrade <= $grade && $grade < $feedback->maxgrade) {
$feedbackid = $feedback->id;
$feedbacktext = $feedback->feedbacktext;
$feedbacktextformat = $feedback->feedbacktextformat;
@@ -332,64 +279,89 @@ function quiz_report_feedback_for_grade($grade, $quizid, $context) {
}
// Clean the text, ready for display.
- $formatoptions = new stdClass;
+ $formatoptions = new stdClass();
$formatoptions->noclean = true;
- $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php', $context->id, 'mod_quiz', 'feedback', $feedbackid);
+ $feedbacktext = file_rewrite_pluginfile_urls($feedbacktext, 'pluginfile.php',
+ $context->id, 'mod_quiz', 'feedback', $feedbackid);
$feedbacktext = format_text($feedbacktext, $feedbacktextformat, $formatoptions);
return $feedbacktext;
}
-function quiz_report_scale_sumgrades_as_percentage($rawgrade, $quiz, $round = true) {
- if ($quiz->sumgrades != 0) {
- $grade = $rawgrade * 100 / $quiz->sumgrades;
- if ($round) {
- $grade = quiz_format_grade($quiz, $grade);
- }
- } else {
+/**
+ * Format a number as a percentage out of $quiz->sumgrades
+ * @param number $rawgrade the mark to format.
+ * @param object $quiz the quiz settings
+ * @param bool $round whether to round the results ot $quiz->decimalpoints.
+ */
+function quiz_report_scale_summarks_as_percentage($rawmark, $quiz, $round = true) {
+ if ($quiz->sumgrades == 0) {
return '';
}
- return $grade.'%';
+ if (!is_numeric($rawmark)) {
+ return $rawmark;
+ }
+
+ $mark = $rawmark * 100 / $quiz->sumgrades;
+ if ($round) {
+ $mark = quiz_format_grade($quiz, $mark);
+ }
+ return $mark . '%';
}
+
/**
* Returns an array of reports to which the current user has access to.
- * Reports are ordered as they should be for display in tabs.
+ * @return array reports are ordered as they should be for display in tabs.
*/
function quiz_report_list($context) {
global $DB;
static $reportlist = null;
- if (!is_null($reportlist)){
+ if (!is_null($reportlist)) {
return $reportlist;
}
- $reports = $DB->get_records('quiz_report', null, 'displayorder DESC', 'name, capability');
+
+ $reports = $DB->get_records('quiz_reports', null, 'displayorder DESC', 'name, capability');
$reportdirs = get_plugin_list('quiz');
// Order the reports tab in descending order of displayorder
$reportcaps = array();
- foreach ($reports as $key => $obj) {
- if (array_key_exists($obj->name, $reportdirs)) {
- $reportcaps[$obj->name] = $obj->capability;
+ foreach ($reports as $key => $report) {
+ if (array_key_exists($report->name, $reportdirs)) {
+ $reportcaps[$report->name] = $report->capability;
}
}
- // Add any other reports on the end
+ // Add any other reports, which are on disc but not in the DB, on the end
foreach ($reportdirs as $reportname => $notused) {
if (!isset($reportcaps[$reportname])) {
$reportcaps[$reportname] = null;
}
}
$reportlist = array();
- foreach ($reportcaps as $name => $capability){
- if (empty($capability)){
+ foreach ($reportcaps as $name => $capability) {
+ if (empty($capability)) {
$capability = 'mod/quiz:viewreports';
}
- if (has_capability($capability, $context)){
+ if (has_capability($capability, $context)) {
$reportlist[] = $name;
}
}
return $reportlist;
}
+/**
+ * Create a filename for use when downloading data from a quiz report. It is
+ * expected that this will be passed to flexible_table::is_downloading, which
+ * cleans the filename of bad characters and adds the file extension.
+ * @param string $report the type of report.
+ * @param string $courseshortname the course shortname.
+ * @param string $quizname the quiz name.
+ * @return string the filename.
+ */
+function quiz_report_download_filename($report, $courseshortname, $quizname) {
+ return $courseshortname . '-' . format_string($quizname, true) . '-' . $report;
+}
+
/**
* Get the default report for the current user.
* @param object $context the quiz context.
diff --git a/mod/quiz/report/responses/lang/en/quiz_responses.php b/mod/quiz/report/responses/lang/en/quiz_responses.php
index 45dc6b7f4fa..edd4a425366 100644
--- a/mod/quiz/report/responses/lang/en/quiz_responses.php
+++ b/mod/quiz/report/responses/lang/en/quiz_responses.php
@@ -1,5 +1,4 @@
.
-require_once($CFG->libdir.'/tablelib.php');
+/**
+ * This file defines the quiz responses report class.
+ *
+ * @package quiz
+ * @subpackage responses
+ * @copyright 2006 Jean-Michel Vedrine
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/mod/quiz/report/attemptsreport.php');
require_once($CFG->dirroot.'/mod/quiz/report/responses/responsessettings_form.php');
require_once($CFG->dirroot.'/mod/quiz/report/responses/responses_table.php');
-class quiz_responses_report extends quiz_default_report {
- /**
- * Display the report.
- */
- function display($quiz, $cm, $course) {
+/**
+ * Quiz report subclass for the responses report.
+ *
+ * This report lists some combination of
+ * * what question each student saw (this makes sense if random questions were used).
+ * * the response they gave,
+ * * and what the right answer is.
+ *
+ * Like the overview report, there are options for showing students with/without
+ * attempts, and for deleting selected attempts.
+ *
+ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_responses_report extends quiz_attempt_report {
+
+ public function display($quiz, $cm, $course) {
global $CFG, $COURSE, $DB, $PAGE, $OUTPUT;
- $context = get_context_instance(CONTEXT_MODULE, $cm->id);
-
- // Work out some display options - whether there is feedback, and whether scores should be shown.
- $hasfeedback = quiz_has_feedback($quiz);
- $fakeattempt = new stdClass();
- $fakeattempt->preview = false;
- $fakeattempt->timefinish = $quiz->timeopen;
- $fakeattempt->userid = 0;
- $reviewoptions = quiz_get_reviewoptions($quiz, $fakeattempt, $context);
- $showgrades = quiz_has_grades($quiz) && $reviewoptions->scores;
+ $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
$download = optional_param('download', '', PARAM_ALPHA);
+ list($currentgroup, $students, $groupstudents, $allowed) =
+ $this->load_relevant_students($cm);
+
$pageoptions = array();
$pageoptions['id'] = $cm->id;
$pageoptions['mode'] = 'responses';
@@ -39,106 +64,88 @@ class quiz_responses_report extends quiz_default_report {
$reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
$qmsubselect = quiz_report_qm_filter_select($quiz);
- /// find out current groups mode
- $currentgroup = groups_get_activity_group($cm, true);
+ $mform = new mod_quiz_report_responses_settings($reporturl,
+ array('qmsubselect' => $qmsubselect, 'quiz' => $quiz,
+ 'currentgroup' => $currentgroup, 'context' => $this->context));
- $mform = new mod_quiz_report_responses_settings($reporturl, array('qmsubselect'=> $qmsubselect, 'quiz'=>$quiz, 'currentgroup'=>$currentgroup));
if ($fromform = $mform->get_data()) {
$attemptsmode = $fromform->attemptsmode;
if ($qmsubselect) {
- //control is not on the form if
- //the grading method is not set
- //to grade one attempt per user eg. for average attempt grade.
$qmfilter = $fromform->qmfilter;
} else {
$qmfilter = 0;
}
+ set_user_preference('quiz_report_responses_qtext', $fromform->qtext);
+ set_user_preference('quiz_report_responses_resp', $fromform->resp);
+ set_user_preference('quiz_report_responses_right', $fromform->right);
set_user_preference('quiz_report_pagesize', $fromform->pagesize);
+ $includeqtext = $fromform->qtext;
+ $includeresp = $fromform->resp;
+ $includeright = $fromform->right;
$pagesize = $fromform->pagesize;
+
} else {
- $qmfilter = optional_param('qmfilter', 0, PARAM_INT);
$attemptsmode = optional_param('attemptsmode', null, PARAM_INT);
- if ($attemptsmode === null) {
- //default
- $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
- } else if ($currentgroup) {
- //default for when a group is selected
- if ($attemptsmode === null || $attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) {
- $attemptsmode = QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH;
- }
- } else if (!$currentgroup && $course->id == SITEID) {
- //force report on front page to show all, unless a group is selected.
- $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
+ if ($qmsubselect) {
+ $qmfilter = optional_param('qmfilter', 0, PARAM_INT);
+ } else {
+ $qmfilter = 0;
}
+ $includeqtext = get_user_preferences('quiz_report_responses_qtext', 0);
+ $includeresp = get_user_preferences('quiz_report_responses_resp', 1);
+ $includeright = get_user_preferences('quiz_report_responses_right', 0);
$pagesize = get_user_preferences('quiz_report_pagesize', 0);
}
- if ($pagesize < 1) {
- $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE;
+
+ $this->validate_common_options($attemptsmode, $pagesize, $course, $currentgroup);
+ if (!$includeqtext && !$includeresp && !$includeright) {
+ $includeresp = 1;
+ set_user_preference('quiz_report_responses_resp', 1);
}
+
// We only want to show the checkbox to delete attempts
// if the user has permissions and if the report mode is showing attempts.
- $candelete = has_capability('mod/quiz:deleteattempts', $context)
- && ($attemptsmode!= QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
+ $candelete = has_capability('mod/quiz:deleteattempts', $this->context)
+ && ($attemptsmode != QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
$displayoptions = array();
$displayoptions['attemptsmode'] = $attemptsmode;
$displayoptions['qmfilter'] = $qmfilter;
+ $displayoptions['qtext'] = $includeqtext;
+ $displayoptions['resp'] = $includeresp;
+ $displayoptions['right'] = $includeright;
- //work out the sql for this table.
- if (!$students = get_users_by_capability($context, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'u.id,1','','','','','',false)) {
- $students = array();
- } else {
- $students = array_keys($students);
+ $mform->set_data($displayoptions + array('pagesize' => $pagesize));
+
+ if ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) {
+ // This option is only available to users who can access all groups in
+ // groups mode, so setting allowed to empty (which means all quiz attempts
+ // are accessible, is not a security porblem.
+ $allowed = array();
}
- if (empty($currentgroup)) {
- // all users who can attempt quizzes
- $allowed = $students;
- $groupstudents = array();
- } else {
- // all users who can attempt quizzes and who are in the currently selected group
- if (!$groupstudents = get_users_by_capability($context, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'u.id,1','','','',$currentgroup,'',false)) {
- $groupstudents = array();
- } else {
- $groupstudents = array_keys($groupstudents);
- }
- $allowed = $groupstudents;
- }
-
- if ($students && ($attemptids = optional_param('attemptid', array(), PARAM_INT)) && confirm_sesskey()) {
- //attempts need to be deleted
- require_capability('mod/quiz:deleteattempts', $context);
- foreach ($attemptids as $attemptid) {
- $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid));
- if (!$attempt || $attempt->quiz != $quiz->id || $attempt->preview != 0) {
- // Ensure the attempt exists, and belongs to this quiz. If not skip.
- continue;
- }
- if ($attemptsmode != QUIZ_REPORT_ATTEMPTS_ALL && !array_key_exists($attempt->userid, $students)) {
- // Ensure the attempt belongs to a student included in the report. If not skip.
- continue;
- }
- if ($groupstudents && !array_key_exists($attempt->userid, $groupstudents)) {
- // Additional check in groups mode.
- continue;
- }
- add_to_log($course->id, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id,
- $attemptid, $cm->id);
- quiz_delete_attempt($attempt, $quiz);
- }
+ if ($attemptids = optional_param('attemptid', array(), PARAM_INT) && confirm_sesskey()) {
+ require_capability('mod/quiz:deleteattempts', $this->context);
+ $this->delete_selected_attempts($quiz, $cm, $attemptids, $allowed);
redirect($reporturl->out(false, $displayoptions));
}
- $questions = quiz_report_load_questions($quiz);
+ // Load the required questions.
+ $questions = quiz_report_get_significant_questions($quiz);
+
+ $table = new quiz_report_responses_table($quiz, $this->context, $qmsubselect,
+ $groupstudents, $students, $questions, $candelete, $reporturl, $displayoptions);
+ $filename = quiz_report_download_filename(get_string('responsesfilename', 'quiz_responses'),
+ $course->shortname, $quiz->name);
+ $table->is_downloading($download, $filename,
+ $COURSE->shortname . ' ' . format_string($quiz->name, true));
+ if ($table->is_downloading()) {
+ raise_memory_limit(MEMORY_EXTRA);
+ }
- $table = new quiz_report_responses_table($quiz , $qmsubselect, $groupstudents,
- $students, $questions, $candelete, $reporturl, $displayoptions, $context);
- $table->is_downloading($download, get_string('reportresponses','quiz_responses'),
- "$COURSE->shortname ".format_string($quiz->name,true));
if (!$table->is_downloading()) {
-
// Only print headers if not asked to download data
- $this->print_header_and_tabs($cm, $course, $quiz, 'responses', '');
+ $this->print_header_and_tabs($cm, $course, $quiz, 'responses');
}
if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
@@ -147,24 +154,6 @@ class quiz_responses_report extends quiz_default_report {
}
}
- $nostudents = false;
- if (!$students) {
- if (!$table->is_downloading()) {
- echo $OUTPUT->notification(get_string('nostudentsyet'));
- }
- $nostudents = true;
- } else if ($currentgroup && !$groupstudents) {
- if (!$table->is_downloading()) {
- echo $OUTPUT->notification(get_string('nostudentsingroup'));
- }
- $nostudents = true;
- }
- if (!$table->is_downloading()) {
- // Print display options
- $mform->set_data($displayoptions +compact('pagesize'));
- $mform->display();
- }
-
// Print information on the number of existing attempts
if (!$table->is_downloading()) { //do not print notices when downloading
if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
@@ -172,85 +161,32 @@ class quiz_responses_report extends quiz_default_report {
}
}
- if (!$nostudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL)) {
+ $hasquestions = quiz_questions_in_quiz($quiz->questions);
+ if (!$table->is_downloading()) {
+ if (!$hasquestions) {
+ echo quiz_no_questions_message($quiz, $cm, $this->context);
+ } else if (!$students) {
+ echo $OUTPUT->notification(get_string('nostudentsyet'));
+ } else if ($currentgroup && !$groupstudents) {
+ echo $OUTPUT->notification(get_string('nostudentsingroup'));
+ }
+
+ // Print display options
+ $mform->display();
+ }
+
+ $hasstudents = $students && (!$currentgroup || $groupstudents);
+ if ($hasquestions && ($hasstudents || $attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL)) {
// Print information on the grading method and whether we are displaying
- //
if (!$table->is_downloading()) { //do not print notices when downloading
- if ($strattempthighlight = quiz_report_highlighting_grading_method($quiz, $qmsubselect, $qmfilter)) {
+ if ($strattempthighlight = quiz_report_highlighting_grading_method(
+ $quiz, $qmsubselect, $qmfilter)) {
echo '' . $strattempthighlight . '
';
}
}
- $showgrades = quiz_has_grades($quiz) && $reviewoptions->scores;
- $hasfeedback = quiz_has_feedback($quiz);
-
- // Construct the SQL
- $fields = $DB->sql_concat('u.id', '\'#\'', 'COALESCE(qa.attempt, 0)').' AS concattedid, ';
- if ($qmsubselect) {
- $fields .=
- "(CASE " .
- " WHEN $qmsubselect THEN 1" .
- " ELSE 0 " .
- "END) AS gradedattempt, ";
- }
-
- $fields .='qa.uniqueid,
- qa.id AS attempt,
- u.id AS userid,
- u.idnumber,
- u.firstname,
- u.lastname,
- u.picture,
- u.imagealt,
- u.email,
- u.institution,
- u.department,
- qa.sumgrades,
- qa.timefinish,
- qa.timestart,
- qa.timefinish - qa.timestart AS duration,
- CASE WHEN qa.timefinish = 0 THEN null
- WHEN qa.timefinish > qa.timestart THEN qa.timefinish - qa.timestart
- ELSE 0 END AS duration';
- // To explain that last bit, in MySQL, qa.timestart and qa.timefinish
- // are unsigned. Since MySQL 5.5.5, when they introduced strict mode,
- // subtracting a larger unsigned int from a smaller one gave an error.
- // Therefore, we avoid doing that. timefinish can be non-zero and less
- // than timestart when you have two load-balanced servers with very
- // badly synchronised clocks, and a student does a really quick attempt.
-
- // This part is the same for all cases - join users and quiz_attempts tables
- $from = '{user} u ';
- $from .= 'LEFT JOIN {quiz_attempts} qa ON qa.userid = u.id AND qa.quiz = :quizid';
- $params = array('quizid' => $quiz->id);
-
- if ($qmsubselect && $qmfilter) {
- $from .= ' AND '.$qmsubselect;
- }
- switch ($attemptsmode) {
- case QUIZ_REPORT_ATTEMPTS_ALL:
- // Show all attempts, including students who are no longer in the course
- $where = 'qa.id IS NOT NULL AND qa.preview = 0';
- break;
- case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH:
- // Show only students with attempts
- list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u');
- $params += $allowed_params;
- $where = "u.id $allowed_usql AND qa.preview = 0 AND qa.id IS NOT NULL";
- break;
- case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
- // Show only students without attempts
- list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u');
- $params += $allowed_params;
- $where = "u.id $allowed_usql AND qa.id IS NULL";
- break;
- case QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS:
- // Show all students with or without attempts
- list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u');
- $params += $allowed_params;
- $where = "u.id $allowed_usql AND (qa.preview = 0 OR qa.preview IS NULL)";
- break;
- }
+ list($fields, $from, $where, $params) =
+ $this->base_sql($quiz, $qmsubselect, $qmfilter, $attemptsmode, $allowed);
$table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
@@ -261,90 +197,49 @@ class quiz_responses_report extends quiz_default_report {
$headers = array();
if (!$table->is_downloading() && $candelete) {
- $columns[]= 'checkbox';
- $headers[]= NULL;
+ $columns[] = 'checkbox';
+ $headers[] = null;
}
- if (!$table->is_downloading() && $CFG->grade_report_showuserimage) {
- $columns[]= 'picture';
- $headers[]= '';
- }
- if (!$table->is_downloading()) {
- $columns[]= 'fullname';
- $headers[]= get_string('name');
- } else {
- $columns[]= 'lastname';
- $headers[]= get_string('lastname');
- $columns[]= 'firstname';
- $headers[]= get_string('firstname');
- }
+ $this->add_user_columns($table, $columns, $headers);
- if ($CFG->grade_report_showuseridnumber) {
- $columns[]= 'idnumber';
- $headers[]= get_string('idnumber');
- }
if ($table->is_downloading()) {
- $columns[]= 'institution';
- $headers[]= get_string('institution');
-
- $columns[]= 'department';
- $headers[]= get_string('department');
-
- $columns[]= 'email';
- $headers[]= get_string('email');
-
- $columns[]= 'timestart';
- $headers[]= get_string('startedon', 'quiz');
-
- $columns[]= 'timefinish';
- $headers[]= get_string('timecompleted','quiz');
-
- $columns[]= 'duration';
- $headers[]= get_string('attemptduration', 'quiz');
+ $this->add_time_columns($columns, $headers);
}
- if ($showgrades) {
- $columns[] = 'sumgrades';
- $headers[] = get_string('grade', 'quiz').'/'.quiz_format_grade($quiz, $quiz->grade);
- }
+ $this->add_grade_columns($quiz, $columns, $headers);
- if ($hasfeedback) {
- $columns[] = 'feedbacktext';
- $headers[] = get_string('feedback', 'quiz');
- }
-
- // we want to display responses for all questions
foreach ($questions as $id => $question) {
- // Ignore questions of zero length
- $columns[] = 'qsanswer'.$id;
- $headers[] = '#'.$question->number;
- $question->formattedname = strip_tags(format_string($question->name));
- }
-
- // Load the question type specific information
- if (!get_question_options($questions)) {
- print_error('cannotloadoptions', 'quiz_responses');
+ if ($displayoptions['qtext']) {
+ $columns[] = 'question' . $id;
+ $headers[] = get_string('questionx', 'question', $question->number);
+ }
+ if ($displayoptions['resp']) {
+ $columns[] = 'response' . $id;
+ $headers[] = get_string('responsex', 'quiz_responses', $question->number);
+ }
+ if ($displayoptions['right']) {
+ $columns[] = 'right' . $id;
+ $headers[] = get_string('rightanswerx', 'quiz_responses', $question->number);
+ }
}
$table->define_columns($columns);
$table->define_headers($headers);
- $table->sortable(true, 'concattedid');
+ $table->sortable(true, 'uniqueid');
// Set up the table
$table->define_baseurl($reporturl->out(true, $displayoptions));
- $table->collapsible(true);
+ $this->configure_user_columns($table);
$table->no_sorting('feedbacktext');
-
- $table->column_class('picture', 'picture');
- $table->column_class('lastname', 'bold');
- $table->column_class('firstname', 'bold');
- $table->column_class('fullname', 'bold');
$table->column_class('sumgrades', 'bold');
$table->set_attribute('id', 'attempts');
+ $table->collapsible(true);
+
$table->out($pagesize, true);
}
return true;
diff --git a/mod/quiz/report/responses/responses_table.php b/mod/quiz/report/responses/responses_table.php
index 12a05bbd5b4..c994908e603 100644
--- a/mod/quiz/report/responses/responses_table.php
+++ b/mod/quiz/report/responses/responses_table.php
@@ -1,140 +1,92 @@
.
-class quiz_report_responses_table extends table_sql {
+/**
+ * This file defines the quiz responses report class.
+ *
+ * @package quiz
+ * @subpackage responses
+ * @copyright 2008 Jean-Michel Vedrine
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
- var $useridfield = 'userid';
- var $reporturl;
- var $displayoptions;
+defined('MOODLE_INTERNAL') || die();
- function quiz_report_responses_table($quiz , $qmsubselect, $groupstudents,
- $students, $questions, $candelete, $reporturl, $displayoptions, $context){
- parent::__construct('mod-quiz-report-responses-report');
- $this->quiz = $quiz;
- $this->qmsubselect = $qmsubselect;
- $this->groupstudents = $groupstudents;
- $this->students = $students;
- $this->questions = $questions;
- $this->candelete = $candelete;
- $this->reporturl = $reporturl;
- $this->displayoptions = $displayoptions;
- $this->context = $context;
+
+/**
+ * This is a table subclass for displaying the quiz responses report.
+ *
+ * @copyright 2008 Jean-Michel Vedrine
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_report_responses_table extends quiz_attempt_report_table {
+
+ public function __construct($quiz, $context, $qmsubselect, $groupstudents,
+ $students, $questions, $candelete, $reporturl, $displayoptions) {
+ parent::__construct('mod-quiz-report-responses-report', $quiz, $context,
+ $qmsubselect, $groupstudents, $students, $questions, $candelete,
+ $reporturl, $displayoptions);
}
- function build_table(){
+
+ public function build_table() {
if ($this->rawdata) {
$this->strtimeformat = str_replace(',', ' ', get_string('strftimedatetime'));
parent::build_table();
}
}
- function wrap_html_start(){
- if (!$this->is_downloading()) {
- if ($this->candelete) {
- // Start form
- $displayurl = new moodle_url($this->reporturl, $this->displayoptions);
- $strreallydel = addslashes_js(get_string('deleteattemptcheck','quiz'));
- echo '';
- }
- }
- }
-
-
- function col_checkbox($attempt){
- if ($attempt->attempt){
- return '';
- } else {
- return '';
- }
- }
-
- function col_picture($attempt){
- global $COURSE, $OUTPUT;
- $user = new stdClass();
- $user->id = $attempt->userid;
- $user->lastname = $attempt->lastname;
- $user->firstname = $attempt->firstname;
- $user->imagealt = $attempt->imagealt;
- $user->picture = $attempt->picture;
- $user->email = $attempt->email;
- return $OUTPUT->user_picture($user);
- }
-
- function col_fullname($attempt){
- $html = parent::col_fullname($attempt);
- if ($this->is_downloading()) {
- return $html;
+ public function wrap_html_start() {
+ global $PAGE;
+ if ($this->is_downloading() || !$this->candelete) {
+ return;
}
- return $html . '
'.get_string('reviewattempt', 'quiz').'';
+ // Start form
+ $url = new moodle_url($this->reporturl, $this->displayoptions);
+ $url->param('sesskey', sesskey());
+
+ echo '';
+ echo '';
}
- function col_duration($attempt){
- if ($attempt->timefinish) {
- return format_time($attempt->timefinish - $attempt->timestart);
- } elseif ($attempt->timestart) {
- return get_string('unfinished', 'quiz');
- } else {
- return '-';
- }
- }
-
- function col_sumgrades($attempt){
+ public function col_sumgrades($attempt) {
if (!$attempt->timefinish) {
return '-';
}
@@ -144,116 +96,72 @@ class quiz_report_responses_table extends table_sql {
return $grade;
}
- $gradehtml = ''.$grade.'';
- if ($this->qmsubselect && $attempt->gradedattempt){
- $gradehtml = ''.$gradehtml.'
';
- }
+ $gradehtml = '' . $grade . '';
return $gradehtml;
}
- function other_cols($colname, $attempt){
- global $QTYPES, $OUTPUT;
- static $states =array();
- if (preg_match('/^qsanswer([0-9]+)$/', $colname, $matches)){
- if ($attempt->uniqueid == 0) {
- return '-';
- }
- $questionid = $matches[1];
- if (isset($this->gradedstatesbyattempt[$attempt->uniqueid][$questionid])){
- $stateforqinattempt = $this->gradedstatesbyattempt[$attempt->uniqueid][$questionid];
- } else {
- return '-';
- }
+ public function data_col($slot, $field, $attempt) {
+ global $CFG;
- $question = $this->questions[$questionid];
- restore_question_state($question, $stateforqinattempt);
-
- if (!$this->is_downloading() || $this->is_downloading() == 'xhtml'){
- $formathtml = true;
- } else {
- $formathtml = false;
- }
-
- $summary = $QTYPES[$question->qtype]->response_summary($question, $stateforqinattempt,
- QUIZ_REPORT_RESPONSES_MAX_LEN_TO_DISPLAY, $formathtml);
- if (!$this->is_downloading()) {
- if ($summary){
- $link = new moodle_url("/mod/quiz/reviewquestion.php?attempt=$attempt->attempt&question=$question->id");
- $action = new popup_action('click', $link, 'reviewquestion', array('height' => 450, 'width' => 650));
- $summary = $OUTPUT->action_link($link, $summary, $action, array('title'=>get_string('reviewresponsetoq', 'quiz', $question->formattedname)));
-
- if (question_state_is_graded($stateforqinattempt)
- && ($question->maxgrade > 0)){
- $grade = $stateforqinattempt->grade
- / $question->maxgrade;
- $qclass = question_get_feedback_class($grade);
- $feedbackimg = question_get_feedback_image($grade);
- $questionclass = "que";
- return "".$summary."$feedbackimg";
- } else {
- return $summary;
- }
- } else {
- return '';
- }
-
- } else {
- return $summary;
- }
- } else {
- return NULL;
- }
- }
-
- function col_feedbacktext($attempt){
- if ($attempt->timefinish) {
- if (!$this->is_downloading()) {
- return quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz, false), $this->quiz->id, $this->context);
- } else {
- return strip_tags(quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz, false), $this->quiz->id, $this->context));
- }
- } else {
+ if ($attempt->usageid == 0) {
return '-';
}
+ $question = $this->questions[$slot];
+ if (!isset($this->lateststeps[$attempt->usageid][$slot])) {
+ return '-';
+ }
+
+ $stepdata = $this->lateststeps[$attempt->usageid][$slot];
+
+ if (is_null($stepdata->$field)) {
+ $summary = '-';
+ } else {
+ $summary = trim($stepdata->$field);
+ }
+
+ if ($this->is_downloading() || $field != 'responsesummary') {
+ return $summary;
+ }
+
+ return $this->make_review_link($summary, $attempt, $slot);
}
- function query_db($pagesize, $useinitialsbar=true){
- // Add table joins so we can sort by question answer
- // unfortunately can't join all tables necessary to fetch all answers
- // to get the state for one question per attempt row we must join two tables
- // and there is a limit to how many joins you can have in one query. In MySQL it
- // is 61. This means that when having more than 29 questions the query will fail.
- // So we join just the tables needed to sort the attempts.
- if($sort = $this->get_sql_sort()) {
- $this->sql->from .= ' ';
- $sortparts = explode(',', $sort);
- $matches = array();
- foreach($sortparts as $sortpart) {
- $sortpart = trim($sortpart);
- if (preg_match('/^qsanswer([0-9]+)/', $sortpart, $matches)){
- $qid = intval($matches[1]);
- $this->sql->fields .= ", qs$qid.grade AS qsgrade$qid, qs$qid.answer AS qsanswer$qid, qs$qid.event AS qsevent$qid, qs$qid.id AS qsid$qid";
- $this->sql->from .= "LEFT JOIN {question_sessions} qns$qid ON qns$qid.attemptid = qa.uniqueid AND qns$qid.questionid = :qid$qid ";
- $this->sql->from .= "LEFT JOIN {question_states} qs$qid ON qs$qid.id = qns$qid.newgraded ";
- $this->sql->params['qid'.$qid] = $qid;
- }
- }
- }
- parent::query_db($pagesize, $useinitialsbar);
- $qsfields = 'qs.id, qs.grade, qs.event, qs.question, qs.answer, qs.attempt';
- if (!$this->is_downloading()) {
- $attemptids = array();
- foreach ($this->rawdata as $attempt){
- if ($attempt->uniqueid > 0){
- $attemptids[] = $attempt->uniqueid;
- }
- }
- $this->gradedstatesbyattempt = quiz_get_newgraded_states($attemptids, true, $qsfields);
+ public function other_cols($colname, $attempt) {
+ if (preg_match('/^question(\d+)$/', $colname, $matches)) {
+ return $this->data_col($matches[1], 'questionsummary', $attempt);
+
+ } else if (preg_match('/^response(\d+)$/', $colname, $matches)) {
+ return $this->data_col($matches[1], 'responsesummary', $attempt);
+
+ } else if (preg_match('/^right(\d+)$/', $colname, $matches)) {
+ return $this->data_col($matches[1], 'rightanswer', $attempt);
+
} else {
- $this->gradedstatesbyattempt = quiz_get_newgraded_states($this->sql, true, $qsfields);
+ return null;
}
}
+
+ protected function requires_latest_steps_loaded() {
+ return true;
+ }
+
+ protected function is_latest_step_column($column) {
+ if (preg_match('/^(?:question|response|right)([0-9]+)/', $column, $matches)) {
+ return $matches[1];
+ }
+ return false;
+ }
+
+ /**
+ * Get any fields that might be needed when sorting on date for a particular slot.
+ * @param int $slot the slot for the column we want.
+ * @param string $alias the table alias for latest state information relating to that slot.
+ */
+ protected function get_required_latest_state_fields($slot, $alias) {
+ return "$alias.questionsummary AS question$slot,
+ $alias.rightanswer AS right$slot,
+ $alias.responsesummary AS response$slot";
+ }
}
-
diff --git a/mod/quiz/report/responses/responsessettings_form.php b/mod/quiz/report/responses/responsessettings_form.php
index 15fa3985102..6c77bd4f604 100644
--- a/mod/quiz/report/responses/responsessettings_form.php
+++ b/mod/quiz/report/responses/responsessettings_form.php
@@ -1,46 +1,102 @@
libdir/formslib.php";
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * This file defines the setting form for the quiz responses report.
+ *
+ * @package quiz
+ * @subpackage responses
+ * @copyright 2008 Jean-Michel Vedrine
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+
+/**
+ * Quiz responses report settings form.
+ *
+ * @copyright 2008 Jean-Michel Vedrine
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class mod_quiz_report_responses_settings extends moodleform {
- function definition() {
- global $COURSE;
- $mform =& $this->_form;
-//-------------------------------------------------------------------------------
- $mform->addElement('header', 'preferencespage', get_string('preferencespage', 'quiz_overview'));
+ protected function definition() {
+ $mform = $this->_form;
- if (!$this->_customdata['currentgroup']){
+ $mform->addElement('header', 'preferencespage',
+ get_string('preferencespage', 'quiz_overview'));
+
+ if (!$this->_customdata['currentgroup']) {
$studentsstring = get_string('participants');
} else {
$a = new stdClass();
$a->coursestudent = get_string('participants');
$a->groupname = groups_get_group_name($this->_customdata['currentgroup']);
- if (20 < strlen($a->groupname)){
+ if (20 < strlen($a->groupname)) {
$studentsstring = get_string('studentingrouplong', 'quiz_overview', $a);
} else {
$studentsstring = get_string('studentingroup', 'quiz_overview', $a);
}
}
$options = array();
- if (!$this->_customdata['currentgroup']){
- $options[QUIZ_REPORT_ATTEMPTS_ALL] = get_string('optallattempts','quiz_overview');
+ if (!$this->_customdata['currentgroup']) {
+ $options[QUIZ_REPORT_ATTEMPTS_ALL] = get_string('optallattempts', 'quiz_overview');
}
- if ($this->_customdata['currentgroup'] || $COURSE->id != SITEID) {
- $options[QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS] = get_string('optallstudents','quiz_overview', $studentsstring);
+ if ($this->_customdata['currentgroup'] ||
+ !is_inside_frontpage($this->_customdata['context'])) {
+ $options[QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS] =
+ get_string('optallstudents', 'quiz_overview', $studentsstring);
$options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH] =
- get_string('optattemptsonly','quiz_overview', $studentsstring);
- $options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO] = get_string('optnoattemptsonly', 'quiz_overview', $studentsstring);
+ get_string('optattemptsonly', 'quiz_overview', $studentsstring);
+ $options[QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO] =
+ get_string('optnoattemptsonly', 'quiz_overview', $studentsstring);
}
- $mform->addElement('select', 'attemptsmode', get_string('show', 'quiz_overview'), $options);
- if ($this->_customdata['qmsubselect']){
- $gm = ''.quiz_get_grading_option_name($this->_customdata['quiz']->grademethod).'';
- $mform->addElement('advcheckbox', 'qmfilter', get_string('showattempts', 'quiz_overview'), get_string('optonlygradedattempts', 'quiz_overview', $gm), null, array(0,1));
+ $mform->addElement('select', 'attemptsmode',
+ get_string('show', 'quiz_overview'), $options);
+
+ if ($this->_customdata['qmsubselect']) {
+ $gm = '' .
+ quiz_get_grading_option_name($this->_customdata['quiz']->grademethod) .
+ '';
+ $mform->addElement('advcheckbox', 'qmfilter',
+ get_string('showattempts', 'quiz_overview'),
+ get_string('optonlygradedattempts', 'quiz_overview', $gm), null, array(0, 1));
}
-//-------------------------------------------------------------------------------
- $mform->addElement('header', 'preferencesuser', get_string('preferencesuser', 'quiz_overview'));
+
+ $colsgroup = array();
+ $colsgroup[] = $mform->createElement('advcheckbox', 'qtext', '',
+ get_string('summaryofquestiontext', 'quiz_responses'));
+ $colsgroup[] = $mform->createElement('advcheckbox', 'resp', '',
+ get_string('summaryofresponse', 'quiz_responses'));
+ $colsgroup[] = $mform->createElement('advcheckbox', 'right', '',
+ get_string('summaryofrightanswer', 'quiz_responses'));
+ $mform->addGroup($colsgroup, null,
+ get_string('include', 'quiz_responses'), '
', false);
+
+ $mform->addElement('header', 'preferencesuser',
+ get_string('preferencesuser', 'quiz_overview'));
$mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz_overview'));
$mform->setType('pagesize', PARAM_INT);
- $mform->addElement('submit', 'submitbutton', get_string('preferencessave', 'quiz_overview'));
+ $mform->addElement('submit', 'submitbutton',
+ get_string('preferencessave', 'quiz_overview'));
}
}
diff --git a/mod/quiz/report/responses/styles.css b/mod/quiz/report/responses/styles.css
deleted file mode 100644
index 2af7eaa5495..00000000000
--- a/mod/quiz/report/responses/styles.css
+++ /dev/null
@@ -1,23 +0,0 @@
-body#mod-quiz-report table#attempts {
- margin: 20px auto;
-}
-body#mod-quiz-report table#attempts .header,
-body#mod-quiz-report table#attempts .cell
-{
- padding: 4px;
-}
-body#mod-quiz-report table#attempts .header .commands {
- display: inline;
-}
-body#mod-quiz-report table#attempts td {
- border-width: 1px;
- border-style: solid;
-}
-body#mod-quiz-report table#attempts .header {
- text-align: left;
-}
-body#mod-quiz-report table#attempts .numcol {
- text-align: center;
- vertical-align : middle !important;
-}
-
diff --git a/mod/quiz/report/responses/version.php b/mod/quiz/report/responses/version.php
new file mode 100644
index 00000000000..d47825adcb3
--- /dev/null
+++ b/mod/quiz/report/responses/version.php
@@ -0,0 +1,29 @@
+.
+
+/**
+ * Quiz responses report version information.
+ *
+ * @package quiz
+ * @subpackage responses
+ * @copyright 2011 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2011051200;
+$plugin->requires = 2011060313;
diff --git a/mod/quiz/report/simpletest/testreportlib.php b/mod/quiz/report/simpletest/testreportlib.php
index ed8629b89d3..fcdb40ff523 100644
--- a/mod/quiz/report/simpletest/testreportlib.php
+++ b/mod/quiz/report/simpletest/testreportlib.php
@@ -1,23 +1,48 @@
.
+
/**
* Unit tests for (some of) mod/quiz/report/reportlib.php
*
- * @author me@jamiep.org
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2008 Jamie Pratt me@jamiep.org
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
-/** */
+
+defined('MOODLE_INTERNAL') || die();
+
require_once(dirname(__FILE__) . '/../../../../config.php');
global $CFG;
require_once($CFG->libdir . '/simpletestlib.php'); // Include the test libraries
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); // Include the code to test
-/** This class contains the test cases for the functions in reportlib.php. */
+
+/**
+ * This class contains the test cases for the functions in reportlib.php.
+ *
+ * @copyright 2008 Jamie Pratt me@jamiep.org
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
class question_reportlib_test extends UnitTestCase {
public static $includecoverage = array('mod/quiz/report/reportlib.php');
- function test_quiz_report_index_by_keys() {
+
+ public function test_quiz_report_index_by_keys() {
$datum = array();
$object = new stdClass();
$object->qid = 3;
@@ -26,20 +51,31 @@ class question_reportlib_test extends UnitTestCase {
$object->grade = 3;
$datum[] = $object;
- $indexed = quiz_report_index_by_keys($datum, array('aid','qid'));
+ $indexed = quiz_report_index_by_keys($datum, array('aid', 'qid'));
$this->assertEqual($indexed[101][3]->qid, 3);
$this->assertEqual($indexed[101][3]->aid, 101);
$this->assertEqual($indexed[101][3]->response, '');
$this->assertEqual($indexed[101][3]->grade, 3);
- $indexed = quiz_report_index_by_keys($datum, array('aid','qid'), false);
+ $indexed = quiz_report_index_by_keys($datum, array('aid', 'qid'), false);
$this->assertEqual($indexed[101][3][0]->qid, 3);
$this->assertEqual($indexed[101][3][0]->aid, 101);
$this->assertEqual($indexed[101][3][0]->response, '');
$this->assertEqual($indexed[101][3][0]->grade, 3);
+ }
+ public function test_quiz_report_scale_summarks_as_percentage() {
+ $quiz = new stdClass();
+ $quiz->sumgrades = 10;
+ $quiz->decimalpoints = 2;
+
+ $this->assertEqual('12.34567%',
+ quiz_report_scale_summarks_as_percentage(1.234567, $quiz, false));
+ $this->assertEqual('12.35%',
+ quiz_report_scale_summarks_as_percentage(1.234567, $quiz, true));
+ $this->assertEqual('-',
+ quiz_report_scale_summarks_as_percentage('-', $quiz, true));
}
}
-
diff --git a/mod/quiz/report/statistics/cron.php b/mod/quiz/report/statistics/cron.php
index 7ddd3bbe2f3..35ebabc612a 100644
--- a/mod/quiz/report/statistics/cron.php
+++ b/mod/quiz/report/statistics/cron.php
@@ -1,15 +1,62 @@
.
+
+/**
+ * Quiz statistics report cron code.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Quiz statistics report cron code. Deletes cached data more than a certain age.
+ */
+function quiz_report_statistics_cron() {
global $DB;
- if ($todelete = $DB->get_records_select_menu('quiz_statistics', 'timemodified < ?', array(time()-5*HOURSECS))){
- list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
- if (!$DB->delete_records_select('quiz_statistics', "id $todeletesql", $todeleteparams)){
- mtrace('Error deleting out of date quiz_statistics records.');
- }
- if (!$DB->delete_records_select('quiz_question_statistics', "quizstatisticsid $todeletesql", $todeleteparams)){
- mtrace('Error deleting out of date quiz_question_statistics records.');
- }
+
+ $expiretime = time() - 5*HOURSECS;
+ $todelete = $DB->get_records_select_menu('quiz_statistics', 'timemodified < ?',
+ array($expiretime), '', 'id, 1');
+
+ if (!$todelete) {
+ return true;
}
+
+ list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
+
+ if (!$DB->delete_records_select('quiz_question_statistics',
+ 'quizstatisticsid ' . $todeletesql, $todeleteparams)) {
+ mtrace('Error deleting out of date quiz_question_statistics records.');
+ }
+
+ if (!$DB->delete_records_select('quiz_question_response_stats',
+ 'quizstatisticsid ' . $todeletesql, $todeleteparams)) {
+ mtrace('Error deleting out of date quiz_question_response_stats records.');
+ }
+
+ if (!$DB->delete_records_select('quiz_statistics',
+ 'id ' . $todeletesql, $todeleteparams)) {
+ mtrace('Error deleting out of date quiz_statistics records.');
+ }
+
return true;
}
-
diff --git a/mod/quiz/report/statistics/db/access.php b/mod/quiz/report/statistics/db/access.php
index ae2f1f9e8fc..c29f1d45a05 100644
--- a/mod/quiz/report/statistics/db/access.php
+++ b/mod/quiz/report/statistics/db/access.php
@@ -1,11 +1,32 @@
.
+
/**
* Capability definitions for the quiz statistics report.
*
- * For naming conventions, see lib/db/access.php.
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
+defined('MOODLE_INTERNAL') || die();
+
$capabilities = array(
- 'quizreport/statistics:view' => array(
+ 'quiz/statistics:view' => array(
'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array(
diff --git a/mod/quiz/report/statistics/db/install.php b/mod/quiz/report/statistics/db/install.php
index 24ddba85d3c..d127a593ba7 100644
--- a/mod/quiz/report/statistics/db/install.php
+++ b/mod/quiz/report/statistics/db/install.php
@@ -1,16 +1,48 @@
.
+/**
+ * Post-install script for the quiz statistics report.
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2010 Petr Skoda (http://skodak.org)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Post-install script
+ */
function xmldb_quiz_statistics_install() {
global $DB;
+ $dbman = $DB->get_manager();
+
$record = new stdClass();
$record->name = 'statistics';
$record->displayorder = 8000;
$record->cron = 18000;
- $record->capability = 'quizreport/statistics:view';
- $DB->insert_record('quiz_report', $record);
+ $record->capability = 'quiz/statistics:view';
-}
\ No newline at end of file
+ if ($dbman->table_exists('quiz_reports')) {
+ $DB->insert_record('quiz_reports', $record);
+ } else {
+ $DB->insert_record('quiz_report', $record);
+ }
+}
diff --git a/mod/quiz/report/statistics/db/install.xml b/mod/quiz/report/statistics/db/install.xml
index b44b0ebd0f9..fc95b19aa87 100644
--- a/mod/quiz/report/statistics/db/install.xml
+++ b/mod/quiz/report/statistics/db/install.xml
@@ -31,8 +31,9 @@
-
-
+
+
+
@@ -40,9 +41,10 @@
-
-
-
+
+
+
+
@@ -54,7 +56,7 @@
-
+
@@ -64,4 +66,4 @@
-
\ No newline at end of file
+
diff --git a/mod/quiz/report/statistics/db/upgrade.php b/mod/quiz/report/statistics/db/upgrade.php
index eaba3db0025..78944f3c98e 100644
--- a/mod/quiz/report/statistics/db/upgrade.php
+++ b/mod/quiz/report/statistics/db/upgrade.php
@@ -1,102 +1,152 @@
.
+/**
+ * Post-install script for the quiz statistics report.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Quiz statistics report upgrade code.
+ */
function xmldb_quiz_statistics_upgrade($oldversion) {
global $DB;
$dbman = $DB->get_manager();
-//===== 1.9.0 upgrade line ======//
+ // In Moodle 2.0, this table was incorrectly called quiz_report, which breaks
+ // the moodle coding guidelines. In 2.1 it was renamed to quiz_reports. This
+ // bit of code lets us handle all the various upgrade paths without problems.
+ if ($dbman->table_exists('quiz_reports')) {
+ $quizreportstablename = 'quiz_reports';
+ } else {
+ $quizreportstablename = 'quiz_report';
+ }
+
+ //===== 1.9.0 upgrade line ======//
if ($oldversion < 2008072401) {
//register cron to run every 5 hours.
- $DB->set_field('quiz_report', 'cron', HOURSECS*5, array('name'=>'statistics'));
+ $DB->set_field($quizreportstablename, 'cron', HOURSECS*5, array('name'=>'statistics'));
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008072401, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008072401, 'quiz', 'statistics');
}
if ($oldversion < 2008072500) {
- /// Define field s to be added to quiz_question_statistics
+ // Define field s to be added to quiz_question_statistics
$table = new xmldb_table('quiz_question_statistics');
- $field = new xmldb_field('s', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'subquestion');
+ $field = new xmldb_field('s', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, '0', 'subquestion');
- /// Conditionally launch add field s
+ // Conditionally launch add field s
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008072500, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008072500, 'quiz', 'statistics');
}
if ($oldversion < 2008072800) {
- /// Define field maxgrade to be added to quiz_question_statistics
+ // Define field maxgrade to be added to quiz_question_statistics
$table = new xmldb_table('quiz_question_statistics');
- $field = new xmldb_field('maxgrade', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, 'subquestions');
+ $field = new xmldb_field('maxgrade', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ null, null, null, 'subquestions');
- /// Conditionally launch add field maxgrade
+ // Conditionally launch add field maxgrade
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008072800, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008072800, 'quiz', 'statistics');
}
if ($oldversion < 2008072801) {
- /// Define field positions to be added to quiz_question_statistics
+ // Define field positions to be added to quiz_question_statistics
$table = new xmldb_table('quiz_question_statistics');
- $field = new xmldb_field('positions', XMLDB_TYPE_TEXT, 'medium', null, null, null, null, 'maxgrade');
+ $field = new xmldb_field('positions', XMLDB_TYPE_TEXT, 'medium', null,
+ null, null, null, 'maxgrade');
- /// Conditionally launch add field positions
+ // Conditionally launch add field positions
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008072801, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008072801, 'quiz', 'statistics');
}
if ($oldversion < 2008081500) {
- /// Changing type of field maxgrade on table quiz_question_statistics to number
+ // Changing type of field maxgrade on table quiz_question_statistics to number
$table = new xmldb_table('quiz_question_statistics');
- $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'subquestions');
+ $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'subquestions');
- /// Launch change of type for field maxgrade
+ // Launch change of type for field maxgrade
$dbman->change_field_type($table, $field);
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008081500, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008081500, 'quiz', 'statistics');
}
if ($oldversion < 2008082600) {
- /// Define table quiz_question_response_stats to be created
+ // Define table quiz_question_response_stats to be created
$table = new xmldb_table('quiz_question_response_stats');
- /// Adding fields to table quiz_question_response_stats
- $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
- $table->add_field('quizstatisticsid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
- $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
- $table->add_field('anssubqid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null);
- $table->add_field('response', XMLDB_TYPE_TEXT, 'big', null, null, null, null);
- $table->add_field('rcount', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null);
- $table->add_field('credit', XMLDB_TYPE_NUMBER, '15, 5', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
+ // Adding fields to table quiz_question_response_stats
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+ $table->add_field('quizstatisticsid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null);
+ $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null);
+ $table->add_field('anssubqid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ null, null, null);
+ $table->add_field('response', XMLDB_TYPE_TEXT, 'big', null,
+ null, null, null);
+ $table->add_field('rcount', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ null, null, null);
+ $table->add_field('credit', XMLDB_TYPE_NUMBER, '15, 5', null,
+ XMLDB_NOTNULL, null, null);
- /// Adding keys to table quiz_question_response_stats
+ // Adding keys to table quiz_question_response_stats
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
- /// Conditionally launch create table for quiz_question_response_stats
+ // Conditionally launch create table for quiz_question_response_stats
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008082600, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008082600, 'quiz', 'statistics');
}
if ($oldversion < 2008090500) {
@@ -104,60 +154,64 @@ function xmldb_quiz_statistics_upgrade($oldversion) {
$DB->delete_records('quiz_statistics');
$DB->delete_records('quiz_question_statistics');
$DB->delete_records('quiz_question_response_stats');
- /// Define field anssubqid to be dropped from quiz_question_response_stats
+ // Define field anssubqid to be dropped from quiz_question_response_stats
$table = new xmldb_table('quiz_question_response_stats');
$field = new xmldb_field('anssubqid');
- /// Conditionally launch drop field subqid
+ // Conditionally launch drop field subqid
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
- /// Define field subqid to be added to quiz_question_response_stats
- $field = new xmldb_field('subqid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, 'questionid');
+ // Define field subqid to be added to quiz_question_response_stats
+ $field = new xmldb_field('subqid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null, 'questionid');
- /// Conditionally launch add field subqid
+ // Conditionally launch add field subqid
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
- /// Define field aid to be added to quiz_question_response_stats
- $field = new xmldb_field('aid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, 'subqid');
+ // Define field aid to be added to quiz_question_response_stats
+ $field = new xmldb_field('aid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, null, 'subqid');
- /// Conditionally launch add field aid
+ // Conditionally launch add field aid
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008090500, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008090500, 'quiz', 'statistics');
}
if ($oldversion < 2008111000) {
- //delete all cached results first
+ // Delete all cached results first
$DB->delete_records('quiz_statistics');
$DB->delete_records('quiz_question_statistics');
$DB->delete_records('quiz_question_response_stats');
- /// Define field anssubqid to be dropped from quiz_question_response_stats
+ // Define field anssubqid to be dropped from quiz_question_response_stats
$table = new xmldb_table('quiz_question_statistics');
- /// Define field subqid to be added to quiz_question_response_stats
- $field = new xmldb_field('negcovar', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'effectiveweight');
+ // Define field subqid to be added to quiz_question_response_stats
+ $field = new xmldb_field('negcovar', XMLDB_TYPE_INTEGER, '2', XMLDB_UNSIGNED,
+ XMLDB_NOTNULL, null, '0', 'effectiveweight');
- /// Conditionally launch add field subqid
+ // Conditionally launch add field subqid
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008111000, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008111000, 'quiz', 'statistics');
}
if ($oldversion < 2008112100) {
- $DB->set_field('quiz_report', 'capability', 'quizreport/statistics:view', array('name'=>'statistics'));
+ $DB->set_field($quizreportstablename, 'capability', 'quizreport/statistics:view',
+ array('name'=>'statistics'));
- /// statistics savepoint reached
- upgrade_plugin_savepoint(true, 2008112100, 'quizreport', 'statistics');
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2008112100, 'quiz', 'statistics');
}
if ($oldversion < 2008112101) {
@@ -165,27 +219,33 @@ function xmldb_quiz_statistics_upgrade($oldversion) {
$table = new xmldb_table('quiz_statistics');
// Change of sign for field firstattemptsavg
- $field = new xmldb_field('firstattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 'allattemptscount');
+ $field = new xmldb_field('firstattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null,
+ null, null, null, 'allattemptscount');
$dbman->change_field_unsigned($table, $field);
// Change of sign for field allattemptsavg
- $field = new xmldb_field('allattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 'firstattemptsavg');
+ $field = new xmldb_field('allattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null,
+ null, null, null, 'firstattemptsavg');
$dbman->change_field_unsigned($table, $field);
// Change of sign for field median
- $field = new xmldb_field('median', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 'allattemptsavg');
+ $field = new xmldb_field('median', XMLDB_TYPE_NUMBER, '15, 5', null,
+ null, null, null, 'allattemptsavg');
$dbman->change_field_unsigned($table, $field);
// Change of sign for field standarddeviation
- $field = new xmldb_field('standarddeviation', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 'median');
+ $field = new xmldb_field('standarddeviation', XMLDB_TYPE_NUMBER, '15, 5', null,
+ null, null, null, 'median');
$dbman->change_field_unsigned($table, $field);
// Change of sign for field errorratio
- $field = new xmldb_field('errorratio', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null, 'cic');
+ $field = new xmldb_field('errorratio', XMLDB_TYPE_NUMBER, '15, 10', null,
+ null, null, null, 'cic');
$dbman->change_field_unsigned($table, $field);
// Change of sign for field standarderror
- $field = new xmldb_field('standarderror', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null, 'errorratio');
+ $field = new xmldb_field('standarderror', XMLDB_TYPE_NUMBER, '15, 10', null,
+ null, null, null, 'errorratio');
$dbman->change_field_unsigned($table, $field);
// statistics savepoint reached
@@ -197,19 +257,23 @@ function xmldb_quiz_statistics_upgrade($oldversion) {
$table = new xmldb_table('quiz_question_statistics');
// Change of sign for field effectiveweight
- $field = new xmldb_field('effectiveweight', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 's');
+ $field = new xmldb_field('effectiveweight', XMLDB_TYPE_NUMBER, '15, 5', null,
+ null, null, null, 's');
$dbman->change_field_unsigned($table, $field);
// Change of sign for field sd
- $field = new xmldb_field('sd', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null, 'discriminativeefficiency');
+ $field = new xmldb_field('sd', XMLDB_TYPE_NUMBER, '15, 10', null,
+ null, null, null, 'discriminativeefficiency');
$dbman->change_field_unsigned($table, $field);
// Change of sign for field facility
- $field = new xmldb_field('facility', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null, 'sd');
+ $field = new xmldb_field('facility', XMLDB_TYPE_NUMBER, '15, 10', null,
+ null, null, null, 'sd');
$dbman->change_field_unsigned($table, $field);
// Change of sign for field maxgrade
- $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'subquestions');
+ $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'subquestions');
$dbman->change_field_unsigned($table, $field);
// statistics savepoint reached
@@ -221,13 +285,101 @@ function xmldb_quiz_statistics_upgrade($oldversion) {
$table = new xmldb_table('quiz_question_response_stats');
// Change of sign for field credit
- $field = new xmldb_field('credit', XMLDB_TYPE_NUMBER, '15, 5', null, XMLDB_NOTNULL, null, null, 'rcount');
+ $field = new xmldb_field('credit', XMLDB_TYPE_NUMBER, '15, 5', null,
+ XMLDB_NOTNULL, null, null, 'rcount');
$dbman->change_field_unsigned($table, $field);
// statistics savepoint reached
upgrade_plugin_savepoint(true, 2008112103, 'quiz', 'statistics');
}
+ if ($oldversion < 2010031700) {
+
+ // Define field randomguessscore to be added to quiz_question_statistics
+ $table = new xmldb_table('quiz_question_statistics');
+ $field = new xmldb_field('randomguessscore', XMLDB_TYPE_NUMBER, '12, 7', null,
+ null, null, null, 'positions');
+
+ // Conditionally launch add field randomguessscore
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2010031700, 'quiz', 'statistics');
+ }
+
+ if ($oldversion < 2010032400) {
+
+ // Define field slot to be added to quiz_question_statistics
+ $table = new xmldb_table('quiz_question_statistics');
+ $field = new xmldb_field('slot', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null,
+ null, null, 'questionid');
+
+ // Conditionally launch add field slot
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2010032400, 'quiz', 'statistics');
+ }
+
+ if ($oldversion < 2010032401) {
+
+ // Delete all cached data
+ $DB->delete_records('quiz_question_response_stats');
+ $DB->delete_records('quiz_question_statistics');
+ $DB->delete_records('quiz_statistics');
+
+ // Rename field maxgrade on table quiz_question_statistics to maxmark
+ $table = new xmldb_table('quiz_question_statistics');
+ $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '12, 7', XMLDB_UNSIGNED,
+ null, null, null, 'subquestions');
+
+ // Launch rename field maxmark
+ $dbman->rename_field($table, $field, 'maxmark');
+
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2010032401, 'quiz', 'statistics');
+ }
+
+ if ($oldversion < 2010062200) {
+
+ // Changing nullability of field aid on table quiz_question_response_stats to null
+ $table = new xmldb_table('quiz_question_response_stats');
+ $field = new xmldb_field('aid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED,
+ null, null, null, 'subqid');
+
+ // Launch change of nullability for field aid
+ $dbman->change_field_notnull($table, $field);
+
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2010062200, 'quiz', 'statistics');
+ }
+
+ if ($oldversion < 2010070301) {
+
+ // Changing type of field maxmark on table quiz_question_statistics to number
+ $table = new xmldb_table('quiz_question_statistics');
+ $field = new xmldb_field('maxmark', XMLDB_TYPE_NUMBER, '12, 7', XMLDB_UNSIGNED,
+ null, null, null, 'subquestions');
+
+ // Launch change of type for field maxmark
+ $dbman->change_field_type($table, $field);
+
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2010070301, 'quiz', 'statistics');
+ }
+
+ if ($oldversion < 2011021500) {
+ $DB->set_field($quizreportstablename, 'capability', 'quiz/statistics:view',
+ array('name' => 'statistics'));
+
+ // statistics savepoint reached
+ upgrade_plugin_savepoint(true, 2011021500, 'quiz', 'statistics');
+ }
+
return true;
}
diff --git a/mod/quiz/report/statistics/lang/en/quiz_statistics.php b/mod/quiz/report/statistics/lang/en/quiz_statistics.php
index c64e53556dc..2efbe23aa7a 100644
--- a/mod/quiz/report/statistics/lang/en/quiz_statistics.php
+++ b/mod/quiz/report/statistics/lang/en/quiz_statistics.php
@@ -1,5 +1,4 @@
lastcalculated} ago there have been {$a->count} attempts since then.';
$string['median'] = 'Median grade (for {$a})';
+$string['modelresponse'] = 'Model response';
$string['negcovar'] = 'Negative covariance of grade with total attempt grade';
$string['negcovar_help'] = 'This question\'s grade for this set of attempts on the quiz varies in an opposite way to the overall attempt grade. This means overall attempt grade tends to be below average when the grade for this question is above average and vice-versa.
@@ -73,6 +77,7 @@ $string['questioninformation'] = 'Question information';
$string['questionname'] = 'Question name';
$string['questionnumber'] = 'Q#';
$string['questionstatistics'] = 'Question statistics';
+$string['questionstatsfilename'] = 'questionstats';
$string['questiontype'] = 'Question type';
$string['quizinformation'] = 'Quiz information';
$string['quizname'] = 'Quiz name';
@@ -80,7 +85,7 @@ $string['quizoverallstatistics'] = 'Quiz overall statistics';
$string['quizstructureanalysis'] = 'Quiz structure analysis';
$string['random_guess_score'] = 'Random guess score';
$string['recalculatenow'] = 'Recalculate now';
-$string['response'] = 'Answer';
+$string['response'] = 'Response';
$string['skewness'] = 'Score distribution skewness (for {$a})';
$string['standarddeviation'] = 'Standard deviation (for {$a})';
$string['standarddeviationq'] = 'Standard deviation';
diff --git a/mod/quiz/report/statistics/qstats.php b/mod/quiz/report/statistics/qstats.php
index 60db1a7c25e..857686eb00c 100644
--- a/mod/quiz/report/statistics/qstats.php
+++ b/mod/quiz/report/statistics/qstats.php
@@ -1,299 +1,406 @@
.
+
+/**
+ * Quiz statistics report calculations class.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * This class has methods to compute the question statistics from the raw data.
+ *
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_statistics_question_stats {
+ public $questions;
+ public $subquestions = array();
+
+ protected $s;
+ protected $summarksavg;
+ protected $allattempts;
+
+ /** @var mixed states from which to calculate stats - iteratable. */
+ protected $lateststeps;
+
+ protected $sumofmarkvariance = 0;
+ protected $randomselectors = array();
+
/**
- * @var mixed states from which to calculate stats - iteratable.
+ * Constructor.
+ * @param $questions the questions.
+ * @param $s the number of attempts included in the stats.
+ * @param $summarksavg the average attempt summarks.
*/
- var $states;
-
- var $sumofgradevariance = 0;
- var $questions;
- var $subquestions = array();
- var $randomselectors = array();
- var $responses = array();
-
- function qstats($questions, $s, $sumgradesavg){
+ public function __construct($questions, $s, $summarksavg) {
$this->s = $s;
- $this->sumgradesavg = $sumgradesavg;
+ $this->summarksavg = $summarksavg;
- foreach (array_keys($questions) as $qid){
- $questions[$qid]->_stats = $this->stats_init_object();
+ foreach ($questions as $slot => $question) {
+ $question->_stats = $this->make_blank_question_stats();
+ $question->_stats->questionid = $question->id;
+ $question->_stats->slot = $slot;
}
+
$this->questions = $questions;
}
- function stats_init_object(){
- $statsinit = new stdClass();
- $statsinit->s = 0;
- $statsinit->totalgrades = 0;
- $statsinit->totalothergrades = 0;
- $statsinit->gradevariancesum = 0;
- $statsinit->othergradevariancesum = 0;
- $statsinit->covariancesum = 0;
- $statsinit->covariancemaxsum = 0;
- $statsinit->subquestion = false;
- $statsinit->subquestions = '';
- $statsinit->covariancewithoverallgradesum = 0;
- $statsinit->gradearray = array();
- $statsinit->othergradesarray = array();
- return $statsinit;
+
+ /**
+ * @return object ready to hold all the question statistics.
+ */
+ protected function make_blank_question_stats() {
+ $stats = new stdClass();
+ $stats->slot = null;
+ $stats->s = 0;
+ $stats->totalmarks = 0;
+ $stats->totalothermarks = 0;
+ $stats->markvariancesum = 0;
+ $stats->othermarkvariancesum = 0;
+ $stats->covariancesum = 0;
+ $stats->covariancemaxsum = 0;
+ $stats->subquestion = false;
+ $stats->subquestions = '';
+ $stats->covariancewithoverallmarksum = 0;
+ $stats->randomguessscore = null;
+ $stats->markarray = array();
+ $stats->othermarksarray = array();
+ return $stats;
}
- function get_records($quizid, $currentgroup, $groupstudents, $allattempts){
+
+ /**
+ * Load the data that will be needed to perform the calculations.
+ *
+ * @param int $quizid the quiz id.
+ * @param int $currentgroup the current group. 0 for none.
+ * @param array $groupstudents students in this group.
+ * @param bool $allattempts use all attempts, or just first attempts.
+ */
+ public function load_step_data($quizid, $currentgroup, $groupstudents, $allattempts) {
global $DB;
- list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($this->questions), SQL_PARAMS_NAMED, 'q');
- list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $allattempts);
- $sql = 'SELECT qs.id, ' .
- 'qs.question, ' .
- 'qa.sumgrades, ' .
- 'qs.grade, ' .
- 'qs.answer ' .
- 'FROM ' .
- '{question_sessions} qns, ' .
- '{question_states} qs, '.
- $fromqa.' '.
- 'WHERE ' .$whereqa.
- 'AND qs.question '.$qsql.' '.
- 'AND qns.attemptid = qa.uniqueid '.
- 'AND qns.newgraded = qs.id';
- $this->states = $DB->get_records_sql($sql, $qaparams + $qparams);
- if ($this->states === false){
- print_error('errorstatisticsquestions', 'quiz_statistics');
- }
+
+ $this->allattempts = $allattempts;
+
+ list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($this->questions),
+ SQL_PARAMS_NAMED, 'q');
+ list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
+ $quizid, $currentgroup, $groupstudents, $allattempts, false);
+
+ $this->lateststeps = $DB->get_records_sql("
+ SELECT
+ qas.id,
+ quiza.sumgrades,
+ qa.questionid,
+ qa.slot,
+ qa.maxmark,
+ qas.fraction * qa.maxmark as mark
+
+ FROM $fromqa
+ JOIN {question_attempts} qa ON qa.questionusageid = quiza.uniqueid
+ JOIN (
+ SELECT questionattemptid, MAX(id) AS latestid
+ FROM {question_attempt_steps}
+ GROUP BY questionattemptid
+ ) lateststepid ON lateststepid.questionattemptid = qa.id
+ JOIN {question_attempt_steps} qas ON qas.id = lateststepid.latestid
+
+ WHERE
+ qa.slot $qsql AND
+ $whereqa", $qparams + $qaparams);
}
- function _initial_states_walker($state, &$stats, $positionstat = true){
- $stats->s++;
- $stats->totalgrades += $state->grade;
- if ($positionstat){
- $stats->totalothergrades += $state->sumgrades - $state->grade;
- } else {
- $stats->totalothergrades += $state->sumgrades;
- }
- //need to sort these to calculate max covariance :
- $stats->gradearray[] = $state->grade;
- if ($positionstat){
- $stats->othergradesarray[] = $state->sumgrades - $state->grade;
- } else {
- $stats->othergradesarray[] = $state->sumgrades;
+ public function compute_statistics() {
+ set_time_limit(0);
+
+ $subquestionstats = array();
+
+ // Compute the statistics of position, and for random questions, work
+ // out which questions appear in which positions.
+ foreach ($this->lateststeps as $step) {
+ $this->initial_steps_walker($step, $this->questions[$step->slot]->_stats);
+
+ // If this is a random question what is the real item being used?
+ if ($step->questionid != $this->questions[$step->slot]->id) {
+ if (!isset($subquestionstats[$step->questionid])) {
+ $subquestionstats[$step->questionid] = $this->make_blank_question_stats();
+ $subquestionstats[$step->questionid]->questionid = $step->questionid;
+ $subquestionstats[$step->questionid]->allattempts = $this->allattempts;
+ $subquestionstats[$step->questionid]->usedin = array();
+ $subquestionstats[$step->questionid]->subquestion = true;
+ $subquestionstats[$step->questionid]->differentweights = false;
+ $subquestionstats[$step->questionid]->maxmark = $step->maxmark;
+ } else if ($subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
+ $subquestionstats[$step->questionid]->differentweights = true;
+ }
+
+ $this->initial_steps_walker($step,
+ $subquestionstats[$step->questionid], false);
+
+ $number = $this->questions[$step->slot]->number;
+ $subquestionstats[$step->questionid]->usedin[$number] = $number;
+
+ $randomselectorstring = $this->questions[$step->slot]->category .
+ '/' . $this->questions[$step->slot]->questiontext;
+ if (!isset($this->randomselectors[$randomselectorstring])) {
+ $this->randomselectors[$randomselectorstring] = array();
+ }
+ $this->randomselectors[$randomselectorstring][$step->questionid] =
+ $step->questionid;
+ }
}
- }
-
- function _secondary_states_walker($state, &$stats){
- $gradedifference = ($state->grade - $stats->gradeaverage);
- if ($stats->subquestion){
- $othergradedifference = $state->sumgrades - $stats->othergradeaverage;
- } else {
- $othergradedifference = (($state->sumgrades - $state->grade) - $stats->othergradeaverage);
+ foreach ($this->randomselectors as $key => $notused) {
+ ksort($this->randomselectors[$key]);
}
- $overallgradedifference = $state->sumgrades - $this->sumgradesavg;
- $sortedgradedifference = (array_shift($stats->gradearray) - $stats->gradeaverage);
- $sortedothergradedifference = (array_shift($stats->othergradesarray) - $stats->othergradeaverage);
- $stats->gradevariancesum += pow($gradedifference,2);
- $stats->othergradevariancesum += pow($othergradedifference,2);
- $stats->covariancesum += $gradedifference * $othergradedifference;
- $stats->covariancemaxsum += $sortedgradedifference * $sortedothergradedifference;
- $stats->covariancewithoverallgradesum += $gradedifference * $overallgradedifference;
- }
+ // Compute the statistics of question id, if we need any.
+ $this->subquestions = question_load_questions(array_keys($subquestionstats));
+ foreach ($this->subquestions as $qid => $subquestion) {
+ $subquestion->_stats = $subquestionstats[$qid];
+ $subquestion->maxmark = $subquestion->_stats->maxmark;
+ $subquestion->_stats->randomguessscore = $this->get_random_guess_score($subquestion);
- function add_response_detail_to_array($responsedetail){
- $responsedetail->rcount = 1;
- if (isset($this->responses[$responsedetail->subqid])){
- if (isset($this->responses[$responsedetail->subqid][$responsedetail->aid])){
- if (isset($this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response])){
- $this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response]->rcount++;
+ $this->initial_question_walker($subquestion->_stats);
+
+ if ($subquestionstats[$qid]->differentweights) {
+ // TODO output here really sucks, but throwing is too severe.
+ global $OUTPUT;
+ echo $OUTPUT->notification(
+ get_string('erroritemappearsmorethanoncewithdifferentweight',
+ 'quiz_statistics', $this->subquestions[$qid]->name));
+ }
+
+ if ($subquestion->_stats->usedin) {
+ sort($subquestion->_stats->usedin, SORT_NUMERIC);
+ $subquestion->_stats->positions = implode(',', $subquestion->_stats->usedin);
+ } else {
+ $subquestion->_stats->positions = '';
+ }
+ }
+
+ // Finish computing the averages, and put the subquestion data into the
+ // corresponding questions.
+
+ // This cannot be a foreach loop because we need to have both
+ // $question and $nextquestion available, but apart from that it is
+ // foreach ($this->questions as $qid => $question) {
+ reset($this->questions);
+ while (list($slot, $question) = each($this->questions)) {
+ $nextquestion = current($this->questions);
+ $question->_stats->allattempts = $this->allattempts;
+ $question->_stats->positions = $question->number;
+ $question->_stats->maxmark = $question->maxmark;
+ $question->_stats->randomguessscore = $this->get_random_guess_score($question);
+
+ $this->initial_question_walker($question->_stats);
+
+ if ($question->qtype == 'random') {
+ $randomselectorstring = $question->category.'/'.$question->questiontext;
+ if ($nextquestion && $nextquestion->qtype == 'random') {
+ $nextrandomselectorstring = $nextquestion->category . '/' .
+ $nextquestion->questiontext;
+ if ($randomselectorstring == $nextrandomselectorstring) {
+ continue; // Next loop iteration
+ }
+ }
+ if (isset($this->randomselectors[$randomselectorstring])) {
+ $question->_stats->subquestions = implode(',',
+ $this->randomselectors[$randomselectorstring]);
+ }
+ }
+ }
+
+ // Go through the records one more time
+ foreach ($this->lateststeps as $step) {
+ $this->secondary_steps_walker($step,
+ $this->questions[$step->slot]->_stats);
+
+ if ($this->questions[$step->slot]->qtype == 'random') {
+ $this->secondary_steps_walker($step,
+ $this->subquestions[$step->questionid]->_stats);
+ }
+ }
+
+ $sumofcovariancewithoverallmark = 0;
+ foreach ($this->questions as $slot => $question) {
+ $this->secondary_question_walker($question->_stats);
+
+ $this->sumofmarkvariance += $question->_stats->markvariance;
+
+ if ($question->_stats->covariancewithoverallmark >= 0) {
+ $sumofcovariancewithoverallmark +=
+ sqrt($question->_stats->covariancewithoverallmark);
+ $question->_stats->negcovar = 0;
+ } else {
+ $question->_stats->negcovar = 1;
+ }
+ }
+
+ foreach ($this->subquestions as $subquestion) {
+ $this->secondary_question_walker($subquestion->_stats);
+ }
+
+ foreach ($this->questions as $question) {
+ if ($sumofcovariancewithoverallmark) {
+ if ($question->_stats->negcovar) {
+ $question->_stats->effectiveweight = null;
} else {
- $this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response] = $responsedetail;
+ $question->_stats->effectiveweight = 100 *
+ sqrt($question->_stats->covariancewithoverallmark) /
+ $sumofcovariancewithoverallmark;
}
} else {
- $this->responses[$responsedetail->subqid][$responsedetail->aid] = array($responsedetail->response => $responsedetail);
+ $question->_stats->effectiveweight = null;
}
- } else {
- $this->responses[$responsedetail->subqid] = array();
- $this->responses[$responsedetail->subqid][$responsedetail->aid] = array($responsedetail->response => $responsedetail);
}
}
/**
- * Get the data for the individual question response analysis table.
+ * Update $stats->totalmarks, $stats->markarray, $stats->totalothermarks
+ * and $stats->othermarksarray to include another state.
+ *
+ * @param object $step the state to add to the statistics.
+ * @param object $stats the question statistics we are accumulating.
+ * @param bool $positionstat whether this is a statistic of position of question.
*/
- function _process_actual_responses($question, $state){
- global $QTYPES;
- if ($question->qtype != 'random' &&
- $QTYPES[$question->qtype]->show_analysis_of_responses()){
- $restoredstate = clone($state);
- restore_question_state($question, $restoredstate);
- $responsedetails = $QTYPES[$question->qtype]->get_actual_response_details($question, $restoredstate);
- foreach ($responsedetails as $responsedetail){
- $responsedetail->questionid = $question->id;
- $this->add_response_detail_to_array($responsedetail);
- }
+ protected function initial_steps_walker($step, $stats, $positionstat = true) {
+ $stats->s++;
+ $stats->totalmarks += $step->mark;
+ $stats->markarray[] = $step->mark;
+
+ if ($positionstat) {
+ $stats->totalothermarks += $step->sumgrades - $step->mark;
+ $stats->othermarksarray[] = $step->sumgrades - $step->mark;
+
+ } else {
+ $stats->totalothermarks += $step->sumgrades;
+ $stats->othermarksarray[] = $step->sumgrades;
}
}
- function _initial_question_walker(&$stats){
- $stats->gradeaverage = $stats->totalgrades / $stats->s;
- if ($stats->maxgrade!=0){
- $stats->facility = $stats->gradeaverage / $stats->maxgrade;
+ /**
+ * Perform some computations on the per-question statistics calculations after
+ * we have been through all the states.
+ *
+ * @param object $stats quetsion stats to update.
+ */
+ protected function initial_question_walker($stats) {
+ $stats->markaverage = $stats->totalmarks / $stats->s;
+
+ if ($stats->maxmark != 0) {
+ $stats->facility = $stats->markaverage / $stats->maxmark;
} else {
$stats->facility = null;
}
- $stats->othergradeaverage = $stats->totalothergrades / $stats->s;
- sort($stats->gradearray, SORT_NUMERIC);
- sort($stats->othergradesarray, SORT_NUMERIC);
+
+ $stats->othermarkaverage = $stats->totalothermarks / $stats->s;
+
+ sort($stats->markarray, SORT_NUMERIC);
+ sort($stats->othermarksarray, SORT_NUMERIC);
}
- function _secondary_question_walker(&$stats){
- if ($stats->s > 1){
- $stats->gradevariance = $stats->gradevariancesum / ($stats->s -1);
- $stats->othergradevariance = $stats->othergradevariancesum / ($stats->s -1);
- $stats->covariance = $stats->covariancesum / ($stats->s -1);
- $stats->covariancemax = $stats->covariancemaxsum / ($stats->s -1);
- $stats->covariancewithoverallgrade = $stats->covariancewithoverallgradesum / ($stats->s-1);
- $stats->sd = sqrt($stats->gradevariancesum / ($stats->s -1));
+
+ /**
+ * Now we know the averages, accumulate the date needed to compute the higher
+ * moments of the question scores.
+ *
+ * @param object $step the state to add to the statistics.
+ * @param object $stats the question statistics we are accumulating.
+ * @param bool $positionstat whether this is a statistic of position of question.
+ */
+ protected function secondary_steps_walker($step, $stats) {
+ $markdifference = $step->mark - $stats->markaverage;
+ if ($stats->subquestion) {
+ $othermarkdifference = $step->sumgrades - $stats->othermarkaverage;
} else {
- $stats->gradevariance = null;
- $stats->othergradevariance = null;
+ $othermarkdifference = $step->sumgrades - $step->mark -
+ $stats->othermarkaverage;
+ }
+ $overallmarkdifference = $step->sumgrades - $this->summarksavg;
+
+ $sortedmarkdifference = array_shift($stats->markarray) - $stats->markaverage;
+ $sortedothermarkdifference = array_shift($stats->othermarksarray) -
+ $stats->othermarkaverage;
+
+ $stats->markvariancesum += pow($markdifference, 2);
+ $stats->othermarkvariancesum += pow($othermarkdifference, 2);
+ $stats->covariancesum += $markdifference * $othermarkdifference;
+ $stats->covariancemaxsum += $sortedmarkdifference * $sortedothermarkdifference;
+ $stats->covariancewithoverallmarksum += $markdifference * $overallmarkdifference;
+ }
+
+ /**
+ * Perform more per-question statistics calculations.
+ *
+ * @param object $stats quetsion stats to update.
+ */
+ protected function secondary_question_walker($stats) {
+ if ($stats->s > 1) {
+ $stats->markvariance = $stats->markvariancesum / ($stats->s - 1);
+ $stats->othermarkvariance = $stats->othermarkvariancesum / ($stats->s - 1);
+ $stats->covariance = $stats->covariancesum / ($stats->s - 1);
+ $stats->covariancemax = $stats->covariancemaxsum / ($stats->s - 1);
+ $stats->covariancewithoverallmark = $stats->covariancewithoverallmarksum /
+ ($stats->s - 1);
+ $stats->sd = sqrt($stats->markvariancesum / ($stats->s - 1));
+
+ } else {
+ $stats->markvariance = null;
+ $stats->othermarkvariance = null;
$stats->covariance = null;
$stats->covariancemax = null;
- $stats->covariancewithoverallgrade = null;
+ $stats->covariancewithoverallmark = null;
$stats->sd = null;
}
- //avoid divide by zero
- if ($stats->gradevariance * $stats->othergradevariance){
- $stats->discriminationindex = 100*$stats->covariance
- / sqrt($stats->gradevariance * $stats->othergradevariance);
+
+ if ($stats->markvariance * $stats->othermarkvariance) {
+ $stats->discriminationindex = 100 * $stats->covariance /
+ sqrt($stats->markvariance * $stats->othermarkvariance);
} else {
$stats->discriminationindex = null;
}
- if ($stats->covariancemax){
- $stats->discriminativeefficiency = 100*$stats->covariance / $stats->covariancemax;
+
+ if ($stats->covariancemax) {
+ $stats->discriminativeefficiency = 100 * $stats->covariance /
+ $stats->covariancemax;
} else {
$stats->discriminativeefficiency = null;
}
}
- function process_states(){
- global $DB, $OUTPUT;
- set_time_limit(0);
- $subquestionstats = array();
- foreach ($this->states as $state){
- $this->_initial_states_walker($state, $this->questions[$state->question]->_stats);
- //if this is a random question what is the real item being used?
- if ($this->questions[$state->question]->qtype == 'random'){
- if ($realstate = question_get_real_state($state)){
- if (!isset($subquestionstats[$realstate->question])){
- $subquestionstats[$realstate->question] = $this->stats_init_object();
- $subquestionstats[$realstate->question]->usedin = array();
- $subquestionstats[$realstate->question]->subquestion = true;
- $subquestionstats[$realstate->question]->differentweights = false;
- $subquestionstats[$realstate->question]->maxgrade = $this->questions[$state->question]->maxgrade;
- } else if ($subquestionstats[$realstate->question]->maxgrade != $this->questions[$state->question]->maxgrade){
- $subquestionstats[$realstate->question]->differentweights = true;
- }
- $this->_initial_states_walker($realstate, $subquestionstats[$realstate->question], false);
- $number = $this->questions[$state->question]->number;
- $subquestionstats[$realstate->question]->usedin[$number] = $number;
- $randomselectorstring = $this->questions[$state->question]->category.'/'.$this->questions[$state->question]->questiontext;
- if (!isset($this->randomselectors[$randomselectorstring])){
- $this->randomselectors[$randomselectorstring] = array();
- }
- $this->randomselectors[$randomselectorstring][$realstate->question] = $realstate->question;
- }
- }
- }
- foreach ($this->randomselectors as $key => $randomselector){
- ksort($this->randomselectors[$key]);
- }
- $this->subquestions = question_load_questions(array_keys($subquestionstats));
- foreach (array_keys($this->subquestions) as $qid){
- $this->subquestions[$qid]->_stats = $subquestionstats[$qid];
- $this->subquestions[$qid]->_stats->questionid = $qid;
- $this->subquestions[$qid]->maxgrade = $this->subquestions[$qid]->_stats->maxgrade;
- $this->_initial_question_walker($this->subquestions[$qid]->_stats);
- if ($subquestionstats[$qid]->differentweights){
- echo $OUTPUT->notification(get_string('erroritemappearsmorethanoncewithdifferentweight', 'quiz_statistics', $this->subquestions[$qid]->name));
- }
- if ($this->subquestions[$qid]->_stats->usedin){
- sort($this->subquestions[$qid]->_stats->usedin, SORT_NUMERIC);
- $this->subquestions[$qid]->_stats->positions = join($this->subquestions[$qid]->_stats->usedin, ',');
- } else {
- $this->subquestions[$qid]->_stats->positions = '';
- }
- }
- reset($this->questions);
- do{
- list($qid, $question) = each($this->questions);
- $nextquestion = current($this->questions);
- $this->questions[$qid]->_stats->questionid = $qid;
- $this->questions[$qid]->_stats->positions = $this->questions[$qid]->number;
- $this->questions[$qid]->_stats->maxgrade = $question->maxgrade;
- $this->_initial_question_walker($this->questions[$qid]->_stats);
- if ($question->qtype == 'random'){
- $randomselectorstring = $question->category.'/'.$question->questiontext;
- if ($nextquestion){
- $nextrandomselectorstring = $nextquestion->category.'/'.$nextquestion->questiontext;
- if ($nextquestion->qtype == 'random' && $randomselectorstring == $nextrandomselectorstring){
- continue;//next loop iteration
- }
- }
- if (isset($this->randomselectors[$randomselectorstring])){
- $question->_stats->subquestions = join($this->randomselectors[$randomselectorstring], ',');
- }
- }
- } while ($nextquestion);
- //go through the records one more time
- foreach ($this->states as $state){
- $this->_secondary_states_walker($state, $this->questions[$state->question]->_stats);
- if ($this->questions[$state->question]->qtype == 'random'){
- if ($realstate = question_get_real_state($state)){
- $this->_secondary_states_walker($realstate, $this->subquestions[$realstate->question]->_stats);
- }
- }
- }
- $sumofcovariancewithoverallgrade = 0;
- foreach (array_keys($this->questions) as $qid){
- $this->_secondary_question_walker($this->questions[$qid]->_stats);
- $this->sumofgradevariance += $this->questions[$qid]->_stats->gradevariance;
- if ($this->questions[$qid]->_stats->covariancewithoverallgrade >= 0){
- $sumofcovariancewithoverallgrade += sqrt($this->questions[$qid]->_stats->covariancewithoverallgrade);
- $this->questions[$qid]->_stats->negcovar = 0;
- } else {
- $this->questions[$qid]->_stats->negcovar = 1;
- }
- }
- foreach (array_keys($this->subquestions) as $qid){
- $this->_secondary_question_walker($this->subquestions[$qid]->_stats);
- }
- foreach (array_keys($this->questions) as $qid){
- if ($sumofcovariancewithoverallgrade){
- if ($this->questions[$qid]->_stats->negcovar){
- $this->questions[$qid]->_stats->effectiveweight = null;
- } else {
- $this->questions[$qid]->_stats->effectiveweight = 100 * sqrt($this->questions[$qid]->_stats->covariancewithoverallgrade)
- / $sumofcovariancewithoverallgrade;
- }
- } else {
- $this->questions[$qid]->_stats->effectiveweight = null;
- }
- }
+ /**
+ * @param object $questiondata
+ * @return number the random guess score for this question.
+ */
+ protected function get_random_guess_score($questiondata) {
+ return question_bank::get_qtype(
+ $questiondata->qtype, false)->get_random_guess_score($questiondata);
}
- function process_responses(){
- foreach ($this->states as $state){
- if ($this->questions[$state->question]->qtype == 'random'){
- if ($realstate = question_get_real_state($state)){
- $this->_process_actual_responses($this->subquestions[$realstate->question], $realstate);
- }
- } else {
- $this->_process_actual_responses($this->questions[$state->question], $state);
- }
- }
- $this->responses = quiz_report_unindex($this->responses);
- }
/**
- * Needed by quiz stats calculations.
+ * Used when computing CIC.
+ * @return number
*/
- function sum_of_grade_variance(){
- return $this->sumofgradevariance;
+ public function get_sum_of_mark_variance() {
+ return $this->sumofmarkvariance;
}
}
-
diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php
index 3ba19b0e66e..87a89adb1a3 100644
--- a/mod/quiz/report/statistics/report.php
+++ b/mod/quiz/report/statistics/report.php
@@ -1,672 +1,1078 @@
.
+
/**
- * This script calculates various statistics about student attempts
+ * Quiz statistics report class.
*
- * @author Martin Dougiamas, Jamie Pratt, Tim Hunt and others.
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
- **/
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
-define('QUIZ_REPORT_TIME_TO_CACHE_STATS', MINSECS * 15);
-require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_form.php');
-require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_table.php');
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php');
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/responseanalysis.php');
+
+
+/**
+ * The quiz statistics report provides summary information about each question in
+ * a quiz, compared to the whole quiz. It also provides a drill-down to more
+ * detailed information about each question.
+ *
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class quiz_statistics_report extends quiz_default_report {
+ /** @var integer Time after which statistics are automatically recomputed. */
+ const TIME_TO_CACHE_STATS = 900; // 15 minutes
- /**
- * @var object instance of table class used for main questions stats table.
- */
- var $table;
+ /** @var object instance of table class used for main questions stats table. */
+ protected $table;
/**
* Display the report.
*/
- function display($quiz, $cm, $course) {
- global $CFG, $DB, $QTYPES, $OUTPUT, $PAGE;
+ public function display($quiz, $cm, $course) {
+ global $CFG, $DB, $OUTPUT, $PAGE;
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
+ // Work out the display options.
$download = optional_param('download', '', PARAM_ALPHA);
$everything = optional_param('everything', 0, PARAM_BOOL);
$recalculate = optional_param('recalculate', 0, PARAM_BOOL);
- //pass the question id for detailed analysis question
+ // A qid paramter indicates we should display the detailed analysis of a question.
$qid = optional_param('qid', 0, PARAM_INT);
+ $slot = optional_param('slot', 0, PARAM_INT);
+
$pageoptions = array();
$pageoptions['id'] = $cm->id;
$pageoptions['mode'] = 'statistics';
- if ($qid) {
- $pageoptions['qid'] = $qid;
- }
-
- $questions = quiz_report_load_questions($quiz);
- // Load the question type specific information
- if (!get_question_options($questions)) {
- print_error('cannotloadquestion', 'question');
- }
$reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
- $mform = new mod_quiz_report_statistics($reporturl);
- if ($fromform = $mform->get_data()){
+ $mform = new quiz_statistics_statistics_settings_form($reporturl);
+ if ($fromform = $mform->get_data()) {
$useallattempts = $fromform->useallattempts;
- if ($fromform->useallattempts){
- set_user_preference('quiz_report_statistics_useallattempts', $fromform->useallattempts);
+ if ($fromform->useallattempts) {
+ set_user_preference('quiz_report_statistics_useallattempts',
+ $fromform->useallattempts);
} else {
unset_user_preference('quiz_report_statistics_useallattempts');
}
+
} else {
$useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0);
}
- /// find out current groups mode
+ // Find out current groups mode
+ $groupmode = groups_get_activity_groupmode($cm);
$currentgroup = groups_get_activity_group($cm, true);
+ $nostudentsingroup = false; // True if a group is selected and there is no one in it.
+ if (empty($currentgroup)) {
+ $currentgroup = 0;
+ $groupstudents = array();
- $nostudentsingroup = false;//true if a group is selected and their is noeone in it.
- if (!empty($currentgroup)) {
- // all users who can attempt quizzes and who are in the currently selected group
- $groupstudents = get_users_by_capability($context, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'','','','',$currentgroup,'',false);
- if (!$groupstudents){
+ } else {
+ // All users who can attempt quizzes and who are in the currently selected group
+ $groupstudents = get_users_by_capability($context,
+ array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
+ '', '', '', '', $currentgroup, '', false);
+ if (!$groupstudents) {
$nostudentsingroup = true;
}
- } else {
- $groupstudents = array();
}
+ // If recalculate was requested, handle that.
if ($recalculate && confirm_sesskey()) {
- if ($todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quiz->id, 'groupid'=> (int)$currentgroup, 'allattempts'=>$useallattempts))){
- list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
- if (!$DB->delete_records_select('quiz_statistics', "id $todeletesql", $todeleteparams)){
- print_error('errordeleting', 'quiz_statistics', '', 'quiz_statistics');
- }
- if (!$DB->delete_records_select('quiz_question_statistics', "quizstatisticsid $todeletesql", $todeleteparams)){
- print_error('errordeleting', 'quiz_statistics', '', 'quiz_question_statistics');
- }
- if (!$DB->delete_records_select('quiz_question_response_stats', "quizstatisticsid $todeletesql", $todeleteparams)){
- print_error('errordeleting', 'quiz_statistics', '', 'quiz_question_response_stats');
- }
- }
+ $this->clear_cached_data($quiz->id, $currentgroup, $useallattempts);
redirect($reporturl);
}
+ // Set up the main table.
$this->table = new quiz_report_statistics_table();
- $filename = "$course->shortname-".format_string($quiz->name,true);
- $this->table->is_downloading($download, $filename, get_string('quizstructureanalysis', 'quiz_statistics'));
+ if ($everything) {
+ $report = get_string('completestatsfilename', 'quiz_statistics');
+ } else {
+ $report = get_string('questionstatsfilename', 'quiz_statistics');
+ }
+ $filename = quiz_report_download_filename($report, $course->shortname, $quiz->name);
+ $this->table->is_downloading($download, $filename,
+ get_string('quizstructureanalysis', 'quiz_statistics'));
- list($quizstats, $questions, $subquestions, $s, $usingattemptsstring)
- = $this->quiz_questions_stats($quiz, $currentgroup, $nostudentsingroup,
- $useallattempts, $groupstudents, $questions);
+ // Load the questions.
+ $questions = quiz_report_get_significant_questions($quiz);
+ $questionids = array();
+ foreach ($questions as $question) {
+ $questionids[] = $question->id;
+ }
+ $fullquestions = question_load_questions($questionids);
+ foreach ($questions as $qno => $question) {
+ $q = $fullquestions[$question->id];
+ $q->maxmark = $question->maxmark;
+ $q->slot = $qno;
+ $q->number = $question->number;
+ $questions[$qno] = $q;
+ }
+ // Get the data to be displayed.
+ list($quizstats, $questions, $subquestions, $s) =
+ $this->get_quiz_and_questions_stats($quiz, $currentgroup,
+ $nostudentsingroup, $useallattempts, $groupstudents, $questions);
+ $quizinfo = $this->get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats);
+
+ // Set up the table, if there is data.
if ($s) {
$this->table->setup($quiz, $cm->id, $reporturl, $s);
}
- if (!$qid) {//main page
- if (!$this->table->is_downloading()) {
- // Only print headers if not asked to download data
- $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
+ // Print the page header stuff (if not downloading.
+ if (!$this->table->is_downloading()) {
+ $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
+
+ if ($groupmode) {
+ groups_print_activity_menu($cm, $reporturl->out());
+ if ($currentgroup && !$groupstudents) {
+ $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
+ }
}
- if (!$this->table->is_downloading()) {
- // Print display options
- $mform->set_data(array('useallattempts' => $useallattempts));
- $mform->display();
+
+ if (!quiz_questions_in_quiz($quiz->questions)) {
+ echo quiz_no_questions_message($quiz, $cm, $context);
+ } else if (!$this->table->is_downloading() && $s == 0) {
+ echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
}
- if (!$this->table->is_downloading() && $s == 0){
- echo $OUTPUT->heading(get_string('noattempts','quiz'));
- }
- if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
- if (!$this->table->is_downloading()) {
- groups_print_activity_menu($cm, $reporturl->out());
- echo '
';
- if ($currentgroup && !$groupstudents){
- echo $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
+
+ // Print display options form.
+ $mform->set_data(array('useallattempts' => $useallattempts));
+ $mform->display();
+ }
+
+ if ($everything) { // Implies is downloading.
+ // Overall report, then the analysis of each question.
+ $this->download_quiz_info_table($quizinfo);
+
+ if ($s) {
+ $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
+
+ if ($this->table->is_downloading() == 'xhtml') {
+ $this->output_statistics_graph($quizstats->id, $s);
+ }
+
+ foreach ($questions as $question) {
+ if (question_bank::get_qtype(
+ $question->qtype, false)->can_analyse_responses()) {
+ $this->output_individual_question_response_analysis(
+ $question, $reporturl, $quizstats);
+
+ } else if (!empty($question->_stats->subquestions)) {
+ $subitemstodisplay = explode(',', $question->_stats->subquestions);
+ foreach ($subitemstodisplay as $subitemid) {
+ $this->output_individual_question_response_analysis(
+ $subquestions[$subitemid], $reporturl, $quizstats);
+ }
}
}
}
- $this->output_quiz_info_table($course, $cm, $quiz, $quizstats, $usingattemptsstring, $currentgroup, $groupstudents, $useallattempts, $download, $reporturl, $everything);
- $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
- if (!$this->table->is_downloading() || ($everything && $this->table->is_downloading() == 'xhtml')){
- if ($s > 1){
- $imageurl = $CFG->wwwroot.'/mod/quiz/report/statistics/statistics_graph.php?id='.$quizstats->id;
- echo $OUTPUT->heading(get_string('statisticsreportgraph', 'quiz_statistics'));
- echo '';
- }
- }
- if ($this->table->is_downloading()){
- if ($everything){
- foreach ($questions as $question){
- if ($question->qtype != 'random' && $QTYPES[$question->qtype]->show_analysis_of_responses()){
- $this->output_individual_question_data($quiz, $question, $reporturl, $quizstats);
- } elseif (!empty($question->_stats->subquestions)) {
- $subitemstodisplay = explode(',', $question->_stats->subquestions);
- foreach ($subitemstodisplay as $subitemid){
- $this->output_individual_question_data($quiz, $subquestions[$subitemid], $reporturl, $quizstats);
- }
- }
- }
- $exportclassinstance =& $this->table->export_class_instance();
- } else {
- $this->table->finish_output();
- }
- }
- if ($this->table->is_downloading() && $everything){
- $exportclassinstance->finish_document();
- }
- } else {//individual question page
- $thisquestion = false;
- if (isset($questions[$qid])){
- $thisquestion = $questions[$qid];
- } else if (isset($subquestions[$qid])){
- $thisquestion = $subquestions[$qid];
- } else {
+ $this->table->export_class_instance()->finish_document();
+
+ } else if ($slot) {
+ // Report on an individual question indexed by position.
+ if (!isset($questions[$slot])) {
print_error('questiondoesnotexist', 'question');
}
- if (!$this->table->is_downloading()) {
- // Only print headers if not asked to download data
- navigation_node::override_active_url(
- new moodle_url('/mod/quiz/report.php', array('id' => $cm->id, 'mode' => 'statistics')));
- $PAGE->navbar->add($thisquestion->name);
- $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
+
+ $this->output_individual_question_data($quiz, $questions[$slot]);
+ $this->output_individual_question_response_analysis(
+ $questions[$slot], $reporturl, $quizstats);
+
+ // Back to overview link.
+ echo $OUTPUT->box('' .
+ get_string('backtoquizreport', 'quiz_statistics') . '',
+ 'boxaligncenter generalbox boxwidthnormal mdl-align');
+
+ } else if ($qid) {
+ // Report on an individual sub-question indexed questionid.
+ if (!isset($subquestions[$qid])) {
+ print_error('questiondoesnotexist', 'question');
+ }
+
+ $this->output_individual_question_data($quiz, $subquestions[$qid]);
+ $this->output_individual_question_response_analysis(
+ $subquestions[$qid], $reporturl, $quizstats);
+
+ // Back to overview link.
+ echo $OUTPUT->box('' .
+ get_string('backtoquizreport', 'quiz_statistics') . '',
+ 'boxaligncenter generalbox boxwidthnormal mdl-align');
+
+ } else if ($this->table->is_downloading()) {
+ // Downloading overview report.
+ $this->download_quiz_info_table($quizinfo);
+ $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
+ $this->table->finish_output();
+
+ } else {
+ // On-screen display of overview report.
+ echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
+ echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup,
+ $groupstudents, $useallattempts, $reporturl);
+ echo $this->everything_download_options();
+ echo $this->output_quiz_info_table($quizinfo);
+ if ($s) {
+ echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
+ $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
+ $this->output_statistics_graph($quizstats->id, $s);
}
- $this->output_individual_question_data($quiz, $thisquestion, $reporturl, $quizstats);
}
+
return true;
}
- function sort_response_details($detail1, $detail2){
- if ($detail1->credit == $detail2->credit){
- return strcmp($detail1->answer, $detail2->answer);
- }
- return ($detail1->credit > $detail2->credit) ? -1 : 1;
- }
- function sort_answers($answer1, $answer2){
- if ($answer1->rcount == $answer2->rcount){
- return strcmp($answer1->response, $answer2->response);
- } else {
- return ($answer1->rcount > $answer2->rcount)? -1 : 1;
+ /**
+ * Display the statistical and introductory information about a question.
+ * Only called when not downloading.
+ * @param object $quiz the quiz settings.
+ * @param object $question the question to report on.
+ * @param moodle_url $reporturl the URL to resisplay this report.
+ * @param object $quizstats Holds the quiz statistics.
+ */
+ protected function output_individual_question_data($quiz, $question) {
+ global $OUTPUT;
+
+ // On-screen display. Show a summary of the question's place in the quiz,
+ // and the question statistics.
+ $datumfromtable = $this->table->format_row($question);
+
+ // Set up the question info table.
+ $questioninfotable = new html_table();
+ $questioninfotable->align = array('center', 'center');
+ $questioninfotable->width = '60%';
+ $questioninfotable->attributes['class'] = 'generaltable titlesleft';
+
+ $questioninfotable->data = array();
+ $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
+ $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
+ $question->name.' '.$datumfromtable['actions']);
+ $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
+ $datumfromtable['icon'] . ' ' .
+ question_bank::get_qtype($question->qtype, false)->menu_name() . ' ' .
+ $datumfromtable['icon']);
+ $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
+ $question->_stats->positions);
+
+ // Set up the question statistics table.
+ $questionstatstable = new html_table();
+ $questionstatstable->align = array('center', 'center');
+ $questionstatstable->width = '60%';
+ $questionstatstable->attributes['class'] = 'generaltable titlesleft';
+
+ unset($datumfromtable['number']);
+ unset($datumfromtable['icon']);
+ $actions = $datumfromtable['actions'];
+ unset($datumfromtable['actions']);
+ unset($datumfromtable['name']);
+ $labels = array(
+ 's' => get_string('attempts', 'quiz_statistics'),
+ 'facility' => get_string('facility', 'quiz_statistics'),
+ 'sd' => get_string('standarddeviationq', 'quiz_statistics'),
+ 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
+ 'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
+ 'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
+ 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
+ 'discriminative_efficiency' =>
+ get_string('discriminative_efficiency', 'quiz_statistics')
+ );
+ foreach ($datumfromtable as $item => $value) {
+ $questionstatstable->data[] = array($labels[$item], $value);
}
+
+ // Display the various bits.
+ echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
+ echo html_writer::table($questioninfotable);
+
+ echo $OUTPUT->box(format_text($question->questiontext, $question->questiontextformat,
+ array('overflowdiv' => true)) . $actions,
+ 'boxaligncenter generalbox boxwidthnormal mdl-align');
+
+ echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
+ echo html_writer::table($questionstatstable);
}
- function output_individual_question_data($quiz, $question, $reporturl, $quizstats){
- global $CFG, $DB, $QTYPES, $OUTPUT;
- require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_question_table.php');
- $this->qtable = new quiz_report_statistics_question_table($question->id);
- $downloadtype = $this->table->is_downloading();
- if (!$this->table->is_downloading()){
- $datumfromtable = $this->table->format_row($question);
+ /**
+ * Display the response analysis for a question.
+ * @param object $question the question to report on.
+ * @param moodle_url $reporturl the URL to resisplay this report.
+ * @param object $quizstats Holds the quiz statistics.
+ */
+ protected function output_individual_question_response_analysis($question,
+ $reporturl, $quizstats) {
+ global $OUTPUT;
- $questioninfotable = new html_table();
- $questioninfotable->align = array('center', 'center');
- $questioninfotable->width = '60%';
- $questioninfotable->attributes['class'] = 'generaltable titlesleft';
+ if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
+ return;
+ }
- $questioninfotable->data = array();
- $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
- $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'), $question->name.' '.$datumfromtable['actions']);
- $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $datumfromtable['icon'].' '.get_string($question->qtype,'quiz').' '.$datumfromtable['icon']);
- $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'), $question->_stats->positions);
-
- $questionstatstable = new html_table();
- $questionstatstable->align = array('center', 'center');
- $questionstatstable->width = '60%';
- $questionstatstable->attributes['class'] = 'generaltable titlesleft';
-
- unset($datumfromtable['number']);
- unset($datumfromtable['icon']);
- $actions = $datumfromtable['actions'];
- unset($datumfromtable['actions']);
- unset($datumfromtable['name']);
- $labels = array('s' => get_string('attempts', 'quiz_statistics'),
- 'facility' => get_string('facility', 'quiz_statistics'),
- 'sd' => get_string('standarddeviationq', 'quiz_statistics'),
- 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
- 'intended_weight'=> get_string('intended_weight', 'quiz_statistics'),
- 'effective_weight'=> get_string('effective_weight', 'quiz_statistics'),
- 'discrimination_index'=> get_string('discrimination_index', 'quiz_statistics'),
- 'discriminative_efficiency'=> get_string('discriminative_efficiency', 'quiz_statistics'));
- foreach ($datumfromtable as $item => $value){
- $questionstatstable->data[] = array($labels[$item], $value);
- }
- echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
- echo html_writer::table($questioninfotable);
-
- echo $OUTPUT->box(format_text($question->questiontext, $question->questiontextformat, array('overflowdiv'=>true)).$actions, 'boxaligncenter generalbox boxwidthnormal mdl-align');
-
- echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
- echo html_writer::table($questionstatstable);
+ $qtable = new quiz_report_statistics_question_table($question->id);
+ $exportclass = $this->table->export_class_instance();
+ $qtable->export_class_instance($exportclass);
+ if (!$this->table->is_downloading()) {
+ // Output an appropriate title.
+ echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'));
} else {
- $this->qtable->export_class_instance($this->table->export_class_instance());
- $questiontabletitle = !empty($question->number)?'('.$question->number.') ':'';
- $questiontabletitle .= "\"{$question->name}\"";
- $questiontabletitle = "$questiontabletitle";
- if ($downloadtype == 'xhtml'){
- $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
+ // Work out an appropriate title.
+ $questiontabletitle = '"' . $question->name . '"';
+ if (!empty($question->number)) {
+ $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
}
- $exportclass =& $this->table->export_class_instance();
+ if ($this->table->is_downloading() == 'xhtml') {
+ $questiontabletitle = get_string('analysisofresponsesfor',
+ 'quiz_statistics', $questiontabletitle);
+ }
+
+ // Set up the table.
$exportclass->start_table($questiontabletitle);
}
- if ($QTYPES[$question->qtype]->show_analysis_of_responses()){
- if (!$this->table->is_downloading()){
- echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'));
- }
- $teacherresponses = $QTYPES[$question->qtype]->get_possible_responses($question);
- $this->qtable->setup($reporturl, $question, count($teacherresponses)>1);
- if ($this->table->is_downloading()){
- $exportclass->output_headers($this->qtable->headers);
- }
- $responses = $DB->get_records('quiz_question_response_stats', array('quizstatisticsid' => $quizstats->id, 'questionid' => $question->id), 'credit DESC, subqid ASC, aid ASC, rcount DESC');
- $responses = quiz_report_index_by_keys($responses, array('subqid', 'aid'), false);
- foreach ($responses as $subqid => $response){
- foreach (array_keys($responses[$subqid]) as $aid){
- uasort($responses[$subqid][$aid], array('quiz_statistics_report', 'sort_answers'));
- }
- if (isset($responses[$subqid]['0'])){
- $wildcardresponse = new stdClass();
- $wildcardresponse->answer = '*';
- $wildcardresponse->credit = 0;
- $teacherresponses[$subqid][0] = $wildcardresponse;
- }
- }
- $first = true;
- $subq = 0;
- foreach ($teacherresponses as $subqid => $tresponsesforsubq){
- $subq++;
- $qhaswildcards = $QTYPES[$question->qtype]->has_wildcards_in_responses($question, $subqid);
- if (!$first){
- $this->qtable->add_separator();
- }
- uasort($tresponsesforsubq, array('quiz_statistics_report', 'sort_response_details'));
- foreach ($tresponsesforsubq as $aid => $teacherresponse){
- $teacherresponserow = new stdClass();
- $teacherresponserow->response = $teacherresponse->answer;
- $teacherresponserow->indent = '';
- $teacherresponserow->rcount = 0;
- $teacherresponserow->subq = $subq;
- $teacherresponserow->credit = $teacherresponse->credit;
- if (isset($responses[$subqid][$aid])){
- $singleanswer = count($responses[$subqid][$aid])==1 &&
- ($responses[$subqid][$aid][0]->response == $teacherresponserow->response);
- if (!$singleanswer && $qhaswildcards){
- $this->qtable->add_separator();
- }
- foreach ($responses[$subqid][$aid] as $response){
- $teacherresponserow->rcount += $response->rcount;
- }
- if ($aid!=0 || $qhaswildcards){
- $this->qtable->add_data_keyed($this->qtable->format_row($teacherresponserow));
- }
- if (!$singleanswer){
- foreach ($responses[$subqid][$aid] as $response){
- if (!$downloadtype || $downloadtype=='xhtml'){
- $indent = ' ';
- } else {
- $indent = ' ';
- }
- $response->response = $response->response;
- $response->indent = $qhaswildcards ? $indent : '';
- $response->subq = $subq;
- if ((count($responses[$subqid][$aid])<2) || ($response->rcount > ($teacherresponserow->rcount / 10))){
- $this->qtable->add_data_keyed($this->qtable->format_row($response));
- }
- }
- }
+ $responesstats = new quiz_statistics_response_analyser($question);
+ $responesstats->load_cached($quizstats->id);
+
+ $qtable->setup($reporturl, $question, $responesstats);
+ if ($this->table->is_downloading()) {
+ $exportclass->output_headers($qtable->headers);
+ }
+
+ foreach ($responesstats->responseclasses as $partid => $partclasses) {
+ $rowdata = new stdClass();
+ $rowdata->part = $partid;
+ foreach ($partclasses as $responseclassid => $responseclass) {
+ $rowdata->responseclass = $responseclass->responseclass;
+
+ $responsesdata = $responesstats->responses[$partid][$responseclassid];
+ if (empty($responsesdata)) {
+ if (!array_key_exists('responseclass', $qtable->columns)) {
+ $rowdata->response = $responseclass->responseclass;
} else {
- $this->qtable->add_data_keyed($this->qtable->format_row($teacherresponserow));
+ $rowdata->response = '';
}
+ $rowdata->fraction = $responseclass->fraction;
+ $rowdata->count = 0;
+ $qtable->add_data_keyed($qtable->format_row($rowdata));
+ continue;
}
- $first = false;
- }
- $this->qtable->finish_output(!$this->table->is_downloading());
- }
- if (!$this->table->is_downloading()){
- $url = $reporturl->out();
- $text = get_string('backtoquizreport', 'quiz_statistics');
- echo $OUTPUT->box("$text", 'boxaligncenter generalbox boxwidthnormal mdl-align');
- }
- }
- function output_quiz_structure_analysis_table($s, $questions, $subquestions){
- global $OUTPUT;
- if ($s){
- if (!$this->table->is_downloading()){
- echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
- }
- foreach ($questions as $question){
- $this->table->add_data_keyed($this->table->format_row($question));
- if (!empty($question->_stats->subquestions)){
- $subitemstodisplay = explode(',', $question->_stats->subquestions);
- foreach ($subitemstodisplay as $subitemid){
- $subquestions[$subitemid]->maxgrade = $question->maxgrade;
- $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
- }
+ foreach ($responsesdata as $response => $data) {
+ $rowdata->response = $response;
+ $rowdata->fraction = $data->fraction;
+ $rowdata->count = $data->count;
+ $qtable->add_data_keyed($qtable->format_row($rowdata));
}
}
-
- $this->table->finish_output(!$this->table->is_downloading());
}
+
+ $qtable->finish_output(!$this->table->is_downloading());
}
- function output_quiz_info_table($course, $cm, $quiz, $quizstats, $usingattemptsstring,
- $currentgroup, $groupstudents, $useallattempts, $download, $reporturl, $everything){
- global $DB, $OUTPUT;
- // Print information on the number of existing attempts
- $quizinformationtablehtml = $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 2, 'main');
- $quizinformationtable = new html_table();
- $quizinformationtable->align = array('center', 'center');
- $quizinformationtable->width = '60%';
- $quizinformationtable->attributes['class'] = 'generaltable titlesleft boxaligncenter';
- $quizinformationtable->data = array();
- $quizinformationtable->data[] = array(get_string('quizname', 'quiz_statistics'), $quiz->name);
- $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $course->fullname);
- if ($cm->idnumber){
- $quizinformationtable->data[] = array(get_string('idnumbermod'), $cm->idnumber);
+ /**
+ * Output the table that lists all the questions in the quiz with their statistics.
+ * @param int $s number of attempts.
+ * @param array $questions the questions in the quiz.
+ * @param array $subquestions the subquestions of any random questions.
+ */
+ protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) {
+ if (!$s) {
+ return;
}
- if ($quiz->timeopen){
- $quizinformationtable->data[] = array(get_string('quizopen', 'quiz'), userdate($quiz->timeopen));
- }
- if ($quiz->timeclose){
- $quizinformationtable->data[] = array(get_string('quizclose', 'quiz'), userdate($quiz->timeclose));
- }
- if ($quiz->timeopen && $quiz->timeclose){
- $quizinformationtable->data[] = array(get_string('duration', 'quiz_statistics'), format_time($quiz->timeclose - $quiz->timeopen));
- }
- $format = array('firstattemptscount' => '',
- 'allattemptscount' => '',
- 'firstattemptsavg' => 'sumgrades_as_percentage',
- 'allattemptsavg' => 'sumgrades_as_percentage',
- 'median' => 'sumgrades_as_percentage',
- 'standarddeviation' => 'sumgrades_as_percentage',
- 'skewness' => '',
- 'kurtosis' => '',
- 'cic' => 'number_format',
- 'errorratio' => 'number_format',
- 'standarderror' => 'sumgrades_as_percentage');
- foreach ($quizstats as $property => $value){
- if (!isset($format[$property])){
+
+ foreach ($questions as $question) {
+ // Output the data for this questions.
+ $this->table->add_data_keyed($this->table->format_row($question));
+
+ if (empty($question->_stats->subquestions)) {
continue;
}
- if (!is_null($value)){
- switch ($format[$property]){
- case 'sumgrades_as_percentage' :
- $formattedvalue = quiz_report_scale_sumgrades_as_percentage($value, $quiz);
- break;
- case 'number_format' :
- $formattedvalue = quiz_format_grade($quiz, $value).'%';
- break;
- default :
- $formattedvalue = $value;
- }
- $quizinformationtable->data[] = array(get_string($property, 'quiz_statistics', $usingattemptsstring), $formattedvalue);
- }
- }
- if (!$this->table->is_downloading()){
- if (isset($quizstats->timemodified)){
- list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quiz->id, $currentgroup, $groupstudents, $useallattempts);
- $sql = 'SELECT COUNT(1) ' .
- 'FROM ' .$fromqa.' '.
- 'WHERE ' .$whereqa.' AND qa.timefinish > :time';
- $a = new stdClass();
- $a->lastcalculated = format_time(time() - $quizstats->timemodified);
- if (!$a->count = $DB->count_records_sql($sql, array('time'=>$quizstats->timemodified)+$qaparams)){
- $a->count = 0;
- }
- $quizinformationtablehtml .= $OUTPUT->box_start('boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
- $quizinformationtablehtml .= get_string('lastcalculated', 'quiz_statistics', $a);
- $aurl = new moodle_url($reporturl->out_omit_querystring(), $reporturl->params() + array('recalculate' => 1, 'sesskey' => sesskey()));
- $quizinformationtablehtml .= $OUTPUT->single_button($aurl, get_string('recalculatenow', 'quiz_statistics'));
- $quizinformationtablehtml .= $OUTPUT->box_end();
- }
- $downloadoptions = $this->table->get_download_menu();
- $quizinformationtablehtml .= '';
- }
- $quizinformationtablehtml .= html_writer::table($quizinformationtable);
- if (!$this->table->is_downloading()){
- echo $quizinformationtablehtml;
- } elseif ($everything) {
- $exportclass =& $this->table->export_class_instance();
- if ($download == 'xhtml'){
- echo $quizinformationtablehtml;
- } else {
- $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
- $headers = array();
- $row = array();
- foreach ($quizinformationtable->data as $data){
- $headers[]= $data[0];
- $row[] = $data[1];
- }
- $exportclass->output_headers($headers);
- $exportclass->add_data($row);
- $exportclass->finish_table();
+
+ // And its subquestions, if it has any.
+ $subitemstodisplay = explode(',', $question->_stats->subquestions);
+ foreach ($subitemstodisplay as $subitemid) {
+ $subquestions[$subitemid]->maxmark = $question->maxmark;
+ $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
}
}
+
+ $this->table->finish_output(!$this->table->is_downloading());
}
- function quiz_stats($nostudentsingroup, $quizid, $currentgroup, $groupstudents, $questions, $useallattempts){
- global $CFG, $DB;
- if (!$nostudentsingroup){
- //Calculating_MEAN_of_grades_for_all_attempts_by_students
- //http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Calculating_MEAN_of_grades_for_all_attempts_by_students
+ protected function get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats) {
- list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents);
+ // You can edit this array to control which statistics are displayed.
+ $todisplay = array('firstattemptscount' => 'number',
+ 'allattemptscount' => 'number',
+ 'firstattemptsavg' => 'summarks_as_percentage',
+ 'allattemptsavg' => 'summarks_as_percentage',
+ 'median' => 'summarks_as_percentage',
+ 'standarddeviation' => 'summarks_as_percentage',
+ 'skewness' => 'number_format',
+ 'kurtosis' => 'number_format',
+ 'cic' => 'number_format_percent',
+ 'errorratio' => 'number_format_percent',
+ 'standarderror' => 'summarks_as_percentage');
- $sql = 'SELECT (CASE WHEN attempt=1 THEN 1 ELSE 0 END) AS isfirst, COUNT(1) AS countrecs, SUM(sumgrades) AS total ' .
- 'FROM '.$fromqa.
- 'WHERE ' .$whereqa.
- 'GROUP BY (attempt=1)';
-
- if (!$attempttotals = $DB->get_records_sql($sql, $qaparams)){
- $s = 0;
- $usingattemptsstring = '';
- } else {
- $firstattempt = $attempttotals[1];
- $allattempts = new stdClass();
- $allattempts->countrecs = $firstattempt->countrecs +
- (isset($attempttotals[0])?$attempttotals[0]->countrecs:0);
- $allattempts->total = $firstattempt->total +
- (isset($attempttotals[0])?$attempttotals[0]->total:0);
- if ($useallattempts){
- $usingattempts = $allattempts;
- $usingattempts->attempts = get_string('allattempts', 'quiz_statistics');
- $usingattempts->sql = '';
- } else {
- $usingattempts = $firstattempt;
- $usingattempts->attempts = get_string('firstattempts', 'quiz_statistics');
- $usingattempts->sql = 'AND qa.attempt=1 ';
- }
- $usingattemptsstring = $usingattempts->attempts;
- $s = $usingattempts->countrecs;
- $sumgradesavg = $usingattempts->total / $usingattempts->countrecs;
- }
- } else {
- $s = 0;
+ // General information about the quiz.
+ $quizinfo = array();
+ $quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
+ $quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
+ if ($cm->idnumber) {
+ $quizinfo[get_string('idnumbermod')] = $cm->idnumber;
}
+ if ($quiz->timeopen) {
+ $quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
+ }
+ if ($quiz->timeclose) {
+ $quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
+ }
+ if ($quiz->timeopen && $quiz->timeclose) {
+ $quizinfo[get_string('duration', 'quiz_statistics')] =
+ format_time($quiz->timeclose - $quiz->timeopen);
+ }
+
+ // The statistics.
+ foreach ($todisplay as $property => $format) {
+ if (!isset($quizstats->$property) || empty($format[$property])) {
+ continue;
+ }
+ $value = $quizstats->$property;
+
+ switch ($format) {
+ case 'summarks_as_percentage':
+ $formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
+ break;
+ case 'number_format_percent':
+ $formattedvalue = quiz_format_grade($quiz, $value) . '%';
+ break;
+ case 'number_format':
+ // + 2 decimal places, since not a percentage,
+ // and we want the same number of sig figs.
+ $formattedvalue = format_float($value, $quiz->decimalpoints + 2);
+ break;
+ case 'number':
+ $formattedvalue = $value + 0;
+ break;
+ default:
+ $formattedvalue = $value;
+ }
+
+ $quizinfo[get_string($property, 'quiz_statistics',
+ $this->using_attempts_string(!empty($quizstats->allattempts)))] =
+ $formattedvalue;
+ }
+
+ return $quizinfo;
+ }
+
+ /**
+ * Output the table of overall quiz statistics.
+ * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
+ * @return string the HTML.
+ */
+ protected function output_quiz_info_table($quizinfo) {
+
+ $quizinfotable = new html_table();
+ $quizinfotable->align = array('center', 'center');
+ $quizinfotable->width = '60%';
+ $quizinfotable->attributes['class'] = 'generaltable titlesleft';
+ $quizinfotable->data = array();
+
+ foreach ($quizinfo as $heading => $value) {
+ $quizinfotable->data[] = array($heading, $value);
+ }
+
+ return html_writer::table($quizinfotable);
+ }
+
+ /**
+ * Download the table of overall quiz statistics.
+ * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
+ */
+ protected function download_quiz_info_table($quizinfo) {
+ global $OUTPUT;
+
+ // XHTML download is a special case.
+ if ($this->table->is_downloading() == 'xhtml') {
+ echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
+ echo $this->output_quiz_info_table($quizinfo);
+ return;
+ }
+
+ // Reformat the data ready for output.
+ $headers = array();
+ $row = array();
+ foreach ($quizinfo as $heading => $value) {
+ $headers[] = $heading;
+ $row[] = $value;
+ }
+
+ // Do the output.
+ $exportclass = $this->table->export_class_instance();
+ $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
+ $exportclass->output_headers($headers);
+ $exportclass->add_data($row);
+ $exportclass->finish_table();
+ }
+
+ /**
+ * Output the HTML needed to show the statistics graph.
+ * @param int $quizstatsid the id of the statistics to show in the graph.
+ */
+ protected function output_statistics_graph($quizstatsid, $s) {
+ global $OUTPUT;
+
+ if ($s == 0) {
+ return;
+ }
+
+ $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
+ array('id' => $quizstatsid));
+ $OUTPUT->heading(get_string('statisticsreportgraph', 'quiz_statistics'));
+ echo html_writer::tag('div', html_writer::empty_tag('img', array('src' => $imageurl,
+ 'alt' => get_string('statisticsreportgraph', 'quiz_statistics'))),
+ array('class' => 'graph'));
+ }
+
+ /**
+ * Return the stats data for when there are no stats to show.
+ *
+ * @param array $questions question definitions.
+ * @param int $firstattemptscount number of first attempts (optional).
+ * @param int $firstattemptscount total number of attempts (optional).
+ * @return array with three elements:
+ * - integer $s Number of attempts included in the stats (0).
+ * - array $quizstats The statistics for overall attempt scores.
+ * - array $qstats The statistics for each question.
+ */
+ protected function get_emtpy_stats($questions, $firstattemptscount = 0,
+ $allattemptscount = 0) {
$quizstats = new stdClass();
- if ($s == 0){
- $quizstats->firstattemptscount = 0;
- $quizstats->allattemptscount = 0;
- } else {
- $quizstats->firstattemptscount = $firstattempt->countrecs;
- $quizstats->allattemptscount = $allattempts->countrecs;
- $quizstats->firstattemptsavg = $firstattempt->total / $firstattempt->countrecs;
- $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
- }
- //recalculate sql again this time possibly including test for first attempt.
- list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $useallattempts);
+ $quizstats->firstattemptscount = $firstattemptscount;
+ $quizstats->allattemptscount = $allattemptscount;
- //get the median
- if ($s) {
+ $qstats = new stdClass();
+ $qstats->questions = $questions;
+ $qstats->subquestions = array();
+ $qstats->responses = array();
- if (($s%2)==0){
- //even number of attempts
- $limitoffset = ($s/2) - 1;
- $limit = 2;
- } else {
- $limitoffset = (floor($s/2));
- $limit = 1;
- }
- $sql = 'SELECT id, sumgrades ' .
- 'FROM ' .$fromqa.
- 'WHERE ' .$whereqa.
- 'ORDER BY sumgrades';
- if (!$mediangrades = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit)){
- print_error('errormedian', 'quiz_statistics');
- }
- $quizstats->median = array_sum($mediangrades) / count($mediangrades);
- if ($s>1){
- //fetch sum of squared, cubed and power 4d
- //differences between grades and mean grade
- $mean = $usingattempts->total / $s;
- $sql = "SELECT " .
- "SUM(POWER((qa.sumgrades - :mean1),2)) AS power2, " .
- "SUM(POWER((qa.sumgrades - :mean2),3)) AS power3, ".
- "SUM(POWER((qa.sumgrades - :mean3),4)) AS power4 ".
- 'FROM ' .$fromqa.
- 'WHERE ' .$whereqa;
- $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
- if (!$powers = $DB->get_record_sql($sql, $params)){
- print_error('errorpowers', 'quiz_statistics');
- }
-
- //Standard_Deviation
- //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Standard_Deviation
-
- $quizstats->standarddeviation = sqrt($powers->power2 / ($s -1));
-
-
-
- //Skewness_and_Kurtosis
- if ($s>2){
- //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis
- $m2= $powers->power2 / $s;
- $m3= $powers->power3 / $s;
- $m4= $powers->power4 / $s;
-
- $k2= $s*$m2/($s-1);
- $k3= $s*$s*$m3/(($s-1)*($s-2));
- if ($k2){
- $quizstats->skewness = $k3 / (pow($k2, 3/2));
- }
- }
-
-
- if ($s>3){
- $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
- if ($k2){
- $quizstats->kurtosis = $k4 / ($k2*$k2);
- }
- }
- }
- }
- if ($s){
- require_once("$CFG->dirroot/mod/quiz/report/statistics/qstats.php");
- $qstats = new qstats($questions, $s, $sumgradesavg);
- $qstats->get_records($quizid, $currentgroup, $groupstudents, $useallattempts);
- $qstats->process_states();
- $qstats->process_responses();
- } else {
- $qstats = false;
- }
- if ($s>1){
- $p = count($qstats->questions);//no of positions
- if ($p > 1){
- if (isset($k2)){
- $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($qstats->sum_of_grade_variance())/$k2);
- $quizstats->errorratio = 100 * sqrt(1-($quizstats->cic/100));
- $quizstats->standarderror = ($quizstats->errorratio * $quizstats->standarddeviation / 100);
- }
- }
- }
- return array($s, $usingattemptsstring, $quizstats, $qstats);
+ return array(0, $quizstats, false);
}
- function quiz_questions_stats($quiz, $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions){
+ /**
+ * Compute the quiz statistics.
+ *
+ * @param object $quizid the quiz id.
+ * @param int $currentgroup the current group. 0 for none.
+ * @param bool $nostudentsingroup true if there a no students.
+ * @param bool $useallattempts use all attempts, or just first attempts.
+ * @param array $groupstudents students in this group.
+ * @param array $questions question definitions.
+ * @return array with three elements:
+ * - integer $s Number of attempts included in the stats.
+ * - array $quizstats The statistics for overall attempt scores.
+ * - array $qstats The statistics for each question.
+ */
+ protected function compute_stats($quizid, $currentgroup, $nostudentsingroup,
+ $useallattempts, $groupstudents, $questions) {
global $DB;
- $timemodified = time() - QUIZ_REPORT_TIME_TO_CACHE_STATS;
- $params = array('quizid'=>$quiz->id, 'groupid'=>(int)$currentgroup, 'allattempts'=>$useallattempts, 'timemodified'=>$timemodified);
- if (!$quizstats = $DB->get_record_select('quiz_statistics', 'quizid = :quizid AND groupid = :groupid AND allattempts = :allattempts AND timemodified > :timemodified', $params, '*', true)){
- list($s, $usingattemptsstring, $quizstats, $qstats) = $this->quiz_stats($nostudentsingroup, $quiz->id, $currentgroup, $groupstudents, $questions, $useallattempts);
- if ($s){
- $toinsert = (object)((array)$quizstats + $params);
- if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
- $toinsert->errorratio = NULL;
- }
- if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
- $toinsert->standarderror = NULL;
- }
- $toinsert->timemodified = time();
- $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
- foreach ($qstats->questions as $question){
- $question->_stats->quizstatisticsid = $quizstats->id;
- $DB->insert_record('quiz_question_statistics', $question->_stats, false, true);
- }
- foreach ($qstats->subquestions as $subquestion){
- $subquestion->_stats->quizstatisticsid = $quizstats->id;
- $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false, true);
- }
- foreach ($qstats->responses as $response){
- $response->quizstatisticsid = $quizstats->id;
- $DB->insert_record('quiz_question_response_stats', $response, false);
+
+ // Calculating MEAN of marks for all attempts by students
+ // http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise
+ // #Calculating_MEAN_of_grades_for_all_attempts_by_students
+ if ($nostudentsingroup) {
+ return $this->get_emtpy_stats($questions);
+ }
+
+ list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
+ $quizid, $currentgroup, $groupstudents, true);
+
+ $attempttotals = $DB->get_records_sql("
+ SELECT
+ CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
+ COUNT(1) AS countrecs,
+ SUM(sumgrades) AS total
+ FROM $fromqa
+ WHERE $whereqa
+ GROUP BY attempt = 1", $qaparams);
+
+ if (!$attempttotals) {
+ return $this->get_emtpy_stats($questions);
+ }
+
+ if (isset($attempttotals[1])) {
+ $firstattempts = $attempttotals[1];
+ $firstattempts->average = $firstattempts->total / $firstattempts->countrecs;
+ } else {
+ $firstattempts = new stdClass();
+ $firstattempts->countrecs = 0;
+ $firstattempts->total = 0;
+ $firstattempts->average = '-';
+ }
+
+ $allattempts = new stdClass();
+ if (isset($attempttotals[0])) {
+ $allattempts->countrecs = $firstattempts->countrecs + $attempttotals[0]->countrecs;
+ $allattempts->total = $firstattempts->total + $attempttotals[0]->total;
+ } else {
+ $allattempts->countrecs = $firstattempts->countrecs;
+ $allattempts->total = $firstattempts->total;
+ }
+
+ if ($useallattempts) {
+ $usingattempts = $allattempts;
+ $usingattempts->sql = '';
+ } else {
+ $usingattempts = $firstattempts;
+ $usingattempts->sql = 'AND quiza.attempt = 1 ';
+ }
+
+ $s = $usingattempts->countrecs;
+ if ($s == 0) {
+ return $this->get_emtpy_stats($questions, $firstattempts->countrecs,
+ $allattempts->countrecs);
+ }
+ $summarksavg = $usingattempts->total / $usingattempts->countrecs;
+
+ $quizstats = new stdClass();
+ $quizstats->allattempts = $useallattempts;
+ $quizstats->firstattemptscount = $firstattempts->countrecs;
+ $quizstats->allattemptscount = $allattempts->countrecs;
+ $quizstats->firstattemptsavg = $firstattempts->average;
+ $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
+
+ // Recalculate sql again this time possibly including test for first attempt.
+ list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
+ $quizid, $currentgroup, $groupstudents, $useallattempts);
+
+ // Median
+ if ($s % 2 == 0) {
+ //even number of attempts
+ $limitoffset = $s/2 - 1;
+ $limit = 2;
+ } else {
+ $limitoffset = floor($s/2);
+ $limit = 1;
+ }
+ $sql = "SELECT id, sumgrades
+ FROM $fromqa
+ WHERE $whereqa
+ ORDER BY sumgrades";
+
+ $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
+
+ $quizstats->median = array_sum($medianmarks) / count($medianmarks);
+ if ($s > 1) {
+ //fetch sum of squared, cubed and power 4d
+ //differences between marks and mean mark
+ $mean = $usingattempts->total / $s;
+ $sql = "SELECT
+ SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
+ SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
+ SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
+ FROM $fromqa
+ WHERE $whereqa";
+ $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
+
+ $powers = $DB->get_record_sql($sql, $params, MUST_EXIST);
+
+ // Standard_Deviation
+ // see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise
+ // #Standard_Deviation
+
+ $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
+
+ // Skewness
+ if ($s > 2) {
+ // see http://docs.moodle.org/en/Development:
+ // Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis
+ $m2= $powers->power2 / $s;
+ $m3= $powers->power3 / $s;
+ $m4= $powers->power4 / $s;
+
+ $k2= $s*$m2/($s-1);
+ $k3= $s*$s*$m3/(($s-1)*($s-2));
+ if ($k2) {
+ $quizstats->skewness = $k3 / (pow($k2, 3/2));
}
}
- if ($qstats){
+
+ // Kurtosis
+ if ($s > 3) {
+ $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
+ if ($k2) {
+ $quizstats->kurtosis = $k4 / ($k2*$k2);
+ }
+ }
+ }
+
+ $qstats = new quiz_statistics_question_stats($questions, $s, $summarksavg);
+ $qstats->load_step_data($quizid, $currentgroup, $groupstudents, $useallattempts);
+ $qstats->compute_statistics();
+
+ if ($s > 1) {
+ $p = count($qstats->questions); // No of positions
+ if ($p > 1 && isset($k2)) {
+ $quizstats->cic = (100 * $p / ($p -1)) *
+ (1 - ($qstats->get_sum_of_mark_variance()) / $k2);
+ $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
+ $quizstats->standarderror = $quizstats->errorratio *
+ $quizstats->standarddeviation / 100;
+ }
+ }
+
+ return array($s, $quizstats, $qstats);
+ }
+
+ /**
+ * Load the cached statistics from the database.
+ *
+ * @param object $quiz the quiz settings
+ * @param int $currentgroup the current group. 0 for none.
+ * @param bool $nostudentsingroup true if there a no students.
+ * @param bool $useallattempts use all attempts, or just first attempts.
+ * @param array $groupstudents students in this group.
+ * @param array $questions question definitions.
+ * @return array with 4 elements:
+ * - $quizstats The statistics for overall attempt scores.
+ * - $questions The questions, with an additional _stats field.
+ * - $subquestions The subquestions, if any, with an additional _stats field.
+ * - $s Number of attempts included in the stats.
+ * If there is no cached data in the database, returns an array of four nulls.
+ */
+ protected function try_loading_cached_stats($quiz, $currentgroup,
+ $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
+ global $DB;
+
+ $timemodified = time() - self::TIME_TO_CACHE_STATS;
+ $quizstats = $DB->get_record_select('quiz_statistics',
+ 'quizid = ? AND groupid = ? AND allattempts = ? AND timemodified > ?',
+ array($quiz->id, $currentgroup, $useallattempts, $timemodified));
+
+ if (!$quizstats) {
+ // No cached data found.
+ return array(null, $questions, null, null);
+ }
+
+ if ($useallattempts) {
+ $s = $quizstats->allattemptscount;
+ } else {
+ $s = $quizstats->firstattemptscount;
+ }
+
+ $subquestions = array();
+ $questionstats = $DB->get_records('quiz_question_statistics',
+ array('quizstatisticsid' => $quizstats->id));
+
+ $subquestionstats = array();
+ foreach ($questionstats as $stat) {
+ if ($stat->slot) {
+ $questions[$stat->slot]->_stats = $stat;
+ } else {
+ $subquestionstats[$stat->questionid] = $stat;
+ }
+ }
+
+ if (!empty($subquestionstats)) {
+ $subqstofetch = array_keys($subquestionstats);
+ $subquestions = question_load_questions($subqstofetch);
+ foreach ($subquestions as $subqid => $subq) {
+ $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
+ $subquestions[$subqid]->maxmark = $subq->defaultmark;
+ }
+ }
+
+ return array($quizstats, $questions, $subquestions, $s);
+ }
+
+ /**
+ * Store the statistics in the cache tables in the database.
+ *
+ * @param object $quizid the quiz id.
+ * @param int $currentgroup the current group. 0 for none.
+ * @param bool $useallattempts use all attempts, or just first attempts.
+ * @param object $quizstats The statistics for overall attempt scores.
+ * @param array $questions The questions, with an additional _stats field.
+ * @param array $subquestions The subquestions, if any, with an additional _stats field.
+ */
+ protected function cache_stats($quizid, $currentgroup,
+ $quizstats, $questions, $subquestions) {
+ global $DB;
+
+ $toinsert = clone($quizstats);
+ $toinsert->quizid = $quizid;
+ $toinsert->groupid = $currentgroup;
+ $toinsert->timemodified = time();
+
+ // Fix up some dodgy data.
+ if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
+ $toinsert->errorratio = null;
+ }
+ if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
+ $toinsert->standarderror = null;
+ }
+
+ // Store the data.
+ $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
+
+ foreach ($questions as $question) {
+ $question->_stats->quizstatisticsid = $quizstats->id;
+ $DB->insert_record('quiz_question_statistics', $question->_stats, false);
+ }
+
+ foreach ($subquestions as $subquestion) {
+ $subquestion->_stats->quizstatisticsid = $quizstats->id;
+ $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false);
+ }
+
+ return $quizstats->id;
+ }
+
+ /**
+ * Get the quiz and question statistics, either by loading the cached results,
+ * or by recomputing them.
+ *
+ * @param object $quiz the quiz settings.
+ * @param int $currentgroup the current group. 0 for none.
+ * @param bool $nostudentsingroup true if there a no students.
+ * @param bool $useallattempts use all attempts, or just first attempts.
+ * @param array $groupstudents students in this group.
+ * @param array $questions question definitions.
+ * @return array with 4 elements:
+ * - $quizstats The statistics for overall attempt scores.
+ * - $questions The questions, with an additional _stats field.
+ * - $subquestions The subquestions, if any, with an additional _stats field.
+ * - $s Number of attempts included in the stats.
+ */
+ protected function get_quiz_and_questions_stats($quiz, $currentgroup,
+ $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
+
+ list($quizstats, $questions, $subquestions, $s) =
+ $this->try_loading_cached_stats($quiz, $currentgroup, $nostudentsingroup,
+ $useallattempts, $groupstudents, $questions);
+
+ if (is_null($quizstats)) {
+ list($s, $quizstats, $qstats) = $this->compute_stats($quiz->id,
+ $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions);
+
+ if ($s) {
$questions = $qstats->questions;
$subquestions = $qstats->subquestions;
- } else {
- $questions = array();
- $subquestions = array();
- }
- } else {
- //use cached results
- if ($useallattempts){
- $usingattemptsstring = get_string('allattempts', 'quiz_statistics');
- $s = $quizstats->allattemptscount;
- } else {
- $usingattemptsstring = get_string('firstattempts', 'quiz_statistics');
- $s = $quizstats->firstattemptscount;
- }
- $subquestions = array();
- $questionstats = $DB->get_records('quiz_question_statistics', array('quizstatisticsid'=>$quizstats->id), 'subquestion ASC');
- $questionstats = quiz_report_index_by_keys($questionstats, array('subquestion', 'questionid'));
- if (1 < count($questionstats)){
- list($mainquestionstats, $subquestionstats) = $questionstats;
- $subqstofetch = array_keys($subquestionstats);
- $subquestions = question_load_questions($subqstofetch);
- foreach (array_keys($subquestions) as $subqid){
- $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
- }
- } elseif (count($questionstats)) {
- $mainquestionstats = $questionstats[0];
- }
- if (count($questionstats)) {
- foreach (array_keys($questions) as $qid){
- $questions[$qid]->_stats = $mainquestionstats[$qid];
- }
+
+ $quizstatisticsid = $this->cache_stats($quiz->id, $currentgroup,
+ $quizstats, $questions, $subquestions);
+
+ $this->analyse_responses($quizstatisticsid, $quiz->id, $currentgroup,
+ $nostudentsingroup, $useallattempts, $groupstudents,
+ $questions, $subquestions);
}
}
- return array($quizstats, $questions, $subquestions, $s, $usingattemptsstring);
+
+ return array($quizstats, $questions, $subquestions, $s);
+ }
+
+ protected function analyse_responses($quizstatisticsid, $quizid, $currentgroup,
+ $nostudentsingroup, $useallattempts, $groupstudents, $questions, $subquestions) {
+
+ $qubaids = quiz_statistics_qubaids_condition(
+ $quizid, $currentgroup, $groupstudents, $useallattempts);
+
+ $done = array();
+ foreach ($questions as $question) {
+ if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
+ continue;
+ }
+ $done[$question->id] = 1;
+
+ $responesstats = new quiz_statistics_response_analyser($question);
+ $responesstats->analyse($qubaids);
+ $responesstats->store_cached($quizstatisticsid);
+ }
+
+ foreach ($subquestions as $question) {
+ if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses() ||
+ isset($done[$question->id])) {
+ continue;
+ }
+ $done[$question->id] = 1;
+
+ $responesstats = new quiz_statistics_response_analyser($question);
+ $responesstats->analyse($qubaids);
+ $responesstats->store_cached($quizstatisticsid);
+ }
+ }
+
+ /**
+ * @return string HTML snipped for the Download full report as UI.
+ */
+ protected function everything_download_options() {
+ $downloadoptions = $this->table->get_download_menu();
+
+ $output = '';
+
+ return $output;
+ }
+
+ /**
+ * Generate the snipped of HTML that says when the stats were last caculated,
+ * with a recalcuate now button.
+ * @param object $quizstats the overall quiz statistics.
+ * @param int $quizid the quiz id.
+ * @param int $currentgroup the id of the currently selected group, or 0.
+ * @param array $groupstudents ids of students in the group.
+ * @param bool $useallattempts whether to use all attempts, instead of just
+ * first attempts.
+ * @return string a HTML snipped saying when the stats were last computed,
+ * or blank if that is not appropriate.
+ */
+ protected function output_caching_info($quizstats, $quizid, $currentgroup,
+ $groupstudents, $useallattempts, $reporturl) {
+ global $DB, $OUTPUT;
+
+ if (empty($quizstats->timemodified)) {
+ return '';
+ }
+
+ // Find the number of attempts since the cached statistics were computed.
+ list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
+ $quizid, $currentgroup, $groupstudents, $useallattempts, true);
+ $count = $DB->count_records_sql("
+ SELECT COUNT(1)
+ FROM $fromqa
+ WHERE $whereqa
+ AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
+
+ if (!$count) {
+ $count = 0;
+ }
+
+ // Generate the output.
+ $a = new stdClass();
+ $a->lastcalculated = format_time(time() - $quizstats->timemodified);
+ $a->count = $count;
+
+ $recalcualteurl = new moodle_url($reporturl,
+ array('recalculate' => 1, 'sesskey' => sesskey()));
+ $output = '';
+ $output .= $OUTPUT->box_start(
+ 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
+ $output .= get_string('lastcalculated', 'quiz_statistics', $a);
+ $output .= $OUTPUT->single_button($recalcualteurl,
+ get_string('recalculatenow', 'quiz_statistics'));
+ $output .= $OUTPUT->box_end(true);
+
+ return $output;
+ }
+
+ /**
+ * Clear the cached data for a particular report configuration. This will
+ * trigger a re-computation the next time the report is displayed.
+ * @param int $quizid the quiz id.
+ * @param int $currentgroup a group id, or 0.
+ * @param bool $useallattempts whether all attempts, or just first attempts are included.
+ */
+ protected function clear_cached_data($quizid, $currentgroup, $useallattempts) {
+ global $DB;
+
+ $todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quizid,
+ 'groupid' => $currentgroup, 'allattempts' => $useallattempts), '', 'id, 1');
+
+ if (!$todelete) {
+ return;
+ }
+
+ list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
+
+ $DB->delete_records_select('quiz_question_statistics',
+ 'quizstatisticsid ' . $todeletesql, $todeleteparams);
+ $DB->delete_records_select('quiz_question_response_stats',
+ 'quizstatisticsid ' . $todeletesql, $todeleteparams);
+ $DB->delete_records_select('quiz_statistics',
+ 'id ' . $todeletesql, $todeleteparams);
+ }
+
+ /**
+ * @param bool $useallattempts whether we are using all attempts.
+ * @return the appropriate lang string to describe this option.
+ */
+ protected function using_attempts_string($useallattempts) {
+ if ($useallattempts) {
+ return get_string('allattempts', 'quiz_statistics');
+ } else {
+ return get_string('firstattempts', 'quiz_statistics');
+ }
}
}
-function quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $allattempts = true){
- global $DB;
- $fromqa = '{quiz_attempts} qa ';
- $whereqa = 'qa.quiz = :quizid AND qa.preview=0 AND qa.timefinish !=0 ';
- $qaparams = array('quizid'=>$quizid);
+
+function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
+ $allattempts = true, $includeungraded = false) {
+ global $CFG;
+
+ $fromqa = '{quiz_attempts} quiza ';
+
+ $whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.timefinish <> 0';
+ $qaparams = array('quizid' => $quizid);
+
if (!empty($currentgroup) && $groupstudents) {
- list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents), SQL_PARAMS_NAMED, 'u');
- $whereqa .= 'AND qa.userid '.$grpsql.' ';
+ list($grpsql, $grpparams) = get_in_or_equal(array_keys($groupstudents),
+ SQL_PARAMS_NAMED, 'u');
+ $whereqa .= " AND quiza.userid $grpsql";
$qaparams += $grpparams;
}
- if (!$allattempts){
- $whereqa .= 'AND qa.attempt=1 ';
+
+ if (!$allattempts) {
+ $whereqa .= ' AND quiza.attempt = 1';
}
+
+ if (!$includeungraded) {
+ $whereqa .= ' AND quiza.sumgrades IS NOT NULL';
+ }
+
return array($fromqa, $whereqa, $qaparams);
}
-
+/**
+ * Return a {@link qubaid_condition} from the values returned by
+ * {@link quiz_statistics_attempts_sql}
+ * @param string $fromqa from quiz_statistics_attempts_sql.
+ * @param string $whereqa from quiz_statistics_attempts_sql.
+ */
+function quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents,
+ $allattempts = true, $includeungraded = false) {
+ list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup,
+ $groupstudents, $allattempts, $includeungraded);
+ return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
+}
diff --git a/mod/quiz/report/statistics/responseanalysis.php b/mod/quiz/report/statistics/responseanalysis.php
new file mode 100644
index 00000000000..3aa9022fbb2
--- /dev/null
+++ b/mod/quiz/report/statistics/responseanalysis.php
@@ -0,0 +1,220 @@
+.
+
+/**
+ * This file contains the code to analyse all the responses to a particular
+ * question.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * This class can store and compute the analysis of the responses to a particular
+ * question.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_statistics_response_analyser {
+ /** @var object the data from the database that defines the question. */
+ protected $questiondata;
+ protected $loaded = false;
+
+ /**
+ * @var array This is a multi-dimensional array that stores the results of
+ * the analysis.
+ *
+ * The description of {@link question_type::get_possible_responses()} should
+ * help understand this description.
+ *
+ * $this->responses[$subpartid][$responseclassid][$response] is an
+ * object with two fields, ->count and ->fraction.
+ */
+ public $responses = array();
+
+ /**
+ * @var array $this->fractions[$subpartid][$responseclassid] is an object
+ * with two fields, ->responseclass and ->fraction.
+ */
+ public $responseclasses = array();
+
+ /**
+ * Create a new instance of this class for holding/computing the statistics
+ * for a particular question.
+ * @param object $questiondata the data from the database defining this question.
+ */
+ public function __construct($questiondata) {
+ $this->questiondata = $questiondata;
+
+ $this->responseclasses =
+ question_bank::get_qtype($questiondata->qtype)->get_possible_responses(
+ $questiondata);
+ foreach ($this->responseclasses as $subpartid => $responseclasses) {
+ foreach ($responseclasses as $responseclassid => $notused) {
+ $this->responses[$subpartid][$responseclassid] = array();
+ }
+ }
+ }
+
+ /**
+ * @return bool whether this analysis has more than one subpart.
+ */
+ public function has_subparts() {
+ return count($this->responseclasses) > 1;
+ }
+
+ /**
+ * @return bool whether this analysis has (a subpart with) more than one
+ * response class.
+ */
+ public function has_response_classes() {
+ foreach ($this->responseclasses as $partclasses) {
+ if (count($partclasses) > 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return bool whether this analysis has a response class more than one
+ * different acutal response.
+ */
+ public function has_actual_responses() {
+ foreach ($this->responseclasses as $subpartid => $partclasses) {
+ foreach ($partclasses as $responseclassid => $notused) {
+ if (count($this->responses[$subpartid][$responseclassid]) > 1) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Analyse all the response data for for all the specified attempts at
+ * this question.
+ * @param $qubaids which attempts to consider.
+ */
+ public function analyse($qubaids) {
+ // Load data.
+ $dm = new question_engine_data_mapper();
+ $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
+
+ // Analyse it.
+ foreach ($questionattempts as $qa) {
+ $this->add_data_from_one_attempt($qa);
+ }
+
+ $this->loaded = true;
+ }
+
+ /**
+ * Analyse the data from one question attempt.
+ * @param question_attempt $qa the data to analyse.
+ */
+ protected function add_data_from_one_attempt(question_attempt $qa) {
+ $blankresponse = question_classified_response::no_response();
+
+ $partresponses = $qa->classify_response();
+ foreach ($partresponses as $subpartid => $partresponse) {
+ if (!isset($this->responses[$subpartid][$partresponse->responseclassid]
+ [$partresponse->response])) {
+ $resp = new stdClass();
+ $resp->count = 0;
+ if (!is_null($partresponse->fraction)) {
+ $resp->fraction = $partresponse->fraction;
+ } else {
+ $resp->fraction = $this->responseclasses[$subpartid]
+ [$partresponse->responseclassid]->fraction;
+ }
+
+ $this->responses[$subpartid][$partresponse->responseclassid]
+ [$partresponse->response] = $resp;
+ }
+
+ $this->responses[$subpartid][$partresponse->responseclassid]
+ [$partresponse->response]->count += 1;
+ }
+ }
+
+ /**
+ * Store the computed response analysis in the quiz_question_response_stats
+ * table.
+ * @param int $quizstatisticsid the cached quiz statistics to load the
+ * data corresponding to.
+ * @return bool true if cached data was found in the database and loaded,
+ * otherwise false, to mean no data was loaded.
+ */
+ public function load_cached($quizstatisticsid) {
+ global $DB;
+
+ $rows = $DB->get_records('quiz_question_response_stats',
+ array('quizstatisticsid' => $quizstatisticsid,
+ 'questionid' => $this->questiondata->id));
+ if (!$rows) {
+ return false;
+ }
+
+ foreach ($rows as $row) {
+ $this->responses[$row->subqid][$row->aid][$row->response]->count = $row->rcount;
+ $this->responses[$row->subqid][$row->aid][$row->response]->fraction = $row->credit;
+ }
+ $this->loaded = true;
+ return true;
+ }
+
+ /**
+ * Store the computed response analysis in the quiz_question_response_stats
+ * table.
+ * @param int $quizstatisticsid the cached quiz statistics this correspons to.
+ */
+ public function store_cached($quizstatisticsid) {
+ global $DB;
+
+ if (!$this->loaded) {
+ throw new coding_exception(
+ 'Question responses have not been analyised. Cannot store in the database.');
+ }
+
+ foreach ($this->responses as $subpartid => $partdata) {
+ foreach ($partdata as $responseclassid => $classdata) {
+ foreach ($classdata as $response => $data) {
+ $row = new stdClass();
+ $row->quizstatisticsid = $quizstatisticsid;
+ $row->questionid = $this->questiondata->id;
+ $row->subqid = $subpartid;
+ if ($responseclassid === '') {
+ $row->aid = null;
+ } else {
+ $row->aid = $responseclassid;
+ }
+ $row->response = $response;
+ $row->rcount = $data->count;
+ $row->credit = $data->fraction;
+ $DB->insert_record('quiz_question_response_stats', $row, false);
+ }
+ }
+ }
+ }
+}
diff --git a/mod/quiz/report/statistics/simpletest/mdl_question.csv b/mod/quiz/report/statistics/simpletest/mdl_question.csv
index 1190b7cbe91..b96e29817b6 100644
--- a/mod/quiz/report/statistics/simpletest/mdl_question.csv
+++ b/mod/quiz/report/statistics/simpletest/mdl_question.csv
@@ -1,21 +1,21 @@
-"id","number","category","parent","name","questiontext","questiontextformat","image","generalfeedback","defaultgrade","penalty","qtype","length","stamp","version","hidden","timecreated","timemodified","createdby","modifiedby","maxgrade"
-"1","1","5","0","questionessay-11","What is the purpose of life?","0",,"General feedback","1.0000000","0.0000000","essay","1","localhost+080922073527+WeUaUK","localhost+080922073527+qxLjv1","0","1222068927","0","2",NULL,"1.0000000"
-"2","2","5","0","questionessay-17","What is the purpose of life?","0",,"General feedback","1.0000000","0.0000000","essay","1","localhost+080922073527+pvCseX","localhost+080922073527+mzy6tY","0","1222068927","0","2",NULL,"1.0000000"
-"3","3","5","0","questionessay-6","What is the purpose of life?","0",,"General feedback","1.0000000","0.0000000","essay","1","localhost+080922073527+Cr3gDO","localhost+080922073527+hnxfTy","0","1222068927","0","2",NULL,"1.0000000"
-"4","4","5","0","questionessay-8","What is the purpose of life?","0",,"General feedback","1.0000000","0.0000000","essay","1","localhost+080922073527+sSq9ln","localhost+080922073527+PGyab3","0","1222068927","0","2",NULL,"1.0000000"
-"5","5","5","0","questionmatch-10","test question, generated by script","0",,"Well done","1.0000000","0.1000000","match","1","localhost+080922073527+oG1i2f","localhost+080922073527+S1UxZy","0","1222068927","0","2",NULL,"0.0000000"
-"6","6","5","0","questionmatch-16","test question, generated by script","0",,"Well done","1.0000000","0.1000000","match","1","localhost+080922073527+vMFHyY","localhost+080922073527+4GZIyQ","0","1222068927","0","2",NULL,"0.0000000"
-"7","7","5","0","questionmatch-18","test question, generated by script","0",,"Well done","1.0000000","0.1000000","match","1","localhost+080922073527+Xkxqn1","localhost+080922073527+xbU6U7","0","1222068927","0","2",NULL,"0.0000000"
-"8","8","5","0","questionmultianswer-12","This question consists of some text with an answer embedded right here {#1} and right after that you will have to deal with this short answer {#2} and finally we have a floating point number {#3}. Note that addresses like www.moodle.org and smileys :-) all work as normal: a) How good is this? {#4} b) What grade would you give it? {#5} Good luck!","0",,"General feedback","8.0000000","0.1000000","multianswer","1","localhost+080922073527+0zKgpF","localhost+080922073527+r1gsde","0","1222068927","0","2",NULL,"8.0000000"
-"14","9","5","0","questionmultichoice-13","How old is the sun?","0",,"General feedback","1.0000000","0.1000000","multichoice","1","localhost+080922073527+AjIjeV","localhost+080922073527+UhtTLR","0","1222068927","0","2",NULL,"1.0000000"
-"15","10","5","0","questionmultichoice-14","How old is the sun?","0",,"General feedback","1.0000000","0.1000000","multichoice","1","localhost+080922073527+IrAqRl","localhost+080922073527+xRfta8","0","1222068927","0","2",NULL,"1.0000000"
-"16","11","5","0","questionmultichoice-3","How old is the sun?","0",,"General feedback","1.0000000","0.1000000","multichoice","1","localhost+080922073527+DMGirU","localhost+080922073527+689V8k","0","1222068927","0","2",NULL,"1.0000000"
-"17","12","5","0","questionmultichoice-5","How old is the sun?","0",,"General feedback","1.0000000","0.1000000","multichoice","1","localhost+080922073527+wileZw","localhost+080922073527+zGcaDa","0","1222068927","0","2",NULL,"1.0000000"
-"25","13","5","0","Random Short-Answer Matching","For each of the following questions, select the matching answer from the menu.
","1",,,"1.0000000","0.1000000","randomsamatch","1","localhost+080922073724+qF803I","localhost+080922075820+zbZtaD","0","1222069044","1222070300","2","2","1.0000000"
-"22","14","5","0","Is Thai difficult?","Is Thai difficult?","0",,,"1.0000000","0.1000000","shortanswer","1","localhost+080922073655+2FLtCU","localhost+080922073655+fgUeOj","0","1222069015","0","2",NULL,"1.0000000"
-"23","15","5","0","Is Thai grammar difficult?","Is Thai grammar difficult?","0",,,"1.0000000","0.1000000","shortanswer","1","localhost+080922073655+LYSD32","localhost+080922073655+WgRYk4","0","1222069015","0","2",NULL,"1.0000000"
-"24","16","5","0","Is Thai pronunciation difficult?","Is Thai pronunciation difficult?","0",,,"1.0000000","0.1000000","shortanswer","1","localhost+080922073655+5p1w22","localhost+080922073655+g5jrXa","0","1222069015","0","2",NULL,"1.0000000"
-"20","17","5","0","Who's buried in Grant's tomb?","Who's buried in Grant's tomb?","0",,,"1.0000000","0.1000000","shortanswer","1","localhost+080922073655+PTDcDZ","localhost+080922073655+aghyfu","0","1222069015","0","2",NULL,"1.0000000"
-"21","18","5","0","Who's buried in Jamie's tomb?","Who's buried in Jamie's tomb?","0",,,"1.0000000","0.1000000","shortanswer","1","localhost+080922073655+Xvy1ns","localhost+080922073655+Mx0Izs","0","1222069015","0","2",NULL,"1.0000000"
-"18","19","5","0","questiontruefalse-7","This question is really stupid","0",,"Well done","1.0000000","1.0000000","truefalse","1","localhost+080922073527+9bzTef","localhost+080922073527+hjcQR1","0","1222068927","0","2",NULL,"1.0000000"
-"19","20","5","0","questiontruefalse-9","This question is really stupid","0",,"Well done","1.0000000","1.0000000","truefalse","1","localhost+080922073527+TI0yD4","localhost+080922073527+iXIulQ","0","1222068927","0","2",NULL,"1.0000000"
+slot,id,number,category,parent,name,questiontext,questiontextformat,image,generalfeedback,defaultgrade,penalty,qtype,length,stamp,version,hidden,timecreated,timemodified,createdby,modifiedby,maxmark
+1,1,1,5,0,questionessay-11,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+WeUaUK,localhost+080922073527+qxLjv1,0,1222068927,0,2,NULL,1
+2,2,2,5,0,questionessay-17,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+pvCseX,localhost+080922073527+mzy6tY,0,1222068927,0,2,NULL,1
+3,3,3,5,0,questionessay-6,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+Cr3gDO,localhost+080922073527+hnxfTy,0,1222068927,0,2,NULL,1
+4,4,4,5,0,questionessay-8,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+sSq9ln,localhost+080922073527+PGyab3,0,1222068927,0,2,NULL,1
+5,5,5,5,0,questionmatch-10,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+oG1i2f,localhost+080922073527+S1UxZy,0,1222068927,0,2,NULL,0
+6,6,6,5,0,questionmatch-16,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+vMFHyY,localhost+080922073527+4GZIyQ,0,1222068927,0,2,NULL,0
+7,7,7,5,0,questionmatch-18,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+Xkxqn1,localhost+080922073527+xbU6U7,0,1222068927,0,2,NULL,0
+8,8,8,5,0,questionmultianswer-12,This question consists of some text with an answer embedded right here {#1} and right after that you will have to deal with this short answer {#2} and finally we have a floating point number {#3}. Note that addresses like www.moodle.org and smileys :-) all work as normal: a) How good is this? {#4} b) What grade would you give it? {#5} Good luck!,0,,General feedback,8,0.1,multianswer,1,localhost+080922073527+0zKgpF,localhost+080922073527+r1gsde,0,1222068927,0,2,NULL,8
+9,14,9,5,0,questionmultichoice-13,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+AjIjeV,localhost+080922073527+UhtTLR,0,1222068927,0,2,NULL,1
+10,15,10,5,0,questionmultichoice-14,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+IrAqRl,localhost+080922073527+xRfta8,0,1222068927,0,2,NULL,1
+11,16,11,5,0,questionmultichoice-3,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+DMGirU,localhost+080922073527+689V8k,0,1222068927,0,2,NULL,1
+12,17,12,5,0,questionmultichoice-5,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+wileZw,localhost+080922073527+zGcaDa,0,1222068927,0,2,NULL,1
+13,25,13,5,0,Random Short-Answer Matching,"For each of the following questions, select the matching answer from the menu.
",1,,,1,0.1,randomsamatch,1,localhost+080922073724+qF803I,localhost+080922075820+zbZtaD,0,1222069044,1222070300,2,2,1
+14,22,14,5,0,Is Thai difficult?,Is Thai difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+2FLtCU,localhost+080922073655+fgUeOj,0,1222069015,0,2,NULL,1
+15,23,15,5,0,Is Thai grammar difficult?,Is Thai grammar difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+LYSD32,localhost+080922073655+WgRYk4,0,1222069015,0,2,NULL,1
+16,24,16,5,0,Is Thai pronunciation difficult?,Is Thai pronunciation difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+5p1w22,localhost+080922073655+g5jrXa,0,1222069015,0,2,NULL,1
+17,20,17,5,0,Who's buried in Grant's tomb?,Who's buried in Grant's tomb?,0,,,1,0.1,shortanswer,1,localhost+080922073655+PTDcDZ,localhost+080922073655+aghyfu,0,1222069015,0,2,NULL,1
+18,21,18,5,0,Who's buried in Jamie's tomb?,Who's buried in Jamie's tomb?,0,,,1,0.1,shortanswer,1,localhost+080922073655+Xvy1ns,localhost+080922073655+Mx0Izs,0,1222069015,0,2,NULL,1
+19,18,19,5,0,questiontruefalse-7,This question is really stupid,0,,Well done,1,1,truefalse,1,localhost+080922073527+9bzTef,localhost+080922073527+hjcQR1,0,1222068927,0,2,NULL,1
+20,19,20,5,0,questiontruefalse-9,This question is really stupid,0,,Well done,1,1,truefalse,1,localhost+080922073527+TI0yD4,localhost+080922073527+iXIulQ,0,1222068927,0,2,NULL,1
diff --git a/mod/quiz/report/statistics/simpletest/mdl_question_states.csv b/mod/quiz/report/statistics/simpletest/mdl_question_states.csv
index fb70f51a422..980b607d578 100644
--- a/mod/quiz/report/statistics/simpletest/mdl_question_states.csv
+++ b/mod/quiz/report/statistics/simpletest/mdl_question_states.csv
@@ -1,441 +1,441 @@
-"id","question","sumgrades","grade","answer"
-"39872","1","12.00000","0.0000000",
-"39873","2","12.00000","0.0000000",
-"39874","3","12.00000","0.0000000",
-"39875","4","12.00000","0.0000000",
-"39896","5","12.00000","0.0000000","2-511566162,1-386383057,3-789367675"
-"39897","6","12.00000","0.0000000","6-90576172,4-874542236,5-561218262"
-"39898","7","12.00000","0.0000000","8-572387695,9-204498291,7-955780029"
-"39899","8","12.00000","5.5000000","1-8,2-Wrong answer,3-24.3598688451,4-14,5-4.61546768417"
-"39900","14","12.00000","0.3000000","17,19,18:17"
-"39901","15","12.00000","0.9000000","21,20,22:21"
-"39902","16","12.00000","1.0000000","24,23,25:25"
-"39903","17","12.00000","0.3000000","28,27,26:26"
-"39910","18","12.00000","0.0000000","29"
-"39911","19","12.00000","1.0000000","32"
-"39908","20","12.00000","1.0000000","nobody"
-"39909","21","12.00000","0.0000000","The wrong answer"
-"39905","22","12.00000","0.0000000","The wrong answer"
-"39906","23","12.00000","0.0000000","The wrong answer"
-"39907","24","12.00000","1.0000000","no"
-"39904","25","12.00000","1.0000000","27-43,28-44"
-"39992","1","7.40000","0.0000000",
-"39993","2","7.40000","0.0000000",
-"39994","3","7.40000","0.0000000",
-"39995","4","7.40000","0.0000000",
-"40016","5","7.40000","0.0000000","2-386383057,3-789367675,1-511566162"
-"40017","6","7.40000","0.0000000","4-561218262,5-90576172,6-874542236"
-"40018","7","7.40000","0.0000000","8-955780029,7-572387695,9-204498291"
-"40019","8","7.40000","1.0000000","1-5,2-The wrong answer,3--31759.1567413,4-14,5-1461.25288609"
-"40020","14","7.40000","0.9000000","17,19,18:18"
-"40021","15","7.40000","0.9000000","22,20,21:21"
-"40022","16","7.40000","0.3000000","24,25,23:23"
-"40023","17","7.40000","0.3000000","28,27,26:26"
-"40030","18","7.40000","0.0000000","29"
-"40031","19","7.40000","1.0000000","32"
-"40028","20","7.40000","0.0000000","h560ljJn"
-"40029","21","7.40000","1.0000000","someone"
-"40025","22","7.40000","0.0000000","The wrong answer"
-"40026","23","7.40000","0.0000000","wQ3VDRbwS"
-"40027","24","7.40000","1.0000000","no"
-"40024","25","7.40000","1.0000000","28-44,26-41"
-"40032","1","11.70000","0.0000000",
-"40033","2","11.70000","0.0000000",
-"40034","3","11.70000","0.0000000",
-"40035","4","11.70000","0.0000000",
-"40056","5","11.70000","0.0000000","1-511566162,2-789367675,3-386383057"
-"40057","6","11.70000","0.0000000","4-90576172,6-561218262,5-874542236"
-"40058","7","11.70000","0.0000000","9-955780029,8-204498291,7-572387695"
-"40059","8","11.70000","2.5000000","1-7,2-Answer that gives half the credit,3--11453.355254,4-14,5-20166.4042138"
-"40060","14","11.70000","1.0000000","19,18,17:19"
-"40061","15","11.70000","0.9000000","20,22,21:21"
-"40062","16","11.70000","0.3000000","24,23,25:23"
-"40063","17","11.70000","1.0000000","27,28,26:28"
-"40070","18","11.70000","1.0000000","30"
-"40071","19","11.70000","1.0000000","32"
-"40068","20","11.70000","1.0000000","nobody"
-"40069","21","11.70000","1.0000000","somebody"
-"40065","22","11.70000","0.0000000","The wrong answer"
-"40066","23","11.70000","1.0000000","no"
-"40067","24","11.70000","1.0000000","no"
-"40064","25","11.70000","0.0000000","26-44,28-41"
-"39352","1","8.50000","0.0000000",
-"39353","2","8.50000","0.0000000",
-"39354","3","8.50000","0.0000000",
-"39355","4","8.50000","0.0000000",
-"39376","5","8.50000","0.0000000","1-789367675,2-511566162,3-386383057"
-"39377","6","8.50000","0.0000000","4-90576172,6-561218262,5-874542236"
-"39378","7","8.50000","0.0000000","7-955780029,9-204498291,8-572387695"
-"39379","8","8.50000","4.0000000","1-7,2-Correct answer,3-23.8650406295,4-15,5-13489.4978428"
-"39380","14","8.50000","0.3000000","19,18,17:17"
-"39381","15","8.50000","0.9000000","21,20,22:21"
-"39382","16","8.50000","1.0000000","24,23,25:25"
-"39383","17","8.50000","0.3000000","27,26,28:26"
-"39390","18","8.50000","0.0000000","29"
-"39391","19","8.50000","1.0000000","32"
-"39388","20","8.50000","1.0000000","nobody"
-"39389","21","8.50000","0.0000000","gZnFVM4nLKJ"
-"39385","22","8.50000","0.0000000","The wrong answer"
-"39386","23","8.50000","0.0000000","The wrong answer"
-"39387","24","8.50000","0.0000000","608GwpY"
-"39384","25","8.50000","0.0000000","27-41,26-43"
-"37112","1","10.10000","0.0000000",
-"37113","2","10.10000","0.0000000",
-"37114","3","10.10000","0.0000000",
-"37115","4","10.10000","0.0000000",
-"37136","5","10.10000","0.0000000","3-386383057,2-511566162,1-789367675"
-"37137","6","10.10000","0.0000000","5-561218262,4-90576172,6-874542236"
-"37138","7","10.10000","0.0000000","8-572387695,9-955780029,7-204498291"
-"37139","8","10.10000","3.0000000","1-6,2-The wrong answer,3-4194.34011879,4-15,5-1.51613103701"
-"37140","14","10.10000","0.9000000","18,19,17:18"
-"37141","15","10.10000","0.3000000","20,21,22:20"
-"37142","16","10.10000","0.9000000","24,23,25:24"
-"37143","17","10.10000","1.0000000","26,28,27:28"
-"37150","18","10.10000","1.0000000","30"
-"37151","19","10.10000","0.0000000","31"
-"37148","20","10.10000","1.0000000","nobody"
-"37149","21","10.10000","1.0000000","something"
-"37145","22","10.10000","1.0000000","yes"
-"37146","23","10.10000","0.0000000","tp5yPtP"
-"37147","24","10.10000","0.0000000","WpWsp"
-"37144","25","10.10000","0.0000000","28-41,26-44"
-"37432","1","11.30000","0.0000000",
-"37433","2","11.30000","0.0000000",
-"37434","3","11.30000","0.0000000",
-"37435","4","11.30000","0.0000000",
-"37456","5","11.30000","0.0000000","3-789367675,2-511566162,1-386383057"
-"37457","6","11.30000","0.0000000","4-561218262,5-874542236,6-90576172"
-"37458","7","11.30000","0.0000000","8-955780029,7-572387695,9-204498291"
-"37459","8","11.30000","4.5000000","1-8,2-Correct answer,3-23.8492275909,4-14,5--5146.21930938"
-"37460","14","11.30000","0.9000000","18,17,19:18"
-"37461","15","11.30000","1.0000000","20,22,21:22"
-"37462","16","11.30000","1.0000000","24,23,25:25"
-"37463","17","11.30000","0.9000000","26,28,27:27"
-"37470","18","11.30000","0.0000000","29"
-"37471","19","11.30000","0.0000000","31"
-"37468","20","11.30000","0.0000000","vWvkQNHX"
-"37469","21","11.30000","0.0000000","Llog6lWz"
-"37465","22","11.30000","1.0000000","yes"
-"37466","23","11.30000","1.0000000","no"
-"37467","24","11.30000","1.0000000","no"
-"37464","25","11.30000","0.0000000","28-43,27-44"
-"37472","1","11.20000","0.0000000",
-"37473","2","11.20000","0.0000000",
-"37474","3","11.20000","0.0000000",
-"37475","4","11.20000","0.0000000",
-"37496","5","11.20000","0.0000000","1-386383057,2-789367675,3-511566162"
-"37497","6","11.20000","0.0000000","4-90576172,6-561218262,5-874542236"
-"37498","7","11.20000","0.0000000","9-955780029,7-572387695,8-204498291"
-"37499","8","11.20000","3.0000000","1-5,2-The wrong answer,3-23.7261783051,4-14,5--32402.6363731"
-"37500","14","11.20000","0.3000000","19,18,17:17"
-"37501","15","11.20000","0.3000000","20,21,22:20"
-"37502","16","11.20000","0.3000000","25,24,23:23"
-"37503","17","11.20000","0.3000000","27,28,26:26"
-"37510","18","11.20000","1.0000000","30"
-"37511","19","11.20000","1.0000000","32"
-"37508","20","11.20000","1.0000000","no one"
-"37509","21","11.20000","1.0000000","someone"
-"37505","22","11.20000","0.0000000","HMwPMhROBnOQRn"
-"37506","23","11.20000","1.0000000","no"
-"37507","24","11.20000","1.0000000","no"
-"37504","25","11.20000","1.0000000","28-44,26-41"
-"37632","1","14.10000","0.0000000",
-"37633","2","14.10000","0.0000000",
-"37634","3","14.10000","0.0000000",
-"37635","4","14.10000","0.0000000",
-"37656","5","14.10000","0.0000000","3-789367675,1-386383057,2-511566162"
-"37657","6","14.10000","0.0000000","5-874542236,6-561218262,4-90576172"
-"37658","7","14.10000","0.0000000","7-955780029,8-204498291,9-572387695"
-"37659","8","14.10000","6.0000000","1-6,2-The wrong answer,3-23.7360174732,4-14,5-3.22848046312"
-"37660","14","14.10000","0.9000000","19,18,17:18"
-"37661","15","14.10000","0.9000000","21,22,20:21"
-"37662","16","14.10000","1.0000000","23,24,25:25"
-"37663","17","14.10000","0.3000000","26,28,27:26"
-"37670","18","14.10000","0.0000000","29"
-"37671","19","14.10000","1.0000000","32"
-"37668","20","14.10000","1.0000000","no one"
-"37669","21","14.10000","0.0000000","The wrong answer"
-"37665","22","14.10000","1.0000000","yes"
-"37666","23","14.10000","1.0000000","no"
-"37667","24","14.10000","1.0000000","no"
-"37664","25","14.10000","0.0000000","27-44,28-43"
-"37672","1","8.60000","0.0000000",
-"37673","2","8.60000","0.0000000",
-"37674","3","8.60000","0.0000000",
-"37675","4","8.60000","0.0000000",
-"37696","5","8.60000","0.0000000","1-511566162,3-386383057,2-789367675"
-"37697","6","8.60000","0.0000000","6-90576172,4-561218262,5-874542236"
-"37698","7","8.60000","0.0000000","8-204498291,7-572387695,9-955780029"
-"37699","8","8.60000","0.5000000","1-8,2-The wrong answer,3--21833.4044871,4-15,5--18147.2836286"
-"37700","14","8.60000","0.9000000","17,19,18:18"
-"37701","15","8.60000","1.0000000","22,20,21:22"
-"37702","16","8.60000","0.9000000","24,23,25:24"
-"37703","17","8.60000","0.3000000","28,27,26:26"
-"37710","18","8.60000","0.0000000","29"
-"37711","19","8.60000","1.0000000","32"
-"37708","20","8.60000","1.0000000","no one"
-"37709","21","8.60000","1.0000000","someone"
-"37705","22","8.60000","1.0000000","yes"
-"37706","23","8.60000","1.0000000","no"
-"37707","24","8.60000","0.0000000","The wrong answer"
-"37704","25","8.60000","0.0000000","27-44,28-43"
-"37712","1","11.60000","0.0000000",
-"37713","2","11.60000","0.0000000",
-"37714","3","11.60000","0.0000000",
-"37715","4","11.60000","0.0000000",
-"37736","5","11.60000","0.0000000","2-789367675,1-386383057,3-511566162"
-"37737","6","11.60000","0.0000000","4-561218262,6-874542236,5-90576172"
-"37738","7","11.60000","0.0000000","8-204498291,7-572387695,9-955780029"
-"37739","8","11.60000","5.5000000","1-7,2-Answer that gives half the credit,3--134.762172058,4-14,5-1.48445038141"
-"37740","14","11.60000","0.9000000","18,17,19:18"
-"37741","15","11.60000","1.0000000","21,20,22:22"
-"37742","16","11.60000","0.9000000","24,23,25:24"
-"37743","17","11.60000","0.3000000","26,27,28:26"
-"37750","18","11.60000","0.0000000","29"
-"37751","19","11.60000","0.0000000","31"
-"37748","20","11.60000","1.0000000","no one"
-"37749","21","11.60000","0.0000000","qbCiWy62bNNg5cl"
-"37745","22","11.60000","1.0000000","yes"
-"37746","23","11.60000","0.0000000","rak1hirG0wyS"
-"37747","24","11.60000","0.0000000","The wrong answer"
-"37744","25","11.60000","1.0000000","26-41,27-43"
-"38072","1","9.30000","0.0000000",
-"38073","2","9.30000","0.0000000",
-"38074","3","9.30000","0.0000000",
-"38075","4","9.30000","0.0000000",
-"38096","5","9.30000","0.0000000","1-789367675,2-386383057,3-511566162"
-"38097","6","9.30000","0.0000000","4-90576172,6-561218262,5-874542236"
-"38098","7","9.30000","0.0000000","9-204498291,7-572387695,8-955780029"
-"38099","8","9.30000","2.5000000","1-8,2-The wrong answer,3-24.4542806399,4-14,5--16990.335599"
-"38100","14","9.30000","0.9000000","18,17,19:18"
-"38101","15","9.30000","1.0000000","21,20,22:22"
-"38102","16","9.30000","1.0000000","25,24,23:25"
-"38103","17","9.30000","0.9000000","26,27,28:27"
-"38110","18","9.30000","0.0000000","29"
-"38111","19","9.30000","1.0000000","32"
-"38108","20","9.30000","1.0000000","no one"
-"38109","21","9.30000","0.0000000","GuqxaqNR81kfEM"
-"38105","22","9.30000","1.0000000","yes"
-"38106","23","9.30000","0.0000000","oPeCap"
-"38107","24","9.30000","0.0000000","The wrong answer"
-"38104","25","9.30000","0.0000000","26-44,28-41"
-"38232","1","13.10000","0.0000000",
-"38233","2","13.10000","0.0000000",
-"38234","3","13.10000","0.0000000",
-"38235","4","13.10000","0.0000000",
-"38256","5","13.10000","0.0000000","2-789367675,3-386383057,1-511566162"
-"38257","6","13.10000","0.0000000","5-90576172,4-561218262,6-874542236"
-"38258","7","13.10000","0.0000000","9-572387695,8-204498291,7-955780029"
-"38259","8","13.10000","4.0000000","1-5,2-The wrong answer,3-5915.53602752,4-14,5-3.78315457278"
-"38260","14","13.10000","0.9000000","19,17,18:18"
-"38261","15","13.10000","0.3000000","21,20,22:20"
-"38262","16","13.10000","0.9000000","25,24,23:24"
-"38263","17","13.10000","1.0000000","26,27,28:28"
-"38270","18","13.10000","0.0000000","29"
-"38271","19","13.10000","1.0000000","32"
-"38268","20","13.10000","1.0000000","nobody"
-"38269","21","13.10000","1.0000000","somebody"
-"38265","22","13.10000","1.0000000","yes"
-"38266","23","13.10000","1.0000000","no"
-"38267","24","13.10000","1.0000000","no"
-"38264","25","13.10000","0.0000000","27-44,28-43"
-"38272","1","11.50000","0.0000000",
-"38273","2","11.50000","0.0000000",
-"38274","3","11.50000","0.0000000",
-"38275","4","11.50000","0.0000000",
-"38296","5","11.50000","0.0000000","1-386383057,3-789367675,2-511566162"
-"38297","6","11.50000","0.0000000","5-874542236,4-561218262,6-90576172"
-"38298","7","11.50000","0.0000000","9-955780029,8-572387695,7-204498291"
-"38299","8","11.50000","4.0000000","1-6,2-kX5Sr56nKLjAlg,3--18108.5919771,4-14,5-2.88040082253"
-"38300","14","11.50000","0.9000000","18,17,19:18"
-"38301","15","11.50000","0.3000000","20,22,21:20"
-"38302","16","11.50000","0.3000000","24,25,23:23"
-"38303","17","11.50000","1.0000000","27,28,26:28"
-"38310","18","11.50000","1.0000000","30"
-"38311","19","11.50000","0.0000000","31"
-"38308","20","11.50000","0.0000000","The wrong answer"
-"38309","21","11.50000","1.0000000","somebody"
-"38305","22","11.50000","1.0000000","yes"
-"38306","23","11.50000","1.0000000","no"
-"38307","24","11.50000","0.0000000","wz0qF0phWo"
-"38304","25","11.50000","1.0000000","27-43,26-41"
-"38472","1","11.30000","0.0000000",
-"38473","2","11.30000","0.0000000",
-"38474","3","11.30000","0.0000000",
-"38475","4","11.30000","0.0000000",
-"38496","5","11.30000","0.0000000","1-386383057,2-789367675,3-511566162"
-"38497","6","11.30000","0.0000000","5-874542236,6-561218262,4-90576172"
-"38498","7","11.30000","0.0000000","8-572387695,9-204498291,7-955780029"
-"38499","8","11.30000","3.0000000","1-5,2-nCTxs,3--5848.53314078,4-15,5-2.33952287138"
-"38500","14","11.30000","1.0000000","19,18,17:19"
-"38501","15","11.30000","1.0000000","20,22,21:22"
-"38502","16","11.30000","0.3000000","25,23,24:23"
-"38503","17","11.30000","1.0000000","26,28,27:28"
-"38510","18","11.30000","1.0000000","30"
-"38511","19","11.30000","0.0000000","31"
-"38508","20","11.30000","1.0000000","nobody"
-"38509","21","11.30000","1.0000000","something"
-"38505","22","11.30000","1.0000000","yes"
-"38506","23","11.30000","0.0000000","7IROPXMblfEC"
-"38507","24","11.30000","1.0000000","no"
-"38504","25","11.30000","0.0000000","26-44,28-41"
-"38632","1","5.50000","0.0000000",
-"38633","2","5.50000","0.0000000",
-"38634","3","5.50000","0.0000000",
-"38635","4","5.50000","0.0000000",
-"38656","5","5.50000","0.0000000","3-511566162,1-789367675,2-386383057"
-"38657","6","5.50000","0.0000000","4-90576172,6-874542236,5-561218262"
-"38658","7","5.50000","0.0000000","9-204498291,8-572387695,7-955780029"
-"38659","8","5.50000","1.0000000","1-8,2-Answer that gives half the credit,3--17006.6469703,4-15,5--6504.33320749"
-"38660","14","5.50000","1.0000000","19,17,18:19"
-"38661","15","5.50000","0.9000000","21,22,20:21"
-"38662","16","5.50000","0.3000000","24,23,25:23"
-"38663","17","5.50000","0.3000000","27,28,26:26"
-"38670","18","5.50000","0.0000000","29"
-"38671","19","5.50000","0.0000000","31"
-"38668","20","5.50000","0.0000000","mcLXCaQ0nf"
-"38669","21","5.50000","0.0000000","The wrong answer"
-"38665","22","5.50000","0.0000000","bzl8AIwBOAGa"
-"38666","23","5.50000","1.0000000","no"
-"38667","24","5.50000","0.0000000","w9tmZeS"
-"38664","25","5.50000","1.0000000","27-43,26-41"
-"38672","1","4.80000","0.0000000",
-"38673","2","4.80000","0.0000000",
-"38674","3","4.80000","0.0000000",
-"38675","4","4.80000","0.0000000",
-"38696","5","4.80000","0.0000000","3-511566162,1-386383057,2-789367675"
-"38697","6","4.80000","0.0000000","5-90576172,6-561218262,4-874542236"
-"38698","7","4.80000","0.0000000","7-204498291,8-572387695,9-955780029"
-"38699","8","4.80000","1.0000000","1-6,2-Wrong answer,3-17220.6744582,4-14,5--2846.0946129"
-"38700","14","4.80000","0.3000000","17,19,18:17"
-"38701","15","4.80000","0.3000000","22,21,20:20"
-"38702","16","4.80000","0.3000000","24,23,25:23"
-"38703","17","4.80000","0.9000000","28,27,26:27"
-"38710","18","4.80000","0.0000000","29"
-"38711","19","4.80000","1.0000000","32"
-"38708","20","4.80000","0.0000000","The wrong answer"
-"38709","21","4.80000","0.0000000","JSIRVzHgPpKo3d4"
-"38705","22","4.80000","0.0000000","The wrong answer"
-"38706","23","4.80000","0.0000000","1tt76IaskUZVJ"
-"38707","24","4.80000","1.0000000","no"
-"38704","25","4.80000","0.0000000","27-41,26-43"
-"38992","1","8.70000","0.0000000",
-"38993","2","8.70000","0.0000000",
-"38994","3","8.70000","0.0000000",
-"38995","4","8.70000","0.0000000",
-"39016","5","8.70000","0.0000000","3-386383057,2-789367675,1-511566162"
-"39017","6","8.70000","0.0000000","6-874542236,5-90576172,4-561218262"
-"39018","7","8.70000","0.0000000","7-204498291,9-955780029,8-572387695"
-"39019","8","8.70000","1.5000000","1-6,2-Answer that gives half the credit,3--22154.8216407,4-14,5-23122.3489355"
-"39020","14","8.70000","1.0000000","17,18,19:19"
-"39021","15","8.70000","0.3000000","21,22,20:20"
-"39022","16","8.70000","0.9000000","25,24,23:24"
-"39023","17","8.70000","1.0000000","27,26,28:28"
-"39030","18","8.70000","0.0000000","29"
-"39031","19","8.70000","0.0000000","31"
-"39028","20","8.70000","1.0000000","no one"
-"39029","21","8.70000","0.0000000","aCGpaY"
-"39025","22","8.70000","1.0000000","yes"
-"39026","23","8.70000","0.0000000","The wrong answer"
-"39027","24","8.70000","1.0000000","no"
-"39024","25","8.70000","1.0000000","28-44,26-41"
-"39032","1","11.80000","0.0000000",
-"39033","2","11.80000","0.0000000",
-"39034","3","11.80000","0.0000000",
-"39035","4","11.80000","0.0000000",
-"39056","5","11.80000","0.0000000","3-386383057,2-789367675,1-511566162"
-"39057","6","11.80000","0.0000000","5-90576172,6-561218262,4-874542236"
-"39058","7","11.80000","0.0000000","8-572387695,7-204498291,9-955780029"
-"39059","8","11.80000","4.0000000","1-5,2-The wrong answer,3-12799.1604217,4-14,5-1.91687118712"
-"39060","14","11.80000","0.9000000","19,17,18:18"
-"39061","15","11.80000","1.0000000","20,21,22:22"
-"39062","16","11.80000","0.9000000","25,23,24:24"
-"39063","17","11.80000","1.0000000","28,26,27:28"
-"39070","18","11.80000","0.0000000","29"
-"39071","19","11.80000","0.0000000","31"
-"39068","20","11.80000","0.0000000","The wrong answer"
-"39069","21","11.80000","1.0000000","someone"
-"39065","22","11.80000","1.0000000","yes"
-"39066","23","11.80000","1.0000000","no"
-"39067","24","11.80000","1.0000000","no"
-"39064","25","11.80000","0.0000000","26-43,27-41"
-"39272","1","6.00000","0.0000000",
-"39273","2","6.00000","0.0000000",
-"39274","3","6.00000","0.0000000",
-"39275","4","6.00000","0.0000000",
-"39296","5","6.00000","0.0000000","1-789367675,2-511566162,3-386383057"
-"39297","6","6.00000","0.0000000","5-561218262,6-874542236,4-90576172"
-"39298","7","6.00000","0.0000000","9-204498291,8-955780029,7-572387695"
-"39299","8","6.00000","2.0000000","1-7,2-OhNBFCc,3-21451.3654305,4-14,5-13758.6327684"
-"39300","14","6.00000","0.9000000","19,17,18:18"
-"39301","15","6.00000","0.9000000","22,20,21:21"
-"39302","16","6.00000","0.3000000","24,25,23:23"
-"39303","17","6.00000","0.9000000","28,27,26:27"
-"39310","18","6.00000","0.0000000","29"
-"39311","19","6.00000","0.0000000","31"
-"39308","20","6.00000","0.0000000","The wrong answer"
-"39309","21","6.00000","0.0000000","zNTD0TDf"
-"39305","22","6.00000","1.0000000","yes"
-"39306","23","6.00000","0.0000000","The wrong answer"
-"39307","24","6.00000","0.0000000","The wrong answer"
-"39304","25","6.00000","0.0000000","26-44,28-41"
-"36192","1","5.90000","0.0000000",
-"36193","2","5.90000","0.0000000",
-"36194","3","5.90000","0.0000000",
-"36195","4","5.90000","0.0000000",
-"36216","5","5.90000","0.0000000","1-386383057,3-789367675,2-511566162"
-"36217","6","5.90000","0.0000000","5-90576172,4-874542236,6-561218262"
-"36218","7","5.90000","0.0000000","8-572387695,9-204498291,7-955780029"
-"36219","8","5.90000","1.0000000","1-7,2-pJkaD2j2i,3-6047.21651207,4-15,5-14488.106299"
-"36220","14","5.90000","1.0000000","17,19,18:19"
-"36221","15","5.90000","0.3000000","22,21,20:20"
-"36222","16","5.90000","0.3000000","23,25,24:23"
-"36223","17","5.90000","0.3000000","28,26,27:26"
-"36230","18","5.90000","0.0000000","29"
-"36231","19","5.90000","0.0000000","31"
-"36228","20","5.90000","1.0000000","no one"
-"36229","21","5.90000","0.0000000","HifU35OHtKrv9ao"
-"36225","22","5.90000","1.0000000","yes"
-"36226","23","5.90000","1.0000000","no"
-"36227","24","5.90000","0.0000000","The wrong answer"
-"36224","25","5.90000","0.0000000","26-44,28-41"
-"36352","1","12.80000","0.0000000",
-"36353","2","12.80000","0.0000000",
-"36354","3","12.80000","0.0000000",
-"36355","4","12.80000","0.0000000",
-"36376","5","12.80000","0.0000000","2-511566162,1-386383057,3-789367675"
-"36377","6","12.80000","0.0000000","6-874542236,5-561218262,4-90576172"
-"36378","7","12.80000","0.0000000","8-572387695,9-204498291,7-955780029"
-"36379","8","12.80000","6.0000000","1-7,2-Wrong answer,3-23.8238632628,4-15,5-4.09608160016"
-"36380","14","12.80000","0.9000000","18,19,17:18"
-"36381","15","12.80000","1.0000000","20,22,21:22"
-"36382","16","12.80000","1.0000000","24,25,23:25"
-"36383","17","12.80000","0.9000000","28,27,26:27"
-"36390","18","12.80000","1.0000000","30"
-"36391","19","12.80000","1.0000000","32"
-"36388","20","12.80000","0.0000000","The wrong answer"
-"36389","21","12.80000","0.0000000","The wrong answer"
-"36385","22","12.80000","0.0000000","The wrong answer"
-"36386","23","12.80000","0.0000000","The wrong answer"
-"36387","24","12.80000","1.0000000","no"
-"36384","25","12.80000","0.0000000","27-41,26-43"
-"36832","1","13.80000","0.0000000",
-"36833","2","13.80000","0.0000000",
-"36834","3","13.80000","0.0000000",
-"36835","4","13.80000","0.0000000",
-"36856","5","13.80000","0.0000000","3-511566162,2-386383057,1-789367675"
-"36857","6","13.80000","0.0000000","5-561218262,6-90576172,4-874542236"
-"36858","7","13.80000","0.0000000","8-204498291,9-955780029,7-572387695"
-"36859","8","13.80000","7.0000000","1-7,2-The wrong answer,3-23.8417280892,4-14,5-4.89551127517"
-"36860","14","13.80000","0.9000000","18,19,17:18"
-"36861","15","13.80000","0.3000000","21,22,20:20"
-"36862","16","13.80000","0.3000000","24,23,25:23"
-"36863","17","13.80000","0.3000000","27,26,28:26"
-"36870","18","13.80000","0.0000000","29"
-"36871","19","13.80000","0.0000000","31"
-"36868","20","13.80000","1.0000000","nobody"
-"36869","21","13.80000","1.0000000","something"
-"36865","22","13.80000","0.0000000","dauFTJ"
-"36866","23","13.80000","1.0000000","no"
-"36867","24","13.80000","1.0000000","no"
-"36864","25","13.80000","1.0000000","27-43,26-41"
+id,sumgrades,questionid,slot,maxmark,mark
+39872,12,1,1,1,0
+39873,12,2,2,1,0
+39874,12,3,3,1,0
+39875,12,4,4,1,0
+39896,12,5,5,0,0
+39897,12,6,6,0,0
+39898,12,7,7,0,0
+39899,12,8,8,8,5.5
+39900,12,14,9,1,0.3
+39901,12,15,10,1,0.9
+39902,12,16,11,1,1
+39903,12,17,12,1,0.3
+39910,12,18,19,1,0
+39911,12,19,20,1,1
+39908,12,20,17,1,1
+39909,12,21,18,1,0
+39905,12,22,14,1,0
+39906,12,23,15,1,0
+39907,12,24,16,1,1
+39904,12,25,13,1,1
+39992,7.4,1,1,1,0
+39993,7.4,2,2,1,0
+39994,7.4,3,3,1,0
+39995,7.4,4,4,1,0
+40016,7.4,5,5,0,0
+40017,7.4,6,6,0,0
+40018,7.4,7,7,0,0
+40019,7.4,8,8,8,1
+40020,7.4,14,9,1,0.9
+40021,7.4,15,10,1,0.9
+40022,7.4,16,11,1,0.3
+40023,7.4,17,12,1,0.3
+40030,7.4,18,19,1,0
+40031,7.4,19,20,1,1
+40028,7.4,20,17,1,0
+40029,7.4,21,18,1,1
+40025,7.4,22,14,1,0
+40026,7.4,23,15,1,0
+40027,7.4,24,16,1,1
+40024,7.4,25,13,1,1
+40032,11.7,1,1,1,0
+40033,11.7,2,2,1,0
+40034,11.7,3,3,1,0
+40035,11.7,4,4,1,0
+40056,11.7,5,5,0,0
+40057,11.7,6,6,0,0
+40058,11.7,7,7,0,0
+40059,11.7,8,8,8,2.5
+40060,11.7,14,9,1,1
+40061,11.7,15,10,1,0.9
+40062,11.7,16,11,1,0.3
+40063,11.7,17,12,1,1
+40070,11.7,18,19,1,1
+40071,11.7,19,20,1,1
+40068,11.7,20,17,1,1
+40069,11.7,21,18,1,1
+40065,11.7,22,14,1,0
+40066,11.7,23,15,1,1
+40067,11.7,24,16,1,1
+40064,11.7,25,13,1,0
+39352,8.5,1,1,1,0
+39353,8.5,2,2,1,0
+39354,8.5,3,3,1,0
+39355,8.5,4,4,1,0
+39376,8.5,5,5,0,0
+39377,8.5,6,6,0,0
+39378,8.5,7,7,0,0
+39379,8.5,8,8,8,4
+39380,8.5,14,9,1,0.3
+39381,8.5,15,10,1,0.9
+39382,8.5,16,11,1,1
+39383,8.5,17,12,1,0.3
+39390,8.5,18,19,1,0
+39391,8.5,19,20,1,1
+39388,8.5,20,17,1,1
+39389,8.5,21,18,1,0
+39385,8.5,22,14,1,0
+39386,8.5,23,15,1,0
+39387,8.5,24,16,1,0
+39384,8.5,25,13,1,0
+37112,10.1,1,1,1,0
+37113,10.1,2,2,1,0
+37114,10.1,3,3,1,0
+37115,10.1,4,4,1,0
+37136,10.1,5,5,0,0
+37137,10.1,6,6,0,0
+37138,10.1,7,7,0,0
+37139,10.1,8,8,8,3
+37140,10.1,14,9,1,0.9
+37141,10.1,15,10,1,0.3
+37142,10.1,16,11,1,0.9
+37143,10.1,17,12,1,1
+37150,10.1,18,19,1,1
+37151,10.1,19,20,1,0
+37148,10.1,20,17,1,1
+37149,10.1,21,18,1,1
+37145,10.1,22,14,1,1
+37146,10.1,23,15,1,0
+37147,10.1,24,16,1,0
+37144,10.1,25,13,1,0
+37432,11.3,1,1,1,0
+37433,11.3,2,2,1,0
+37434,11.3,3,3,1,0
+37435,11.3,4,4,1,0
+37456,11.3,5,5,0,0
+37457,11.3,6,6,0,0
+37458,11.3,7,7,0,0
+37459,11.3,8,8,8,4.5
+37460,11.3,14,9,1,0.9
+37461,11.3,15,10,1,1
+37462,11.3,16,11,1,1
+37463,11.3,17,12,1,0.9
+37470,11.3,18,19,1,0
+37471,11.3,19,20,1,0
+37468,11.3,20,17,1,0
+37469,11.3,21,18,1,0
+37465,11.3,22,14,1,1
+37466,11.3,23,15,1,1
+37467,11.3,24,16,1,1
+37464,11.3,25,13,1,0
+37472,11.2,1,1,1,0
+37473,11.2,2,2,1,0
+37474,11.2,3,3,1,0
+37475,11.2,4,4,1,0
+37496,11.2,5,5,0,0
+37497,11.2,6,6,0,0
+37498,11.2,7,7,0,0
+37499,11.2,8,8,8,3
+37500,11.2,14,9,1,0.3
+37501,11.2,15,10,1,0.3
+37502,11.2,16,11,1,0.3
+37503,11.2,17,12,1,0.3
+37510,11.2,18,19,1,1
+37511,11.2,19,20,1,1
+37508,11.2,20,17,1,1
+37509,11.2,21,18,1,1
+37505,11.2,22,14,1,0
+37506,11.2,23,15,1,1
+37507,11.2,24,16,1,1
+37504,11.2,25,13,1,1
+37632,14.1,1,1,1,0
+37633,14.1,2,2,1,0
+37634,14.1,3,3,1,0
+37635,14.1,4,4,1,0
+37656,14.1,5,5,0,0
+37657,14.1,6,6,0,0
+37658,14.1,7,7,0,0
+37659,14.1,8,8,8,6
+37660,14.1,14,9,1,0.9
+37661,14.1,15,10,1,0.9
+37662,14.1,16,11,1,1
+37663,14.1,17,12,1,0.3
+37670,14.1,18,19,1,0
+37671,14.1,19,20,1,1
+37668,14.1,20,17,1,1
+37669,14.1,21,18,1,0
+37665,14.1,22,14,1,1
+37666,14.1,23,15,1,1
+37667,14.1,24,16,1,1
+37664,14.1,25,13,1,0
+37672,8.6,1,1,1,0
+37673,8.6,2,2,1,0
+37674,8.6,3,3,1,0
+37675,8.6,4,4,1,0
+37696,8.6,5,5,0,0
+37697,8.6,6,6,0,0
+37698,8.6,7,7,0,0
+37699,8.6,8,8,8,0.5
+37700,8.6,14,9,1,0.9
+37701,8.6,15,10,1,1
+37702,8.6,16,11,1,0.9
+37703,8.6,17,12,1,0.3
+37710,8.6,18,19,1,0
+37711,8.6,19,20,1,1
+37708,8.6,20,17,1,1
+37709,8.6,21,18,1,1
+37705,8.6,22,14,1,1
+37706,8.6,23,15,1,1
+37707,8.6,24,16,1,0
+37704,8.6,25,13,1,0
+37712,11.6,1,1,1,0
+37713,11.6,2,2,1,0
+37714,11.6,3,3,1,0
+37715,11.6,4,4,1,0
+37736,11.6,5,5,0,0
+37737,11.6,6,6,0,0
+37738,11.6,7,7,0,0
+37739,11.6,8,8,8,5.5
+37740,11.6,14,9,1,0.9
+37741,11.6,15,10,1,1
+37742,11.6,16,11,1,0.9
+37743,11.6,17,12,1,0.3
+37750,11.6,18,19,1,0
+37751,11.6,19,20,1,0
+37748,11.6,20,17,1,1
+37749,11.6,21,18,1,0
+37745,11.6,22,14,1,1
+37746,11.6,23,15,1,0
+37747,11.6,24,16,1,0
+37744,11.6,25,13,1,1
+38072,9.3,1,1,1,0
+38073,9.3,2,2,1,0
+38074,9.3,3,3,1,0
+38075,9.3,4,4,1,0
+38096,9.3,5,5,0,0
+38097,9.3,6,6,0,0
+38098,9.3,7,7,0,0
+38099,9.3,8,8,8,2.5
+38100,9.3,14,9,1,0.9
+38101,9.3,15,10,1,1
+38102,9.3,16,11,1,1
+38103,9.3,17,12,1,0.9
+38110,9.3,18,19,1,0
+38111,9.3,19,20,1,1
+38108,9.3,20,17,1,1
+38109,9.3,21,18,1,0
+38105,9.3,22,14,1,1
+38106,9.3,23,15,1,0
+38107,9.3,24,16,1,0
+38104,9.3,25,13,1,0
+38232,13.1,1,1,1,0
+38233,13.1,2,2,1,0
+38234,13.1,3,3,1,0
+38235,13.1,4,4,1,0
+38256,13.1,5,5,0,0
+38257,13.1,6,6,0,0
+38258,13.1,7,7,0,0
+38259,13.1,8,8,8,4
+38260,13.1,14,9,1,0.9
+38261,13.1,15,10,1,0.3
+38262,13.1,16,11,1,0.9
+38263,13.1,17,12,1,1
+38270,13.1,18,19,1,0
+38271,13.1,19,20,1,1
+38268,13.1,20,17,1,1
+38269,13.1,21,18,1,1
+38265,13.1,22,14,1,1
+38266,13.1,23,15,1,1
+38267,13.1,24,16,1,1
+38264,13.1,25,13,1,0
+38272,11.5,1,1,1,0
+38273,11.5,2,2,1,0
+38274,11.5,3,3,1,0
+38275,11.5,4,4,1,0
+38296,11.5,5,5,0,0
+38297,11.5,6,6,0,0
+38298,11.5,7,7,0,0
+38299,11.5,8,8,8,4
+38300,11.5,14,9,1,0.9
+38301,11.5,15,10,1,0.3
+38302,11.5,16,11,1,0.3
+38303,11.5,17,12,1,1
+38310,11.5,18,19,1,1
+38311,11.5,19,20,1,0
+38308,11.5,20,17,1,0
+38309,11.5,21,18,1,1
+38305,11.5,22,14,1,1
+38306,11.5,23,15,1,1
+38307,11.5,24,16,1,0
+38304,11.5,25,13,1,1
+38472,11.3,1,1,1,0
+38473,11.3,2,2,1,0
+38474,11.3,3,3,1,0
+38475,11.3,4,4,1,0
+38496,11.3,5,5,0,0
+38497,11.3,6,6,0,0
+38498,11.3,7,7,0,0
+38499,11.3,8,8,8,3
+38500,11.3,14,9,1,1
+38501,11.3,15,10,1,1
+38502,11.3,16,11,1,0.3
+38503,11.3,17,12,1,1
+38510,11.3,18,19,1,1
+38511,11.3,19,20,1,0
+38508,11.3,20,17,1,1
+38509,11.3,21,18,1,1
+38505,11.3,22,14,1,1
+38506,11.3,23,15,1,0
+38507,11.3,24,16,1,1
+38504,11.3,25,13,1,0
+38632,5.5,1,1,1,0
+38633,5.5,2,2,1,0
+38634,5.5,3,3,1,0
+38635,5.5,4,4,1,0
+38656,5.5,5,5,0,0
+38657,5.5,6,6,0,0
+38658,5.5,7,7,0,0
+38659,5.5,8,8,8,1
+38660,5.5,14,9,1,1
+38661,5.5,15,10,1,0.9
+38662,5.5,16,11,1,0.3
+38663,5.5,17,12,1,0.3
+38670,5.5,18,19,1,0
+38671,5.5,19,20,1,0
+38668,5.5,20,17,1,0
+38669,5.5,21,18,1,0
+38665,5.5,22,14,1,0
+38666,5.5,23,15,1,1
+38667,5.5,24,16,1,0
+38664,5.5,25,13,1,1
+38672,4.8,1,1,1,0
+38673,4.8,2,2,1,0
+38674,4.8,3,3,1,0
+38675,4.8,4,4,1,0
+38696,4.8,5,5,0,0
+38697,4.8,6,6,0,0
+38698,4.8,7,7,0,0
+38699,4.8,8,8,8,1
+38700,4.8,14,9,1,0.3
+38701,4.8,15,10,1,0.3
+38702,4.8,16,11,1,0.3
+38703,4.8,17,12,1,0.9
+38710,4.8,18,19,1,0
+38711,4.8,19,20,1,1
+38708,4.8,20,17,1,0
+38709,4.8,21,18,1,0
+38705,4.8,22,14,1,0
+38706,4.8,23,15,1,0
+38707,4.8,24,16,1,1
+38704,4.8,25,13,1,0
+38992,8.7,1,1,1,0
+38993,8.7,2,2,1,0
+38994,8.7,3,3,1,0
+38995,8.7,4,4,1,0
+39016,8.7,5,5,0,0
+39017,8.7,6,6,0,0
+39018,8.7,7,7,0,0
+39019,8.7,8,8,8,1.5
+39020,8.7,14,9,1,1
+39021,8.7,15,10,1,0.3
+39022,8.7,16,11,1,0.9
+39023,8.7,17,12,1,1
+39030,8.7,18,19,1,0
+39031,8.7,19,20,1,0
+39028,8.7,20,17,1,1
+39029,8.7,21,18,1,0
+39025,8.7,22,14,1,1
+39026,8.7,23,15,1,0
+39027,8.7,24,16,1,1
+39024,8.7,25,13,1,1
+39032,11.8,1,1,1,0
+39033,11.8,2,2,1,0
+39034,11.8,3,3,1,0
+39035,11.8,4,4,1,0
+39056,11.8,5,5,0,0
+39057,11.8,6,6,0,0
+39058,11.8,7,7,0,0
+39059,11.8,8,8,8,4
+39060,11.8,14,9,1,0.9
+39061,11.8,15,10,1,1
+39062,11.8,16,11,1,0.9
+39063,11.8,17,12,1,1
+39070,11.8,18,19,1,0
+39071,11.8,19,20,1,0
+39068,11.8,20,17,1,0
+39069,11.8,21,18,1,1
+39065,11.8,22,14,1,1
+39066,11.8,23,15,1,1
+39067,11.8,24,16,1,1
+39064,11.8,25,13,1,0
+39272,6,1,1,1,0
+39273,6,2,2,1,0
+39274,6,3,3,1,0
+39275,6,4,4,1,0
+39296,6,5,5,0,0
+39297,6,6,6,0,0
+39298,6,7,7,0,0
+39299,6,8,8,8,2
+39300,6,14,9,1,0.9
+39301,6,15,10,1,0.9
+39302,6,16,11,1,0.3
+39303,6,17,12,1,0.9
+39310,6,18,19,1,0
+39311,6,19,20,1,0
+39308,6,20,17,1,0
+39309,6,21,18,1,0
+39305,6,22,14,1,1
+39306,6,23,15,1,0
+39307,6,24,16,1,0
+39304,6,25,13,1,0
+36192,5.9,1,1,1,0
+36193,5.9,2,2,1,0
+36194,5.9,3,3,1,0
+36195,5.9,4,4,1,0
+36216,5.9,5,5,0,0
+36217,5.9,6,6,0,0
+36218,5.9,7,7,0,0
+36219,5.9,8,8,8,1
+36220,5.9,14,9,1,1
+36221,5.9,15,10,1,0.3
+36222,5.9,16,11,1,0.3
+36223,5.9,17,12,1,0.3
+36230,5.9,18,19,1,0
+36231,5.9,19,20,1,0
+36228,5.9,20,17,1,1
+36229,5.9,21,18,1,0
+36225,5.9,22,14,1,1
+36226,5.9,23,15,1,1
+36227,5.9,24,16,1,0
+36224,5.9,25,13,1,0
+36352,12.8,1,1,1,0
+36353,12.8,2,2,1,0
+36354,12.8,3,3,1,0
+36355,12.8,4,4,1,0
+36376,12.8,5,5,0,0
+36377,12.8,6,6,0,0
+36378,12.8,7,7,0,0
+36379,12.8,8,8,8,6
+36380,12.8,14,9,1,0.9
+36381,12.8,15,10,1,1
+36382,12.8,16,11,1,1
+36383,12.8,17,12,1,0.9
+36390,12.8,18,19,1,1
+36391,12.8,19,20,1,1
+36388,12.8,20,17,1,0
+36389,12.8,21,18,1,0
+36385,12.8,22,14,1,0
+36386,12.8,23,15,1,0
+36387,12.8,24,16,1,1
+36384,12.8,25,13,1,0
+36832,13.8,1,1,1,0
+36833,13.8,2,2,1,0
+36834,13.8,3,3,1,0
+36835,13.8,4,4,1,0
+36856,13.8,5,5,0,0
+36857,13.8,6,6,0,0
+36858,13.8,7,7,0,0
+36859,13.8,8,8,8,7
+36860,13.8,14,9,1,0.9
+36861,13.8,15,10,1,0.3
+36862,13.8,16,11,1,0.3
+36863,13.8,17,12,1,0.3
+36870,13.8,18,19,1,0
+36871,13.8,19,20,1,0
+36868,13.8,20,17,1,1
+36869,13.8,21,18,1,1
+36865,13.8,22,14,1,0
+36866,13.8,23,15,1,1
+36867,13.8,24,16,1,1
+36864,13.8,25,13,1,1
diff --git a/mod/quiz/report/statistics/simpletest/test_qstats.php b/mod/quiz/report/statistics/simpletest/test_qstats.php
index f234aecb52f..185ea2f5df5 100644
--- a/mod/quiz/report/statistics/simpletest/test_qstats.php
+++ b/mod/quiz/report/statistics/simpletest/test_qstats.php
@@ -1,77 +1,129 @@
.
+
/**
* Unit tests for (some of) mod/quiz/report/statistics/qstats.php.
*
- * @copyright ยฉ 2006 Jamie Pratt
- * @author me@jamiep.org
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-/** */
-require_once(dirname(__FILE__) . '/../../../../../config.php');
-require_once($CFG->libdir . '/simpletestlib.php'); // Include the test libraries
-require_once("$CFG->dirroot/mod/quiz/report/statistics/qstats.php");
-require_once($CFG->dirroot.'/mod/quiz/locallib.php');
-require_once($CFG->dirroot.'/mod/quiz/report/reportlib.php');
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
+
/**
- * This class contains the test cases for the functions in qstats.php.
+ * Test helper subclass of quiz_statistics_question_stats
*
- * */
-class quiz_report_qstats_test extends UnitTestCase {
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_quiz_statistics_question_stats extends quiz_statistics_question_stats {
+ public function set_step_data($states) {
+ $this->lateststeps = $states;
+ }
+
+ protected function get_random_guess_score($questiondata) {
+ return 0;
+ }
+}
+
+
+/**
+ * Unit tests for (some of) quiz_statistics_question_stats.
+ *
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_statistics_question_stats_test extends UnitTestCase {
public static $includecoverage = array('mod/quiz/report/reportlib.php');
- /**
- * @var qstats object created to test class
- */
- var $qstats;
- function test_qstats() {
+ /** @var qstats object created to test class. */
+ protected $qstats;
+
+ public function test_qstats() {
global $CFG;
- //data is taken from randomly generated attempts data generated by contrib/tools/generators/qagenerator/
- $states = $this->get_records_from_csv($CFG->dirroot.'/mod/quiz/report/statistics/simpletest/mdl_question_states.csv');
- //data is taken from questions mostly generated by contrib/tools/generators/generator.php
- $questions = $this->get_records_from_csv($CFG->dirroot.'/mod/quiz/report/statistics/simpletest/mdl_question.csv');
- $this->qstats = new qstats($questions, 22, 10045.45455);
- $this->qstats->states = $states;
- $this->qstats->process_states();
+ // Data is taken from randomly generated attempts data generated by
+ // contrib/tools/generators/qagenerator/
+ $steps = $this->get_records_from_csv($CFG->dirroot .
+ '/mod/quiz/report/statistics/simpletest/mdl_question_states.csv');
+ // Data is taken from questions mostly generated by
+ // contrib/tools/generators/generator.php
+ $questions = $this->get_records_from_csv($CFG->dirroot .
+ '/mod/quiz/report/statistics/simpletest/mdl_question.csv');
+ $this->qstats = new testable_quiz_statistics_question_stats($questions, 22, 10045.45455);
+ $this->qstats->set_step_data($steps);
+ $this->qstats->compute_statistics();
- //values expected are taken from contrib/tools/quiz_tools/stats.xls
- $facility = array(0,0,0,0,null,null,null,41.19318182,81.36363636,71.36363636,65.45454545,65.90909091,36.36363636,59.09090909,50,59.09090909,63.63636364,45.45454545,27.27272727,50);
+ // Values expected are taken from contrib/tools/quiz_tools/stats.xls
+ $facility = array(0, 0, 0, 0, null, null, null, 41.19318182, 81.36363636,
+ 71.36363636, 65.45454545, 65.90909091, 36.36363636, 59.09090909, 50,
+ 59.09090909, 63.63636364, 45.45454545, 27.27272727, 50);
$this->qstats_q_fields('facility', $facility, 100);
- $sd = array(0,0,0,0,null,null,null,1912.733589,251.2738111,322.6312277,333.4199022,337.5811591,492.3659639,503.2362797,511.7663157,503.2362797,492.3659639,509.6471914,455.8423058,511.7663157);
+ $sd = array(0, 0, 0, 0, null, null, null, 1912.733589, 251.2738111,
+ 322.6312277, 333.4199022, 337.5811591, 492.3659639, 503.2362797,
+ 511.7663157, 503.2362797, 492.3659639, 509.6471914, 455.8423058, 511.7663157);
$this->qstats_q_fields('sd', $sd, 1000);
- $effectiveweight = array(0,0,0,0,0,0,0,26.58464457,3.368456046,3.253955259,7.584083694,3.79658376,3.183278505,4.532356904,7.78856243,10.08351572,8.381139345,8.727645713,7.946277111,4.769500946);
+ $effectiveweight = array(0, 0, 0, 0, 0, 0, 0, 26.58464457, 3.368456046,
+ 3.253955259, 7.584083694, 3.79658376, 3.183278505, 4.532356904,
+ 7.78856243, 10.08351572, 8.381139345, 8.727645713, 7.946277111, 4.769500946);
$this->qstats_q_fields('effectiveweight', $effectiveweight);
- $discriminationindex = array(null,null,null,null,null,null,null,25.88327077,1.170256965,-4.207816809,28.16930644,-2.513606859,-12.99017581,-8.900638238,8.670004606,29.63337745,15.18945843,16.21079629,15.52451404,-8.396734802);
+ $discriminationindex = array(null, null, null, null, null, null, null,
+ 25.88327077, 1.170256965, -4.207816809, 28.16930644, -2.513606859,
+ -12.99017581, -8.900638238, 8.670004606, 29.63337745, 15.18945843,
+ 16.21079629, 15.52451404, -8.396734802);
$this->qstats_q_fields('discriminationindex', $discriminationindex);
- $discriminativeefficiency = array(null,null,null,null,null,null,null,27.23492723,1.382386552,-4.691171307,31.12404354,-2.877487579,-17.5074184,-10.27568922,10.86956522,34.58997279,17.4790556,20.14359793,22.06477733,-10);
+ $discriminativeefficiency = array(null, null, null, null, null, null, null,
+ 27.23492723, 1.382386552, -4.691171307, 31.12404354, -2.877487579,
+ -17.5074184, -10.27568922, 10.86956522, 34.58997279, 17.4790556,
+ 20.14359793, 22.06477733, -10);
$this->qstats_q_fields('discriminativeefficiency', $discriminativeefficiency);
}
- function qstats_q_fields($fieldname, $values, $multiplier=1) {
- foreach ($this->qstats->questions as $question){
+
+ public function qstats_q_fields($fieldname, $values, $multiplier=1) {
+ foreach ($this->qstats->questions as $question) {
$value = array_shift($values);
- if ($value !== null){
- $this->assertWithinMargin($question->_stats->{$fieldname}*$multiplier, $value, 1E-6);
+ if ($value !== null) {
+ $this->assertWithinMargin($question->_stats->{$fieldname} * $multiplier,
+ $value, 1E-6);
} else {
- $this->assertEqual($question->_stats->{$fieldname}*$multiplier, $value);
+ $this->assertEqual($question->_stats->{$fieldname} * $multiplier, $value);
}
}
}
- function get_fields_from_csv($line){
+ public function get_fields_from_csv($line) {
$line = trim($line);
$items = preg_split('!,!', $line);
- while (list($key) = each($items)){
- if ($items[$key]!=''){
- if ($start = ($items[$key]{0}=='"')){
+ while (list($key) = each($items)) {
+ if ($items[$key]!='') {
+ if ($start = ($items[$key]{0}=='"')) {
$items[$key] = substr($items[$key], 1);
- while (!$end = ($items[$key]{strlen($items[$key])-1}=='"')){
+ while (!$end = ($items[$key]{strlen($items[$key])-1}=='"')) {
$item = $items[$key];
unset($items[$key]);
list($key) = each($items);
- $items[$key] = $item.','.$items[$key];
+ $items[$key] = $item . ',' . $items[$key];
}
$items[$key] = substr($items[$key], 0, strlen($items[$key])-1);
}
@@ -81,17 +133,17 @@ class quiz_report_qstats_test extends UnitTestCase {
return $items;
}
- function get_records_from_csv($filename){
+ public function get_records_from_csv($filename) {
$filecontents = file($filename, FILE_IGNORE_NEW_LINES);
$records = array();
$keys = $this->get_fields_from_csv(array_shift($filecontents));//first line is field names
- while (NULL !== ($line = array_shift($filecontents))) {
+ while (null !== ($line = array_shift($filecontents))) {
$data = $this->get_fields_from_csv($line);
$arraykey = reset($data);
$object = new stdClass();
foreach ($keys as $key) {
$value = array_shift($data);
- if ($value !== NULL){
+ if ($value !== null) {
$object->{$key} = $value;
} else {
$object->{$key} = '';
@@ -102,7 +154,3 @@ class quiz_report_qstats_test extends UnitTestCase {
return $records;
}
}
-
-//$test = new quiz_report_qstats_test();
-//$test->test_qstats();
-
diff --git a/mod/quiz/report/statistics/statistics_form.php b/mod/quiz/report/statistics/statistics_form.php
index 7bdc068d37a..e3c285ccf6f 100644
--- a/mod/quiz/report/statistics/statistics_form.php
+++ b/mod/quiz/report/statistics/statistics_form.php
@@ -1,20 +1,55 @@
libdir/formslib.php";
-class mod_quiz_report_statistics extends moodleform {
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
- function definition() {
- global $COURSE;
- $mform =& $this->_form;
-//-------------------------------------------------------------------------------
- $mform->addElement('header', 'preferencespage', get_string('preferencespage', 'quiz_overview'));
+/**
+ * Quiz statistics settings form definition.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+
+/**
+ * This is the settings form for the quiz statistics report.
+ *
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_statistics_statistics_settings_form extends moodleform {
+ protected function definition() {
+ $mform = $this->_form;
+
+ $mform->addElement('header', 'preferencespage',
+ get_string('preferencespage', 'quiz_overview'));
$options = array();
- $options[0] = get_string('attemptsfirst','quiz_statistics');
- $options[1] = get_string('attemptsall','quiz_statistics');
- $mform->addElement('select', 'useallattempts', get_string('calculatefrom', 'quiz_statistics'), $options);
+ $options[0] = get_string('attemptsfirst', 'quiz_statistics');
+ $options[1] = get_string('attemptsall', 'quiz_statistics');
+ $mform->addElement('select', 'useallattempts',
+ get_string('calculatefrom', 'quiz_statistics'), $options);
$mform->setDefault('useallattempts', 0);
-//-------------------------------------------------------------------------------
- $mform->addElement('submit', 'submitbutton', get_string('preferencessave', 'quiz_overview'));
+
+ $mform->addElement('submit', 'submitbutton',
+ get_string('preferencessave', 'quiz_overview'));
}
}
-
diff --git a/mod/quiz/report/statistics/statistics_graph.php b/mod/quiz/report/statistics/statistics_graph.php
index 08a8af5976d..6ba700c2279 100644
--- a/mod/quiz/report/statistics/statistics_graph.php
+++ b/mod/quiz/report/statistics/statistics_graph.php
@@ -1,104 +1,166 @@
dirroot."/lib/graphlib.php";
-include $CFG->dirroot."/mod/quiz/locallib.php";
-include $CFG->dirroot."/mod/quiz/report/reportlib.php";
-function graph_get_new_colour(){
- static $colourindex = 0;
- $colours = array('red', 'green', 'yellow', 'orange', 'purple', 'black', 'maroon', 'blue', 'ltgreen', 'navy', 'ltred', 'ltltgreen', 'ltltorange', 'olive', 'gray', 'ltltred', 'ltorange', 'lime', 'ltblue', 'ltltblue');
- $colour = $colours[$colourindex];
- $colourindex++;
- if ($colourindex > (count($colours)-1)){
- $colourindex =0;
- }
- return $colour;
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * This script renders the quiz statistics graph.
+ *
+ * It takes one parameter, the quiz_statistics.id. This is enough to identify the
+ * quiz etc.
+ *
+ * It plots a bar graph showing certain question statistics plotted against
+ * question number.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+require_once(dirname(__FILE__) . '/../../../../config.php');
+require_once($CFG->libdir . '/graphlib.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
+
+
+/**
+ * This helper function returns a sequence of colours each time it is called.
+ * Used for chooseing colours for graph data series.
+ * @return string colour name.
+ */
+function graph_get_new_colour() {
+ static $colourindex = -1;
+ $colours = array('red', 'green', 'yellow', 'orange', 'purple', 'black',
+ 'maroon', 'blue', 'ltgreen', 'navy', 'ltred', 'ltltgreen', 'ltltorange',
+ 'olive', 'gray', 'ltltred', 'ltorange', 'lime', 'ltblue', 'ltltblue');
+
+ $colourindex = ($colourindex + 1) % count($colours);
+
+ return $colours[$colourindex];
}
+
+// Get the parameters.
$quizstatisticsid = required_param('id', PARAM_INT);
+// Load enough data to check permissions.
$quizstatistics = $DB->get_record('quiz_statistics', array('id' => $quizstatisticsid));
-$questionstatistics = $DB->get_records('quiz_question_statistics', array('quizstatisticsid' => $quizstatistics->id, 'subquestion' => 0));
-$quiz = $DB->get_record('quiz', array('id' => $quizstatistics->quizid));
-require_login($quiz->course);
-$questions = quiz_report_load_questions($quiz);
+$quiz = $DB->get_record('quiz', array('id' => $quizstatistics->quizid), '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
-if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
+
+// Check access.
+require_login($quiz->course, false, $cm);
+$modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
+require_capability('quiz/statistics:view', $modcontext);
+
+if (groups_get_activity_groupmode($cm)) {
$groups = groups_get_activity_allowed_groups($cm);
} else {
- $groups = false;
+ $groups = array();
}
-if ($quizstatistics->groupid){
- if (!in_array($quizstatistics->groupid, array_keys($groups))){
- print_error('groupnotamember', 'group');
- }
+if ($quizstatistics->groupid && !in_array($quizstatistics->groupid, array_keys($groups))) {
+ print_error('groupnotamember', 'group');
}
-$modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
-require_capability('quizreport/statistics:view', $modcontext);
-$line = new graph(800,600);
-$line->parameter['title'] = '';
-$line->parameter['y_label_left'] = '%';
-$line->parameter['x_label'] = get_string('position', 'quiz_statistics');
-$line->parameter['y_label_angle'] = 90;
-$line->parameter['x_label_angle'] = 0;
-$line->parameter['x_axis_angle'] = 60;
+// Load the rest of the required data.
+$questions = quiz_report_get_significant_questions($quiz);
+$questionstatistics = $DB->get_records_select('quiz_question_statistics',
+ 'quizstatisticsid = ? AND slot IS NOT NULL', array($quizstatistics->id));
-$line->parameter['legend'] = 'outside-right';
-$line->parameter['legend_border'] = 'black';
-$line->parameter['legend_offset'] = 4;
+// Create the graph, and set the basic options.
+$graph = new graph(800, 600);
+$graph->parameter['title'] = '';
+$graph->parameter['y_label_left'] = '%';
+$graph->parameter['x_label'] = get_string('position', 'quiz_statistics');
+$graph->parameter['y_label_angle'] = 90;
+$graph->parameter['x_label_angle'] = 0;
+$graph->parameter['x_axis_angle'] = 60;
-$line->parameter['bar_size'] = 1;
+$graph->parameter['legend'] = 'outside-right';
+$graph->parameter['legend_border'] = 'black';
+$graph->parameter['legend_offset'] = 4;
-$line->parameter['zero_axis'] = 'grayEE';
+$graph->parameter['bar_size'] = 1;
+$graph->parameter['zero_axis'] = 'grayEE';
-$fieldstoplot = array('facility' => get_string('facility', 'quiz_statistics'), 'discriminativeefficiency' => get_string('discriminative_efficiency', 'quiz_statistics'));
+// Configure what to display.
+$fieldstoplot = array(
+ 'facility' => get_string('facility', 'quiz_statistics'),
+ 'discriminativeefficiency' => get_string('discriminative_efficiency', 'quiz_statistics')
+);
$fieldstoplotfactor = array('facility' => 100, 'discriminativeefficiency' => 1);
-$line->x_data = array();
-foreach (array_keys($fieldstoplot) as $fieldtoplot){
- $line->y_data[$fieldtoplot] = array();
- $line->y_format[$fieldtoplot] =
- array('colour' => graph_get_new_colour(), 'bar' => 'fill', 'shadow_offset' => 1, 'legend' => $fieldstoplot[$fieldtoplot]);
+// Prepare the arrays to hold the data.
+$xdata = array();
+foreach (array_keys($fieldstoplot) as $fieldtoplot) {
+ $ydata[$fieldtoplot] = array();
+ $graph->y_format[$fieldtoplot] = array(
+ 'colour' => graph_get_new_colour(),
+ 'bar' => 'fill',
+ 'shadow_offset' => 1,
+ 'legend' => $fieldstoplot[$fieldtoplot]
+ );
}
-foreach ($questionstatistics as $questionstatistic){
- $line->x_data[$questions[$questionstatistic->questionid]->number] = $questions[$questionstatistic->questionid]->number;
- foreach (array_keys($fieldstoplot) as $fieldtoplot){
- $value = !is_null($questionstatistic->$fieldtoplot)?$questionstatistic->$fieldtoplot:0;
- $value = $value * $fieldstoplotfactor[$fieldtoplot];
- $line->y_data[$fieldtoplot][$questions[$questionstatistic->questionid]->number] = $value;
+
+// Fill in the data for each question.
+foreach ($questionstatistics as $questionstatistic) {
+ $number = $questions[$questionstatistic->slot]->number;
+ $xdata[$number] = $number;
+
+ foreach ($fieldstoplot as $fieldtoplot => $notused) {
+ $value = $questionstatistic->$fieldtoplot;
+ if (is_null($value)) {
+ $value = 0;
+ }
+ $value *= $fieldstoplotfactor[$fieldtoplot];
+
+ $ydata[$fieldtoplot][$number] = $value;
}
}
-foreach (array_keys($line->y_data) as $fieldtoplot){
- ksort($line->y_data[$fieldtoplot]);
- $line->y_data[$fieldtoplot] = array_values($line->y_data[$fieldtoplot]);
+
+// Sort the fields into order.
+sort($xdata);
+$graph->x_data = array_values($xdata);
+
+foreach ($fieldstoplot as $fieldtoplot => $notused) {
+ ksort($ydata[$fieldtoplot]);
+ $graph->y_data[$fieldtoplot] = array_values($ydata[$fieldtoplot]);
}
-ksort($line->x_data);
-$line->x_data = array_values($line->x_data);
+$graph->y_order = array_keys($fieldstoplot);
+
+// Find appropriate axis limits.
$max = 0;
$min = 0;
-foreach (array_keys($fieldstoplot) as $fieldtoplot){
- ksort($line->y_data[$fieldtoplot]);
- $line->y_data[$fieldtoplot] = array_values($line->y_data[$fieldtoplot]);
- $max = (max($line->y_data[$fieldtoplot])> $max)? max($line->y_data[$fieldtoplot]): $max;
- $min = (min($line->y_data[$fieldtoplot])< $min)? min($line->y_data[$fieldtoplot]): $min;
+foreach ($fieldstoplot as $fieldtoplot => $notused) {
+ $max = max($max, max($graph->y_data[$fieldtoplot]));
+ $min = min($min, min($graph->y_data[$fieldtoplot]));
}
-$line->y_order = array_keys($fieldstoplot);
+// Set the remaining graph options that depend on the data.
$gridresolution = 10;
-
$max = ceil($max / $gridresolution) * $gridresolution;
$min = floor($min / $gridresolution) * $gridresolution;
+$gridlines = ceil(($max - $min) / $gridresolution) + 1;
-$gridlines = ceil(($max - $min) / $gridresolution);
+$graph->parameter['y_axis_gridlines'] = $gridlines;
+$graph->parameter['y_min_left'] = $min;
+$graph->parameter['y_max_left'] = $max;
+$graph->parameter['y_decimal_left'] = 0;
-$line->parameter['y_axis_gridlines'] = $gridlines+1;
-
-$line->parameter['y_min_left'] = $min;
-$line->parameter['y_max_left'] = $max;
-$line->parameter['y_decimal_left'] = 0;
-
-
-$line->draw();
-
+// Output the graph.
+$graph->draw();
diff --git a/mod/quiz/report/statistics/statistics_question_table.php b/mod/quiz/report/statistics/statistics_question_table.php
index 27811429cd9..7ac4eb82cfd 100644
--- a/mod/quiz/report/statistics/statistics_question_table.php
+++ b/mod/quiz/report/statistics/statistics_question_table.php
@@ -1,92 +1,147 @@
libdir.'/tablelib.php');
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+/**
+ * Quiz statistics report, table for showing statistics about a particular question.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/tablelib.php');
+
+
+/**
+ * This table shows statistics about a particular question.
+ *
+ * Lists the responses that students made to this question, with frequency counts.
+ *
+ * The responses may be grouped, either by subpart of the question, or by the
+ * answer they match.
+ *
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class quiz_report_statistics_question_table extends flexible_table {
- /**
- * @var object this question with _stats object.
- */
- var $question;
+ /** @var object this question with a _stats field. */
+ protected $questiondata;
- function quiz_report_statistics_question_table($qid){
- parent::__construct('mod-quiz-report-statistics-question-table'.$qid);
+ /**
+ * Constructor.
+ * @param $qid the id of the particular question whose statistics are being
+ * displayed.
+ */
+ public function __construct($qid) {
+ parent::__construct('mod-quiz-report-statistics-question-table' . $qid);
}
/**
* Setup the columns and headers and other properties of the table and then
* call flexible_table::setup() method.
+ *
+ * @param moodle_url $reporturl the URL to redisplay this report.
+ * @param object $question a question with a _stats field
+ * @param bool $hassubqs
*/
- function setup($reporturl, $question, $hassubqs){
- $this->question = $question;
+ public function setup($reporturl, $questiondata,
+ quiz_statistics_response_analyser $responesstats) {
+ $this->questiondata = $questiondata;
+
+ $this->define_baseurl($reporturl->out());
+ $this->collapsible(false);
+ $this->set_attribute('class', 'generaltable generalbox boxaligncenter');
+
// Define table columns
$columns = array();
$headers = array();
- if ($hassubqs){
- $columns[]= 'subq';
- $headers[]= '';
+ if ($responesstats->has_subparts()) {
+ $columns[] = 'part';
+ $headers[] = 'Part of question';
}
- $columns[]= 'response';
- $headers[]= get_string('response', 'quiz_statistics');
+ if ($responesstats->has_response_classes()) {
+ $columns[] = 'responseclass';
+ $headers[] = get_string('modelresponse', 'quiz_statistics');
+ if ($responesstats->has_actual_responses()) {
+ $columns[] = 'response';
+ $headers[] = get_string('actualresponse', 'quiz_statistics');
+ }
- $columns[]= 'credit';
- $headers[]= get_string('optiongrade', 'quiz_statistics');
+ } else {
+ $columns[] = 'response';
+ $headers[] = get_string('response', 'quiz_statistics');
+ }
- $columns[]= 'rcount';
- $headers[]= get_string('count', 'quiz_statistics');
+ $columns[] = 'fraction';
+ $headers[] = get_string('optiongrade', 'quiz_statistics');
- $columns[]= 'frequency';
- $headers[]= get_string('frequency', 'quiz_statistics');
+ $columns[] = 'count';
+ $headers[] = get_string('count', 'quiz_statistics');
+
+ $columns[] = 'frequency';
+ $headers[] = get_string('frequency', 'quiz_statistics');
$this->define_columns($columns);
$this->define_headers($headers);
$this->sortable(false);
- $this->column_class('credit', 'numcol');
- $this->column_class('rcount', 'numcol');
+ $this->column_class('fraction', 'numcol');
+ $this->column_class('count', 'numcol');
$this->column_class('frequency', 'numcol');
- // Set up the table
- $this->define_baseurl($reporturl->out());
-
- $this->collapsible(false);
-
- $this->set_attribute('class', 'generaltable generalbox boxaligncenter');
+ $this->column_suppress('part');
+ $this->column_suppress('responseclass');
parent::setup();
}
- function col_response($response){
- global $QTYPES;
- if (!$this->is_downloading() || $this->is_downloading() == 'xhtml'){
- return $response->indent . $QTYPES[$this->question->qtype]->format_response($response->response, $this->question->questiontextformat);
- } else {
- return $response->indent . $response->response;
- }
+ protected function format_percentage($fraction) {
+ return format_float($fraction * 100, 2) . '%';
}
- function col_subq($response){
- return $response->subq;
- }
-
- function col_credit($response){
- if (!is_null($response->credit)){
- return ($response->credit*100).'%';
- } else {
+ /**
+ * The mark fraction that this response earns.
+ * @param object $response containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_fraction($response) {
+ if (is_null($response->fraction)) {
return '';
}
+
+ return $this->format_percentage($response->fraction);
}
- function col_frequency($response){
- if ($this->question->_stats->s){
- return format_float((($response->rcount / $this->question->_stats->s)*100),2).'%';
- } else {
+ /**
+ * The frequency with which this response was given.
+ * @param object $response containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_frequency($response) {
+ if (!$this->questiondata->_stats->s) {
return '';
}
+
+ return $this->format_percentage($response->count / $this->questiondata->_stats->s);
}
-
-
-
}
-
diff --git a/mod/quiz/report/statistics/statistics_table.php b/mod/quiz/report/statistics/statistics_table.php
index fafc27c890b..ac14ceb8136 100644
--- a/mod/quiz/report/statistics/statistics_table.php
+++ b/mod/quiz/report/statistics/statistics_table.php
@@ -1,75 +1,124 @@
.
+
+/**
+ * Quiz statistics report, table for showing statistics of each question in the quiz.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
require_once($CFG->libdir.'/tablelib.php');
+
+/**
+ * This table has one row for each question in the quiz, with sub-rows when
+ * random questions appear. There are columns for the various statistics.
+ *
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class quiz_report_statistics_table extends flexible_table {
+ /** @var object the quiz settings. */
+ protected $quiz;
- var $quiz;
+ /** @var integer the quiz course_module id. */
+ protected $cmid;
- function quiz_report_statistics_table(){
+ /**
+ * Constructor.
+ */
+ public function __construct() {
parent::__construct('mod-quiz-report-statistics-report');
}
/**
* Setup the columns and headers and other properties of the table and then
* call flexible_table::setup() method.
+ *
+ * @param object $quiz the quiz settings
+ * @param int $cmid the quiz course_module id
+ * @param moodle_url $reporturl the URL to redisplay this report.
+ * @param int $s number of attempts included in the statistics.
*/
- function setup($quiz, $cmid, $reporturl, $s){
+ public function setup($quiz, $cmid, $reporturl, $s) {
$this->quiz = $quiz;
$this->cmid = $cmid;
+
// Define table columns
$columns = array();
$headers = array();
- $columns[]= 'number';
- $headers[]= get_string('questionnumber', 'quiz_statistics');
+ $columns[] = 'number';
+ $headers[] = get_string('questionnumber', 'quiz_statistics');
- if (!$this->is_downloading()){
- $columns[]= 'icon';
- $headers[]= '';
- $columns[]= 'actions';
- $headers[]= '';
+ if (!$this->is_downloading()) {
+ $columns[] = 'icon';
+ $headers[] = '';
+ $columns[] = 'actions';
+ $headers[] = '';
} else {
- $columns[]= 'qtype';
- $headers[]= get_string('questiontype', 'quiz_statistics');
+ $columns[] = 'qtype';
+ $headers[] = get_string('questiontype', 'quiz_statistics');
}
- $columns[]= 'name';
- $headers[]= get_string('questionname', 'quiz');
- $columns[]= 's';
- $headers[]= get_string('attempts', 'quiz_statistics');
+ $columns[] = 'name';
+ $headers[] = get_string('questionname', 'quiz');
- if ($s>1){
- $columns[]= 'facility';
- $headers[]= get_string('facility', 'quiz_statistics');
+ $columns[] = 's';
+ $headers[] = get_string('attempts', 'quiz_statistics');
- $columns[]= 'sd';
- $headers[]= get_string('standarddeviationq', 'quiz_statistics');
+ if ($s > 1) {
+ $columns[] = 'facility';
+ $headers[] = get_string('facility', 'quiz_statistics');
+
+ $columns[] = 'sd';
+ $headers[] = get_string('standarddeviationq', 'quiz_statistics');
}
- $columns[]= 'random_guess_score';
- $headers[]= get_string('random_guess_score', 'quiz_statistics');
- $columns[]= 'intended_weight';
- $headers[]= get_string('intended_weight', 'quiz_statistics');
+ $columns[] = 'random_guess_score';
+ $headers[] = get_string('random_guess_score', 'quiz_statistics');
- $columns[]= 'effective_weight';
- $headers[]= get_string('effective_weight', 'quiz_statistics');
+ $columns[] = 'intended_weight';
+ $headers[] = get_string('intended_weight', 'quiz_statistics');
- $columns[]= 'discrimination_index';
- $headers[]= get_string('discrimination_index', 'quiz_statistics');
+ $columns[] = 'effective_weight';
+ $headers[] = get_string('effective_weight', 'quiz_statistics');
- $columns[]= 'discriminative_efficiency';
- $headers[]= get_string('discriminative_efficiency', 'quiz_statistics');
+ $columns[] = 'discrimination_index';
+ $headers[] = get_string('discrimination_index', 'quiz_statistics');
+
+ $columns[] = 'discriminative_efficiency';
+ $headers[] = get_string('discriminative_efficiency', 'quiz_statistics');
$this->define_columns($columns);
$this->define_headers($headers);
$this->sortable(false);
$this->column_class('s', 'numcol');
+ $this->column_class('facility', 'numcol');
+ $this->column_class('sd', 'numcol');
$this->column_class('random_guess_score', 'numcol');
$this->column_class('intended_weight', 'numcol');
$this->column_class('effective_weight', 'numcol');
- $this->column_class('sd', 'numcol');
- $this->column_class('facility', 'numcol');
$this->column_class('discrimination_index', 'numcol');
$this->column_class('discriminative_efficiency', 'numcol');
@@ -78,143 +127,226 @@ class quiz_report_statistics_table extends flexible_table {
$this->collapsible(true);
-
-
$this->set_attribute('id', 'questionstatistics');
$this->set_attribute('class', 'generaltable generalbox boxaligncenter');
parent::setup();
}
-
- function col_name($question){
- if (!$this->is_downloading()){
- if ($question->qtype!='random'){
- $tooltip = get_string('detailedanalysis', 'quiz_statistics');
- $url = $this->baseurl .'qid='.$question->id;
- $html = "".$question->name."";
- } else {
- $html = $question->name;
- }
- if ($this->is_dubious_question($question)){
- return "$html
";
- } else {
- return $html;
- }
- } else {
- return $question->name;
+ /**
+ * The question number.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_number($question) {
+ if ($question->_stats->subquestion) {
+ return '';
}
+ return $question->number;
}
/**
- * @param object question the question object with a property _stats which
- * includes all the stats for the question.
- * @return boolean is this question possibly not pulling it's weight?
+ * The question type icon.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
*/
- function is_dubious_question($question){
- if (!is_numeric($question->_stats->discriminativeefficiency)){
- return false;
- } else {
- return $question->_stats->discriminativeefficiency < 15;
- }
- }
-
- function col_icon($question){
+ protected function col_icon($question) {
return print_question_icon($question, true);
}
- function col_number($question){
- if (!$question->_stats->subquestion){
- return $question->number;
- } else {
+ /**
+ * Actions that can be performed on the question by this user (e.g. edit or preview).
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_actions($question) {
+ return quiz_question_action_icons($this->quiz, $this->cmid, $question, $this->baseurl);
+ }
+
+ /**
+ * The question type name.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_qtype($question) {
+ return question_bank::get_qtype_name($question->qtype);
+ }
+
+ /**
+ * The question name.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_name($question) {
+ $name = $question->name;
+
+ if ($this->is_downloading()) {
+ return $name;
+ }
+
+ $url = null;
+ if ($question->_stats->subquestion) {
+ $url = new moodle_url($this->baseurl, array('qid' => $question->id));
+ } else if ($question->_stats->slot && $question->qtype != 'random') {
+ $url = new moodle_url($this->baseurl, array('slot' => $question->_stats->slot));
+ }
+
+ if ($url) {
+ $name = html_writer::link($url, $name,
+ array('title' => get_string('detailedanalysis', 'quiz_statistics')));
+ }
+
+ if ($this->is_dubious_question($question)) {
+ $name = html_writer::tag('div', $name, array('class' => 'dubious'));
+ }
+
+ return $name;
+ }
+
+ /**
+ * The number of attempts at this question.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_s($question) {
+ if (!isset($question->_stats->s)) {
+ return 0;
+ }
+
+ return $question->_stats->s;
+ }
+
+ /**
+ * The facility index (average fraction).
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_facility($question) {
+ if (is_null($question->_stats->facility)) {
return '';
}
+
+ return number_format($question->_stats->facility*100, 2) . '%';
}
- function col_actions($question){
- global $CFG;
- $editreturnurl = str_replace($CFG->wwwroot, '', $this->baseurl);
- $editreturnurl = str_replace('&', '&', $editreturnurl);
- $editreturnurl = preg_replace('/&$/', '', $editreturnurl);
- return quiz_question_action_icons($this->quiz, $this->cmid, $question, $editreturnurl);
+
+ /**
+ * The standard deviation of the fractions.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_sd($question) {
+ if (is_null($question->_stats->sd) || $question->_stats->maxmark == 0) {
+ return '';
+ }
+
+ return number_format($question->_stats->sd*100 / $question->_stats->maxmark, 2) . '%';
}
- function col_qtype($question){
- return get_string($question->qtype,'quiz');
+
+ /**
+ * An estimate of the fraction a student would get by guessing randomly.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_random_guess_score($question) {
+ if (is_null($question->_stats->randomguessscore)) {
+ return '';
+ }
+
+ return number_format($question->_stats->randomguessscore * 100, 2).'%';
}
- function col_intended_weight($question){
- return quiz_report_scale_sumgrades_as_percentage($question->_stats->maxgrade, $this->quiz);
+
+ /**
+ * The intended question weight. Maximum mark for the question as a percentage
+ * of maximum mark for the quiz. That is, the indended influence this question
+ * on the student's overall mark.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_intended_weight($question) {
+ return quiz_report_scale_summarks_as_percentage(
+ $question->_stats->maxmark, $this->quiz);
}
- function col_effective_weight($question){
- global $OUTPUT;
- if (!$question->_stats->subquestion){
- if ($question->_stats->negcovar){
- $negcovar = get_string('negcovar', 'quiz_statistics');
- if (!$this->is_downloading()){
- $negcovar .= $OUTPUT->help_icon('negcovar', 'quiz_statistics');
- return ''.$negcovar.'
';
- } else {
- return $negcovar;
- }
- } else {
- return number_format($question->_stats->effectiveweight, 2).'%';
+
+ /**
+ * The effective question weight. That is, an estimate of the actual
+ * influence this question has on the student's overall mark.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_effective_weight($question) {
+ if ($question->_stats->subquestion) {
+ return '';
+ }
+
+ if ($question->_stats->negcovar) {
+ $negcovar = get_string('negcovar', 'quiz_statistics');
+
+ if (!$this->is_downloading()) {
+ $negcovar .= helpbutton('negcovar', $negcovar, 'quiz_statistics',
+ true, false, '', true);
+ $negcovar = '' . $negcovar . '
';
}
- } else {
- return '';
+
+ return $negcovar;
}
+
+ return number_format($question->_stats->effectiveweight, 2) . '%';
}
- function col_discrimination_index($question){
- if (is_numeric($question->_stats->discriminationindex)){
- return number_format($question->_stats->discriminationindex, 2).'%';
- } else {
+
+ /**
+ * Discrimination index. This is the product moment correlation coefficient
+ * between the fraction for this qestion, and the average fraction for the
+ * other questions in this quiz.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_discrimination_index($question) {
+ if (!is_numeric($question->_stats->discriminationindex)) {
return $question->_stats->discriminationindex;
}
+
+ return number_format($question->_stats->discriminationindex, 2) . '%';
}
- function col_discriminative_efficiency($question){
- if (is_numeric($question->_stats->discriminativeefficiency)){
- return number_format($question->_stats->discriminativeefficiency, 2).'%';
- } else {
+
+ /**
+ * Discrimination efficiency, similar to, but different from, the Discrimination index.
+ * @param object $question containst the data to display.
+ * @return string contents of this table cell.
+ */
+ protected function col_discriminative_efficiency($question) {
+ if (!is_numeric($question->_stats->discriminativeefficiency)) {
return '';
}
+
+ return number_format($question->_stats->discriminativeefficiency, 2) . '%';
}
- function col_random_guess_score($question){
- $randomguessscore = question_get_random_guess_score($question);
- if (is_numeric($randomguessscore)){
- return number_format($randomguessscore * 100, 2).'%';
- } else {
- return $randomguessscore; // empty string returned by random question.
+
+ /**
+ * This method encapsulates the test for wheter a question should be considered dubious.
+ * @param object question the question object with a property _stats which
+ * includes all the stats for the question.
+ * @return bool is this question possibly not pulling it's weight?
+ */
+ protected function is_dubious_question($question) {
+ if (!is_numeric($question->_stats->discriminativeefficiency)) {
+ return false;
+ }
+
+ return $question->_stats->discriminativeefficiency < 15;
+ }
+
+ public function wrap_html_start() {
+ // Horrible Moodle 2.0 wide-content work-around.
+ if (!$this->is_downloading()) {
+ echo html_writer::start_tag('div', array('id' => 'tablecontainer',
+ 'class' => 'statistics-tablecontainer'));
}
}
- function col_sd($question){
- if (!is_null($question->_stats->sd) && ($question->_stats->maxgrade!=0)){
- return number_format($question->_stats->sd*100 / $question->_stats->maxgrade, 2).'%';
- } else {
- return '';
- }
- }
- function col_s($question){
- if (isset($question->_stats->s)){
- return $question->_stats->s;
- } else {
- return 0;
- }
- }
- function col_facility($question){
- if (!is_null($question->_stats->facility)){
- return number_format($question->_stats->facility*100, 2).'%';
- } else {
- return '';
- }
- }
- public function wrap_html_start() {
- if (!$this->is_downloading()) {
- echo html_writer::start_tag('div', array('id'=>'tablecontainer', 'class'=>'statistics-tablecontainer'));
- }
- }
public function wrap_html_finish() {
if (!$this->is_downloading()) {
- echo html_writer::end_tag('div'); // Opened in this::wrap_html_start
+ echo html_writer::end_tag('div');
}
}
}
-
diff --git a/mod/quiz/report/statistics/version.php b/mod/quiz/report/statistics/version.php
index 464cbbe3ebe..0359678f9c2 100644
--- a/mod/quiz/report/statistics/version.php
+++ b/mod/quiz/report/statistics/version.php
@@ -1,2 +1,29 @@
version = 2008112103; // The (date) version of this module
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Quiz statistics report version information.
+ *
+ * @package quiz
+ * @subpackage statistics
+ * @copyright 2008 Jamie Pratt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version = 2011051200;
+$plugin->requires = 2011060313;
diff --git a/mod/quiz/review.php b/mod/quiz/review.php
index 105d758ddd4..3f7b2b73b29 100644
--- a/mod/quiz/review.php
+++ b/mod/quiz/review.php
@@ -1,253 +1,261 @@
.
+
/**
* This page prints a review of a particular quiz attempt
*
- * @author Martin Dougiamas and many others.
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * It is used either by the student whose attempts this is, after the attempt,
+ * or by a teacher reviewing another's attempt during or afterwards.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
- require_once(dirname(__FILE__) . '/../../config.php');
- require_once($CFG->dirroot . '/mod/quiz/locallib.php');
- require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
- $attemptid = required_param('attempt', PARAM_INT);
- $page = optional_param('page', 0, PARAM_INT);
- $showall = optional_param('showall', 0, PARAM_BOOL);
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
- $url = new moodle_url('/mod/quiz/review.php', array('attempt'=>$attemptid));
- if ($page !== 0) {
- $url->param('page', $page);
+$attemptid = required_param('attempt', PARAM_INT);
+$page = optional_param('page', 0, PARAM_INT);
+$showall = optional_param('showall', 0, PARAM_BOOL);
+
+$url = new moodle_url('/mod/quiz/review.php', array('attempt'=>$attemptid));
+if ($page !== 0) {
+ $url->param('page', $page);
+}
+if ($showall !== 0) {
+ $url->param('showall', $showall);
+}
+$PAGE->set_url($url);
+
+$attemptobj = quiz_attempt::create($attemptid);
+
+// Check login.
+require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
+$attemptobj->check_review_capability();
+
+// Create an object to manage all the other (non-roles) access rules.
+$accessmanager = $attemptobj->get_access_manager(time());
+$options = $attemptobj->get_display_options(true);
+
+// Check permissions.
+if ($attemptobj->is_own_attempt()) {
+ if (!$attemptobj->is_finished()) {
+ redirect($attemptobj->attempt_url(0, $page));
+ } else if (!$options->attempt) {
+ $accessmanager->back_to_view_page($attemptobj->is_preview_user(),
+ $accessmanager->cannot_review_message($attemptobj->get_attempt_state()));
}
- if ($showall !== 0) {
- $url->param('showall', $showall);
+
+} else if (!$attemptobj->is_review_allowed()) {
+ throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt');
+}
+
+// Load the questions and states needed by this page.
+if ($showall) {
+ $questionids = $attemptobj->get_slots();
+} else {
+ $questionids = $attemptobj->get_slots($page);
+}
+
+// Save the flag states, if they are being changed.
+if ($options->flags == question_display_options::EDITABLE && optional_param('savingflags', false,
+ PARAM_BOOL)) {
+ require_sesskey();
+ $attemptobj->save_question_flags();
+ redirect($attemptobj->review_url(0, $page, $showall));
+}
+
+// Log this review.
+add_to_log($attemptobj->get_courseid(), 'quiz', 'review', 'review.php?attempt=' .
+ $attemptobj->get_attemptid(), $attemptobj->get_quizid(), $attemptobj->get_cmid());
+
+// Work out appropriate title and whether blocks should be shown
+if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) {
+ $strreviewtitle = get_string('reviewofpreview', 'quiz');
+ navigation_node::override_active_url($attemptobj->start_attempt_url());
+
+} else {
+ $strreviewtitle = get_string('reviewofattempt', 'quiz', $attemptobj->get_attempt_number());
+ if (empty($attemptobj->get_quiz()->showblocks) && !$attemptobj->is_preview_user()) {
+ $PAGE->blocks->show_only_fake_blocks();
}
- $PAGE->set_url($url);
+}
- $attemptobj = quiz_attempt::create($attemptid);
+// Set up the page header
+$headtags = $attemptobj->get_html_head_contributions($page, $showall);
+if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) {
+ $accessmanager->setup_secure_page($attemptobj->get_course()->shortname.': '.
+ format_string($attemptobj->get_quiz_name()), $headtags);
+} else if ($accessmanager->safebrowser_required($attemptobj->is_preview_user())) {
+ $PAGE->set_title($attemptobj->get_course()->shortname . ': '.
+ format_string($attemptobj->get_quiz_name()));
+ $PAGE->set_heading($attemptobj->get_course()->fullname);
+ $PAGE->set_cacheable(false);
+} else {
+ $PAGE->navbar->add($strreviewtitle);
+ $PAGE->set_title(format_string($attemptobj->get_quiz_name()));
+ $PAGE->set_heading($attemptobj->get_course()->fullname);
+}
-/// Check login.
- require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
- $attemptobj->check_review_capability();
+// Summary table start ============================================================================
-/// Create an object to manage all the other (non-roles) access rules.
- $accessmanager = $attemptobj->get_access_manager(time());
- $options = $attemptobj->get_review_options();
+// Work out some time-related things.
+$attempt = $attemptobj->get_attempt();
+$quiz = $attemptobj->get_quiz();
+$overtime = 0;
- // Check permissions.
- if ($attemptobj->is_own_attempt()) {
- if (!$attemptobj->is_finished()) {
- redirect($attemptobj->attempt_url(0, $page));
- } else if (!$options->responses) {
- $accessmanager->back_to_view_page($attemptobj->is_preview_user(),
- $accessmanager->cannot_review_message($options));
+if ($attempt->timefinish) {
+ if ($timetaken = ($attempt->timefinish - $attempt->timestart)) {
+ if ($quiz->timelimit && $timetaken > ($quiz->timelimit + 60)) {
+ $overtime = $timetaken - $quiz->timelimit;
+ $overtime = format_time($overtime);
+ }
+ $timetaken = format_time($timetaken);
+ } else {
+ $timetaken = "-";
+ }
+} else {
+ $timetaken = get_string('unfinished', 'quiz');
+}
+
+// Print summary table about the whole attempt.
+// First we assemble all the rows that are appopriate to the current situation in
+// an array, then later we only output the table if there are any rows to show.
+$summarydata = array();
+if (!$attemptobj->get_quiz()->showuserpicture && $attemptobj->get_userid() != $USER->id) {
+ // If showuserpicture is true, the picture is shown elsewhere, so don't repeat it.
+ $student = $DB->get_record('user', array('id' => $attemptobj->get_userid()));
+ $usrepicture = new user_picture($student);
+ $usrepicture->courseid = $attemptobj->get_courseid();
+ $summarydata['user'] = array(
+ 'title' => $usrepicture,
+ 'content' => new action_link(new moodle_url('/user/view.php', array(
+ 'id' => $student->id, 'course' => $attemptobj->get_courseid())),
+ fullname($student, true)),
+ );
+}
+if ($attemptobj->has_capability('mod/quiz:viewreports')) {
+ $attemptlist = $attemptobj->links_to_other_attempts($attemptobj->review_url(0, $page,
+ $showall));
+ if ($attemptlist) {
+ $summarydata['attemptlist'] = array(
+ 'title' => get_string('attempts', 'quiz'),
+ 'content' => $attemptlist,
+ );
+ }
+}
+
+// Timing information.
+$summarydata['startedon'] = array(
+ 'title' => get_string('startedon', 'quiz'),
+ 'content' => userdate($attempt->timestart),
+);
+
+if ($attempt->timefinish) {
+ $summarydata['completedon'] = array(
+ 'title' => get_string('completedon', 'quiz'),
+ 'content' => userdate($attempt->timefinish),
+ );
+ $summarydata['timetaken'] = array(
+ 'title' => get_string('timetaken', 'quiz'),
+ 'content' => format_time($timetaken),
+ );
+}
+
+if (!empty($overtime)) {
+ $summarydata['overdue'] = array(
+ 'title' => get_string('overdue', 'quiz'),
+ 'content' => format_time($overtime),
+ );
+}
+
+// Show marks (if the user is allowed to see marks at the moment).
+$grade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
+if ($options->marks >= question_display_options::MARK_AND_MAX && quiz_has_grades($quiz)) {
+
+ if (!$attempt->timefinish) {
+ $summarydata['grade'] = array(
+ 'title' => get_string('grade', 'quiz'),
+ 'content' => get_string('attemptstillinprogress', 'quiz'),
+ );
+
+ } else if (is_null($grade)) {
+ $summarydata['grade'] = array(
+ 'title' => get_string('grade', 'quiz'),
+ 'content' => quiz_format_grade($quiz, $grade),
+ );
+
+ } else {
+ // Show raw marks only if they are different from the grade (like on the view page).
+ if ($quiz->grade != $quiz->sumgrades) {
+ $a = new stdClass();
+ $a->grade = quiz_format_grade($quiz, $attempt->sumgrades);
+ $a->maxgrade = quiz_format_grade($quiz, $quiz->sumgrades);
+ $summarydata['marks'] = array(
+ 'title' => get_string('marks', 'quiz'),
+ 'content' => get_string('outofshort', 'quiz', $a),
+ );
}
- } else if (!$attemptobj->is_review_allowed()) {
- throw new moodle_quiz_exception($attemptobj, 'noreviewattempt');
- }
-
-/// Load the questions and states needed by this page.
- if ($showall) {
- $questionids = $attemptobj->get_question_ids();
- } else {
- $questionids = $attemptobj->get_question_ids($page);
- }
- $attemptobj->load_questions($questionids);
- $attemptobj->load_question_states($questionids);
-
-/// Save the flag states, if they are being changed.
- if ($options->flags == QUESTION_FLAGSEDITABLE && optional_param('savingflags', false, PARAM_BOOL)) {
- require_sesskey();
- $formdata = data_submitted();
-
- question_save_flags($formdata, $attemptid, $questionids);
- redirect($attemptobj->review_url(0, $page, $showall));
- }
-
-/// Log this review.
- add_to_log($attemptobj->get_courseid(), 'quiz', 'review', 'review.php?attempt=' .
- $attemptobj->get_attemptid(), $attemptobj->get_quizid(), $attemptobj->get_cmid());
-
-/// Work out appropriate title and whether blocks should be shown
- if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) {
- // Normal blocks
- $strreviewtitle = get_string('reviewofpreview', 'quiz');
- navigation_node::override_active_url($attemptobj->start_attempt_url());
-
- } else {
- $strreviewtitle = get_string('reviewofattempt', 'quiz', $attemptobj->get_attempt_number());
- if (empty($attemptobj->get_quiz()->showblocks) && !$attemptobj->is_preview_user()) {
- // Only show pretend blocks
- $PAGE->blocks->show_only_fake_blocks();
- }
- }
-
- // Initialise the JavaScript.
- $headtags = $attemptobj->get_html_head_contributions($page);
-
- // Arrange for the navigation to be displayed.
- $navbc = $attemptobj->get_navigation_panel('quiz_review_nav_panel', $page, $showall);
- $firstregion = reset($PAGE->blocks->get_regions());
- $PAGE->blocks->add_fake_block($navbc, $firstregion);
-
-/// Print the page header
- $headtags = $attemptobj->get_html_head_contributions($page);
- if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) {
- $accessmanager->setup_secure_page($attemptobj->get_course()->shortname.': '.format_string($attemptobj->get_quiz_name()), $headtags);
- } elseif ($accessmanager->safebrowser_required($attemptobj->is_preview_user())) {
- $PAGE->set_title($attemptobj->get_course()->shortname . ': '.format_string($attemptobj->get_quiz_name()));
- $PAGE->set_heading($attemptobj->get_course()->fullname);
- $PAGE->set_cacheable(false);
- echo $OUTPUT->header();
- } else {
- $attemptobj->navigation($strreviewtitle);
- $PAGE->set_title(format_string($attemptobj->get_quiz_name()));
- $PAGE->set_heading($attemptobj->get_course()->fullname);
- echo $OUTPUT->header();
- }
-
-/// Print heading.
- if ($attemptobj->is_preview_user() && $attemptobj->is_own_attempt()) {
- $attemptobj->print_restart_preview_button();
- }
- echo $OUTPUT->heading($strreviewtitle);
-
-/// Summary table start ============================================================================
-
-/// Work out some time-related things.
- $attempt = $attemptobj->get_attempt();
- $quiz = $attemptobj->get_quiz();
- $overtime = 0;
-
- if ($attempt->timefinish) {
- if ($timetaken = ($attempt->timefinish - $attempt->timestart)) {
- if($quiz->timelimit && $timetaken > ($quiz->timelimit + 60)) {
- $overtime = $timetaken - $quiz->timelimit;
- $overtime = format_time($overtime);
- }
- $timetaken = format_time($timetaken);
+ // Now the scaled grade.
+ $a = new stdClass();
+ $a->grade = html_writer::tag('b', quiz_format_grade($quiz, $grade));
+ $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
+ if ($quiz->grade != 100) {
+ $a->percent = html_writer::tag('b', format_float(
+ $attempt->sumgrades * 100 / $quiz->sumgrades, 0));
+ $formattedgrade = get_string('outofpercent', 'quiz', $a);
} else {
- $timetaken = "-";
+ $formattedgrade = get_string('outof', 'quiz', $a);
}
- } else {
- $timetaken = get_string('unfinished', 'quiz');
+ $summarydata['grade'] = array(
+ 'title' => get_string('grade', 'quiz'),
+ 'content' => $formattedgrade,
+ );
}
+}
-/// Print summary table about the whole attempt.
-/// First we assemble all the rows that are appopriate to the current situation in
-/// an array, then later we only output the table if there are any rows to show.
- $rows = array();
- if (!$attemptobj->get_quiz()->showuserpicture && $attemptobj->get_userid() <> $USER->id) {
- /// If showuserpicture is true, the picture is shown elsewhere, so don't repeat it.
- $student = $DB->get_record('user', array('id' => $attemptobj->get_userid()));
- $picture = $OUTPUT->user_picture($student, array('courseid'=>$attemptobj->get_courseid()));
- $rows[] = '' . $picture . ' | ' .
- fullname($student, true) . ' |
';
- }
- if ($attemptobj->has_capability('mod/quiz:viewreports')) {
- $attemptlist = $attemptobj->links_to_other_attempts($attemptobj->review_url(0, $page, $showall));
- if ($attemptlist) {
- $rows[] = '' . get_string('attempts', 'quiz') .
- ' | ' . $attemptlist . ' |
';
- }
- }
+// Feedback if there is any, and the user is allowed to see it now.
+$feedback = $attemptobj->get_overall_feedback($grade);
+if ($options->overallfeedback && $feedback) {
+ $summarydata['feedback'] = array(
+ 'title' => get_string('feedback', 'quiz'),
+ 'content' => $feedback,
+ );
+}
-/// Timing information.
- $rows[] = '' . get_string('startedon', 'quiz') .
- ' | ' . userdate($attempt->timestart) . ' |
';
- if ($attempt->timefinish) {
- $rows[] = '' . get_string('completedon', 'quiz') . ' | ' .
- userdate($attempt->timefinish) . ' |
';
- $rows[] = '' . get_string('timetaken', 'quiz') . ' | ' .
- $timetaken . ' |
';
- }
- if (!empty($overtime)) {
- $rows[] = '' . get_string('overdue', 'quiz') . ' | ' . $overtime . ' |
';
- }
+// Summary table end ==============================================================================
-/// Show scores (if the user is allowed to see scores at the moment).
- $grade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
- if ($options->scores) {
- if (quiz_has_grades($quiz)) {
- if($overtime) {
- $result->sumgrades = "0";
- $result->grade = "0.0";
- }
+if ($showall) {
+ $slots = $attemptobj->get_slots();
+ $lastpage = true;
+} else {
+ $slots = $attemptobj->get_slots($page);
+ $lastpage = $attemptobj->is_last_page($page);
+}
- /// Show raw marks only if they are different from the grade (like on the view page.
- if ($quiz->grade != $quiz->sumgrades) {
- $a = new stdClass;
- $a->grade = quiz_format_grade($quiz, $attempt->sumgrades);
- $a->maxgrade = quiz_format_grade($quiz, $quiz->sumgrades);
- $rows[] = '' . get_string('marks', 'quiz') . ' | ' .
- get_string('outofshort', 'quiz', $a) . ' |
';
- }
+$output = $PAGE->get_renderer('mod_quiz');
- /// Now the scaled grade.
- $a = new stdClass;
- $a->grade = '' . quiz_format_grade($quiz, $grade) . '';
- $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
- $a->percent = '' . round(($attempt->sumgrades/$quiz->sumgrades)*100, 0) . '';
- $rows[] = '' . get_string('grade') . ' | ' .
- get_string('outofpercent', 'quiz', $a) . ' |
';
- }
- }
-
-/// Feedback if there is any, and the user is allowed to see it now.
- $feedback = $attemptobj->get_overall_feedback($grade);
- if ($options->overallfeedback && $feedback) {
- $rows[] = '' . get_string('feedback', 'quiz') .
- ' | ' . $feedback . ' |
';
- }
-
-/// Now output the summary table, if there are any rows to be shown.
- if (!empty($rows)) {
- echo '', "\n";
- echo implode("\n", $rows);
- echo "\n
\n";
- }
-
-/// Summary table end ==============================================================================
-
-/// Form for saving flags if necessary.
- if ($options->flags == QUESTION_FLAGSEDITABLE) {
- echo '\n";
- $PAGE->requires->js_init_call('M.mod_quiz.init_review_form', null, false, quiz_get_js_module());
- }
-
-/// Print a link to the next page.
- echo '';
- if ($lastpage) {
- $accessmanager->print_finish_review_link($attemptobj->is_preview_user());
- } else {
- echo link_arrow_right(get_string('next'), s($attemptobj->review_url(0, $page + 1)));
- }
- echo "
";
-
- echo $OUTPUT->footer();
+// Arrange for the navigation to be displayed.
+$navbc = $attemptobj->get_navigation_panel($output, 'quiz_review_nav_panel', $page, $showall);
+$firstregion = reset($PAGE->blocks->get_regions());
+$PAGE->blocks->add_fake_block($navbc, $firstregion);
+echo $output->review_page($attemptobj, $slots, $page, $showall, $lastpage, $options, $summarydata);
diff --git a/mod/quiz/reviewquestion.php b/mod/quiz/reviewquestion.php
index a61851e4b3b..583a18e3ffc 100644
--- a/mod/quiz/reviewquestion.php
+++ b/mod/quiz/reviewquestion.php
@@ -1,135 +1,112 @@
.
+
/**
* This page prints a review of a particular question attempt.
* This page is expected to only be used in a popup window.
*
- * @author Martin Dougiamas, Tim Hunt and many others.
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
- require_once(dirname(__FILE__) . '/../../config.php');
- require_once('locallib.php');
- $attemptid = required_param('attempt', PARAM_INT); // attempt id
- $questionid = required_param('question', PARAM_INT); // question id
- $stateid = optional_param('state', 0, PARAM_INT); // state id
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once('locallib.php');
- $url = new moodle_url('/mod/quiz/reviewquestion.php', array('attempt'=>$attemptid,'question'=>$questionid));
- if ($stateid !== 0) {
- $url->param('state', $stateid);
- }
- $PAGE->set_url($url);
- $PAGE->set_pagelayout('popup');
+$attemptid = required_param('attempt', PARAM_INT); // attempt id
+$slot = required_param('slot', PARAM_INT); // question number in usage
+$seq = optional_param('step', null, PARAM_INT); // sequence number
- $attemptobj = quiz_attempt::create($attemptid);
+$baseurl = new moodle_url('/mod/quiz/reviewquestion.php',
+ array('attempt' => $attemptid, 'slot' => $slot));
+$currenturl = new moodle_url($baseurl);
+if ($seq !== 0) {
+ $currenturl->param('step', $seq);
+}
+$PAGE->set_url($currenturl);
+$PAGE->set_pagelayout('popup');
-/// Check login.
- require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
- $attemptobj->check_review_capability();
+$attemptobj = quiz_attempt::create($attemptid);
-/// Create an object to manage all the other (non-roles) access rules.
- $accessmanager = $attemptobj->get_access_manager(time());
- $options = $attemptobj->get_review_options();
+// Check login.
+require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
+$attemptobj->check_review_capability();
- // Check permissions.
- if ($attemptobj->is_own_attempt()) {
- if (!$attemptobj->is_finished()) {
- echo $OUTPUT->header();
- echo $OUTPUT->notification(get_string('cannotreviewopen', 'quiz'));
- echo $OUTPUT->close_window_button();
- echo $OUTPUT->footer();
- die;
- } else if (!$options->responses) {
- $accessmanager = $attemptobj->get_access_manager(time());
- echo $OUTPUT->header();
- echo $OUTPUT->notification($accessmanager->cannot_review_message($attemptobj->get_review_options()));
- echo $OUTPUT->close_window_button();
- echo $OUTPUT->footer();
- die;
- }
+echo $OUTPUT->header();
- } else if (!$attemptobj->is_review_allowed()) {
- throw new moodle_quiz_exception($attemptobj, 'noreviewattempt');
+// Check permissions.
+if ($attemptobj->is_own_attempt()) {
+ if (!$attemptobj->is_finished()) {
+ echo $OUTPUT->notification(get_string('cannotreviewopen', 'quiz'));
+ echo $OUTPUT->close_window_button();
+ echo $OUTPUT->footer();
+ die();
+ } else if (!$options->responses) {
+ $accessmanager = $attemptobj->get_access_manager(time());
+ echo $OUTPUT->notification($accessmanager->cannot_review_message(
+ $attemptobj->get_review_options()));
+ echo $OUTPUT->close_window_button();
+ echo $OUTPUT->footer();
+ die();
}
-/// Load the questions and states.
- $questionids = array($questionid);
- $attemptobj->load_questions($questionids);
- $attemptobj->load_question_states($questionids);
+} else if (!$attemptobj->is_review_allowed()) {
+ throw new moodle_quiz_exception($attemptobj->get_quizobj(), 'noreviewattempt');
+}
-/// If it was asked for, load another state, instead of the latest.
- if ($stateid) {
- $attemptobj->load_specific_question_state($questionid, $stateid);
+// Quiz name.
+$rows[] = '' . get_string('modulename', 'quiz') .
+ ' | ' . format_string($attemptobj->get_quiz_name()) . ' |
';
+
+// Question name.
+$rows[] = '' . get_string('question', 'quiz') .
+ ' | ' . format_string(
+ $attemptobj->get_question_name($slot)) . ' |
';
+
+// Other attempts at the quiz.
+if ($attemptobj->has_capability('mod/quiz:viewreports')) {
+ $attemptlist = $attemptobj->links_to_other_attempts($baseurl);
+ if ($attemptlist) {
+ $rows[] = '' . get_string('attempts', 'quiz') .
+ ' | ' . $attemptlist . ' |
';
}
+}
-/// Work out the base URL of this page.
- $baseurl = $CFG->wwwroot . '/mod/quiz/reviewquestion.php?attempt=' .
- $attemptobj->get_attemptid() . '&question=' . $questionid;
+// Timestamp of this action.
+$timestamp = $attemptobj->get_question_action_time($slot);
+if ($timestamp) {
+ $rows[] = '' . get_string('completedon', 'quiz') .
+ ' | ' . userdate($timestamp) . ' |
';
+}
-/// Log this review.
- add_to_log($attemptobj->get_courseid(), 'quiz', 'review', 'reviewquestion.php?attempt=' .
- $attemptobj->get_attemptid() . '&question=' . $questionid .
- ($stateid ? '&state=' . $stateid : ''),
- $attemptobj->get_quizid(), $attemptobj->get_cmid());
+// Now output the summary table, if there are any rows to be shown.
+if (!empty($rows)) {
+ echo '', "\n";
+ echo implode("\n", $rows);
+ echo "\n
\n";
+}
-/// Print the page header
- $attemptobj->get_question_html_head_contributions($questionid);
- $PAGE->set_title($attemptobj->get_course()->shortname . ': '.format_string($attemptobj->get_quiz_name()));
- $PAGE->set_heading($COURSE->fullname);
- echo $OUTPUT->header();
-
-/// Print infobox
- $rows = array();
-
-/// User picture and name.
- if ($attemptobj->get_userid() <> $USER->id) {
- // Print user picture and name
- $student = $DB->get_record('user', array('id' => $attemptobj->get_userid()));
- $picture = $OUTPUT->user_picture($student, array('courseid'=>$attemptobj->get_courseid()));
- $rows[] = '' . $picture . ' | ' .
- fullname($student, true) . ' |
';
- }
-
-/// Quiz name.
- $rows[] = '' . get_string('modulename', 'quiz') .
- ' | ' . format_string($attemptobj->get_quiz_name()) . ' |
';
-
-/// Question name.
- $rows[] = '' . get_string('question', 'quiz') .
- ' | ' . format_string(
- $attemptobj->get_question($questionid)->name) . ' |
';
-
-/// Other attempts at the quiz.
- if ($attemptobj->has_capability('mod/quiz:viewreports')) {
- $attemptlist = $attemptobj->links_to_other_attempts($baseurl);
- if ($attemptlist) {
- $rows[] = '' . get_string('attempts', 'quiz') .
- ' | ' . $attemptlist . ' |
';
- }
- }
-
-/// Timestamp of this action.
- $timestamp = $attemptobj->get_question_state($questionid)->timestamp;
- if ($timestamp) {
- $rows[] = '' . get_string('completedon', 'quiz') .
- ' | ' . userdate($timestamp) . ' |
';
- }
-
-/// Now output the summary table, if there are any rows to be shown.
- if (!empty($rows)) {
- echo '', "\n";
- echo implode("\n", $rows);
- echo "\n
\n";
- }
-
-/// Print the question in the requested state.
- if ($stateid) {
- $baseurl .= '&state=' . $stateid;
- }
- $attemptobj->print_question($questionid, true, $baseurl);
-
-/// Finish the page
- echo $OUTPUT->footer();
+// Print the question in the requested state.
+if (!is_null($seq)) {
+ echo $attemptobj->render_question_at_step($slot, $seq, true, $currenturl);
+} else {
+ echo $attemptobj->render_question($slot, true, $currenturl);
+}
+// Finish the page
+echo $OUTPUT->footer();
diff --git a/mod/quiz/settings.php b/mod/quiz/settings.php
index 5866e92b026..8bbb14b666d 100644
--- a/mod/quiz/settings.php
+++ b/mod/quiz/settings.php
@@ -1,15 +1,33 @@
.
+
/**
- * settingstree.php - Tells the admin menu that there are sub menu pages to
- * include for this activity.
+ * Administration settings definitions for the quiz module.
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2010 Petr Skoda
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die;
+
+defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/lib.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/settingslib.php');
// First get a list of quiz reports with there own settings pages. If there none,
@@ -85,25 +103,32 @@ $quizsettings->add(new admin_setting_configcheckbox_with_advanced('quiz/shufflea
get_string('shufflewithin', 'quiz'), get_string('configshufflewithin', 'quiz'),
array('value' => 1, 'adv' => false)));
-// Adaptive mode.
-$quizsettings->add(new admin_setting_configcheckbox_with_advanced('quiz/optionflags',
- get_string('adaptive', 'quiz'), get_string('configadaptive', 'quiz'),
- array('value' => 1, 'adv' => false)));
-
-// Apply penalties.
-$quizsettings->add(new admin_setting_configcheckbox_with_advanced('quiz/penaltyscheme',
- get_string('penaltyscheme', 'quiz'), get_string('configpenaltyscheme', 'quiz'),
- array('value' => 1, 'adv' => true)));
+// Preferred behaviour.
+$quizsettings->add(new admin_setting_question_behaviour('quiz/preferredbehaviour',
+ get_string('howquestionsbehave', 'question'), get_string('howquestionsbehave_desc', 'quiz'),
+ 'deferredfeedback'));
// Each attempt builds on last.
$quizsettings->add(new admin_setting_configcheckbox_with_advanced('quiz/attemptonlast',
- get_string('eachattemptbuildsonthelast', 'quiz'), get_string('configeachattemptbuildsonthelast', 'quiz'),
+ get_string('eachattemptbuildsonthelast', 'quiz'),
+ get_string('configeachattemptbuildsonthelast', 'quiz'),
array('value' => 0, 'adv' => true)));
// Review options.
-$quizsettings->add(new admin_setting_quiz_reviewoptions('quiz/review',
- get_string('reviewoptions', 'quiz'), get_string('configreviewoptions', 'quiz'),
- array('value' => QUIZ_REVIEW_IMMEDIATELY | QUIZ_REVIEW_OPEN | QUIZ_REVIEW_CLOSED, 'fix' => false)));
+$quizsettings->add(new admin_setting_heading('reviewheading',
+ get_string('reviewoptionsheading', 'quiz'), ''));
+foreach (mod_quiz_admin_review_setting::fields() as $field => $name) {
+ $default = mod_quiz_admin_review_setting::all_on();
+ $forceduring = null;
+ if ($field == 'attempt') {
+ $forceduring = true;
+ } else if ($field == 'overallfeedback') {
+ $default = $default ^ mod_quiz_admin_review_setting::DURING;
+ $forceduring = false;
+ }
+ $quizsettings->add(new mod_quiz_admin_review_setting('quiz/review' . $field,
+ $name, '', $default, $forceduring));
+}
// Show the user's picture
$quizsettings->add(new admin_setting_configcheckbox_with_advanced('quiz/showuserpicture',
@@ -125,7 +150,8 @@ for ($i = 0; $i <= QUIZ_MAX_Q_DECIMAL_OPTION; $i++) {
$options[$i] = $i;
}
$quizsettings->add(new admin_setting_configselect_with_advanced('quiz/questiondecimalpoints',
- get_string('decimalplacesquestion', 'quiz'), get_string('configdecimalplacesquestion', 'quiz'),
+ get_string('decimalplacesquestion', 'quiz'),
+ get_string('configdecimalplacesquestion', 'quiz'),
array('value' => -1, 'fix' => true), $options));
// Show blocks during quiz attempts
@@ -156,19 +182,21 @@ $quizsettings->add(new admin_setting_configcheckbox_with_advanced('quiz/popup',
get_string('showinsecurepopup', 'quiz'), get_string('configpopup', 'quiz'),
array('value' => 0, 'adv' => true)));
-/// Now, depending on whether any reports have their own settings page, add
-/// the quiz setting page to the appropriate place in the tree.
+// Now, depending on whether any reports have their own settings page, add
+// the quiz setting page to the appropriate place in the tree.
if (empty($reportsbyname)) {
$ADMIN->add('modsettings', $quizsettings);
} else {
- $ADMIN->add('modsettings', new admin_category('modsettingsquizcat', get_string('modulename', 'quiz'), !$module->visible));
+ $ADMIN->add('modsettings', new admin_category('modsettingsquizcat',
+ get_string('modulename', 'quiz'), !$module->visible));
$ADMIN->add('modsettingsquizcat', $quizsettings);
-/// Add the report pages for the settings.php files in sub directories of mod/quiz/report
+ // Add the report pages for the settings.php files in sub directories of mod/quiz/report
foreach ($reportsbyname as $strreportname => $report) {
$reportname = $report;
- $settings = new admin_settingpage('modsettingsquizcat'.$reportname, $strreportname, 'moodle/site:config', !$module->visible);
+ $settings = new admin_settingpage('modsettingsquizcat'.$reportname,
+ $strreportname, 'moodle/site:config', !$module->visible);
if ($ADMIN->fulltree) {
include($CFG->dirroot."/mod/quiz/report/$reportname/settings.php");
}
@@ -176,4 +204,4 @@ if (empty($reportsbyname)) {
}
}
-$settings = NULL; // we do not want standard settings link
+$settings = null; // we do not want standard settings link
diff --git a/mod/quiz/settingslib.php b/mod/quiz/settingslib.php
index b81a2906c17..c0dc19859b6 100644
--- a/mod/quiz/settingslib.php
+++ b/mod/quiz/settingslib.php
@@ -1,93 +1,148 @@
.
+
+/**
+ * This page is the entry page into the quiz UI. Displays information about the
+ * quiz to students and teachers, and lets students see their previous attempts.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2008 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
-}
/**
* Quiz specific admin settings class.
+ *
+ * @copyright 2008 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class admin_setting_quiz_reviewoptions extends admin_setting {
- private static $times = array(
- QUIZ_REVIEW_IMMEDIATELY => 'reviewimmediately',
- QUIZ_REVIEW_OPEN => 'reviewopen',
- QUIZ_REVIEW_CLOSED => 'reviewclosed');
- private static $things = array(
- QUIZ_REVIEW_RESPONSES => 'responses',
- QUIZ_REVIEW_ANSWERS => 'answers',
- QUIZ_REVIEW_FEEDBACK => 'feedback',
- QUIZ_REVIEW_GENERALFEEDBACK => 'generalfeedback',
- QUIZ_REVIEW_SCORES => 'scores',
- QUIZ_REVIEW_OVERALLFEEDBACK => 'overallfeedback');
+class mod_quiz_admin_review_setting extends admin_setting {
+ /**#@+
+ * @var integer should match the constants defined in {@link mod_quiz_display_options}.
+ * again, copied for performance reasons.
+ */
+ const DURING = 0x10000;
+ const IMMEDIATELY_AFTER = 0x01000;
+ const LATER_WHILE_OPEN = 0x00100;
+ const AFTER_CLOSE = 0x00010;
+ /**#@-*/
- public function __construct($name, $visiblename, $description, $defaultsetting) {
- $this->plugin = 'quiz';
+ /**
+ * @var boolean|null forced checked / disabled attributes for the during time.
+ */
+ protected $duringstate;
+
+ /**
+ * This should match {@link mod_quiz_mod_form::$reviewfields} but copied
+ * here becuase generating the admin tree needs to be fast.
+ * @return array
+ */
+ public static function fields() {
+ return array(
+ 'attempt' => get_string('theattempt', 'quiz'),
+ 'correctness' => get_string('whethercorrect', 'question'),
+ 'marks' => get_string('marks', 'question'),
+ 'specificfeedback' => get_string('specificfeedback', 'question'),
+ 'generalfeedback' => get_string('generalfeedback', 'question'),
+ 'rightanswer' => get_string('rightanswer', 'question'),
+ 'overallfeedback' => get_string('overallfeedback', 'quiz'),
+ );
+ }
+
+ public function __construct($name, $visiblename, $description,
+ $defaultsetting, $duringstate = null) {
+ $this->duringstate = $duringstate;
parent::__construct($name, $visiblename, $description, $defaultsetting);
}
- private function normalise_data($data) {
+ /**
+ * @return int all times.
+ */
+ public static function all_on() {
+ return self::DURING | self::IMMEDIATELY_AFTER | self::LATER_WHILE_OPEN |
+ self::AFTER_CLOSE;
+ }
+
+ protected static function times() {
+ return array(
+ self::DURING => get_string('reviewduring', 'quiz'),
+ self::IMMEDIATELY_AFTER => get_string('reviewimmediately', 'quiz'),
+ self::LATER_WHILE_OPEN => get_string('reviewopen', 'quiz'),
+ self::AFTER_CLOSE => get_string('reviewclosed', 'quiz'),
+ );
+ }
+
+ protected function normalise_data($data) {
+ $times = self::times();
$value = 0;
- foreach (admin_setting_quiz_reviewoptions::$times as $timemask => $timestring) {
- foreach (admin_setting_quiz_reviewoptions::$things as $thingmask => $thingstring) {
- if (!empty($data[$timemask][$thingmask])) {
- $value += $timemask & $thingmask;
+ foreach ($times as $timemask => $name) {
+ if ($timemask == self::DURING && !is_null($this->duringstate)) {
+ if ($this->duringstate) {
+ $value += $timemask;
}
+ } else if (!empty($data[$timemask])) {
+ $value += $timemask;
}
}
return $value;
}
public function get_setting() {
- $value = $this->config_read($this->name);
- $adv = $this->config_read($this->name.'_adv');
- if (is_null($value) or is_null($adv)) {
- return NULL;
- }
- return array('value' => $value, 'adv' => $adv);
+ return $this->config_read($this->name);
}
public function write_setting($data) {
- if (!isset($data['value'])) {
- $data['value'] = $this->normalise_data($data);
+ if (is_array($data) || empty($data)) {
+ $data = $this->normalise_data($data);
}
- $this->config_write($this->name, $data['value']);
- $value = empty($data['adv']) ? 0 : 1;
- $this->config_write($this->name.'_adv', $value);
+ $this->config_write($this->name, $data);
return '';
}
- public function output_html($data, $query='') {
- if (!isset($data['value'])) {
- $data['value'] = $this->normalise_data($data);
+ public function output_html($data, $query = '') {
+ if (is_array($data) || empty($data)) {
+ $data = $this->normalise_data($data);
}
- $return = '' . "\n";
- foreach (admin_setting_quiz_reviewoptions::$times as $timemask => $timestring) {
- $return .= '
' . get_string($timestring, 'quiz') . "
\n";
- $nameprefix = $this->get_full_name() . '[' . $timemask . ']';
- $idprefix = $this->get_id(). '_' . $timemask . '_';
- foreach (admin_setting_quiz_reviewoptions::$things as $thingmask => $thingstring) {
- $id = $idprefix . $thingmask;
- $state = '';
- if ($data['value'] & $timemask & $thingmask) {
- $state = 'checked="checked" ';
- }
- $return .= '
\n";
+ $return = '
';
+ foreach (self::times() as $timemask => $namestring) {
+ $id = $this->get_id(). '_' . $timemask;
+ $state = '';
+ if ($data & $timemask) {
+ $state = 'checked="checked" ';
}
- $return .= "
\n";
+ if ($timemask == self::DURING && !is_null($this->duringstate)) {
+ $state = 'disabled="disabled" ';
+ if ($this->duringstate) {
+ $state .= 'checked="checked" ';
+ }
+ }
+ $return .= '
\n";
}
$return .= "
\n";
- $adv = !empty($data['adv']);
- $return .= '
' .
- '
';
-
return format_admin_setting($this, $this->visiblename, $return,
$this->description, true, '', get_string('everythingon', 'quiz'), $query);
}
diff --git a/mod/quiz/simpletest/testaccessrules.php b/mod/quiz/simpletest/testaccessrules.php
index aba504202f5..8048b37a4cb 100644
--- a/mod/quiz/simpletest/testaccessrules.php
+++ b/mod/quiz/simpletest/testaccessrules.php
@@ -1,37 +1,60 @@
.
+
/**
* Unit tests for (some of) mod/quiz/accessrules.php.
*
- * @copyright © 2008 The Open University
- * @author T.J.Hunt@open.ac.uk
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
-}
+
+defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+/**
+ * Unit tests for (some of) mod/quiz/accessrules.php.
+ *
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class simple_rules_test extends UnitTestCase {
public static $includecoverage = array('mod/quiz/locallib.php');
- function test_num_attempts_access_rule() {
- $quiz = new stdClass;
+ public function test_num_attempts_access_rule() {
+ $quiz = new stdClass();
$quiz->attempts = 3;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
$rule = new num_attempts_access_rule($quizobj, 0);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$this->assertEqual($rule->description(), get_string('attemptsallowedn', 'quiz', 3));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
- $this->assertEqual($rule->prevent_new_attempt(3, $attempt), get_string('nomoreattempts', 'quiz'));
- $this->assertEqual($rule->prevent_new_attempt(666, $attempt), get_string('nomoreattempts', 'quiz'));
+ $this->assertEqual($rule->prevent_new_attempt(3, $attempt),
+ get_string('nomoreattempts', 'quiz'));
+ $this->assertEqual($rule->prevent_new_attempt(666, $attempt),
+ get_string('nomoreattempts', 'quiz'));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->is_finished(2, $attempt));
@@ -42,10 +65,10 @@ class simple_rules_test extends UnitTestCase {
$this->assertFalse($rule->time_left($attempt, 1));
}
- function test_ipaddress_access_rule() {
- $quiz = new stdClass;
- $attempt = new stdClass;
- $cm = new stdClass;
+ public function test_ipaddress_access_rule() {
+ $quiz = new stdClass();
+ $attempt = new stdClass();
+ $cm = new stdClass();
$cm->id = 0;
// Test the allowed case by getting the user's IP address. However, this
@@ -73,17 +96,18 @@ class simple_rules_test extends UnitTestCase {
$this->assertFalse($rule->time_left($attempt, 1));
}
- function test_time_limit_access_rule() {
- $quiz = new stdClass;
+ public function test_time_limit_access_rule() {
+ $quiz = new stdClass();
$quiz->timelimit = 3600;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
$rule = new time_limit_access_rule($quizobj, 10000);
- $attempt = new stdClass;
+ $attempt = new stdClass();
- $this->assertEqual($rule->description(), get_string('quiztimelimit', 'quiz', format_time(3600)));
+ $this->assertEqual($rule->description(),
+ get_string('quiztimelimit', 'quiz', format_time(3600)));
$attempt->timestart = 10000;
$this->assertEqual($rule->time_left($attempt, 10000), 3600);
@@ -96,16 +120,21 @@ class simple_rules_test extends UnitTestCase {
}
}
+
+/**
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class open_close_date_access_rule_test extends UnitTestCase {
- function test_no_dates() {
- $quiz = new stdClass;
+ public function test_no_dates() {
+ $quiz = new stdClass();
$quiz->timeopen = 0;
$quiz->timeclose = 0;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->preview = 0;
$rule = new open_close_date_access_rule($quizobj, 10000);
@@ -124,45 +153,49 @@ class open_close_date_access_rule_test extends UnitTestCase {
$this->assertFalse($rule->time_left($attempt, 0));
}
- function test_start_date() {
- $quiz = new stdClass;
+ public function test_start_date() {
+ $quiz = new stdClass();
$quiz->timeopen = 10000;
$quiz->timeclose = 0;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->preview = 0;
$rule = new open_close_date_access_rule($quizobj, 9999);
- $this->assertEqual($rule->description(), array(get_string('quiznotavailable', 'quiz', userdate(10000))));
- $this->assertEqual($rule->prevent_access(), get_string('notavailable', 'quiz'));
+ $this->assertEqual($rule->description(),
+ array(get_string('quiznotavailable', 'quiz', userdate(10000))));
+ $this->assertEqual($rule->prevent_access(),
+ get_string('notavailable', 'quiz'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->time_left($attempt, 0));
$rule = new open_close_date_access_rule($quizobj, 10000);
- $this->assertEqual($rule->description(), array(get_string('quizopenedon', 'quiz', userdate(10000))));
+ $this->assertEqual($rule->description(),
+ array(get_string('quizopenedon', 'quiz', userdate(10000))));
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$this->assertFalse($rule->time_left($attempt, 0));
}
- function test_close_date() {
- $quiz = new stdClass;
+ public function test_close_date() {
+ $quiz = new stdClass();
$quiz->timeopen = 0;
$quiz->timeclose = 20000;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->preview = 0;
$rule = new open_close_date_access_rule($quizobj, 20000);
- $this->assertEqual($rule->description(), array(get_string('quizcloseson', 'quiz', userdate(20000))));
+ $this->assertEqual($rule->description(),
+ array(get_string('quizcloseson', 'quiz', userdate(20000))));
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
@@ -172,8 +205,10 @@ class open_close_date_access_rule_test extends UnitTestCase {
$this->assertEqual($rule->time_left($attempt, 20100), -100);
$rule = new open_close_date_access_rule($quizobj, 20001);
- $this->assertEqual($rule->description(), array(get_string('quizclosed', 'quiz', userdate(20000))));
- $this->assertEqual($rule->prevent_access(), get_string('notavailable', 'quiz'));
+ $this->assertEqual($rule->description(),
+ array(get_string('quizclosed', 'quiz', userdate(20000))));
+ $this->assertEqual($rule->prevent_access(),
+ get_string('notavailable', 'quiz'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertTrue($rule->is_finished(0, $attempt));
$this->assertFalse($rule->time_left($attempt, 20000 - QUIZ_SHOW_TIME_BEFORE_DEADLINE));
@@ -182,40 +217,46 @@ class open_close_date_access_rule_test extends UnitTestCase {
$this->assertEqual($rule->time_left($attempt, 20100), -100);
}
- function test_both_dates() {
- $quiz = new stdClass;
+ public function test_both_dates() {
+ $quiz = new stdClass();
$quiz->timeopen = 10000;
$quiz->timeclose = 20000;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->preview = 0;
$rule = new open_close_date_access_rule($quizobj, 9999);
- $this->assertEqual($rule->description(), array(get_string('quiznotavailable', 'quiz', userdate(10000))));
- $this->assertEqual($rule->prevent_access(), get_string('notavailable', 'quiz'));
+ $this->assertEqual($rule->description(),
+ array(get_string('quiznotavailable', 'quiz', userdate(10000))));
+ $this->assertEqual($rule->prevent_access(),
+ get_string('notavailable', 'quiz'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$rule = new open_close_date_access_rule($quizobj, 10000);
- $this->assertEqual($rule->description(), array(get_string('quizopenedon', 'quiz', userdate(10000)),
+ $this->assertEqual($rule->description(),
+ array(get_string('quizopenedon', 'quiz', userdate(10000)),
get_string('quizcloseson', 'quiz', userdate(20000))));
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$rule = new open_close_date_access_rule($quizobj, 20000);
- $this->assertEqual($rule->description(), array(get_string('quizopenedon', 'quiz', userdate(10000)),
+ $this->assertEqual($rule->description(),
+ array(get_string('quizopenedon', 'quiz', userdate(10000)),
get_string('quizcloseson', 'quiz', userdate(20000))));
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->is_finished(0, $attempt));
$rule = new open_close_date_access_rule($quizobj, 20001);
- $this->assertEqual($rule->description(), array(get_string('quizclosed', 'quiz', userdate(20000))));
- $this->assertEqual($rule->prevent_access(), get_string('notavailable', 'quiz'));
+ $this->assertEqual($rule->description(),
+ array(get_string('quizclosed', 'quiz', userdate(20000))));
+ $this->assertEqual($rule->prevent_access(),
+ get_string('notavailable', 'quiz'));
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertTrue($rule->is_finished(0, $attempt));
@@ -226,19 +267,24 @@ class open_close_date_access_rule_test extends UnitTestCase {
}
}
+
+/**
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class inter_attempt_delay_access_rule_test extends UnitTestCase {
- function test_just_first_delay() {
- $quiz = new stdClass;
+ public function test_just_first_delay() {
+ $quiz = new stdClass();
$quiz->attempts = 3;
$quiz->timelimit = 0;
$quiz->delay1 = 1000;
$quiz->delay2 = 0;
$quiz->timeclose = 0;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->timefinish = 10000;
$rule = new inter_attempt_delay_access_rule($quizobj, 10000);
@@ -249,28 +295,30 @@ class inter_attempt_delay_access_rule_test extends UnitTestCase {
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$attempt->timefinish = 9000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$attempt->timefinish = 9001;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
}
- function test_just_second_delay() {
- $quiz = new stdClass;
+ public function test_just_second_delay() {
+ $quiz = new stdClass();
$quiz->attempts = 5;
$quiz->timelimit = 0;
$quiz->delay1 = 0;
$quiz->delay2 = 1000;
$quiz->timeclose = 0;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->timefinish = 10000;
$rule = new inter_attempt_delay_access_rule($quizobj, 10000);
@@ -282,30 +330,34 @@ class inter_attempt_delay_access_rule_test extends UnitTestCase {
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
- $this->assertEqual($rule->prevent_new_attempt(3, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(3, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
$attempt->timefinish = 9000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timefinish = 9001;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
- $this->assertEqual($rule->prevent_new_attempt(4, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(4, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
}
- function test_just_both_delays() {
- $quiz = new stdClass;
+ public function test_just_both_delays() {
+ $quiz = new stdClass();
$quiz->attempts = 5;
$quiz->timelimit = 0;
$quiz->delay1 = 2000;
$quiz->delay2 = 1000;
$quiz->timeclose = 0;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->timefinish = 10000;
$rule = new inter_attempt_delay_access_rule($quizobj, 10000);
@@ -316,39 +368,47 @@ class inter_attempt_delay_access_rule_test extends UnitTestCase {
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(12000)));
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
- $this->assertEqual($rule->prevent_new_attempt(3, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(12000)));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(3, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
$attempt->timefinish = 8000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timefinish = 8001;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(4, $attempt));
$attempt->timefinish = 9000;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timefinish = 9001;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11001)));
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
- $this->assertEqual($rule->prevent_new_attempt(4, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11001)));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(4, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
}
- function test_with_close_date() {
- $quiz = new stdClass;
+ public function test_with_close_date() {
+ $quiz = new stdClass();
$quiz->attempts = 5;
$quiz->timelimit = 0;
$quiz->delay1 = 2000;
$quiz->delay2 = 1000;
$quiz->timeclose = 15000;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->timefinish = 13000;
$rule = new inter_attempt_delay_access_rule($quizobj, 10000);
@@ -358,23 +418,29 @@ class inter_attempt_delay_access_rule_test extends UnitTestCase {
$this->assertFalse($rule->time_left($attempt, 0));
$attempt->timefinish = 13000;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(15000)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(15000)));
$attempt->timefinish = 13001;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youcannotwait', 'quiz'));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youcannotwait', 'quiz'));
$attempt->timefinish = 14000;
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(15000)));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youmustwait', 'quiz', userdate(15000)));
$attempt->timefinish = 14001;
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youcannotwait', 'quiz'));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youcannotwait', 'quiz'));
$rule = new inter_attempt_delay_access_rule($quizobj, 15000);
$attempt->timefinish = 13000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
$attempt->timefinish = 13001;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youcannotwait', 'quiz'));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youcannotwait', 'quiz'));
$attempt->timefinish = 14000;
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$attempt->timefinish = 14001;
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youcannotwait', 'quiz'));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youcannotwait', 'quiz'));
$rule = new inter_attempt_delay_access_rule($quizobj, 15001);
$attempt->timefinish = 13000;
@@ -387,18 +453,18 @@ class inter_attempt_delay_access_rule_test extends UnitTestCase {
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
}
- function test_time_limit_and_overdue() {
- $quiz = new stdClass;
+ public function test_time_limit_and_overdue() {
+ $quiz = new stdClass();
$quiz->attempts = 5;
$quiz->timelimit = 100;
$quiz->delay1 = 2000;
$quiz->delay2 = 1000;
$quiz->timeclose = 0;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$attempt->timestart = 9900;
$attempt->timefinish = 10100;
@@ -410,9 +476,12 @@ class inter_attempt_delay_access_rule_test extends UnitTestCase {
$this->assertFalse($rule->prevent_new_attempt(0, $attempt));
$this->assertFalse($rule->prevent_new_attempt(5, $attempt));
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(12000)));
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
- $this->assertEqual($rule->prevent_new_attempt(3, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(12000)));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(3, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
$attempt->timestart = 7950;
$attempt->timefinish = 8000;
$this->assertFalse($rule->prevent_new_attempt(1, $attempt));
@@ -420,42 +489,56 @@ class inter_attempt_delay_access_rule_test extends UnitTestCase {
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timestart = 7950;
$attempt->timefinish = 8001;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(4, $attempt));
$attempt->timestart = 8950;
$attempt->timefinish = 9000;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timestart = 8950;
$attempt->timefinish = 9001;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11001)));
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
- $this->assertEqual($rule->prevent_new_attempt(4, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11001)));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(4, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
$attempt->timestart = 8900;
$attempt->timefinish = 9100;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11000)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11000)));
$this->assertFalse($rule->prevent_new_attempt(2, $attempt));
$this->assertFalse($rule->prevent_new_attempt(3, $attempt));
$attempt->timestart = 8901;
$attempt->timefinish = 9100;
- $this->assertEqual($rule->prevent_new_attempt(1, $attempt), get_string('youmustwait', 'quiz', userdate(11001)));
- $this->assertEqual($rule->prevent_new_attempt(2, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
- $this->assertEqual($rule->prevent_new_attempt(4, $attempt), get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(1, $attempt),
+ get_string('youmustwait', 'quiz', userdate(11001)));
+ $this->assertEqual($rule->prevent_new_attempt(2, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
+ $this->assertEqual($rule->prevent_new_attempt(4, $attempt),
+ get_string('youmustwait', 'quiz', userdate(10001)));
}
}
+
+/**
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class password_access_rule_test extends UnitTestCase {
- function test_password_access_rule() {
- $quiz = new stdClass;
+ public function test_password_access_rule() {
+ $quiz = new stdClass();
$quiz->password = 'frog';
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
$rule = new password_access_rule($quizobj, 0);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$this->assertFalse($rule->prevent_access());
$this->assertEqual($rule->description(), get_string('requirepasswordmessage', 'quiz'));
@@ -465,18 +548,23 @@ class password_access_rule_test extends UnitTestCase {
}
}
+
+/**
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class securewindow_access_rule_test extends UnitTestCase {
// Nothing very testable in this class, just test that it obeys the general access rule contact.
- function test_securewindow_access_rule() {
- $quiz = new stdClass;
+ public function test_securewindow_access_rule() {
+ $quiz = new stdClass();
$quiz->popup = 1;
$quiz->questions = '';
- $cm = new stdClass;
+ $cm = new stdClass();
$cm->id = 0;
$quizobj = new quiz($quiz, $cm, null);
$rule = new securewindow_access_rule($quizobj, 0);
- $attempt = new stdClass;
+ $attempt = new stdClass();
$this->assertFalse($rule->prevent_access());
$this->assertFalse($rule->description());
@@ -487,3 +575,40 @@ class securewindow_access_rule_test extends UnitTestCase {
}
+/**
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_access_manager_test extends UnitTestCase {
+ public function test_cannot_review_message() {
+ $quiz = new stdClass();
+ $quiz->reviewattempt = 0x10010;
+ $quiz->timeclose = 0;
+ $quiz->attempts = 0;
+ $quiz->questions = '1,2,0,3,4,0';
+
+ $cm = new stdClass();
+ $cm->id = 123;
+
+ $quizobj = new quiz($quiz, $cm, new stdClass(), false);
+
+ $am = new quiz_access_manager($quizobj, time(), false);
+
+ $this->assertEqual('',
+ $am->cannot_review_message(mod_quiz_display_options::DURING));
+ $this->assertEqual('',
+ $am->cannot_review_message(mod_quiz_display_options::IMMEDIATELY_AFTER));
+ $this->assertEqual(get_string('noreview', 'quiz'),
+ $am->cannot_review_message(mod_quiz_display_options::LATER_WHILE_OPEN));
+ $this->assertEqual(get_string('noreview', 'quiz'),
+ $am->cannot_review_message(mod_quiz_display_options::AFTER_CLOSE));
+
+ $closetime = time() + 10000;
+ $quiz->timeclose = $closetime;
+ $quizobj = new quiz($quiz, $cm, new stdClass(), false);
+ $am = new quiz_access_manager($quizobj, time(), false);
+
+ $this->assertEqual(get_string('noreviewuntil', 'quiz', userdate($closetime)),
+ $am->cannot_review_message(mod_quiz_display_options::LATER_WHILE_OPEN));
+ }
+}
diff --git a/mod/quiz/simpletest/testeditlib.php b/mod/quiz/simpletest/testeditlib.php
index c70299da666..6b745d64658 100644
--- a/mod/quiz/simpletest/testeditlib.php
+++ b/mod/quiz/simpletest/testeditlib.php
@@ -1,20 +1,43 @@
.
+
/**
* Unit tests for (some of) mod/quiz/editlib.php.
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2009 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
-}
+
+defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/editlib.php');
+
+/**
+ * Unit tests for (some of) mod/quiz/editlib.php.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class quiz_editlib_test extends UnitTestCase {
public static $includecoverage = array('mod/quiz/editlib.php');
- function test_quiz_move_question_up() {
+ public function test_quiz_move_question_up() {
$this->assertEqual(quiz_move_question_up('0', 123), '0');
$this->assertEqual(quiz_move_question_up('1,2,0', 1), '1,2,0');
$this->assertEqual(quiz_move_question_up('1,2,0', 0), '1,2,0');
@@ -23,7 +46,7 @@ class quiz_editlib_test extends UnitTestCase {
$this->assertEqual(quiz_move_question_up('1,2,3,0,4,0', 4), '1,2,3,4,0,0');
}
- function test_quiz_move_question_down() {
+ public function test_quiz_move_question_down() {
$this->assertEqual(quiz_move_question_down('0', 123), '0');
$this->assertEqual(quiz_move_question_down('1,2,0', 2), '1,2,0');
$this->assertEqual(quiz_move_question_down('1,2,0', 0), '1,2,0');
@@ -32,7 +55,7 @@ class quiz_editlib_test extends UnitTestCase {
$this->assertEqual(quiz_move_question_down('1,0,2,3,0,4,0', 1), '0,1,2,3,0,4,0');
}
- function test_quiz_delete_empty_page() {
+ public function test_quiz_delete_empty_page() {
$this->assertEqual(quiz_delete_empty_page('0', 0), '0');
$this->assertEqual(quiz_delete_empty_page('1,2,0', 2), '1,2,0');
$this->assertEqual(quiz_delete_empty_page('0,1,2,0', -1), '1,2,0');
@@ -45,14 +68,14 @@ class quiz_editlib_test extends UnitTestCase {
$this->assertEqual(quiz_delete_empty_page('0,0,1,2,0', 0), '0,1,2,0');
}
- function test_quiz_add_page_break_after() {
+ public function test_quiz_add_page_break_after() {
$this->assertEqual(quiz_add_page_break_after('0', 1), '0');
$this->assertEqual(quiz_add_page_break_after('1,2,0', 1), '1,0,2,0');
$this->assertEqual(quiz_add_page_break_after('1,2,0', 2), '1,2,0,0');
$this->assertEqual(quiz_add_page_break_after('1,2,0', 0), '1,2,0');
}
- function test_quiz_add_page_break_at() {
+ public function test_quiz_add_page_break_at() {
$this->assertEqual(quiz_add_page_break_at('0', 0), '0,0');
$this->assertEqual(quiz_add_page_break_at('1,2,0', 0), '0,1,2,0');
$this->assertEqual(quiz_add_page_break_at('1,2,0', 1), '1,0,2,0');
@@ -60,4 +83,3 @@ class quiz_editlib_test extends UnitTestCase {
$this->assertEqual(quiz_add_page_break_at('1,2,0', 3), '1,2,0');
}
}
-
diff --git a/mod/quiz/simpletest/testlib.php b/mod/quiz/simpletest/testlib.php
index 89b712fe1c6..831677940a5 100644
--- a/mod/quiz/simpletest/testlib.php
+++ b/mod/quiz/simpletest/testlib.php
@@ -1,22 +1,42 @@
.
+
/**
* Unit tests for (some of) mod/quiz/locallib.php.
*
- * @author T.J.Hunt@open.ac.uk
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
-}
+
+defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/lib.php');
+
+/**
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
class quiz_lib_test extends UnitTestCase {
public static $includecoverage = array('mod/quiz/lib.php');
- function test_quiz_has_grades() {
- $quiz = new stdClass;
+ public function test_quiz_has_grades() {
+ $quiz = new stdClass();
$quiz->grade = '100.0000';
$quiz->sumgrades = '100.0000';
$this->assertTrue(quiz_has_grades($quiz));
@@ -28,8 +48,8 @@ class quiz_lib_test extends UnitTestCase {
$this->assertFalse(quiz_has_grades($quiz));
}
- function test_quiz_format_grade() {
- $quiz = new stdClass;
+ public function test_quiz_format_grade() {
+ $quiz = new stdClass();
$quiz->decimalpoints = 2;
$this->assertEqual(quiz_format_grade($quiz, 0.12345678), format_float(0.12, 2));
$this->assertEqual(quiz_format_grade($quiz, 0), format_float(0, 2));
@@ -38,8 +58,8 @@ class quiz_lib_test extends UnitTestCase {
$this->assertEqual(quiz_format_grade($quiz, 0.12345678), '0');
}
- function test_quiz_format_question_grade() {
- $quiz = new stdClass;
+ public function test_quiz_format_question_grade() {
+ $quiz = new stdClass();
$quiz->decimalpoints = 2;
$quiz->questiondecimalpoints = 2;
$this->assertEqual(quiz_format_question_grade($quiz, 0.12345678), format_float(0.12, 2));
diff --git a/mod/quiz/simpletest/testlocallib.php b/mod/quiz/simpletest/testlocallib.php
index 7f93b89431d..25d0e032cea 100644
--- a/mod/quiz/simpletest/testlocallib.php
+++ b/mod/quiz/simpletest/testlocallib.php
@@ -1,21 +1,43 @@
.
+
/**
* Unit tests for (some of) mod/quiz/locallib.php.
*
- * @author T.J.Hunt@open.ac.uk
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2008 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page.
-}
+
+defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+/**
+ * Unit tests for (some of) mod/quiz/locallib.php.
+ *
+ * @copyright 2008 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class quiz_locallib_test extends UnitTestCase {
public static $includecoverage = array('mod/quiz/locallib.php');
- function test_quiz_questions_in_quiz() {
+ public function test_quiz_questions_in_quiz() {
$this->assertEqual(quiz_questions_in_quiz(''), '');
$this->assertEqual(quiz_questions_in_quiz('0'), '');
$this->assertEqual(quiz_questions_in_quiz('0,0'), '');
@@ -26,7 +48,7 @@ class quiz_locallib_test extends UnitTestCase {
$this->assertEqual(quiz_questions_in_quiz('0,1,0,0,2,0'), '1,2');
}
- function test_quiz_number_of_pages() {
+ public function test_quiz_number_of_pages() {
$this->assertEqual(quiz_number_of_pages('0'), 1);
$this->assertEqual(quiz_number_of_pages('0,0'), 2);
$this->assertEqual(quiz_number_of_pages('0,0,0'), 3);
@@ -38,7 +60,7 @@ class quiz_locallib_test extends UnitTestCase {
$this->assertEqual(quiz_number_of_pages('0,1,0,0,2,0'), 4);
}
- function test_quiz_number_of_questions_in_quiz() {
+ public function test_quiz_number_of_questions_in_quiz() {
$this->assertEqual(quiz_number_of_questions_in_quiz('0'), 0);
$this->assertEqual(quiz_number_of_questions_in_quiz('0,0'), 0);
$this->assertEqual(quiz_number_of_questions_in_quiz('0,0,0'), 0);
@@ -51,7 +73,7 @@ class quiz_locallib_test extends UnitTestCase {
$this->assertEqual(quiz_number_of_questions_in_quiz('10,,0,0'), 1);
}
- function test_quiz_clean_layout() {
+ public function test_quiz_clean_layout() {
// Without stripping empty pages.
$this->assertEqual(quiz_clean_layout(',,1,,,2,,'), '1,2,0');
$this->assertEqual(quiz_clean_layout(''), '0');
@@ -74,18 +96,27 @@ class quiz_locallib_test extends UnitTestCase {
$this->assertEqual(quiz_clean_layout('0,1,0,0,2,0', true), '1,0,2,0');
}
- function test_quiz_rescale_grade() {
- $quiz = new stdClass;
+ public function test_quiz_rescale_grade() {
+ $quiz = new stdClass();
$quiz->decimalpoints = 2;
$quiz->questiondecimalpoints = 3;
$quiz->grade = 10;
$quiz->sumgrades = 10;
$this->assertEqual(quiz_rescale_grade(0.12345678, $quiz, false), 0.12345678);
$this->assertEqual(quiz_rescale_grade(0.12345678, $quiz, true), format_float(0.12, 2));
- $this->assertEqual(quiz_rescale_grade(0.12345678, $quiz, 'question'), format_float(0.123, 3));
+ $this->assertEqual(quiz_rescale_grade(0.12345678, $quiz, 'question'),
+ format_float(0.123, 3));
$quiz->sumgrades = 5;
$this->assertEqual(quiz_rescale_grade(0.12345678, $quiz, false), 0.24691356);
$this->assertEqual(quiz_rescale_grade(0.12345678, $quiz, true), format_float(0.25, 2));
- $this->assertEqual(quiz_rescale_grade(0.12345678, $quiz, 'question'), format_float(0.247, 3));
+ $this->assertEqual(quiz_rescale_grade(0.12345678, $quiz, 'question'),
+ format_float(0.247, 3));
+ }
+
+ public function test_quiz_get_slot_for_question() {
+ $quiz = new stdClass();
+ $quiz->questions = '1,2,0,7,0';
+ $this->assertEqual(1, quiz_get_slot_for_question($quiz, 1));
+ $this->assertEqual(3, quiz_get_slot_for_question($quiz, 7));
}
}
diff --git a/mod/quiz/simpletest/testquizdisplayoptions.php b/mod/quiz/simpletest/testquizdisplayoptions.php
new file mode 100644
index 00000000000..688c88a437b
--- /dev/null
+++ b/mod/quiz/simpletest/testquizdisplayoptions.php
@@ -0,0 +1,80 @@
+.
+
+/**
+ * Unit tests for the mod_quiz_display_options class.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+
+/**
+ * Unit tests for {@link mod_quiz_display_options}.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_quiz_display_options_test extends UnitTestCase {
+ public function test_num_attempts_access_rule() {
+ $quiz = new stdClass();
+ $quiz->decimalpoints = 2;
+ $quiz->questiondecimalpoints = -1;
+ $quiz->reviewattempt = 0x11110;
+ $quiz->reviewcorrectness = 0x10000;
+ $quiz->reviewmarks = 0x01110;
+ $quiz->reviewspecificfeedback = 0x10000;
+ $quiz->reviewgeneralfeedback = 0x01000;
+ $quiz->reviewrightanswer = 0x00100;
+ $quiz->reviewoverallfeedback = 0x00010;
+
+ $options = mod_quiz_display_options::make_from_quiz($quiz,
+ mod_quiz_display_options::DURING);
+
+ $this->assertEqual(true, $options->attempt);
+ $this->assertEqual(mod_quiz_display_options::VISIBLE, $options->correctness);
+ $this->assertEqual(mod_quiz_display_options::MAX_ONLY, $options->marks);
+ $this->assertEqual(2, $options->markdp);
+
+ $quiz->questiondecimalpoints = 5;
+ $options = mod_quiz_display_options::make_from_quiz($quiz,
+ mod_quiz_display_options::IMMEDIATELY_AFTER);
+
+ $this->assertEqual(mod_quiz_display_options::MARK_AND_MAX, $options->marks);
+ $this->assertEqual(mod_quiz_display_options::VISIBLE, $options->generalfeedback);
+ $this->assertEqual(mod_quiz_display_options::HIDDEN, $options->feedback);
+ $this->assertEqual(5, $options->markdp);
+
+ $options = mod_quiz_display_options::make_from_quiz($quiz,
+ mod_quiz_display_options::LATER_WHILE_OPEN);
+
+ $this->assertEqual(mod_quiz_display_options::VISIBLE, $options->rightanswer);
+ $this->assertEqual(mod_quiz_display_options::HIDDEN, $options->generalfeedback);
+
+ $options = mod_quiz_display_options::make_from_quiz($quiz,
+ mod_quiz_display_options::AFTER_CLOSE);
+
+ $this->assertEqual(mod_quiz_display_options::VISIBLE, $options->overallfeedback);
+ $this->assertEqual(mod_quiz_display_options::HIDDEN, $options->rightanswer);
+ }
+}
diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php
index 099480de086..2b119de63cf 100644
--- a/mod/quiz/startattempt.php
+++ b/mod/quiz/startattempt.php
@@ -1,20 +1,36 @@
.
+
/**
- * This page deals with starting a new attempt at a quiz.
+ * This script deals with starting a new attempt at a quiz.
*
* Normally, it will end up redirecting to attempt.php - unless a password form is displayed.
*
* This code used to be at the top of attempt.php, if you are looking for CVS history.
*
- * @author Tim Hunt.
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
-/// Get submitted parameters.
+// Get submitted parameters.
$id = required_param('cmid', PARAM_INT); // Course Module ID
$forcenew = optional_param('forcenew', false, PARAM_BOOL); // Used to force a new preview
@@ -32,75 +48,156 @@ $quizobj = quiz::create($quiz->id, $USER->id);
// This script should only ever be posted to, so set page URL to the view page.
$PAGE->set_url($quizobj->view_url());
-/// Check login and sesskey.
+// Check login and sesskey.
require_login($quizobj->get_courseid(), false, $quizobj->get_cm());
-if (!confirm_sesskey()) {
- throw new moodle_exception('confirmsesskeybad', 'error', $quizobj->view_url());
-}
+require_sesskey();
$PAGE->set_pagelayout('base');
-/// if no questions have been set up yet redirect to edit.php
-if (!$quizobj->get_question_ids() && $quizobj->has_capability('mod/quiz:manage')) {
+// if no questions have been set up yet redirect to edit.php
+if (!$quizobj->has_questions() && $quizobj->has_capability('mod/quiz:manage')) {
redirect($quizobj->edit_url());
}
-/// Create an object to manage all the other (non-roles) access rules.
+// Create an object to manage all the other (non-roles) access rules.
$accessmanager = $quizobj->get_access_manager(time());
if ($quizobj->is_preview_user() && $forcenew) {
$accessmanager->clear_password_access();
}
-
-/// Check capabilities.
+// Check capabilities.
if (!$quizobj->is_preview_user()) {
$quizobj->require_capability('mod/quiz:attempt');
}
-/// Check to see if a new preview was requested.
+// Check to see if a new preview was requested.
if ($quizobj->is_preview_user() && $forcenew) {
-/// To force the creation of a new preview, we set a finish time on the
-/// current attempt (if any). It will then automatically be deleted below
- $DB->set_field('quiz_attempts', 'timefinish', time(), array('quiz' => $quiz->id, 'userid' => $USER->id));
+ // To force the creation of a new preview, we set a finish time on the
+ // current attempt (if any). It will then automatically be deleted below
+ $DB->set_field('quiz_attempts', 'timefinish', time(),
+ array('quiz' => $quiz->id, 'userid' => $USER->id));
}
-/// Look for an existing attempt.
+// Look for an existing attempt.
$lastattempt = quiz_get_latest_attempt_by_user($quiz->id, $USER->id);
if ($lastattempt && !$lastattempt->timefinish) {
-/// Continuation of an attempt - check password then redirect.
+ // Continuation of an attempt - check password then redirect.
$accessmanager->do_password_check($quizobj->is_preview_user());
redirect($quizobj->attempt_url($lastattempt->id));
}
-/// Get number for the next or unfinished attempt
+// Get number for the next or unfinished attempt
if ($lastattempt && !$lastattempt->preview && !$quizobj->is_preview_user()) {
- $lastattemptid = $lastattempt->uniqueid;
$attemptnumber = $lastattempt->attempt + 1;
} else {
$lastattempt = false;
- $lastattemptid = false;
$attemptnumber = 1;
}
-/// Check access.
+// Check access.
$messages = $accessmanager->prevent_access() +
- $accessmanager->prevent_new_attempt($attemptnumber - 1, $lastattempt);
+$accessmanager->prevent_new_attempt($attemptnumber - 1, $lastattempt);
+$output = $PAGE->get_renderer('mod_quiz');
if (!$quizobj->is_preview_user() && $messages) {
print_error('attempterror', 'quiz', $quizobj->view_url(),
- $accessmanager->print_messages($messages, true));
+ $output->print_messages($messages));
}
$accessmanager->do_password_check($quizobj->is_preview_user());
-/// Delete any previous preview attempts belonging to this user.
+// Delete any previous preview attempts belonging to this user.
quiz_delete_previews($quiz, $USER->id);
-/// Create the new attempt and initialize the question sessions
-$attempt = quiz_create_attempt($quiz, $attemptnumber, $lastattempt, time(), $quizobj->is_preview_user());
+$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
+$quba->set_preferred_behaviour($quiz->preferredbehaviour);
-/// Save the attempt in the database.
+// Create the new attempt and initialize the question sessions
+$attempt = quiz_create_attempt($quiz, $attemptnumber, $lastattempt, time(),
+ $quizobj->is_preview_user());
+
+if (!($quiz->attemptonlast && $lastattempt)) {
+ // Starting a normal, new, quiz attempt.
+
+ // Fully load all the questions in this quiz.
+ $quizobj->preload_questions();
+ $quizobj->load_questions();
+
+ // Add them all to the $quba.
+ $idstoslots = array();
+ $questionsinuse = array_keys($quizobj->get_questions());
+ foreach ($quizobj->get_questions() as $i => $questiondata) {
+ if ($questiondata->qtype != 'random') {
+ if (!$quiz->shuffleanswers) {
+ $questiondata->options->shuffleanswers = false;
+ }
+ $question = question_bank::make_question($questiondata);
+
+ } else {
+ $question = question_bank::get_qtype('random')->choose_other_question(
+ $questiondata, $questionsinuse, $quiz->shuffleanswers);
+ if (is_null($question)) {
+ throw new moodle_exception('notenoughrandomquestions', 'quiz',
+ $quizobj->view_url(), $questiondata);
+ }
+ }
+
+ $idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark);
+ $questionsinuse[] = $question->id;
+ }
+
+ // Start all the questions.
+ if ($attempt->preview) {
+ $variantoffset = rand(1, 100);
+ } else {
+ $variantoffset = $attemptnumber;
+ }
+ $quba->start_all_questions(
+ new question_variant_pseudorandom_no_repeats_strategy($variantoffset),
+ time());
+
+ // Update attempt layout.
+ $newlayout = array();
+ foreach (explode(',', $attempt->layout) as $qid) {
+ if ($qid != 0) {
+ $newlayout[] = $idstoslots[$qid];
+ } else {
+ $newlayout[] = 0;
+ }
+ }
+ $attempt->layout = implode(',', $newlayout);
+
+} else {
+ // Starting a subsequent attempt in each attempt builds on last mode.
+
+ $oldquba = question_engine::load_questions_usage_by_activity($lastattempt->uniqueid);
+
+ $oldnumberstonew = array();
+ foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) {
+ $newslot = $quba->add_question($oldqa->get_question(), $oldqa->get_max_mark());
+
+ $quba->start_question_based_on($newslot, $oldqa);
+
+ $oldnumberstonew[$oldslot] = $newslot;
+ }
+
+ // Update attempt layout.
+ $newlayout = array();
+ foreach (explode(',', $lastattempt->layout) as $oldslot) {
+ if ($oldslot != 0) {
+ $newlayout[] = $oldnumberstonew[$oldslot];
+ } else {
+ $newlayout[] = 0;
+ }
+ }
+ $attempt->layout = implode(',', $newlayout);
+}
+
+// Save the attempt in the database.
+$transaction = $DB->start_delegated_transaction();
+question_engine::save_questions_usage_by_activity($quba);
+$attempt->uniqueid = $quba->get_id();
$attempt->id = $DB->insert_record('quiz_attempts', $attempt);
-/// Log the new attempt.
+// Log the new attempt.
if ($attempt->preview) {
add_to_log($course->id, 'quiz', 'preview', 'view.php?id=' . $quizobj->get_cmid(),
$quizobj->get_quizid(), $quizobj->get_cmid());
@@ -109,23 +206,7 @@ if ($attempt->preview) {
$quizobj->get_quizid(), $quizobj->get_cmid());
}
-/// Fully load all the questions in this quiz.
-$quizobj->preload_questions();
-$quizobj->load_questions();
-
-/// Create initial states for all questions in this quiz.
-if (!$quiz->attemptonlast) {
- $lastattemptid = false;
-}
-if (!$states = get_question_states($quizobj->get_questions(), $quizobj->get_quiz(), $attempt, $lastattemptid)) {
- print_error('cannotrestore', 'quiz');
-}
-
-/// Save all the newly created states.
-foreach ($quizobj->get_questions() as $i => $question) {
- save_question_session($question, $states[$i]);
-}
-/// Trigger event
+// Trigger event
$eventdata = new stdClass();
$eventdata->component = 'mod_quiz';
$eventdata->course = $quizobj->get_courseid();
@@ -135,5 +216,7 @@ $eventdata->user = $USER;
$eventdata->attempt = $attempt->id;
events_trigger('quiz_attempt_started', $eventdata);
-/// Redirect to the attempt page.
+$transaction->allow_commit();
+
+// Redirect to the attempt page.
redirect($quizobj->attempt_url($attempt->id));
diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css
index 0f5ffea7b23..31be125f0f0 100644
--- a/mod/quiz/styles.css
+++ b/mod/quiz/styles.css
@@ -1,76 +1,53 @@
-.path-mod-quiz .graph.flexible-wrap {text-align:center;overflow:auto;}
-
-#page-mod-quiz-comment #manualgradingform,
-#page-mod-quiz-report #manualgradingform {width: 100%;}
-
-/** Mixed **/
-#page-mod-quiz-attempt .submitbtns,
-#page-mod-quiz-review .submitbtns,
-#page-mod-quiz-summary .submitbtns {text-align: left;margin-top: 1.5em;}
-
+/** Attempt and review pages **/
#page-mod-quiz-attempt #page .controls,
#page-mod-quiz-summary #page .controls,
#page-mod-quiz-review #page .controls {text-align: center;margin: 8px auto;}
+#page-mod-quiz-attempt .submitbtns,
+#page-mod-quiz-review .submitbtns {clear: left; text-align: left; padding-top: 1.5em;}
+
body.jsenabled .questionflagcheckbox {display: none;}
-#page-mod-quiz-edit div.question div.content .questiontext,
-#categoryquestions .questiontext {-o-text-overflow:ellipsis;text-overflow:ellipsis;position:relative;zoom:1;padding-left:0.3em;max-width:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}
+/** Mod quiz attempt **/
+.generalbox#passwordbox { /* Should probably match .generalbox#intro above */width:70%;margin-left:auto;margin-right:auto;}
+#passwordform {margin: 1em 0;}
-#page-mod-quiz-edit div.question div.content .questionname,
-#categoryquestions .questionname {white-space:nowrap;overflow:hidden;zoom:1;position:relative;max-width:20%;}
+/* Question navigation block. */
+#quiznojswarning {color: red;}
+#quiznojswarning {font-size: 0.7em;line-height: 1.1;}
+.jsenabled #quiznojswarning {display: none;}
-#page-mod-quiz-edit div.editq div.question div.content .singlequestion a .questionname,
-div.editq div.question div.content .singlequestion a .questiontext{text-decoration:underline;}
+.path-mod-quiz #user-picture {margin: 0.5em 0;}
+.path-mod-quiz #user-picture img {width: auto;height: auto;float: left;}
-#page-mod-quiz-edit.ie6 div.question div.content .questiontext,
-#categoryquestions .questiontext {width:50%;}
-#page-mod-quiz-edit.ie6 div.question div.content .questionname,
-#categoryquestions .questionname {width:20%;}
+.path-mod-quiz .qnbutton {display: block; position: relative; float: left; width: 1.5em; height: 1.5em; overflow: hidden; margin: 0.3em 0.3em 0.3em 0; padding: 0; border: 1px solid #bbb; background: #ddd; text-align: center; vertical-align: middle;line-height: 1.5em !important; font-weight: bold; text-decoration: none;}
+.path-mod-quiz .qnbutton:hover {text-decoration: underline;}
+.path-mod-quiz .qnbutton span {cursor: pointer; cursor: hand;}
-#page-mod-quiz-mod #reviewoptionshdr .fitem,
-#adminquizreviewoptions .group {float: left;width: 33%;clear: none;}
+.path-mod-quiz .qnbutton .trafficlight,
+.path-mod-quiz .qnbutton .thispageholder {display: block; position: absolute; top: 0; bottom: 0; left: 0; right: 0;}
-#page-mod-quiz-mod #reviewoptionshdr .fitemtitle,
-#adminquizreviewoptions .fitemtitle {width: 100%;font-weight: bold;text-align: left;height: 2.5em;margin-left: 0;}
+.path-mod-quiz .qnbutton.thispage {border-color: #666;}
+.path-mod-quiz .qnbutton.thispage .thispageholder {border: 1px solid #666;}
-#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup,
-#adminquizreviewoptions {clear: left;}
-#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup span,
-#adminquizreviewoptions span {float: left;clear: left; margin: 0.1em 0;}
-#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup span label,
-#adminquizreviewoptions span label {margin-left: 0.4em;}
+.path-mod-quiz .qnbutton.flagged .trafficlight {background: url([[pix:quiz|navflagged]]) no-repeat top right;}
-table#categoryquestions td,
-#page-mod-quiz-edit table#categoryquestions th{overflow:hidden;white-space:nowrap;}
+.path-mod-quiz .qnbutton.notyetanswered,
+.path-mod-quiz .qnbutton.requiresgrading,
+.path-mod-quiz .qnbutton.invalidanswer {background-color: white;}
+.path-mod-quiz .qnbutton.correct {background-color: #cfc;}
+.path-mod-quiz .qnbutton.correct .trafficlight {border-bottom: 3px solid #080;}
+.path-mod-quiz .qnbutton.partiallycorrect {background-color: #ffa;}
+.path-mod-quiz .qnbutton.notanswered,
+.path-mod-quiz .qnbutton.incorrect {background-color: #fcc;}
+.path-mod-quiz .qnbutton.notanswered .trafficlight,
+.path-mod-quiz .qnbutton.incorrect .trafficlight {border-top: 3px solid #800;}
-/** mod quiz mod **/
-#page-mod-quiz-mod #reviewoptionshdr .fitem {width: 30%;margin-left: 10px;}
-#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup {width: 100%;text-align: left;margin-left: 0;}
+.path-mod-quiz .othernav {clear: both; margin: 0.5em 0;}
+.path-mod-quiz .othernav a,
+.path-mod-quiz .othernav input {display: block;margin: 0.5em 0;}
-/** Mod quiz view **/
-#page-mod-quiz-view .quizinfo,
-#page-mod-quiz-view #page .quizgradefeedback,
-#page-mod-quiz-view #page .quizattempt {text-align: center;}
-#page-mod-quiz-view #page .quizattemptsummary td p {margin-top: 0;}
-#page-mod-quiz-view .generaltable.quizattemptsummary {margin-left:auto;margin-right:auto;}
-#page-mod-quiz-view .generalbox#feedback {width:70%;margin-left:auto;margin-right:auto;padding-bottom:15px;}
-#page-mod-quiz-view .generalbox#feedback h2 {margin: 0;}
-#page-mod-quiz-view .generalbox#feedback h3 {text-align: left;}
-#page-mod-quiz-view .generalbox#feedback .overriddennotice {text-align: center;font-size: 0.7em;}
-.quizstartbuttondiv.quizsecuremoderequired input { display: none; }
-.jsenabled .quizstartbuttondiv.quizsecuremoderequired input { display: inline; }
-
-/** Mod quiz summary **/
-
-#page-mod-quiz-summary #content {text-align: center;}
-#page-mod-quiz-summary .questionflag {width: 16px;height: 16px;vertical-align: middle;}
-#page-mod-quiz-summary #quiz-timer {text-align: center;}
-@media print {
- .quiz-secure-window * { display: none !important; }
-}
-
-/** Countdown timer. */
+/* Countdown timer. */
#quiz-timer {display: none; margin-top: 1em;}
#quiz-time-left {font-weight: bold;}
#quiz-timer.timeleft15 {background: #ffffff;}
@@ -90,8 +67,73 @@ table#categoryquestions td,
#quiz-timer.timeleft1 {background: #ff1111;}
#quiz-timer.timeleft0 {background: #ff0000;}
+/** mod quiz mod **/
+#page-mod-quiz-mod #reviewoptionshdr .fitem {width: 23%;margin-left: 10px;}
+#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup {width: 100%;text-align: left;margin-left: 0;}
+
+#page-mod-quiz-edit div.question div.content .questiontext,
+#categoryquestions .questiontext {-o-text-overflow:ellipsis;text-overflow:ellipsis;position:relative;zoom:1;padding-left:0.3em;max-width:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}
+
+#page-mod-quiz-edit div.question div.content .questionname,
+#categoryquestions .questionname {white-space:nowrap;overflow:hidden;zoom:1;position:relative;max-width:20%;}
+
+#page-mod-quiz-edit div.editq div.question div.content .singlequestion a .questionname,
+div.editq div.question div.content .singlequestion a .questiontext{text-decoration:underline;}
+
+#page-mod-quiz-edit.ie6 div.question div.content .questiontext {width:50%;}
+#page-mod-quiz-edit.ie6 div.question div.content .questionname {width:20%;}
+
+#page-mod-quiz-mod #reviewoptionshdr .fitem,
+#adminquizreviewoptions .group {float: left;width: 33%;clear: none;}
+
+#page-mod-quiz-mod #reviewoptionshdr .fitemtitle,
+#adminquizreviewoptions .fitemtitle {width: 100%;font-weight: bold;text-align: left;height: 2.5em;margin-left: 0;}
+
+#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup,
+#adminquizreviewoptions {clear: left;}
+#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup span,
+#adminquizreviewoptions span {float: left;clear: left; margin: 0.1em 0;}
+#page-mod-quiz-mod #reviewoptionshdr fieldset.fgroup span label,
+#adminquizreviewoptions span label {margin-left: 0.4em;}
+
+/** Mod quiz view **/
+#page-mod-quiz-view .quizinfo,
+#page-mod-quiz-view #page .quizgradefeedback,
+#page-mod-quiz-view #page .quizattempt {text-align: center;}
+#page-mod-quiz-view #page .quizattemptsummary td p {margin-top: 0;}
+table.quizattemptsummary .bestrow td {background-color: #e8e8e8;}
+table.quizattemptsummary .noreviewmessage {color: gray;}
+#page-mod-quiz-view .generaltable.quizattemptsummary {margin-left:auto;margin-right:auto;}
+#page-mod-quiz-view .generalbox#feedback {width:70%;margin-left:auto;margin-right:auto;padding-bottom:15px;}
+#page-mod-quiz-view .generalbox#feedback h2 {margin: 0;}
+#page-mod-quiz-view .generalbox#feedback h3 {text-align: left;}
+#page-mod-quiz-view .generalbox#feedback .overriddennotice {text-align: center;font-size: 0.7em;}
+.quizstartbuttondiv.quizsecuremoderequired input { display: none; }
+.jsenabled .quizstartbuttondiv.quizsecuremoderequired input { display: inline; }
+
+.mod-quiz .gradedattempt,
+.mod-quiz tr.gradedattempt td { background-color: #e8e8e8; }
+
+.quizattemptcounts {clear: left; text-align: center;}
+
+/** Mod quiz summary **/
+#page-mod-quiz-summary #content {text-align: center;}
+#page-mod-quiz-summary .questionflag {width: 16px;height: 16px;vertical-align: middle;}
+#page-mod-quiz-summary #quiz-timer {text-align: center; margin-top: 1em;}
+#page-mod-quiz-summary .submitbtns {margin-top: 1.5em;}
+@media print {
+ .quiz-secure-window * { display: none !important; }
+}
+
/** Mod quiz review **/
-#page-mod-quiz-review .pagingbar {margin: 1.5em auto;}
+table.quizreviewsummary {width: 100%;}
+table.quizreviewsummary th.cell {padding: 1px 0.5em 1px 1em;font-weight: bold;text-align: right;width: 10em;background: #f0f0f0;}
+table.quizreviewsummary td.cell {padding: 1px 1em 1px 0.5em;text-align: left;background: #fafafa;}
+
+/** Mod quiz make comment or override grade popup. **/
+#page-mod-quiz-comment .mform {width: 100%;}
+#page-mod-quiz-comment .mform fieldset {margin: 0;}
+#page-mod-quiz-comment .que {margin: 0;}
/** Mod quiz report **/
#page-mod-quiz-report h2.main {clear: both;}
@@ -100,9 +142,13 @@ table#categoryquestions td,
#page-mod-quiz-report .dubious{background-color: #fcc;}
#page-mod-quiz-report .highlight{border :medium solid yellow;background-color:lightYellow;}
#page-mod-quiz-report .negcovar{border :medium solid pink;}
-#page-mod-quiz-report #manualgradingform .que {margin-bottom: 0.7em;}
-#page-mod-quiz-report table.titlesleft td.c0{font-weight: bold;}
+#page-mod-quiz-report .toggleincludeauto {text-align: center;}
+#page-mod-quiz-report .gradetheselink {font-size: 0.8em;}
+#page-mod-quiz-report .mform fieldset {margin: 0;}
+#page-mod-quiz-report fieldset.felement.fgroup {margin: 0;}
+#page-mod-quiz-report table.titlesleft td.c0 {font-weight: bold;}
#page-mod-quiz-report table .numcol {text-align: center;vertical-align : middle !important;}
+
#page-mod-quiz-report table#attempts {clear: both;width: 80%; margin: 0.2em auto;}
#page-mod-quiz-report table#attempts .header,
#page-mod-quiz-report table#attempts .cell{padding: 4px;}
@@ -111,29 +157,20 @@ table#categoryquestions td,
#page-mod-quiz-report table#attempts td {border-left-width: 1px;border-right-width: 1px;border-left-style: solid;border-right-style: solid;vertical-align: middle;}
#page-mod-quiz-report table#attempts .header {text-align: left;}
#page-mod-quiz-report table#attempts .picture {text-align: center !important;}
-#page-mod-quiz-report table#itemanalysis {width: 80%;margin: 20px auto;}
-#page-mod-quiz-report table#itemanalysis td {border-width: 1px;border-style: solid;}
-#page-mod-quiz-report table#itemanalysis .header {text-align: left;padding: 4px;}
-#page-mod-quiz-report table#itemanalysis .header .commands {display: inline;}
-#page-mod-quiz-report table#itemanalysis .uncorrect {color: red;}
-#page-mod-quiz-report table#itemanalysis .correct {color: blue;font-weight : bold;}
-#page-mod-quiz-report table#itemanalysis .partialcorrect {color: green !important;}
-#page-mod-quiz-report table#itemanalysis .cell{padding: 4px;}
-#page-mod-quiz-report table#itemanalysis .qname {color: green !important;}
-#page-mod-quiz-report fieldset.felement.fgroup {margin: 0;}
+#page-mod-quiz-report table#attempts.grades span.que,
+#page-mod-quiz-report table#attempts span.avgcell {white-space: nowrap;}
+#page-mod-quiz-report table#attempts span.que .requiresgrading {white-space: normal;}
+#page-mod-quiz-report table#attempts .questionflag {width: 16px; height: 16px; vertical-align: middle;}
+
+#page-mod-quiz-report .graph.flexible-wrap {text-align:center; overflow:auto;}
+
#page-mod-quiz-report #cachingnotice {margin-bottom: 1em; padding: 0.2em; }
#page-mod-quiz-report #cachingnotice .singlebutton {margin: 0.5em 0 0;}
#page-mod-quiz-report .bold .reviewlink {font-weight: normal;}
-/** Mod quiz grading **/
-#page-mod-quiz-grading table#grading {width: 80%;margin: 20px auto;}
-#page-mod-quiz-grading table#grading .header,
-#page-mod-quiz-grading table#grading .cell {padding: 4px;}
-#page-mod-quiz-grading table#grading .header .commands{display: inline;}
-#page-mod-quiz-grading table#grading .picture{width: 40px;}
-#page-mod-quiz-grading table#grading td {border-left-width: 1px;border-right-width: 1px;border-left-style: solid;border-right-style: solid;vertical-align: bottom;}
+/** Mod quiz edit **/
+#page-mod-quiz-edit h2.main{display:inline;padding-right:1em;clear:left;}
-/** Mod quiz attempt **/
#categoryquestions .r1 {background: #e4e4e4;}
#categoryquestions .header {text-align: center;padding: 0 2px;border: 0 none;}
#categoryquestions th.modifiername .sorters,
@@ -145,69 +182,6 @@ table#categoryquestions {width: 100%;overflow: hidden;table-layout: fixed;}
#categoryquestions .qtype {width: 24px;padding: 0;}
#categoryquestions .questiontext p {margin: 0;}
-table.quizattemptsummary .bestrow td {background-color: #e8e8e8;}
-table.quizattemptsummary .noreviewmessage {color: gray;}
-
-table.quizreviewsummary {width: 100%;}
-table.quizreviewsummary th.cell {padding: 1px 0.5em 1px 1em;font-weight: bold;text-align: right;width: 10em;background: #f0f0f0;}
-table.quizreviewsummary td.cell {padding: 1px 1em 1px 0.5em;text-align: left;background: #fafafa;}
-
-.path-mod-quiz #user-picture {margin: 0.5em 0;}
-.path-mod-quiz #user-picture img {width: auto;height: auto;float: left;}
-.path-mod-quiz .othernav {clear: both;}
-.path-mod-quiz .othernav a,
-.path-mod-quiz .othernav input {display: block;margin: 0.5em 0;}
-.path-mod-quiz .qnbutton {display: block;float: left;width: 1.5em;height: 1.5em;overflow: hidden;margin: 0.3em 0.3em 0.3em 0;padding: 0;border: 1px solid #bbb;background: #eee no-repeat top right;text-align: center;vertical-align: middle;cursor: pointer;white-space: normal;font: inherit;line-height: 1.5em;font-weight: bold;color: #00f;border-color: #bbb;background-color: #ddd;}
-.path-mod-quiz .qnbutton:hover {text-decoration: underline;color: #f00;}
-.path-mod-quiz .qnbutton.flagged {background-image: url([[pix:i/ne_red_mark]]);}
-.path-mod-quiz .qnbutton.thispage {border-color: black;}
-.path-mod-quiz .qnbutton.open {background-color: white;}
-.path-mod-quiz .qnbutton.correct {background-color: #cfc;}
-.path-mod-quiz .qnbutton.partiallycorrect {background-color: #ffa;}
-.path-mod-quiz .qnbutton.incorrect {background-color: #fcc;}
-
-#quiznojswarning {color: red;}
-#quiznojswarning {font-size: 0.7em;line-height: 1.1;}
-.jsenabled #quiznojswarning {display: none;}
-
-body#question-preview .quemodname,
-body#question-preview .controls{text-align: center;}
-
-.quizattemptcounts {clear: left; text-align: center;}
-.generalbox#passwordbox { /* Should probably match .generalbox#intro above */width:70%;margin-left:auto;margin-right:auto;}
-#passwordform {margin: 1em 0;}
-
-.questionbankwindow.block {float:right;width:30%;right:0.3em;padding-bottom:0.5em;display:block;border-width:0;}
-.questionbankwindow.block .content {padding:0;}
-.questionbankwindow .choosecategory,
-.questionbankwindow .createnewquestion {padding: 0.3em;}
-.questionbankwindow .createnewquestion .singlebutton {display: inline;}
-.questionbankwindow #catmenu_jump {display: block;}
-
-.questionbank div.categoryquestionscontainer,
-.questionbank .categorysortopotionscontainer,
-.questionbank .categorypagingbarcontainer,
-.questionbank .categoryselectallcontainer{padding-left:0.3em;padding-right:0.3em;}
-
-.noquestionsincategory{clear:both;padding-top:1em;padding-bottom:1em;}
-.modulespecificbuttonscontainer{padding-left:0.3em;padding-right:0.3em;}
-
-#adminquizreviewoptions {margin-bottom: 0.5em;}
-.quizquestionlistcontrols {text-align: center;}
-
-.categoryinfo {padding: 0.3em;}
-
-.path-mod-quiz .gradingdetails {font-size: small;}
-.path-mod-quiz .highlightgraded {background:yellow;}
-.path-mod-quiz div.tabtree a span img.iconsmall {vertical-align: baseline;}
-.ie6.path-mod-quiz div.tabtree a span img.iconsmall {margin: 0;vertical-align: baseline;position: relative;top: 1px;}
-.ie7.path-mod-quiz div.tabtree a span img.iconsmall {margin: 0;vertical-align: baseline;position: relative;top: 2px;}
-
-/** Mod quiz edit **/
-#page-mod-quiz-edit h2.main{display:inline;padding-right:1em;clear:left;}
-
-
-
#page-mod-quiz-edit div.quizcontents {float:left;width:70%;display:block;clear:left;}
#page-mod-quiz-edit div.quizwhenbankcollapsed {width:100%;}
#page-mod-quiz-edit div.quizpage {display:block;clear:both;width:100%;}
@@ -288,6 +262,30 @@ body#question-preview .controls{text-align: center;}
#page-mod-quiz-edit .editq div.question div.description div.content .questiontext {max-width: 75%;}
#page-mod-quiz-edit .editq div.question div.qnum{font-size:1.5em;}
+table#categoryquestions td,
+#page-mod-quiz-edit table#categoryquestions th{overflow:hidden;white-space:nowrap;}
+
+.questionbankwindow.block {float:right;width:30%;right:0.3em;padding-bottom:0.5em;display:block;border-width:0;}
+.questionbankwindow.block .content {padding:0;}
+.questionbankwindow .choosecategory,
+.questionbankwindow .createnewquestion {padding: 0.3em;}
+.questionbankwindow .createnewquestion .singlebutton {display: inline;}
+.questionbankwindow #catmenu_jump {display: block;}
+
+.questionbank div.categoryquestionscontainer,
+.questionbank .categorysortopotionscontainer,
+.questionbank .categorypagingbarcontainer,
+.questionbank .categoryselectallcontainer{padding-left:0.3em;padding-right:0.3em;}
+
+.noquestionsincategory{clear:both;padding-top:1em;padding-bottom:1em;}
+.modulespecificbuttonscontainer{padding-left:0.3em;padding-right:0.3em;}
+
+.quizquestionlistcontrols {text-align: center;}
+
+.categoryinfo {padding: 0.3em;}
+
+.path-mod-quiz .gradingdetails {font-size: small;}
+
body #quizcontentsblock #repaginatedialog {display: none;}
body.jsenabled #quizcontentsblock #repaginatedialog .hd {display:block;}
body.jsenabled #quizcontentsblock #repaginatedialog .bd {padding:1em;}
@@ -365,7 +363,7 @@ bank window's title is prominent enough*/
.questionbank .categoryselectallcontainer{background-color:#FFF;}
#categoryquestions .questiontext {width:50%;}
-#categoryquestions .questionname {width:20%;}
+#categoryquestions .questionname {width:50%;}
.ie6#page-mod-quiz-edit div.question div.content .questiontext,
.ie6#page-mod-quiz-edit #categoryquestions .questionname{/*ie6 shows this as an arrow if this is not specified*/cursor: pointer;}
@@ -375,4 +373,7 @@ bank window's title is prominent enough*/
.ie6#page-mod-quiz-edit div.question div.content .questiontext {width:50%;}
.ie6#page-mod-quiz-edit div.question div.content .questionname {width:20%;}
.ie6#page-mod-quiz-edit .editq div.question div.content .randomquestioncategory a{width:40%;}
-.ie6#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestioncategory label{width: 35%;}
\ No newline at end of file
+.ie6#page-mod-quiz-edit .reorder .questioncontentcontainer .randomquestioncategory label{width: 35%;}
+
+/** settings.php */
+#adminquizreviewoptions {margin-bottom: 0.5em;}
diff --git a/mod/quiz/summary.php b/mod/quiz/summary.php
index 0f678139c48..0024f4223bc 100644
--- a/mod/quiz/summary.php
+++ b/mod/quiz/summary.php
@@ -1,12 +1,29 @@
.
+
/**
* This page prints a summary of a quiz attempt before it is submitted.
*
- * @author Tim Hunt others.
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package quiz
+ * @package mod
+ * @subpackage quiz
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
@@ -16,15 +33,20 @@ $PAGE->set_url('/mod/quiz/summary.php', array('attempt' => $attemptid));
$attemptobj = quiz_attempt::create($attemptid);
-/// Check login.
+// Check login.
require_login($attemptobj->get_course(), false, $attemptobj->get_cm());
-/// If this is not our own attempt, display an error.
+// If this is not our own attempt, display an error.
if ($attemptobj->get_userid() != $USER->id) {
print_error('notyourattempt', 'quiz', $attemptobj->view_url());
}
-/// If the attempt is alreadyuj closed, redirect them to the review page.
+// Check capabilites.
+if (!$attemptobj->is_preview_user()) {
+ $attemptobj->require_capability('mod/quiz:attempt');
+}
+
+// If the attempt is already closed, redirect them to the review page.
if ($attemptobj->is_finished()) {
redirect($attemptobj->review_url());
}
@@ -33,112 +55,52 @@ if ($attemptobj->is_preview_user()) {
navigation_node::override_active_url($attemptobj->start_attempt_url());
}
-/// Check access.
+// Check access.
$accessmanager = $attemptobj->get_access_manager(time());
$messages = $accessmanager->prevent_access();
+$output = $PAGE->get_renderer('mod_quiz');
if (!$attemptobj->is_preview_user() && $messages) {
print_error('attempterror', 'quiz', $attemptobj->view_url(),
- $accessmanager->print_messages($messages, true));
+ $output->print_messages($messages));
}
$accessmanager->do_password_check($attemptobj->is_preview_user());
-/// Log this page view.
-add_to_log($attemptobj->get_courseid(), 'quiz', 'view summary', 'summary.php?attempt=' . $attemptobj->get_attemptid(),
+$displayoptions = $attemptobj->get_display_options(false);
+
+// Log this page view.
+add_to_log($attemptobj->get_courseid(), 'quiz', 'view summary',
+ 'summary.php?attempt=' . $attemptobj->get_attemptid(),
$attemptobj->get_quizid(), $attemptobj->get_cmid());
-/// Load the questions and states.
-$attemptobj->load_questions();
-$attemptobj->load_question_states();
-
+// Print the page header
if (empty($attemptobj->get_quiz()->showblocks)) {
$PAGE->blocks->show_only_fake_blocks();
}
-/// Print the page header
$title = get_string('summaryofattempt', 'quiz');
if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) {
$accessmanager->setup_secure_page($attemptobj->get_course()->shortname . ': ' .
format_string($attemptobj->get_quiz_name()), '');
-} elseif ($accessmanager->safebrowser_required($attemptobj->is_preview_user())) {
- $PAGE->set_title($attemptobj->get_course()->shortname . ': '.format_string($attemptobj->get_quiz_name()));
+} else if ($accessmanager->safebrowser_required($attemptobj->is_preview_user())) {
+ $PAGE->set_title($attemptobj->get_course()->shortname . ': ' .
+ format_string($attemptobj->get_quiz_name()));
$PAGE->set_heading($attemptobj->get_course()->fullname);
$PAGE->set_cacheable(false);
echo $OUTPUT->header();
} else {
- $attemptobj->navigation($title);
+ $PAGE->navbar->add($title);
$PAGE->set_title(format_string($attemptobj->get_quiz_name()));
$PAGE->set_heading($attemptobj->get_course()->fullname);
echo $OUTPUT->header();
}
-/// Print heading.
+// Print heading.
echo $OUTPUT->heading(format_string($attemptobj->get_quiz_name()));
-if ($attemptobj->is_preview_user()) {
- $attemptobj->print_restart_preview_button();
-}
-echo $OUTPUT->heading($title);
+echo $OUTPUT->heading($title, 3);
-/// Prepare the summary table header
-$table = new html_table();
-$table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter';
-$table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz'));
-$table->align = array('left', 'left');
-$table->size = array('', '');
-$scorescolumn = $attemptobj->get_review_options()->scores &&
- ($attemptobj->get_quiz()->optionflags & QUESTION_ADAPTIVE);
-if ($scorescolumn) {
- $table->head[] = get_string('marks', 'quiz');
- $table->align[] = 'left';
- $table->size[] = '';
-}
-$table->data = array();
+echo $output->summary_page($attemptobj, $displayoptions);
-/// Get the summary info for each question.
-$questionids = $attemptobj->get_question_ids();
-foreach ($attemptobj->get_question_iterator() as $number => $question) {
- if ($question->length == 0) {
- continue;
- }
- $flag = '';
- if ($attemptobj->is_question_flagged($question->id)) {
- $flag = '
';
- }
- $row = array('
' . $number . $flag . '',
- get_string($attemptobj->get_question_status($question->id), 'quiz'));
- if ($scorescolumn) {
- $row[] = $attemptobj->get_question_score($question->id);
- }
- $table->data[] = $row;
-}
-
-/// Print the summary table.
-echo html_writer::table($table);
-
-/// countdown timer
-echo $attemptobj->get_timer_html();
-
-/// Finish attempt button.
-echo $OUTPUT->container_start('submitbtns mdl-align');
-$options = array(
- 'attempt' => $attemptobj->get_attemptid(),
- 'finishattempt' => 1,
- 'timeup' => 0,
- 'questionids' => '',
- 'sesskey' => sesskey(),
-);
-
-$button = new single_button(new moodle_url($attemptobj->processattempt_url(), $options), get_string('submitallandfinish', 'quiz'));
-$button->id = 'responseform';
-$button->add_confirm_action(get_string('confirmclose', 'quiz'));
-
-echo $OUTPUT->container_start('controls');
-echo $OUTPUT->render($button);
-echo $OUTPUT->container_end();
-echo $OUTPUT->container_end();
-
-/// Finish the page
+// Finish the page
$accessmanager->show_attempt_timer_if_needed($attemptobj->get_attempt(), time());
echo $OUTPUT->footer();
-
diff --git a/mod/quiz/version.php b/mod/quiz/version.php
index 1a37e4763f4..9b7fd7edc31 100644
--- a/mod/quiz/version.php
+++ b/mod/quiz/version.php
@@ -1,12 +1,30 @@
.
-////////////////////////////////////////////////////////////////////////////////
-// Code fragment to define the version of quiz
-// This fragment is called by moodle_needs_upgrading() and /admin/index.php
-////////////////////////////////////////////////////////////////////////////////
-
-$module->version = 2010122304; // The (date) version of this module
-$module->requires = 2010080300; // Requires this Moodle version
-$module->cron = 0; // How often should cron check this module (seconds)?
+/**
+ * Quiz statistics report version information.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die();
+$module->version = 2011051250;
+$module->requires = 2011060313;
+$module->cron = 0;
diff --git a/mod/quiz/view.php b/mod/quiz/view.php
index ce970257688..642e8bf5d84 100644
--- a/mod/quiz/view.php
+++ b/mod/quiz/view.php
@@ -1,389 +1,228 @@
.
-/// This page prints a particular instance of quiz
+/**
+ * This page is the entry page into the quiz UI. Displays information about the
+ * quiz to students and teachers, and lets students see their previous attempts.
+ *
+ * @package mod
+ * @subpackage quiz
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
- require_once(dirname(__FILE__) . '/../../config.php');
- require_once($CFG->libdir.'/gradelib.php');
- require_once($CFG->dirroot.'/mod/quiz/locallib.php');
- require_once($CFG->libdir . '/completionlib.php');
- $id = optional_param('id', 0, PARAM_INT); // Course Module ID, or
- $q = optional_param('q', 0, PARAM_INT); // quiz ID
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->libdir.'/gradelib.php');
+require_once($CFG->dirroot.'/mod/quiz/locallib.php');
+require_once($CFG->libdir . '/completionlib.php');
- if ($id) {
- if (! $cm = get_coursemodule_from_id('quiz', $id)) {
- print_error('invalidcoursemodule');
+$id = optional_param('id', 0, PARAM_INT); // Course Module ID, or
+$q = optional_param('q', 0, PARAM_INT); // quiz ID
+
+if ($id) {
+ if (!$cm = get_coursemodule_from_id('quiz', $id)) {
+ print_error('invalidcoursemodule');
+ }
+ if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
+ print_error('coursemisconf');
+ }
+ if (!$quiz = $DB->get_record('quiz', array('id' => $cm->instance))) {
+ print_error('invalidcoursemodule');
+ }
+} else {
+ if (!$quiz = $DB->get_record('quiz', array('id' => $q))) {
+ print_error('invalidquizid', 'quiz');
+ }
+ if (!$course = $DB->get_record('course', array('id' => $quiz->course))) {
+ print_error('invalidcourseid');
+ }
+ if (!$cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
+ print_error('invalidcoursemodule');
+ }
+}
+
+// Check login and get context.
+require_login($course->id, false, $cm);
+$context = get_context_instance(CONTEXT_MODULE, $cm->id);
+require_capability('mod/quiz:view', $context);
+
+// Cache some other capabilities we use several times.
+$canattempt = has_capability('mod/quiz:attempt', $context);
+$canreviewmine = has_capability('mod/quiz:reviewmyattempts', $context);
+$canpreview = has_capability('mod/quiz:preview', $context);
+
+// Create an object to manage all the other (non-roles) access rules.
+$timenow = time();
+$accessmanager = new quiz_access_manager(quiz::create($quiz->id, $USER->id), $timenow,
+ has_capability('mod/quiz:ignoretimelimits', $context, null, false));
+
+// Log this request.
+add_to_log($course->id, 'quiz', 'view', 'view.php?id=' . $cm->id, $quiz->id, $cm->id);
+
+$completion = new completion_info($course);
+$completion->set_module_viewed($cm);
+
+// Initialize $PAGE, compute blocks
+$PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id));
+
+$edit = optional_param('edit', -1, PARAM_BOOL);
+if ($edit != -1 && $PAGE->user_allowed_editing()) {
+ $USER->editing = $edit;
+}
+
+// Update the quiz with overrides for the current user
+$quiz = quiz_update_effective_access($quiz, $USER->id);
+
+// Get this user's attempts.
+$attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
+$lastfinishedattempt = end($attempts);
+$unfinished = false;
+if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
+ $attempts[] = $unfinishedattempt;
+ $unfinished = true;
+}
+$numattempts = count($attempts);
+
+// Work out the final grade, checking whether it was overridden in the gradebook.
+$mygrade = quiz_get_best_grade($quiz, $USER->id);
+$mygradeoverridden = false;
+$gradebookfeedback = '';
+
+$grading_info = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $USER->id);
+if (!empty($grading_info->items)) {
+ $item = $grading_info->items[0];
+ if (isset($item->grades[$USER->id])) {
+ $grade = $item->grades[$USER->id];
+
+ if ($grade->overridden) {
+ $mygrade = $grade->grade + 0; // Convert to number.
+ $mygradeoverridden = true;
}
- if (! $course = $DB->get_record('course', array('id' => $cm->course))) {
- print_error('coursemisconf');
+ if (!empty($grade->str_feedback)) {
+ $gradebookfeedback = $grade->str_feedback;
}
- if (! $quiz = $DB->get_record('quiz', array('id' => $cm->instance))) {
- print_error('invalidcoursemodule');
+ }
+}
+
+$title = $course->shortname . ': ' . format_string($quiz->name);
+$PAGE->set_title($title);
+$PAGE->set_heading($course->fullname);
+$output = $PAGE->get_renderer('mod_quiz');
+
+/*
+ * Create view object for use within renderers file
+ */
+$viewobj = new mod_quiz_view_object();
+$viewobj->attempts = $attempts;
+$viewobj->accessmanager = $accessmanager;
+$viewobj->canattempt = $canattempt;
+$viewobj->canpreview = $canpreview;
+$viewobj->canreviewmine = $canreviewmine;
+
+// Print table with existing attempts
+if ($attempts) {
+ // Work out which columns we need, taking account what data is available in each attempt.
+ list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts, $context);
+
+ $viewobj->attemptcolumn = $quiz->attempts != 1;
+
+ $viewobj->gradecolumn = $someoptions->marks >= question_display_options::MARK_AND_MAX &&
+ quiz_has_grades($quiz);
+ $viewobj->markcolumn = $viewobj->gradecolumn && ($quiz->grade != $quiz->sumgrades);
+ $viewobj->overallstats = $alloptions->marks >= question_display_options::MARK_AND_MAX;
+
+ $viewobj->feedbackcolumn = quiz_has_feedback($quiz) && $alloptions->overallfeedback;
+} else {
+ $viewobj->attemptcolumn = 1;
+}
+
+$moreattempts = $unfinished || !$accessmanager->is_finished($numattempts, $lastfinishedattempt);
+
+$viewobj->timenow = $timenow;
+$viewobj->numattempts = $numattempts;
+$viewobj->mygrade = $mygrade;
+$viewobj->moreattempts = $moreattempts;
+$viewobj->mygradeoverridden = $mygradeoverridden;
+$viewobj->gradebookfeedback = $gradebookfeedback;
+$viewobj->unfinished = $unfinished;
+$viewobj->lastfinishedattempt = $lastfinishedattempt;
+
+// Display information about this quiz.
+$infomessages = $viewobj->accessmanager->describe_rules();
+if ($quiz->attempts != 1) {
+ $infomessages[] = get_string('gradingmethod', 'quiz',
+ quiz_get_grading_option_name($quiz->grademethod));
+}
+
+// This will be set something if as start/continue attempt button should appear.
+$buttontext = '';
+$preventmessages = array();
+if (!quiz_clean_layout($quiz->questions, true)) {
+ $buttontext = '';
+
+} else {
+ if ($viewobj->unfinished) {
+ if ($viewobj->canattempt) {
+ $buttontext = get_string('continueattemptquiz', 'quiz');
+ } else if ($viewobj->canpreview) {
+ $buttontext = get_string('continuepreview', 'quiz');
}
+
} else {
- if (! $quiz = $DB->get_record('quiz', array('id' => $q))) {
- print_error('invalidquizid', 'quiz');
- }
- if (! $course = $DB->get_record('course', array('id' => $quiz->course))) {
- print_error('invalidcourseid');
- }
- if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
- print_error('invalidcoursemodule');
- }
- }
-
-/// Check login and get context.
- require_login($course->id, false, $cm);
- $context = get_context_instance(CONTEXT_MODULE, $cm->id);
- require_capability('mod/quiz:view', $context);
-
-/// Cache some other capabilities we use several times.
- $canattempt = has_capability('mod/quiz:attempt', $context);
- $canreviewmine = has_capability('mod/quiz:reviewmyattempts', $context);
- $canpreview = has_capability('mod/quiz:preview', $context);
-
-/// Create an object to manage all the other (non-roles) access rules.
- $timenow = time();
- $accessmanager = new quiz_access_manager(quiz::create($quiz->id, $USER->id), $timenow,
- has_capability('mod/quiz:ignoretimelimits', $context, NULL, false));
-
-/// If no questions have been set up yet redirect to edit.php
- if (!$quiz->questions && has_capability('mod/quiz:manage', $context)) {
- redirect($CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $cm->id);
- }
-
-/// Log this request.
- add_to_log($course->id, "quiz", "view", "view.php?id=$cm->id", $quiz->id, $cm->id);
-
- // Mark module as viewed
- $completion = new completion_info($course);
- $completion->set_module_viewed($cm);
-
-/// Initialize $PAGE, compute blocks
- $PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id));
-
- $edit = optional_param('edit', -1, PARAM_BOOL);
- if ($edit != -1 && $PAGE->user_allowed_editing()) {
- $USER->editing = $edit;
- }
-
- $PAGE->requires->yui2_lib('event');
-
- // Note: MDL-19010 there will be further changes to printing header and blocks.
- // The code will be much nicer than this eventually.
- $title = $course->shortname . ': ' . format_string($quiz->name);
-
- if ($PAGE->user_allowed_editing()) {
- $buttons = '
';
- $PAGE->set_button($buttons);
- }
-
- $PAGE->set_title($title);
- $PAGE->set_heading($course->fullname);
-
- echo $OUTPUT->header();
-
-/// Print quiz name and description
- echo $OUTPUT->heading(format_string($quiz->name));
- if (trim(strip_tags($quiz->intro))) {
- echo $OUTPUT->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro');
- }
-
-/// Display information about this quiz.
- $messages = $accessmanager->describe_rules();
- if ($quiz->attempts != 1) {
- $messages[] = get_string('gradingmethod', 'quiz', quiz_get_grading_option_name($quiz->grademethod));
- }
- echo $OUTPUT->box_start('quizinfo');
- $accessmanager->print_messages($messages);
- echo $OUTPUT->box_end();
-
-/// Show number of attempts summary to those who can view reports.
- if (has_capability('mod/quiz:viewreports', $context)) {
- if ($strattemptnum = quiz_attempt_summary_link_to_reports($quiz, $cm, $context)) {
- echo '
' . $strattemptnum . "
\n";
- }
- }
-
-/// Guests can't do a quiz, so offer them a choice of logging in or going back.
- if (isguestuser()) {
- echo $OUTPUT->confirm('
' . get_string('guestsno', 'quiz') . "
\n\n
" .
- get_string('liketologin') . "
\n", get_login_url(), get_referer(false));
- echo $OUTPUT->footer();
- exit;
- }
-
-/// If they are not enrolled in this course in a good enough role, tell them to enrol.
- if (!($canattempt || $canpreview || $canreviewmine)) {
- echo $OUTPUT->box('
' . get_string('youneedtoenrol', 'quiz') . "
\n\n
" .
- $OUTPUT->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id) .
- "
\n", 'generalbox', 'notice');
- echo $OUTPUT->footer();
- exit;
- }
-
-/// Update the quiz with overrides for the current user
- $quiz = quiz_update_effective_access($quiz, $USER->id);
-
-/// Get this user's attempts.
- $attempts = quiz_get_user_attempts($quiz->id, $USER->id);
- $lastfinishedattempt = end($attempts);
- $unfinished = false;
- if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
- $attempts[] = $unfinishedattempt;
- $unfinished = true;
- }
- $numattempts = count($attempts);
-
-/// Work out the final grade, checking whether it was overridden in the gradebook.
- $mygrade = quiz_get_best_grade($quiz, $USER->id);
- $mygradeoverridden = false;
- $gradebookfeedback = '';
-
- $grading_info = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $USER->id);
- if (!empty($grading_info->items)) {
- $item = $grading_info->items[0];
- if (isset($item->grades[$USER->id])) {
- $grade = $item->grades[$USER->id];
-
- if ($grade->overridden) {
- $mygrade = $grade->grade + 0; // Convert to number.
- $mygradeoverridden = true;
- }
- if (!empty($grade->str_feedback)) {
- $gradebookfeedback = $grade->str_feedback;
- }
- }
- }
-
-/// Print table with existing attempts
- if ($attempts) {
-
- echo $OUTPUT->heading(get_string('summaryofattempts', 'quiz'));
-
- // Work out which columns we need, taking account what data is available in each attempt.
- list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts, $context);
-
- $attemptcolumn = $quiz->attempts != 1;
-
- $gradecolumn = $someoptions->scores && quiz_has_grades($quiz);
- $markcolumn = $gradecolumn && ($quiz->grade != $quiz->sumgrades);
- $overallstats = $alloptions->scores;
-
- $feedbackcolumn = quiz_has_feedback($quiz) && $alloptions->overallfeedback;
-
- // Prepare table header
- $table = new html_table();
- $table->attributes['class'] = 'generaltable quizattemptsummary';
- $table->head = array();
- $table->align = array();
- $table->size = array();
- if ($attemptcolumn) {
- $table->head[] = get_string('attemptnumber', 'quiz');
- $table->align[] = 'center';
- $table->size[] = '';
- }
- $table->head[] = get_string('timecompleted', 'quiz');
- $table->align[] = 'left';
- $table->size[] = '';
- if ($markcolumn) {
- $table->head[] = get_string('marks', 'quiz') . ' / ' . quiz_format_grade($quiz, $quiz->sumgrades);
- $table->align[] = 'center';
- $table->size[] = '';
- }
- if ($gradecolumn) {
- $table->head[] = get_string('grade') . ' / ' . quiz_format_grade($quiz, $quiz->grade);
- $table->align[] = 'center';
- $table->size[] = '';
- }
- if ($canreviewmine) {
- $table->head[] = get_string('review', 'quiz');
- $table->align[] = 'center';
- $table->size[] = '';
- }
- if ($feedbackcolumn) {
- $table->head[] = get_string('feedback', 'quiz');
- $table->align[] = 'left';
- $table->size[] = '';
- }
- if (isset($quiz->showtimetaken)) {
- $table->head[] = get_string('timetaken', 'quiz');
- $table->align[] = 'left';
- $table->size[] = '';
- }
-
- // One row for each attempt
- foreach ($attempts as $attempt) {
- $attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
- $row = array();
-
- // Add the attempt number, making it a link, if appropriate.
- if ($attemptcolumn) {
- if ($attempt->preview) {
- $row[] = get_string('preview', 'quiz');
- } else {
- $row[] = $attempt->attempt;
- }
- }
-
- // prepare strings for time taken and date completed
- $timetaken = '';
- $datecompleted = '';
- if ($attempt->timefinish > 0) {
- // attempt has finished
- $timetaken = format_time($attempt->timefinish - $attempt->timestart);
- $datecompleted = userdate($attempt->timefinish);
- } else if (!$quiz->timeclose || $timenow < $quiz->timeclose) {
- // The attempt is still in progress.
- $timetaken = format_time($timenow - $attempt->timestart);
- $datecompleted = '';
- } else {
- $timetaken = format_time($quiz->timeclose - $attempt->timestart);
- $datecompleted = userdate($quiz->timeclose);
- }
- $row[] = $datecompleted;
-
- if ($markcolumn) {
- if ($attemptoptions->scores && $attempt->timefinish > 0) {
- $row[] = quiz_format_grade($quiz, $attempt->sumgrades);
- } else {
- $row[] = '';
- }
- }
-
- // Ouside the if because we may be showing feedback but not grades.
- $attemptgrade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
-
- if ($gradecolumn) {
- if ($attemptoptions->scores && $attempt->timefinish > 0) {
- $formattedgrade = quiz_format_grade($quiz, $attemptgrade);
- // highlight the highest grade if appropriate
- if ($overallstats && !$attempt->preview && $numattempts > 1 && !is_null($mygrade) &&
- $attemptgrade == $mygrade && $quiz->grademethod == QUIZ_GRADEHIGHEST) {
- $table->rowclasses[$attempt->attempt] = 'bestrow';
- }
-
- $row[] = $formattedgrade;
- } else {
- $row[] = '';
- }
- }
-
- if ($canreviewmine) {
- $row[] = $accessmanager->make_review_link($attempt, $canpreview, $attemptoptions);
- }
-
- if ($feedbackcolumn && $attempt->timefinish > 0) {
- if ($attemptoptions->overallfeedback) {
- $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context, $cm);
- } else {
- $row[] = '';
- }
- }
-
- if (isset($quiz->showtimetaken)) {
- $row[] = $timetaken;
- }
-
- if ($attempt->preview) {
- $table->data['preview'] = $row;
- } else {
- $table->data[$attempt->attempt] = $row;
- }
- } // End of loop over attempts.
- echo html_writer::table($table);
- }
-
-/// Print information about the student's best score for this quiz if possible.
- $moreattempts = $unfinished || !$accessmanager->is_finished($numattempts, $lastfinishedattempt);
- if (!$moreattempts) {
- echo $OUTPUT->heading(get_string("nomoreattempts", "quiz"));
- }
-
- if ($numattempts && $gradecolumn && !is_null($mygrade)) {
- $resultinfo = '';
-
- if ($overallstats) {
- if ($moreattempts) {
- $a = new stdClass;
- $a->method = quiz_get_grading_option_name($quiz->grademethod);
- $a->mygrade = quiz_format_grade($quiz, $mygrade);
- $a->quizgrade = quiz_format_grade($quiz, $quiz->grade);
- $resultinfo .= $OUTPUT->heading(get_string('gradesofar', 'quiz', $a), 2, 'main');
- } else {
- $a = new stdClass;
- $a->grade = quiz_format_grade($quiz, $mygrade);
- $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
- $a = get_string('outofshort', 'quiz', $a);
- $resultinfo .= $OUTPUT->heading(get_string('yourfinalgradeis', 'quiz', $a), 2, 'main');
- }
- }
-
- if ($mygradeoverridden) {
- $resultinfo .= '
'.get_string('overriddennotice', 'grades')."
\n";
- }
- if ($gradebookfeedback) {
- $resultinfo .= $OUTPUT->heading(get_string('comment', 'quiz'), 3, 'main');
- $resultinfo .= '
'.$gradebookfeedback."
\n";
- }
- if ($feedbackcolumn) {
- $resultinfo .= $OUTPUT->heading(get_string('overallfeedback', 'quiz'), 3, 'main');
- $resultinfo .= '
'.quiz_feedback_for_grade($mygrade, $quiz, $context, $cm)."
\n";
- }
-
- if ($resultinfo) {
- echo $OUTPUT->box($resultinfo, 'generalbox', 'feedback');
- }
- }
-
-/// Determine if we should be showing a start/continue attempt button,
-/// or a button to go back to the course page.
- echo $OUTPUT->box_start('quizattempt');
- $buttontext = ''; // This will be set something if as start/continue attempt button should appear.
- if (!quiz_clean_layout($quiz->questions, true)) {
- echo $OUTPUT->heading(get_string("noquestions", "quiz"));
- } else {
- if ($unfinished) {
- if ($canpreview) {
- $buttontext = get_string('continuepreview', 'quiz');
- } else if ($canattempt) {
- $buttontext = get_string('continueattemptquiz', 'quiz');
- }
- } else {
- if ($canpreview) {
- $buttontext = get_string('previewquiznow', 'quiz');
- } else if ($canattempt) {
- $messages = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt);
- if ($messages) {
- $accessmanager->print_messages($messages);
- } else if ($numattempts == 0) {
- $buttontext = get_string('attemptquiznow', 'quiz');
- } else {
- $buttontext = get_string('reattemptquiz', 'quiz');
- }
- }
- }
-
- // If, so far, we think a button should be printed, so check if they will be allowed to access it.
- if ($buttontext) {
- if (!$moreattempts) {
- $buttontext = '';
- } else if ($canattempt && $messages = $accessmanager->prevent_access()) {
- $accessmanager->print_messages($messages);
+ if ($viewobj->canattempt) {
+ $preventmessages = $viewobj->accessmanager->prevent_new_attempt($viewobj->numattempts,
+ $viewobj->lastfinishedattempt);
+ if ($preventmessages) {
$buttontext = '';
+ } else if ($viewobj->numattempts == 0) {
+ $buttontext = get_string('attemptquiznow', 'quiz');
+ } else {
+ $buttontext = get_string('reattemptquiz', 'quiz');
}
+
+ } else if ($viewobj->canpreview) {
+ $buttontext = get_string('previewquiznow', 'quiz');
}
}
-/// Now actually print the appropriate button.
+ // If, so far, we think a button should be printed, so check if they will be
+ // allowed to access it.
if ($buttontext) {
- $accessmanager->print_start_attempt_button($canpreview, $buttontext, $unfinished);
- } else {
- echo $OUTPUT->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id);
+ if (!$viewobj->moreattempts) {
+ $buttontext = '';
+ } else if ($viewobj->canattempt
+ && $preventmessages = $viewobj->accessmanager->prevent_access()) {
+ $buttontext = '';
+ }
}
- echo $OUTPUT->box_end();
+}
- echo $OUTPUT->footer();
+echo $OUTPUT->header();
+
+// Guests can't do a quiz, so offer them a choice of logging in or going back.
+if (isguestuser()) {
+ echo $output->view_page_guest($course, $quiz, $cm, $context, $infomessages, $viewobj);
+} else if (!isguestuser() && !($viewobj->canattempt || $viewobj->canpreview
+ || $viewobj->canreviewmine)) {
+ // If they are not enrolled in this course in a good enough role, tell them to enrol.
+ echo $output->view_page_notenrolled($course, $quiz, $cm, $context, $infomessages, $viewobj);
+} else {
+ echo $output->view_page($course, $quiz, $cm, $context, $infomessages, $viewobj,
+ $buttontext, $preventmessages);
+}
+
+echo $OUTPUT->footer();
diff --git a/mod/wiki/comments.php b/mod/wiki/comments.php
index 8885b9d2ee7..a2699044e88 100644
--- a/mod/wiki/comments.php
+++ b/mod/wiki/comments.php
@@ -57,7 +57,7 @@ if (!$cm = get_coursemodule_from_instance('wiki', $wiki->id)) {
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
-require_course_login($course->id, true, $cm);
+require_login($course->id, true, $cm);
add_to_log($course->id, 'wiki', 'comments', 'comments.php?id=' . $cm->id, $wiki->id);
diff --git a/mod/wiki/db/access.php b/mod/wiki/db/access.php
index a69d8c32777..ead3849d9c0 100644
--- a/mod/wiki/db/access.php
+++ b/mod/wiki/db/access.php
@@ -38,7 +38,7 @@ $capabilities = array(
)
),
- 'mod/wiki:createpage' => array(
+ 'mod/wiki:createpage' => array(
'riskbitmask' => RISK_SPAM,
@@ -89,6 +89,17 @@ $capabilities = array(
)
),
+ 'mod/wiki:managefiles' => array(
+
+ 'captype' => 'write',
+ 'contextlevel' => CONTEXT_MODULE,
+ 'archetypes' => array(
+ 'teacher' => CAP_ALLOW,
+ 'editingteacher' => CAP_ALLOW,
+ 'manager' => CAP_ALLOW
+ )
+ ),
+
'mod/wiki:overridelock' => array(
'captype' => 'write',
diff --git a/mod/wiki/db/upgrade.php b/mod/wiki/db/upgrade.php
index 9273189f2a5..f56865cebdf 100644
--- a/mod/wiki/db/upgrade.php
+++ b/mod/wiki/db/upgrade.php
@@ -357,7 +357,7 @@ function xmldb_wiki_upgrade($oldversion) {
// TODO: Will hold the old tables so we will have chance to fix problems
// Will remove old tables once migrating 100% stable
// Step 10: delete old tables
- //if ($oldversion < 2011000000) {
+ //if ($oldversion < 2011060300) {
//$tables = array('wiki_pages', 'wiki_locks', 'wiki_entries');
//foreach ($tables as $tablename) {
@@ -367,7 +367,7 @@ function xmldb_wiki_upgrade($oldversion) {
//}
//}
//echo $OUTPUT->notification('Droping old tables', 'notifysuccess');
- //upgrade_mod_savepoint(true, 2011000000, 'wiki');
+ //upgrade_mod_savepoint(true, 2011060300, 'wiki');
//}
return true;
diff --git a/mod/wiki/diff.php b/mod/wiki/diff.php
index 85c105e454f..cf620960c65 100644
--- a/mod/wiki/diff.php
+++ b/mod/wiki/diff.php
@@ -66,7 +66,7 @@ if ($compare >= $comparewith) {
print_error("A page version can only be compared with an older version.");
}
-require_course_login($course->id, true, $cm);
+require_login($course->id, true, $cm);
add_to_log($course->id, "wiki", "diff", "diff.php?id=$cm->id", "$wiki->id");
$wikipage = new page_wiki_diff($wiki, $subwiki, $cm);
diff --git a/mod/wiki/edit_form.php b/mod/wiki/edit_form.php
index 17f6d14e3d5..8a73529fa0c 100644
--- a/mod/wiki/edit_form.php
+++ b/mod/wiki/edit_form.php
@@ -31,8 +31,7 @@ if (!defined('MOODLE_INTERNAL')) {
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
}
-require_once($CFG->dirroot . "/mod/wiki/editors/wikieditor.php");
-require_once($CFG->dirroot . "/mod/wiki/editors/wikifiletable.php");
+require_once($CFG->dirroot . '/mod/wiki/editors/wikieditor.php');
class mod_wiki_edit_form extends moodleform {
@@ -42,13 +41,12 @@ class mod_wiki_edit_form extends moodleform {
$mform =& $this->_form;
$version = $this->_customdata['version'];
- $format = $this->_customdata['format'];
- $tags = !isset($this->_customdata['tags'])?"":$this->_customdata['tags'];
-
+ $format = $this->_customdata['format'];
+ $tags = !isset($this->_customdata['tags'])?"":$this->_customdata['tags'];
if ($format != 'html') {
- $contextid = $this->_customdata['contextid'];
- $filearea = $this->_customdata['filearea'];
+ $contextid = $this->_customdata['contextid'];
+ $filearea = $this->_customdata['filearea'];
$fileitemid = $this->_customdata['fileitemid'];
}
@@ -63,12 +61,25 @@ class mod_wiki_edit_form extends moodleform {
$fieldname = get_string('format' . $format, 'wiki');
if ($format != 'html') {
- $mform->addElement('wikieditor', 'newcontent', $fieldname, array('cols' => 50, 'rows' => 20, 'wiki_format' => $format));
+ // Use wiki editor
+ $ft = new filetype_parser;
+ $extensions = $ft->get_extensions('image');
+ $fs = get_file_storage();
+ $tree = $fs->get_area_tree($contextid, 'mod_wiki', 'attachments', $fileitemid);
+ $files = array();
+ foreach ($tree['files'] as $file) {
+ $filename = $file->get_filename();
+ foreach ($extensions as $ext) {
+ if (preg_match('#'.$ext.'$#', $filename)) {
+ $files[] = $filename;
+ }
+ }
+ }
+ $mform->addElement('wikieditor', 'newcontent', $fieldname, array('cols' => 100, 'rows' => 20, 'wiki_format' => $format, 'files'=>$files));
$mform->addHelpButton('newcontent', 'format'.$format, 'wiki');
} else {
$mform->addElement('editor', 'newcontent_editor', $fieldname, null, page_wiki_edit::$attachmentoptions);
$mform->addHelpButton('newcontent_editor', 'formathtml', 'wiki');
-
}
//hiddens
@@ -80,21 +91,6 @@ class mod_wiki_edit_form extends moodleform {
$mform->addElement('hidden', 'contentformat');
$mform->setDefault('contentformat', $format);
-// if ($format != 'html') {
-// //uploads
-// $mform->addElement('header', 'attachments_tags', get_string('attachments', 'wiki'));
-// $mform->addElement('filemanager', 'attachments', get_string('attachments', 'wiki'), null, page_wiki_edit::$attachmentoptions);
-// $fileinfo = array(
-// 'contextid'=>$contextid,
-// 'component'=>'mod_wiki',
-// 'filearea'=>$filearea,
-// 'itemid'=>$fileitemid,
-// );
-//
-// $mform->addElement('wikifiletable', 'deleteuploads', get_string('wikifiletable', 'wiki'), null, $fileinfo, $format);
-// $mform->addElement('submit', 'editoption', get_string('upload', 'wiki'), array('id' => 'tags'));
-// }
-
if (!empty($CFG->usetags)) {
$mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
$mform->addElement('tags', 'tags', get_string('tags'));
diff --git a/mod/wiki/editors/wiki/buttons.js b/mod/wiki/editors/wiki/buttons.js
index 233aa707a57..52cd0008de8 100644
--- a/mod/wiki/editors/wiki/buttons.js
+++ b/mod/wiki/editors/wiki/buttons.js
@@ -17,8 +17,8 @@ if (clientPC.indexOf('opera')!=-1) {
// copied and adapted from phpBB
function insertTags(tagOpen, tagClose, sampleText) {
- tagOpen = unescape(tagOpen);
- tagClose = unescape(tagClose);
+ tagOpen = unescape(tagOpen);
+ tagClose = unescape(tagClose);
var txtarea = document.forms['mform1'].newcontent;
@@ -79,4 +79,4 @@ function insertTags(tagOpen, tagClose, sampleText) {
}
// reposition cursor if possible
if (txtarea.createTextRange) txtarea.caretPos = document.selection.createRange().duplicate();
-}
\ No newline at end of file
+}
diff --git a/mod/wiki/editors/wikieditor.php b/mod/wiki/editors/wikieditor.php
index f8389cc9c9d..e8804936079 100644
--- a/mod/wiki/editors/wikieditor.php
+++ b/mod/wiki/editors/wikieditor.php
@@ -31,11 +31,17 @@ require_once($CFG->dirroot.'/lib/form/textarea.php');
class MoodleQuickForm_wikieditor extends MoodleQuickForm_textarea {
+ private $files;
+
function MoodleQuickForm_wikieditor($elementName = null, $elementLabel = null, $attributes = null) {
if (isset($attributes['wiki_format'])) {
$this->wikiformat = $attributes['wiki_format'];
unset($attributes['wiki_format']);
}
+ if (isset($attributes['files'])) {
+ $this->files = $attributes['files'];
+ unset($attributes['files']);
+ }
parent::MoodleQuickForm_textarea($elementName, $elementLabel, $attributes);
}
@@ -71,7 +77,7 @@ class MoodleQuickForm_wikieditor extends MoodleQuickForm_textarea {
}
private function getButtons() {
- global $PAGE, $CFG;
+ global $PAGE, $OUTPUT, $CFG;
$editor = $this->wikiformat;
@@ -81,6 +87,9 @@ class MoodleQuickForm_wikieditor extends MoodleQuickForm_textarea {
$tag = $this->getTokens($editor, 'italic');
$wiki_editor['italic'] = array('ed_italic.gif', get_string('wikiitalictext', 'wiki'), $tag[0], $tag[1], get_string('wikiitalictext', 'wiki'));
+ $imagetag = $this->getTokens($editor, 'image');
+ $wiki_editor['image'] = array('ed_img.gif', get_string('wikiimage', 'wiki'), $imagetag[0], $imagetag[1], get_string('wikiimage', 'wiki'));
+
$tag = $this->getTokens($editor, 'link');
$wiki_editor['internal'] = array('ed_internal.gif', get_string('wikiinternalurl', 'wiki'), $tag[0], $tag[1], get_string('wikiinternalurl', 'wiki'));
@@ -91,9 +100,6 @@ class MoodleQuickForm_wikieditor extends MoodleQuickForm_textarea {
$wiki_editor['u_list'] = array('ed_ul.gif', get_string('wikiunorderedlist', 'wiki'), '\\n'.$tag[0], '', '');
$wiki_editor['o_list'] = array('ed_ol.gif', get_string('wikiorderedlist', 'wiki'), '\\n'.$tag[1], '', '');
- $tag = $this->getTokens($editor, 'image');
- $wiki_editor['image'] = array('ed_img.gif', get_string('wikiimage', 'wiki'), $tag[0], $tag[1], get_string('wikiimage', 'wiki'));
-
$tag = $this->getTokens($editor, 'header');
$wiki_editor['h1'] = array('ed_h1.gif', get_string('wikiheader', 'wiki', 1), '\\n'.$tag.' ', ' '.$tag.'\\n', get_string('wikiheader', 'wiki', 1));
$wiki_editor['h2'] = array('ed_h2.gif', get_string('wikiheader', 'wiki', 2), '\\n'.$tag.$tag.' ', ' '.$tag.$tag.'\\n', get_string('wikiheader', 'wiki', 2));
@@ -107,13 +113,23 @@ class MoodleQuickForm_wikieditor extends MoodleQuickForm_textarea {
$PAGE->requires->js('/mod/wiki/editors/wiki/buttons.js');
- $html = "";
+ $html = '
';
return $html;
}
diff --git a/mod/wiki/files.php b/mod/wiki/files.php
new file mode 100644
index 00000000000..377ed741afb
--- /dev/null
+++ b/mod/wiki/files.php
@@ -0,0 +1,107 @@
+.
+
+/**
+ * Wiki files management
+ *
+ * @package mod-wiki-2.0
+ * @copyrigth 2011 Dongsheng Cai
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../../config.php');
+require_once($CFG->dirroot . '/mod/wiki/lib.php');
+require_once($CFG->dirroot . '/mod/wiki/locallib.php');
+
+$pageid = required_param('pageid', PARAM_INT); // Page ID
+$wid = optional_param('wid', 0, PARAM_INT); // Wiki ID
+$currentgroup = optional_param('group', 0, PARAM_INT); // Group ID
+$userid = optional_param('uid', 0, PARAM_INT); // User ID
+$groupanduser = optional_param('groupanduser', null, PARAM_TEXT);
+
+if (!$page = wiki_get_page($pageid)) {
+ print_error('incorrectpageid', 'wiki');
+}
+
+if ($groupanduser) {
+ list($currentgroup, $userid) = explode('-', $groupanduser);
+ $currentgroup = clean_param($currentgroup, PARAM_INT);
+ $userid = clean_param($userid, PARAM_INT);
+}
+
+if ($wid) {
+ // in group mode
+ if (!$wiki = wiki_get_wiki($wid)) {
+ print_error('incorrectwikiid', 'wiki');
+ }
+ if (!$subwiki = wiki_get_subwiki_by_group($wiki->id, $currentgroup, $userid)) {
+ // create subwiki if doesn't exist
+ $subwikiid = wiki_add_subwiki($wiki->id, $currentgroup, $userid);
+ $subwiki = wiki_get_subwiki($subwikiid);
+ }
+} else {
+ // no group
+ if (!$subwiki = wiki_get_subwiki($page->subwikiid)) {
+ print_error('incorrectsubwikiid', 'wiki');
+ }
+
+ // Checking wiki instance of that subwiki
+ if (!$wiki = wiki_get_wiki($subwiki->wikiid)) {
+ print_error('incorrectwikiid', 'wiki');
+ }
+}
+
+// Checking course module instance
+if (!$cm = get_coursemodule_from_instance("wiki", $subwiki->wikiid)) {
+ print_error('invalidcoursemodule');
+}
+
+// Checking course instance
+$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
+
+$context = get_context_instance(CONTEXT_MODULE, $cm->id);
+
+
+$PAGE->set_url('/mod/wiki/files.php', array('pageid'=>$pageid));
+require_login($course, true, $cm);
+$PAGE->set_context($context);
+$PAGE->set_title(get_string('wikifiles', 'wiki'));
+$PAGE->set_heading(get_string('wikifiles', 'wiki'));
+$PAGE->navbar->add(format_string(get_string('wikifiles', 'wiki')));
+echo $OUTPUT->header();
+
+$renderer = $PAGE->get_renderer('mod_wiki');
+
+$tabitems = array('view' => 'view', 'edit' => 'edit', 'comments' => 'comments', 'history' => 'history', 'map' => 'map', 'files' => 'files');
+
+$options = array('activetab'=>'files');
+echo $renderer->tabs($page, $tabitems, $options);
+
+
+echo $OUTPUT->box_start('generalbox');
+if (has_capability('mod/wiki:viewpage', $context)) {
+ echo $renderer->wiki_print_subwiki_selector($PAGE->activityrecord, $subwiki, $page, 'files');
+ echo $renderer->wiki_files_tree($context, $subwiki);
+} else {
+ echo $OUTPUT->notification(get_string('cannotviewfiles', 'wiki'));
+}
+echo $OUTPUT->box_end();
+
+if (has_capability('mod/wiki:managefiles', $context)) {
+ echo $OUTPUT->single_button(new moodle_url('/mod/wiki/filesedit.php', array('subwiki'=>$subwiki->id, 'pageid'=>$pageid)), get_string('editfiles', 'wiki'), 'get');
+}
+echo $OUTPUT->footer();
diff --git a/mod/wiki/filesedit.php b/mod/wiki/filesedit.php
new file mode 100644
index 00000000000..0982095e5ea
--- /dev/null
+++ b/mod/wiki/filesedit.php
@@ -0,0 +1,97 @@
+.
+
+/**
+ * Manage files in wiki
+ *
+ * @package mod-wiki-2.0
+ * @copyright 2011 Dongsheng Cai
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
+require_once('lib.php');
+require_once('locallib.php');
+require_once("$CFG->dirroot/mod/wiki/filesedit_form.php");
+require_once("$CFG->dirroot/repository/lib.php");
+
+$subwikiid = required_param('subwiki', PARAM_INT);
+// not being used for file management, we use it to generate navbar link
+$pageid = optional_param('pageid', 0, PARAM_INT);
+$returnurl = optional_param('returnurl', '', PARAM_URL);
+
+if (!$subwiki = wiki_get_subwiki($subwikiid)) {
+ print_error('incorrectsubwikiid', 'wiki');
+}
+
+// Checking wiki instance of that subwiki
+if (!$wiki = wiki_get_wiki($subwiki->wikiid)) {
+ print_error('incorrectwikiid', 'wiki');
+}
+
+// Checking course module instance
+if (!$cm = get_coursemodule_from_instance("wiki", $subwiki->wikiid)) {
+ print_error('invalidcoursemodule');
+}
+
+// Checking course instance
+$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
+
+$context = get_context_instance(CONTEXT_MODULE, $cm->id);
+
+require_login($course, true, $cm);
+require_capability('mod/wiki:managefiles', $context);
+
+if (empty($returnurl)) {
+ if (!empty($_SERVER["HTTP_REFERER"])) {
+ $returnurl = $_SERVER["HTTP_REFERER"];
+ } else {
+ $returnurl = new moodle_url('/mod/wiki/files.php', array('subwiki'=>$subwiki->id));
+ }
+}
+
+$title = get_string('editfiles', 'wiki');
+
+$struser = get_string('user');
+$url = new moodle_url('/mod/wiki/filesedit.php', array('subwiki'=>$subwiki->id, 'pageid'=>$pageid));
+$PAGE->set_url($url);
+$PAGE->set_context($context);
+$PAGE->set_title($title);
+$PAGE->set_heading($title);
+$PAGE->navbar->add(format_string(get_string('wikifiles', 'wiki')), $CFG->wwwroot . '/mod/wiki/files.php?pageid=' . $pageid);
+$PAGE->navbar->add(format_string($title));
+
+$data = new stdClass();
+$data->returnurl = $returnurl;
+$data->subwikiid = $subwiki->id;
+$maxbytes = get_max_upload_file_size($CFG->maxbytes, $COURSE->maxbytes);
+$options = array('subdirs'=>0, 'maxbytes'=>$maxbytes, 'maxfiles'=>-1, 'accepted_types'=>'*', 'return_types'=>FILE_INTERNAL);
+file_prepare_standard_filemanager($data, 'files', $options, $context, 'mod_wiki', 'attachments', $subwiki->id);
+
+$mform = new mod_wiki_filesedit_form(null, array('data'=>$data, 'options'=>$options));
+
+if ($mform->is_cancelled()) {
+ redirect($returnurl);
+} else if ($formdata = $mform->get_data()) {
+ $formdata = file_postupdate_standard_filemanager($formdata, 'files', $options, $context, 'mod_wiki', 'attachments', $subwiki->id);
+ redirect($returnurl);
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->box_start('generalbox');
+$mform->display();
+echo $OUTPUT->box_end();
+echo $OUTPUT->footer();
diff --git a/mod/wiki/filesedit_form.php b/mod/wiki/filesedit_form.php
new file mode 100644
index 00000000000..f086611a7e7
--- /dev/null
+++ b/mod/wiki/filesedit_form.php
@@ -0,0 +1,44 @@
+.
+
+/**
+ * Edit wiki files form
+ *
+ * @package mod-wiki-2.0
+ * @copyright 2011 Dongsheng Cai
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->libdir/formslib.php");
+
+class mod_wiki_filesedit_form extends moodleform {
+ function definition() {
+ $mform = $this->_form;
+
+ $data = $this->_customdata['data'];
+ $options = $this->_customdata['options'];
+
+ $mform->addElement('filemanager', 'files_filemanager', get_string('files'), null, $options);
+ $mform->addElement('hidden', 'returnurl', $data->returnurl);
+ $mform->addElement('hidden', 'subwiki', $data->subwikiid);
+
+ $this->add_action_buttons(true, get_string('savechanges'));
+
+ $this->set_data($data);
+ }
+}
diff --git a/mod/wiki/history.php b/mod/wiki/history.php
index 94dde94cf27..3b8a344c396 100644
--- a/mod/wiki/history.php
+++ b/mod/wiki/history.php
@@ -58,7 +58,9 @@ if (!$cm = get_coursemodule_from_instance('wiki', $wiki->id)) {
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
-require_course_login($course->id, true, $cm);
+require_login($course->id, true, $cm);
+$context = get_context_instance(CONTEXT_MODULE, $cm->id);
+require_capability('mod/wiki:viewpage', $context);
add_to_log($course->id, 'wiki', 'history', 'history.php?id=' . $cm->id, $wiki->id);
/// Print the page header
diff --git a/mod/wiki/index.php b/mod/wiki/index.php
index e872883d737..d1db74a22d9 100644
--- a/mod/wiki/index.php
+++ b/mod/wiki/index.php
@@ -41,7 +41,7 @@ if (!$course = $DB->get_record('course', array('id' => $id))) {
print_error('invalidcourseid');
}
-require_course_login($course->id, true);
+require_login($course->id, true);
$PAGE->set_pagelayout('incourse');
$context = get_context_instance(CONTEXT_COURSE, $course->id);
diff --git a/mod/wiki/lang/en/wiki.php b/mod/wiki/lang/en/wiki.php
index 5a2069cae2b..133a2fde92c 100644
--- a/mod/wiki/lang/en/wiki.php
+++ b/mod/wiki/lang/en/wiki.php
@@ -19,7 +19,9 @@ $string['backhistory'] = 'Back to history';
$string['backoldversion'] = 'Back to old version';
$string['backpage'] = 'Back to page';
$string['backtomapmenu'] = 'Back to map menu';
-$string['changerate']='Do you wish to change it?';
+$string['changerate'] = 'Do you wish to change it?';
+$string['cannotmanagefiles'] = 'You don\'t have permission to manage the wiki files.';
+$string['cannotviewfiles'] = 'You don\'t have permission to view the wiki files.';
$string['comparesel'] = 'Compare selected';
$string['comments'] = 'Comments';
$string['commentscount'] = 'Comments ({$a})';
@@ -48,9 +50,11 @@ $string['diff_help'] = 'Selected versions of the page may be compared in order t
$string['edit'] = 'Edit';
$string['editcomment'] = 'Edit comment';
$string['editblocks'] = 'Turn edit blocks on';
+$string['editfiles'] = 'Edit wiki files';
$string['editing'] = 'Editing wiki page';
$string['editingcomment'] = 'Editing comment';
$string['editingpage'] = 'Editing this page \'{$a}\'';
+$string['files'] = 'Files';
$string['filenotuploadederror'] = 'File \'{$a}\' could not be uploaded correctly.';
$string['filtername'] = 'Wiki auto-linking';
$string['firstpagetitle'] = 'First page name';
@@ -79,6 +83,8 @@ $string['history'] = 'History';
$string['history_help'] = 'The history lists links to previous versions of the page.';
$string['html'] = 'HTML';
$string['insertcomment'] = 'Insert comment';
+$string['insertimage'] = 'Insert an image...';
+$string['insertimage_help'] = 'This drop-down list will insert an image to the wiki editor. If you need to add more images to the wiki, please use "Files" tab.';
$string['invalidlock'] = 'This page is already locked by another user.';
$string['invalidparameters'] = 'Invalid parameters have been given.';
$string['invalidsesskey'] = 'The given sesskey is not valid. Please resend data again';
@@ -187,6 +193,7 @@ $string['wikiattachments'] = 'Wiki attachments';
$string['wikiboldtext'] = 'Bold text';
$string['wikiexternalurl'] = 'External URL';
$string['wikifiletable'] = 'Uploaded file list';
+$string['wikifiles'] = 'Wiki files';
$string['wikiheader'] = 'Level {$a} Header';
$string['wikihr'] = 'Horizontal rule';
$string['wikiimage'] = 'Image';
@@ -208,6 +215,7 @@ $string['wiki:editcomment'] = 'Add comments to pages';
$string['wiki:editpage'] = 'Save wiki pages';
$string['wiki:managecomment'] = 'Manage wiki comments';
$string['wiki:managewiki'] = 'Manage wiki settings';
+$string['wiki:managefiles'] = 'Manage wiki files';
$string['wiki:overridelock'] = 'Override wiki locks';
$string['wiki:viewcomment'] = 'View page comments';
$string['wiki:viewpage'] = 'View wiki pages';
diff --git a/mod/wiki/lib.php b/mod/wiki/lib.php
index 230e7bd7299..2141261e927 100644
--- a/mod/wiki/lib.php
+++ b/mod/wiki/lib.php
@@ -445,7 +445,7 @@ function wiki_pluginfile($course, $cm, $context, $filearea, $args, $forcedownloa
return false;
}
- require_course_login($course, true, $cm);
+ require_login($course, true, $cm);
require_once($CFG->dirroot . "/mod/wiki/locallib.php");
@@ -495,6 +495,7 @@ function wiki_extend_navigation(navigation_node $navref, $course, $module, $cm)
require_once($CFG->dirroot . '/mod/wiki/locallib.php');
+ $context = get_context_instance(CONTEXT_MODULE, $cm->id);
$url = $PAGE->url;
$userid = 0;
if ($module->wikimode == 'individual') {
@@ -521,25 +522,43 @@ function wiki_extend_navigation(navigation_node $navref, $course, $module, $cm)
$page = wiki_get_page_by_title($swid, $wiki->firstpagetitle);
$pageid = $page->id;
}
- $link = new moodle_url('/mod/wiki/create.php', array('action' => 'new', 'swid' => $swid));
- $node = $navref->add(get_string('newpage', 'wiki'), $link, navigation_node::TYPE_SETTING);
+
+ if (has_capability('mod/wiki:createpage', $context)) {
+ $link = new moodle_url('/mod/wiki/create.php', array('action' => 'new', 'swid' => $swid));
+ $node = $navref->add(get_string('newpage', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ }
if (is_numeric($pageid)) {
- $link = new moodle_url('/mod/wiki/view.php', array('pageid' => $pageid));
- $node = $navref->add(get_string('view', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ if (has_capability('mod/wiki:viewpage', $context)) {
+ $link = new moodle_url('/mod/wiki/view.php', array('pageid' => $pageid));
+ $node = $navref->add(get_string('view', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ }
- $link = new moodle_url('/mod/wiki/edit.php', array('pageid' => $pageid));
- $node = $navref->add(get_string('edit', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ if (has_capability('mod/wiki:editpage', $context)) {
+ $link = new moodle_url('/mod/wiki/edit.php', array('pageid' => $pageid));
+ $node = $navref->add(get_string('edit', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ }
- $link = new moodle_url('/mod/wiki/comments.php', array('pageid' => $pageid));
- $node = $navref->add(get_string('comments', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ if (has_capability('mod/wiki:viewcomment', $context)) {
+ $link = new moodle_url('/mod/wiki/comments.php', array('pageid' => $pageid));
+ $node = $navref->add(get_string('comments', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ }
- $link = new moodle_url('/mod/wiki/history.php', array('pageid' => $pageid));
- $node = $navref->add(get_string('history', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ if (has_capability('mod/wiki:viewpage', $context)) {
+ $link = new moodle_url('/mod/wiki/history.php', array('pageid' => $pageid));
+ $node = $navref->add(get_string('history', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ }
- $link = new moodle_url('/mod/wiki/map.php', array('pageid' => $pageid));
- $node = $navref->add(get_string('map', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ if (has_capability('mod/wiki:viewpage', $context)) {
+ $link = new moodle_url('/mod/wiki/map.php', array('pageid' => $pageid));
+ $node = $navref->add(get_string('map', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ }
+
+ if (has_capability('mod/wiki:viewpage', $context)) {
+ $link = new moodle_url('/mod/wiki/files.php', array('pageid' => $pageid));
+ $node = $navref->add(get_string('files', 'wiki'), $link, navigation_node::TYPE_SETTING);
+ }
}
}
/**
diff --git a/mod/wiki/locallib.php b/mod/wiki/locallib.php
index 91feed29b3a..6bd876842e2 100644
--- a/mod/wiki/locallib.php
+++ b/mod/wiki/locallib.php
@@ -578,7 +578,22 @@ function wiki_parse_content($markup, $pagecontent, $options = array()) {
$cm = get_coursemodule_from_instance("wiki", $subwiki->wikiid);
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
- $parser_options = array('link_callback' => '/mod/wiki/locallib.php:wiki_parser_link', 'link_callback_args' => array('swid' => $options['swid']), 'table_callback' => '/mod/wiki/locallib.php:wiki_parser_table', 'real_path_callback' => '/mod/wiki/locallib.php:wiki_parser_real_path', 'real_path_callback_args' => array('context' => $context, 'component' => 'mod_wiki', 'filearea' => 'attachments', 'pageid' => $options['pageid']), 'pageid' => $options['pageid'], 'pretty_print' => (isset($options['pretty_print']) && $options['pretty_print']), 'printable' => (isset($options['printable']) && $options['printable']));
+ $parser_options = array(
+ 'link_callback' => '/mod/wiki/locallib.php:wiki_parser_link',
+ 'link_callback_args' => array('swid' => $options['swid']),
+ 'table_callback' => '/mod/wiki/locallib.php:wiki_parser_table',
+ 'real_path_callback' => '/mod/wiki/locallib.php:wiki_parser_real_path',
+ 'real_path_callback_args' => array(
+ 'context' => $context,
+ 'component' => 'mod_wiki',
+ 'filearea' => 'attachments',
+ 'subwikiid'=> $subwiki->id,
+ 'pageid' => $options['pageid']
+ ),
+ 'pageid' => $options['pageid'],
+ 'pretty_print' => (isset($options['pretty_print']) && $options['pretty_print']),
+ 'printable' => (isset($options['printable']) && $options['printable'])
+ );
return wiki_parser_proxy::parse($pagecontent, $markup, $parser_options);
}
@@ -661,21 +676,29 @@ function wiki_parser_table($table) {
/**
* Returns an absolute path link, unless there is no such link.
*
- * @param string url Link's URL
- * @param stdClass context filearea params
- * @param string filearea
- * @param int fileareaid
+ * @param string $url Link's URL or filename
+ * @param stdClass $context filearea params
+ * @param string $component The component the file is associated with
+ * @param string $filearea The filearea the file is stored in
+ * @param int $swid Sub wiki id
*
- * @return File full path
+ * @return string URL for files full path
*/
-function wiki_parser_real_path($url, $context, $filearea, $fileareaid) {
+function wiki_parser_real_path($url, $context, $component, $filearea, $swid) {
global $CFG;
if (preg_match("/^(?:http|ftp)s?\:\/\//", $url)) {
return $url;
} else {
- return "{$CFG->wwwroot}/pluginfile.php/{$context->id}/$filearea/$fileareaid/$url";
+
+ $file = 'pluginfile.php';
+ if (!$CFG->slasharguments) {
+ $file = $file . '?file=';
+ }
+ $baseurl = "$CFG->wwwroot/$file/{$context->id}/$component/$filearea/$swid/";
+ // it is a file in current file area
+ return $baseurl . $url;
}
}
@@ -995,85 +1018,6 @@ function wiki_delete_old_locks() {
$DB->delete_records_select('wiki_locks', "lockedat < ?", array(time() - 3600));
}
-/**
- * File processing
- */
-
-/**
- * Uploads files to permanent disk space.
- *
- * @param int draftitemid Draft space ID
- * @param int contextid
- *
- * @return array of files that have not been inserted.
- */
-
-function wiki_process_attachments($draftitemid, $deleteuploads, $contextid, $filearea, $itemid, $options = null) {
- global $CFG, $USER;
-
- if (empty($options)) {
- $options = page_wiki_edit::$attachmentoptions;
- }
-
- $errors = array();
-
- $usercontext = get_context_instance(CONTEXT_USER, $USER->id);
- $fs = get_file_storage();
-
- $oldfiles = $fs->get_area_files($contextid, 'mod_wiki', 'attachments', $itemid, 'id');
-
- foreach ($oldfiles as $file) {
- if (in_array($file->get_pathnamehash(), $deleteuploads)) {
- $file->delete();
- }
- }
-
- $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');
- $oldfiles = $fs->get_area_files($contextid, 'mod_wiki', 'attachments', $itemid, 'id');
-
- $file_record = array('contextid' => $contextid, 'component' => 'mod_wiki', 'filearea' => 'attachments', 'itemid' => $itemid);
- //more or less a merge...
- $newhashes = array();
- foreach ($draftfiles as $file) {
- $newhash = sha1("/$contextid/mod_wiki/attachments/$itemid" . $file->get_filepath() . $file->get_filename());
- $newhashes[$newhash] = $file;
- }
-
- $filecount = 0;
- foreach ($oldfiles as $file) {
- $oldhash = $file->get_pathnamehash();
- if (!$file->is_directory() && isset($newhashes[$oldhash])) {
- //repeated file: ERROR!!!
- unset($newhashes[$oldhash]);
- $errors[] = $file;
- }
-
- if (!$file->is_directory()) {
- $filecount++;
- }
- }
-
- foreach ($newhashes as $file) {
- if ($file->get_filepath() !== '/' or $file->is_directory()) {
- continue;
- }
-
- if ($options['maxfiles'] and $options['maxfiles'] <= $filecount) {
- break;
- }
-
- if (!$file->is_directory()) {
- $filecount++;
- $fs->create_file_from_storedfile($file_record, $file);
- }
- }
-
- //delete all draft files
- $fs->delete_area_files($usercontext->id, 'user', 'draft', $draftitemid);
-
- return $errors;
-}
-
function wiki_get_comment($commentid){
global $DB;
return $DB->get_record('comments', array('id' => $commentid));
diff --git a/mod/wiki/lock.php b/mod/wiki/lock.php
index fd2d99bc455..562aabfcf01 100644
--- a/mod/wiki/lock.php
+++ b/mod/wiki/lock.php
@@ -66,7 +66,7 @@ if (!empty($section) && !$sectioncontent = wiki_get_section_page($page, $section
print_error('invalidsection', 'wiki');
}
-require_course_login($course->id, false, $cm);
+require_login($course->id, false, $cm);
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
require_capability('mod/wiki:editpage', $context);
diff --git a/mod/wiki/map.php b/mod/wiki/map.php
index ca2a7e7de18..341667bfcf6 100644
--- a/mod/wiki/map.php
+++ b/mod/wiki/map.php
@@ -53,8 +53,9 @@ if (!$wiki = wiki_get_wiki($subwiki->wikiid)) {
print_error('incorrectwikiid', 'wiki');
}
-require_course_login($course->id, true, $cm);
-
+require_login($course->id, true, $cm);
+$context = get_context_instance(CONTEXT_MODULE, $cm->id);
+require_capability('mod/wiki:viewpage', $context);
add_to_log($course->id, "wiki", "map", "map.php?id=$cm->id", "$wiki->id");
/// Print page header
diff --git a/mod/wiki/module.js b/mod/wiki/module.js
index 3e597c3f2f7..963f3b5f2aa 100644
--- a/mod/wiki/module.js
+++ b/mod/wiki/module.js
@@ -76,3 +76,20 @@ M.mod_wiki.history = function(Y, args) {
}
}
}
+
+M.mod_wiki.init_tree = function(Y, expand_all, htmlid) {
+ Y.use('yui2-treeview', function(Y) {
+ var tree = new YAHOO.widget.TreeView(htmlid);
+
+ tree.subscribe("clickEvent", function(node, event) {
+ // we want normal clicking which redirects to url
+ return false;
+ });
+
+ if (expand_all) {
+ tree.expandAll();
+ }
+
+ tree.render();
+ });
+};
diff --git a/mod/wiki/pagelib.php b/mod/wiki/pagelib.php
index 2d1597c05a9..8e9e9012bab 100644
--- a/mod/wiki/pagelib.php
+++ b/mod/wiki/pagelib.php
@@ -39,7 +39,6 @@ require_once($CFG->dirroot . '/tag/lib.php');
/**
* Class page_wiki contains the common code between all pages
*
- * @package mod-wiki
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class page_wiki {
@@ -77,7 +76,7 @@ abstract class page_wiki {
* @var array The tabs set used in wiki module
*/
protected $tabs = array('view' => 'view', 'edit' => 'edit', 'comments' => 'comments',
- 'history' => 'history', 'map' => 'map');
+ 'history' => 'history', 'map' => 'map', 'files' => 'files');
/**
* @var array tabs options
*/
@@ -269,7 +268,6 @@ abstract class page_wiki {
/**
* View a wiki page
*
- * @package mod-wiki
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class page_wiki_view extends page_wiki {
@@ -283,7 +281,7 @@ class page_wiki_view extends page_wiki {
parent::print_header();
- $this->wikioutput->wiki_print_subwiki_selector($PAGE->activityrecord, $this->subwiki, $this->page);
+ $this->wikioutput->wiki_print_subwiki_selector($PAGE->activityrecord, $this->subwiki, $this->page, 'view');
if (!empty($this->page)) {
echo $this->wikioutput->prettyview_link($this->page);
@@ -352,7 +350,6 @@ class page_wiki_view extends page_wiki {
/**
* Wiki page editing page
*
- * @package mod-wiki
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class page_wiki_edit extends page_wiki {
@@ -541,16 +538,14 @@ class page_wiki_edit extends page_wiki {
$data = file_prepare_standard_editor($data, 'newcontent', page_wiki_edit::$attachmentoptions, $this->modcontext, 'mod_wiki', 'attachments', $this->subwiki->id);
break;
default:
- //$draftitemid = file_get_submitted_draft_itemid('attachments');
- //file_prepare_draft_area($draftitemid, $this->modcontext->id, 'mod_wiki', 'attachments', $this->subwiki->id);
- //$data->attachments = $draftitemid;
+ break;
}
if ($version->contentformat != 'html') {
- $params['contextid'] = $this->modcontext->id;
- $params['component'] = 'mod_wiki';
- $params['filearea'] = 'attachments';
$params['fileitemid'] = $this->subwiki->id;
+ $params['contextid'] = $this->modcontext->id;
+ $params['component'] = 'mod_wiki';
+ $params['filearea'] = 'attachments';
}
if (!empty($CFG->usetags)) {
@@ -560,16 +555,6 @@ class page_wiki_edit extends page_wiki {
$form = new mod_wiki_edit_form($url, $params);
if ($formdata = $form->get_data()) {
- if ($format != 'html') {
- $errors = $this->process_uploads($this->modcontext);
- if (!empty($errors)) {
- $contenterror = "";
- foreach ($errors as $e) {
- $contenterror .= "" . get_string('filenotuploadederror', 'wiki', $e->get_filename()) . "
";
- }
- print $OUTPUT->box($contenterror, 'errorbox');
- }
- }
if (!empty($CFG->usetags)) {
$data->tags = $formdata->tags;
}
@@ -583,21 +568,11 @@ class page_wiki_edit extends page_wiki {
$form->display();
}
- protected function process_uploads($context) {
- global $PAGE, $OUTPUT;
-
- if ($this->upload) {
- file_save_draft_area_files($this->attachments, $context->id, 'mod_wiki', 'attachments', $this->subwiki->id);
- return null;
- //return wiki_process_attachments($this->attachments, $this->deleteuploads, $context->id, 'mod_wiki', 'attachments', $this->subwiki->id);
- }
- }
}
/**
* Class that models the behavior of wiki's view comments page
*
- * @package mod-wiki
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class page_wiki_comments extends page_wiki {
@@ -715,7 +690,6 @@ class page_wiki_comments extends page_wiki {
/**
* Class that models the behavior of wiki's edit comment
*
- * @package mod-wiki
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class page_wiki_editcomment extends page_wiki {
@@ -817,7 +791,6 @@ class page_wiki_editcomment extends page_wiki {
/**
* Wiki page search page
*
- * @package mod-wiki
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class page_wiki_search extends page_wiki {
@@ -1939,10 +1912,10 @@ class page_wiki_save extends page_wiki_edit {
$params = array('attachmentoptions' => page_wiki_edit::$attachmentoptions, 'format' => $this->format, 'version' => $this->versionnumber);
if ($this->format != 'html') {
- $params['contextid'] = $this->modcontext->id;
- $params['component'] = 'mod_wiki';
- $params['filearea'] = 'attachments';
$params['fileitemid'] = $this->page->id;
+ $params['contextid'] = $context->id;
+ $params['component'] = 'mod_wiki';
+ $params['filearea'] = 'attachments';
}
$form = new mod_wiki_edit_form($url, $params);
@@ -1962,9 +1935,6 @@ class page_wiki_save extends page_wiki_edit {
}
if ($save && $data) {
- if ($this->format != 'html') {
- $errors = $this->process_uploads($this->modcontext);
- }
if (!empty($CFG->usetags)) {
tag_set('wiki_pages', $this->page->id, $data->tags);
}
diff --git a/mod/wiki/prettyview.php b/mod/wiki/prettyview.php
index fdc0a812272..30a19ea1a3b 100644
--- a/mod/wiki/prettyview.php
+++ b/mod/wiki/prettyview.php
@@ -51,7 +51,7 @@ if (!$wiki = wiki_get_wiki($subwiki->wikiid)) {
print_error('incorrectwikiid', 'wiki');
}
-require_course_login($course->id, true, $cm);
+require_login($course->id, true, $cm);
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
require_capability('mod/wiki:viewpage', $context);
diff --git a/mod/wiki/renderer.php b/mod/wiki/renderer.php
index 515860b7936..8853b767015 100644
--- a/mod/wiki/renderer.php
+++ b/mod/wiki/renderer.php
@@ -134,7 +134,7 @@ class mod_wiki_renderer extends plugin_renderer_base {
$newheading .= $this->output->container_end();
$oldheading = html_writer::tag('div', $oldheading, array('class'=>'wiki-diff-heading header clearfix'));
- $newheading = html_writer::tag('div', $newheading, array('class'=>'wiki-diff-heading header clearfix'));
+ $newheading = html_writer::tag('div', $newheading, array('class'=>'wiki-diff-heading header clearfix'));
$output = '';
$output .= html_writer::start_tag('div', array('class'=>'wiki-diff-container clearfix'));
@@ -238,16 +238,15 @@ class mod_wiki_renderer extends plugin_renderer_base {
global $PAGE;
return $this->output->box(format_module_intro('wiki', $this->page->activityrecord, $PAGE->cm->id), 'generalbox', 'intro');
}
+
public function tabs($page, $tabitems, $options) {
global $CFG;
- if (empty($page)) {
- return null;
- }
$tabs = array();
$baseurl = $CFG->wwwroot . '/mod/wiki/';
+ $context = get_context_instance(CONTEXT_MODULE, $this->page->cm->id);
$pageid = null;
- if (isset($page)) {
+ if (!empty($page)) {
$pageid = $page->id;
}
@@ -267,6 +266,18 @@ class mod_wiki_renderer extends plugin_renderer_base {
}
foreach ($tabitems as $tab) {
+ if ($tab == 'edit' && !has_capability('mod/wiki:editpage', $context)) {
+ continue;
+ }
+ if ($tab == 'comments' && !has_capability('mod/wiki:viewcomment', $context)) {
+ continue;
+ }
+ if ($tab == 'files' && !has_capability('mod/wiki:viewpage', $context)) {
+ continue;
+ }
+ if (($tab == 'view' || $tab == 'map' || $tab == 'history') && !has_capability('mod/wiki:viewpage', $context)) {
+ continue;
+ }
$link = $baseurl . $tab . '.php?pageid=' . $pageid;
if ($linked == $tab) {
$tabs[] = new tabobject($tab, $link, get_string($tab, 'wiki'), '', true);
@@ -287,9 +298,18 @@ class mod_wiki_renderer extends plugin_renderer_base {
return $html;
}
- public function wiki_print_subwiki_selector($wiki, $subwiki, $page) {
+ public function wiki_print_subwiki_selector($wiki, $subwiki, $page, $pagetype = 'view') {
global $CFG, $USER;
require_once($CFG->dirroot . '/user/lib.php');
+ switch ($pagetype) {
+ case 'files':
+ $baseurl = new moodle_url('/mod/wiki/files.php');
+ break;
+ case 'view':
+ default:
+ $baseurl = new moodle_url('/mod/wiki/view.php');
+ break;
+ }
$cm = get_coursemodule_from_instance('wiki', $wiki->id);
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
@@ -317,10 +337,13 @@ class mod_wiki_renderer extends plugin_renderer_base {
echo $this->output->container_start('wiki_right');
$params = array('wid' => $wiki->id, 'title' => $page->title);
- $url = new moodle_url('/mod/wiki/view.php', $params);
+ if ($pagetype == 'files') {
+ $params['pageid'] = $page->id;
+ }
+ $baseurl->params($params);
$name = 'uid';
$selected = $subwiki->userid;
- echo $this->output->single_select($url, $name, $options, $selected);
+ echo $this->output->single_select($baseurl, $name, $options, $selected);
echo $this->output->container_end();
}
return;
@@ -332,10 +355,14 @@ class mod_wiki_renderer extends plugin_renderer_base {
if ($wiki->wikimode == 'collaborative') {
// We need to print a select to choose a course group
- $params = 'wid=' . $wiki->id . '&title=' . urlencode($page->title);
+ $params = array('wid'=>$wiki->id, 'title'=>$page->title);
+ if ($pagetype == 'files') {
+ $params['pageid'] = $page->id;
+ }
+ $baseurl->params($params);
echo $this->output->container_start('wiki_right');
- groups_print_activity_menu($cm, $CFG->wwwroot . '/mod/wiki/view.php?' . $params);
+ groups_print_activity_menu($cm, $baseurl->out());
echo $this->output->container_end();
return;
} else if ($wiki->wikimode == 'individual') {
@@ -367,10 +394,13 @@ class mod_wiki_renderer extends plugin_renderer_base {
}
echo $this->output->container_start('wiki_right');
$params = array('wid' => $wiki->id, 'title' => $page->title);
- $url = new moodle_url('/mod/wiki/view.php', $params);
+ if ($pagetype == 'files') {
+ $params['pageid'] = $page->id;
+ }
+ $baseurl->params($params);
$name = 'groupanduser';
$selected = $subwiki->groupid . '-' . $subwiki->userid;
- echo $this->output->single_select($url, $name, $options, $selected);
+ echo $this->output->single_select($baseurl, $name, $options, $selected);
echo $this->output->container_end();
return;
@@ -382,10 +412,14 @@ class mod_wiki_renderer extends plugin_renderer_base {
CASE VISIBLEGROUPS:
if ($wiki->wikimode == 'collaborative') {
// We need to print a select to choose a course group
- $params = 'wid=' . $wiki->id . '&title=' . urlencode($page->title);
+ $params = array('wid'=>$wiki->id, 'title'=>urlencode($page->title));
+ if ($pagetype == 'files') {
+ $params['pageid'] = $page->id;
+ }
+ $baseurl->params($params);
echo $this->output->container_start('wiki_right');
- groups_print_activity_menu($cm, $CFG->wwwroot . '/mod/wiki/view.php?' . $params);
+ groups_print_activity_menu($cm, $baseurl->out());
echo $this->output->container_end();
return;
@@ -406,10 +440,13 @@ class mod_wiki_renderer extends plugin_renderer_base {
echo $this->output->container_start('wiki_right');
$params = array('wid' => $wiki->id, 'title' => $page->title);
- $url = new moodle_url('/mod/wiki/view.php', $params);
+ if ($pagetype == 'files') {
+ $params['pageid'] = $page->id;
+ }
+ $baseurl->params($params);
$name = 'groupanduser';
$selected = $subwiki->groupid . '-' . $subwiki->userid;
- echo $this->output->single_select($url, $name, $options, $selected);
+ echo $this->output->single_select($baseurl, $name, $options, $selected);
echo $this->output->container_end();
return;
@@ -440,4 +477,60 @@ class mod_wiki_renderer extends plugin_renderer_base {
$select->label = get_string('mapmenu', 'wiki') . ': ';
return $this->output->container($this->output->render($select), 'midpad');
}
+ public function wiki_files_tree($context, $subwiki) {
+ return $this->render(new wiki_files_tree($context, $subwiki));
+ }
+ public function render_wiki_files_tree(wiki_files_tree $tree) {
+ if (empty($tree->dir['subdirs']) && empty($tree->dir['files'])) {
+ $html = $this->output->box(get_string('nofilesavailable', 'repository'));
+ } else {
+ $htmlid = 'wiki_files_tree_'.uniqid();
+ $module = array('name'=>'mod_wiki', 'fullpath'=>'/mod/wiki/module.js');
+ $this->page->requires->js_init_call('M.mod_wiki.init_tree', array(false, $htmlid), false, $module);
+ $html = '';
+ $html .= $this->htmllize_tree($tree, $tree->dir);
+ $html .= '
';
+ }
+ return $html;
+ }
+
+ /**
+ * Internal function - creates htmls structure suitable for YUI tree.
+ */
+ protected function htmllize_tree($tree, $dir) {
+ global $CFG;
+ $yuiconfig = array();
+ $yuiconfig['type'] = 'html';
+
+ if (empty($dir['subdirs']) and empty($dir['files'])) {
+ return '';
+ }
+ $result = '';
+
+ return $result;
+ }
+}
+
+class wiki_files_tree implements renderable {
+ public $context;
+ public $dir;
+ public $subwiki;
+ public function __construct($context, $subwiki) {
+ $fs = get_file_storage();
+ $this->context = $context;
+ $this->subwiki = $subwiki;
+ $this->dir = $fs->get_area_tree($context->id, 'mod_wiki', 'attachments', $subwiki->id);
+ }
}
diff --git a/mod/wiki/search.php b/mod/wiki/search.php
index 90c944ab1d4..518195f920d 100644
--- a/mod/wiki/search.php
+++ b/mod/wiki/search.php
@@ -38,7 +38,7 @@ if (!$cm = get_coursemodule_from_id('wiki', $cmid)) {
print_error('invalidcoursemodule');
}
-require_course_login($course, true, $cm);
+require_login($course, true, $cm);
// @TODO: Fix call to wiki_get_subwiki_by_group
if (!$gid = groups_get_activity_group($cm)) {
diff --git a/mod/wiki/styles.css b/mod/wiki/styles.css
index badfdb37933..b76bb8f9e72 100644
--- a/mod/wiki/styles.css
+++ b/mod/wiki/styles.css
@@ -352,4 +352,10 @@ a.wiki_edit_section {
.wiki-diff-container .wiki-diff-rightside {margin-left:1%;}
.wiki-diff-container .wiki-diff-heading,
.wiki-diff-container .no-overflow {padding:10px;border:1px solid #DDD;}
-.wiki-diff-container .wiki-diff-rightside .wiki_diffversion {text-align:right;}
\ No newline at end of file
+.wiki-diff-container .wiki-diff-rightside .wiki_diffversion {text-align:right;}
+
+.wikieditor-toolbar img{
+ width: 22px;
+ height: 22px;
+ vertical-align: middle;
+}
diff --git a/mod/wiki/version.php b/mod/wiki/version.php
index c7f3582c50a..8d7b259d1dc 100644
--- a/mod/wiki/version.php
+++ b/mod/wiki/version.php
@@ -19,8 +19,7 @@
* Code fragment to define the version of wiki
* This fragment is called by moodle_needs_upgrading() and /admin/index.php
*
- * @package mod
- * @subpackage wiki
+ * @package mod-wiki-2.0
* @copyrigth 2009 Marc Alier, Jordi Piguillem marc.alier@upc.edu
* @copyrigth 2009 Universitat Politecnica de Catalunya http://www.upc.edu
*
@@ -33,6 +32,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-$module->version = 2011011000; // The current module version (Date: YYYYMMDDXX)
+$module->version = 2011011001; // The current module version (Date: YYYYMMDDXX)
$module->requires = 2010080300;
$module->cron = 0; // Period for cron to check this module (secs)
diff --git a/mod/wiki/view.php b/mod/wiki/view.php
index c4adf1c4679..6b6a40b4639 100644
--- a/mod/wiki/view.php
+++ b/mod/wiki/view.php
@@ -251,7 +251,7 @@ if ($id) {
} else {
print_error('incorrectparameters');
}
-require_course_login($course, true, $cm);
+require_login($course, true, $cm);
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
require_capability('mod/wiki:viewpage', $context);
diff --git a/mod/wiki/viewversion.php b/mod/wiki/viewversion.php
index 52029a26769..ca420d24f5b 100644
--- a/mod/wiki/viewversion.php
+++ b/mod/wiki/viewversion.php
@@ -58,7 +58,7 @@ if (!$cm = get_coursemodule_from_instance('wiki', $wiki->id)) {
$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
-require_course_login($course->id, true, $cm);
+require_login($course->id, true, $cm);
add_to_log($course->id, "wiki", "history", "history.php?id=$cm->id", "$wiki->id");
/// Print the page header
diff --git a/mod/workshop/db/upgradelib.php b/mod/workshop/db/upgradelib.php
index 4b79846f45a..ce59ed60579 100644
--- a/mod/workshop/db/upgradelib.php
+++ b/mod/workshop/db/upgradelib.php
@@ -210,6 +210,14 @@ function workshop_upgrade_transform_instance(stdClass $old) {
$new->strategy = 'rubric';
break;
}
+ if ($old->submissionstart < $old->submissionend) {
+ $new->submissionstart = $old->submissionstart;
+ $new->submissionend = $old->submissionend;
+ }
+ if ($old->assessmentstart < $old->assessmentend) {
+ $new->assessmentstart = $old->assessmentstart;
+ $new->assessmentend = $old->assessmentend;
+ }
return $new;
}
@@ -331,6 +339,9 @@ function workshop_upgrade_assessments() {
// list of teachers in every workshop: array of (int)workshopid => array of (int)userid => notused
$workshopteachers = array();
+ // get the list of ids of the new example submissions
+ $examplesubmissions = $DB->get_records('workshop_submissions', array('example' => 1), '', 'id');
+
$rs = $DB->get_recordset_select('workshop_assessments_old', 'newid IS NULL');
foreach ($rs as $old) {
if (!isset($workshopteachers[$old->workshopid])) {
@@ -338,8 +349,9 @@ function workshop_upgrade_assessments() {
$context = get_context_instance(CONTEXT_MODULE, $cm->id);
$workshopteachers[$old->workshopid] = get_users_by_capability($context, 'mod/workshop:manage', 'u.id');
}
+ $ofexample = isset($examplesubmissions[$newsubmissionids[$old->submissionid]]);
$new = workshop_upgrade_transform_assessment($old, $newsubmissionids[$old->submissionid],
- $workshopteachers[$old->workshopid], $teacherweights[$old->workshopid]);
+ $workshopteachers[$old->workshopid], $teacherweights[$old->workshopid], $ofexample);
$newid = $DB->insert_record('workshop_assessments', $new, true, true);
$DB->set_field('workshop_assessments_old', 'newplugin', 'assessments', array('id' => $old->id));
$DB->set_field('workshop_assessments_old', 'newid', $newid, array('id' => $old->id));
@@ -354,9 +366,10 @@ function workshop_upgrade_assessments() {
* @param int $newsubmissionid new submission id
* @param array $legacyteachers (int)userid => notused the list of legacy workshop teachers for the submission's workshop
* @param int $legacyteacherweight weight of teacher's assessment in legacy workshop
+ * @param bool $ofexample is this the assessment of an example submission?
* @return stdClass
*/
-function workshop_upgrade_transform_assessment(stdClass $old, $newsubmissionid, array $legacyteachers, $legacyteacherweight) {
+function workshop_upgrade_transform_assessment(stdClass $old, $newsubmissionid, array $legacyteachers, $legacyteacherweight, $ofexample) {
global $CFG;
require_once($CFG->libdir . '/gradelib.php');
@@ -364,10 +377,21 @@ function workshop_upgrade_transform_assessment(stdClass $old, $newsubmissionid,
$new->submissionid = $newsubmissionid;
$new->reviewerid = $old->userid;
- if (isset($legacyteachers[$old->userid])) {
- $new->weight = $legacyteacherweight;
+ if ($ofexample) {
+ // this is the assessment of an example submission
+ if (isset($legacyteachers[$old->userid])) {
+ // this is probably the reference assessment of the example submission
+ $new->weight = 1;
+ } else {
+ $new->weight = 0;
+ }
+
} else {
- $new->weight = 1;
+ if (isset($legacyteachers[$old->userid])) {
+ $new->weight = $legacyteacherweight;
+ } else {
+ $new->weight = 1;
+ }
}
if ($old->grade < 0) {
diff --git a/notes/externallib.php b/notes/externallib.php
new file mode 100644
index 00000000000..d5b9c5334d7
--- /dev/null
+++ b/notes/externallib.php
@@ -0,0 +1,184 @@
+.
+
+/**
+ * External notes API
+ *
+ * @package moodlecore
+ * @subpackage notes
+ * @copyright 2011 Moodle Pty Ltd (http://moodle.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+require_once("$CFG->libdir/externallib.php");
+
+class moodle_notes_external extends external_api {
+
+ /**
+ * Returns description of method parameters
+ * @return external_function_parameters
+ */
+ public static function create_notes_parameters() {
+ return new external_function_parameters(
+ array(
+ 'notes' => new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'userid' => new external_value(PARAM_INT, 'id of the user the note is about'),
+ 'publishstate' => new external_value(PARAM_ALPHA, '\'personal\', \'course\' or \'site\''),
+ 'courseid' => new external_value(PARAM_INT, 'course id of the note (in Moodle a note can only be created into a course, even for site and personal notes)'),
+ 'text' => new external_value(PARAM_RAW, 'the text of the message - text or HTML'),
+ 'format' => new external_value(PARAM_ALPHA, '\'text\' or \'html\'', VALUE_DEFAULT, 'text'),
+ 'clientnoteid' => new external_value(PARAM_ALPHANUMEXT, 'your own client id for the note. If this id is provided, the fail message id will be returned to you', VALUE_OPTIONAL),
+ )
+ )
+ )
+ )
+ );
+ }
+
+ /**
+ * Create notes about some users
+ * Note: code should be matching the /notes/edit.php checks
+ * and the /user/addnote.php checks. (they are similar cheks)
+ * @param array $notes An array of notes to create.
+ * @return array (success infos and fail infos)
+ */
+ public static function create_notes($notes = array()) {
+ global $CFG, $DB;
+ require_once($CFG->dirroot . "/notes/lib.php");
+
+ $params = self::validate_parameters(self::create_notes_parameters(), array('notes' => $notes));
+
+ //check if note system is enabled
+ if (!$CFG->enablenotes) {
+ throw new moodle_exception('notesdisabled', 'notes');
+ }
+
+ //retrieve all courses
+ $courseids = array();
+ foreach($params['notes'] as $note) {
+ $courseids[] = $note['courseid'];
+ }
+ $courses = $DB->get_records_list("course", "id", $courseids);
+
+ //retrieve all users of the notes
+ $userids = array();
+ foreach($params['notes'] as $note) {
+ $userids[] = $note['userid'];
+ }
+ list($sqluserids, $sqlparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'userid_');
+ $users = $DB->get_records_select("user", "id " . $sqluserids . " AND deleted = 0", $sqlparams);
+
+ $resultnotes = array();
+ foreach ($params['notes'] as $note) {
+
+ $success = true;
+ $resultnote = array(); //the infos about the success of the operation
+
+ //check the course exists
+ if (empty($courses[$note['courseid']])) {
+ $success = false;
+ $errormessage = get_string('invalidcourseid', 'notes', $note['courseid']);
+ } else {
+ // Ensure the current user is allowed to run this function
+ $context = get_context_instance(CONTEXT_COURSE, $note['courseid']);
+ self::validate_context($context);
+ require_capability('moodle/notes:manage', $context);
+ }
+
+ //check the user exists
+ if (empty($users[$note['userid']])) {
+ $success = false;
+ $errormessage = get_string('invaliduserid', 'notes', $note['userid']);
+ }
+
+ //build the resultnote
+ if (isset($note['clientnoteid'])) {
+ $resultnote['clientnoteid'] = $note['clientnoteid'];
+ }
+
+ if ($success) {
+ //now we can create the note
+ $dbnote = new stdClass;
+ $dbnote->courseid = $note['courseid'];
+ $dbnote->userid = $note['userid'];
+ //clean param text and set format accordingly
+ switch (strtolower($note['format'])) {
+ case 'html':
+ $dbnote->content = clean_param($note['text'], PARAM_CLEANHTML);
+ $dbnote->format = FORMAT_HTML;
+ break;
+ case 'text':
+ default:
+ $dbnote->content = clean_param($note['text'], PARAM_TEXT);
+ $dbnote->format = FORMAT_PLAIN;
+ break;
+ }
+
+ //get the state ('personal', 'course', 'site')
+ switch ($note['publishstate']) {
+ case 'personal':
+ $dbnote->publishstate = NOTES_STATE_DRAFT;
+ break;
+ case 'course':
+ $dbnote->publishstate = NOTES_STATE_PUBLIC;
+ break;
+ case 'site':
+ $dbnote->publishstate = NOTES_STATE_SITE;
+ $dbnote->courseid = SITEID;
+ break;
+ default:
+ break;
+ }
+
+ //TODO: performance improvement - if possible create a bulk functions for saving multiple notes at once
+ if (note_save($dbnote)) { //note_save attribut an id in case of success
+ add_to_log($dbnote->courseid, 'notes', 'add',
+ 'index.php?course='.$dbnote->courseid.'&user='.$dbnote->userid
+ . '#note-' . $dbnote->id , 'add note');
+ $success = $dbnote->id;
+ }
+
+ $resultnote['noteid'] = $success;
+ } else {
+ $resultnote['noteid'] = -1;
+ $resultnote['errormessage'] = $errormessage;
+ }
+
+ $resultnotes[] = $resultnote;
+ }
+
+ return $resultnotes;
+ }
+
+ /**
+ * Returns description of method result value
+ * @return external_description
+ */
+ public static function create_notes_returns() {
+ return new external_multiple_structure(
+ new external_single_structure(
+ array(
+ 'clientnoteid' => new external_value(PARAM_ALPHANUMEXT, 'your own id for the note', VALUE_OPTIONAL),
+ 'noteid' => new external_value(PARAM_INT, 'test this to know if it success: id of the created note when successed, -1 when failed'),
+ 'errormessage' => new external_value(PARAM_TEXT, 'error message - if failed', VALUE_OPTIONAL)
+ )
+ )
+ );
+ }
+
+}
diff --git a/pluginfile.php b/pluginfile.php
index 0ab84289706..e749921586b 100644
--- a/pluginfile.php
+++ b/pluginfile.php
@@ -305,7 +305,7 @@ if ($component === 'blog') {
}
if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'/.png')) {
if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'/.jpg')) {
- redirect($OUTPUT->pix_url('u/f1'));
+ redirect($OUTPUT->pix_url('u/'.$filename));
}
}
diff --git a/question/addquestion.php b/question/addquestion.php
index 01325dd13a9..7c1d3fe3aa2 100644
--- a/question/addquestion.php
+++ b/question/addquestion.php
@@ -1,35 +1,29 @@
.
/**
* Shows a screen where the user can choose a question type, before being
* redirected to question.php
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package questionbank
- *//** */
+ * @package moodlecore
+ * @subpackage questionbank
+ * @copyright 2009 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
require_once(dirname(__FILE__) . '/../config.php');
require_once(dirname(__FILE__) . '/editlib.php');
@@ -87,12 +81,12 @@ if ($cm !== null) {
$PAGE->navbar->add(get_string('editinga', 'moodle', get_string('modulename', $cm->modname)),$returnurl);
}
$PAGE->navbar->add($chooseqtype);
- $PAGE->set_title($chooseqtype);
+ $PAGE->set_title($chooseqtype);
echo $OUTPUT->header();
} else {
$PAGE->navbar->add(get_string('questionbank', 'question'),$returnurl);
$PAGE->navbar->add($chooseqtype);
- $PAGE->set_title($chooseqtype);
+ $PAGE->set_title($chooseqtype);
echo $OUTPUT->header();
}
diff --git a/question/behaviour/adaptive/behaviour.php b/question/behaviour/adaptive/behaviour.php
new file mode 100644
index 00000000000..7e8ad03fd4c
--- /dev/null
+++ b/question/behaviour/adaptive/behaviour.php
@@ -0,0 +1,180 @@
+.
+
+/**
+ * Question behaviour for the old adaptive mode.
+ *
+ * @package qbehaviour
+ * @subpackage adaptive
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Question behaviour for adaptive mode.
+ *
+ * This is the old version of interactive mode.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_adaptive extends question_behaviour_with_save {
+ const IS_ARCHETYPAL = true;
+
+ public function required_question_definition_type() {
+ return 'question_automatically_gradable';
+ }
+
+ public function get_expected_data() {
+ if ($this->qa->get_state()->is_active()) {
+ return array('submit' => PARAM_BOOL);
+ }
+ return parent::get_expected_data();
+ }
+
+ public function get_right_answer_summary() {
+ return $this->question->get_right_answer_summary();
+ }
+
+ public function adjust_display_options(question_display_options $options) {
+ parent::adjust_display_options($options);
+ if (!$this->qa->get_state()->is_finished() &&
+ $this->qa->get_last_behaviour_var('_try')) {
+ $options->feedback = true;
+ }
+ }
+
+ public function get_state_string($showcorrectness) {
+ $state = $this->qa->get_state();
+
+ $laststep = $this->qa->get_last_step();
+ if ($laststep->has_behaviour_var('_try')) {
+ $state = question_state::graded_state_for_fraction(
+ $laststep->get_behaviour_var('_rawfraction'));
+ }
+
+ return $state->default_string($showcorrectness);
+ }
+
+ public function process_action(question_attempt_pending_step $pendingstep) {
+ if ($pendingstep->has_behaviour_var('comment')) {
+ return $this->process_comment($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('finish')) {
+ return $this->process_finish($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('submit')) {
+ return $this->process_submit($pendingstep);
+ } else {
+ return $this->process_save($pendingstep);
+ }
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ if ($step->has_behaviour_var('comment')) {
+ return $this->summarise_manual_comment($step);
+ } else if ($step->has_behaviour_var('finish')) {
+ return $this->summarise_finish($step);
+ } else if ($step->has_behaviour_var('submit')) {
+ return $this->summarise_submit($step);
+ } else {
+ return $this->summarise_save($step);
+ }
+ }
+
+ public function process_save(question_attempt_pending_step $pendingstep) {
+ $status = parent::process_save($pendingstep);
+ $prevgrade = $this->qa->get_fraction();
+ if (!is_null($prevgrade)) {
+ $pendingstep->set_fraction($prevgrade);
+ }
+ $pendingstep->set_state(question_state::$todo);
+ return $status;
+ }
+
+ protected function adjusted_fraction($fraction, $prevtries) {
+ return $fraction - $this->question->penalty * $prevtries;
+ }
+
+ public function process_submit(question_attempt_pending_step $pendingstep) {
+ $status = $this->process_save($pendingstep);
+
+ $response = $pendingstep->get_qt_data();
+ if (!$this->question->is_gradable_response($response)) {
+ $pendingstep->set_state(question_state::$invalid);
+ if ($this->qa->get_state() != question_state::$invalid) {
+ $status = question_attempt::KEEP;
+ }
+ return $status;
+ }
+
+ $prevtries = $this->qa->get_last_behaviour_var('_try', 0);
+ $prevbest = $pendingstep->get_fraction();
+ if (is_null($prevbest)) {
+ $prevbest = 0;
+ }
+
+ list($fraction, $state) = $this->question->grade_response($response);
+
+ $pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
+ if ($state == question_state::$gradedright) {
+ $pendingstep->set_state(question_state::$complete);
+ } else {
+ $pendingstep->set_state(question_state::$todo);
+ }
+ $pendingstep->set_behaviour_var('_try', $prevtries + 1);
+ $pendingstep->set_behaviour_var('_rawfraction', $fraction);
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+
+ return question_attempt::KEEP;
+ }
+
+ public function process_finish(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ $laststep = $this->qa->get_last_step();
+ $response = $laststep->get_qt_data();
+ if (!$this->question->is_gradable_response($response)) {
+ $pendingstep->set_state(question_state::$gaveup);
+ return question_attempt::KEEP;
+ }
+
+ $prevtries = $this->qa->get_last_behaviour_var('_try', 0);
+ $prevbest = $pendingstep->get_fraction();
+ if (is_null($prevbest)) {
+ $prevbest = 0;
+ }
+
+ if ($laststep->has_behaviour_var('_try')) {
+ // Last answer was graded, we want to regrade it. Otherwise the answer
+ // has changed, and we are grading a new try.
+ $prevtries -= 1;
+ }
+
+ list($fraction, $state) = $this->question->grade_response($response);
+
+ $pendingstep->set_fraction(max($prevbest, $this->adjusted_fraction($fraction, $prevtries)));
+ $pendingstep->set_state($state);
+ $pendingstep->set_behaviour_var('_try', $prevtries + 1);
+ $pendingstep->set_behaviour_var('_rawfraction', $fraction);
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+ return question_attempt::KEEP;
+ }
+}
diff --git a/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php b/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php
new file mode 100644
index 00000000000..0bb96fa44a6
--- /dev/null
+++ b/question/behaviour/adaptive/lang/en/qbehaviour_adaptive.php
@@ -0,0 +1,29 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_adaptive', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage adaptive
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['gradingdetails'] = 'Marks for this submission: {$a->raw}/{$a->max}.';
+$string['gradingdetailsadjustment'] = 'With previous penalties this gives {$a->cur}/{$a->max}.';
+$string['gradingdetailspenalty'] = 'This submission attracted a penalty of {$a}.';
+$string['pluginname'] = 'Adaptive mode';
diff --git a/question/behaviour/adaptive/renderer.php b/question/behaviour/adaptive/renderer.php
new file mode 100644
index 00000000000..f9faf30e527
--- /dev/null
+++ b/question/behaviour/adaptive/renderer.php
@@ -0,0 +1,110 @@
+.
+
+/**
+ * Renderer for outputting parts of a question belonging to the legacy
+ * adaptive behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage adaptive
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the legacy
+ * adaptive behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_adaptive_renderer extends qbehaviour_renderer {
+ protected function get_graded_step(question_attempt $qa) {
+ foreach ($qa->get_reverse_step_iterator() as $step) {
+ if ($step->has_behaviour_var('_try')) {
+ return $step;
+ }
+ }
+ }
+
+ public function controls(question_attempt $qa, question_display_options $options) {
+ return $this->submit_button($qa, $options);
+ }
+
+ public function feedback(question_attempt $qa, question_display_options $options) {
+ // Try to find the last graded step.
+
+ $gradedstep = $this->get_graded_step($qa);
+ if (is_null($gradedstep) || $qa->get_max_mark() == 0 ||
+ $options->marks < question_display_options::MARK_AND_MAX) {
+ return '';
+ }
+
+ // Display the grading details from the last graded state
+ $mark = new stdClass();
+ $mark->max = $qa->format_max_mark($options->markdp);
+
+ $actualmark = $gradedstep->get_fraction() * $qa->get_max_mark();
+ $mark->cur = format_float($actualmark, $options->markdp);
+
+ $rawmark = $gradedstep->get_behaviour_var('_rawfraction') * $qa->get_max_mark();
+ $mark->raw = format_float($rawmark, $options->markdp);
+
+ // let student know wether the answer was correct
+ if ($qa->get_state()->is_commented()) {
+ $class = $qa->get_state()->get_feedback_class();
+ } else {
+ $class = question_state::graded_state_for_fraction(
+ $gradedstep->get_behaviour_var('_rawfraction'))->get_feedback_class();
+ }
+
+ $gradingdetails = get_string('gradingdetails', 'qbehaviour_adaptive', $mark);
+
+ $gradingdetails .= $this->penalty_info($qa, $mark);
+
+ $output = '';
+ $output .= html_writer::tag('div', get_string($class, 'question'),
+ array('class' => 'correctness ' . $class));
+ $output .= html_writer::tag('div', $gradingdetails,
+ array('class' => 'gradingdetails'));
+ return $output;
+ }
+
+ protected function penalty_info($qa, $mark) {
+ if (!$qa->get_question()->penalty) {
+ return '';
+ }
+ $output = '';
+
+ // print details of grade adjustment due to penalties
+ if ($mark->raw != $mark->cur) {
+ $output .= ' ' . get_string('gradingdetailsadjustment', 'qbehaviour_adaptive', $mark);
+ }
+
+ // print info about new penalty
+ // penalty is relevant only if the answer is not correct and further attempts are possible
+ if (!$qa->get_state()->is_finished()) {
+ $output .= ' ' . get_string('gradingdetailspenalty', 'qbehaviour_adaptive',
+ $qa->get_question()->penalty);
+ }
+
+ return $output;
+ }
+}
diff --git a/question/behaviour/adaptive/simpletest/testwalkthrough.php b/question/behaviour/adaptive/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..88fd82c1f05
--- /dev/null
+++ b/question/behaviour/adaptive/simpletest/testwalkthrough.php
@@ -0,0 +1,247 @@
+.
+
+
+/**
+ * This file contains tests that walks a question through the adaptive
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage adaptive
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the adaptive behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_adaptive_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_adaptive_multichoice() {
+
+ // Create a multiple choice, single response question.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $mc->penalty = 0.3333333;
+ $this->start_attempt_at_question($mc, 'adaptive', 3);
+
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process a submit.
+ $this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0),
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 2) % 3, true, false),
+ $this->get_contains_incorrect_expectation());
+ $this->assertPattern('/B|C/',
+ $this->quba->get_response_summary($this->slot));
+
+ // Process a change of answer to the right one, but not sumbitted.
+ $this->process_submission(array('answer' => $rightindex));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0),
+ $this->get_contains_mc_radio_expectation($rightindex, true, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false));
+ $this->assertPattern('/B|C/',
+ $this->quba->get_response_summary($this->slot));
+
+ // Now submit the right answer.
+ $this->process_submission(array('answer' => $rightindex, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(3 * (1 - $mc->penalty));
+ $this->check_current_output(
+ $this->get_contains_mark_summary(3 * (1 - $mc->penalty)),
+ $this->get_contains_mc_radio_expectation($rightindex, true, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false),
+ $this->get_contains_correct_expectation());
+ $this->assertEqual('A',
+ $this->quba->get_response_summary($this->slot));
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(3 * (1 - $mc->penalty));
+ $this->check_current_output(
+ $this->get_contains_mark_summary(3 * (1 - $mc->penalty)),
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, false, false),
+ $this->get_contains_correct_expectation());
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 1);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(1),
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+
+ // Now change the correct answer to the question, and regrade.
+ $mc->answers[13]->fraction = -0.33333333;
+ $mc->answers[15]->fraction = 1;
+ $this->quba->regrade_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(1),
+ $this->get_contains_partcorrect_expectation());
+
+ $autogradedstep = $this->get_step($this->get_step_count() - 2);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), 0, 0.0000001);
+ }
+
+ public function test_adaptive_multichoice2() {
+
+ // Create a multiple choice, multiple response question.
+ $mc = test_question_maker::make_a_multichoice_multi_question();
+ $mc->penalty = 0.3333333;
+ $mc->shuffleanswers = 0;
+ $this->start_attempt_at_question($mc, 'adaptive', 2);
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process a submit.
+ $this->process_submission(array('choice0' => 1, 'choice2' => 1, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(2);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(2),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_correct_expectation());
+
+ // Save the same correct answer again. Should no do anything.
+ $numsteps = $this->get_step_count();
+ $this->process_submission(array('choice0' => 1, 'choice2' => 1));
+
+ // Verify.
+ $this->check_step_count($numsteps);
+ $this->check_current_state(question_state::$complete);
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_step_count($numsteps + 1);
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(2);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(2),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_correct_expectation());
+ }
+
+ public function test_adaptive_shortanswer_try_to_submit_blank() {
+
+ // Create a short answer question with correct answer true.
+ $sa = test_question_maker::make_a_shortanswer_question();
+ $this->start_attempt_at_question($sa, 'adaptive');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit with blank answer.
+ $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+ // Verify.
+ $this->check_current_state(question_state::$invalid);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_validation_error_expectation());
+ $this->assertNull($this->quba->get_response_summary($this->slot));
+
+ // Now get it wrong.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'toad'));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0.8);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.8),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_partcorrect_expectation(),
+ $this->get_does_not_contain_validation_error_expectation());
+
+ // Now submit blank again.
+ $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+ // Verify.
+ $this->check_current_state(question_state::$invalid);
+ $this->check_current_mark(0.8);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.8),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_partcorrect_expectation(),
+ $this->get_contains_validation_error_expectation());
+ }
+}
diff --git a/question/behaviour/adaptivenopenalty/behaviour.php b/question/behaviour/adaptivenopenalty/behaviour.php
new file mode 100644
index 00000000000..0740fa6b192
--- /dev/null
+++ b/question/behaviour/adaptivenopenalty/behaviour.php
@@ -0,0 +1,46 @@
+.
+
+/**
+ * Question behaviour for the old adaptive mode, with no penalties.
+ *
+ * @package qbehaviour
+ * @subpackage adaptivenopenalty
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../adaptive/behaviour.php');
+
+
+/**
+ * Question behaviour for adaptive mode, with no penalties.
+ *
+ * This is the old version of interactive mode, without penalties.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_adaptivenopenalty extends qbehaviour_adaptive {
+ const IS_ARCHETYPAL = true;
+
+ protected function adjusted_fraction($fraction, $prevtries) {
+ return $fraction;
+ }
+}
diff --git a/question/behaviour/adaptivenopenalty/lang/en/qbehaviour_adaptivenopenalty.php b/question/behaviour/adaptivenopenalty/lang/en/qbehaviour_adaptivenopenalty.php
new file mode 100644
index 00000000000..d15c66830e5
--- /dev/null
+++ b/question/behaviour/adaptivenopenalty/lang/en/qbehaviour_adaptivenopenalty.php
@@ -0,0 +1,26 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_adaptivenopenalty', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage adaptivenopenalty
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Adaptive mode (no penalties)';
diff --git a/question/behaviour/adaptivenopenalty/renderer.php b/question/behaviour/adaptivenopenalty/renderer.php
new file mode 100644
index 00000000000..60ca96b662e
--- /dev/null
+++ b/question/behaviour/adaptivenopenalty/renderer.php
@@ -0,0 +1,44 @@
+.
+
+/**
+ * Renderer for outputting parts of a question belonging to the legacy
+ * adaptive (no penalties) behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage adaptivenopenalty
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../adaptive/renderer.php');
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the legacy
+ * adaptive (no penalties) behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_adaptivenopenalty_renderer extends qbehaviour_adaptive_renderer {
+ protected function penalty_info($qa, $mark) {
+ return '';
+ }
+}
diff --git a/question/behaviour/adaptivenopenalty/simpletest/testwalkthrough.php b/question/behaviour/adaptivenopenalty/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..0cd5ff7d443
--- /dev/null
+++ b/question/behaviour/adaptivenopenalty/simpletest/testwalkthrough.php
@@ -0,0 +1,194 @@
+.
+
+/**
+ * This file contains tests that walks a question through the adaptive (no penalties)k
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage adaptivenopenalty
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the adaptive (no penalties) behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_adaptivenopenalty_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_multichoice() {
+
+ // Create a multiple choice, single response question.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $mc->penalty = 0.3333333;
+ $this->start_attempt_at_question($mc, 'adaptivenopenalty', 3);
+
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process a submit.
+ $this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0),
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 2) % 3, true, false),
+ $this->get_contains_incorrect_expectation());
+ $this->assertPattern('/B|C/',
+ $this->quba->get_response_summary($this->slot));
+
+ // Process a change of answer to the right one, but not sumbitted.
+ $this->process_submission(array('answer' => $rightindex));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(0);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0),
+ $this->get_contains_mc_radio_expectation($rightindex, true, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false));
+ $this->assertPattern('/B|C/',
+ $this->quba->get_response_summary($this->slot));
+
+ // Now submit the right answer.
+ $this->process_submission(array('answer' => $rightindex, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(3);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(3),
+ $this->get_contains_mc_radio_expectation($rightindex, true, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, true, false),
+ $this->get_contains_correct_expectation());
+ $this->assertEqual('A',
+ $this->quba->get_response_summary($this->slot));
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(3);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(3),
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 2) % 3, false, false),
+ $this->get_contains_correct_expectation());
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 1);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(1),
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+
+ // Now change the correct answer to the question, and regrade.
+ $mc->answers[13]->fraction = -0.33333333;
+ $mc->answers[15]->fraction = 1;
+ $this->quba->regrade_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(1),
+ $this->get_contains_partcorrect_expectation());
+
+ $autogradedstep = $this->get_step($this->get_step_count() - 2);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), 0, 0.0000001);
+ }
+
+ public function test_multichoice2() {
+
+ // Create a multiple choice, multiple response question.
+ $mc = test_question_maker::make_a_multichoice_multi_question();
+ $mc->penalty = 0.3333333;
+ $mc->shuffleanswers = 0;
+ $this->start_attempt_at_question($mc, 'adaptivenopenalty', 2);
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process a submit.
+ $this->process_submission(array('choice0' => 1, 'choice2' => 1, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(2);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(2),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_contains_correct_expectation());
+
+ // Save the same correct answer again. Should no do anything.
+ $numsteps = $this->get_step_count();
+ $this->process_submission(array('choice0' => 1, 'choice2' => 1));
+
+ // Verify.
+ $this->check_step_count($numsteps);
+ $this->check_current_state(question_state::$complete);
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_step_count($numsteps + 1);
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(2);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(2),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_correct_expectation());
+ }
+}
diff --git a/question/behaviour/behaviourbase.php b/question/behaviour/behaviourbase.php
new file mode 100644
index 00000000000..95c0e2a4dfe
--- /dev/null
+++ b/question/behaviour/behaviourbase.php
@@ -0,0 +1,654 @@
+.
+
+/**
+ * Defines the quetsion behaviour base class
+ *
+ * @package moodlecore
+ * @subpackage questionbehaviours
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * The base class for question behaviours.
+ *
+ * A question behaviour is used by the question engine, specifically by
+ * a {@link question_attempt} to manage the flow of actions a student can take
+ * as they work through a question, and later, as a teacher manually grades it.
+ * In turn, the behaviour will delegate certain processing to the
+ * relevant {@link question_definition}.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class question_behaviour {
+ /**
+ * Certain behaviours are definitive of a way that questions can
+ * behave when attempted. For example deferredfeedback model, interactive
+ * model, etc. These are the options that should be listed in the
+ * user-interface. These models should define the class constant
+ * IS_ARCHETYPAL as true. Other models are more implementation details, for
+ * example the informationitem model, or a special subclass like
+ * interactive_adapted_for_my_qtype. These models should IS_ARCHETYPAL as
+ * false.
+ * @var boolean
+ */
+ const IS_ARCHETYPAL = false;
+
+ /** @var question_attempt the question attempt we are managing. */
+ protected $qa;
+ /** @var question_definition shortcut to $qa->get_question(). */
+ protected $question;
+
+ /**
+ * Normally you should not call this constuctor directly. The appropriate
+ * behaviour object is created automatically as part of
+ * {@link question_attempt::start()}.
+ * @param question_attempt $qa the question attempt we will be managing.
+ * @param string $preferredbehaviour the type of behaviour that was actually
+ * requested. This information is not needed in most cases, the type of
+ * subclass is enough, but occasionally it is needed.
+ */
+ public function __construct(question_attempt $qa, $preferredbehaviour) {
+ $this->qa = $qa;
+ $this->question = $qa->get_question();
+ $requiredclass = $this->required_question_definition_type();
+ if (!$this->question instanceof $requiredclass) {
+ throw new coding_exception('This behaviour (' . $this->get_name() .
+ ') cannot work with this question (' . get_class($this->question) . ')');
+ }
+ }
+
+ /**
+ * Most behaviours can only work with {@link question_definition}s
+ * of a particular subtype, or that implement a particular interface.
+ * This method lets the behaviour document that. The type of
+ * question passed to the constructor is then checked against this type.
+ * @return string class/interface name.
+ */
+ public abstract function required_question_definition_type();
+
+ /**
+ * @return string the name of this behaviour. For example the name of
+ * qbehaviour_mymodle is 'mymodel'.
+ */
+ public function get_name() {
+ return substr(get_class($this), 11);
+ }
+
+ /**
+ * 'Override' this method if there are some display options that do not make
+ * sense 'during the attempt'.
+ * @return array of {@link question_display_options} field names, that are
+ * not relevant to this behaviour before a 'finish' action.
+ */
+ public static function get_unused_display_options() {
+ return array();
+ }
+
+ /**
+ * Cause the question to be renderered. This gets the appropriate behaviour
+ * renderer using {@link get_renderer()}, and adjusts the display
+ * options using {@link adjust_display_options()} and then calls
+ * {@link core_question_renderer::question()} to do the work.
+ * @param question_display_options $options controls what should and should not be displayed.
+ * @param unknown_type $number the question number to display.
+ * @param core_question_renderer $qoutput the question renderer that will coordinate everything.
+ * @param qtype_renderer $qtoutput the question type renderer that will be helping.
+ * @return HTML fragment.
+ */
+ public function render(question_display_options $options, $number,
+ core_question_renderer $qoutput, qtype_renderer $qtoutput) {
+ $behaviouroutput = $this->get_renderer($qoutput->get_page());
+ $options = clone($options);
+ $this->adjust_display_options($options);
+ return $qoutput->question($this->qa, $behaviouroutput, $qtoutput, $options, $number);
+ }
+
+ /**
+ * Checks whether the users is allow to be served a particular file.
+ * @param question_display_options $options the options that control display of the question.
+ * @param string $component the name of the component we are serving files for.
+ * @param string $filearea the name of the file area.
+ * @param array $args the remaining bits of the file path.
+ * @param bool $forcedownload whether the user must be forced to download the file.
+ * @return bool true if the user can access this file.
+ */
+ public function check_file_access($options, $component, $filearea, $args, $forcedownload) {
+ $this->adjust_display_options($options);
+ return $this->question->check_file_access($this->qa, $options, $component,
+ $filearea, $args, $forcedownload);
+ }
+
+ /**
+ * @param moodle_page $page the page to render for.
+ * @return qbehaviour_renderer get the appropriate renderer to use for this model.
+ */
+ public function get_renderer(moodle_page $page) {
+ return $page->get_renderer(get_class($this));
+ }
+
+ /**
+ * Make any changes to the display options before a question is rendered, so
+ * that it can be displayed in a way that is appropriate for the statue it is
+ * currently in. For example, by default, if the question is finished, we
+ * ensure that it is only ever displayed read-only.
+ * @param question_display_options $options the options to adjust. Just change
+ * the properties of this object - objects are passed by referece.
+ */
+ public function adjust_display_options(question_display_options $options) {
+ if (!$this->qa->has_marks()) {
+ $options->correctness = false;
+ $options->numpartscorrect = false;
+ }
+ if ($this->qa->get_state()->is_finished()) {
+ $options->readonly = true;
+ $options->numpartscorrect = $options->numpartscorrect &&
+ $this->qa->get_state()->is_partially_correct() &&
+ !empty($this->question->shownumcorrect);
+ } else {
+ $options->hide_all_feedback();
+ }
+ }
+
+ /**
+ * Get the most applicable hint for the question in its current state.
+ * @return question_hint the most applicable hint, or null, if none.
+ */
+ public function get_applicable_hint() {
+ return null;
+ }
+
+ /**
+ * What is the minimum fraction that can be scored for this question.
+ * Normally this will be based on $this->question->get_min_fraction(),
+ * but may be modified in some way by the model.
+ *
+ * @return number the minimum fraction when this question is attempted under
+ * this model.
+ */
+ public function get_min_fraction() {
+ return 0;
+ }
+
+ /**
+ * Adjust a random guess score for a question using this model. You have to
+ * do this without knowing details of the specific question, or which usage
+ * it is in.
+ * @param number $fraction the random guess score from the question type.
+ * @return number the adjusted fraction.
+ */
+ public static function adjust_random_guess_score($fraction) {
+ return $fraction;
+ }
+
+ /**
+ * Return an array of the behaviour variables that could be submitted
+ * as part of a question of this type, with their types, so they can be
+ * properly cleaned.
+ * @return array variable name => PARAM_... constant.
+ */
+ public function get_expected_data() {
+ if (!$this->qa->get_state()->is_finished()) {
+ return array();
+ }
+
+ $vars = array('comment' => PARAM_RAW);
+ if ($this->qa->get_max_mark()) {
+ $vars['mark'] = question_attempt::PARAM_MARK;
+ $vars['maxmark'] = PARAM_NUMBER;
+ }
+ return $vars;
+ }
+
+ /**
+ * Return an array of question type variables for the question in its current
+ * state. Normally, if {@link adjust_display_options()} would set
+ * {@link question_display_options::$readonly} to true, then this method
+ * should return an empty array, otherwise it should return
+ * $this->question->get_expected_data(). Thus, there should be little need to
+ * override this method.
+ * @return array|string variable name => PARAM_... constant, or, as a special case
+ * that should only be used in unavoidable, the constant question_attempt::USE_RAW_DATA
+ * meaning take all the raw submitted data belonging to this question.
+ */
+ public function get_expected_qt_data() {
+ $fakeoptions = new question_display_options();
+ $fakeoptions->readonly = false;
+ $this->adjust_display_options($fakeoptions);
+ if ($fakeoptions->readonly) {
+ return array();
+ } else {
+ return $this->question->get_expected_data();
+ }
+ }
+
+ /**
+ * Return an array of any im variables, and the value required to get full
+ * marks.
+ * @return array variable name => value.
+ */
+ public function get_correct_response() {
+ return array();
+ }
+
+ /**
+ * Generate a brief, plain-text, summary of this question. This is used by
+ * various reports. This should show the particular variant of the question
+ * as presented to students. For example, the calculated quetsion type would
+ * fill in the particular numbers that were presented to the student.
+ * This method will return null if such a summary is not possible, or
+ * inappropriate.
+ *
+ * Normally, this method delegates to {question_definition::get_question_summary()}.
+ *
+ * @return string|null a plain text summary of this question.
+ */
+ public function get_question_summary() {
+ return $this->question->get_question_summary();
+ }
+
+ /**
+ * Generate a brief, plain-text, summary of the correct answer to this question.
+ * This is used by various reports, and can also be useful when testing.
+ * This method will return null if such a summary is not possible, or
+ * inappropriate.
+ *
+ * @return string|null a plain text summary of the right answer to this question.
+ */
+ public function get_right_answer_summary() {
+ return null;
+ }
+
+ /**
+ * Used by {@link start_based_on()} to get the data needed to start a new
+ * attempt from the point this attempt has go to.
+ * @return array name => value pairs.
+ */
+ public function get_resume_data() {
+ $olddata = $this->qa->get_step(0)->get_all_data();
+ $olddata = $this->qa->get_last_qt_data() + $olddata;
+ $olddata = $this->get_our_resume_data() + $olddata;
+ return $olddata;
+ }
+
+ /**
+ * Used by {@link start_based_on()} to get the data needed to start a new
+ * attempt from the point this attempt has go to.
+ * @return unknown_type
+ */
+ protected function get_our_resume_data() {
+ return array();
+ }
+
+ /**
+ * @return array subpartid => object with fields
+ * ->responseclassid matches one of the values returned from
+ * quetion_type::get_possible_responses.
+ * ->response the actual response the student gave to this part, as a string.
+ * ->fraction the credit awarded for this subpart, may be null.
+ * returns an empty array if no analysis is possible.
+ */
+ public function classify_response() {
+ return $this->question->classify_response($this->qa->get_last_qt_data());
+ }
+
+ /**
+ * Generate a brief textual description of the current state of the question,
+ * normally displayed under the question number.
+ *
+ * @param bool $showcorrectness Whether right/partial/wrong states should
+ * be distinguised.
+ * @return string a brief summary of the current state of the qestion attempt.
+ */
+ public function get_state_string($showcorrectness) {
+ return $this->qa->get_state()->default_string($showcorrectness);
+ }
+
+ public abstract function summarise_action(question_attempt_step $step);
+
+ /**
+ * Initialise the first step in a question attempt when a new
+ * {@link question_attempt} is being started.
+ *
+ * This method must call $this->question->start_attempt($step), and may
+ * perform additional processing if the model requries it.
+ *
+ * @param question_attempt_step $step the first step of the
+ * question_attempt being started.
+ * @param int $variant which variant of the question to use.
+ */
+ public function init_first_step(question_attempt_step $step, $variant) {
+ $this->question->start_attempt($step, $variant);
+ }
+
+ /**
+ * Checks whether two manual grading actions are the same. That is, whether
+ * the comment, and the mark (if given) is the same.
+ *
+ * @param question_attempt_step $pendingstep contains the new responses.
+ * @return bool whether the new response is the same as we already have.
+ */
+ protected function is_same_comment($pendingstep) {
+ $previouscomment = $this->qa->get_last_behaviour_var('comment');
+ $newcomment = $pendingstep->get_behaviour_var('comment');
+
+ if (is_null($previouscomment) && !html_is_blank($newcomment) ||
+ $previouscomment != $newcomment) {
+ return false;
+ }
+
+ // So, now we know the comment is the same, so check the mark, if present.
+ $previousfraction = $this->qa->get_fraction();
+ $newmark = $pendingstep->get_behaviour_var('mark');
+
+ if (is_null($previousfraction)) {
+ return is_null($newmark) || $newmark === '';
+ } else if (is_null($newmark) || $newmark === '') {
+ return false;
+ }
+
+ $newfraction = $newmark / $pendingstep->get_behaviour_var('maxmark');
+
+ return abs($newfraction - $previousfraction) < 0.0000001;
+ }
+
+ /**
+ * The main entry point for processing an action.
+ *
+ * All the various operations that can be performed on a
+ * {@link question_attempt} get channeled through this function, except for
+ * {@link question_attempt::start()} which goes to {@link init_first_step()}.
+ * {@link question_attempt::finish()} becomes an action with im vars
+ * finish => 1, and manual comment/grade becomes an action with im vars
+ * comment => comment text, and mark => ..., max_mark => ... if the question
+ * is graded.
+ *
+ * This method should first determine whether the action is significant. For
+ * example, if no actual action is being performed, but instead the current
+ * responses are being saved, and there has been no change since the last
+ * set of responses that were saved, this the action is not significatn. In
+ * this case, this method should return {@link question_attempt::DISCARD}.
+ * Otherwise it should return {@link question_attempt::KEEP}.
+ *
+ * If the action is significant, this method should also perform any
+ * necessary updates to $pendingstep. For example, it should call
+ * {@link question_attempt_step::set_state()} to set the state that results
+ * from this action, and if this is a grading action, it should call
+ * {@link question_attempt_step::set_fraction()}.
+ *
+ * This method can also call {@link question_attempt_step::set_behaviour_var()} to
+ * store additional infomation. There are two main uses for this. This can
+ * be used to store the result of any randomisation done. It is important to
+ * store the result of randomisation once, and then in future use the same
+ * outcome if the actions are ever replayed. This is how regrading works.
+ * The other use is to cache the result of expensive computations performed
+ * on the raw response data, so that subsequent display and review of the
+ * question does not have to repeat the same expensive computations.
+ *
+ * Often this method is implemented as a dispatching method that examines
+ * the pending step to determine the kind of action being performed, and
+ * then calls a more specific method like {@link process_save()} or
+ * {@link process_comment()}. Look at some of the standard behaviours
+ * for examples.
+ *
+ * @param question_attempt_pending_step $pendingstep a partially initialised step
+ * containing all the information about the action that is being peformed. This
+ * information can be accessed using {@link question_attempt_step::get_behaviour_var()}.
+ * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
+ */
+ public abstract function process_action(question_attempt_pending_step $pendingstep);
+
+ /**
+ * Implementation of processing a manual comment/grade action that should
+ * be suitable for most subclasses.
+ * @param question_attempt_pending_step $pendingstep a partially initialised step
+ * containing all the information about the action that is being peformed.
+ * @return bool either {@link question_attempt::KEEP}
+ */
+ public function process_comment(question_attempt_pending_step $pendingstep) {
+ if (!$this->qa->get_state()->is_finished()) {
+ throw new coding_exception('Cannot manually grade a question before it is finshed.');
+ }
+
+ if ($this->is_same_comment($pendingstep)) {
+ return question_attempt::DISCARD;
+ }
+
+ if ($pendingstep->has_behaviour_var('mark')) {
+ $fraction = $pendingstep->get_behaviour_var('mark') /
+ $pendingstep->get_behaviour_var('maxmark');
+ if ($pendingstep->get_behaviour_var('mark') === '') {
+ $fraction = null;
+ } else if ($fraction > 1 || $fraction < $this->qa->get_min_fraction()) {
+ throw new coding_exception('Score out of range when processing ' .
+ 'a manual grading action.', $pendingstep);
+ }
+ $pendingstep->set_fraction($fraction);
+ }
+
+ $pendingstep->set_state($this->qa->get_state()->corresponding_commented_state(
+ $pendingstep->get_fraction()));
+ return question_attempt::KEEP;
+ }
+
+ /**
+ * @param $comment the comment text to format. If omitted,
+ * $this->qa->get_manual_comment() is used.
+ * @param $commentformat the format of the comment, one of the FORMAT_... constants.
+ * @return string the comment, ready to be output.
+ */
+ public function format_comment($comment = null, $commentformat = null) {
+ $formatoptions = new stdClass();
+ $formatoptions->noclean = true;
+ $formatoptions->para = false;
+
+ if (is_null($comment)) {
+ list($comment, $commentformat) = $this->qa->get_manual_comment();
+ }
+
+ return format_text($comment, $commentformat, $formatoptions);
+ }
+
+ /**
+ * @return string a summary of a manual comment action.
+ * @param unknown_type $step
+ */
+ protected function summarise_manual_comment($step) {
+ $a = new stdClass();
+ if ($step->has_behaviour_var('comment')) {
+ $a->comment = shorten_text(html_to_text($this->format_comment(
+ $step->get_behaviour_var('comment')), 0, false), 200);
+ } else {
+ $a->comment = '';
+ }
+
+ $mark = $step->get_behaviour_var('mark');
+ if (is_null($mark) || $mark === '') {
+ return get_string('commented', 'question', $a->comment);
+ } else {
+ $a->mark = $mark / $step->get_behaviour_var('maxmark') * $this->qa->get_max_mark();
+ return get_string('manuallygraded', 'question', $a);
+ }
+ }
+
+ public function summarise_start($step) {
+ return get_string('started', 'question');
+ }
+
+ public function summarise_finish($step) {
+ return get_string('attemptfinished', 'question');
+ }
+}
+
+
+/**
+ * A subclass of {@link question_behaviour} that implements a save
+ * action that is suitable for most questions that implement the
+ * {@link question_manually_gradable} interface.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class question_behaviour_with_save extends question_behaviour {
+ public function required_question_definition_type() {
+ return 'question_manually_gradable';
+ }
+
+ /**
+ * Work out whether the response in $pendingstep are significantly different
+ * from the last set of responses we have stored.
+ * @param question_attempt_step $pendingstep contains the new responses.
+ * @return bool whether the new response is the same as we already have.
+ */
+ protected function is_same_response(question_attempt_step $pendingstep) {
+ return $this->question->is_same_response(
+ $this->qa->get_last_step()->get_qt_data(), $pendingstep->get_qt_data());
+ }
+
+ /**
+ * Work out whether the response in $pendingstep represent a complete answer
+ * to the question. Normally this will call
+ * {@link question_manually_gradable::is_complete_response}, but some
+ * behaviours, for example the CBM ones, have their own parts to the
+ * response.
+ * @param question_attempt_step $pendingstep contains the new responses.
+ * @return bool whether the new response is complete.
+ */
+ protected function is_complete_response(question_attempt_step $pendingstep) {
+ return $this->question->is_complete_response($pendingstep->get_qt_data());
+ }
+
+ /**
+ * Implementation of processing a save action that should be suitable for
+ * most subclasses.
+ * @param question_attempt_pending_step $pendingstep a partially initialised step
+ * containing all the information about the action that is being peformed.
+ * @return bool either {@link question_attempt::KEEP} or {@link question_attempt::DISCARD}
+ */
+ public function process_save(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ } else if (!$this->qa->get_state()->is_active()) {
+ throw new coding_exception('Question is not active, cannot process_actions.');
+ }
+
+ if ($this->is_same_response($pendingstep)) {
+ return question_attempt::DISCARD;
+ }
+
+ if ($this->is_complete_response($pendingstep)) {
+ $pendingstep->set_state(question_state::$complete);
+ } else {
+ $pendingstep->set_state(question_state::$todo);
+ }
+ return question_attempt::KEEP;
+ }
+
+ public function summarise_submit(question_attempt_step $step) {
+ return get_string('submitted', 'question',
+ $this->question->summarise_response($step->get_qt_data()));
+ }
+
+ public function summarise_save(question_attempt_step $step) {
+ $data = $step->get_submitted_data();
+ if (empty($data)) {
+ return $this->summarise_start($step);
+ }
+ return get_string('saved', 'question',
+ $this->question->summarise_response($step->get_qt_data()));
+ }
+
+
+ public function summarise_finish($step) {
+ $data = $step->get_qt_data();
+ if ($data) {
+ return get_string('attemptfinishedsubmitting', 'question',
+ $this->question->summarise_response($data));
+ }
+ return get_string('attemptfinished', 'question');
+ }
+}
+
+
+/**
+ * This helper class contains the constants and methods required for
+ * manipulating scores for certainly based marking.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class question_cbm {
+ /**#@+ @var integer named constants for the certainty levels. */
+ const LOW = 1;
+ const MED = 2;
+ const HIGH = 3;
+ /**#@-*/
+
+ /** @var array list of all the certainty levels. */
+ public static $certainties = array(self::LOW, self::MED, self::HIGH);
+
+ /**#@+ @var array coefficients used to adjust the fraction based on certainty.. */
+ protected static $factor = array(
+ self::LOW => 0.333333333333333,
+ self::MED => 1.333333333333333,
+ self::HIGH => 3,
+ );
+ protected static $offset = array(
+ self::LOW => 0,
+ self::MED => -0.666666666666667,
+ self::HIGH => -2,
+ );
+ /**#@-*/
+
+ /**
+ * @return int the default certaintly level that should be assuemd if
+ * the student does not choose one.
+ */
+ public static function default_certainty() {
+ return self::LOW;
+ }
+
+ /**
+ * Given a fraction, and a certainly, compute the adjusted fraction.
+ * @param number $fraction the raw fraction for this question.
+ * @param int $certainty one of the certainly level constants.
+ * @return number the adjusted fraction taking the certainly into account.
+ */
+ public static function adjust_fraction($fraction, $certainty) {
+ return self::$offset[$certainty] + self::$factor[$certainty] * $fraction;
+ }
+
+ /**
+ * @param int $certainty one of the LOW/MED/HIGH constants.
+ * @return string a textual desciption of this certainly.
+ */
+ public static function get_string($certainty) {
+ return get_string('certainty' . $certainty, 'qbehaviour_deferredcbm');
+ }
+
+ public static function summary_with_certainty($summary, $certainty) {
+ if (is_null($certainty)) {
+ return $summary;
+ }
+ return $summary . ' [' . self::get_string($certainty) . ']';
+ }
+}
diff --git a/question/behaviour/deferredcbm/behaviour.php b/question/behaviour/deferredcbm/behaviour.php
new file mode 100644
index 00000000000..1c6bcf2e2d1
--- /dev/null
+++ b/question/behaviour/deferredcbm/behaviour.php
@@ -0,0 +1,129 @@
+.
+
+/**
+ * Question behaviour that is like the deferred feedback model, but with
+ * certainly based marking. That is, in addition to the other controls, there are
+ * where the student can indicate how certain they are that their answer is right.
+ *
+ * @package qbehaviour
+ * @subpackage deferredcbm
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../deferredfeedback/behaviour.php');
+
+
+/**
+ * Question behaviour for deferred feedback with certainty based marking.
+ *
+ * The student enters their response during the attempt, along with a certainty,
+ * that is, how sure they are that they are right, and it is saved. Later,
+ * when the whole attempt is finished, their answer is graded. Their degree
+ * of certainty affects their score.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_deferredcbm extends qbehaviour_deferredfeedback {
+ const IS_ARCHETYPAL = true;
+
+ public static function get_unused_display_options() {
+ return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
+ 'rightanswer');
+ }
+
+ public function get_min_fraction() {
+ return question_cbm::adjust_fraction(parent::get_min_fraction(), question_cbm::HIGH);
+ }
+
+ public function get_expected_data() {
+ if ($this->qa->get_state()->is_active()) {
+ return array('certainty' => PARAM_INT);
+ }
+ return parent::get_expected_data();
+ }
+
+ public function get_right_answer_summary() {
+ $summary = parent::get_right_answer_summary();
+ return $summary . ' [' . question_cbm::get_string(question_cbm::HIGH) . ']';
+ }
+
+ public function get_correct_response() {
+ if ($this->qa->get_state()->is_active()) {
+ return array('certainty' => question_cbm::HIGH);
+ }
+ return array();
+ }
+
+ protected function get_our_resume_data() {
+ $lastcertainty = $this->qa->get_last_behaviour_var('certainty');
+ if ($lastcertainty) {
+ return array('-certainty' => $lastcertainty);
+ } else {
+ return array();
+ }
+ }
+
+ protected function is_same_response($pendingstep) {
+ return parent::is_same_response($pendingstep) &&
+ $this->qa->get_last_behaviour_var('certainty') ==
+ $pendingstep->get_behaviour_var('certainty');
+ }
+
+ protected function is_complete_response($pendingstep) {
+ return parent::is_complete_response($pendingstep) &&
+ $pendingstep->has_behaviour_var('certainty');
+ }
+
+ public function process_finish(question_attempt_pending_step $pendingstep) {
+ $status = parent::process_finish($pendingstep);
+ if ($status == question_attempt::KEEP) {
+ $fraction = $pendingstep->get_fraction();
+ if ($this->qa->get_last_step()->has_behaviour_var('certainty')) {
+ $certainty = $this->qa->get_last_step()->get_behaviour_var('certainty');
+ } else {
+ $certainty = question_cbm::default_certainty();
+ $pendingstep->set_behaviour_var('_assumedcertainty', $certainty);
+ }
+ if (!is_null($fraction)) {
+ $pendingstep->set_behaviour_var('_rawfraction', $fraction);
+ $pendingstep->set_fraction(question_cbm::adjust_fraction($fraction, $certainty));
+ }
+ $pendingstep->set_new_response_summary(
+ question_cbm::summary_with_certainty($pendingstep->get_new_response_summary(),
+ $this->qa->get_last_step()->get_behaviour_var('certainty')));
+ }
+ return $status;
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ $summary = parent::summarise_action($step);
+ if ($step->has_behaviour_var('certainty')) {
+ $summary = question_cbm::summary_with_certainty($summary,
+ $step->get_behaviour_var('certainty'));
+ }
+ return $summary;
+ }
+
+ public static function adjust_random_guess_score($fraction) {
+ return question_cbm::adjust_fraction($fraction, question_cbm::default_certainty());
+ }
+}
diff --git a/question/behaviour/deferredcbm/lang/en/qbehaviour_deferredcbm.php b/question/behaviour/deferredcbm/lang/en/qbehaviour_deferredcbm.php
new file mode 100644
index 00000000000..cf266ae24cb
--- /dev/null
+++ b/question/behaviour/deferredcbm/lang/en/qbehaviour_deferredcbm.php
@@ -0,0 +1,32 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_deferredcbm', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage deferredcbm
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['assumingcertainty'] = 'You did not select a certainty. Assuming: {$a}.';
+$string['certainty1'] = 'Not very (less than 67%)';
+$string['certainty2'] = 'Fairly (more than 67%)';
+$string['certainty3'] = 'Very (more than 85%)';
+$string['howcertainareyou'] = 'How certain are you? {$a}';
+$string['markadjustment'] = 'Based on the certainty you expressed, your base mark of {$a->rawmark} was adjusted to {$a->mark}.';
+$string['pluginname'] = 'Deferred feedback with CBM';
diff --git a/question/behaviour/deferredcbm/renderer.php b/question/behaviour/deferredcbm/renderer.php
new file mode 100644
index 00000000000..45a9d06befa
--- /dev/null
+++ b/question/behaviour/deferredcbm/renderer.php
@@ -0,0 +1,100 @@
+.
+
+/**
+ * Defines the renderer for the deferred feedback with certainty based marking
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage deferredcbm
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the deferred
+ * feedback with certainty based marking behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_deferredcbm_renderer extends qbehaviour_renderer {
+ protected function certainly_choices($controlname, $selected, $readonly) {
+ $attributes = array(
+ 'type' => 'radio',
+ 'name' => $controlname,
+ );
+ if ($readonly) {
+ $attributes['disabled'] = 'disabled';
+ }
+
+ $choices = '';
+ foreach (question_cbm::$certainties as $certainty) {
+ $id = $controlname . $certainty;
+ $attributes['id'] = $id;
+ $attributes['value'] = $certainty;
+ if ($selected == $certainty) {
+ $attributes['checked'] = 'checked';
+ } else {
+ unset($attributes['checked']);
+ }
+ $choices .= ' ' . html_writer::empty_tag('input', $attributes) . ' ' .
+ html_writer::tag('label', question_cbm::get_string($certainty),
+ array('for' => $id));
+ }
+ return $choices;
+ }
+
+ public function controls(question_attempt $qa, question_display_options $options) {
+ return html_writer::tag('div', get_string('howcertainareyou', 'qbehaviour_deferredcbm',
+ $this->certainly_choices($qa->get_behaviour_field_name('certainty'),
+ $qa->get_last_behaviour_var('certainty'), $options->readonly)),
+ array('class' => 'certaintychoices'));
+ }
+
+ public function feedback(question_attempt $qa, question_display_options $options) {
+ if (!$options->feedback) {
+ return '';
+ }
+
+ if ($qa->get_state() == question_state::$gaveup || $qa->get_state() ==
+ question_state::$mangaveup) {
+ return '';
+ }
+
+ $feedback = '';
+ if (!$qa->get_last_behaviour_var('certainty') &&
+ $qa->get_last_behaviour_var('_assumedcertainty')) {
+ $feedback .= html_writer::tag('p',
+ get_string('assumingcertainty', 'qbehaviour_deferredcbm',
+ question_cbm::get_string($qa->get_last_behaviour_var('_assumedcertainty'))));
+ }
+
+ if ($options->marks >= question_display_options::MARK_AND_MAX) {
+ $a->rawmark = format_float($qa->get_last_behaviour_var('_rawfraction') *
+ $qa->get_max_mark(), $options->markdp);
+ $a->mark = $qa->format_mark($options->markdp);
+ $feedback .= html_writer::tag('p',
+ get_string('markadjustment', 'qbehaviour_deferredcbm', $a));
+ }
+
+ return $feedback;
+ }
+}
\ No newline at end of file
diff --git a/question/behaviour/deferredcbm/simpletest/testwalkthrough.php b/question/behaviour/deferredcbm/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..5233b7f4f3f
--- /dev/null
+++ b/question/behaviour/deferredcbm/simpletest/testwalkthrough.php
@@ -0,0 +1,272 @@
+.
+
+/**
+ * This file contains tests that walks a question through the deferred feedback
+ * with certainty base marking behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage deferredcbm
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the deferred feedback with certainty base marking behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_deferredcbm_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_deferred_cbm_truefalse_high_certainty() {
+
+ // Create a true-false question with correct answer true.
+ $tf = test_question_maker::make_question('truefalse', 'true');
+ $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($tf),
+ $this->get_contains_tf_true_radio_expectation(true, false),
+ $this->get_contains_tf_false_radio_expectation(true, false),
+ $this->get_contains_cbm_radio_expectation(1, true, false),
+ $this->get_contains_cbm_radio_expectation(2, true, false),
+ $this->get_contains_cbm_radio_expectation(3, true, false),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process the data extracted for this question.
+ $this->process_submission(array('answer' => 1, '-certainty' => 3));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_tf_true_radio_expectation(true, true),
+ $this->get_contains_cbm_radio_expectation(3, true, true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process the same data again, check it does not create a new step.
+ $numsteps = $this->get_step_count();
+ $this->process_submission(array('answer' => 1, '-certainty' => 3));
+ $this->check_step_count($numsteps);
+
+ // Process different data, check it creates a new step.
+ $this->process_submission(array('answer' => 1, '-certainty' => 1));
+ $this->check_step_count($numsteps + 1);
+ $this->check_current_state(question_state::$complete);
+
+ // Change back, check it creates a new step.
+ $this->process_submission(array('answer' => 1, '-certainty' => 3));
+ $this->check_step_count($numsteps + 2);
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(2);
+ $this->check_current_output(
+ $this->get_contains_tf_true_radio_expectation(false, true),
+ $this->get_contains_cbm_radio_expectation(3, false, true),
+ $this->get_contains_correct_expectation());
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 1);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+ $this->check_current_output(new PatternExpectation('/' .
+ preg_quote('Not good enough!') . '/'));
+
+ // Now change the correct answer to the question, and regrade.
+ $tf->rightanswer = false;
+ $this->quba->regrade_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+ $autogradedstep = $this->get_step($this->get_step_count() - 2);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), -2, 0.0000001);
+ }
+
+ public function test_deferred_cbm_truefalse_low_certainty() {
+
+ // Create a true-false question with correct answer true.
+ $tf = test_question_maker::make_question('truefalse', 'true');
+ $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_cbm_radio_expectation(1, true, false),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit ansewer with low certainty.
+ $this->process_submission(array('answer' => 1, '-certainty' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output($this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_cbm_radio_expectation(1, true, true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(0.6666667);
+ $this->check_current_output($this->get_contains_correct_expectation(),
+ $this->get_contains_cbm_radio_expectation(1, false, true));
+ $this->assertEqual(get_string('true', 'qtype_truefalse') . ' [' .
+ question_cbm::get_string(1) . ']',
+ $this->quba->get_response_summary($this->slot));
+ }
+
+ public function test_deferred_cbm_truefalse_default_certainty() {
+
+ // Create a true-false question with correct answer true.
+ $tf = test_question_maker::make_question('truefalse', 'true');
+ $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_cbm_radio_expectation(1, true, false),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit ansewer with low certainty and finish the attempt.
+ $this->process_submission(array('answer' => 1));
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $qa = $this->quba->get_question_attempt($this->slot);
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(0.6666667);
+ $this->check_current_output($this->get_contains_correct_expectation(),
+ $this->get_contains_cbm_radio_expectation(1, false, false),
+ new PatternExpectation('/' . preg_quote(
+ get_string('assumingcertainty', 'qbehaviour_deferredcbm',
+ question_cbm::get_string(
+ $qa->get_last_behaviour_var('_assumedcertainty')))) . '/'));
+ $this->assertEqual(get_string('true', 'qtype_truefalse'),
+ $this->quba->get_response_summary($this->slot));
+ }
+
+ public function test_deferredcbm_resume_multichoice_single() {
+
+ // Create a multiple-choice question.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+
+ // Attempt it getting it wrong.
+ $this->start_attempt_at_question($mc, 'deferredcbm', 3);
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+ $this->process_submission(array('answer' => $wrongindex, '-certainty' => 2));
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(-3.3333333);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+ $this->get_contains_cbm_radio_expectation(2, false, true),
+ $this->get_contains_incorrect_expectation());
+ $this->assertEqual('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
+ $this->quba->get_right_answer_summary($this->slot));
+ $this->assertPattern('/' . preg_quote($mc->questiontext) . '/',
+ $this->quba->get_question_summary($this->slot));
+ $this->assertPattern('/(B|C) \[' . preg_quote(question_cbm::get_string(2)) . '\]/',
+ $this->quba->get_response_summary($this->slot));
+
+ // Save the old attempt.
+ $oldqa = $this->quba->get_question_attempt($this->slot);
+
+ // Reinitialise.
+ $this->setUp();
+ $this->quba->set_preferred_behaviour('deferredcbm');
+ $this->slot = $this->quba->add_question($mc, 3);
+ $this->quba->start_question_based_on($this->slot, $oldqa);
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_cbm_radio_expectation(2, true, true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_does_not_contain_correctness_expectation());
+ $this->assertEqual('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
+ $this->quba->get_right_answer_summary($this->slot));
+ $this->assertPattern('/' . preg_quote($mc->questiontext) . '/',
+ $this->quba->get_question_summary($this->slot));
+ $this->assertNull($this->quba->get_response_summary($this->slot));
+
+ // Now get it right.
+ $this->process_submission(array('answer' => $rightindex, '-certainty' => 3));
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(3);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_cbm_radio_expectation(3, false, true),
+ $this->get_contains_correct_expectation());
+ $this->assertPattern('/(A) \[' . preg_quote(question_cbm::get_string(3)) . '\]/',
+ $this->quba->get_response_summary($this->slot));
+ }
+
+ public function test_deferred_cbm_truefalse_no_certainty_feedback_when_not_answered() {
+
+ // Create a true-false question with correct answer true.
+ $tf = test_question_maker::make_question('truefalse', 'true');
+ $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_cbm_radio_expectation(1, true, false),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Finish without answering.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gaveup);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ new NoPatternExpectation('/class=\"im-feedback/'));
+ }
+}
diff --git a/question/behaviour/deferredfeedback/behaviour.php b/question/behaviour/deferredfeedback/behaviour.php
new file mode 100644
index 00000000000..8bc59fc7a70
--- /dev/null
+++ b/question/behaviour/deferredfeedback/behaviour.php
@@ -0,0 +1,123 @@
+.
+
+/**
+ * Question behaviour for the case when the student's answer is just
+ * saved until they submit the whole attempt, and then it is graded.
+ *
+ * @package qbehaviour
+ * @subpackage deferredfeedback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Question behaviour for deferred feedback.
+ *
+ * The student enters their response during the attempt, and it is saved. Later,
+ * when the whole attempt is finished, their answer is graded.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_deferredfeedback extends question_behaviour_with_save {
+ const IS_ARCHETYPAL = true;
+
+ public function required_question_definition_type() {
+ return 'question_automatically_gradable';
+ }
+
+ public static function get_unused_display_options() {
+ return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
+ 'rightanswer');
+ }
+
+ public function get_min_fraction() {
+ return $this->question->get_min_fraction();
+ }
+
+ public function get_right_answer_summary() {
+ return $this->question->get_right_answer_summary();
+ }
+
+ public function process_action(question_attempt_pending_step $pendingstep) {
+ if ($pendingstep->has_behaviour_var('comment')) {
+ return $this->process_comment($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('finish')) {
+ return $this->process_finish($pendingstep);
+ } else {
+ return $this->process_save($pendingstep);
+ }
+ }
+
+ /*
+ * Like the parent method, except that when a respones is gradable, but not
+ * completely, we move it to the invalid state.
+ *
+ * TODO refactor, to remove the duplication.
+ */
+ public function process_save(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ } else if (!$this->qa->get_state()->is_active()) {
+ throw new coding_exception('Question is not active, cannot process_actions.');
+ }
+
+ if ($this->is_same_response($pendingstep)) {
+ return question_attempt::DISCARD;
+ }
+
+ if ($this->is_complete_response($pendingstep)) {
+ $pendingstep->set_state(question_state::$complete);
+ } else if ($this->question->is_gradable_response($pendingstep->get_qt_data())) {
+ $pendingstep->set_state(question_state::$invalid);
+ } else {
+ $pendingstep->set_state(question_state::$todo);
+ }
+ return question_attempt::KEEP;
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ if ($step->has_behaviour_var('comment')) {
+ return $this->summarise_manual_comment($step);
+ } else if ($step->has_behaviour_var('finish')) {
+ return $this->summarise_finish($step);
+ } else {
+ return $this->summarise_save($step);
+ }
+ }
+
+ public function process_finish(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ $response = $this->qa->get_last_step()->get_qt_data();
+ if (!$this->question->is_gradable_response($response)) {
+ $pendingstep->set_state(question_state::$gaveup);
+ } else {
+ list($fraction, $state) = $this->question->grade_response($response);
+ $pendingstep->set_fraction($fraction);
+ $pendingstep->set_state($state);
+ }
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+ return question_attempt::KEEP;
+ }
+}
diff --git a/question/behaviour/deferredfeedback/lang/en/qbehaviour_deferredfeedback.php b/question/behaviour/deferredfeedback/lang/en/qbehaviour_deferredfeedback.php
new file mode 100644
index 00000000000..61be24286d3
--- /dev/null
+++ b/question/behaviour/deferredfeedback/lang/en/qbehaviour_deferredfeedback.php
@@ -0,0 +1,26 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_deferredfeedback', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage deferredfeedback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Deferred feedback';
diff --git a/question/behaviour/deferredfeedback/renderer.php b/question/behaviour/deferredfeedback/renderer.php
new file mode 100644
index 00000000000..718f3eedd1e
--- /dev/null
+++ b/question/behaviour/deferredfeedback/renderer.php
@@ -0,0 +1,38 @@
+.
+
+/**
+ * Defines the renderer for the deferred feedback behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage deferredfeedback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the deferred
+ * feedback behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_deferredfeedback_renderer extends qbehaviour_renderer {
+}
diff --git a/question/behaviour/deferredfeedback/simpletest/testwalkthrough.php b/question/behaviour/deferredfeedback/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..65963053180
--- /dev/null
+++ b/question/behaviour/deferredfeedback/simpletest/testwalkthrough.php
@@ -0,0 +1,213 @@
+.
+
+/**
+ * This file contains tests that walks a question through the deferred feedback
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage deferredfeedback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the deferred feedback behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_deferredfeedback_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_deferredfeedback_feedback_truefalse() {
+
+ // Create a true-false question with correct answer true.
+ $tf = test_question_maker::make_question('truefalse', 'true');
+ $this->start_attempt_at_question($tf, 'deferredfeedback', 2);
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output($this->get_contains_question_text_expectation($tf),
+ $this->get_does_not_contain_feedback_expectation());
+ $this->assertEqual(get_string('true', 'qtype_truefalse'),
+ $this->quba->get_right_answer_summary($this->slot));
+ $this->assertPattern('/' . preg_quote($tf->questiontext) . '/',
+ $this->quba->get_question_summary($this->slot));
+ $this->assertNull($this->quba->get_response_summary($this->slot));
+
+ // Process a true answer and check the expected result.
+ $this->process_submission(array('answer' => 1));
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output($this->get_contains_tf_true_radio_expectation(true, true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process the same data again, check it does not create a new step.
+ $numsteps = $this->get_step_count();
+ $this->process_submission(array('answer' => 1));
+ $this->check_step_count($numsteps);
+
+ // Process different data, check it creates a new step.
+ $this->process_submission(array('answer' => 0));
+ $this->check_step_count($numsteps + 1);
+ $this->check_current_state(question_state::$complete);
+
+ // Change back, check it creates a new step.
+ $this->process_submission(array('answer' => 1));
+ $this->check_step_count($numsteps + 2);
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(2);
+ $this->check_current_output($this->get_contains_correct_expectation(),
+ $this->get_contains_tf_true_radio_expectation(false, true),
+ new PatternExpectation('/class="r0 correct"/'));
+ $this->assertEqual(get_string('true', 'qtype_truefalse'),
+ $this->quba->get_response_summary($this->slot));
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 1);
+
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+
+ // Now change the correct answer to the question, and regrade.
+ $tf->rightanswer = false;
+ $this->quba->regrade_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+
+ $autogradedstep = $this->get_step($this->get_step_count() - 2);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), 0, 0.0000001);
+ }
+
+ public function test_deferredfeedback_feedback_multichoice_single() {
+
+ // Create a true-false question with correct answer true.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
+
+ // Start a deferred feedback attempt and add the question to it.
+ $rightindex = $this->get_mc_right_answer_index($mc);
+
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process the data extracted for this question.
+ $this->process_submission(array('answer' => $rightindex));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, true, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, true, false),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(3);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_correct_expectation());
+
+ // Now change the correct answer to the question, and regrade.
+ $mc->answers[13]->fraction = -0.33333333;
+ $mc->answers[14]->fraction = 1;
+ $this->quba->regrade_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(-1);
+ $this->check_current_output(
+ $this->get_contains_incorrect_expectation());
+ }
+
+ public function test_deferredfeedback_resume_multichoice_single() {
+
+ // Create a multiple-choice question.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+
+ // Attempt it getting it wrong.
+ $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+ $this->process_submission(array('answer' => $wrongindex));
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(-1);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+ $this->get_contains_incorrect_expectation());
+
+ // Save the old attempt.
+ $oldqa = $this->quba->get_question_attempt($this->slot);
+
+ // Reinitialise.
+ $this->setUp();
+ $this->quba->set_preferred_behaviour('deferredfeedback');
+ $this->slot = $this->quba->add_question($mc, 3);
+ $this->quba->start_question_based_on($this->slot, $oldqa);
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_does_not_contain_correctness_expectation());
+
+ // Now get it right.
+ $this->process_submission(array('answer' => $rightindex));
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(3);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_correct_expectation());
+ }
+}
diff --git a/question/behaviour/immediatecbm/behaviour.php b/question/behaviour/immediatecbm/behaviour.php
new file mode 100644
index 00000000000..a188c2c871a
--- /dev/null
+++ b/question/behaviour/immediatecbm/behaviour.php
@@ -0,0 +1,155 @@
+.
+
+/**
+ * Question behaviour where the student can submit questions one at a
+ * time for immediate feedback, with certainty based marking.
+ *
+ * @package qbehaviour
+ * @subpackage immediatecbm
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../immediatefeedback/behaviour.php');
+
+
+/**
+ * Question behaviour for immediate feedback with CBM.
+ *
+ * Each question has a submit button next to it along with some radio buttons
+ * to input a certainly, that is, how sure they are that they are right.
+ * The student can submit their answer at any time for immediate feedback.
+ * Once the qustion is submitted, it is not possible for the student to change
+ * their answer any more. The student's degree of certainly affects their score.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_immediatecbm extends qbehaviour_immediatefeedback {
+ const IS_ARCHETYPAL = true;
+
+ public function get_min_fraction() {
+ return question_cbm::adjust_fraction(parent::get_min_fraction(), question_cbm::HIGH);
+ }
+
+ public function get_expected_data() {
+ if ($this->qa->get_state()->is_active()) {
+ return array(
+ 'submit' => PARAM_BOOL,
+ 'certainty' => PARAM_INT,
+ );
+ }
+ return parent::get_expected_data();
+ }
+
+ public function get_right_answer_summary() {
+ $summary = parent::get_right_answer_summary();
+ return question_cbm::summary_with_certainty($summary, question_cbm::HIGH);
+ }
+
+ public function get_correct_response() {
+ if ($this->qa->get_state()->is_active()) {
+ return array('certainty' => question_cbm::HIGH);
+ }
+ return array();
+ }
+
+ protected function get_our_resume_data() {
+ $lastcertainty = $this->qa->get_last_behaviour_var('certainty');
+ if ($lastcertainty) {
+ return array('-certainty' => $lastcertainty);
+ } else {
+ return array();
+ }
+ }
+
+ protected function is_same_response($pendingstep) {
+ return parent::is_same_response($pendingstep) &&
+ $this->qa->get_last_behaviour_var('certainty') ==
+ $pendingstep->get_behaviour_var('certainty');
+ }
+
+ protected function is_complete_response($pendingstep) {
+ return parent::is_complete_response($pendingstep) &&
+ $pendingstep->has_behaviour_var('certainty');
+ }
+
+ public function process_submit(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ if (!$this->qa->get_question()->is_gradable_response($pendingstep->get_qt_data()) ||
+ !$pendingstep->has_behaviour_var('certainty')) {
+ $pendingstep->set_state(question_state::$invalid);
+ return question_attempt::KEEP;
+ }
+
+ return $this->do_grading($pendingstep, $pendingstep);
+ }
+
+ public function process_finish(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ $laststep = $this->qa->get_last_step();
+ return $this->do_grading($laststep, $pendingstep);
+ }
+
+ protected function do_grading(question_attempt_step $responsesstep,
+ question_attempt_pending_step $pendingstep) {
+ if (!$this->question->is_gradable_response($responsesstep->get_qt_data())) {
+ $pendingstep->set_state(question_state::$gaveup);
+
+ } else {
+ $response = $responsesstep->get_qt_data();
+ list($fraction, $state) = $this->question->grade_response($response);
+
+ if ($responsesstep->has_behaviour_var('certainty')) {
+ $certainty = $responsesstep->get_behaviour_var('certainty');
+ } else {
+ $certainty = question_cbm::default_certainty();
+ $pendingstep->set_behaviour_var('_assumedcertainty', $certainty);
+ }
+
+ $pendingstep->set_behaviour_var('_rawfraction', $fraction);
+ $pendingstep->set_fraction(question_cbm::adjust_fraction($fraction, $certainty));
+ $pendingstep->set_state($state);
+ $pendingstep->set_new_response_summary(question_cbm::summary_with_certainty(
+ $this->question->summarise_response($response),
+ $responsesstep->get_behaviour_var('certainty')));
+ }
+ return question_attempt::KEEP;
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ $summary = parent::summarise_action($step);
+ if ($step->has_behaviour_var('certainty')) {
+ $summary = question_cbm::summary_with_certainty($summary,
+ $step->get_behaviour_var('certainty'));
+ }
+ return $summary;
+ }
+
+ public static function adjust_random_guess_score($fraction) {
+ return question_cbm::adjust_fraction($fraction, question_cbm::default_certainty());
+ }
+}
diff --git a/question/behaviour/immediatecbm/lang/en/qbehaviour_immediatecbm.php b/question/behaviour/immediatecbm/lang/en/qbehaviour_immediatecbm.php
new file mode 100644
index 00000000000..cc0fe79fccf
--- /dev/null
+++ b/question/behaviour/immediatecbm/lang/en/qbehaviour_immediatecbm.php
@@ -0,0 +1,27 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_immediatecbm', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage immediatecbm
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pleaseselectacertainty'] = 'Please select a certainty.';
+$string['pluginname'] = 'Immediate feedback with CBM';
diff --git a/question/behaviour/immediatecbm/renderer.php b/question/behaviour/immediatecbm/renderer.php
new file mode 100644
index 00000000000..cc0811f71c9
--- /dev/null
+++ b/question/behaviour/immediatecbm/renderer.php
@@ -0,0 +1,51 @@
+.
+
+/**
+ * Defines the renderer for the immediate feedback with CBM behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage immediatecbm
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../deferredcbm/renderer.php');
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the immediate
+ * feedback with CBM behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_immediatecbm_renderer extends qbehaviour_deferredcbm_renderer {
+ public function controls(question_attempt $qa, question_display_options $options) {
+ $output = parent::controls($qa, $options);
+ if ($qa->get_state() == question_state::$invalid &&
+ !$qa->get_last_step()->has_behaviour_var('certainty')) {
+ $output .= html_writer::tag('div',
+ get_string('pleaseselectacertainty', 'qbehaviour_immediatecbm'),
+ array('class' => 'validationerror'));
+ }
+ $output .= $this->submit_button($qa, $options);
+ return $output;
+ }
+}
diff --git a/question/behaviour/immediatecbm/simpletest/testwalkthrough.php b/question/behaviour/immediatecbm/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..7724a84de80
--- /dev/null
+++ b/question/behaviour/immediatecbm/simpletest/testwalkthrough.php
@@ -0,0 +1,291 @@
+.
+
+/**
+ * This file contains tests that walks a question through the immediate cbm
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage immediatecbm
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the immediate cbm behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_immediatecbm_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_immediatecbm_feedback_multichoice_right() {
+
+ // Create a true-false question with correct answer true.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $this->start_attempt_at_question($mc, 'immediatecbm');
+
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+ $this->assertEqual('A [' . question_cbm::get_string(question_cbm::HIGH) . ']',
+ $this->quba->get_right_answer_summary($this->slot));
+ $this->assertPattern('/' . preg_quote($mc->questiontext) . '/',
+ $this->quba->get_question_summary($this->slot));
+ $this->assertNull($this->quba->get_response_summary($this->slot));
+
+ // Save the wrong answer.
+ $this->process_submission(array('answer' => $wrongindex, '-certainty' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit the right answer.
+ $this->process_submission(
+ array('answer' => $rightindex, '-certainty' => 2, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(2/3);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_correct_expectation());
+ $this->assertEqual('A [' . question_cbm::get_string(2) . ']',
+ $this->quba->get_response_summary($this->slot));
+
+ $numsteps = $this->get_step_count();
+
+ // Finish the attempt - should not need to add a new state.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->assertEqual($numsteps, $this->get_step_count());
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(2/3);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_correct_expectation());
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 0.5);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(0.5);
+ $this->check_current_output(
+ $this->get_contains_partcorrect_expectation(),
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+
+ // Now change the correct answer to the question, and regrade.
+ $mc->answers[13]->fraction = -0.33333333;
+ $mc->answers[15]->fraction = 1;
+ $this->quba->regrade_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(0.5);
+ $this->check_current_output(
+ $this->get_contains_partcorrect_expectation());
+
+ $autogradedstep = $this->get_step($this->get_step_count() - 2);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), -10/9, 0.0000001);
+ }
+
+ public function test_immediatecbm_feedback_multichoice_try_to_submit_blank() {
+
+ // Create a true-false question with correct answer true.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $this->start_attempt_at_question($mc, 'immediatecbm');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit nothing.
+ $this->process_submission(array('-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$invalid);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_validation_error_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gaveup);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation(0, false, false),
+ $this->get_contains_mc_radio_expectation(1, false, false),
+ $this->get_contains_mc_radio_expectation(2, false, false));
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 0.5);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(0.5);
+ $this->check_current_output(
+ $this->get_contains_partcorrect_expectation(),
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+ }
+
+ public function test_immediatecbm_feedback_shortanswer_try_to_submit_no_certainty() {
+
+ // Create a short answer question with correct answer true.
+ $sa = test_question_maker::make_a_shortanswer_question();
+ $this->start_attempt_at_question($sa, 'immediatecbm');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit with certainty missing.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+ // Verify.
+ $this->check_current_state(question_state::$invalid);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_validation_error_expectation());
+
+ // Now get it right.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'frog', '-certainty' => 3));
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_does_not_contain_validation_error_expectation());
+ }
+
+ public function test_immediatecbm_feedback_multichoice_wrong_on_finish() {
+
+ // Create a true-false question with correct answer true.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $this->start_attempt_at_question($mc, 'immediatecbm');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+
+ // Save the wrong answer.
+ $this->process_submission(array('answer' => $wrongindex, '-certainty' => 3));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(-3);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_incorrect_expectation());
+ }
+
+ public function test_immediatecbm_cbm_truefalse_no_certainty_feedback_when_not_answered() {
+
+ // Create a true-false question with correct answer true.
+ $tf = test_question_maker::make_question('truefalse', 'true');
+ $this->start_attempt_at_question($tf, 'deferredcbm', 2);
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_contains_cbm_radio_expectation(1, true, false),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Finish without answering.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gaveup);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ new NoPatternExpectation('/class=\"im-feedback/'));
+ }
+}
diff --git a/question/behaviour/immediatefeedback/behaviour.php b/question/behaviour/immediatefeedback/behaviour.php
new file mode 100644
index 00000000000..21238e3a844
--- /dev/null
+++ b/question/behaviour/immediatefeedback/behaviour.php
@@ -0,0 +1,134 @@
+.
+
+/**
+ * Question behaviour where the student can submit questions one at a
+ * time for immediate feedback.
+ *
+ * @package qbehaviour
+ * @subpackage immediatefeedback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Question behaviour for immediate feedback.
+ *
+ * Each question has a submit button next to it which the student can use to
+ * submit it. Once the qustion is submitted, it is not possible for the
+ * student to change their answer any more, but the student gets full feedback
+ * straight away.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_immediatefeedback extends question_behaviour_with_save {
+ const IS_ARCHETYPAL = true;
+
+ public function required_question_definition_type() {
+ return 'question_automatically_gradable';
+ }
+
+ public function get_min_fraction() {
+ return $this->question->get_min_fraction();
+ }
+
+ public function get_expected_data() {
+ if ($this->qa->get_state()->is_active()) {
+ return array(
+ 'submit' => PARAM_BOOL,
+ );
+ }
+ return parent::get_expected_data();
+ }
+
+ public function get_right_answer_summary() {
+ return $this->question->get_right_answer_summary();
+ }
+
+ public function process_action(question_attempt_pending_step $pendingstep) {
+ if ($pendingstep->has_behaviour_var('comment')) {
+ return $this->process_comment($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('submit')) {
+ return $this->process_submit($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('finish')) {
+ return $this->process_finish($pendingstep);
+ } else {
+ return $this->process_save($pendingstep);
+ }
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ if ($step->has_behaviour_var('comment')) {
+ return $this->summarise_manual_comment($step);
+ } else if ($step->has_behaviour_var('finish')) {
+ return $this->summarise_finish($step);
+ } else if ($step->has_behaviour_var('submit')) {
+ return $this->summarise_submit($step);
+ } else {
+ return $this->summarise_save($step);
+ }
+ }
+
+ public function process_submit(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ if (!$this->is_complete_response($pendingstep)) {
+ $pendingstep->set_state(question_state::$invalid);
+
+ } else {
+ $response = $pendingstep->get_qt_data();
+ list($fraction, $state) = $this->question->grade_response($response);
+ $pendingstep->set_fraction($fraction);
+ $pendingstep->set_state($state);
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+ }
+ return question_attempt::KEEP;
+ }
+
+ public function process_finish(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ $response = $this->qa->get_last_step()->get_qt_data();
+ if (!$this->question->is_gradable_response($response)) {
+ $pendingstep->set_state(question_state::$gaveup);
+
+ } else {
+ list($fraction, $state) = $this->question->grade_response($response);
+ $pendingstep->set_fraction($fraction);
+ $pendingstep->set_state($state);
+ }
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+ return question_attempt::KEEP;
+ }
+
+ public function process_save(question_attempt_pending_step $pendingstep) {
+ $status = parent::process_save($pendingstep);
+ if ($status == question_attempt::KEEP &&
+ $pendingstep->get_state() == question_state::$complete) {
+ $pendingstep->set_state(question_state::$todo);
+ }
+ return $status;
+ }
+}
diff --git a/question/behaviour/immediatefeedback/lang/en/qbehaviour_immediatefeedback.php b/question/behaviour/immediatefeedback/lang/en/qbehaviour_immediatefeedback.php
new file mode 100644
index 00000000000..6c4f0235a4f
--- /dev/null
+++ b/question/behaviour/immediatefeedback/lang/en/qbehaviour_immediatefeedback.php
@@ -0,0 +1,26 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_immediatefeedback', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage immediatefeedback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Immediate feedback';
diff --git a/question/behaviour/immediatefeedback/renderer.php b/question/behaviour/immediatefeedback/renderer.php
new file mode 100644
index 00000000000..539f5eea957
--- /dev/null
+++ b/question/behaviour/immediatefeedback/renderer.php
@@ -0,0 +1,41 @@
+.
+
+/**
+ * Defines the renderer for the immediate feedback behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage immediatefeedback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the immediate
+ * feedback behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_immediatefeedback_renderer extends qbehaviour_renderer {
+ public function controls(question_attempt $qa, question_display_options $options) {
+ return $this->submit_button($qa, $options);
+ }
+}
diff --git a/question/behaviour/immediatefeedback/simpletest/testwalkthrough.php b/question/behaviour/immediatefeedback/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..4c349d867a6
--- /dev/null
+++ b/question/behaviour/immediatefeedback/simpletest/testwalkthrough.php
@@ -0,0 +1,245 @@
+.
+
+/**
+ * This file contains tests that walks a question through the immediate feedback
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage immediatefeedback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the immediate feedback behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_immediatefeedback_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_immediatefeedback_feedback_multichoice_right() {
+
+ // Create a true-false question with correct answer true.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $this->start_attempt_at_question($mc, 'immediatefeedback');
+
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Save the wrong answer.
+ $this->process_submission(array('answer' => $wrongindex));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit the right answer.
+ $this->process_submission(array('answer' => $rightindex, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_correct_expectation());
+ $this->assertEqual('A',
+ $this->quba->get_response_summary($this->slot));
+
+ $numsteps = $this->get_step_count();
+
+ // Now try to save again - as if the user clicked next in the quiz.
+ $this->process_submission(array('answer' => $rightindex));
+
+ // Verify.
+ $this->assertEqual($numsteps, $this->get_step_count());
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_correct_expectation());
+
+ // Finish the attempt - should not need to add a new state.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->assertEqual($numsteps, $this->get_step_count());
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_correct_expectation());
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 0.5);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(0.5);
+ $this->check_current_output(
+ $this->get_contains_partcorrect_expectation(),
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+
+ // Now change the correct answer to the question, and regrade.
+ $mc->answers[13]->fraction = -0.33333333;
+ $mc->answers[15]->fraction = 1;
+ $this->quba->regrade_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(0.5);
+ $this->check_current_output(
+ $this->get_contains_partcorrect_expectation());
+
+ $autogradedstep = $this->get_step($this->get_step_count() - 2);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), -0.3333333, 0.0000001);
+ }
+
+ public function test_immediatefeedback_feedback_multichoice_try_to_submit_blank() {
+
+ // Create a true-false question with correct answer true.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $this->start_attempt_at_question($mc, 'immediatefeedback');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Submit nothing.
+ $this->process_submission(array('-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$invalid);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_contains_validation_error_expectation());
+ $this->assertNull($this->quba->get_response_summary($this->slot));
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gaveup);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation(0, false, false),
+ $this->get_contains_mc_radio_expectation(1, false, false),
+ $this->get_contains_mc_radio_expectation(2, false, false));
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 0.5);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(0.5);
+ $this->check_current_output(
+ $this->get_contains_partcorrect_expectation(),
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+ }
+
+ public function test_immediatefeedback_feedback_multichoice_wrong_on_finish() {
+
+ // Create a true-false question with correct answer true.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $this->start_attempt_at_question($mc, 'immediatefeedback');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation());
+
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+
+ // Save the wrong answer.
+ $this->process_submission(array('answer' => $wrongindex));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(-0.3333333);
+ $this->check_current_output(
+ $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_incorrect_expectation());
+ $this->assertPattern('/B|C/',
+ $this->quba->get_response_summary($this->slot));
+ }
+}
diff --git a/question/behaviour/informationitem/behaviour.php b/question/behaviour/informationitem/behaviour.php
new file mode 100644
index 00000000000..8c2cefed705
--- /dev/null
+++ b/question/behaviour/informationitem/behaviour.php
@@ -0,0 +1,116 @@
+.
+
+/**
+ * This behaviour is for informaiton items.
+ *
+ * @package qbehaviour
+ * @subpackage informationitem
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Question behaviour informaiton items.
+ *
+ * For example for the 'Description' 'Question type'. There is no grade,
+ * and the question type is marked complete the first time the user navigates
+ * away from a page that contains that question.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_informationitem extends question_behaviour {
+
+ public function required_question_definition_type() {
+ return 'question_definition';
+ }
+
+ public function get_expected_data() {
+ if ($this->qa->get_state() == question_state::$todo) {
+ return array('seen' => PARAM_BOOL);
+ }
+ return parent::get_expected_data();
+ }
+
+ public function get_correct_response() {
+ if ($this->qa->get_state() == question_state::$todo) {
+ return array('seen' => 1);
+ }
+ return array();
+ }
+
+ public function adjust_display_options(question_display_options $options) {
+ parent::adjust_display_options($options);
+
+ $options->marks = question_display_options::HIDDEN;
+
+ // At the moment, the code exists to process a manual comment on an
+ // information item, but we don't display the UI unless there is already
+ // a comment.
+ if (!$this->qa->get_state()->is_commented()) {
+ $options->manualcomment = question_display_options::HIDDEN;
+ }
+ }
+
+ public function get_state_string($showcorrectness) {
+ return '';
+ }
+
+ public function process_action(question_attempt_pending_step $pendingstep) {
+ if ($pendingstep->has_behaviour_var('comment')) {
+ return $this->process_comment($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('finish')) {
+ return $this->process_finish($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('seen')) {
+ return $this->process_seen($pendingstep);
+ } else {
+ return question_attempt::DISCARD;
+ }
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ if ($step->has_behaviour_var('comment')) {
+ return $this->summarise_manual_comment($step);
+ } else if ($step->has_behaviour_var('finish')) {
+ return $this->summarise_finish($step);
+ } else if ($step->has_behaviour_var('seen')) {
+ return get_string('seen', 'qbehaviour_informationitem');
+ }
+ return $this->summarise_start($step);
+ }
+
+ public function process_comment(question_attempt_pending_step $pendingstep) {
+ if ($pendingstep->has_behaviour_var('mark')) {
+ throw new coding_exception('Information items cannot be graded.');
+ }
+ return parent::process_comment($pendingstep);
+ }
+
+ public function process_finish(question_attempt_pending_step $pendingstep) {
+ $pendingstep->set_state(question_state::$finished);
+ return question_attempt::KEEP;
+ }
+
+ public function process_seen(question_attempt_pending_step $pendingstep) {
+ $pendingstep->set_state(question_state::$complete);
+ return question_attempt::KEEP;
+ }
+}
diff --git a/question/behaviour/informationitem/lang/en/qbehaviour_informationitem.php b/question/behaviour/informationitem/lang/en/qbehaviour_informationitem.php
new file mode 100644
index 00000000000..8fef1d73acd
--- /dev/null
+++ b/question/behaviour/informationitem/lang/en/qbehaviour_informationitem.php
@@ -0,0 +1,27 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_informationitem', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage informationitem
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'behaviour for information items';
+$string['seen'] = 'Seen';
\ No newline at end of file
diff --git a/question/behaviour/informationitem/renderer.php b/question/behaviour/informationitem/renderer.php
new file mode 100644
index 00000000000..29c5f8c0829
--- /dev/null
+++ b/question/behaviour/informationitem/renderer.php
@@ -0,0 +1,50 @@
+.
+
+/**
+ * Defines the renderer the information item behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage informationitem
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the information
+ * item behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_informationitem_renderer extends qbehaviour_renderer {
+ public function controls(question_attempt $qa, question_display_options $options) {
+ if ($qa->get_state() != question_state::$todo) {
+ return '';
+ }
+
+ // Hidden input to move the question into the complete state.
+ return html_writer::empty_tag('input', array(
+ 'type' => 'hidden',
+ 'name' => $qa->get_behaviour_field_name('seen'),
+ 'value' => 1,
+ ));
+ }
+}
diff --git a/question/behaviour/informationitem/simpletest/testwalkthrough.php b/question/behaviour/informationitem/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..3b14ff8dc64
--- /dev/null
+++ b/question/behaviour/informationitem/simpletest/testwalkthrough.php
@@ -0,0 +1,87 @@
+.
+
+/**
+ * This file contains tests that walks a question through the information item
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage informationitem
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the information item behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_informationitem_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_informationitem_feedback_description() {
+
+ // Create a true-false question with correct answer true.
+ $description = test_question_maker::make_a_description_question();
+ $this->start_attempt_at_question($description, 'deferredfeedback');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output($this->get_contains_question_text_expectation($description),
+ new ContainsTagWithAttributes('input', array('type' => 'hidden',
+ 'name' => $this->quba->get_field_prefix($this->slot) . '-seen', 'value' => 1)),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process a submission indicating this question has been seen.
+ $this->process_submission(array('-seen' => 1));
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output($this->get_does_not_contain_correctness_expectation(),
+ new NoPatternExpectation(
+ '/type=\"hidden\"[^>]*name=\"[^"]*seen\"|name=\"[^"]*seen\"[^>]*type=\"hidden\"/'),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$finished);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($description),
+ $this->get_contains_general_feedback_expectation($description));
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', null);
+
+ $this->check_current_state(question_state::$manfinished);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+
+ // Check that trying to process a manual comment with a grade causes an exception.
+ $this->expectException();
+ $this->manual_grade('Not good enough!', 1);
+ }
+}
diff --git a/question/behaviour/interactive/behaviour.php b/question/behaviour/interactive/behaviour.php
new file mode 100644
index 00000000000..976bb08e95f
--- /dev/null
+++ b/question/behaviour/interactive/behaviour.php
@@ -0,0 +1,246 @@
+.
+
+/**
+ * Question behaviour where the student can submit questions one at a
+ * time for immediate feedback.
+ *
+ * @package qbehaviour
+ * @subpackage interactive
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Question behaviour for the interactive model.
+ *
+ * Each question has a submit button next to it which the student can use to
+ * submit it. Once the qustion is submitted, it is not possible for the
+ * student to change their answer any more, but the student gets full feedback
+ * straight away.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_interactive extends question_behaviour_with_save {
+ const IS_ARCHETYPAL = true;
+
+ /**
+ * Special value used for {@link question_display_options::$readonly when
+ * we are showing the try again button to the student during an attempt.
+ * The particular number was chosen randomly. PHP will treat it the same
+ * as true, but in the renderer we reconginse it display the try again
+ * button enabled even though the rest of the question is disabled.
+ * @var integer
+ */
+ const READONLY_EXCEPT_TRY_AGAIN = 23485299;
+
+ public function required_question_definition_type() {
+ return 'question_automatically_gradable';
+ }
+
+ public function get_right_answer_summary() {
+ return $this->question->get_right_answer_summary();
+ }
+
+ /**
+ * @return bool are we are currently in the try_again state.
+ */
+ protected function is_try_again_state() {
+ $laststep = $this->qa->get_last_step();
+ return $this->qa->get_state()->is_active() && $laststep->has_behaviour_var('submit') &&
+ $laststep->has_behaviour_var('_triesleft');
+ }
+
+ public function adjust_display_options(question_display_options $options) {
+ // We only need different behaviour in try again states.
+ if (!$this->is_try_again_state()) {
+ parent::adjust_display_options($options);
+ return;
+ }
+
+ // Let the hint adjust the options.
+ $hint = $this->get_applicable_hint();
+ if (!is_null($hint)) {
+ $hint->adjust_display_options($options);
+ }
+
+ // Now call the base class method, but protect some fields from being overwritten.
+ $save = clone($options);
+ parent::adjust_display_options($options);
+ $options->feedback = $save->feedback;
+ $options->numpartscorrect = $save->numpartscorrect;
+
+ // In a try-again state, everything except the try again button
+ // Should be read-only. This is a mild hack to achieve this.
+ if (!$options->readonly) {
+ $options->readonly = self::READONLY_EXCEPT_TRY_AGAIN;
+ }
+ }
+
+ public function get_applicable_hint() {
+ if (!$this->is_try_again_state()) {
+ return null;
+ }
+ return $this->question->get_hint(count($this->question->hints) -
+ $this->qa->get_last_behaviour_var('_triesleft'), $this->qa);
+ }
+
+ public function get_expected_data() {
+ if ($this->is_try_again_state()) {
+ return array(
+ 'tryagain' => PARAM_BOOL,
+ );
+ } else if ($this->qa->get_state()->is_active()) {
+ return array(
+ 'submit' => PARAM_BOOL,
+ );
+ }
+ return parent::get_expected_data();
+ }
+
+ public function get_expected_qt_data() {
+ $hint = $this->get_applicable_hint();
+ if (!empty($hint->clearwrong)) {
+ return $this->question->get_expected_data();
+ }
+ return parent::get_expected_qt_data();
+ }
+
+ public function get_state_string($showcorrectness) {
+ $state = $this->qa->get_state();
+ if (!$state->is_active() || $state == question_state::$invalid) {
+ return parent::get_state_string($showcorrectness);
+ }
+
+ if ($this->is_try_again_state()) {
+ return get_string('notcomplete', 'qbehaviour_interactive');
+ } else {
+ return get_string('triesremaining', 'qbehaviour_interactive',
+ $this->qa->get_last_behaviour_var('_triesleft'));
+ }
+ }
+
+ public function init_first_step(question_attempt_step $step, $variant) {
+ parent::init_first_step($step, $variant);
+ $step->set_behaviour_var('_triesleft', count($this->question->hints) + 1);
+ }
+
+ public function process_action(question_attempt_pending_step $pendingstep) {
+ if ($pendingstep->has_behaviour_var('finish')) {
+ return $this->process_finish($pendingstep);
+ }
+ if ($this->is_try_again_state()) {
+ if ($pendingstep->has_behaviour_var('tryagain')) {
+ return $this->process_try_again($pendingstep);
+ } else {
+ return question_attempt::DISCARD;
+ }
+ } else {
+ if ($pendingstep->has_behaviour_var('comment')) {
+ return $this->process_comment($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('submit')) {
+ return $this->process_submit($pendingstep);
+ } else {
+ return $this->process_save($pendingstep);
+ }
+ }
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ if ($step->has_behaviour_var('comment')) {
+ return $this->summarise_manual_comment($step);
+ } else if ($step->has_behaviour_var('finish')) {
+ return $this->summarise_finish($step);
+ } else if ($step->has_behaviour_var('tryagain')) {
+ return get_string('tryagain', 'qbehaviour_interactive');
+ } else if ($step->has_behaviour_var('submit')) {
+ return $this->summarise_submit($step);
+ } else {
+ return $this->summarise_save($step);
+ }
+ }
+
+ public function process_try_again(question_attempt_pending_step $pendingstep) {
+ $pendingstep->set_state(question_state::$todo);
+ return question_attempt::KEEP;
+ }
+
+ public function process_submit(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ if (!$this->is_complete_response($pendingstep)) {
+ $pendingstep->set_state(question_state::$invalid);
+
+ } else {
+ $triesleft = $this->qa->get_last_behaviour_var('_triesleft');
+ $response = $pendingstep->get_qt_data();
+ list($fraction, $state) = $this->question->grade_response($response);
+ if ($state == question_state::$gradedright || $triesleft == 1) {
+ $pendingstep->set_state($state);
+ $pendingstep->set_fraction($this->adjust_fraction($fraction, $pendingstep));
+
+ } else {
+ $pendingstep->set_behaviour_var('_triesleft', $triesleft - 1);
+ $pendingstep->set_state(question_state::$todo);
+ }
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+ }
+ return question_attempt::KEEP;
+ }
+
+ protected function adjust_fraction($fraction, question_attempt_pending_step $pendingstep) {
+ $totaltries = $this->qa->get_step(0)->get_behaviour_var('_triesleft');
+ $triesleft = $this->qa->get_last_behaviour_var('_triesleft');
+
+ $fraction -= ($totaltries - $triesleft) * $this->question->penalty;
+ $fraction = max($fraction, 0);
+ return $fraction;
+ }
+
+ public function process_finish(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ $response = $this->qa->get_last_qt_data();
+ if (!$this->question->is_gradable_response($response)) {
+ $pendingstep->set_state(question_state::$gaveup);
+
+ } else {
+ list($fraction, $state) = $this->question->grade_response($response);
+ $pendingstep->set_fraction($this->adjust_fraction($fraction, $pendingstep));
+ $pendingstep->set_state($state);
+ }
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+ return question_attempt::KEEP;
+ }
+
+ public function process_save(question_attempt_pending_step $pendingstep) {
+ $status = parent::process_save($pendingstep);
+ if ($status == question_attempt::KEEP &&
+ $pendingstep->get_state() == question_state::$complete) {
+ $pendingstep->set_state(question_state::$todo);
+ }
+ return $status;
+ }
+}
diff --git a/question/behaviour/interactive/lang/en/qbehaviour_interactive.php b/question/behaviour/interactive/lang/en/qbehaviour_interactive.php
new file mode 100644
index 00000000000..b6fe2e8de09
--- /dev/null
+++ b/question/behaviour/interactive/lang/en/qbehaviour_interactive.php
@@ -0,0 +1,29 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_interactive', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage interactive
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['notcomplete'] = 'Not complete';
+$string['pluginname'] = 'Interactive with multiple tries';
+$string['triesremaining'] = 'Tries remaining: {$a}';
+$string['tryagain'] = 'Try again';
diff --git a/question/behaviour/interactive/renderer.php b/question/behaviour/interactive/renderer.php
new file mode 100644
index 00000000000..4bae37a6a2e
--- /dev/null
+++ b/question/behaviour/interactive/renderer.php
@@ -0,0 +1,64 @@
+.
+
+/**
+ * Renderer for outputting parts of a question belonging to the interactive
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage interactive
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Interactive behaviour renderer.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_interactive_renderer extends qbehaviour_renderer {
+ public function controls(question_attempt $qa, question_display_options $options) {
+ return $this->submit_button($qa, $options);
+ }
+
+ public function feedback(question_attempt $qa, question_display_options $options) {
+ if (!$qa->get_state()->is_active() || !$options->readonly) {
+ return '';
+ }
+
+ $attributes = array(
+ 'type' => 'submit',
+ 'id' => $qa->get_behaviour_field_name('tryagain'),
+ 'name' => $qa->get_behaviour_field_name('tryagain'),
+ 'value' => get_string('tryagain', 'qbehaviour_interactive'),
+ 'class' => 'submit btn',
+ );
+ if ($options->readonly !== qbehaviour_interactive::READONLY_EXCEPT_TRY_AGAIN) {
+ $attributes['disabled'] = 'disabled';
+ }
+ $output = html_writer::empty_tag('input', $attributes);
+ if (empty($attributes['disabled'])) {
+ $this->page->requires->js_init_call('M.core_question_engine.init_submit_button',
+ array($attributes['id'], $qa->get_slot()));
+ }
+ return $output;
+ }
+}
diff --git a/question/behaviour/interactive/simpletest/testwalkthrough.php b/question/behaviour/interactive/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..7f7f2e14bcb
--- /dev/null
+++ b/question/behaviour/interactive/simpletest/testwalkthrough.php
@@ -0,0 +1,489 @@
+.
+
+/**
+ * This file contains tests that walks a question through the interactive
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage interactive
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the interactive behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_interactive_walkthrough_test extends qbehaviour_walkthrough_test_base {
+
+ public function test_interactive_feedback_multichoice_right() {
+
+ // Create a multichoice single question.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $mc->hints = array(
+ new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, false, false),
+ new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+ );
+ $this->start_attempt_at_question($mc, 'interactive');
+
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_tries_remaining_expectation(3),
+ $this->get_no_hint_visible_expectation());
+
+ // Save the wrong answer.
+ $this->process_submission(array('answer' => $wrongindex));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_tries_remaining_expectation(3),
+ $this->get_no_hint_visible_expectation());
+
+ // Submit the wrong answer.
+ $this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_try_again_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ new PatternExpectation('/' .
+ preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+ $this->get_contains_hint_expectation('This is the first hint'));
+
+ // Check that, if we review in this state, the try again button is disabled.
+ $displayoptions = new question_display_options();
+ $displayoptions->readonly = true;
+ $html = $this->quba->render_question($this->slot, $displayoptions);
+ $this->assert($this->get_contains_try_again_button_expectation(false), $html);
+
+ // Do try again.
+ $this->process_submission(array('-tryagain' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_mc_radio_expectation($wrongindex, true, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_tries_remaining_expectation(2),
+ $this->get_no_hint_visible_expectation());
+
+ // Submit the right answer.
+ $this->process_submission(array('answer' => $rightindex, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(0.6666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.6666667),
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_correct_expectation(),
+ $this->get_no_hint_visible_expectation());
+
+ // Finish the attempt - should not need to add a new state.
+ $numsteps = $this->get_step_count();
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->assertEqual($numsteps, $this->get_step_count());
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(0.6666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.6666667),
+ $this->get_contains_mc_radio_expectation($rightindex, false, true),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($rightindex + 1) % 3, false, false),
+ $this->get_contains_correct_expectation(),
+ $this->get_no_hint_visible_expectation());
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 0.5);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(0.5);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.5),
+ $this->get_contains_partcorrect_expectation(),
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+
+ // Check regrading does not mess anything up.
+ $this->quba->regrade_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(0.5);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.5),
+ $this->get_contains_partcorrect_expectation());
+
+ $autogradedstep = $this->get_step($this->get_step_count() - 2);
+ $this->assertWithinMargin($autogradedstep->get_fraction(), 0.6666667, 0.0000001);
+ }
+
+ public function test_interactive_finish_when_try_again_showing() {
+
+ // Create a multichoice single question.
+ $mc = test_question_maker::make_a_multichoice_single_question();
+ $mc->hints = array(
+ new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, false, false),
+ );
+ $this->start_attempt_at_question($mc, 'interactive');
+
+ $rightindex = $this->get_mc_right_answer_index($mc);
+ $wrongindex = ($rightindex + 1) % 3;
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_radio_expectation(0, true, false),
+ $this->get_contains_mc_radio_expectation(1, true, false),
+ $this->get_contains_mc_radio_expectation(2, true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_tries_remaining_expectation(2),
+ $this->get_no_hint_visible_expectation(),
+ new PatternExpectation('/' .
+ preg_quote(get_string('selectone', 'qtype_multichoice'), '/') . '/'));
+
+ // Submit the wrong answer.
+ $this->process_submission(array('answer' => $wrongindex, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_try_again_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ new PatternExpectation('/' .
+ preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+ $this->get_contains_hint_expectation('This is the first hint'));
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedwrong);
+ $this->check_current_mark(0);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0),
+ $this->get_contains_mc_radio_expectation($wrongindex, false, true),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_mc_radio_expectation(($wrongindex + 1) % 3, false, false),
+ $this->get_contains_incorrect_expectation(),
+ $this->get_no_hint_visible_expectation());
+ }
+
+ public function test_interactive_shortanswer_try_to_submit_blank() {
+
+ // Create a short answer question.
+ $sa = test_question_maker::make_a_shortanswer_question();
+ $sa->hints = array(
+ new question_hint(0, 'This is the first hint.', FORMAT_HTML),
+ new question_hint(0, 'This is the second hint.', FORMAT_HTML),
+ );
+ $this->start_attempt_at_question($sa, 'interactive');
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_does_not_contain_validation_error_expectation(),
+ $this->get_does_not_contain_try_again_button_expectation(),
+ $this->get_no_hint_visible_expectation());
+
+ // Submit blank.
+ $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+ // Verify.
+ $this->check_current_state(question_state::$invalid);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_contains_validation_error_expectation(),
+ $this->get_does_not_contain_try_again_button_expectation(),
+ $this->get_no_hint_visible_expectation());
+
+ // Now get it wrong.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'newt'));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_does_not_contain_validation_error_expectation(),
+ $this->get_contains_try_again_button_expectation(true),
+ new PatternExpectation('/' .
+ preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+ $this->get_contains_hint_expectation('This is the first hint'));
+ $this->assertEqual('newt',
+ $this->quba->get_response_summary($this->slot));
+
+ // Try again.
+ $this->process_submission(array('-tryagain' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_does_not_contain_validation_error_expectation(),
+ $this->get_does_not_contain_try_again_button_expectation(),
+ $this->get_no_hint_visible_expectation());
+
+ // Now submit blank again.
+ $this->process_submission(array('-submit' => 1, 'answer' => ''));
+
+ // Verify.
+ $this->check_current_state(question_state::$invalid);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_contains_validation_error_expectation(),
+ $this->get_does_not_contain_try_again_button_expectation(),
+ $this->get_no_hint_visible_expectation());
+
+ // Now get it right.
+ $this->process_submission(array('-submit' => 1, 'answer' => 'frog'));
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(0.6666667);
+ $this->check_current_output(
+ $this->get_contains_mark_summary(0.6666667),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_correct_expectation(),
+ $this->get_does_not_contain_validation_error_expectation(),
+ $this->get_no_hint_visible_expectation());
+ $this->assertEqual('frog',
+ $this->quba->get_response_summary($this->slot));
+ }
+
+ public function test_interactive_feedback_multichoice_multiple_reset() {
+
+ // Create a multichoice multiple question.
+ $mc = test_question_maker::make_a_multichoice_multi_question();
+ $mc->hints = array(
+ new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
+ new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+ );
+ $this->start_attempt_at_question($mc, 'interactive', 2);
+
+ $right = array_keys($mc->get_correct_response());
+ $wrong = array_diff(array('choice0', 'choice1', 'choice2', 'choice3'), $right);
+ $wrong = array_values(array_diff(
+ array('choice0', 'choice1', 'choice2', 'choice3'), $right));
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_question_text_expectation($mc),
+ $this->get_contains_mc_checkbox_expectation('choice0', true, false),
+ $this->get_contains_mc_checkbox_expectation('choice1', true, false),
+ $this->get_contains_mc_checkbox_expectation('choice2', true, false),
+ $this->get_contains_mc_checkbox_expectation('choice3', true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_does_not_contain_num_parts_correct(),
+ $this->get_tries_remaining_expectation(3),
+ $this->get_no_hint_visible_expectation(),
+ new PatternExpectation('/' .
+ preg_quote(get_string('selectmulti', 'qtype_multichoice'), '/') . '/'));
+
+ // Submit an answer with one right, and one wrong.
+ $this->process_submission(array($right[0] => 1, $wrong[0] => 1, '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_mc_checkbox_expectation($right[0], false, true),
+ $this->get_contains_mc_checkbox_expectation($right[1], false, false),
+ $this->get_contains_mc_checkbox_expectation($wrong[0], false, true),
+ $this->get_contains_mc_checkbox_expectation($wrong[1], false, false),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_try_again_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ new PatternExpectation('/' .
+ preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+ $this->get_contains_hint_expectation('This is the first hint'),
+ $this->get_contains_num_parts_correct(1),
+ $this->get_contains_standard_incorrect_combined_feedback_expectation(),
+ $this->get_contains_hidden_expectation(
+ $this->quba->get_field_prefix($this->slot) . $right[0], '1'),
+ $this->get_does_not_contain_hidden_expectation(
+ $this->quba->get_field_prefix($this->slot) . $right[1]),
+ $this->get_contains_hidden_expectation(
+ $this->quba->get_field_prefix($this->slot) . $wrong[0], '0'),
+ $this->get_does_not_contain_hidden_expectation(
+ $this->quba->get_field_prefix($this->slot) . $wrong[1]));
+
+ // Do try again.
+ $this->process_submission(array($right[0] => 1, '-tryagain' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_marked_out_of_summary(),
+ $this->get_contains_mc_checkbox_expectation($right[0], true, true),
+ $this->get_contains_mc_checkbox_expectation($right[1], true, false),
+ $this->get_contains_mc_checkbox_expectation($wrong[0], true, false),
+ $this->get_contains_mc_checkbox_expectation($wrong[1], true, false),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_tries_remaining_expectation(2),
+ $this->get_no_hint_visible_expectation());
+ }
+
+ public function test_interactive_regrade_changing_num_tries_leaving_open() {
+ // Create a multichoice multiple question.
+ $q = test_question_maker::make_a_shortanswer_question();
+ $q->hints = array(
+ new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
+ new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+ );
+ $this->start_attempt_at_question($q, 'interactive', 3);
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_tries_remaining_expectation(3));
+
+ // Submit the right answer.
+ $this->process_submission(array('answer' => 'frog', '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(3);
+
+ // Now change the quiestion so that answer is only partially right, and regrade.
+ $q->answers[13]->fraction = 0.6666667;
+ $q->answers[14]->fraction = 1;
+
+ $this->quba->regrade_all_questions(false);
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ }
+
+ public function test_interactive_regrade_changing_num_tries_finished() {
+ // Create a multichoice multiple question.
+ $q = test_question_maker::make_a_shortanswer_question();
+ $q->hints = array(
+ new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
+ new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+ );
+ $this->start_attempt_at_question($q, 'interactive', 3);
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_tries_remaining_expectation(3));
+
+ // Submit the right answer.
+ $this->process_submission(array('answer' => 'frog', '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(3);
+
+ // Now change the quiestion so that answer is only partially right, and regrade.
+ $q->answers[13]->fraction = 0.6666667;
+ $q->answers[14]->fraction = 1;
+
+ $this->quba->regrade_all_questions(true);
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedpartial);
+ // TODO I don't think 1 is the right fraction here. However, it is what
+ // you get attempting a question like this without regrading being involved,
+ // and I am currently interested in testing regrading here.
+ $this->check_current_mark(1);
+ }
+}
diff --git a/question/behaviour/interactivecountback/behaviour.php b/question/behaviour/interactivecountback/behaviour.php
new file mode 100644
index 00000000000..5b3e7f44164
--- /dev/null
+++ b/question/behaviour/interactivecountback/behaviour.php
@@ -0,0 +1,94 @@
+.
+
+/**
+ * Question behaviour that is like the interactive behaviour, but where the
+ * student is credited for parts of the question they got right on earlier tries.
+ *
+ * @package qbehaviour
+ * @subpackage interactivecountback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../interactive/behaviour.php');
+
+
+/**
+ * Question behaviour for interactive mode with count-back scoring.
+ *
+ * As an example, suppose we have a matching question with 4 parts, and 3 tries
+ * (penalty 1/3), and the question is worth 12 marks (so, 3 marks for each part).
+ * Suppose also that:
+ * - on the first try, the student gets the first two parts right, and the
+ * other two wrong.
+ * - on the second try, they are sure they got the first part right, so keep
+ * their answer the same, but they change their answer to the second part.
+ * They also get the answer to the thrid part right on this try, but still
+ * get the 4th part wrong.
+ * - On the final try, they get the first 3 parts right, but the 4th part still
+ * wrong.
+ * We want to grade them as follows.
+ * - For the first part, they were right first time, and did not change their
+ * answer, so we credit that part as right first time: 3/3
+ * - For the second part, although they were right first time, they then changed
+ * their mind, an only finally got it right on the third try, so 1/3.
+ * - For the third part, they got it right on the second try, and then did not
+ * change their answer, so 2/3.
+ * - For the last part, they were wrong at the last try, so 0/3.
+ * So, total mark is 6/12. (Really, a fraction of 0.5.)
+ *
+ * Of course, the details of the grading are acutally up to the particular
+ * question type. The point is that the final grade can take into account all
+ * of the tries the student made.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_interactivecountback extends qbehaviour_interactive {
+ const IS_ARCHETYPAL = false;
+
+ public function required_question_definition_type() {
+ return 'question_automatically_gradable_with_countback';
+ }
+
+ protected function adjust_fraction($fraction, question_attempt_pending_step $pendingstep) {
+ $totaltries = $this->qa->get_step(0)->get_behaviour_var('_triesleft');
+
+ $responses = array();
+ $lastsave = array();
+ foreach ($this->qa->get_step_iterator() as $step) {
+ if ($step->has_behaviour_var('submit') &&
+ $step->get_state() != question_state::$invalid) {
+ $responses[] = $step->get_qt_data();
+ $lastsave = array();
+ } else {
+ $lastsave = $step->get_qt_data();
+ }
+ }
+ $lastresponse = $pendingstep->get_qt_data();
+ if (!empty($lastresponse)) {
+ $responses[] = $lastresponse;
+ } else if (!empty($lastsave)) {
+ $responses[] = $lastsave;
+ }
+
+ return $this->question->compute_final_grade($responses, $totaltries);
+ }
+}
diff --git a/question/behaviour/interactivecountback/lang/en/qbehaviour_interactivecountback.php b/question/behaviour/interactivecountback/lang/en/qbehaviour_interactivecountback.php
new file mode 100644
index 00000000000..c02023e17bf
--- /dev/null
+++ b/question/behaviour/interactivecountback/lang/en/qbehaviour_interactivecountback.php
@@ -0,0 +1,26 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_interactivecountback', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage interactivecountback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Interactive with multiple tries (credit for earlier tries)';
diff --git a/question/behaviour/interactivecountback/renderer.php b/question/behaviour/interactivecountback/renderer.php
new file mode 100644
index 00000000000..55875384115
--- /dev/null
+++ b/question/behaviour/interactivecountback/renderer.php
@@ -0,0 +1,43 @@
+.
+
+/**
+ * Defines the renderer for the interactive with countback behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage interactivecountback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../interactive/renderer.php');
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the interactive with
+ * countback behaviour.
+ *
+ * There are not differences from the interactive output. We just need a class
+ * definition.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_interactivecountback_renderer extends qbehaviour_interactive_renderer {
+}
\ No newline at end of file
diff --git a/question/behaviour/interactivecountback/simpletest/testwalkthrough.php b/question/behaviour/interactivecountback/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..11a303d9553
--- /dev/null
+++ b/question/behaviour/interactivecountback/simpletest/testwalkthrough.php
@@ -0,0 +1,148 @@
+.
+
+/**
+ * This file contains tests that walks a question through the interactive with
+ * countback behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage interactivecountback
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the interactive with countback behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_interactivecountback_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_interactive_feedback_match_reset() {
+
+ // Create a matching question.
+ $m = test_question_maker::make_a_matching_question();
+ $m->shufflestems = false;
+ $m->hints = array(
+ new question_hint_with_parts(0, 'This is the first hint.', FORMAT_HTML, true, true),
+ new question_hint_with_parts(0, 'This is the second hint.', FORMAT_HTML, true, true),
+ );
+ $this->start_attempt_at_question($m, 'interactive', 12);
+
+ $choiceorder = $m->get_choice_order();
+ $orderforchoice = array_combine(array_values($choiceorder), array_keys($choiceorder));
+ $choices = array(0 => get_string('choose') . '...');
+ foreach ($choiceorder as $key => $choice) {
+ $choices[$key] = $m->choices[$choice];
+ }
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->assertEqual('interactivecountback',
+ $this->quba->get_question_attempt($this->slot)->get_behaviour_name());
+ $this->check_current_output(
+ $this->get_contains_select_expectation('sub0', $choices, null, true),
+ $this->get_contains_select_expectation('sub1', $choices, null, true),
+ $this->get_contains_select_expectation('sub2', $choices, null, true),
+ $this->get_contains_select_expectation('sub3', $choices, null, true),
+ $this->get_contains_question_text_expectation($m),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_tries_remaining_expectation(3),
+ $this->get_does_not_contain_num_parts_correct(),
+ $this->get_no_hint_visible_expectation());
+
+ // Submit an answer with two right, and two wrong.
+ $this->process_submission(array('sub0' => $orderforchoice[1],
+ 'sub1' => $orderforchoice[1], 'sub2' => $orderforchoice[1],
+ 'sub3' => $orderforchoice[1], '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], false),
+ $this->get_contains_select_expectation('sub1', $choices, $orderforchoice[1], false),
+ $this->get_contains_select_expectation('sub2', $choices, $orderforchoice[1], false),
+ $this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], false),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_contains_try_again_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ new PatternExpectation('/' .
+ preg_quote(get_string('notcomplete', 'qbehaviour_interactive')) . '/'),
+ $this->get_contains_hint_expectation('This is the first hint'),
+ $this->get_contains_num_parts_correct(2),
+ $this->get_contains_standard_partiallycorrect_combined_feedback_expectation(),
+ $this->get_contains_hidden_expectation(
+ $this->quba->get_field_prefix($this->slot) . 'sub0', $orderforchoice[1]),
+ $this->get_contains_hidden_expectation(
+ $this->quba->get_field_prefix($this->slot) . 'sub1', '0'),
+ $this->get_contains_hidden_expectation(
+ $this->quba->get_field_prefix($this->slot) . 'sub2', '0'),
+ $this->get_contains_hidden_expectation(
+ $this->quba->get_field_prefix($this->slot) . 'sub3', $orderforchoice[1]));
+
+ // Check that extract responses will return the reset data.
+ $prefix = $this->quba->get_field_prefix($this->slot);
+ $this->assertEqual(array('sub0' => 1),
+ $this->quba->extract_responses($this->slot, array($prefix . 'sub0' => 1)));
+
+ // Do try again.
+ $this->process_submission(array('sub0' => $orderforchoice[1],
+ 'sub3' => $orderforchoice[1], '-tryagain' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], true),
+ $this->get_contains_select_expectation('sub1', $choices, null, true),
+ $this->get_contains_select_expectation('sub2', $choices, null, true),
+ $this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], true),
+ $this->get_contains_submit_button_expectation(true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation(),
+ $this->get_tries_remaining_expectation(2),
+ $this->get_no_hint_visible_expectation());
+
+ // Submit the right answer.
+ $this->process_submission(array('sub0' => $orderforchoice[1],
+ 'sub1' => $orderforchoice[2], 'sub2' => $orderforchoice[2],
+ 'sub3' => $orderforchoice[1], '-submit' => 1));
+
+ // Verify.
+ $this->check_current_state(question_state::$gradedright);
+ $this->check_current_mark(10);
+ $this->check_current_output(
+ $this->get_contains_select_expectation('sub0', $choices, $orderforchoice[1], false),
+ $this->get_contains_select_expectation('sub1', $choices, $orderforchoice[2], false),
+ $this->get_contains_select_expectation('sub2', $choices, $orderforchoice[2], false),
+ $this->get_contains_select_expectation('sub3', $choices, $orderforchoice[1], false),
+ $this->get_contains_submit_button_expectation(false),
+ $this->get_does_not_contain_try_again_button_expectation(),
+ $this->get_contains_correct_expectation(),
+ $this->get_contains_standard_correct_combined_feedback_expectation(),
+ new NoPatternExpectation('/class="control\b[^"]*\bpartiallycorrect"/'));
+ }
+}
diff --git a/question/behaviour/manualgraded/behaviour.php b/question/behaviour/manualgraded/behaviour.php
new file mode 100644
index 00000000000..d8a8ef6eb2d
--- /dev/null
+++ b/question/behaviour/manualgraded/behaviour.php
@@ -0,0 +1,94 @@
+.
+
+/**
+ * Question behaviour for questions that can only be graded manually.
+ *
+ * @package qbehaviour
+ * @subpackage manualgraded
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Question behaviour for questions that can only be graded manually.
+ *
+ * The student enters their response during the attempt, and it is saved. Later,
+ * when the whole attempt is finished, the attempt goes into the NEEDS_GRADING
+ * state, and the teacher must grade it manually.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_manualgraded extends question_behaviour_with_save {
+ const IS_ARCHETYPAL = true;
+
+ public static function get_unused_display_options() {
+ return array('correctness', 'marks', 'specificfeedback', 'generalfeedback',
+ 'rightanswer');
+ }
+
+ public function adjust_display_options(question_display_options $options) {
+ parent::adjust_display_options($options);
+
+ if ($this->qa->get_state()->is_finished()) {
+ // Hide all feedback except genfeedback and manualcomment.
+ $save = clone($options);
+ $options->hide_all_feedback();
+ $options->generalfeedback = $save->generalfeedback;
+ $options->manualcomment = $save->manualcomment;
+ }
+ }
+
+ public function process_action(question_attempt_pending_step $pendingstep) {
+ if ($pendingstep->has_behaviour_var('comment')) {
+ return $this->process_comment($pendingstep);
+ } else if ($pendingstep->has_behaviour_var('finish')) {
+ return $this->process_finish($pendingstep);
+ } else {
+ return $this->process_save($pendingstep);
+ }
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ if ($step->has_behaviour_var('comment')) {
+ return $this->summarise_manual_comment($step);
+ } else if ($step->has_behaviour_var('finish')) {
+ return $this->summarise_finish($step);
+ } else {
+ return $this->summarise_save($step);
+ }
+ }
+
+ public function process_finish(question_attempt_pending_step $pendingstep) {
+ if ($this->qa->get_state()->is_finished()) {
+ return question_attempt::DISCARD;
+ }
+
+ $response = $this->qa->get_last_step()->get_qt_data();
+ if (!$this->question->is_complete_response($response)) {
+ $pendingstep->set_state(question_state::$gaveup);
+ } else {
+ $pendingstep->set_state(question_state::$needsgrading);
+ }
+ $pendingstep->set_new_response_summary($this->question->summarise_response($response));
+ return question_attempt::KEEP;
+ }
+}
diff --git a/question/behaviour/manualgraded/lang/en/qbehaviour_manualgraded.php b/question/behaviour/manualgraded/lang/en/qbehaviour_manualgraded.php
new file mode 100644
index 00000000000..5ff907a5927
--- /dev/null
+++ b/question/behaviour/manualgraded/lang/en/qbehaviour_manualgraded.php
@@ -0,0 +1,26 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_manualgraded', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage manualgraded
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'Manually graded';
diff --git a/question/behaviour/manualgraded/renderer.php b/question/behaviour/manualgraded/renderer.php
new file mode 100644
index 00000000000..b591ee4b834
--- /dev/null
+++ b/question/behaviour/manualgraded/renderer.php
@@ -0,0 +1,38 @@
+.
+
+/**
+ * Defines the renderer for the manual graded behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage manualgraded
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for outputting parts of a question belonging to the manual
+ * graded behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_manualgraded_renderer extends qbehaviour_renderer {
+}
diff --git a/question/behaviour/manualgraded/simpletest/testwalkthrough.php b/question/behaviour/manualgraded/simpletest/testwalkthrough.php
new file mode 100644
index 00000000000..3b7e7ef4fe8
--- /dev/null
+++ b/question/behaviour/manualgraded/simpletest/testwalkthrough.php
@@ -0,0 +1,269 @@
+.
+
+/**
+ * This file contains tests that walks a question through the manual graded
+ * behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage manualgraded
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+
+
+/**
+ * Unit tests for the manual graded behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_manualgraded_walkthrough_test extends qbehaviour_walkthrough_test_base {
+ public function test_manual_graded_essay() {
+
+ // Create an essay question.
+ $essay = test_question_maker::make_an_essay_question();
+ $this->start_attempt_at_question($essay, 'deferredfeedback', 10);
+
+ // Check the right model is being used.
+ $this->assertEqual('manualgraded', $this->quba->get_question_attempt(
+ $this->slot)->get_behaviour_name());
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output($this->get_contains_question_text_expectation($essay),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Simulate some data submitted by the student.
+ $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ new ContainsTagWithAttribute('textarea', 'name',
+ $this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process the same data again, check it does not create a new step.
+ $numsteps = $this->get_step_count();
+ $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+ $this->check_step_count($numsteps);
+
+ // Process different data, check it creates a new step.
+ $this->process_submission(array('answer' => ''));
+ $this->check_step_count($numsteps + 1);
+ $this->check_current_state(question_state::$todo);
+
+ // Change back, check it creates a new step.
+ $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+ $this->check_step_count($numsteps + 2);
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->assertEqual('This is my wonderful essay!',
+ $this->quba->get_response_summary($this->slot));
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 10);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrright);
+ $this->check_current_mark(10);
+ $this->check_current_output(
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+
+ // Now change the max mark for the question and regrade.
+ $this->quba->regrade_question($this->slot, true, 1);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrright);
+ $this->check_current_mark(1);
+ }
+
+ public function test_manual_graded_truefalse() {
+
+ // Create a true-false question with correct answer true.
+ $tf = test_question_maker::make_question('truefalse', 'true');
+ $this->start_attempt_at_question($tf, 'manualgraded', 2);
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_question_text_expectation($tf),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Process a true answer and check the expected result.
+ $this->process_submission(array('answer' => 1));
+
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_contains_tf_true_radio_expectation(true, true),
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_specific_feedback_expectation());
+
+ // Process a manual comment.
+ $this->manual_grade('Not good enough!', 1);
+
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(1);
+ $this->check_current_output(
+ $this->get_does_not_contain_correctness_expectation(),
+ $this->get_does_not_contain_specific_feedback_expectation(),
+ new PatternExpectation('/' . preg_quote('Not good enough!') . '/'));
+ }
+
+ public function test_manual_graded_ignore_repeat_sumbission() {
+ // Create an essay question.
+ $essay = test_question_maker::make_an_essay_question();
+ $this->start_attempt_at_question($essay, 'deferredfeedback', 10);
+
+ // Check the right model is being used.
+ $this->assertEqual('manualgraded', $this->quba->get_question_attempt(
+ $this->slot)->get_behaviour_name());
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+
+ // Simulate some data submitted by the student.
+ $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->assertEqual('This is my wonderful essay!',
+ $this->quba->get_response_summary($this->slot));
+
+ // Process a blank manual comment. Ensure it does not change the state.
+ $numsteps = $this->get_step_count();
+ $this->manual_grade('', '');
+ $this->check_step_count($numsteps);
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+
+ // Process a comment, but with the mark blank. Should be recorded, but
+ // not change the mark.
+ $this->manual_grade('I am not sure what grade to award.', '');
+ $this->check_step_count($numsteps + 1);
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ new PatternExpectation('/' .
+ preg_quote('I am not sure what grade to award.') . '/'));
+
+ // Now grade it.
+ $this->manual_grade('Pretty good!', '9.00000');
+ $this->check_step_count($numsteps + 2);
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(9);
+ $this->check_current_output(
+ new PatternExpectation('/' . preg_quote('Pretty good!') . '/'));
+
+ // Process the same data again, and make sure it does not add a step.
+ $this->manual_grade('Pretty good!', '9.00000');
+ $this->check_step_count($numsteps + 2);
+ $this->check_current_state(question_state::$mangrpartial);
+ $this->check_current_mark(9);
+
+ // Now set the mark back to blank.
+ $this->manual_grade('Actually, I am not sure any more.', '');
+ $this->check_step_count($numsteps + 3);
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ new PatternExpectation('/' .
+ preg_quote('Actually, I am not sure any more.') . '/'));
+
+ $qa = $this->quba->get_question_attempt($this->slot);
+ $this->assertEqual('Commented: Actually, I am not sure any more.',
+ $qa->summarise_action($qa->get_last_step()));
+ }
+
+ public function test_manual_graded_essay_can_grade_0() {
+
+ // Create an essay question.
+ $essay = test_question_maker::make_an_essay_question();
+ $this->start_attempt_at_question($essay, 'deferredfeedback', 10);
+
+ // Check the right model is being used.
+ $this->assertEqual('manualgraded', $this->quba->get_question_attempt(
+ $this->slot)->get_behaviour_name());
+
+ // Check the initial state.
+ $this->check_current_state(question_state::$todo);
+ $this->check_current_mark(null);
+ $this->check_current_output($this->get_contains_question_text_expectation($essay),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Simulate some data submitted by the student.
+ $this->process_submission(array('answer' => 'This is my wonderful essay!'));
+
+ // Verify.
+ $this->check_current_state(question_state::$complete);
+ $this->check_current_mark(null);
+ $this->check_current_output(
+ new ContainsTagWithAttribute('textarea', 'name',
+ $this->quba->get_question_attempt($this->slot)->get_qt_field_name('answer')),
+ $this->get_does_not_contain_feedback_expectation());
+
+ // Finish the attempt.
+ $this->quba->finish_all_questions();
+
+ // Verify.
+ $this->check_current_state(question_state::$needsgrading);
+ $this->check_current_mark(null);
+ $this->assertEqual('This is my wonderful essay!',
+ $this->quba->get_response_summary($this->slot));
+
+ // Process a blank comment and a grade of 0.
+ $this->manual_grade('', 0);
+
+ // Verify.
+ $this->check_current_state(question_state::$mangrwrong);
+ $this->check_current_mark(0);
+ }
+}
diff --git a/question/behaviour/missing/behaviour.php b/question/behaviour/missing/behaviour.php
new file mode 100644
index 00000000000..1726e011166
--- /dev/null
+++ b/question/behaviour/missing/behaviour.php
@@ -0,0 +1,70 @@
+.
+
+/**
+ * Fake question behaviour that is used when the actual qim was not
+ * available.
+ *
+ * @package qbehaviour
+ * @subpackage missing
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Fake question behaviour that is used when the actual behaviour
+ * is not available.
+ *
+ * Imagine, for example, that a quiz attempt has been restored from another
+ * Moodle site with more behaviours installed, or an behaviour
+ * that used to be available in this site has been uninstalled. Obviously all we
+ * can do is have some code to prevent fatal errors.
+ *
+ * The approach we take is: The rendering code is still implemented, as far as
+ * possible. A warning is shown that behaviour specific bits may be missing.
+ * Any attempt to process anything causes an exception to be thrown.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_missing extends question_behaviour {
+ public function required_question_definition_type() {
+ return 'question_definition';
+ }
+
+ public function summarise_action(question_attempt_step $step) {
+ return '';
+ }
+
+ public function init_first_step(question_attempt_step $step, $variant) {
+ throw new coding_exception('The behaviour used for this question is not available. ' .
+ 'No processing is possible.');
+ }
+
+ public function process_action(question_attempt_pending_step $pendingstep) {
+ throw new coding_exception('The behaviour used for this question is not available. ' .
+ 'No processing is possible.');
+ }
+
+ public function get_min_fraction() {
+ throw new coding_exception('The behaviour used for this question is not available. ' .
+ 'No processing is possible.');
+ }
+}
diff --git a/question/behaviour/missing/lang/en/qbehaviour_missing.php b/question/behaviour/missing/lang/en/qbehaviour_missing.php
new file mode 100644
index 00000000000..934302c1223
--- /dev/null
+++ b/question/behaviour/missing/lang/en/qbehaviour_missing.php
@@ -0,0 +1,26 @@
+.
+
+/**
+ * Strings for component 'qbehaviour_missing', language 'en'.
+ *
+ * @package qbehaviour
+ * @subpackage missing
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['questionusedunknownmodel'] = 'This question was attempted with an behaviour that is not currently availalbe. We are attempting to show the question, but there may be problems.';
diff --git a/question/behaviour/missing/renderer.php b/question/behaviour/missing/renderer.php
new file mode 100644
index 00000000000..88800554ef6
--- /dev/null
+++ b/question/behaviour/missing/renderer.php
@@ -0,0 +1,43 @@
+.
+
+/**
+ * Defines the renderer for when the actual behaviour used is not available.
+ *
+ * @package qbehaviour
+ * @subpackage missing
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer for outputting parts of a question when the actual behaviour
+ * used is not available.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_missing_renderer extends qbehaviour_renderer {
+ public function controls(question_attempt $qa, question_display_options $options) {
+ return html_writer::tag('div',
+ get_string('questionusedunknownmodel', 'qbehaviour_missing'),
+ array('class' => 'warning'));
+ }
+}
\ No newline at end of file
diff --git a/question/behaviour/missing/simpletest/testmissingbehaviour.php b/question/behaviour/missing/simpletest/testmissingbehaviour.php
new file mode 100644
index 00000000000..fb958648540
--- /dev/null
+++ b/question/behaviour/missing/simpletest/testmissingbehaviour.php
@@ -0,0 +1,108 @@
+.
+
+/**
+ * This file contains tests for the 'missing' behaviour.
+ *
+ * @package qbehaviour
+ * @subpackage missing
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once(dirname(__FILE__) . '/../../../engine/lib.php');
+require_once(dirname(__FILE__) . '/../../../engine/simpletest/helpers.php');
+require_once(dirname(__FILE__) . '/../behaviour.php');
+
+
+/**
+ * Unit tests for the 'missing' behaviour.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_missing_test extends UnitTestCase {
+ public function test_missing_cannot_start() {
+ $qa = new question_attempt(test_question_maker::make_question('truefalse', 'true'), 0);
+ $behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
+ $this->expectException();
+ $behaviour->init_first_step(new question_attempt_step(array()), 1);
+ }
+
+ public function test_missing_cannot_process() {
+ $qa = new question_attempt(test_question_maker::make_question('truefalse', 'true'), 0);
+ $behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
+ $this->expectException();
+ $behaviour->process_action(new question_attempt_pending_step(array()));
+ }
+
+ public function test_missing_cannot_get_min_grade() {
+ $qa = new question_attempt(test_question_maker::make_question('truefalse', 'true'), 0);
+ $behaviour = new qbehaviour_missing($qa, 'deferredfeedback');
+ $this->expectException();
+ $behaviour->get_min_fraction();
+ }
+
+ public function test_render_missing() {
+ $records = testing_db_record_builder::build_db_records(array(
+ array('id', 'questionattemptid', 'contextid', 'questionusageid', 'slot',
+ 'behaviour', 'questionid', 'variant', 'maxmark', 'minfraction', 'flagged',
+ 'questionsummary', 'rightanswer', 'responsesummary',
+ 'timemodified', 'attemptstepid', 'sequencenumber', 'state', 'fraction',
+ 'timecreated', 'userid', 'name', 'value'),
+ array(1, 1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 0, '', '', '',
+ 1256233790, 1, 0, 'todo', null, 1256233700, 1, '_order', '1,2,3'),
+ array(2, 1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 0, '', '', '',
+ 1256233790, 2, 1, 'complete', 0.50, 1256233705, 1, '-submit', '1'),
+ array(3, 1, 123, 1, 1, 'strangeunknown', -1, 1, 2.0000000, 0.0000000, 0, '', '', '',
+ 1256233790, 2, 1, 'complete', 0.50, 1256233705, 1, 'choice0', '1'),
+ ));
+
+ $question = test_question_maker::make_question('truefalse', 'true');
+ $question->id = -1;
+
+ question_bank::start_unit_test();
+ question_bank::load_test_question_data($question);
+ $qa = question_attempt::load_from_records($records, 1,
+ new question_usage_null_observer(), 'deferredfeedback');
+ question_bank::end_unit_test();
+
+ $this->assertEqual(2, $qa->get_num_steps());
+
+ $step = $qa->get_step(0);
+ $this->assertEqual(question_state::$todo, $step->get_state());
+ $this->assertNull($step->get_fraction());
+ $this->assertEqual(1256233700, $step->get_timecreated());
+ $this->assertEqual(1, $step->get_user_id());
+ $this->assertEqual(array('_order' => '1,2,3'), $step->get_all_data());
+
+ $step = $qa->get_step(1);
+ $this->assertEqual(question_state::$complete, $step->get_state());
+ $this->assertEqual(0.5, $step->get_fraction());
+ $this->assertEqual(1256233705, $step->get_timecreated());
+ $this->assertEqual(1, $step->get_user_id());
+ $this->assertEqual(array('-submit' => '1', 'choice0' => '1'), $step->get_all_data());
+
+ $output = $qa->render(new question_display_options(), '1');
+ $this->assertPattern('/' . preg_quote($qa->get_question()->questiontext) . '/', $output);
+ $this->assertPattern('/' . preg_quote(
+ get_string('questionusedunknownmodel', 'qbehaviour_missing')) . '/', $output);
+ $this->assert(new ContainsTagWithAttribute('div', 'class', 'warning'), $output);
+ }
+}
diff --git a/question/behaviour/rendererbase.php b/question/behaviour/rendererbase.php
new file mode 100644
index 00000000000..c8d64794a76
--- /dev/null
+++ b/question/behaviour/rendererbase.php
@@ -0,0 +1,204 @@
+.
+
+/**
+ * Defines the renderer base class for question behaviours.
+ *
+ * @package moodlecore
+ * @subpackage questionbehaviours
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Renderer base class for question behaviours.
+ *
+ * The methods in this class are mostly called from {@link core_question_renderer}
+ * which coordinates the overall output of questions.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class qbehaviour_renderer extends plugin_renderer_base {
+ /**
+ * Generate some HTML (which may be blank) that appears in the question
+ * formulation area, afer the question type generated output.
+ *
+ * For example.
+ * immediatefeedback and interactive mode use this to show the Submit button,
+ * and CBM use this to display the certainty choices.
+ *
+ * @param question_attempt $qa a question attempt.
+ * @param question_display_options $options controls what should and should not be displayed.
+ * @return string HTML fragment.
+ */
+ public function controls(question_attempt $qa, question_display_options $options) {
+ return '';
+ }
+
+ /**
+ * Generate some HTML (which may be blank) that appears in the outcome area,
+ * after the question-type generated output.
+ *
+ * For example, the CBM models use this to display an explanation of the score
+ * adjustment that was made based on the certainty selected.
+ *
+ * @param question_attempt $qa a question attempt.
+ * @param question_display_options $options controls what should and should not be displayed.
+ * @return string HTML fragment.
+ */
+ public function feedback(question_attempt $qa, question_display_options $options) {
+ return '';
+ }
+
+ public function manual_comment_fields(question_attempt $qa, question_display_options $options) {
+
+ $commentfield = $qa->get_behaviour_field_name('comment');
+
+ list($commenttext, $commentformat) = $qa->get_manual_comment();
+ $comment = print_textarea(can_use_html_editor(), 10, 80, null, null,
+ $commentfield, $commenttext, 0, true);
+ $comment = html_writer::tag('div', html_writer::tag('div',
+ html_writer::tag('label', get_string('comment', 'question'),
+ array('for' => $commentfield)), array('class' => 'fitemtitle')) .
+ html_writer::tag('div', $comment, array('class' => 'felement fhtmleditor')),
+ array('class' => 'fitem'));
+
+ $mark = '';
+ if ($qa->get_max_mark()) {
+ $currentmark = $qa->get_current_manual_mark();
+ $maxmark = $qa->get_max_mark();
+
+ $fieldsize = strlen($qa->format_max_mark($options->markdp)) - 1;
+ $markfield = $qa->get_behaviour_field_name('mark');
+
+ $attributes = array(
+ 'type' => 'text',
+ 'size' => $fieldsize,
+ 'name' => $markfield,
+ );
+ if (!is_null($currentmark)) {
+ $attributes['value'] = $qa->format_fraction_as_mark(
+ $currentmark / $maxmark, $options->markdp);
+ }
+ $a = new stdClass();
+ $a->max = $qa->format_max_mark($options->markdp);
+ $a->mark = html_writer::empty_tag('input', $attributes);
+
+ $markrange = html_writer::empty_tag('input', array(
+ 'type' => 'hidden',
+ 'name' => $qa->get_behaviour_field_name('maxmark'),
+ 'value' => $maxmark,
+ )) . html_writer::empty_tag('input', array(
+ 'type' => 'hidden',
+ 'name' => $qa->get_control_field_name('minfraction'),
+ 'value' => $qa->get_min_fraction(),
+ ));
+
+ $errorclass = '';
+ $error = '';
+ if ($currentmark > $maxmark || $currentmark < $maxmark * $qa->get_min_fraction()) {
+ $errorclass = ' error';
+ $error = html_writer::tag('span', get_string('manualgradeoutofrange', 'question'),
+ array('class' => 'error')) . html_writer::empty_tag('br');
+ }
+
+ $mark = html_writer::tag('div', html_writer::tag('div',
+ html_writer::tag('label', get_string('mark', 'question'),
+ array('for' => $markfield)),
+ array('class' => 'fitemtitle')) .
+ html_writer::tag('div', $error . get_string('xoutofmax', 'question', $a) .
+ $markrange, array('class' => 'felement ftext' . $errorclass)
+ ), array('class' => 'fitem'));
+ }
+
+ return html_writer::tag('fieldset', html_writer::tag('div', $comment . $mark,
+ array('class' => 'fcontainer clearfix')), array('class' => 'hidden'));
+ }
+
+ public function manual_comment_view(question_attempt $qa, question_display_options $options) {
+ $output = '';
+ if ($qa->has_manual_comment()) {
+ $output .= get_string('commentx', 'question', $qa->get_behaviour()->format_comment());
+ }
+ if ($options->manualcommentlink) {
+ $url = new moodle_url($options->manualcommentlink, array('slot' => $qa->get_slot()));
+ $link = $this->output->action_link($url, get_string('commentormark', 'question'),
+ new popup_action('click', $url, 'commentquestion',
+ array('width' => 600, 'height' => 800)));
+ $output .= html_writer::tag('div', $link, array('class' => 'commentlink'));
+ }
+ return $output;
+ }
+
+ /**
+ * Display the manual comment, and a link to edit it, if appropriate.
+ *
+ * @param question_attempt $qa a question attempt.
+ * @param question_display_options $options controls what should and should not be displayed.
+ * @return string HTML fragment.
+ */
+ public function manual_comment(question_attempt $qa, question_display_options $options) {
+ if ($options->manualcomment == question_display_options::EDITABLE) {
+ return $this->manual_comment_fields($qa, $options);
+
+ } else if ($options->manualcomment == question_display_options::VISIBLE) {
+ return $this->manual_comment_view($qa, $options);
+
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Several behaviours need a submit button, so put the common code here.
+ * The button is disabled if the question is displayed read-only.
+ * @param question_display_options $options controls what should and should not be displayed.
+ * @return string HTML fragment.
+ */
+ protected function submit_button(question_attempt $qa, question_display_options $options) {
+ $attributes = array(
+ 'type' => 'submit',
+ 'id' => $qa->get_behaviour_field_name('submit'),
+ 'name' => $qa->get_behaviour_field_name('submit'),
+ 'value' => get_string('check', 'question'),
+ 'class' => 'submit btn',
+ );
+ if ($options->readonly) {
+ $attributes['disabled'] = 'disabled';
+ }
+ $output = html_writer::empty_tag('input', $attributes);
+ if (!$options->readonly) {
+ $this->page->requires->js_init_call('M.core_question_engine.init_submit_button',
+ array($attributes['id'], $qa->get_slot()));
+ }
+ return $output;
+ }
+
+ /**
+ * Return any HTML that needs to be included in the page's when
+ * questions using this model are used.
+ * @param $qa the question attempt that will be displayed on the page.
+ * @return string HTML fragment.
+ */
+ public function head_code(question_attempt $qa) {
+ return '';
+ }
+}
diff --git a/question/category.php b/question/category.php
index 2f60034d302..8b43360dc93 100644
--- a/question/category.php
+++ b/question/category.php
@@ -1,107 +1,122 @@
.
+
/**
- * Allows a teacher to create, edit and delete categories
+ * This script allows a teacher to create, edit and delete question categories.
*
- * @author Martin Dougiamas and many others.
- * {@link http://moodle.org}
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package questionbank
+ * @package moodlecore
+ * @subpackage questionbank
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
- require_once("../config.php");
- require_once($CFG->dirroot."/question/editlib.php");
- require_once($CFG->dirroot."/question/category_class.php");
- list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
- question_edit_setup('categories', '/question/category.php');
+require_once("../config.php");
+require_once($CFG->dirroot."/question/editlib.php");
+require_once($CFG->dirroot."/question/category_class.php");
- // get values from form for actions on this page
- $param = new stdClass();
- $param->moveup = optional_param('moveup', 0, PARAM_INT);
- $param->movedown = optional_param('movedown', 0, PARAM_INT);
- $param->moveupcontext = optional_param('moveupcontext', 0, PARAM_INT);
- $param->movedowncontext = optional_param('movedowncontext', 0, PARAM_INT);
- $param->tocontext = optional_param('tocontext', 0, PARAM_INT);
- $param->left = optional_param('left', 0, PARAM_INT);
- $param->right = optional_param('right', 0, PARAM_INT);
- $param->delete = optional_param('delete', 0, PARAM_INT);
- $param->confirm = optional_param('confirm', 0, PARAM_INT);
- $param->cancel = optional_param('cancel', '', PARAM_ALPHA);
- $param->move = optional_param('move', 0, PARAM_INT);
- $param->moveto = optional_param('moveto', 0, PARAM_INT);
- $param->edit = optional_param('edit', 0, PARAM_INT);
+list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
+ question_edit_setup('categories', '/question/category.php');
- $url = new moodle_url($thispageurl);
- foreach ((array)$param as $key=>$value) {
- if (($key !== 'cancel' && $value !== 0) || ($key === 'cancel' && $value !== '')) {
- $url->param($key, $value);
- }
+// get values from form for actions on this page
+$param = new stdClass();
+$param->moveup = optional_param('moveup', 0, PARAM_INT);
+$param->movedown = optional_param('movedown', 0, PARAM_INT);
+$param->moveupcontext = optional_param('moveupcontext', 0, PARAM_INT);
+$param->movedowncontext = optional_param('movedowncontext', 0, PARAM_INT);
+$param->tocontext = optional_param('tocontext', 0, PARAM_INT);
+$param->left = optional_param('left', 0, PARAM_INT);
+$param->right = optional_param('right', 0, PARAM_INT);
+$param->delete = optional_param('delete', 0, PARAM_INT);
+$param->confirm = optional_param('confirm', 0, PARAM_INT);
+$param->cancel = optional_param('cancel', '', PARAM_ALPHA);
+$param->move = optional_param('move', 0, PARAM_INT);
+$param->moveto = optional_param('moveto', 0, PARAM_INT);
+$param->edit = optional_param('edit', 0, PARAM_INT);
+
+$url = new moodle_url($thispageurl);
+foreach ((array)$param as $key=>$value) {
+ if (($key !== 'cancel' && $value !== 0) || ($key === 'cancel' && $value !== '')) {
+ $url->param($key, $value);
}
- $PAGE->set_url($url);
- $PAGE->set_pagelayout('standard');
+}
+$PAGE->set_url($url);
+$PAGE->set_pagelayout('standard');
- $qcobject = new question_category_object($pagevars['cpage'], $thispageurl, $contexts->having_one_edit_tab_cap('categories'), $param->edit, $pagevars['cat'], $param->delete,
- $contexts->having_cap('moodle/question:add'));
+$qcobject = new question_category_object($pagevars['cpage'], $thispageurl, $contexts->having_one_edit_tab_cap('categories'), $param->edit, $pagevars['cat'], $param->delete,
+ $contexts->having_cap('moodle/question:add'));
- $streditingcategories = get_string('editcategories', 'quiz');
- if ($param->left || $param->right || $param->moveup || $param->movedown|| $param->moveupcontext || $param->movedowncontext){
- require_sesskey();
- foreach ($qcobject->editlists as $list){
- //processing of these actions is handled in the method where appropriate and page redirects.
- $list->process_actions($param->left, $param->right, $param->moveup, $param->movedown,
- $param->moveupcontext, $param->movedowncontext, $param->tocontext);
- }
+$streditingcategories = get_string('editcategories', 'question');
+if ($param->left || $param->right || $param->moveup || $param->movedown|| $param->moveupcontext || $param->movedowncontext){
+ require_sesskey();
+ foreach ($qcobject->editlists as $list){
+ //processing of these actions is handled in the method where appropriate and page redirects.
+ $list->process_actions($param->left, $param->right, $param->moveup, $param->movedown,
+ $param->moveupcontext, $param->movedowncontext, $param->tocontext);
}
- if ($param->delete && ($questionstomove = $DB->count_records("question", array("category" => $param->delete)))){
- if (!$category = $DB->get_record("question_categories", array("id" => $param->delete))) { // security
- print_error('nocate', 'question', $thispageurl->out(), $param->delete);
- }
- $categorycontext = get_context_instance_by_id($category->contextid);
- $qcobject->moveform = new question_move_form($thispageurl,
- array('contexts'=>array($categorycontext), 'currentcat'=>$param->delete));
- if ($qcobject->moveform->is_cancelled()){
- redirect($thispageurl);
- } elseif ($formdata = $qcobject->moveform->get_data()) {
- /// 'confirm' is the category to move existing questions to
- list($tocategoryid, $tocontextid) = explode(',', $formdata->category);
- $qcobject->move_questions_and_delete_category($formdata->delete, $tocategoryid);
- $thispageurl->remove_params('cat', 'category');
- redirect($thispageurl);
- }
- } else {
- $questionstomove = 0;
+}
+if ($param->delete && ($questionstomove = $DB->count_records("question", array("category" => $param->delete)))){
+ if (!$category = $DB->get_record("question_categories", array("id" => $param->delete))) { // security
+ print_error('nocate', 'question', $thispageurl->out(), $param->delete);
}
- if ($qcobject->catform->is_cancelled()) {
+ $categorycontext = get_context_instance_by_id($category->contextid);
+ $qcobject->moveform = new question_move_form($thispageurl,
+ array('contexts'=>array($categorycontext), 'currentcat'=>$param->delete));
+ if ($qcobject->moveform->is_cancelled()){
redirect($thispageurl);
- } else if ($catformdata = $qcobject->catform->get_data()) {
- if (!$catformdata->id) {//new category
- $qcobject->add_category($catformdata->parent, $catformdata->name, $catformdata->info);
- } else {
- $qcobject->update_category($catformdata->id, $catformdata->parent, $catformdata->name, $catformdata->info);
- }
- redirect($thispageurl);
- } else if ((!empty($param->delete) and (!$questionstomove) and confirm_sesskey())) {
- $qcobject->delete_category($param->delete);//delete the category now no questions to move
+ } elseif ($formdata = $qcobject->moveform->get_data()) {
+ /// 'confirm' is the category to move existing questions to
+ list($tocategoryid, $tocontextid) = explode(',', $formdata->category);
+ $qcobject->move_questions_and_delete_category($formdata->delete, $tocategoryid);
$thispageurl->remove_params('cat', 'category');
redirect($thispageurl);
}
-
- if ($param->edit){
- $PAGE->navbar->add(get_string('editingcategory', 'question'));
- }
-
- $PAGE->set_title($streditingcategories);
- $PAGE->set_heading($COURSE->fullname);
- echo $OUTPUT->header();
-
- // display UI
- if (!empty($param->edit)) {
- $qcobject->edit_single_category($param->edit);
- } else if ($questionstomove){
- $qcobject->display_move_form($questionstomove, $category);
+} else {
+ $questionstomove = 0;
+}
+if ($qcobject->catform->is_cancelled()) {
+ redirect($thispageurl);
+} else if ($catformdata = $qcobject->catform->get_data()) {
+ if (!$catformdata->id) {//new category
+ $qcobject->add_category($catformdata->parent, $catformdata->name, $catformdata->info);
} else {
- // display the user interface
- $qcobject->display_user_interface();
+ $qcobject->update_category($catformdata->id, $catformdata->parent, $catformdata->name, $catformdata->info);
}
- echo $OUTPUT->footer();
+ redirect($thispageurl);
+} else if ((!empty($param->delete) and (!$questionstomove) and confirm_sesskey())) {
+ $qcobject->delete_category($param->delete);//delete the category now no questions to move
+ $thispageurl->remove_params('cat', 'category');
+ redirect($thispageurl);
+}
+if ($param->edit){
+ $PAGE->navbar->add(get_string('editingcategory', 'question'));
+}
+
+$PAGE->set_title($streditingcategories);
+$PAGE->set_heading($COURSE->fullname);
+echo $OUTPUT->header();
+
+// display UI
+if (!empty($param->edit)) {
+ $qcobject->edit_single_category($param->edit);
+} else if ($questionstomove){
+ $qcobject->display_move_form($questionstomove, $category);
+} else {
+ // display the user interface
+ $qcobject->display_user_interface();
+}
+echo $OUTPUT->footer();
diff --git a/question/category_class.php b/question/category_class.php
index 195e29c6eff..8881ce428a2 100644
--- a/question/category_class.php
+++ b/question/category_class.php
@@ -1,19 +1,45 @@
.
+
/**
- * Class representing question categories
+ * A class for representing question categories.
*
- * @author Martin Dougiamas and many others. {@link http://moodle.org}
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package questionbank
+ * @package moodlecore
+ * @subpackage questionbank
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
+defined('MOODLE_INTERNAL') || die();
+
// number of categories to display on page
-define("QUESTION_PAGE_LENGTH", 25);
+define('QUESTION_PAGE_LENGTH', 25);
require_once($CFG->libdir . '/listlib.php');
require_once($CFG->dirroot . '/question/category_form.php');
-require_once('move_form.php');
+require_once($CFG->dirroot . '/question/move_form.php');
+
+/**
+ * Class representing a list of question categories
+ *
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class question_category_list extends moodle_list {
public $table = "question_categories";
public $listitemclassname = 'question_category_list_item';
@@ -39,6 +65,13 @@ class question_category_list extends moodle_list {
}
}
+
+/**
+ * An item in a list of question categories.
+ *
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class question_category_list_item extends list_item {
public function set_icon_html($first, $last, &$lastitem){
global $CFG;
@@ -64,7 +97,7 @@ class question_category_list_item extends list_item {
$str = $extraargs['str'];
$category = $this->item;
- $editqestions = get_string('editquestions', 'quiz');
+ $editqestions = get_string('editquestions', 'question');
/// Each section adds html to be displayed as part of this list item
$questionbankurl = new moodle_url("/question/edit.php", ($this->parentlist->pageurl->params() + array('category'=>"$category->id,$category->contextid")));
@@ -85,9 +118,10 @@ class question_category_list_item extends list_item {
/**
- * Class representing question categories
+ * Class representing q question category
*
- * @package questionbank
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_category_object {
@@ -101,7 +135,7 @@ class question_category_object {
var $newtable;
var $tab;
var $tabsize = 3;
-//------------------------------------------------------
+
/**
* @var moodle_url Object representing url for this page
*/
@@ -122,35 +156,31 @@ class question_category_object {
$this->tab = str_repeat(' ', $this->tabsize);
$this->str->course = get_string('course');
- $this->str->category = get_string('category', 'quiz');
- $this->str->categoryinfo = get_string('categoryinfo', 'quiz');
- $this->str->questions = get_string('questions', 'quiz');
+ $this->str->category = get_string('category', 'question');
+ $this->str->categoryinfo = get_string('categoryinfo', 'question');
+ $this->str->questions = get_string('questions', 'question');
$this->str->add = get_string('add');
$this->str->delete = get_string('delete');
$this->str->moveup = get_string('moveup');
$this->str->movedown = get_string('movedown');
$this->str->edit = get_string('editthiscategory', 'question');
$this->str->hide = get_string('hide');
- $this->str->publish = get_string('publish', 'quiz');
$this->str->order = get_string('order');
- $this->str->parent = get_string('parent', 'quiz');
+ $this->str->parent = get_string('parent', 'question');
$this->str->add = get_string('add');
$this->str->action = get_string('action');
- $this->str->top = get_string('top', 'quiz');
- $this->str->addcategory = get_string('addcategory', 'quiz');
- $this->str->editcategory = get_string('editcategory', 'quiz');
+ $this->str->top = get_string('top');
+ $this->str->addcategory = get_string('addcategory', 'question');
+ $this->str->editcategory = get_string('editcategory', 'question');
$this->str->cancel = get_string('cancel');
- $this->str->editcategories = get_string('editcategories', 'quiz');
+ $this->str->editcategories = get_string('editcategories', 'question');
$this->str->page = get_string('page');
$this->pageurl = $pageurl;
$this->initialize($page, $contexts, $currentcat, $defaultcategory, $todelete, $addcontexts);
-
}
-
-
/**
* Initializes this classes general category-related variables
*/
@@ -175,6 +205,7 @@ class question_category_object {
$this->catform->set_data(array('parent'=>$defaultcategory));
}
}
+
/**
* Displays the user interface
*
@@ -222,8 +253,6 @@ class question_category_object {
echo $list->display_page_numbers();
}
-
-
/**
* gets all the courseids for the given categories
*
@@ -241,8 +270,6 @@ class question_category_object {
return $courseids;
}
-
-
public function edit_single_category($categoryid) {
/// Interface for adding a new category
global $COURSE, $DB;
@@ -259,7 +286,6 @@ class question_category_object {
}
}
-
/**
* Sets the viable parents
*
@@ -323,10 +349,10 @@ class question_category_object {
public function display_move_form($questionsincategory, $category){
global $OUTPUT;
- $vars = new stdClass;
+ $vars = new stdClass();
$vars->name = $category->name;
$vars->count = $questionsincategory;
- echo $OUTPUT->box(get_string('categorymove', 'quiz', $vars), 'generalbox boxaligncenter');
+ echo $OUTPUT->box(get_string('categorymove', 'question', $vars), 'generalbox boxaligncenter');
$this->moveform->display();
}
@@ -343,7 +369,7 @@ class question_category_object {
public function add_category($newparent, $newcategory, $newinfo, $return = false) {
global $DB;
if (empty($newcategory)) {
- print_error('categorynamecantbeblank', 'quiz');
+ print_error('categorynamecantbeblank', 'question');
}
list($parentid, $contextid) = explode(',', $newparent);
//moodle_form makes sure select element output is legal no need for further cleaning
@@ -374,9 +400,9 @@ class question_category_object {
* Updates an existing category with given params
*/
public function update_category($updateid, $newparent, $newname, $newinfo) {
- global $CFG, $QTYPES, $DB;
+ global $CFG, $DB;
if (empty($newname)) {
- print_error('categorynamecantbeblank', 'quiz');
+ print_error('categorynamecantbeblank', 'question');
}
// Get the record we are updating.
@@ -401,7 +427,7 @@ class question_category_object {
}
// Update the category record.
- $cat = NULL;
+ $cat = null;
$cat->id = $updateid;
$cat->name = $newname;
$cat->info = $newinfo;
@@ -413,10 +439,11 @@ class question_category_object {
if ($oldcat->name != $cat->name) {
$where = "qtype = 'random' AND category = ? AND " . $DB->sql_compare_text('questiontext') . " = ?";
- $randomqname = $QTYPES[RANDOM]->question_name($cat, false);
+ $randomqtype = question_bank::get_qtype('random');
+ $randomqname = $randomqtype->question_name($cat, false);
$DB->set_field_select('question', 'name', $randomqname, $where, array($cat->id, '0'));
- $randomqname = $QTYPES[RANDOM]->question_name($cat, true);
+ $randomqname = $randomqtype->question_name($cat, true);
$DB->set_field_select('question', 'name', $randomqname, $where, array($cat->id, '1'));
}
diff --git a/question/category_form.php b/question/category_form.php
index 22ece91b741..357b1a4f8be 100644
--- a/question/category_form.php
+++ b/question/category_form.php
@@ -1,21 +1,50 @@
.
-if (!defined('MOODLE_INTERNAL')) {
- die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
-}
+/**
+ * Defines the form for editing question categories.
+ *
+ * @package moodlecore
+ * @subpackage questionbank
+ * @copyright 2007 Jamie Pratt me@jamiep.org
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Form for editing qusetions categories (name, description, etc.)
+ *
+ * @copyright 2007 Jamie Pratt me@jamiep.org
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
class question_category_edit_form extends moodleform {
- function definition() {
+ protected function definition() {
global $CFG, $DB;
- $mform =& $this->_form;
+ $mform = $this->_form;
$contexts = $this->_customdata['contexts'];
$currentcat = $this->_customdata['currentcat'];
-//--------------------------------------------------------------------------------
- $mform->addElement('header', 'categoryheader', get_string('addcategory', 'quiz'));
+
+ $mform->addElement('header', 'categoryheader', get_string('addcategory', 'question'));
$questioncategoryel = $mform->addElement('questioncategory', 'parent', get_string('parentcategory', 'question'),
array('contexts'=>$contexts, 'top'=>true, 'currentcat'=>$currentcat, 'nochildrenof'=>$currentcat));
@@ -25,17 +54,17 @@ class question_category_edit_form extends moodleform {
}
$mform->addHelpButton('parent', 'parentcategory', 'question');
- $mform->addElement('text','name', get_string('name'),'maxlength="254" size="50"');
+ $mform->addElement('text', 'name', get_string('name'),'maxlength="254" size="50"');
$mform->setDefault('name', '');
- $mform->addRule('name', get_string('categorynamecantbeblank', 'quiz'), 'required', null, 'client');
+ $mform->addRule('name', get_string('categorynamecantbeblank', 'question'), 'required', null, 'client');
$mform->setType('name', PARAM_MULTILANG);
- $mform->addElement('textarea', 'info', get_string('categoryinfo', 'quiz'), array('rows'=> '10', 'cols'=>'45'));
+ $mform->addElement('textarea', 'info', get_string('categoryinfo', 'question'), array('rows'=> '10', 'cols'=>'45'));
$mform->setDefault('info', '');
$mform->setType('info', PARAM_MULTILANG);
-//--------------------------------------------------------------------------------
- $this->add_action_buttons(false, get_string('addcategory', 'quiz'));
-//--------------------------------------------------------------------------------
+
+ $this->add_action_buttons(false, get_string('addcategory', 'question'));
+
$mform->addElement('hidden', 'id', 0);
$mform->setType('id', PARAM_INT);
}
diff --git a/question/edit.php b/question/edit.php
index f19bb1c3444..5373a89f4e6 100644
--- a/question/edit.php
+++ b/question/edit.php
@@ -1,81 +1,74 @@
.
/**
* Page to edit the question bank
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package questionbank
+ * @package moodlecore
+ * @subpackage questionbank
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
- require_once("../config.php");
- require_once("editlib.php");
- $url = new moodle_url('/question/edit.php');
- if (($lastchanged = optional_param('lastchanged', 0, PARAM_INT)) !== 0) {
- $url->param('lastchanged', $lastchanged);
- }
- if (($category = optional_param('category', 0, PARAM_TEXT)) !== 0) {
- $url->param('category', $category);
- }
- if (($qpage = optional_param('qpage', 0, PARAM_INT)) !== 0) {
- $url->param('qpage', $qpage);
- }
- if (($cat = optional_param('cat', 0, PARAM_TEXT)) !== 0) {
- $url->param('cat', $cat);
- }
- if (($courseid = optional_param('courseid', 0, PARAM_INT)) !== 0) {
- $url->param('courseid', $courseid);
- }
- if (($returnurl = optional_param('returnurl', 0, PARAM_INT)) !== 0) {
- $url->param('returnurl', $returnurl);
- }
- if (($cmid = optional_param('cmid', 0, PARAM_INT)) !== 0) {
- $url->param('cmid', $cmid);
- }
- $PAGE->set_url($url);
- $PAGE->set_pagelayout('standard');
+require_once(dirname(__FILE__) . '/../config.php');
+require_once($CFG->dirroot . '/question/editlib.php');
- list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
- question_edit_setup('questions', '/question/edit.php');
- $questionbank = new question_bank_view($contexts, $thispageurl, $COURSE, $cm);
- $questionbank->process_actions();
+$url = new moodle_url('/question/edit.php');
+if (($lastchanged = optional_param('lastchanged', 0, PARAM_INT)) !== 0) {
+ $url->param('lastchanged', $lastchanged);
+}
+if (($category = optional_param('category', 0, PARAM_TEXT)) !== 0) {
+ $url->param('category', $category);
+}
+if (($qpage = optional_param('qpage', 0, PARAM_INT)) !== 0) {
+ $url->param('qpage', $qpage);
+}
+if (($cat = optional_param('cat', 0, PARAM_TEXT)) !== 0) {
+ $url->param('cat', $cat);
+}
+if (($courseid = optional_param('courseid', 0, PARAM_INT)) !== 0) {
+ $url->param('courseid', $courseid);
+}
+if (($returnurl = optional_param('returnurl', 0, PARAM_INT)) !== 0) {
+ $url->param('returnurl', $returnurl);
+}
+if (($cmid = optional_param('cmid', 0, PARAM_INT)) !== 0) {
+ $url->param('cmid', $cmid);
+}
+$PAGE->set_url($url);
+$PAGE->set_pagelayout('standard');
- // TODO log this page view.
+list($thispageurl, $contexts, $cmid, $cm, $module, $pagevars) =
+ question_edit_setup('questions', '/question/edit.php');
+$questionbank = new question_bank_view($contexts, $thispageurl, $COURSE, $cm);
+$questionbank->process_actions();
- $context = $contexts->lowest();
- $streditingquestions = get_string('editquestions', "quiz");
- $PAGE->set_title($streditingquestions);
- $PAGE->set_heading($COURSE->fullname);
- echo $OUTPUT->header();
+// TODO log this page view.
- echo '';
- $questionbank->display('questions', $pagevars['qpage'],
- $pagevars['qperpage'], $pagevars['qsortorder'], $pagevars['qsortorderdecoded'],
- $pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'], $pagevars['showquestiontext']);
- echo "
\n";
+$context = $contexts->lowest();
+$streditingquestions = get_string('editquestions', 'question');
+$PAGE->set_title($streditingquestions);
+$PAGE->set_heading($COURSE->fullname);
+echo $OUTPUT->header();
- echo $OUTPUT->footer();
+echo '';
+$questionbank->display('questions', $pagevars['qpage'],
+ $pagevars['qperpage'], $pagevars['qsortorder'], $pagevars['qsortorderdecoded'],
+ $pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'], $pagevars['showquestiontext']);
+echo "
\n";
+echo $OUTPUT->footer();
diff --git a/question/editlib.php b/question/editlib.php
index 6e75c60967b..9848e870a32 100644
--- a/question/editlib.php
+++ b/question/editlib.php
@@ -1,36 +1,32 @@
.
/**
* Functions used to show question editing interface
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package questionbank
- *//** */
+ * @package moodlecore
+ * @subpackage questionbank
+ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
-require_once($CFG->libdir.'/questionlib.php');
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/questionlib.php');
define('DEFAULT_QUESTIONS_PER_PAGE', 20);
@@ -61,8 +57,7 @@ function get_module_from_cmid($cmid) {
* @author added by Howard Miller June 2004
*/
function get_questions_category( $category, $noparent=false, $recurse=true, $export=true ) {
-
- global $QTYPES, $DB;
+ global $DB;
// questions will be added to an array
$qresults = array();
@@ -73,23 +68,21 @@ function get_questions_category( $category, $noparent=false, $recurse=true, $exp
$npsql = " and parent='0' ";
}
- // get (list) of categories
+ // Get list of categories
if ($recurse) {
$categorylist = question_categorylist($category->id);
- }
- else {
- $categorylist = $category->id;
+ } else {
+ $categorylist = array($category->id);
}
// get the list of questions for the category
- list ($usql, $params) = $DB->get_in_or_equal(explode(',', $categorylist));
- if ($questions = $DB->get_records_select("question","category $usql $npsql", $params, "qtype, name ASC")) {
+ list($usql, $params) = $DB->get_in_or_equal($categorylist);
+ if ($questions = $DB->get_records_select('question', "category $usql $npsql", $params, 'qtype, name')) {
// iterate through questions, getting stuff we need
foreach($questions as $question) {
- $questiontype = $QTYPES[$question->qtype];
$question->export_process = $export;
- $questiontype->get_question_options($question);
+ question_bank::get_qtype($question->qtype)->get_question_options($question);
$qresults[] = $question;
}
}
@@ -98,8 +91,8 @@ function get_questions_category( $category, $noparent=false, $recurse=true, $exp
}
/**
- * @param integer $categoryid a category id.
- * @return boolean whether this is the only top-level category in a context.
+ * @param int $categoryid a category id.
+ * @return bool whether this is the only top-level category in a context.
*/
function question_is_only_toplevel_category_in_context($categoryid) {
global $DB;
@@ -115,7 +108,7 @@ function question_is_only_toplevel_category_in_context($categoryid) {
/**
* Check whether this user is allowed to delete this category.
*
- * @param integer $todelete a category id.
+ * @param int $todelete a category id.
*/
function question_can_delete_cat($todelete) {
global $DB;
@@ -127,6 +120,13 @@ function question_can_delete_cat($todelete) {
}
}
+
+/**
+ * Base class for representing a column in a {@link question_bank_view}.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
abstract class question_bank_column_base {
/**
* @var question_bank_view
@@ -155,7 +155,7 @@ abstract class question_bank_column_base {
/**
* Output the column header cell.
- * @param integer $currentsort 0 for none. 1 for normal sort, -1 for reverse sort.
+ * @param int $currentsort 0 for none. 1 for normal sort, -1 for reverse sort.
*/
public function display_header() {
echo '