Merge branch 'wip-MDL-32611-m23' of git://github.com/samhemelryk/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 14 May 2012 05:12:53 +0000 (13:12 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 14 May 2012 05:12:53 +0000 (13:12 +0800)
34 files changed:
calendar/lib.php
config-dist.php
course/lib.php
grade/export/grade_export_form.php
grade/export/lib.php
grade/export/ods/export.php
grade/export/ods/grade_export_ods.php
grade/export/ods/index.php
grade/export/txt/export.php
grade/export/txt/grade_export_txt.php
grade/export/txt/index.php
grade/export/xls/export.php
grade/export/xls/grade_export_xls.php
grade/export/xls/index.php
grade/export/xml/export.php
grade/export/xml/grade_export_xml.php
grade/export/xml/index.php
grade/lib.php
lang/en/grades.php
lib/form/yui/dateselector/dateselector.js
lib/moodlelib.php
lib/navigationlib.php
mod/data/lib.php
mod/data/view.php
mod/feedback/lib.php
mod/lti/lib.php
mod/quiz/lib.php
mod/survey/lib.php
mod/url/lib.php
mod/wiki/lib.php
mod/workshop/form/rubric/lib.php
mod/workshop/lib.php
question/engine/simpletest/helpers.php [new file with mode: 0644]
theme/base/style/core.css

index 7d55605..16c5f28 100644 (file)
@@ -1049,28 +1049,6 @@ function calendar_get_link_href($linkbase, $d, $m, $y) {
     return $linkbase;
 }
 
