MDL-14679 fixed references to mod.html
[moodle.git] / mod / quiz / lib.php
1 <?php  // $Id$
2 /**
3 * Library of functions for the quiz module.
4 *
5 * This contains functions that are called also from outside the quiz module
6 * Functions that are only called by the quiz module itself are in {@link locallib.php}
7 * @author Martin Dougiamas and many others.
8 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
9 * @package quiz
10 */
12 require_once($CFG->libdir.'/pagelib.php');
13 require_once($CFG->libdir.'/questionlib.php');
15 /// CONSTANTS ///////////////////////////////////////////////////////////////////
17 /**#@+
18  * The different review options are stored in the bits of $quiz->review
19  * These constants help to extract the options
20  *
21  * This is more of a mess than you might think necessary, because originally
22  * it was though that 3x6 bits were enough, but then they ran out. PHP integers
23  * are only reliably 32 bits signed, so the simplest solution was then to
24  * add 4x3 more bits.
25  */
26 /**
27  * The first 6 + 4 bits refer to the time immediately after the attempt
28  */
29 define('QUIZ_REVIEW_IMMEDIATELY', 0x3c003f);
30 /**
31  * the next 6 + 4 bits refer to the time after the attempt but while the quiz is open
32  */
33 define('QUIZ_REVIEW_OPEN',       0x3c00fc0);
34 /**
35  * the final 6 + 4 bits refer to the time after the quiz closes
36  */
37 define('QUIZ_REVIEW_CLOSED',    0x3c03f000);
39 // within each group of 6 bits we determine what should be shown
40 define('QUIZ_REVIEW_RESPONSES',       1*0x1041); // Show responses
41 define('QUIZ_REVIEW_SCORES',          2*0x1041); // Show scores
42 define('QUIZ_REVIEW_FEEDBACK',        4*0x1041); // Show question feedback
43 define('QUIZ_REVIEW_ANSWERS',         8*0x1041); // Show correct answers
44 // Some handling of worked solutions is already in the code but not yet fully supported
45 // and not switched on in the user interface.
46 define('QUIZ_REVIEW_SOLUTIONS',      16*0x1041); // Show solutions
47 define('QUIZ_REVIEW_GENERALFEEDBACK',32*0x1041); // Show question general feedback
48 define('QUIZ_REVIEW_OVERALLFEEDBACK', 1*0x4440000); // Show quiz overall feedback
49 // Multipliers 2*0x4440000, 4*0x4440000 and 8*0x4440000 are still available
50 /**#@-*/
52 /**
53  * If start and end date for the quiz are more than this many seconds apart
54  * they will be represented by two separate events in the calendar
55  */
56 define("QUIZ_MAX_EVENT_LENGTH", 5*24*60*60);   // 5 days maximum
58 /// FUNCTIONS ///////////////////////////////////////////////////////////////////
60 /**
61  * Given an object containing all the necessary data,
62  * (defined by the form in mod_form.php) this function
63  * will create a new instance and return the id number
64  * of the new instance.
65  *
66  * @param object $quiz the data that came from the form.
67  * @return mixed the id of the new instance on success,
68  *          false or a string error message on failure.
69  */
70 function quiz_add_instance($quiz) {
72     // Process the options from the form.
73     $quiz->created = time();
74     $quiz->questions = '';
75     $result = quiz_process_options($quiz);
76     if ($result && is_string($result)) {
77         return $result;
78     }
80     // Try to store it in the database.
81     if (!$quiz->id = insert_record("quiz", $quiz)) {
82         return false;
83     }
85     // Do the processing required after an add or an update.
86     quiz_after_add_or_update($quiz);
88     return $quiz->id;
89 }
91 /**
92  * Given an object containing all the necessary data,
93  * (defined by the form in mod_form.php) this function
94  * will update an existing instance with new data.
95  *
96  * @param object $quiz the data that came from the form.
97  * @return mixed true on success, false or a string error message on failure.
98  */
99 function quiz_update_instance($quiz) {
101     // Process the options from the form.
102     $result = quiz_process_options($quiz);
103     if ($result && is_string($result)) {
104         return $result;
105     }
107     // Update the database.
108     $quiz->id = $quiz->instance;
109     if (!update_record("quiz", $quiz)) {
110         return false;  // some error occurred
111     }
113     // Do the processing required after an add or an update.
114     quiz_after_add_or_update($quiz);
116     // Delete any previous preview attempts
117     delete_records('quiz_attempts', 'preview', '1', 'quiz', $quiz->id);
119     return true;
123 function quiz_delete_instance($id) {
124 /// Given an ID of an instance of this module,
125 /// this function will permanently delete the instance
126 /// and any data that depends on it.
128     if (! $quiz = get_record("quiz", "id", "$id")) {
129         return false;
130     }
132     $result = true;
134     if ($attempts = get_records("quiz_attempts", "quiz", "$quiz->id")) {
135         foreach ($attempts as $attempt) {
136             // TODO: this should use the delete_attempt($attempt->uniqueid) function in questionlib.php
137             if (! delete_records("question_states", "attempt", "$attempt->uniqueid")) {
138                 $result = false;
139             }
140             if (! delete_records("question_sessions", "attemptid", "$attempt->uniqueid")) {
141                 $result = false;
142             }
143         }
144     }
146     $tables_to_purge = array(
147         'quiz_attempts' => 'quiz',
148         'quiz_grades' => 'quiz',
149         'quiz_question_instances' => 'quiz',
150         'quiz_grades' => 'quiz',
151         'quiz_feedback' => 'quizid',
152         'quiz' => 'id'
153     );
154     foreach ($tables_to_purge as $table => $keyfield) {
155         if (!delete_records($table, $keyfield, $quiz->id)) {
156             $result = false;
157         }
158     }
160     $pagetypes = page_import_types('mod/quiz/');
161     foreach($pagetypes as $pagetype) {
162         if(!delete_records('block_instance', 'pageid', $quiz->id, 'pagetype', $pagetype)) {
163             $result = false;
164         }
165     }
167     if ($events = get_records_select('event', "modulename = 'quiz' and instance = '$quiz->id'")) {
168         foreach($events as $event) {
169             delete_event($event->id);
170         }
171     }
173     quiz_grade_item_delete($quiz);
175     return $result;
179 function quiz_user_outline($course, $user, $mod, $quiz) {
180 /// Return a small object with summary information about what a
181 /// user has done with a given particular instance of this module
182 /// Used for user activity reports.
183 /// $return->time = the time they did it
184 /// $return->info = a short text description
185     if ($grade = get_record('quiz_grades', 'userid', $user->id, 'quiz', $quiz->id)) {
187         $result = new stdClass;
188         if ((float)$grade->grade) {
189             $result->info = get_string('grade').':&nbsp;'.round($grade->grade, $quiz->decimalpoints);
190         }
191         $result->time = $grade->timemodified;
192         return $result;
193     }
194     return NULL;
199 function quiz_user_complete($course, $user, $mod, $quiz) {
200 /// Print a detailed representation of what a  user has done with
201 /// a given particular instance of this module, for user activity reports.
203     if ($attempts = get_records_select('quiz_attempts', "userid='$user->id' AND quiz='$quiz->id'", 'attempt ASC')) {
204         if ($quiz->grade  and $quiz->sumgrades && $grade = get_record('quiz_grades', 'userid', $user->id, 'quiz', $quiz->id)) {
205             echo get_string('grade').': '.round($grade->grade, $quiz->decimalpoints).'/'.$quiz->grade.'<br />';
206         }
207         foreach ($attempts as $attempt) {
208             echo get_string('attempt', 'quiz').' '.$attempt->attempt.': ';
209             if ($attempt->timefinish == 0) {
210                 print_string('unfinished');
211             } else {
212                 echo round($attempt->sumgrades, $quiz->decimalpoints).'/'.$quiz->sumgrades;
213             }
214             echo ' - '.userdate($attempt->timemodified).'<br />';
215         }
216     } else {
217        print_string('noattempts', 'quiz');
218     }
220     return true;
224 function quiz_cron () {
225 /// Function to be run periodically according to the moodle cron
226 /// This function searches for things that need to be done, such
227 /// as sending out mail, toggling flags etc ...
229     global $CFG;
231     return true;
234 /**
235  * @param integer $quizid the quiz id.
236  * @param integer $userid the userid.
237  * @param string $status 'all', 'finished' or 'unfinished' to control
238  * @return an array of all the user's attempts at this quiz. Returns an empty array if there are none.
239  */
240 function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
241     $status_condition = array(
242         'all' => '',
243         'finished' => ' AND timefinish > 0',
244         'unfinished' => ' AND timefinish = 0'
245     );
246     $previewclause = '';
247     if (!$includepreviews) {
248         $previewclause = ' AND preview = 0';
249     }
250     if ($attempts = get_records_select('quiz_attempts',
251             "quiz = '$quizid' AND userid = '$userid'" . $previewclause . $status_condition[$status],
252             'attempt ASC')) {
253         return $attempts;
254     } else {
255         return array();
256     }
259 /**
260  * Return grade for given user or all users.
261  *
262  * @param int $quizid id of quiz
263  * @param int $userid optional user id, 0 means all users
264  * @return array array of grades, false if none
265  */
266 function quiz_get_user_grades($quiz, $userid=0) {
267     global $CFG;
269     $user = $userid ? "AND u.id = $userid" : "";
271     $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade, g.timemodified AS dategraded, MAX(a.timefinish) AS datesubmitted
272             FROM {$CFG->prefix}user u, {$CFG->prefix}quiz_grades g, {$CFG->prefix}quiz_attempts a
273             WHERE u.id = g.userid AND g.quiz = {$quiz->id} AND a.quiz = g.quiz AND u.id = a.userid
274                   $user
275             GROUP BY u.id, g.grade, g.timemodified";
277     return get_records_sql($sql);
280 /**
281  * Update grades in central gradebook
282  *
283  * @param object $quiz null means all quizs
284  * @param int $userid specific user only, 0 mean all
285  */
286 function quiz_update_grades($quiz=null, $userid=0, $nullifnone=true) {
287     global $CFG;
288     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
289         require_once($CFG->libdir.'/gradelib.php');
290     }
292     if ($quiz != null) {
293         if ($grades = quiz_get_user_grades($quiz, $userid)) {
294             quiz_grade_item_update($quiz, $grades);
296         } else if ($userid and $nullifnone) {
297             $grade = new object();
298             $grade->userid   = $userid;
299             $grade->rawgrade = NULL;
300             quiz_grade_item_update($quiz, $grade);
302         } else {
303             quiz_grade_item_update($quiz);
304         }
306     } else {
307         $sql = "SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid
308                   FROM {$CFG->prefix}quiz a, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m
309                  WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=a.id";
310         if ($rs = get_recordset_sql($sql)) {
311             while ($quiz = rs_fetch_next_record($rs)) {
312                 if ($quiz->grade != 0) {
313                     quiz_update_grades($quiz, 0, false);
314                 } else {
315                     quiz_grade_item_update($quiz);
316                 }
317             }
318             rs_close($rs);
319         }
320     }
323 /**
324  * Create grade item for given quiz
325  *
326  * @param object $quiz object with extra cmidnumber
327  * @param mixed optional array/object of grade(s); 'reset' means reset grades in gradebook
328  * @return int 0 if ok, error code otherwise
329  */
330 function quiz_grade_item_update($quiz, $grades=NULL) {
331     global $CFG;
332     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
333         require_once($CFG->libdir.'/gradelib.php');
334     }
336     if (array_key_exists('cmidnumber', $quiz)) { //it may not be always present
337         $params = array('itemname'=>$quiz->name, 'idnumber'=>$quiz->cmidnumber);
338     } else {
339         $params = array('itemname'=>$quiz->name);
340     }
342     if ($quiz->grade > 0) {
343         $params['gradetype'] = GRADE_TYPE_VALUE;
344         $params['grademax']  = $quiz->grade;
345         $params['grademin']  = 0;
347     } else {
348         $params['gradetype'] = GRADE_TYPE_NONE;
349     }
351 /* description by TJ:
352 1/ If the quiz is set to not show scores while the quiz is still open, and is set to show scores after
353    the quiz is closed, then create the grade_item with a show-after date that is the quiz close date.
354 2/ If the quiz is set to not show scores at either of those times, create the grade_item as hidden.
355 3/ If the quiz is set to show scores, create the grade_item visible.
356 */
357     if (!($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
358     and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
359         $params['hidden'] = 1;
361     } else if ( ($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED)
362            and !($quiz->review & QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN)) {
363         if ($quiz->timeclose) {
364             $params['hidden'] = $quiz->timeclose;
365         } else {
366             $params['hidden'] = 1;
367         }
369     } else {
370         // a) both open and closed enabled
371         // b) open enabled, closed disabled - we can not "hide after", grades are kept visible even after closing
372         $params['hidden'] = 0;
373     }
375     if ($grades  === 'reset') {
376         $params['reset'] = true;
377         $grades = NULL;
378     }
379     
380     $gradebook_grades = grade_get_grades($quiz->course, 'mod', 'quiz', $quiz->id);
381     if (!empty($gradebook_grades->items)) {
382         $grade_item = $gradebook_grades->items[0];
383         if ($grade_item->locked) {
384             $confirm_regrade = optional_param('confirm_regrade', 0, PARAM_INT);
385             if (!$confirm_regrade) {
386                 $message = get_string('gradeitemislocked', 'grades');
387                 $back_link = $CFG->wwwroot . '/mod/quiz/report.php?q=' . $quiz->id . '&amp;mode=overview';
388                 $regrade_link = qualified_me() . '&amp;confirm_regrade=1';
389                 print_box_start('generalbox', 'notice');
390                 echo '<p>'. $message .'</p>';
391                 echo '<div class="buttons">';
392                 print_single_button($regrade_link, null, get_string('regradeanyway', 'grades'), 'post', $CFG->framename);
393                 print_single_button($back_link,  null,  get_string('cancel'),  'post',  $CFG->framename);
394                 echo '</div>';
395                 print_box_end();
396     
397                 return GRADE_UPDATE_ITEM_LOCKED;
398             }
399         }
400     }
402     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, $grades, $params);
405 /**
406  * Delete grade item for given quiz
407  *
408  * @param object $quiz object
409  * @return object quiz
410  */
411 function quiz_grade_item_delete($quiz) {
412     global $CFG;
413     require_once($CFG->libdir.'/gradelib.php');
415     return grade_update('mod/quiz', $quiz->course, 'mod', 'quiz', $quiz->id, 0, NULL, array('deleted'=>1));
419 function quiz_get_participants($quizid) {
420 /// Returns an array of users who have data in a given quiz
421 /// (users with records in quiz_attempts and quiz_question_versions)
423     global $CFG;
425     //Get users from attempts
426     $us_attempts = get_records_sql("SELECT DISTINCT u.id, u.id
427                                     FROM {$CFG->prefix}user u,
428                                          {$CFG->prefix}quiz_attempts a
429                                     WHERE a.quiz = '$quizid' and
430                                           u.id = a.userid");
432     //Get users from question_versions
433     $us_versions = get_records_sql("SELECT DISTINCT u.id, u.id
434                                     FROM {$CFG->prefix}user u,
435                                          {$CFG->prefix}quiz_question_versions v
436                                     WHERE v.quiz = '$quizid' and
437                                           u.id = v.userid");
439     //Add us_versions to us_attempts
440     if ($us_versions) {
441         foreach ($us_versions as $us_version) {
442             $us_attempts[$us_version->id] = $us_version;
443         }
444     }
445     //Return us_attempts array (it contains an array of unique users)
446     return ($us_attempts);
450 function quiz_refresh_events($courseid = 0) {
451 // This horrible function only seems to be called from mod/quiz/db/[dbtype].php.
453 // This standard function will check all instances of this module
454 // and make sure there are up-to-date events created for each of them.
455 // If courseid = 0, then every quiz event in the site is checked, else
456 // only quiz events belonging to the course specified are checked.
457 // This function is used, in its new format, by restore_refresh_events()
459     if ($courseid == 0) {
460         if (! $quizzes = get_records("quiz")) {
461             return true;
462         }
463     } else {
464         if (! $quizzes = get_records("quiz", "course", $courseid)) {
465             return true;
466         }
467     }
468     $moduleid = get_field('modules', 'id', 'name', 'quiz');
470     foreach ($quizzes as $quiz) {
471         $event = NULL;
472         $event2 = NULL;
473         $event2old = NULL;
475         if ($events = get_records_select('event', "modulename = 'quiz' AND instance = '$quiz->id' ORDER BY timestart")) {
476             $event = array_shift($events);
477             if (!empty($events)) {
478                 $event2old = array_shift($events);
479                 if (!empty($events)) {
480                     foreach ($events as $badevent) {
481                         delete_records('event', 'id', $badevent->id);
482                     }
483                 }
484             }
485         }
487         $event->name        = $quiz->name;
488         $event->description = $quiz->intro;
489         $event->courseid    = $quiz->course;
490         $event->groupid     = 0;
491         $event->userid      = 0;
492         $event->modulename  = 'quiz';
493         $event->instance    = $quiz->id;
494         $event->visible     = instance_is_visible('quiz', $quiz);
495         $event->timestart   = $quiz->timeopen;
496         $event->eventtype   = 'open';
497         $event->timeduration = ($quiz->timeclose - $quiz->timeopen);
499         if ($event->timeduration > QUIZ_MAX_EVENT_LENGTH) {  /// Set up two events
501             $event2 = $event;
503             $event->name         = $quiz->name.' ('.get_string('quizopens', 'quiz').')';
504             $event->timeduration = 0;
506             $event2->name        = $quiz->name.' ('.get_string('quizcloses', 'quiz').')';
507             $event2->timestart   = $quiz->timeclose;
508             $event2->eventtype   = 'close';
509             $event2->timeduration = 0;
511             if (empty($event2old->id)) {
512                 unset($event2->id);
513                 add_event($event2);
514             } else {
515                 $event2->id = $event2old->id;
516                 update_event($event2);
517             }
518         } else if (!empty($event2old->id)) {
519             delete_event($event2old->id);
520         }
522         if (empty($event->id)) {
523             if (!empty($event->timestart)) {
524                 add_event($event);
525             }
526         } else {
527             update_event($event);
528         }
530     }
531     return true;
534 /**
535  * Returns all quiz graded users since a given time for specified quiz
536  */
537 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, $cmid, $userid=0, $groupid=0)  {
538     global $CFG, $COURSE, $USER;
540     if ($COURSE->id == $courseid) {
541         $course = $COURSE;
542     } else {
543         $course = get_record('course', 'id', $courseid);
544     }
546     $modinfo =& get_fast_modinfo($course);
548     $cm = $modinfo->cms[$cmid];
550     if ($userid) {
551         $userselect = "AND u.id = $userid";
552     } else {
553         $userselect = "";
554     }
556     if ($groupid) {
557         $groupselect = "AND gm.groupid = $groupid";
558         $groupjoin   = "JOIN {$CFG->prefix}groups_members gm ON  gm.userid=u.id";
559     } else {
560         $groupselect = "";
561         $groupjoin   = "";
562     }
564     if (!$attempts = get_records_sql("SELECT qa.*, q.sumgrades AS maxgrade,
565                                              u.firstname, u.lastname, u.email, u.picture 
566                                         FROM {$CFG->prefix}quiz_attempts qa
567                                              JOIN {$CFG->prefix}quiz q ON q.id = qa.quiz
568                                              JOIN {$CFG->prefix}user u ON u.id = qa.userid
569                                              $groupjoin
570                                        WHERE qa.timefinish > $timestart AND q.id = $cm->instance
571                                              $userselect $groupselect
572                                     ORDER BY qa.timefinish ASC")) {
573          return;
574     }
577     $cm_context      = get_context_instance(CONTEXT_MODULE, $cm->id);
578     $grader          = has_capability('moodle/grade:viewall', $cm_context);
579     $accessallgroups = has_capability('moodle/site:accessallgroups', $cm_context);
580     $viewfullnames   = has_capability('moodle/site:viewfullnames', $cm_context);
581     $grader          = has_capability('mod/quiz:grade', $cm_context);
582     $groupmode       = groups_get_activity_groupmode($cm, $course);
584     if (is_null($modinfo->groups)) {
585         $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
586     }
588     $aname = format_string($cm->name,true);
589     foreach ($attempts as $attempt) {
590         if ($attempt->userid != $USER->id) {
591             if (!$grader) {
592                 // grade permission required
593                 continue;
594             }
596             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { 
597                 $usersgroups = groups_get_all_groups($course->id, $attempt->userid, $cm->groupingid);
598                 if (!is_array($usersgroups)) {
599                     continue;
600                 }
601                 $usersgroups = array_keys($usersgroups);
602                 $interset = array_intersect($usersgroups, $modinfo->groups[$cm->id]);
603                 if (empty($intersect)) {
604                     continue;
605                 }
606             }
607        }
609         $tmpactivity = new object();
611         $tmpactivity->type      = 'quiz';
612         $tmpactivity->cmid      = $cm->id;
613         $tmpactivity->name      = $aname;
614         $tmpactivity->sectionnum= $cm->sectionnum;
615         $tmpactivity->timestamp = $attempt->timefinish;
616         
617         $tmpactivity->content->attemptid = $attempt->id;
618         $tmpactivity->content->sumgrades = $attempt->sumgrades;
619         $tmpactivity->content->maxgrade  = $attempt->maxgrade;
620         $tmpactivity->content->attempt   = $attempt->attempt;
621         
622         $tmpactivity->user->userid   = $attempt->userid;
623         $tmpactivity->user->fullname = fullname($attempt, $viewfullnames);
624         $tmpactivity->user->picture  = $attempt->picture;
625         
626         $activities[$index++] = $tmpactivity;
627     }
629   return;
633 function quiz_print_recent_mod_activity($activity, $courseid, $detail, $modnames) {
634     global $CFG;
636     echo '<table border="0" cellpadding="3" cellspacing="0" class="forum-recent">';
638     echo "<tr><td class=\"userpicture\" valign=\"top\">";
639     print_user_picture($activity->user->userid, $courseid, $activity->user->picture);
640     echo "</td><td>";
642     if ($detail) {
643         $modname = $modnames[$activity->type];
644         echo '<div class="title">';
645         echo "<img src=\"$CFG->modpixpath/{$activity->type}/icon.gif\" ".
646              "class=\"icon\" alt=\"$modname\" />";
647         echo "<a href=\"$CFG->wwwroot/mod/quiz/view.php?id={$activity->cmid}\">{$activity->name}</a>";
648         echo '</div>';
649     }
651     echo '<div class="grade">';
652     echo  get_string("attempt", "quiz")." {$activity->content->attempt}: ";
653     $grades = "({$activity->content->sumgrades} / {$activity->content->maxgrade})";
654     echo "<a href=\"$CFG->wwwroot/mod/quiz/review.php?attempt={$activity->content->attemptid}\">$grades</a>";
655     echo '</div>';
657     echo '<div class="user">';
658     echo "<a href=\"$CFG->wwwroot/user/view.php?id={$activity->user->userid}&amp;course=$courseid\">"
659          ."{$activity->user->fullname}</a> - ".userdate($activity->timestamp);
660     echo '</div>';
662     echo "</td></tr></table>";
664     return;
667 /**
668  * Pre-process the quiz options form data, making any necessary adjustments.
669  * Called by add/update instance in this file, and the save code in admin/module.php.
670  *
671  * @param object $quiz The variables set on the form.
672  */
673 function quiz_process_options(&$quiz) {
674     $quiz->timemodified = time();
676     // Quiz open time.
677     if (empty($quiz->timeopen)) {
678         $quiz->preventlate = 0;
679     }
681     // Quiz name.
682     if (!empty($quiz->name)) {
683         $quiz->name = trim($quiz->name);
684     }
686     // Time limit. (Get rid of it if the checkbox was not ticked.)
687     if (empty($quiz->timelimitenable)) {
688         $quiz->timelimit = 0;
689     }
690     $quiz->timelimit = round($quiz->timelimit);
692     // Password field - different in form to stop browsers that remember passwords
693     // getting confused.
694     $quiz->password = $quiz->quizpassword;
695     unset($quiz->quizpassword);
697     // Quiz feedback
698     if (isset($quiz->feedbacktext)) {
699         // Clean up the boundary text.
700         for ($i = 0; $i < count($quiz->feedbacktext); $i += 1) {
701             if (empty($quiz->feedbacktext[$i])) {
702                 $quiz->feedbacktext[$i] = '';
703             } else {
704                 $quiz->feedbacktext[$i] = trim($quiz->feedbacktext[$i]);
705             }
706         }
708         // Check the boundary value is a number or a percentage, and in range.
709         $i = 0;
710         while (!empty($quiz->feedbackboundaries[$i])) {
711             $boundary = trim($quiz->feedbackboundaries[$i]);
712             if (!is_numeric($boundary)) {
713                 if (strlen($boundary) > 0 && $boundary[strlen($boundary) - 1] == '%') {
714                     $boundary = trim(substr($boundary, 0, -1));
715                     if (is_numeric($boundary)) {
716                         $boundary = $boundary * $quiz->grade / 100.0;
717                     } else {
718                         return get_string('feedbackerrorboundaryformat', 'quiz', $i + 1);
719                     }
720                 }
721             }
722             if ($boundary <= 0 || $boundary >= $quiz->grade) {
723                 return get_string('feedbackerrorboundaryoutofrange', 'quiz', $i + 1);
724             }
725             if ($i > 0 && $boundary >= $quiz->feedbackboundaries[$i - 1]) {
726                 return get_string('feedbackerrororder', 'quiz', $i + 1);
727             }
728             $quiz->feedbackboundaries[$i] = $boundary;
729             $i += 1;
730         }
731         $numboundaries = $i;
733         // Check there is nothing in the remaining unused fields.
734         for ($i = $numboundaries; $i < count($quiz->feedbackboundaries); $i += 1) {
735             if (!empty($quiz->feedbackboundaries[$i]) && trim($quiz->feedbackboundaries[$i]) != '') {
736                 return get_string('feedbackerrorjunkinboundary', 'quiz', $i + 1);
737             }
738         }
739         for ($i = $numboundaries + 1; $i < count($quiz->feedbacktext); $i += 1) {
740             if (!empty($quiz->feedbacktext[$i]) && trim($quiz->feedbacktext[$i]) != '') {
741                 return get_string('feedbackerrorjunkinfeedback', 'quiz', $i + 1);
742             }
743         }
744         $quiz->feedbackboundaries[-1] = $quiz->grade + 1; // Needs to be bigger than $quiz->grade because of '<' test in quiz_feedback_for_grade().
745         $quiz->feedbackboundaries[$numboundaries] = 0;
746         $quiz->feedbackboundarycount = $numboundaries;
747     }
749     // Settings that get combined to go into the optionflags column.
750     $quiz->optionflags = 0;
751     if (!empty($quiz->adaptive)) {
752         $quiz->optionflags |= QUESTION_ADAPTIVE;
753     }
755     // Settings that get combined to go into the review column.
756     $review = 0;
757     if (isset($quiz->responsesimmediately)) {
758         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_IMMEDIATELY);
759         unset($quiz->responsesimmediately);
760     }
761     if (isset($quiz->responsesopen)) {
762         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_OPEN);
763         unset($quiz->responsesopen);
764     }
765     if (isset($quiz->responsesclosed)) {
766         $review += (QUIZ_REVIEW_RESPONSES & QUIZ_REVIEW_CLOSED);
767         unset($quiz->responsesclosed);
768     }
770     if (isset($quiz->scoreimmediately)) {
771         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_IMMEDIATELY);
772         unset($quiz->scoreimmediately);
773     }
774     if (isset($quiz->scoreopen)) {
775         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_OPEN);
776         unset($quiz->scoreopen);
777     }
778     if (isset($quiz->scoreclosed)) {
779         $review += (QUIZ_REVIEW_SCORES & QUIZ_REVIEW_CLOSED);
780         unset($quiz->scoreclosed);
781     }
783     if (isset($quiz->feedbackimmediately)) {
784         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
785         unset($quiz->feedbackimmediately);
786     }
787     if (isset($quiz->feedbackopen)) {
788         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_OPEN);
789         unset($quiz->feedbackopen);
790     }
791     if (isset($quiz->feedbackclosed)) {
792         $review += (QUIZ_REVIEW_FEEDBACK & QUIZ_REVIEW_CLOSED);
793         unset($quiz->feedbackclosed);
794     }
796     if (isset($quiz->answersimmediately)) {
797         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_IMMEDIATELY);
798         unset($quiz->answersimmediately);
799     }
800     if (isset($quiz->answersopen)) {
801         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_OPEN);
802         unset($quiz->answersopen);
803     }
804     if (isset($quiz->answersclosed)) {
805         $review += (QUIZ_REVIEW_ANSWERS & QUIZ_REVIEW_CLOSED);
806         unset($quiz->answersclosed);
807     }
809     if (isset($quiz->solutionsimmediately)) {
810         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_IMMEDIATELY);
811         unset($quiz->solutionsimmediately);
812     }
813     if (isset($quiz->solutionsopen)) {
814         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_OPEN);
815         unset($quiz->solutionsopen);
816     }
817     if (isset($quiz->solutionsclosed)) {
818         $review += (QUIZ_REVIEW_SOLUTIONS & QUIZ_REVIEW_CLOSED);
819         unset($quiz->solutionsclosed);
820     }
822     if (isset($quiz->generalfeedbackimmediately)) {
823         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
824         unset($quiz->generalfeedbackimmediately);
825     }
826     if (isset($quiz->generalfeedbackopen)) {
827         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_OPEN);
828         unset($quiz->generalfeedbackopen);
829     }
830     if (isset($quiz->generalfeedbackclosed)) {
831         $review += (QUIZ_REVIEW_GENERALFEEDBACK & QUIZ_REVIEW_CLOSED);
832         unset($quiz->generalfeedbackclosed);
833     }
835     if (isset($quiz->overallfeedbackimmediately)) {
836         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_IMMEDIATELY);
837         unset($quiz->overallfeedbackimmediately);
838     }
839     if (isset($quiz->overallfeedbackopen)) {
840         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_OPEN);
841         unset($quiz->overallfeedbackopen);
842     }
843     if (isset($quiz->overallfeedbackclosed)) {
844         $review += (QUIZ_REVIEW_OVERALLFEEDBACK & QUIZ_REVIEW_CLOSED);
845         unset($quiz->overallfeedbackclosed);
846     }
848     $quiz->review = $review;
851 /**
852  * This function is called at the end of quiz_add_instance
853  * and quiz_update_instance, to do the common processing.
854  *
855  * @param object $quiz the quiz object.
856  */
857 function quiz_after_add_or_update($quiz) {
859     // Save the feedback
860     delete_records('quiz_feedback', 'quizid', $quiz->id);
862     for ($i = 0; $i <= $quiz->feedbackboundarycount; $i += 1) {
863         $feedback = new stdClass;
864         $feedback->quizid = $quiz->id;
865         $feedback->feedbacktext = $quiz->feedbacktext[$i];
866         $feedback->mingrade = $quiz->feedbackboundaries[$i];
867         $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1];
868         if (!insert_record('quiz_feedback', $feedback, false)) {
869             return "Could not save quiz feedback.";
870         }
871     }
874     // Update the events relating to this quiz.
875     // This is slightly inefficient, deleting the old events and creating new ones. However,
876     // there are at most two events, and this keeps the code simpler.
877     if ($events = get_records_select('event', "modulename = 'quiz' and instance = '$quiz->id'")) {
878         foreach($events as $event) {
879             delete_event($event->id);
880         }
881     }
883     $event = new stdClass;
884     $event->description = $quiz->intro;
885     $event->courseid    = $quiz->course;
886     $event->groupid     = 0;
887     $event->userid      = 0;
888     $event->modulename  = 'quiz';
889     $event->instance    = $quiz->id;
890     $event->timestart   = $quiz->timeopen;
891     $event->timeduration = $quiz->timeclose - $quiz->timeopen;
892     $event->visible     = instance_is_visible('quiz', $quiz);
893     $event->eventtype   = 'open';
895     if ($quiz->timeclose and $quiz->timeopen and $event->timeduration <= QUIZ_MAX_EVENT_LENGTH) {
896         // Single event for the whole quiz.
897         $event->name = $quiz->name;
898         add_event($event);
899     } else {
900         // Separate start and end events.
901         $event->timeduration  = 0;
902         if ($quiz->timeopen) {
903             $event->name = $quiz->name.' ('.get_string('quizopens', 'quiz').')';
904             add_event($event);
905             unset($event->id); // So we can use the same object for the close event.
906         }
907         if ($quiz->timeclose) {
908             $event->name      = $quiz->name.' ('.get_string('quizcloses', 'quiz').')';
909             $event->timestart = $quiz->timeclose;
910             $event->eventtype = 'close';
911             add_event($event);
912         }
913     }
915     //update related grade item
916     quiz_grade_item_update(stripslashes_recursive($quiz));
919 function quiz_get_view_actions() {
920     return array('view','view all','report');
923 function quiz_get_post_actions() {
924     return array('attempt','editquestions','review','submit');
927 /**
928  * Returns an array of names of quizzes that use this question
929  *
930  * @param object $questionid
931  * @return array of strings
932  */
933 function quiz_question_list_instances($questionid) {
934     global $CFG, $DB;
936     // TODO: we should also consider other questions that are used by
937     // random questions in this quiz, but that is very hard.
939     $sql = "SELECT q.id, q.name
940             FROM {quiz} q
941             JOIN {quiz_question_instances} qqi ON q.id = qqi.quiz
942             WHERE qqi.question = ?";
944     if ($instances = $DB->get_records_sql_menu($sql, array($questionid))) {
945         return $instances;
946     }
947     return array();
950 /**
951  * Implementation of the function for printing the form elements that control
952  * whether the course reset functionality affects the quiz.
953  * @param $mform form passed by reference
954  */
955 function quiz_reset_course_form_definition(&$mform) {
956     $mform->addElement('header', 'forumheader', get_string('modulenameplural', 'quiz'));
957     $mform->addElement('advcheckbox', 'reset_quiz_attempts', get_string('removeallquizattempts','quiz'));
960 /**
961  * Course reset form defaults.
962  */
963 function quiz_reset_course_form_defaults($course) {
964     return array('reset_quiz_attempts'=>1);
967 /**
968  * Removes all grades from gradebook
969  * @param int $courseid
970  * @param string optional type
971  */
972 function quiz_reset_gradebook($courseid, $type='') {
973     global $CFG;
975     $sql = "SELECT q.*, cm.idnumber as cmidnumber, q.course as courseid
976               FROM {$CFG->prefix}quiz q, {$CFG->prefix}course_modules cm, {$CFG->prefix}modules m
977              WHERE m.name='quiz' AND m.id=cm.module AND cm.instance=q.id AND q.course=$courseid";
979     if ($quizs = get_records_sql($sql)) {
980         foreach ($quizs as $quiz) {
981             quiz_grade_item_update($quiz, 'reset');
982         }
983     }
986 /**
987  * Actual implementation of the rest coures functionality, delete all the
988  * quiz attempts for course $data->courseid, if $data->reset_quiz_attempts is
989  * set and true.
990  *
991  * Also, move the quiz open and close dates, if the course start date is changing.
992  * @param $data the data submitted from the reset course.
993  * @return array status array
994  */
995 function quiz_reset_userdata($data) {
996     global $CFG, $QTYPES;
998     $componentstr = get_string('modulenameplural', 'quiz');
999     $status = array();
1001     /// Delete attempts.
1002     if (!empty($data->reset_quiz_attempts)) {
1004         $stateslistsql = "SELECT s.id
1005                             FROM {$CFG->prefix}question_states s
1006                                  INNER JOIN {$CFG->prefix}quiz_attempts qza ON s.attempt=qza.uniqueid
1007                                  INNER JOIN {$CFG->prefix}quiz q ON qza.quiz=q.id
1008                            WHERE q.course={$data->courseid}";
1010         $attemptssql   = "SELECT a.uniqueid
1011                             FROM {$CFG->prefix}quiz_attempts a, {$CFG->prefix}quiz q
1012                            WHERE q.course={$data->courseid} AND a.quiz=q.id";
1014         $quizessql     = "SELECT q.id
1015                             FROM {$CFG->prefix}quiz q
1016                            WHERE q.course={$data->courseid}";
1018         if ($states = get_records_sql($stateslistsql)) {
1019             //TODO: not sure if this works
1020             $stateslist = implode(',', array_keys($states));
1021             foreach ($QTYPES as $qtype) {
1022                 $qtype->delete_states($stateslist);
1023             }
1024         }
1026         delete_records_select('question_states', "attempt IN ($attemptssql)");
1027         delete_records_select('question_sessions', "attemptid IN ($attemptssql)");
1028         delete_records_select('question_attempts', "id IN ($attemptssql)");
1030         // remove all grades from gradebook
1031         if (empty($data->reset_gradebook_grades)) {
1032             quiz_reset_gradebook($data->courseid);
1033         }
1035         delete_records_select('quiz_grades', "quiz IN ($quizessql)");
1036         $status[] = array('component'=>$componentstr, 'item'=>get_string('gradesdeleted','quiz'), 'error'=>false);
1038         delete_records_select('quiz_attempts', "quiz IN ($quizessql)");
1039         $status[] = array('component'=>$componentstr, 'item'=>get_string('attemptsdeleted','quiz'), 'error'=>false);
1040     }
1042     /// updating dates - shift may be negative too
1043     if ($data->timeshift) {
1044         shift_course_mod_dates('quiz', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
1045         $status[] = array('component'=>$componentstr, 'item'=>get_string('openclosedatesupdated', 'quiz'), 'error'=>false);
1046     }
1048     return $status;
1051 /**
1052  * Checks whether the current user is allowed to view a file uploaded in a quiz.
1053  * Teachers can view any from their courses, students can only view their own.
1054  *
1055  * @param int $attemptuniqueid int attempt id
1056  * @param int $questionid int question id
1057  * @return boolean to indicate access granted or denied
1058  */
1059 function quiz_check_file_access($attemptuniqueid, $questionid) {
1060     global $USER;
1062     $attempt = get_record("quiz_attempts", 'uniqueid', $attemptid);
1063     $quiz = get_record("quiz", 'id', $attempt->quiz);
1064     $context = get_context_instance(CONTEXT_COURSE, $quiz->course);
1066     // access granted if the current user submitted this file
1067     if ($attempt->userid == $USER->id) {
1068         return true;
1069     // access granted if the current user has permission to grade quizzes in this course
1070     } else if (has_capability('mod/quiz:viewreports', $context) || has_capability('mod/quiz:grade', $context)) {
1071         return true;
1072     }
1074     // otherwise, this user does not have permission
1075     return false;
1078 /**
1079  * Prints quiz summaries on MyMoodle Page
1080  */
1081 function quiz_print_overview($courses, &$htmlarray) {
1082     global $USER, $CFG;
1083 /// These next 6 Lines are constant in all modules (just change module name)
1084     if (empty($courses) || !is_array($courses) || count($courses) == 0) {
1085         return array();
1086     }
1088     if (!$quizzes = get_all_instances_in_courses('quiz', $courses)) {
1089         return;
1090     }
1092 /// Fetch some language strings outside the main loop.
1093     $strquiz = get_string('modulename', 'quiz');
1094     $strnoattempts = get_string('noattempts', 'quiz');
1096 /// We want to list quizzes that are currently available, and which have a close date.
1097 /// This is the same as what the lesson does, and the dabate is in MDL-10568.
1098     $now = time();
1099     foreach ($quizzes as $quiz) {
1100         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
1101         /// Give a link to the quiz, and the deadline.
1102             $str = '<div class="quiz overview">' .
1103                     '<div class="name">' . $strquiz . ': <a ' . ($quiz->visible ? '' : ' class="dimmed"') .
1104                     ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->coursemodule . '">' .
1105                     $quiz->name . '</a></div>';
1106             $str .= '<div class="info">' . get_string('quizcloseson', 'quiz', userdate($quiz->timeclose)) . '</div>';
1108         /// Now provide more information depending on the uers's role.
1109             $context = get_context_instance(CONTEXT_MODULE, $quiz->coursemodule);
1110             if (has_capability('mod/quiz:viewreports', $context)) {
1111             /// For teacher-like people, show a summary of the number of student attempts.
1112                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm 
1113                 // fields set to make the following call work.
1114                 $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
1115             } else if (has_capability('mod/quiz:attempt', $context)){ // Student
1116             /// For student-like people, tell them how many attempts they have made.
1117                 if (isset($USER->id) && ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
1118                     $numattempts = count($attempts);
1119                     $str .= '<div class="info">' . get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';  
1120                 } else {
1121                     $str .= '<div class="info">' . $strnoattempts . '</div>';
1122                 }
1123             } else {
1124             /// For ayone else, there is no point listing this quiz, so stop processing.
1125                 continue;
1126             }
1128         /// Add the output for this quiz to the rest.
1129             $str .= '</div>';
1130             if (empty($htmlarray[$quiz->course]['quiz'])) {
1131                 $htmlarray[$quiz->course]['quiz'] = $str;
1132             } else {
1133                 $htmlarray[$quiz->course]['quiz'] .= $str;
1134             }
1135         }
1136     }
1139 /**
1140  * Return a textual summary of the number of attemtps that have been made at a particular quiz,
1141  * returns '' if no attemtps have been made yet, unless $returnzero is passed as true.
1142  * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
1143  * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid fields are used at the moment.
1144  * @param boolean $returnzero if false (default), when no attempts have been made '' is returned instead of 'Attempts: 0'.
1145  * @param int $currentgroup if there is a concept of current group where this method is being called
1146  *         (e.g. a report) pass it in here. Default 0 which means no current group.
1147  * @return string a string like "Attempts: 123", "Attemtps 123 (45 from your groups)" or
1148  *          "Attemtps 123 (45 from this group)".
1149  */
1150 function quiz_num_attempt_summary($quiz, $cm, $returnzero = false, $currentgroup = 0) {
1151     global $CFG, $USER;
1152     $numattempts = count_records('quiz_attempts', 'quiz', $quiz->id, 'preview', 0);
1153     if ($numattempts || $returnzero) {
1154         if (groups_get_activity_groupmode($cm)) {
1155             $a->total = $numattempts;
1156             if ($currentgroup) {
1157                 $a->group = count_records_sql('SELECT count(1) FROM ' .
1158                         $CFG->prefix . 'quiz_attempts qa JOIN ' .
1159                         $CFG->prefix . 'groups_members gm ON qa.userid = gm.userid ' .
1160                         'WHERE quiz = ' . $quiz->id . ' AND preview = 0 AND groupid = ' . $currentgroup);
1161                 return get_string('attemptsnumthisgroup', 'quiz', $a);
1162             } else if ($groups = groups_get_all_groups($cm->course, $USER->id, $cm->groupingid)) { 
1163                 $a->group = count_records_sql('SELECT count(1) FROM ' .
1164                         $CFG->prefix . 'quiz_attempts qa JOIN ' .
1165                         $CFG->prefix . 'groups_members gm ON qa.userid = gm.userid ' .
1166                         'WHERE quiz = ' . $quiz->id . ' AND preview = 0 AND ' .
1167                         'groupid IN (' . implode(',', array_keys($groups)) . ')');
1168                 return get_string('attemptsnumyourgroups', 'quiz', $a);
1169             }
1170         }
1171         return get_string('attemptsnum', 'quiz', $numattempts);
1172     }
1173     return '';
1175 ?>