### Eclipse Workspace Patch 1.0 #P moodle20 Index: mod/quiz/tabs.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/tabs.php,v retrieving revision 1.40 diff -u -r1.40 tabs.php --- mod/quiz/tabs.php 17 Jan 2010 10:54:13 -0000 1.40 +++ mod/quiz/tabs.php 10 Feb 2010 03:27:27 -0000 @@ -45,6 +45,9 @@ if (has_capability('mod/quiz:manage', $context)) { $row[] = new tabobject('edit', "$CFG->wwwroot/mod/quiz/edit.php?cmid=$cm->id", "pix_url('t/edit') . "\" class=\"iconsmall\" alt=\"$stredit\" /> $stredit",$stredit); } +if (has_capability('mod/quiz:manage', $context)) { + $row[] = new tabobject('overrides', "$CFG->wwwroot/mod/quiz/override.php?cmid=$cm->id", get_string('overrides', 'quiz')); +} if ($currenttab == 'info' && count($row) == 1) { // Don't show only an info tab (e.g. to students). Index: mod/quiz/lib.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/lib.php,v retrieving revision 1.354 diff -u -r1.354 lib.php --- mod/quiz/lib.php 28 Jan 2010 07:57:48 -0000 1.354 +++ mod/quiz/lib.php 10 Feb 2010 03:27:26 -0000 @@ -179,6 +179,7 @@ } quiz_delete_all_attempts($quiz); + quiz_delete_all_overrides($quiz); $DB->delete_records('quiz_question_instances', array('quiz' => $quiz->id)); $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id)); @@ -196,6 +197,128 @@ } /** + * Deletes a quiz override from the database and clears any corresponding calendar events + * + * @global object + * @param object $quiz The quiz object. + * @param integer $overrideid The id of the override being deleted + * @return bool + */ +function quiz_delete_override($quiz, $overrideid) { + global $DB; + + if (!$override = $DB->get_record('quiz_overrides', array('id' => $overrideid))) { + return false; + } + $groupid = empty($override->groupid)? 0 : $override->groupid; + $userid = empty($override->userid)? 0 : $override->userid; + + // Delete the events + if ($events = $DB->get_records('event', array('modulename'=>'quiz', 'instance'=>$quiz->id, 'groupid'=>$groupid, 'userid'=>$userid))) { + foreach($events as $event) { + $event2old = calendar_event::load($event); + $event2old->delete(); + } + } + + return $DB->delete_records('quiz_overrides', array('id' => $overrideid)); +} + +/** + * Deletes all quiz overrides from the database and clears any corresponding calendar events + * + * @global object + * @param object $quiz The quiz object. + */ +function quiz_delete_all_overrides($quiz) { + global $DB; + + if (!$overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id), 'id')) { + return false; + } + foreach ($overrides as $override) { + quiz_delete_override($quiz, $override->id); + } +} + +/** + * Computes the effective override for a user taking the quiz + * + * Algorithm: If there is a matching user-specific override, then use that + * otherwise, if there are group-specific overrides, return the most lenient + * combination of them. If neither applies, return the defaults for the quiz. + * + * @global object + * @param object $quiz The quiz object. + * @param object $user The user object, or $USER if null. + * @return object $override + */ +function quiz_compute_effective_override($quiz, $user = null) { + global $USER, $DB; + + $effective = new stdClass; + $effective->timeopen = $quiz->timeopen; + $effective->timeclose = $quiz->timeclose; + $effective->timelimit = $quiz->timelimit; + + if (empty($user)) { + $user = $USER; + } + + // user override + $override = $DB->get_record('quiz_overrides', array('quiz' => $quiz->id, 'userid' => $user->id)); + + if (!$override) { + // group override + $groupings = groups_get_user_groups($quiz->course, $user->id); + $groupingid = empty($cm->groupingid)? 0 : $cm->groupingid; + + if (!empty($groupings[$groupingid])) { + + // Select all overrides that apply to the User's groups + list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[$groupingid])); + $SQL = "SELECT * + FROM {quiz_overrides} + WHERE quiz = ".$quiz->id." + AND groupid $extra"; + if (!$records = $DB->get_records_sql($SQL, $params)) { + $records = array(); + } + + // Combine the overrides + $opens = array(); + $closes = array(); + $limits = array(); + + foreach ($records as $gpoverride) { + if (!empty($gpoverride->timeopen)) { + $opens[] = $gpoverride->timeopen; + } + if (!empty($gpoverride->timeclose)) { + $closes[] = $gpoverride->timeclose; + } + if (!empty($gpoverride->timelimit)) { + $limits[] = $gpoverride->timelimit; + } + } + $override = new stdClass; + $override->timeopen = count($opens)? min($opens) : 0; + $override->timeclose = count($closes)? max($closes) : 0; + $override->timelimit = count($limits)? max($limits) : 0; + } + } + + if ($override) { + // merge with course defaults + $effective->timeopen = ($override->timeopen)? $override->timeopen : $effective->timeopen; + $effective->timeclose = ($override->timeclose)? $override->timeclose : $effective->timeclose; + $effective->timelimit = ($override->timelimit)? $override->timelimit : $effective->timelimit; + } + + return $effective; +} + +/** * Delete all the attempts belonging to a quiz. * * @global stdClass @@ -639,74 +762,11 @@ return true; } } - $moduleid = $DB->get_field('modules', 'id', array('name' => 'quiz')); foreach ($quizzes as $quiz) { - $cm = get_coursemodule_from_id('quiz', $quiz->id); - $event = NULL; - $event2 = NULL; - $event2old = NULL; - - if ($events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id), 'timestart')) { - $event = array_shift($events); - if (!empty($events)) { - $event2old = array_shift($events); - if (!empty($events)) { - foreach ($events as $badevent) { - $badevent = calendar_event::load($badevent); - $badevent->delete(); - } - } - } - } - - $event->name = $quiz->name; - $event->description = format_module_intro('quiz', $quiz, $cm->id); - $event->courseid = $quiz->course; - $event->groupid = 0; - $event->userid = 0; - $event->modulename = 'quiz'; - $event->instance = $quiz->id; - $event->visible = instance_is_visible('quiz', $quiz); - $event->timestart = $quiz->timeopen; - $event->eventtype = 'open'; - $event->timeduration = ($quiz->timeclose - $quiz->timeopen); - - if ($event->timeduration > QUIZ_MAX_EVENT_LENGTH) { /// Set up two events - - $event2 = $event; - - $event->name = $quiz->name.' ('.get_string('quizopens', 'quiz').')'; - $event->timeduration = 0; - - $event2->name = $quiz->name.' ('.get_string('quizcloses', 'quiz').')'; - $event2->timestart = $quiz->timeclose; - $event2->eventtype = 'close'; - $event2->timeduration = 0; - - if (empty($event2old->id)) { - unset($event2->id); - calendar_event::create($event2); - } else { - $event2->id = $event2old->id; - $event2 = calendar_event::load($event2); - $event2->update($event2); - } - } else if (!empty($event2old->id)) { - $event2old = calendar_event::load($event2old); - $event2old->delete(); - } - - if (empty($event->id)) { - if (!empty($event->timestart)) { - calendar_event::create($event); - } - } else { - $event = calendar_event::load($event); - $event->update($event); - } - + quiz_events_update($quiz); } + return true; } @@ -1079,9 +1139,53 @@ } // Update the events relating to this quiz. + quiz_events_update($quiz); + + //update related grade item + quiz_grade_item_update($quiz); + +} + +/** + * This function updates the events associated to the quiz. + * If $override is non-zero, then it updates only the events + * associated with the specified override. + * + * @global object + * @uses QUIZ_MAX_EVENT_LENGTH + * @param object $quiz the quiz object. + * @param object optional $override + */ +function quiz_events_update($quiz, $override=null) { + global $DB; + + $groupid = empty($override->groupid)? 0 : $override->groupid; + $userid = empty($override->userid)? 0 : $override->userid; + $timeopen = empty($override->timeopen)? $quiz->timeopen : $override->timeopen; + $timeclose = empty($override->timeclose)? $quiz->timeclose : $override->timeclose; + + // only add open/close events for an override if they differ from the quiz default + $addopen = empty($override) || !empty($override->timeopen); + $addclose = empty($override) || !empty($override->timeclose); + + $overridename = ''; + if ($override) { + if ($groupid) { + $overridename .= ' - '.groups_get_group_name($groupid).' '; + } + else { + $overridename .= ' - '.get_string('overrideuserstr', 'quiz').' '; + } + } + + // Update the events relating to this quiz. // This is slightly inefficient, deleting the old events and creating new ones. However, // there are at most two events, and this keeps the code simpler. - if ($events = $DB->get_records('event', array('modulename'=>'quiz', 'instance'=>$quiz->id))) { + $conds = array('modulename'=>'quiz', + 'instance'=>$quiz->id, + 'groupid'=>$groupid, + 'userid'=>$userid); + if ($events = $DB->get_records('event', $conds)) { foreach($events as $event) { $event2old = calendar_event::load($event); $event2old->delete(); @@ -1090,38 +1194,46 @@ $event = new stdClass; $event->description = $quiz->intro; - $event->courseid = $quiz->course; - $event->groupid = 0; - $event->userid = 0; + $event->courseid = ($userid)? 0 : $quiz->course; // Events module won't show user events when the courseid is nonzero + $event->groupid = $groupid; + $event->userid = $userid; $event->modulename = 'quiz'; $event->instance = $quiz->id; - $event->timestart = $quiz->timeopen; - $event->timeduration = $quiz->timeclose - $quiz->timeopen; + $event->timestart = $timeopen; + $event->timeduration = $timeclose - $timeopen; $event->visible = instance_is_visible('quiz', $quiz); $event->eventtype = 'open'; - if ($quiz->timeclose and $quiz->timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) { + if ($timeclose and $timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH + and ($addopen or $addclose)) { // Single event for the whole quiz. - $event->name = $quiz->name; + $event->name = $quiz->name.$overridename; calendar_event::create($event); } else { // Separate start and end events. $event->timeduration = 0; - if ($quiz->timeopen) { - $event->name = $quiz->name.' ('.get_string('quizopens', 'quiz').')'; + if ($timeopen && $addopen) { + $event->name = $quiz->name.$overridename.' ('.get_string('quizopens', 'quiz').')'; calendar_event::create($event); unset($event->id); // So we can use the same object for the close event. } - if ($quiz->timeclose) { - $event->name = $quiz->name.' ('.get_string('quizcloses', 'quiz').')'; - $event->timestart = $quiz->timeclose; + if ($timeclose && $addclose) { + $event->name = $quiz->name.$overridename.' ('.get_string('quizcloses', 'quiz').')'; + $event->timestart = $timeclose; $event->eventtype = 'close'; calendar_event::create($event); } } - //update related grade item - quiz_grade_item_update($quiz); + if (empty($override)) { + // We are updating the primary settings for the quiz, so all overrides have to update as well + if (!$overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id))) { + $overrides = array(); + } + foreach ($overrides as $override) { + quiz_events_update($quiz, $override); + } + } } /** Index: mod/quiz/version.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/version.php,v retrieving revision 1.144 diff -u -r1.144 version.php --- mod/quiz/version.php 19 Nov 2009 17:31:41 -0000 1.144 +++ mod/quiz/version.php 10 Feb 2010 03:27:27 -0000 @@ -5,7 +5,7 @@ // This fragment is called by moodle_needs_upgrading() and /admin/index.php //////////////////////////////////////////////////////////////////////////////// -$module->version = 2009111900; // The (date) version of this module +$module->version = 2010020901; // The (date) version of this module $module->requires = 2009041700; // Requires this Moodle version $module->cron = 0; // How often should cron check this module (seconds)? Index: mod/quiz/attemptlib.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/attemptlib.php,v retrieving revision 1.56 diff -u -r1.56 attemptlib.php --- mod/quiz/attemptlib.php 6 Feb 2010 14:09:33 -0000 1.56 +++ mod/quiz/attemptlib.php 10 Feb 2010 03:27:25 -0000 @@ -64,6 +64,7 @@ protected $context; protected $questionids; // All question ids in order that they appear in the quiz. protected $pagequestionids; // array page no => array of questionids on the page in order. + protected $override; // override information for the current user // Fields set later if that data is needed. protected $questions = null; @@ -87,6 +88,10 @@ if ($getcontext && !empty($cm->id)) { $this->context = get_context_instance(CONTEXT_MODULE, $cm->id); } + + // Find which override applies to this attempt + $this->override = quiz_compute_effective_override($quiz); + $this->determine_layout(); } @@ -174,6 +179,21 @@ return $this->cm; } + /** @return integer the effective open time for this user. */ + public function get_effective_timeopen() { + return $this->override->timeopen; + } + + /** @return integer the effective close time for this user. */ + public function get_effective_timeclose() { + return $this->override->timeclose; + } + + /** @return integer the effective time limit for this user. */ + public function get_effective_timelimit() { + return $this->override->timelimit; + } + /** * @return boolean wether the current user is someone who previews the quiz, * rather than attempting it. Index: mod/quiz/locallib.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/locallib.php,v retrieving revision 1.191 diff -u -r1.191 locallib.php --- mod/quiz/locallib.php 30 Dec 2009 15:19:55 -0000 1.191 +++ mod/quiz/locallib.php 10 Feb 2010 03:27:27 -0000 @@ -934,10 +934,11 @@ $options->quizstate = QUIZ_STATE_TEACHERACCESS; } else { // Work out the state of the attempt ... + $override = quiz_compute_effective_override($quiz); 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) { + } else if (!$override->timeclose or time() < $override->timeclose) { $quiz_state_mask = QUIZ_REVIEW_OPEN; $options->quizstate = QUIZ_STATE_OPEN; } else { Index: mod/quiz/accessrules.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/accessrules.php,v retrieving revision 1.38 diff -u -r1.38 accessrules.php --- mod/quiz/accessrules.php 26 Jan 2010 09:42:19 -0000 1.38 +++ mod/quiz/accessrules.php 10 Feb 2010 03:27:25 -0000 @@ -31,7 +31,7 @@ $this->_rules[] = new num_attempts_access_rule($this->_quizobj, $this->_timenow); } $this->_rules[] = new open_close_date_access_rule($this->_quizobj, $this->_timenow); - if (!empty($quiz->timelimit) && !$canignoretimelimits) { + if ($this->_quizobj->get_effective_timelimit() > 0 && !$canignoretimelimits) { $this->_rules[] = new time_limit_access_rule($this->_quizobj, $this->_timenow); } if (!empty($quiz->delay1) || !empty($quiz->delay2)) { @@ -317,9 +317,9 @@ */ public function confirm_start_attempt_message() { $quiz = $this->_quizobj->get_quiz(); - if ($quiz->timelimit && $quiz->attempts) { + if ($this->_quizobj->get_effective_timelimit() && $quiz->attempts) { return get_string('confirmstartattempttimelimit','quiz', $quiz->attempts); - } else if ($quiz->timelimit) { + } else if ($this->_quizobj->get_effective_timelimit()) { return get_string('confirmstarttimelimit','quiz'); } else if ($quiz->attempts) { return get_string('confirmstartattemptlimit','quiz', $quiz->attempts); @@ -380,9 +380,9 @@ } if ($reviewoptions->quizstate == QUIZ_STATE_IMMEDIATELY) { return ''; - } else if ($reviewoptions->quizstate == QUIZ_STATE_OPEN && $quiz->timeclose && + } else if ($reviewoptions->quizstate == QUIZ_STATE_OPEN && $this->_quizobj->get_effective_timeclose() && ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES)) { - return get_string('noreviewuntil' . $langstrsuffix, 'quiz', userdate($quiz->timeclose, $dateformat)); + return get_string('noreviewuntil' . $langstrsuffix, 'quiz', userdate($this->_quizobj->get_effective_timeclose(), $dateformat)); } else { return get_string('noreview' . $langstrsuffix, 'quiz'); } @@ -488,41 +488,47 @@ class open_close_date_access_rule extends quiz_access_rule_base { public function description() { $result = array(); - if ($this->_timenow < $this->_quiz->timeopen) { - $result[] = get_string('quiznotavailable', 'quiz', userdate($this->_quiz->timeopen)); - } else if ($this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose) { - $result[] = get_string("quizclosed", "quiz", userdate($this->_quiz->timeclose)); + $timeopen = $this->_quizobj->get_effective_timeopen(); + $timeclose = $this->_quizobj->get_effective_timeclose(); + if ($this->_timenow < $timeopen) { + $result[] = get_string('quiznotavailable', 'quiz', userdate($timeopen)); + } else if ($timeclose && $this->_timenow > $timeclose) { + $result[] = get_string("quizclosed", "quiz", userdate($timeclose)); } else { - if ($this->_quiz->timeopen) { - $result[] = get_string('quizopenedon', 'quiz', userdate($this->_quiz->timeopen)); + if ($timeopen) { + $result[] = get_string('quizopenedon', 'quiz', userdate($timeopen)); } - if ($this->_quiz->timeclose) { - $result[] = get_string('quizcloseson', 'quiz', userdate($this->_quiz->timeclose)); + if ($timeclose) { + $result[] = get_string('quizcloseson', 'quiz', userdate($timeclose)); } } return $result; } public function prevent_access() { - if ($this->_timenow < $this->_quiz->timeopen || - ($this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose)) { + $timeopen = $this->_quizobj->get_effective_timeopen(); + $timeclose = $this->_quizobj->get_effective_timeclose(); + if ($this->_timenow < $timeopen || + ($timeclose && $this->_timenow > $timeclose)) { return get_string('notavailable', 'quiz'); } return false; } public function is_finished($numprevattempts, $lastattempt) { - return $this->_quiz->timeclose && $this->_timenow > $this->_quiz->timeclose; + $timeclose = $this->_quizobj->get_effective_timeclose(); + return $timeclose && $this->_timenow > $timeclose; } public function time_left($attempt, $timenow) { + $timeclose = $this->_quizobj->get_effective_timeclose(); // If this is a teacher preview after the close date, do not show // the time. - if ($attempt->preview && $timenow > $this->_quiz->timeclose) { + if ($attempt->preview && $timenow > $timeclose) { return false; } // Otherwise, return to the time left until the close date, providing // that is less than QUIZ_SHOW_TIME_BEFORE_DEADLINE - if ($this->_quiz->timeclose) { - $timeleft = $this->_quiz->timeclose - $timenow; + if ($timeclose) { + $timeleft = $timeclose - $timenow; if ($timeleft < QUIZ_SHOW_TIME_BEFORE_DEADLINE) { return $timeleft; } @@ -536,11 +542,12 @@ */ class inter_attempt_delay_access_rule extends quiz_access_rule_base { public function prevent_new_attempt($numprevattempts, $lastattempt) { + $timeclose = $this->_quizobj->get_effective_timeclose(); if ($this->_quiz->attempts > 0 && $numprevattempts >= $this->_quiz->attempts) { /// No more attempts allowed anyway. return false; } - if ($this->_quiz->timeclose != 0 && $this->_timenow > $this->_quiz->timeclose) { + if ($timeclose != 0 && $this->_timenow > $timeclose) { /// No more attempts allowed anyway. return false; } @@ -551,7 +558,7 @@ $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay2; } if ($this->_timenow < $nextstarttime) { - if ($this->_quiz->timeclose == 0 || $nextstarttime <= $this->_quiz->timeclose) { + if ($timeclose == 0 || $nextstarttime <= $timeclose) { return get_string('youmustwait', 'quiz', userdate($nextstarttime)); } else { return get_string('youcannotwait', 'quiz'); @@ -566,8 +573,9 @@ } else if ($numprevattempts > 1 && $this->_quiz->delay2) { $nextstarttime = $lastattempt->timefinish + $this->_quiz->delay2; } + $timeclose = $this->_quizobj->get_effective_timeclose(); return $this->_timenow <= $nextstarttime && - $this->_quiz->timeclose != 0 && $nextstarttime >= $this->_quiz->timeclose; + $timeclose != 0 && $nextstarttime >= $timeclose; } } @@ -686,10 +694,10 @@ */ class time_limit_access_rule extends quiz_access_rule_base { public function description() { - return get_string('quiztimelimit', 'quiz', format_time($this->_quiz->timelimit)); + return get_string('quiztimelimit', 'quiz', format_time($this->_quizobj->get_effective_timelimit())); } public function time_left($attempt, $timenow) { - return $attempt->timestart + $this->_quiz->timelimit - $timenow; + return $attempt->timestart + $this->_quizobj->get_effective_timelimit() - $timenow; } } Index: mod/quiz/view.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/view.php,v retrieving revision 1.182 diff -u -r1.182 view.php --- mod/quiz/view.php 16 Jan 2010 15:40:05 -0000 1.182 +++ mod/quiz/view.php 10 Feb 2010 03:27:27 -0000 @@ -82,7 +82,7 @@ $PAGE->set_title($title); $PAGE->set_heading($course->fullname); - + echo $OUTPUT->header(); /// Print heading and tabs (if there is more than one). @@ -237,13 +237,16 @@ // 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); + $override = quiz_compute_effective_override($quiz); + if (!$override->timeclose || $timenow < $override->timeclose) { + // The attempt is still in progress. + $timetaken = format_time($timenow - $attempt->timestart); + $datecompleted = ''; + } else { + $timetaken = format_time($override->timeclose - $attempt->timestart); + $datecompleted = userdate($override->timeclose); + } } $row[] = $datecompleted; Index: mod/quiz/db/install.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/db/install.php,v retrieving revision 1.3 diff -u -r1.3 install.php --- mod/quiz/db/install.php 4 Nov 2009 11:58:33 -0000 1.3 +++ mod/quiz/db/install.php 10 Feb 2010 03:27:27 -0000 @@ -21,6 +21,7 @@ update_log_display_entry('quiz', 'start attempt', 'quiz', 'name'); update_log_display_entry('quiz', 'close attempt', 'quiz', 'name'); update_log_display_entry('quiz', 'continue attempt', 'quiz', 'name'); + update_log_display_entry('quiz', 'edit overrides', 'quiz', 'name'); $record = new object(); $record->name = 'overview'; Index: mod/quiz/db/upgrade.php =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/db/upgrade.php,v retrieving revision 1.33 diff -u -r1.33 upgrade.php --- mod/quiz/db/upgrade.php 4 Nov 2009 11:58:33 -0000 1.33 +++ mod/quiz/db/upgrade.php 10 Feb 2010 03:27:28 -0000 @@ -292,6 +292,39 @@ upgrade_mod_savepoint($result, 2009042000, 'quiz'); } + if ($result && $oldversion < 2010020901) { + /// fix log actions + update_log_display_entry('quiz', 'edit overrides', 'quiz', 'name'); + + /// Define table quiz_overrides to be created + $table = new xmldb_table('quiz_overrides'); + + /// Adding fields to table quiz_overrides + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('quiz', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + $table->add_field('groupid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + $table->add_field('timeopen', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + $table->add_field('timeclose', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + $table->add_field('timelimit', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0'); + + /// Adding keys to table quiz_overrides + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('quiz', XMLDB_KEY_FOREIGN, array('quiz'), 'quiz', array('id')); + + /// Adding indexes to table quiz_overrides + $table->add_index('userid', XMLDB_INDEX_NOTUNIQUE, array('userid')); + $table->add_index('groupid', XMLDB_INDEX_NOTUNIQUE, array('groupid')); + + /// Conditionally launch create table for quiz_overrides + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + /// quiz savepoint reached + upgrade_mod_savepoint($result, 2010020901, 'quiz'); + } + return $result; } Index: mod/quiz/db/install.xml =================================================================== RCS file: /cvsroot/moodle/moodle/mod/quiz/db/install.xml,v retrieving revision 1.23 diff -u -r1.23 install.xml --- mod/quiz/db/install.xml 1 May 2009 14:07:48 -0000 1.23 +++ mod/quiz/db/install.xml 10 Feb 2010 03:27:27 -0000 @@ -1,5 +1,5 @@ - @@ -109,7 +109,7 @@ - +
@@ -122,5 +122,24 @@
+ + + + + + + + + + + + + + + + + + +
\ No newline at end of file Index: lang/en_utf8/quiz.php =================================================================== RCS file: /cvsroot/moodle/moodle/lang/en_utf8/quiz.php,v retrieving revision 1.143 diff -u -r1.143 quiz.php --- lang/en_utf8/quiz.php 30 Sep 2009 10:57:57 -0000 1.143 +++ lang/en_utf8/quiz.php 10 Feb 2010 03:27:24 -0000 @@ -26,8 +26,10 @@ $string['addingshortanswer'] = 'Adding a Short-Answer question'; $string['addingtruefalse'] = 'Adding a True/False question'; $string['addmoreoverallfeedbacks'] = 'Add {no} more feedback fields'; +$string['addnewgroupoverride'] = 'Add group override'; $string['addnewpagesafterselected'] = 'Add new pages after selected questions'; $string['addnewquestionsqbank'] = 'Add questions to the category $a->catname: $a->link'; +$string['addnewuseroverride'] = 'Add user override'; $string['addpagehere'] = 'Add page here'; $string['addquestion'] = 'Add question'; $string['addquestions'] = 'Add questions'; @@ -248,6 +250,7 @@ $string['editquestions'] = 'Edit questions'; $string['editquiz'] = 'Edit Quiz'; $string['editquizquestions'] = 'Edit Quiz Questions'; +$string['editoverride'] = 'Edit overrides'; $string['emailconfirmbody'] = 'Dear $a->username, Thank you for submitting your answers to @@ -350,6 +353,7 @@ $string['gradingdetailspenalty'] = 'This submission attracted a penalty of $a.'; $string['gradingdetailszeropenalty'] = 'You were not penalized for this submission.'; $string['gradingmethod'] = 'Grading method: $a'; +$string['groupoverrides'] = 'Group Overrides'; $string['guestsno'] = 'Sorry, guests cannot see or attempt quizzes'; $string['hidebreaks'] = 'Hide page breaks'; $string['hidereordertool'] = 'Hide the reordering tool'; @@ -454,6 +458,7 @@ $string['nominal'] = 'Nominal'; $string['nomoreattempts'] = 'No more attempts are allowed'; $string['none'] = 'None'; +$string['nooverridedata'] = 'You must override at least one of the quiz settings.'; $string['nopossibledatasets'] = 'No possible datasets'; $string['noquestionintext'] = 'The question text does not contain any embedded questions'; $string['noquestions'] = 'No questions have been added yet'; @@ -483,6 +488,7 @@ $string['onlyteachersexport'] = 'Only teachers can export questions'; $string['onlyteachersimport'] = 'Only teachers with editing rights can import questions'; $string['open'] = 'Started'; +$string['openafterclose'] = 'Could not update the quiz. You have specified a open date after the close date.'; $string['openclosedatesupdated'] = 'Quiz open and close dates updated'; $string['optional'] = 'optional'; $string['orderandpaging'] = 'Order and paging'; @@ -492,6 +498,10 @@ $string['outofshort'] = '$a->grade/$a->maxgrade'; $string['overallfeedback'] = 'Overall feedback'; $string['overdue'] = 'Overdue'; +$string['overrides'] = 'Overrides'; +$string['overridegroup'] = 'Override group'; +$string['overrideuser'] = 'Override user'; +$string['overrideuserstr'] = 'Override'; $string['pagesize'] = 'Attempts shown per page:'; $string['paragraphquestion'] = 'Paragraph Question not supported at line $a. The question will be ignored'; $string['parent'] = 'Parent'; @@ -659,6 +669,7 @@ $string['savegrades'] = 'Save grades'; $string['savemyanswers'] = 'Save my answers'; $string['savenosubmit'] = 'Save without submitting'; +$string['saveoverrideandstay'] = 'Save and enter another override'; $string['savequiz'] = 'Save this whole quiz'; $string['score'] = 'Raw score'; $string['scores'] = 'Scores'; @@ -752,6 +763,7 @@ $string['upgradesure'] = '
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['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'; $string['validate'] = 'Validate'; $string['viewallanswers'] = 'View $a quiz attempts'; $string['viewallreports'] = 'View reports for $a attempts'; Index: mod/quiz/override_form.php =================================================================== RCS file: mod/quiz/override_form.php diff -N mod/quiz/override_form.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ mod/quiz/override_form.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,218 @@ +. + + +/** + * Settings form for overrides in the quiz module. + * + * @package mod-quiz + * @author Matt Petro + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once $CFG->libdir.'/formslib.php'; + +class quiz_override_form extends moodleform { + + protected $cm; // course module object + protected $quiz; // quiz object + protected $context; // context object + protected $groupmode; // editing group override (true) or user override (false) + + protected $currentgroupid; // what was the previous group id + protected $currentuserid; // what was the previous user id + + const filterlists = false; // should the user and group lists be filtered to remove existing overrides? + // If this is false, then new overrides silently overwrite old ones + + public function quiz_override_form($submiturl, $current, $cm, $quiz, $context, $groupmode){ + + $this->cm = $cm; + $this->quiz = $quiz; + $this->context = $context; + $this->groupmode = $groupmode; + + $this->currentgroupid = isset($current->groupid)? $current->groupid : 0; + $this->currentuserid = isset($current->userid)? $current->userid : 0; + + parent::moodleform($submiturl, null, 'post'); + + } + + public function definition() { + global $CFG, $USER, $DB; + + $cm = $this->cm; + $mform = $this->_form; + + $cancelonly = false; + + $mform->addElement('header', 'override', get_string('overrides', 'quiz')); + + if ($this->groupmode) { + // Prepare the list of groups + $groups = groups_get_all_groups($cm->course, null, $cm->groupingid); + if (empty($groups)) { + $groups = array(); + } + + $groupchoices = array(); + foreach ($groups as $group) { + $groupchoices[$group->id] = format_string($group->name); + } + unset($groups); + + // Remove any groups that are already overridden + + if (self::filterlists) { + $overrides = $DB->get_records('quiz_overrides', array('quiz' => $cm->instance)); + if (!empty($overrides)) { + foreach ($overrides as $taken) { + if ($taken->groupid && array_key_exists($taken->groupid, $groupchoices) + && $taken->groupid != $this->currentgroupid) { + unset($groupchoices[$taken->groupid]); + } + } + } + } + + if (count($groupchoices) == 0) { + $groupchoices[0] = get_string('none'); + } + + $mform->addElement('select', 'groupid', get_string('overridegroup', 'quiz'), $groupchoices); + $mform->addRule('groupid', get_string('required'), 'required', null, 'client'); + + } else { + // Prepare the list of users + if (!empty($cm->groupingid)) { + $groups = groups_get_all_groups($cm->course, 0, $cm->groupingid); + $groups = array_keys($groups); + } else { + $groups = null; + } + $users = get_users_by_capability($this->context, 'mod/quiz:attempt', 'u.id,u.firstname,u.lastname,u.email' , + 'firstname ASC, lastname ASC', '', '', $groups, '', false, true); + if (empty($users)) { + $users = array(); + } + + $userchoices = array(); + foreach ($users as $id=>$user) { + if (empty($invalidusers[$id]) || (!empty($override) && $id == $override->userid)) { + $userchoices[$id] = fullname($user) . ', ' . $user->email; + } + } + unset($users); + + // Remove any users that are already overridden + if (self::filterlists) { + $overrides = $DB->get_records('quiz_overrides', array('quiz' => $cm->instance)); + + if (!empty($overrides)) { + foreach ($overrides as $taken) { + if ($taken->userid && array_key_exists($taken->userid, $userchoices) + && $taken->userid != $this->currentuserid) { + unset($userchoices[$taken->userid]); + } + } + } + } + + if (count($userchoices) == 0) { + $userchoices[0] = get_string('none'); + } + + $mform->addElement('searchableselector', 'userid', get_string('overrideuser', 'quiz'), $userchoices); + $mform->addRule('userid', get_string('required'), 'required', null, 'client'); + + } + + // Open and close dates. + $mform->addElement('date_time_selector', 'timeopen', get_string('quizopen', 'quiz'), array('optional' => true)); + $mform->setHelpButton('timeopen', array('timeopen', get_string('quizopen', 'quiz'), 'quiz')); + + $mform->addElement('date_time_selector', 'timeclose', get_string('quizclose', 'quiz'), array('optional' => true)); + $mform->setHelpButton('timeclose', array('timeopen', get_string('quizclose', 'quiz'), 'quiz')); + + // Time limit. + $mform->addElement('duration', 'timelimit', get_string('quiztimer', 'quiz'), array('optional' => true)); + $mform->setHelpButton('timelimit', array('timelimit', get_string('quiztimer','quiz'), 'quiz')); + + $mform->addElement('hidden', 'id', 0); + $mform->setType('override', PARAM_INT); + + $mform->addElement('hidden', 'cmid', 0); + $mform->setType('cmid', PARAM_INT); + + // Submit buttons + $buttonarray = array(); + $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); + $mform->closeHeaderBefore('buttonbar'); + + } + + // form verification + public function validation($data, $files) { + global $COURSE, $DB; + $errors = parent::validation($data, $files); + + $mform =& $this->_form; + $quiz = $this->quiz; + + if ($mform->elementExists('userid')) { + if (empty($data['userid'])) { + $errors['userid'] = get_string('required'); + } + } + + if ($mform->elementExists('groupid')) { + if (empty($data['groupid'])) { + $errors['groupid'] = get_string('required'); + } + } + + // Ensure that the dates make sense + $effectivetimeopen = (!empty($data['timeopen']))? $data['timeopen'] : $quiz->timeopen; + $effectivetimeclose = (!empty($data['timeclose']))? $data['timeclose'] : $quiz->timeclose; + + if (!empty($data['timeopen'])) { + if ($effectivetimeclose && $data['timeopen'] > $effectivetimeclose ) { + $errors['timeopen'] = get_string('openafterclose', 'quiz'); + } + } + + if (!empty($data['timeclose']) && empty($errors['timeopen'])) { + if ($data['timeclose'] < $effectivetimeopen ) { + $errors['timeclose'] = get_string('closebeforeopen', 'quiz'); + } + } + + if (empty($data['timeopen']) && empty($data['timeclose']) && empty($data['timelimit'])) { + $errors['timeopen'] = get_string('nooverridedata', 'quiz'); + $errors['timeclose'] = $errors['timeopen']; + $errors['timelimit'] = $errors['timeopen']; + } + + return $errors; + } + +} \ No newline at end of file Index: mod/quiz/override.php =================================================================== RCS file: mod/quiz/override.php diff -N mod/quiz/override.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ mod/quiz/override.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,256 @@ +. + + +/** + * This page handles listing and editing of quiz overrides + * + * @package mod-quiz + * @author 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, or +$overrideid = optional_param('id', 0, PARAM_INT); // override ID + +$action = optional_param('action', '', PARAM_ALPHA); // one of 'delete','adduser','addgroup','edit' + +if ($overrideid) { + + if (! $override = $DB->get_record('quiz_overrides', array('id' => $overrideid))) { + print_error('invalidoverrideid', 'quiz'); + } + if (! $quiz = $DB->get_record('quiz', array('id' => $override->quiz))) { + print_error('invalidcoursemodule'); + } + if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $quiz->course)) { + print_error('invalidcoursemodule'); + } +} else if ($cmid) { + + if (! $cm = get_coursemodule_from_id('quiz', $cmid)) { + print_error('invalidcoursemodule'); + } + if (! $quiz = $DB->get_record('quiz', array('id' => $cm->instance))) { + print_error('invalidcoursemodule'); + } +} else { + print_error('invalidcoursemodule'); +} + +$url = new moodle_url('/mod/quiz/override.php', array('cmid'=>$cm->id, 'action'=>$action)); + +$PAGE->set_url($url); + +require_login($cm->course, false, $cm); + +$context = get_context_instance(CONTEXT_MODULE, $cm->id); + + +if ($action === 'delete' && confirm_sesskey()) { + + // Remove it + + require_capability('mod/quiz:manage', $context); + + if (empty($override->id)) { + print_error('invalidoverrideid', 'quiz'); + } + + quiz_delete_override($quiz, $override->id); + + $url->param('action', 'list'); + redirect($url); +} +else if ($action === 'adduser' || $action === 'addgroup' || $action === 'edit') { + + // Add or edit an override + + require_capability('mod/quiz:manage', $context); + + if ($action === 'edit') { + if (empty($override->id)) { + print_error('invalidoverrideid', 'quiz'); + } + $data = $override; + } + else { + $data = new object(); + $data->quiz = $quiz->id; + } + + $data->cmid = $cm->id; + + // Setup the form. + $group_mode = $action === 'addgroup' || !empty($data->groupid); // true if group-based override + + $mform = new quiz_override_form($url, $data, $cm, $quiz, $context, $group_mode); + $mform->set_data($data); + + if ($mform->is_cancelled()) { + $url->param('action', 'list'); + redirect($url); + } else if ($fromform = $mform->get_data()) { + + // Process the data + + add_to_log($cm->course, 'quiz', 'edit overrides', + "override.php?cmid=$cm->id", $quiz->id, $cm->id); + + $fromform->quiz = $quiz->id; + + // See if we are replacing an existing override + if (empty($override->id)) { + $conditions = array( + 'quiz' => $quiz->id, + 'userid' => empty($fromform->userid)? 0 : $fromform->userid, + 'groupid' => empty($fromform->groupid)? 0 : $fromform->groupid); + if ($oldoverride = $DB->get_record('quiz_overrides', $conditions)) { + $fromform->id = $oldoverride->id; + } + } + + if (!empty($fromform->id)) { + $DB->update_record('quiz_overrides', $fromform); + } + else { + $fromform->id = $DB->insert_record('quiz_overrides', $fromform); + } + + quiz_events_update($quiz, $fromform); + + if ($fromform->submitbutton) { + $url->param('action', 'list'); + redirect($url); + } + + // 'again' button pressed, so print the form + $mform->set_data(array('action' => ($group_mode)? 'addgroup' : 'adduser')); + } + + // Print the form + + $pagetitle = get_string('editoverride', 'quiz'); + $PAGE->navbar->add($pagetitle); + $PAGE->set_title($pagetitle); + + echo $OUTPUT->header(); + echo $OUTPUT->heading($pagetitle); + + $mform->display(); + + echo $OUTPUT->footer(); +} +else { + // Display a list of overrides + + require_capability('mod/quiz:manage', $context); + + $overrides = $DB->get_records('quiz_overrides', array('quiz' => $quiz->id)); + if (!$overrides) { + $overrides = array(); + } + + echo $OUTPUT->header(); + + // Print heading and tabs (if there is more than one). + $currenttab = 'overrides'; + include('tabs.php'); + + // Initialise user table + $utable = new html_table(); + $utable->tablealign = 'center'; + $utable->align = array('left', 'left', 'left', 'left'); + $utable->wrap = array('nowrap', '', 'nowrap','nowrap'); + $utable->width = '90%'; + $utable->head = array( + get_string('user'), + get_string('quizopen', 'quiz'), + get_string('quizclose', 'quiz'), + get_string('quiztimer', 'quiz'), + '' + ); + + // Initialise group table + $gtable = new html_table(); + $gtable->tablealign = 'center'; + $gtable->align = array('left', 'left', 'left', 'left'); + $gtable->wrap = array('nowrap', '', 'nowrap','nowrap'); + $gtable->width = '90%'; + $gtable->head = array( + get_string('group'), + get_string('quizopen', 'quiz'), + get_string('quizclose', 'quiz'), + get_string('quiztimer', 'quiz'), + '' + ); + + $userurl = new moodle_url('/user/view.php', array()); + $groupurl = new moodle_url('/group/overview.php', array('id' => $cm->course)); + + foreach ($overrides as $override) { + + $timeopenstr = ($override->timeopen > 0)? userdate($override->timeopen) : '-'; + $timeclosestr = ($override->timeclose > 0)? userdate($override->timeclose) : '-'; + $timelimitstr = ($override->timelimit > 0)? format_time($override->timelimit) : '-'; + + // Icons: + + // edit + $editurlstr = $url->out(true, array('action' => 'edit', 'id' => $override->id)); + $iconstr = '' . + '' . $stredit . ' '; + // delete + $deleteurlstr = $url->out(true, array('action' => 'delete', 'id' => $override->id, 'sesskey' => sesskey())); + $iconstr .= '' . + '' . $strdelete . ' '; + + if ($override->groupid) { + $group = $DB->get_record('groups', array('id' => $override->groupid)); + $usergroupstr = '' . $group->name . ''; + $thistable =& $gtable; + } + else { + $user = $DB->get_record('user', array('id' => $override->userid), 'id,firstname,lastname' ); + $usergroupstr = '' . fullname($user) . ''; + $thistable =& $utable; + } + + $thistable->data[] = array($usergroupstr, $timeopenstr, $timeclosestr, $timelimitstr, $iconstr); + } + + // Output the tables and buttons + + echo $OUTPUT->heading(get_string('groupoverrides', 'quiz'), 3); + echo $OUTPUT->table($gtable); + + echo $OUTPUT->single_button($url->out(true, array('action' => 'addgroup')), get_string('addnewgroupoverride', 'quiz'), $method='post'); + + echo $OUTPUT->heading(get_string('useroverrides', 'quiz'), 3); + echo $OUTPUT->table($utable); + + echo $OUTPUT->single_button($url->out(true, array('action' => 'adduser')), get_string('addnewuseroverride', 'quiz'), $method='post'); + + // Finish the page + echo $OUTPUT->footer(); +} \ No newline at end of file