-/**
- * This function has been deprecated as of Moodle 2.0... DO NOT USE!!!!!
- *
- * @deprecated Moodle 2.0 - MDL-24284 please do not use this function any more.
- * @todo MDL-31134 - will be removed in Moodle 2.3
- * @see calendar_get_link_href()
- *
- * @param string $text
- * @param string|moodle_url $linkbase
- * @param int|null $d The number of the day.
- * @param int|null $m The number of the month.
- * @param int|null $y The number of the year.
- * @return string HTML link
- */
-function calendar_get_link_tag($text, $linkbase, $d, $m, $y) {
-    $url = calendar_get_link_href(new moodle_url($linkbase), $d, $m, $y);
-    if (empty($url)) {
-        return $text;
-    }
-    return html_writer::link($url, $text);
-}
-
 /**
  * Build and return a previous month HTML link, with an arrow.
  *
@@ -1491,15 +1469,16 @@ function calendar_get_default_courses() {
     }
 
     $courses = array();
-    if (!empty($CFG->calendar_adminseesall) && has_capability('moodle/calendar:manageentries', get_context_instance(CONTEXT_SYSTEM))) {
+    if (!empty($CFG->calendar_adminseesall) && has_capability('moodle/calendar:manageentries', context_system::instance())) {
         list ($select, $join) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
-        $sql = "SELECT DISTINCT c.* $select
+        $sql = "SELECT c.* $select
                   FROM {course} c
-                  JOIN {event} e ON e.courseid = c.id
-                  $join";
+                  $join
+                  WHERE EXISTS (SELECT 1 FROM {event} e WHERE e.courseid = c.id)
+                  ";
         $courses = $DB->get_records_sql($sql, null, 0, 20);
         foreach ($courses as $course) {
-            context_instance_preload($course);
+            context_helper::preload_from_record($course);
         }
         return $courses;
     }
index 4b3534f..986d2ba 100644 (file)
@@ -38,7 +38,7 @@ $CFG = new stdClass();
 // will be stored.  This database must already have been created         //
 // and a username/password created to access it.                         //
 
-$CFG->dbtype    = 'pgsql';      // 'pgsql', 'mysqli', 'mssql' or 'oci'
+$CFG->dbtype    = 'pgsql';      // 'pgsql', 'mysqli', 'mssql', 'sqlsrv' or 'oci'
 $CFG->dblibrary = 'native';     // 'native' only at the moment
 $CFG->dbhost    = 'localhost';  // eg 'localhost' or 'db.isp.com' or IP
 $CFG->dbname    = 'moodle';     // database name, eg moodle
index 262582f..d71dcfe 100644 (file)
@@ -3026,11 +3026,11 @@ function move_section_to($course, $section, $destination) {
     // If we move the highlighted section itself, then just highlight the destination.
     // Adjust the higlighted section location if we move something over it either direction.
     if ($section == $course->marker) {
-        course_set_marker($course, $destination);
+        course_set_marker($course->id, $destination);
     } elseif ($moveup && $section > $course->marker && $course->marker >= $destination) {
-        course_set_marker($course, $course->marker+1);
+        course_set_marker($course->id, $course->marker+1);
     } elseif (!$moveup && $section < $course->marker && $course->marker <= $destination) {
-        course_set_marker($course, $course->marker-1);
+        course_set_marker($course->id, $course->marker-1);
     }
 
     $transaction->allow_commit();
@@ -4396,6 +4396,43 @@ function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
     }
 }
 
+/**
+ * Determine whether course ajax should be enabled for the specified course
+ *
+ * @param stdClass $course The course to test against
+ * @return boolean Whether course ajax is enabled or note
+ */
+function course_ajax_enabled($course) {
+    global $CFG, $PAGE, $SITE;
+
+    // Ajax must be enabled globally
+    if (!$CFG->enableajax) {
+        return false;
+    }
+
+    // The user must be editing for AJAX to be included
+    if (!$PAGE->user_is_editing()) {
+        return false;
+    }
+
+    // Check that the theme suports
+    if (!$PAGE->theme->enablecourseajax) {
+        return false;
+    }
+
+    // Check that the course format supports ajax functionality
+    // The site 'format' doesn't have information on course format support
+    if ($SITE->id !== $course->id) {
+        $courseformatajaxsupport = course_format_ajax_support($course->format);
+        if (!$courseformatajaxsupport->capable) {
+            return false;
+        }
+    }
+
+    // All conditions have been met so course ajax should be enabled
+    return true;
+}
+
 /**
  * Include the relevant javascript and language strings for the resource
  * toolbox YUI module
@@ -4403,23 +4440,18 @@ function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
  * @param integer $id The ID of the course being applied to
  * @param array $modules An array containing the names of the modules in
  *                       use on the page
- * @param object $config An object containing configuration parameters for ajax modules including:
+ * @param stdClass $config An object containing configuration parameters for ajax modules including:
  *          * resourceurl   The URL to post changes to for resource changes
  *          * sectionurl    The URL to post changes to for section changes
  *          * pageparams    Additional parameters to pass through in the post
  * @return void
  */
 function include_course_ajax($course, $modules = array(), $config = null) {
-    global $PAGE, $CFG, $USER;
+    global $PAGE, $SITE;
 
     // Ensure that ajax should be included
-    $courseformatajaxsupport = course_format_ajax_support($course->format);
-    if (!$PAGE->theme->enablecourseajax
-        || !$CFG->enableajax
-        || empty($USER->editing)
-        || !$PAGE->user_is_editing()
-        || ($course->id != SITEID && !$courseformatajaxsupport->capable)) {
-        return;
+    if (!course_ajax_enabled($course)) {
+        return false;
     }
 
     if (!$config) {
@@ -4461,7 +4493,7 @@ function include_course_ajax($course, $modules = array(), $config = null) {
     );
 
     // Include course dragdrop
-    if ($course->id != SITEID) {
+    if ($course->id != $SITE->id) {
         $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop',
             array(array(
                 'courseid' => $course->id,
@@ -4504,7 +4536,7 @@ function include_course_ajax($course, $modules = array(), $config = null) {
         ), 'moodle');
 
     // Include format-specific strings
-    if ($course->id != SITEID) {
+    if ($course->id != $SITE->id) {
         $PAGE->requires->strings_for_js(array(
                 'showfromothers',
                 'hidefromothers',
@@ -4515,6 +4547,10 @@ function include_course_ajax($course, $modules = array(), $config = null) {
     foreach ($modules as $module => $modname) {
         $PAGE->requires->string_for_js('pluginname', $module);
     }
+
+    // Prevent caching of this page to stop confusion when changing page after making AJAX changes
+    $PAGE->set_cacheable(false);
+    return true;
 }
 
 /**
index a5c02c6..daccfa8 100644 (file)
@@ -37,6 +37,10 @@ class grade_export_form extends moodleform {
         $mform->addElement('advcheckbox', 'export_feedback', get_string('exportfeedback', 'grades'));
         $mform->setDefault('export_feedback', 0);
 
+        $mform->addElement('advcheckbox', 'export_onlyactive', get_string('exportonlyactive', 'grades'));
+        $mform->setDefault('export_onlyactive', 0);
+        $mform->addHelpButton('export_onlyactive', 'exportonlyactive', 'grades');
+
         $options = array('10'=>10, '20'=>20, '100'=>100, '1000'=>1000, '100000'=>100000);
         $mform->addElement('select', 'previewrows', get_string('previewrows', 'grades'), $options);
 
index b71a7b3..1530d2c 100644 (file)
@@ -39,6 +39,7 @@ abstract class grade_export {
     public $updatedgradesonly; // only export updated grades
     public $displaytype; // display type (e.g. real, percentages, letter) for exports
     public $decimalpoints; // number of decimal points for exports
+    public $onlyactive; // only include users with an active enrolment
     /**
      * Constructor should set up all the private variables ready to be pulled
      * @access public
@@ -49,7 +50,7 @@ abstract class grade_export {
      * @param boolean $export_letters
      * @note Exporting as letters will lead to data loss if that exported set it re-imported.
      */
-    public function grade_export($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2) {
+    public function grade_export($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $onlyactive = false) {
         $this->course = $course;
         $this->groupid = $groupid;
         $this->grade_items = grade_item::fetch_all(array('courseid'=>$this->course->id));
@@ -83,6 +84,7 @@ abstract class grade_export {
 
         $this->displaytype = $displaytype;
         $this->decimalpoints = $decimalpoints;
+        $this->onlyactive = $onlyactive;
     }
 
     /**
@@ -125,6 +127,10 @@ abstract class grade_export {
             $this->export_feedback = $formdata->export_feedback;
         }
 
+        if (isset($formdata->export_onlyactive)) {
+            $this->onlyactive = $formdata->export_onlyactive;
+        }
+
         if (isset($formdata->previewrows)) {
             $this->previewrows = $formdata->previewrows;
         }
@@ -222,6 +228,7 @@ abstract class grade_export {
 
         $i = 0;
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             // number of preview rows
@@ -290,7 +297,8 @@ abstract class grade_export {
                         'export_feedback'   =>$this->export_feedback,
                         'updatedgradesonly' =>$this->updatedgradesonly,
                         'displaytype'       =>$this->displaytype,
-                        'decimalpoints'     =>$this->decimalpoints);
+                        'decimalpoints'     =>$this->decimalpoints,
+                        'export_onlyactive' =>$this->onlyactive);
 
         return $params;
     }
index f81f1d6..04a00e8 100644 (file)
@@ -26,6 +26,7 @@ $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
 $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive        = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -44,7 +45,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 
 // print all the exported data here
-$export = new grade_export_ods($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_ods($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
 $export->print_grades();
 
 
index e9ff095..e366131 100644 (file)
@@ -64,6 +64,7 @@ class grade_export_ods extends grade_export {
         $i = 0;
         $geub = new grade_export_update_buffer();
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $i++;
index f0d4b3c..78cd116 100644 (file)
@@ -51,7 +51,7 @@ if ($groupmode == SEPARATEGROUPS and !$currentgroup and !has_capability('moodle/
 
 // process post information
 if ($data = $mform->get_data()) {
-    $export = new grade_export_ods($course, $currentgroup, '', false, false, $data->display, $data->decimals);
+    $export = new grade_export_ods($course, $currentgroup, '', false, false, $data->display, $data->decimals, $data->export_onlyactive);
 
     // print the grades on screen for feedbacks
     $export->process_form($data);
index ab7a03b..e5e572c 100644 (file)
@@ -27,6 +27,7 @@ $separator         = optional_param('separator', 'comma', PARAM_ALPHA);
 $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive        = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -45,7 +46,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 
 // print all the exported data here
-$export = new grade_export_txt($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $separator);
+$export = new grade_export_txt($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $separator, $onlyactive);
 $export->print_grades();
 
 
index 77f804b..6ca697a 100644 (file)
@@ -23,13 +23,13 @@ class grade_export_txt extends grade_export {
 
     public $separator; // default separator
 
-    public function grade_export_txt($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator='comma') {
-        $this->grade_export($course, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+    public function grade_export_txt($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator='comma', $onlyactive = false) {
+        $this->grade_export($course, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
         $this->separator = $separator;
     }
 
-    public function __construct($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator='comma') {
-        parent::__construct($course, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+    public function __construct($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator = 'comma', $onlyactive = false) {
+        parent::__construct($course, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
         $this->separator = $separator;
     }
 
@@ -91,6 +91,7 @@ class grade_export_txt extends grade_export {
 /// Print all the lines of data.
         $geub = new grade_export_update_buffer();
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
 
index 47de3e7..7f5f842 100644 (file)
@@ -51,7 +51,7 @@ if ($groupmode == SEPARATEGROUPS and !$currentgroup and !has_capability('moodle/
 
 // process post information
 if ($data = $mform->get_data()) {
-    $export = new grade_export_txt($course, $currentgroup, '', false, false, $data->display, $data->decimals, $data->separator);
+    $export = new grade_export_txt($course, $currentgroup, '', false, false, $data->display, $data->decimals, $data->separator, $data->export_onlyactive);
 
     // print the grades on screen for feedback
 
index 4df1fec..8526f3c 100644 (file)
@@ -26,6 +26,7 @@ $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
 $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive        = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -44,7 +45,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 
 // print all the exported data here
-$export = new grade_export_xls($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_xls($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
 $export->print_grades();
 
 
index f1efc6e..a89c910 100644 (file)
@@ -63,6 +63,7 @@ class grade_export_xls extends grade_export {
         $i = 0;
         $geub = new grade_export_update_buffer();
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $i++;
index fc509bc..68d7f67 100644 (file)
@@ -51,7 +51,7 @@ if ($groupmode == SEPARATEGROUPS and !$currentgroup and !has_capability('moodle/
 
 // process post information
 if ($data = $mform->get_data()) {
-    $export = new grade_export_xls($course, $currentgroup, '', false, false, $data->display, $data->decimals);
+    $export = new grade_export_xls($course, $currentgroup, '', false, false, $data->display, $data->decimals, $data->export_onlyactive);
 
     // print the grades on screen for feedbacks
     $export->process_form($data);
index 7807993..1424fe8 100644 (file)
@@ -26,6 +26,7 @@ $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
 $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive        = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -44,7 +45,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 
 // print all the exported data here
-$export = new grade_export_xml($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_xml($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
 $export->print_grades();
 
 
index b349b89..396a044 100644 (file)
@@ -54,6 +54,7 @@ class grade_export_xml extends grade_export {
 
         $geub = new grade_export_update_buffer();
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $user = $userdata->user;
index bc4d925..3903846 100644 (file)
@@ -52,7 +52,7 @@ if ($groupmode == SEPARATEGROUPS and !$currentgroup and !has_capability('moodle/
 
 // process post information
 if ($data = $mform->get_data()) {
-    $export = new grade_export_xml($course, $currentgroup, '', false, $data->updatedgradesonly, $data->display, $data->decimals);
+    $export = new grade_export_xml($course, $currentgroup, '', false, $data->updatedgradesonly, $data->display, $data->decimals, $data->export_onlyactive);
 
     // print the grades on screen for feedbacks
     $export->process_form($data);
index 8c969f2..7173c89 100644 (file)
@@ -43,6 +43,11 @@ class graded_users_iterator {
     public $sortfield2;
     public $sortorder2;
 
+    /**
+     * Should users whose enrolment has been suspended be ignored?
+     */
+    protected $onlyactive = false;
+
     /**
      * Constructor
      *
@@ -89,9 +94,7 @@ class graded_users_iterator {
 
         list($gradebookroles_sql, $params) =
             $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
-
-        //limit to users with an active enrolment
-        list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext);
+        list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, '', 0, $this->onlyactive);
 
         $params = array_merge($params, $enrolledparams);
 
@@ -253,6 +256,18 @@ class graded_users_iterator {
         $this->gradestack = array();
     }
 
+    /**
+     * Should all enrolled users be exported or just those with an active enrolment?
+     *
+     * @param bool $onlyactive True to limit the export to users with an active enrolment
+     */
+    public function require_active_enrolment($onlyactive = true) {
+        if (!empty($this->users_rs)) {
+            debugging('Calling require_active_enrolment() has no effect unless you call init() again', DEBUG_DEVELOPER);
+        }
+        $this->onlyactive  = $onlyactive;
+    }
+
 
     /**
      * _push
index f79ae7f..5443b07 100644 (file)
@@ -190,6 +190,8 @@ $string['exportalloutcomes'] = 'Export all outcomes';
 $string['exportfeedback'] = 'Include feedback in export';
 $string['exportplugins'] = 'Export plugins';
 $string['exportsettings'] = 'Export settings';
+$string['exportonlyactive'] = 'Require active enrolment';
+$string['exportonlyactive_help'] = 'Only include students in the export whose enrolment has not been suspended';
 $string['exportto'] = 'Export to';
 $string['extracreditwarning'] = 'Note: Setting all items for a category to extra credit will effectively remove them from the grade calculation. Since there will be no point total';
 $string['feedback'] = 'Feedback';
index 34b76f7..371e82f 100644 (file)
@@ -136,6 +136,7 @@ YUI.add('moodle-form-dateselector', function(Y) {
             this.yearselect.set('selectedIndex', newindex);
             this.monthselect.set('selectedIndex', date[1] - this.monthselect.firstOptionValue());
             this.dayselect.set('selectedIndex', date[2] - this.dayselect.firstOptionValue());
+            M.form.dateselector.release_current();
         },
         connect_handlers : function() {
             M.form.dateselector.calendar.selectEvent.subscribe(this.set_selects_from_date, this, true);
index d781292..8f18a11 100644 (file)
@@ -10096,20 +10096,22 @@ function get_performance_info() {
              } else {
                  $othercount += 1;
              }
-             $details .= "<div class='yui-module'><p>$module</p>";
-             foreach ($backtraces as $backtrace) {
-                 $details .= "<div class='backtrace'>$backtrace</div>";
+             if (!empty($CFG->yuimoduledebug)) {
+                 // hidden feature for developers working on YUI module infrastructure
+                 $details .= "<div class='yui-module'><p>$module</p>";
+                 foreach ($backtraces as $backtrace) {
+                     $details .= "<div class='backtrace'>$backtrace</div>";
+                 }
+                 $details .= '</div>';
              }
-             $details .= '</div>';
          }
          $info['html'] .= "<span class='includedyuimodules'>Included YUI modules: $yuicount</span> ";
          $info['txt'] .= "includedyuimodules: $yuicount ";
          $info['html'] .= "<span class='includedjsmodules'>Other JavaScript modules: $othercount</span> ";
          $info['txt'] .= "includedjsmodules: $othercount ";
-         // Slightly odd to output the details in a display: none div. The point
-         // Is that it takes a lot of space, and if you care you can reveal it
-         // using firebug.
-         $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
+         if ($details) {
+             $info['html'] .= '<div id="yui-module-debug" class="notifytiny">'.$details.'</div>';
+         }
      }
 
     if (!empty($PERF->logwrites)) {
index 707ea92..5f44996 100644 (file)
@@ -1577,21 +1577,29 @@ class global_navigation extends navigation_node {
                 // First up fetch all of the courses in categories where we know that we are going to
                 // need the majority of courses.
                 list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
-                list($courseids, $courseparams) = $DB->get_in_or_equal(array_keys($this->addedcourses) + array($SITE->id), SQL_PARAMS_NAMED, 'lcourse', false);
                 list($categoryids, $categoryparams) = $DB->get_in_or_equal($fullfetch, SQL_PARAMS_NAMED, 'lcategory');
                 $sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect
                             FROM {course} c
                                 $ccjoin
-                            WHERE c.category {$categoryids} AND
-                                c.id {$courseids}
+                            WHERE c.category {$categoryids}
                         ORDER BY c.sortorder ASC";
-                $coursesrs = $DB->get_recordset_sql($sql, $courseparams + $categoryparams);
+                $coursesrs = $DB->get_recordset_sql($sql, $categoryparams);
                 foreach ($coursesrs as $course) {
+                    if ($course->id == $SITE->id) {
+                        // This should not be necessary, frontpage is not in any category.
+                        continue;
+                    }
+                    if (array_key_exists($course->id, $this->addedcourses)) {
+                        // It is probably better to not include the already loaded courses
+                        // directly in SQL because inequalities may confuse query optimisers
+                        // and may interfere with query caching.
+                        continue;
+                    }
                     if (!$this->can_add_more_courses_to_category($course->category)) {
                         continue;
                     }
                     context_instance_preload($course);
-                    if ($course->id != $SITE->id && !$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
+                    if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
                         continue;
                     }
                     $coursenodes[$course->id] = $this->add_course($course);
@@ -1604,21 +1612,30 @@ class global_navigation extends navigation_node {
                 // proportion of the courses.
                 foreach ($partfetch as $categoryid) {
                     list($ccselect, $ccjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
-                    list($courseids, $courseparams) = $DB->get_in_or_equal(array_keys($this->addedcourses) + array($SITE->id), SQL_PARAMS_NAMED, 'lcourse', false);
                     $sql = "SELECT c.id, c.sortorder, c.visible, c.fullname, c.shortname, c.category $ccselect
                                 FROM {course} c
                                     $ccjoin
-                                WHERE c.category = :categoryid AND
-                                    c.id {$courseids}
+                                WHERE c.category = :categoryid
                             ORDER BY c.sortorder ASC";
-                    $courseparams['categoryid'] = $categoryid;
+                    $courseparams = array('categoryid' => $categoryid);
                     $coursesrs = $DB->get_recordset_sql($sql, $courseparams, 0, $limit * 5);
                     foreach ($coursesrs as $course) {
+                        if ($course->id == $SITE->id) {
+                            // This should not be necessary, frontpage is not in any category.
+                            continue;
+                        }
+                        if (array_key_exists($course->id, $this->addedcourses)) {
+                            // It is probably better to not include the already loaded courses
+                            // directly in SQL because inequalities may confuse query optimisers
+                            // and may interfere with query caching.
+                            // This also helps to respect expected $limit on repeated executions.
+                            continue;
+                        }
                         if (!$this->can_add_more_courses_to_category($course->category)) {
                             break;
                         }
                         context_instance_preload($course);
-                        if ($course->id != $SITE->id && !$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
+                        if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
                             continue;
                         }
                         $coursenodes[$course->id] = $this->add_course($course);
@@ -1637,8 +1654,12 @@ class global_navigation extends navigation_node {
                     ORDER BY c.sortorder ASC";
             $coursesrs = $DB->get_recordset_sql($sql, $courseparams);
             foreach ($coursesrs as $course) {
+                if ($course->id == $SITE->id) {
+                    // frotpage is not wanted here
+                    continue;
+                }
                 context_instance_preload($course);
-                if ($course->id != $SITE->id && !$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
+                if (!$course->visible && !is_role_switched($course->id) && !has_capability('moodle/course:viewhiddencourses', get_context_instance(CONTEXT_COURSE, $course->id))) {
                     continue;
                 }
                 $coursenodes[$course->id] = $this->add_course($course);
index 6fb22d3..717dd01 100644 (file)
@@ -3424,7 +3424,7 @@ function data_get_all_recordids($dataid) {
  * runs out of records or returns a subset of records.
  *
  * @param array $recordids    An array of record ids.
- * @param array $searcharray   Contains information for the advanced search criteria
+ * @param array $searcharray  Contains information for the advanced search criteria
  * @param int $dataid         The data id of the database.
  * @return array $recordids   An array of record ids.
  */
@@ -3444,8 +3444,8 @@ function data_get_advance_search_ids($recordids, $searcharray, $dataid) {
 /**
  * Gets the record IDs given the search criteria
  *
- * @param string $alias       record alias.
- * @param array $searcharray  criteria for the search.
+ * @param string $alias       Record alias.
+ * @param array $searcharray  Criteria for the search.
  * @param int $dataid         Data ID for the database
  * @param array $recordids    An array of record IDs.
  * @return array $nestarray   An arry of record IDs
@@ -3494,11 +3494,11 @@ function data_get_recordids($alias, $searcharray, $dataid, $recordids) {
  * Returns an array with an sql string for advanced searches and the parameters that go with them.
  *
  * @param int $sort            DATA_*
- * @param stdClass $data       data module object
+ * @param stdClass $data       Data module object
  * @param array $recordids     An array of record IDs.
- * @param string $selectdata   information for the select part of the sql statement.
+ * @param string $selectdata   Information for the select part of the sql statement.
  * @param string $sortorder    Additional sort parameters
- * @return array sqlselect     sqlselect['sql] has the sql string, sqlselect['params'] contains an array of parameters.
+ * @return array sqlselect     sqlselect['sql'] has the sql string, sqlselect['params'] contains an array of parameters.
  */
 function data_get_advanced_search_sql($sort, $data, $recordids, $selectdata, $sortorder) {
     global $DB;
@@ -3509,9 +3509,31 @@ function data_get_advanced_search_sql($sort, $data, $recordids, $selectdata, $so
                              {user} u ';
         $groupsql = ' GROUP BY r.id, r.approved, r.timecreated, r.timemodified, r.userid, u.firstname, u.lastname ';
     } else {
-        $sortfield = data_get_field_from_id($sort, $data);
-        $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field());
-        $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
+        // Sorting through 'Other' criteria
+        if ($sort <= 0) {
+            switch ($sort) {
+                case DATA_LASTNAME:
+                    $sortcontentfull = "u.lastname";
+                    break;
+                case DATA_FIRSTNAME:
+                    $sortcontentfull = "u.firstname";
+                    break;
+                case DATA_APPROVED:
+                    $sortcontentfull = "r.approved";
+                    break;
+                case DATA_TIMEMODIFIED:
+                    $sortcontentfull = "r.timemodified";
+                    break;
+                case DATA_TIMEADDED:
+                default:
+                    $sortcontentfull = "r.timecreated";
+            }
+        } else {
+            $sortfield = data_get_field_from_id($sort, $data);
+            $sortcontent = $DB->sql_compare_text('c.' . $sortfield->get_sort_field());
+            $sortcontentfull = $sortfield->get_sort_sql($sortcontent);
+        }
+
         $nestselectsql = 'SELECT r.id, r.approved, r.timecreated, r.timemodified, r.userid, u.firstname, u.lastname, ' . $sortcontentfull . '
                               AS _order
                             FROM {data_content} c,
index 721402a..73d206b 100644 (file)
@@ -592,7 +592,7 @@ if ($showactivity) {
 
         $recordids = data_get_all_recordids($data->id);
         $newrecordids = data_get_advance_search_ids($recordids, $search_array, $data->id);
-        $totalcount = (count($newrecordids));
+        $totalcount = count($newrecordids);
         $selectdata = $groupselect . $approveselect;
 
         if (!empty($advanced)) {
index 2e95a10..3c9809b 100644 (file)
@@ -557,15 +557,6 @@ function feedback_cron () {
     return true;
 }
 
-/**
- * @todo: deprecated - to be deleted in 2.2
- * @return bool false
- */
-function feedback_get_participants($feedbackid) {
-    return false;
-}
-
-
 /**
  * @return bool false
  */
index 0a72477..0e154d8 100644 (file)
@@ -270,21 +270,6 @@ function lti_grades($basicltiid) {
     return null;
 }
 
-/**
- * Must return an array of user records (all data) who are participants
- * for a given instance of basiclti. Must include every user involved
- * in the instance, independient of his role (student, teacher, admin...)
- * See other modules as example.
- *
- * @param int $basicltiid ID of an instance of this module
- * @return mixed boolean/array of students
- *
- * @TODO: implement this moodle function
- **/
-function lti_get_participants($basicltiid) {
-    return false;
-}
-
 /**
  * This function returns if a scale is being used by one basiclti
  * it it has support for grading and scales. Commented code should be
index f7fc003..38a066c 100644 (file)
@@ -745,23 +745,6 @@ function quiz_grade_item_delete($quiz) {
             null, array('deleted' => 1));
 }
 
-/**
- * Returns an array of users who have data in a given quiz
- *
- * @todo: deprecated - to be deleted in 2.2
- *
- * @param int $quizid the quiz id.
- * @return array of userids.
- */
-function quiz_get_participants($quizid) {
-    global $CFG, $DB;
-
-    return $DB->get_records_sql('
-            SELECT DISTINCT userid, userid
-            JOIN {quiz_attempts} qa
-            WHERE a.quiz = ?', array($quizid));
-}
-
 /**
  * This standard function will check all instances of this module
  * and make sure there are up-to-date events created for each of them.
index 2ce8548..cebafac 100644 (file)
@@ -267,39 +267,6 @@ function survey_print_recent_activity($course, $viewfullnames, $timestart) {
     return true;
 }
 
-/**
- * Returns the users with data in one survey
- * (users with records in survey_analysis and survey_answers, students)
- *
- * @todo: deprecated - to be deleted in 2.2
- *
- * @param int $surveyid
- * @return array
- */
-function survey_get_participants($surveyid) {
-    global $DB;
-
-    //Get students from survey_analysis
-    $st_analysis = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
-                                           FROM {user} u, {survey_analysis} a
-                                          WHERE a.survey = ? AND
-                                                u.id = a.userid", array($surveyid));
-    //Get students from survey_answers
-    $st_answers = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
-                                          FROM {user} u, {survey_answers} a
-                                         WHERE a.survey = ? AND
-                                               u.id = a.userid", array($surveyid));
-
-    //Add st_answers to st_analysis
-    if ($st_answers) {
-        foreach ($st_answers as $st_answer) {
-            $st_analysis[$st_answer->id] = $st_answer;
-        }
-    }
-    //Return st_analysis array (it contains an array of unique users)
-    return ($st_analysis);
-}
-
 // SQL FUNCTIONS ////////////////////////////////////////////////////////
 
 /**
index 197b75a..fec574e 100644 (file)
@@ -235,18 +235,6 @@ function url_user_complete($course, $user, $mod, $url) {
     }
 }
 
-/**
- * Returns the users with data in one url
- *
- * @todo: deprecated - to be deleted in 2.2
- *
- * @param int $urlid
- * @return bool false
- */
-function url_get_participants($urlid) {
-    return false;
-}
-
 /**
  * Given a course_module object, this function returns any
  * "extra" information that may be needed when printing
index ff84aea..9d12728 100644 (file)
@@ -376,21 +376,6 @@ function wiki_grades($wikiid) {
     return null;
 }
 
-/**
- * Must return an array of user records (all data) who are participants
- * for a given instance of wiki. Must include every user involved
- * in the instance, independient of his role (student, teacher, admin...)
- * See other modules as example.
- *
- * @todo: deprecated - to be deleted in 2.2
- *
- * @param int $wikiid ID of an instance of this module
- * @return mixed boolean/array of students
- **/
-function wiki_get_participants($wikiid) {
-    return false;
-}
-
 /**
  * This function returns if a scale is being used by one wiki
  * it it has support for grading and scales. Commented code should be
index eb6425d..d0e26d4 100644 (file)
@@ -430,16 +430,17 @@ class workshop_rubric_strategy implements workshop_strategy {
     protected function load_fields() {
         global $DB;
 
-        $sql = 'SELECT l.id AS lid, r.id AS rid, r.*, l.*
+        $sql = "SELECT r.id AS rid, r.sort, r.description, r.descriptionformat,
+                       l.id AS lid, l.grade, l.definition, l.definitionformat
                   FROM {workshopform_rubric} r
              LEFT JOIN {workshopform_rubric_levels} l ON (l.dimensionid = r.id)
                  WHERE r.workshopid = :workshopid
-                 ORDER BY r.sort, l.grade';
+                 ORDER BY r.sort, l.grade";
         $params = array('workshopid' => $this->workshop->id);
 
-        $records = $DB->get_records_sql($sql, $params);
+        $rs = $DB->get_recordset_sql($sql, $params);
         $fields = array();
-        foreach ($records as $record) {
+        foreach ($rs as $record) {
             if (!isset($fields[$record->rid])) {
                 $fields[$record->rid] = new stdclass();
                 $fields[$record->rid]->id = $record->rid;
@@ -456,6 +457,8 @@ class workshop_rubric_strategy implements workshop_strategy {
                 $fields[$record->rid]->levels[$record->lid]->definitionformat = $record->definitionformat;
             }
         }
+        $rs->close();
+
         return $fields;
     }
 
index 8d68ec1..97e91f5 100644 (file)
@@ -949,49 +949,6 @@ function workshop_cron() {
     return true;
 }
 
-/**
- * Returns an array of user ids who are participanting in this workshop
- *
- * Participants are either submission authors or reviewers or both.
- * Authors of the example submissions and their referential assessments
- * are not returned as the example submission is considered non-user
- * data for the purpose of workshop backup.
- *
- * @todo: deprecated - to be deleted in 2.2
- *
- * @param int $workshopid ID of an instance of this module
- * @return array of user ids, empty if there are no participants
- */
-function workshop_get_participants($workshopid) {
-    global $DB;
-
-    $sql = "SELECT u.id
-              FROM {workshop} w
-              JOIN {workshop_submissions} s ON s.workshopid = w.id
-              JOIN {user} u ON s.authorid = u.id
-             WHERE w.id = ? AND s.example = 0
-
-             UNION
-
-            SELECT u.id
-              FROM {workshop} w
-              JOIN {workshop_submissions} s ON s.workshopid = w.id
-              JOIN {workshop_assessments} a ON a.submissionid = s.id
-              JOIN {user} u ON a.reviewerid = u.id
-             WHERE w.id = ? AND (s.example = 0 OR a.weight = 0)";
-
-    $users = array();
-    $rs = $DB->get_recordset_sql($sql, array($workshopid, $workshopid));
-    foreach ($rs as $user) {
-        if (empty($users[$user->id])) {
-            $users[$user->id] = $user;
-        }
-    }
-    $rs->close();
-
-    return $users;
-}
-
 /**
  * Is a given scale used by the instance of workshop?
  *
diff --git a/question/engine/simpletest/helpers.php b/question/engine/simpletest/helpers.php
new file mode 100644 (file)
index 0000000..b7093ef
--- /dev/null
@@ -0,0 +1,757 @@
+<?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 <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains helper classes for testing the question engine.
+ *
+ * @package    moodlecore
+ * @subpackage questionengine
+ * @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__) . '/../lib.php');
+
+
+/**
+ * Makes some protected methods of question_attempt public to facilitate testing.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_question_attempt extends question_attempt {
+    public function add_step(question_attempt_step $step) {
+        parent::add_step($step);
+    }
+    public function set_min_fraction($fraction) {
+        $this->minfraction = $fraction;
+    }
+    public function set_behaviour(question_behaviour $behaviour) {
+        $this->behaviour = $behaviour;
+    }
+}
+
+
+/**
+ * Base class for question type test helpers.
+ *
+ * @copyright  2011 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class question_test_helper {
+    /**
+     * @return array of example question names that can be passed as the $which
+     * argument of {@link test_question_maker::make_question} when $qtype is
+     * this question type.
+     */
+    abstract public function get_test_questions();
+}
+
+
+/**
+ * This class creates questions of various types, which can then be used when
+ * testing.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_question_maker {
+    const STANDARD_OVERALL_CORRECT_FEEDBACK = 'Well done!';
+    const STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK =
+            'Parts, but only parts, of your response are correct.';
+    const STANDARD_OVERALL_INCORRECT_FEEDBACK = 'That is not right at all.';
+
+    /** @var array qtype => qtype test helper class. */
+    protected static $testhelpers = array();
+
+    /**
+     * Just make a question_attempt at a question. Useful for unit tests that
+     * need to pass a $qa to methods that call format_text. Probably not safe
+     * to use for anything beyond that.
+     * @param question_definition $question a question.
+     * @param number $maxmark the max mark to set.
+     * @return question_attempt the question attempt.
+     */
+    public static function get_a_qa($question, $maxmark = 3) {
+        return new question_attempt($question, 13, null, $maxmark);
+    }
+
+    /**
+     * Initialise the common fields of a question of any type.
+     */
+    public static function initialise_a_question($q) {
+        global $USER;
+
+        $q->id = 0;
+        $q->category = 0;
+        $q->parent = 0;
+        $q->questiontextformat = FORMAT_HTML;
+        $q->generalfeedbackformat = FORMAT_HTML;
+        $q->defaultmark = 1;
+        $q->penalty = 0.3333333;
+        $q->length = 1;
+        $q->stamp = make_unique_id_code();
+        $q->version = make_unique_id_code();
+        $q->hidden = 0;
+        $q->timecreated = time();
+        $q->timemodified = time();
+        $q->createdby = $USER->id;
+        $q->modifiedby = $USER->id;
+    }
+
+    public static function initialise_question_data($qdata) {
+        global $USER;
+
+        $qdata->id = 0;
+        $qdata->category = 0;
+        $qdata->contextid = 0;
+        $qdata->parent = 0;
+        $qdata->questiontextformat = FORMAT_HTML;
+        $qdata->generalfeedbackformat = FORMAT_HTML;
+        $qdata->defaultmark = 1;
+        $qdata->penalty = 0.3333333;
+        $qdata->length = 1;
+        $qdata->stamp = make_unique_id_code();
+        $qdata->version = make_unique_id_code();
+        $qdata->hidden = 0;
+        $qdata->timecreated = time();
+        $qdata->timemodified = time();
+        $qdata->createdby = $USER->id;
+        $qdata->modifiedby = $USER->id;
+        $qdata->hints = array();
+    }
+
+    public static function initialise_question_form_data($qdata) {
+        $formdata = new stdClass();
+        $formdata->id = 0;
+        $formdata->category = '0,0';
+        $formdata->usecurrentcat = 1;
+        $formdata->categorymoveto = '0,0';
+        $formdata->tags = array();
+        $formdata->penalty = 0.3333333;
+        $formdata->questiontextformat = FORMAT_HTML;
+        $formdata->generalfeedbackformat = FORMAT_HTML;
+    }
+
+    /**
+     * Get the test helper class for a particular question type.
+     * @param $qtype the question type name, e.g. 'multichoice'.
+     * @return question_test_helper the test helper class.
+     */
+    public static function get_test_helper($qtype) {
+        global $CFG;
+
+        if (array_key_exists($qtype, self::$testhelpers)) {
+            return self::$testhelpers[$qtype];
+        }
+
+        $file = get_plugin_directory('qtype', $qtype) . '/simpletest/helper.php';
+        if (!is_readable($file)) {
+            throw new coding_exception('Question type ' . $qtype .
+                    ' does not have test helper code.');
+        }
+        include_once($file);
+
+        $class = 'qtype_' . $qtype . '_test_helper';
+        if (!class_exists($class)) {
+            throw new coding_exception('Class ' . $class . ' is not defined in ' . $file);
+        }
+
+        self::$testhelpers[$qtype] = new $class();
+        return self::$testhelpers[$qtype];
+    }
+
+    /**
+     * Call a method on a qtype_{$qtype}_test_helper class and return the result.
+     *
+     * @param string $methodtemplate e.g. 'make_{qtype}_question_{which}';
+     * @param string $qtype the question type to get a test question for.
+     * @param string $which one of the names returned by the get_test_questions
+     *      method of the relevant qtype_{$qtype}_test_helper class.
+     * @param unknown_type $which
+     */
+    protected static function call_question_helper_method($methodtemplate, $qtype, $which = null) {
+        $helper = self::get_test_helper($qtype);
+
+        $available = $helper->get_test_questions();
+
+        if (is_null($which)) {
+            $which = reset($available);
+        } else if (!in_array($which, $available)) {
+            throw new coding_exception('Example question ' . $which . ' of type ' .
+            $qtype . ' does not exist.');
+        }
+
+        $method = str_replace(array('{qtype}', '{which}'),
+                               array($qtype,    $which), $methodtemplate);
+
+        if (!method_exists($helper, $method)) {
+            throw new coding_exception('Method ' . $method . ' does not exist on the' .
+            $qtype . ' question type test helper class.');
+        }
+
+        return $helper->$method();
+    }
+
+    /**
+     * Question types can provide a number of test question defintions.
+     * They do this by creating a qtype_{$qtype}_test_helper class that extends
+     * question_test_helper. The get_test_questions method returns the list of
+     * test questions available for this question type.
+     *
+     * @param string $qtype the question type to get a test question for.
+     * @param string $which one of the names returned by the get_test_questions
+     *      method of the relevant qtype_{$qtype}_test_helper class.
+     * @return question_definition the requested question object.
+     */
+    public static function make_question($qtype, $which = null) {
+        return self::call_question_helper_method('make_{qtype}_question_{which}',
+                $qtype, $which);
+    }
+
+    /**
+     * Like {@link make_question()} but returns the datastructure from
+     * get_question_options instead of the question_definition object.
+     *
+     * @param string $qtype the question type to get a test question for.
+     * @param string $which one of the names returned by the get_test_questions
+     *      method of the relevant qtype_{$qtype}_test_helper class.
+     * @return stdClass the requested question object.
+     */
+    public static function get_question_data($qtype, $which = null) {
+        return self::call_question_helper_method('get_{qtype}_question_data_{which}',
+                $qtype, $which);
+    }
+
+    /**
+     * Like {@link make_question()} but returns the data what would be saved from
+     * the question editing form instead of the question_definition object.
+     *
+     * @param string $qtype the question type to get a test question for.
+     * @param string $which one of the names returned by the get_test_questions
+     *      method of the relevant qtype_{$qtype}_test_helper class.
+     * @return stdClass the requested question object.
+     */
+    public static function get_question_form_data($qtype, $which = null) {
+        return self::call_question_helper_method('get_{qtype}_question_form_data_{which}',
+                $qtype, $which);
+    }
+
+    /**
+     * Makes a multichoice question with choices 'A', 'B' and 'C' shuffled. 'A'
+     * is correct, defaultmark 1.
+     * @return qtype_multichoice_single_question
+     */
+    public static function make_a_multichoice_single_question() {
+        question_bank::load_question_definition_classes('multichoice');
+        $mc = new qtype_multichoice_single_question();
+        self::initialise_a_question($mc);
+        $mc->name = 'Multi-choice question, single response';
+        $mc->questiontext = 'The answer is A.';
+        $mc->generalfeedback = 'You should have selected A.';
+        $mc->qtype = question_bank::get_qtype('multichoice');
+
+        $mc->shuffleanswers = 1;
+        $mc->answernumbering = 'abc';
+
+        $mc->answers = array(
+            13 => new question_answer(13, 'A', 1, 'A is right', FORMAT_HTML),
+            14 => new question_answer(14, 'B', -0.3333333, 'B is wrong', FORMAT_HTML),
+            15 => new question_answer(15, 'C', -0.3333333, 'C is wrong', FORMAT_HTML),
+        );
+
+        return $mc;
+    }
+
+    /**
+     * Makes a multichoice question with choices 'A', 'B', 'C' and 'D' shuffled.
+     * 'A' and 'C' is correct, defaultmark 1.
+     * @return qtype_multichoice_multi_question
+     */
+    public static function make_a_multichoice_multi_question() {
+        question_bank::load_question_definition_classes('multichoice');
+        $mc = new qtype_multichoice_multi_question();
+        self::initialise_a_question($mc);
+        $mc->name = 'Multi-choice question, multiple response';
+        $mc->questiontext = 'The answer is A and C.';
+        $mc->generalfeedback = 'You should have selected A and C.';
+        $mc->qtype = question_bank::get_qtype('multichoice');
+
+        $mc->shuffleanswers = 1;
+        $mc->answernumbering = 'abc';
+
+        self::set_standard_combined_feedback_fields($mc);
+
+        $mc->answers = array(
+            13 => new question_answer(13, 'A', 0.5, 'A is part of the right answer', FORMAT_HTML),
+            14 => new question_answer(14, 'B', -1, 'B is wrong', FORMAT_HTML),
+            15 => new question_answer(15, 'C', 0.5, 'C is part of the right answer', FORMAT_HTML),
+            16 => new question_answer(16, 'D', -1, 'D is wrong', FORMAT_HTML),
+        );
+
+        return $mc;
+    }
+
+    /**
+     * Makes a matching question to classify 'Dog', 'Frog', 'Toad' and 'Cat' as
+     * 'Mammal', 'Amphibian' or 'Insect'.
+     * defaultmark 1. Stems are shuffled by default.
+     * @return qtype_match_question
+     */
+    public static function make_a_matching_question() {
+        question_bank::load_question_definition_classes('match');
+        $match = new qtype_match_question();
+        self::initialise_a_question($match);
+        $match->name = 'Matching question';
+        $match->questiontext = 'Classify the animals.';
+        $match->generalfeedback = 'Frogs and toads are amphibians, the others are mammals.';
+        $match->qtype = question_bank::get_qtype('match');
+
+        $match->shufflestems = 1;
+
+        self::set_standard_combined_feedback_fields($match);
+
+        // Using unset to get 1-based arrays.
+        $match->stems = array('', 'Dog', 'Frog', 'Toad', 'Cat');
+        $match->stemformat = array('', FORMAT_HTML, FORMAT_HTML, FORMAT_HTML, FORMAT_HTML);
+        $match->choices = array('', 'Mammal', 'Amphibian', 'Insect');
+        $match->right = array('', 1, 2, 2, 1);
+        unset($match->stems[0]);
+        unset($match->stemformat[0]);
+        unset($match->choices[0]);
+        unset($match->right[0]);
+
+        return $match;
+    }
+
+    /**
+     * Makes a truefalse question with correct ansewer true, defaultmark 1.
+     * @return qtype_essay_question
+     */
+    public static function make_an_essay_question() {
+        question_bank::load_question_definition_classes('essay');
+        $essay = new qtype_essay_question();
+        self::initialise_a_question($essay);
+        $essay->name = 'Essay question';
+        $essay->questiontext = 'Write an essay.';
+        $essay->generalfeedback = 'I hope you wrote an interesting essay.';
+        $essay->penalty = 0;
+        $essay->qtype = question_bank::get_qtype('essay');
+
+        $essay->responseformat = 'editor';
+        $essay->responsefieldlines = 15;
+        $essay->attachments = 0;
+        $essay->graderinfo = '';
+        $essay->graderinfoformat = FORMAT_MOODLE;
+
+        return $essay;
+    }
+
+    /**
+     * Add some standard overall feedback to a question. You need to use these
+     * specific feedback strings for the corresponding contains_..._feedback
+     * methods in {@link qbehaviour_walkthrough_test_base} to works.
+     * @param question_definition $q the question to add the feedback to.
+     */
+    public static function set_standard_combined_feedback_fields($q) {
+        $q->correctfeedback = self::STANDARD_OVERALL_CORRECT_FEEDBACK;
+        $q->correctfeedbackformat = FORMAT_HTML;
+        $q->partiallycorrectfeedback = self::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK;
+        $q->partiallycorrectfeedbackformat = FORMAT_HTML;
+        $q->shownumcorrect = true;
+        $q->incorrectfeedback = self::STANDARD_OVERALL_INCORRECT_FEEDBACK;
+        $q->incorrectfeedbackformat = FORMAT_HTML;
+    }
+}
+
+
+/**
+ * Helper for tests that need to simulate records loaded from the database.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class testing_db_record_builder {
+    public static function build_db_records(array $table) {
+        $columns = array_shift($table);
+        $records = array();
+        foreach ($table as $row) {
+            if (count($row) != count($columns)) {
+                throw new coding_exception("Row contains the wrong number of fields.");
+            }
+            $rec = new stdClass();
+            foreach ($columns as $i => $name) {
+                $rec->$name = $row[$i];
+            }
+            $records[] = $rec;
+        }
+        return $records;
+    }
+}
+
+
+/**
+ * Helper base class for tests that need to simulate records loaded from the
+ * database.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class data_loading_method_test_base extends UnitTestCase {
+    public function build_db_records(array $table) {
+        return testing_db_record_builder::build_db_records($table);
+    }
+}
+
+
+/**
+ * Helper base class for tests that walk a question through a sequents of
+ * interactions under the control of a particular behaviour.
+ *
+ * @copyright  2009 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qbehaviour_walkthrough_test_base extends UnitTestCase {
+    /** @var question_display_options */
+    protected $displayoptions;
+    /** @var question_usage_by_activity */
+    protected $quba;
+    /** @var unknown_type integer */
+    protected $slot;
+
+    public function setUp() {
+        $this->displayoptions = new question_display_options();
+        $this->quba = question_engine::make_questions_usage_by_activity('unit_test',
+                get_context_instance(CONTEXT_SYSTEM));
+    }
+
+    public function tearDown() {
+        $this->displayoptions = null;
+        $this->quba = null;
+    }
+
+    protected function start_attempt_at_question($question, $preferredbehaviour,
+            $maxmark = null, $variant = 1) {
+        $this->quba->set_preferred_behaviour($preferredbehaviour);
+        $this->slot = $this->quba->add_question($question, $maxmark);
+        $this->quba->start_question($this->slot, $variant);
+    }
+    protected function process_submission($data) {
+        $this->quba->process_action($this->slot, $data);
+    }
+
+    protected function manual_grade($comment, $mark) {
+        $this->quba->manual_grade($this->slot, $comment, $mark);
+    }
+
+    protected function check_current_state($state) {
+        $this->assertEqual($this->quba->get_question_state($this->slot), $state,
+                'Questions is in the wrong state: %s.');
+    }
+
+    protected function check_current_mark($mark) {
+        if (is_null($mark)) {
+            $this->assertNull($this->quba->get_question_mark($this->slot));
+        } else {
+            if ($mark == 0) {
+                // PHP will think a null mark and a mark of 0 are equal,
+                // so explicity check not null in this case.
+                $this->assertNotNull($this->quba->get_question_mark($this->slot));
+            }
+            $this->assertWithinMargin($mark, $this->quba->get_question_mark($this->slot),
+                    0.000001, 'Expected mark and actual mark differ: %s.');
+        }
+    }
+
+    /**
+     * @param $condition one or more Expectations. (users varargs).
+     */
+    protected function check_current_output() {
+        $html = $this->quba->render_question($this->slot, $this->displayoptions);
+        foreach (func_get_args() as $condition) {
+            $this->assert($condition, $html);
+        }
+    }
+
+    protected function get_question_attempt() {
+        return $this->quba->get_question_attempt($this->slot);
+    }
+
+    protected function get_step_count() {
+        return $this->get_question_attempt()->get_num_steps();
+    }
+
+    protected function check_step_count($expectednumsteps) {
+        $this->assertEqual($expectednumsteps, $this->get_step_count());
+    }
+
+    protected function get_step($stepnum) {
+        return $this->get_question_attempt()->get_step($stepnum);
+    }
+
+    protected function get_contains_question_text_expectation($question) {
+        return new PatternExpectation('/' . preg_quote($question->questiontext) . '/');
+    }
+
+    protected function get_contains_general_feedback_expectation($question) {
+        return new PatternExpectation('/' . preg_quote($question->generalfeedback) . '/');
+    }
+
+    protected function get_does_not_contain_correctness_expectation() {
+        return new NoPatternExpectation('/class=\"correctness/');
+    }
+
+    protected function get_contains_correct_expectation() {
+        return new PatternExpectation('/' . preg_quote(get_string('correct', 'question')) . '/');
+    }
+
+    protected function get_contains_partcorrect_expectation() {
+        return new PatternExpectation('/' .
+                preg_quote(get_string('partiallycorrect', 'question')) . '/');
+    }
+
+    protected function get_contains_incorrect_expectation() {
+        return new PatternExpectation('/' . preg_quote(get_string('incorrect', 'question')) . '/');
+    }
+
+    protected function get_contains_standard_correct_combined_feedback_expectation() {
+        return new PatternExpectation('/' .
+                preg_quote(test_question_maker::STANDARD_OVERALL_CORRECT_FEEDBACK) . '/');
+    }
+
+    protected function get_contains_standard_partiallycorrect_combined_feedback_expectation() {
+        return new PatternExpectation('/' .
+                preg_quote(test_question_maker::STANDARD_OVERALL_PARTIALLYCORRECT_FEEDBACK) . '/');
+    }
+
+    protected function get_contains_standard_incorrect_combined_feedback_expectation() {
+        return new PatternExpectation('/' .
+                preg_quote(test_question_maker::STANDARD_OVERALL_INCORRECT_FEEDBACK) . '/');
+    }
+
+    protected function get_does_not_contain_feedback_expectation() {
+        return new NoPatternExpectation('/class="feedback"/');
+    }
+
+    protected function get_does_not_contain_num_parts_correct() {
+        return new NoPatternExpectation('/class="numpartscorrect"/');
+    }
+
+    protected function get_contains_num_parts_correct($num) {
+        $a = new stdClass();
+        $a->num = $num;
+        return new PatternExpectation('/<div class="numpartscorrect">' .
+                preg_quote(get_string('yougotnright', 'question', $a)) . '/');
+    }
+
+    protected function get_does_not_contain_specific_feedback_expectation() {
+        return new NoPatternExpectation('/class="specificfeedback"/');
+    }
+
+    protected function get_contains_validation_error_expectation() {
+        return new ContainsTagWithAttribute('div', 'class', 'validationerror');
+    }
+
+    protected function get_does_not_contain_validation_error_expectation() {
+        return new NoPatternExpectation('/class="validationerror"/');
+    }
+
+    protected function get_contains_mark_summary($mark) {
+        $a = new stdClass();
+        $a->mark = format_float($mark, $this->displayoptions->markdp);
+        $a->max = format_float($this->quba->get_question_max_mark($this->slot),
+                $this->displayoptions->markdp);
+        return new PatternExpectation('/' .
+                preg_quote(get_string('markoutofmax', 'question', $a)) . '/');
+    }
+
+    protected function get_contains_marked_out_of_summary() {
+        $max = format_float($this->quba->get_question_max_mark($this->slot),
+                $this->displayoptions->markdp);
+        return new PatternExpectation('/' .
+                preg_quote(get_string('markedoutofmax', 'question', $max)) . '/');
+    }
+
+    protected function get_does_not_contain_mark_summary() {
+        return new NoPatternExpectation('/<div class="grade">/');
+    }
+
+    protected function get_contains_checkbox_expectation($baseattr, $enabled, $checked) {
+        $expectedattributes = $baseattr;
+        $forbiddenattributes = array();
+        $expectedattributes['type'] = 'checkbox';
+        if ($enabled === true) {
+            $forbiddenattributes['disabled'] = 'disabled';
+        } else if ($enabled === false) {
+            $expectedattributes['disabled'] = 'disabled';
+        }
+        if ($checked === true) {
+            $expectedattributes['checked'] = 'checked';
+        } else if ($checked === false) {
+            $forbiddenattributes['checked'] = 'checked';
+        }
+        return new ContainsTagWithAttributes('input', $expectedattributes, $forbiddenattributes);
+    }
+
+    protected function get_contains_mc_checkbox_expectation($index, $enabled = null,
+            $checked = null) {
+        return $this->get_contains_checkbox_expectation(array(
+                'name' => $this->quba->get_field_prefix($this->slot) . $index,
+                'value' => 1,
+                ), $enabled, $checked);
+    }
+
+    protected function get_contains_radio_expectation($baseattr, $enabled, $checked) {
+        $expectedattributes = $baseattr;
+        $forbiddenattributes = array();
+        $expectedattributes['type'] = 'radio';
+        if ($enabled === true) {
+            $forbiddenattributes['disabled'] = 'disabled';
+        } else if ($enabled === false) {
+            $expectedattributes['disabled'] = 'disabled';
+        }
+        if ($checked === true) {
+            $expectedattributes['checked'] = 'checked';
+        } else if ($checked === false) {
+            $forbiddenattributes['checked'] = 'checked';
+        }
+        return new ContainsTagWithAttributes('input', $expectedattributes, $forbiddenattributes);
+    }
+
+    protected function get_contains_mc_radio_expectation($index, $enabled = null, $checked = null) {
+        return $this->get_contains_radio_expectation(array(
+                'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
+                'value' => $index,
+                ), $enabled, $checked);
+    }
+
+    protected function get_contains_hidden_expectation($name, $value = null) {
+        $expectedattributes = array('type' => 'hidden', 'name' => s($name));
+        if (!is_null($value)) {
+            $expectedattributes['value'] = s($value);
+        }
+        return new ContainsTagWithAttributes('input', $expectedattributes);
+    }
+
+    protected function get_does_not_contain_hidden_expectation($name, $value = null) {
+        $expectedattributes = array('type' => 'hidden', 'name' => s($name));
+        if (!is_null($value)) {
+            $expectedattributes['value'] = s($value);
+        }
+        return new DoesNotContainTagWithAttributes('input', $expectedattributes);
+    }
+
+    protected function get_contains_tf_true_radio_expectation($enabled = null, $checked = null) {
+        return $this->get_contains_radio_expectation(array(
+                'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
+                'value' => 1,
+                ), $enabled, $checked);
+    }
+
+    protected function get_contains_tf_false_radio_expectation($enabled = null, $checked = null) {
+        return $this->get_contains_radio_expectation(array(
+                'name' => $this->quba->get_field_prefix($this->slot) . 'answer',
+                'value' => 0,
+                ), $enabled, $checked);
+    }
+
+    protected function get_contains_cbm_radio_expectation($certainty, $enabled = null,
+            $checked = null) {
+        return $this->get_contains_radio_expectation(array(
+                'name' => $this->quba->get_field_prefix($this->slot) . '-certainty',
+                'value' => $certainty,
+                ), $enabled, $checked);
+    }
+
+    protected function get_contains_button_expectation($name, $value = null, $enabled = null) {
+        $expectedattributes = array(
+            'type' => 'submit',
+            'name' => $name,
+        );
+        $forbiddenattributes = array();
+        if (!is_null($value)) {
+            $expectedattributes['value'] = $value;
+        }
+        if ($enabled === true) {
+            $forbiddenattributes['disabled'] = 'disabled';
+        } else if ($enabled === false) {
+            $expectedattributes['disabled'] = 'disabled';
+        }
+        return new ContainsTagWithAttributes('input', $expectedattributes, $forbiddenattributes);
+    }
+
+    protected function get_contains_submit_button_expectation($enabled = null) {
+        return $this->get_contains_button_expectation(
+                $this->quba->get_field_prefix($this->slot) . '-submit', null, $enabled);
+    }
+
+    protected function get_tries_remaining_expectation($n) {
+        return new PatternExpectation('/' .
+                preg_quote(get_string('triesremaining', 'qbehaviour_interactive', $n)) . '/');
+    }
+
+    protected function get_invalid_answer_expectation() {
+        return new PatternExpectation('/' .
+                preg_quote(get_string('invalidanswer', 'question')) . '/');
+    }
+
+    protected function get_contains_try_again_button_expectation($enabled = null) {
+        $expectedattributes = array(
+            'type' => 'submit',
+            'name' => $this->quba->get_field_prefix($this->slot) . '-tryagain',
+        );
+        $forbiddenattributes = array();
+        if ($enabled === true) {
+            $forbiddenattributes['disabled'] = 'disabled';
+        } else if ($enabled === false) {
+            $expectedattributes['disabled'] = 'disabled';
+        }
+        return new ContainsTagWithAttributes('input', $expectedattributes, $forbiddenattributes);
+    }
+
+    protected function get_does_not_contain_try_again_button_expectation() {
+        return new NoPatternExpectation('/name="' .
+                $this->quba->get_field_prefix($this->slot) . '-tryagain"/');
+    }
+
+    protected function get_contains_select_expectation($name, $choices,
+            $selected = null, $enabled = null) {
+        $fullname = $this->quba->get_field_prefix($this->slot) . $name;
+        return new ContainsSelectExpectation($fullname, $choices, $selected, $enabled);
+    }
+
+    protected function get_mc_right_answer_index($mc) {
+        $order = $mc->get_order($this->get_question_attempt());
+        foreach ($order as $i => $ansid) {
+            if ($mc->answers[$ansid]->fraction == 1) {
+                return $i;
+            }
+        }
+        $this->fail('This multiple choice question does not seem to have a right answer!');
+    }
+
+    protected function get_no_hint_visible_expectation() {
+        return new NoPatternExpectation('/class="hint"/');
+    }
+
+    protected function get_contains_hint_expectation($hinttext) {
+        // Does not currently verify hint text.
+        return new ContainsTagWithAttribute('div', 'class', 'hint');
+    }
+}
\ No newline at end of file
index 4eca54e..cb5177a 100644 (file)
@@ -149,8 +149,6 @@ a.skip:active {position: static;display: block;}
 .arrow,
 .arrow_button input {font-family: Arial,Helvetica,Courier,sans-serif;}
 
-#yui-module-debug {display:none;}
-
 /**
  * Header
  */