Merge branch 'MDL-37646-23' of git://github.com/damyon/moodle into MOODLE_23_STABLE
authorDan Poltawski <dan@moodle.com>
Mon, 4 Feb 2013 05:49:42 +0000 (13:49 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 4 Feb 2013 05:49:42 +0000 (13:49 +0800)
17 files changed:
calendar/lib.php
course/reset_form.php
enrol/database/lib.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_recordset.php
lib/dml/tests/dml_test.php
message/edit.php
message/index.php
mod/assign/gradingtable.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/renderer.php
mod/scorm/locallib.php
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/renderer.php
question/type/shortanswer/question.php
version.php

index a637eae..5976446 100644 (file)
@@ -729,7 +729,7 @@ function calendar_get_events($tstart, $tend, $users, $groups, $courses, $withdur
  * @return string $content return available control for the calender in html
  */
 function calendar_top_controls($type, $data) {
-    global $CFG;
+    global $CFG, $PAGE;
     $content = '';
     if(!isset($data['d'])) {
         $data['d'] = 1;
@@ -752,6 +752,7 @@ function calendar_top_controls($type, $data) {
 
     $data['m'] = $date['mon'];
     $data['y'] = $date['year'];
+    $urlbase = $PAGE->url;
 
     //Accessibility: calendar block controls, replaced <table> with <div>.
     //$nexttext = link_arrow_right(get_string('monthnext', 'access'), $url='', $accesshide=true);
@@ -761,8 +762,8 @@ function calendar_top_controls($type, $data) {
         case 'frontpage':
             list($prevmonth, $prevyear) = calendar_sub_month($data['m'], $data['y']);
             list($nextmonth, $nextyear) = calendar_add_month($data['m'], $data['y']);
-            $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), 'index.php?', 0, $nextmonth, $nextyear, $accesshide=true);
-            $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), 'index.php?', 0, $prevmonth, $prevyear, true);
+            $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), $urlbase, 0, $nextmonth, $nextyear, true);
+            $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), $urlbase, 0, $prevmonth, $prevyear, true);
 
             $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view'=>'month')), 1, $data['m'], $data['y']);
             if (!empty($data['id'])) {
@@ -788,8 +789,8 @@ function calendar_top_controls($type, $data) {
         case 'course':
             list($prevmonth, $prevyear) = calendar_sub_month($data['m'], $data['y']);
             list($nextmonth, $nextyear) = calendar_add_month($data['m'], $data['y']);
-            $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), 'view.php?id='.$data['id'].'&amp;', 0, $nextmonth, $nextyear, $accesshide=true);
-            $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), 'view.php?id='.$data['id'].'&amp;', 0, $prevmonth, $prevyear, true);
+            $nextlink = calendar_get_link_next(get_string('monthnext', 'access'), $urlbase, 0, $nextmonth, $nextyear, true);
+            $prevlink = calendar_get_link_previous(get_string('monthprev', 'access'), $urlbase, 0, $prevmonth, $prevyear, true);
 
             $calendarlink = calendar_get_link_href(new moodle_url(CALENDAR_URL.'view.php', array('view'=>'month')), 1, $data['m'], $data['y']);
             if (!empty($data['id'])) {
index b4a86fe..f1e6446 100644 (file)
@@ -107,6 +107,11 @@ class course_reset_form extends moodleform {
 
         $defaults = array ('reset_events'=>1, 'reset_logs'=>1, 'reset_roles_local'=>1, 'reset_gradebook_grades'=>1, 'reset_notes'=>1);
 
+        // Set student as default in unenrol user list, if role with student archetype exist.
+        if ($studentrole = get_archetype_roles('student')) {
+            $defaults['unenrol_users'] = array_keys($studentrole);
+        }
+
         if ($allmods = $DB->get_records('modules') ) {
             foreach ($allmods as $mod) {
                 $modname = $mod->name;
index 286bb00..65a53f1 100644 (file)
@@ -43,7 +43,7 @@ class enrol_database_plugin extends enrol_plugin {
         if (!enrol_is_enabled('database')) {
             return true;
         }
-        if (!$this->get_config('dbtype') or !$this->get_config('dbhost') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
+        if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
             return true;
         }
 
@@ -99,7 +99,7 @@ class enrol_database_plugin extends enrol_plugin {
         global $CFG, $DB;
 
         // we do not create courses here intentionally because it requires full sync and is slow
-        if (!$this->get_config('dbtype') or !$this->get_config('dbhost') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
+        if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
             return;
         }
 
@@ -288,7 +288,7 @@ class enrol_database_plugin extends enrol_plugin {
         global $CFG, $DB;
 
         // we do not create courses here intentionally because it requires full sync and is slow
-        if (!$this->get_config('dbtype') or !$this->get_config('dbhost') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
+        if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
             if ($verbose) {
                 mtrace('User enrolment synchronisation skipped.');
             }
@@ -593,7 +593,7 @@ class enrol_database_plugin extends enrol_plugin {
         global $CFG, $DB;
 
         // make sure we sync either enrolments or courses
-        if (!$this->get_config('dbtype') or !$this->get_config('dbhost') or !$this->get_config('newcoursetable') or !$this->get_config('newcoursefullname') or !$this->get_config('newcourseshortname')) {
+        if (!$this->get_config('dbtype') or !$this->get_config('newcoursetable') or !$this->get_config('newcoursefullname') or !$this->get_config('newcourseshortname')) {
             if ($verbose) {
                 mtrace('Course synchronisation skipped.');
             }
index 168c682..bfa96b9 100644 (file)
@@ -41,6 +41,8 @@ class sqlsrv_native_moodle_database extends moodle_database {
     protected $last_error_reporting; // To handle SQL*Server-Native driver default verbosity
     protected $temptables; // Control existing temptables (sqlsrv_moodle_temptables object)
     protected $collation;  // current DB collation cache
+    /** @var array list of open recordsets */
+    protected $recordsets = array();
 
     /**
      * Constructor - instantiates the database, specifying if it's external (connect to other systems) or no (Moodle DB)
@@ -789,7 +791,20 @@ class sqlsrv_native_moodle_database extends moodle_database {
      * @return sqlsrv_native_moodle_recordset
      */
     protected function create_recordset($result) {
-        return new sqlsrv_native_moodle_recordset($result);
+        $rs = new sqlsrv_native_moodle_recordset($result, $this);
+        $this->recordsets[] = $rs;
+        return $rs;
+    }
+
+    /**
+     * Do not use outside of recordset class.
+     * @internal
+     * @param sqlsrv_native_moodle_recordset $rs
+     */
+    public function recordset_closed(sqlsrv_native_moodle_recordset $rs) {
+        if ($key = array_search($rs, $this->recordsets, true)) {
+            unset($this->recordsets[$key]);
+        }
     }
 
     /**
@@ -1363,6 +1378,12 @@ class sqlsrv_native_moodle_database extends moodle_database {
      * @return void
      */
     protected function begin_transaction() {
+        // Recordsets do not work well with transactions in SQL Server,
+        // let's prefetch the recordsets to memory to work around these problems.
+        foreach ($this->recordsets as $rs) {
+            $rs->transaction_starts();
+        }
+
         $this->query_start('native sqlsrv_begin_transaction', NULL, SQL_QUERY_AUX);
         $result = sqlsrv_begin_transaction($this->sqlsrv);
         $this->query_end($result);
index 6ca88aa..677c2d0 100644 (file)
@@ -31,9 +31,49 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset {
     protected $rsrc;
     protected $current;
 
-    public function __construct($rsrc) {
-        $this->rsrc  = $rsrc;
+    /** @var array recordset buffer */
+    protected $buffer = null;
+
+    /** @var sqlsrv_native_moodle_database */
+    protected $db;
+
+    public function __construct($rsrc, sqlsrv_native_moodle_database $db) {
+        $this->rsrc    = $rsrc;
         $this->current = $this->fetch_next();
+        $this->db      = $db;
+    }
+
+    /**
+     * Inform existing open recordsets that transaction
+     * is starting, this works around MARS problem described
+     * in MDL-37734.
+     */
+    public function transaction_starts() {
+        if ($this->buffer !== null) {
+            $this->unregister();
+            return;
+        }
+        if (!$this->rsrc) {
+            $this->unregister();
+            return;
+        }
+        // This might eat memory pretty quickly...
+        raise_memory_limit('2G');
+        $this->buffer = array();
+
+        while($next = $this->fetch_next()) {
+            $this->buffer[] = $next;
+        }
+    }
+
+    /**
+     * Unregister recordset from the global list of open recordsets.
+     */
+    private function unregister() {
+        if ($this->db) {
+            $this->db->recordset_closed($this);
+            $this->db = null;
+        }
     }
 
     public function __destruct() {
@@ -41,10 +81,18 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset {
     }
 
     private function fetch_next() {
-        if ($row = sqlsrv_fetch_array($this->rsrc, SQLSRV_FETCH_ASSOC)) {
-            unset($row['sqlsrvrownumber']);
-            $row = array_change_key_case($row, CASE_LOWER);
+        if (!$this->rsrc) {
+            return false;
         }
+        if (!$row = sqlsrv_fetch_array($this->rsrc, SQLSRV_FETCH_ASSOC)) {
+            sqlsrv_free_stmt($this->rsrc);
+            $this->rsrc = null;
+            $this->unregister();
+            return false;
+        }
+
+        unset($row['sqlsrvrownumber']);
+        $row = array_change_key_case($row, CASE_LOWER);
         return $row;
     }
 
@@ -62,7 +110,11 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset {
     }
 
     public function next() {
-        $this->current = $this->fetch_next();
+        if ($this->buffer === null) {
+            $this->current = $this->fetch_next();
+        } else {
+            $this->current = array_shift($this->buffer);
+        }
     }
 
     public function valid() {
@@ -75,5 +127,7 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset {
             $this->rsrc  = null;
         }
         $this->current = null;
+        $this->buffer  = null;
+        $this->unregister();
     }
 }
index 39481ad..b1b3672 100644 (file)
@@ -4314,6 +4314,35 @@ class dml_testcase extends database_driver_testcase {
         }
         $rs1->close();
         $this->assertEquals(3, $i);
+
+        // Test nested recordsets isolation without transaction.
+        $DB->delete_records($tablename);
+        $DB->insert_record($tablename, array('course'=>1));
+        $DB->insert_record($tablename, array('course'=>2));
+        $DB->insert_record($tablename, array('course'=>3));
+
+        $DB->delete_records($tablename2);
+        $DB->insert_record($tablename2, array('course'=>5));
+        $DB->insert_record($tablename2, array('course'=>6));
+        $DB->insert_record($tablename2, array('course'=>7));
+        $DB->insert_record($tablename2, array('course'=>8));
+
+        $rs1 = $DB->get_recordset($tablename);
+        $i = 0;
+        foreach ($rs1 as $record1) {
+            $i++;
+            $rs2 = $DB->get_recordset($tablename2);
+            $j = 0;
+            foreach ($rs2 as $record2) {
+                $DB->set_field($tablename, 'course', $record1->course+1, array('id'=>$record1->id));
+                $DB->set_field($tablename2, 'course', $record2->course+1, array('id'=>$record2->id));
+                $j++;
+            }
+            $rs2->close();
+            $this->assertEquals(4, $j);
+        }
+        $rs1->close();
+        $this->assertEquals(3, $i);
     }
 
     function test_transactions_forbidden() {
index 84fc654..0878f9f 100644 (file)
@@ -45,7 +45,6 @@ if (!$user = $DB->get_record('user', array('id' => $userid))) {
 
 $systemcontext   = get_context_instance(CONTEXT_SYSTEM);
 $personalcontext = get_context_instance(CONTEXT_USER, $user->id);
-$coursecontext   = get_context_instance(CONTEXT_COURSE, $course->id);
 
 $PAGE->set_context($personalcontext);
 $PAGE->set_pagelayout('course');
index cacf6c7..f057584 100644 (file)
@@ -83,18 +83,12 @@ if ($viewing != MESSAGE_VIEW_UNREAD_MESSAGES) {
 
 $PAGE->set_url($url);
 
-$PAGE->set_context(get_context_instance(CONTEXT_USER, $USER->id));
-$PAGE->navigation->extend_for_user($USER);
-$PAGE->set_pagelayout('course');
-
 $navigationurl = new moodle_url('/message/index.php', array('user1' => $user1id));
 navigation_node::override_active_url($navigationurl);
 
 // Disable message notification popups while the user is viewing their messages
 $PAGE->set_popup_notification_allowed(false);
 
-//$context = get_context_instance(CONTEXT_SYSTEM);
-
 $user1 = null;
 $currentuser = true;
 $showactionlinks = true;
index 5e17895..c724eed 100644 (file)
@@ -105,7 +105,10 @@ class assign_grading_table extends table_sql implements renderable {
             $where .= ' AND s.timecreated > 0 ';
         }
         if ($filter == ASSIGN_FILTER_REQUIRE_GRADING) {
-            $where .= ' AND (s.timemodified > g.timemodified OR (s.timemodified IS NOT NULL AND g.timemodified IS NULL))';
+            $where .= ' AND (s.timemodified IS NOT NULL AND
+                             s.status = :submitted AND
+                             (s.timemodified > g.timemodified OR g.timemodified IS NULL))';
+            $params['submitted'] = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
         }
         if (strpos($filter, ASSIGN_FILTER_SINGLE_USER) === 0) {
             $userfilter = (int) array_pop(explode('=', $filter));
index 2058830..9b26b12 100644 (file)
@@ -295,6 +295,9 @@ function assign_print_overview($courses, &$htmlarray) {
         return true;
     }
 
+    // Definitely something to print, now include the constants we need.
+    require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
     $strduedate = get_string('duedate', 'assign');
     $strduedateno = get_string('duedateno', 'assign');
     $strgraded = get_string('graded', 'assign');
@@ -309,12 +312,25 @@ function assign_print_overview($courses, &$htmlarray) {
     //
     list($sqlassignmentids, $assignmentidparams) = $DB->get_in_or_equal($assignmentids);
 
-    // build up and array of unmarked submissions indexed by assignment id/ userid
-    // for use where the user has grading rights on assignment
-    $rs = $DB->get_recordset_sql("SELECT s.assignment as assignment, s.userid as userid, s.id as id, s.status as status, g.timemodified as timegraded
-                            FROM {assign_submission} s LEFT JOIN {assign_grades} g ON s.userid = g.userid and s.assignment = g.assignment
-                            WHERE g.timemodified = 0 OR s.timemodified > g.timemodified
-                            AND s.assignment $sqlassignmentids", $assignmentidparams);
+    // Build up and array of unmarked submissions indexed by assignment id/ userid
+    // for use where the user has grading rights on assignment.
+    $dbparams = array_merge(array(ASSIGN_SUBMISSION_STATUS_SUBMITTED), $assignmentidparams);
+    $rs = $DB->get_recordset_sql('SELECT
+                                      s.assignment as assignment,
+                                      s.userid as userid,
+                                      s.id as id,
+                                      s.status as status,
+                                      g.timemodified as timegraded
+                                  FROM {assign_submission} s
+                                  LEFT JOIN {assign_grades} g ON
+                                      s.userid = g.userid AND
+                                      s.assignment = g.assignment
+                                  WHERE
+                                      ( g.timemodified is NULL OR
+                                      s.timemodified > g.timemodified ) AND
+                                      s.timemodified IS NOT NULL AND
+                                      s.status = ? AND
+                                      s.assignment ' . $sqlassignmentids, $dbparams);
 
     $unmarkedsubmissions = array();
     foreach ($rs as $rd) {
index e63d0ff..d0eba59 100644 (file)
@@ -2180,8 +2180,12 @@ class assign {
                                         $this->get_instance()->id,
                                         $user->id);
 
-            $gradingitem = $gradinginfo->items[0];
-            $gradebookgrade = $gradingitem->grades[$user->id];
+            $gradingitem = null;
+            $gradebookgrade = '-';
+            if (isset($gradinginfo->items[0])) {
+                $gradingitem = $gradinginfo->items[0];
+                $gradebookgrade = $gradingitem->grades[$user->id];
+            }
 
             // check to see if all feedback plugins are empty
             $emptyplugins = true;
@@ -2196,24 +2200,34 @@ class assign {
             }
 
 
-            if (!($gradebookgrade->hidden) && ($gradebookgrade->grade !== null || !$emptyplugins)) {
+            $cangrade = has_capability('mod/assign:grade', $this->get_context());
+            // If there is feedback or a visible grade, show the summary.
+            if ((!empty($gradebookgrade->grade) && ($cangrade || !$gradebookgrade->hidden)) ||
+                    !$emptyplugins) {
 
-                $gradefordisplay = '';
+                $gradefordisplay = null;
+                $gradeddate = null;
+                $grader = null;
                 $gradingmanager = get_grading_manager($this->get_context(), 'mod_assign', 'submissions');
 
-                if ($controller = $gradingmanager->get_active_controller()) {
-                    $controller->set_grade_range(make_grades_menu($this->get_instance()->grade));
-                    $gradefordisplay = $controller->render_grade($PAGE,
-                                                                 $grade->id,
-                                                                 $gradingitem,
-                                                                 $gradebookgrade->str_long_grade,
-                                                                 has_capability('mod/assign:grade', $this->get_context()));
-                } else {
-                    $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
+                // Only show the grade if it is not hidden in gradebook.
+                if (!empty($gradebookgrade->grade) && ($cangrade || !$gradebookgrade->hidden)) {
+                    if ($controller = $gradingmanager->get_active_controller()) {
+                        $controller->set_grade_range(make_grades_menu($this->get_instance()->grade));
+                        $gradefordisplay = $controller->render_grade($PAGE,
+                                                                     $grade->id,
+                                                                     $gradingitem,
+                                                                     $gradebookgrade->str_long_grade,
+                                                                     $cangrade);
+                    } else {
+                        $gradefordisplay = $this->display_grade($gradebookgrade->grade, false);
+                    }
+                    $gradeddate = $gradebookgrade->dategraded;
+                    if (isset($grade->grader)) {
+                        $grader = $DB->get_record('user', array('id'=>$grade->grader));
+                    }
                 }
 
-                $gradeddate = $gradebookgrade->dategraded;
-                $grader = $DB->get_record('user', array('id'=>$grade->grader));
 
                 $feedbackstatus = new assign_feedback_status($gradefordisplay,
                                                       $gradeddate,
@@ -3096,11 +3110,20 @@ class assign {
         }
 
         if (has_all_capabilities(array('gradereport/grader:view', 'moodle/grade:viewall'), $this->get_course_context())) {
+            $usergrade = '-';
+            if (isset($gradinginfo->items[0]->grades[$userid]->str_grade)) {
+                $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
+            }
             $gradestring = $this->output->action_link(new moodle_url('/grade/report/grader/index.php',
                                                               array('id'=>$this->get_course()->id)),
-                                                $gradinginfo->items[0]->grades[$userid]->str_grade);
+                                                      $usergrade);
         } else {
-            $gradestring = $gradinginfo->items[0]->grades[$userid]->str_grade;
+            $usergrade = '-';
+            if (isset($gradinginfo->items[0]->grades[$userid]) &&
+                    !$grading_info->items[0]->grades[$userid]->hidden) {
+                $usergrade = $gradinginfo->items[0]->grades[$userid]->str_grade;
+            }
+            $gradestring = $usergrade;
         }
         $mform->addElement('static', 'finalgrade', get_string('currentgrade', 'assign').':', $gradestring);
 
index 107eae8..4af7b1f 100644 (file)
@@ -303,17 +303,19 @@ class mod_assign_renderer extends plugin_renderer_base {
         $o .= $this->output->box_start('boxaligncenter feedbacktable');
         $t = new html_table();
 
-        $row = new html_table_row();
-        $cell1 = new html_table_cell(get_string('grade'));
-        $cell2 = new html_table_cell($status->gradefordisplay);
-        $row->cells = array($cell1, $cell2);
-        $t->data[] = $row;
+        if (isset($status->gradefordisplay)) {
+            $row = new html_table_row();
+            $cell1 = new html_table_cell(get_string('grade'));
+            $cell2 = new html_table_cell($status->gradefordisplay);
+            $row->cells = array($cell1, $cell2);
+            $t->data[] = $row;
 
-        $row = new html_table_row();
-        $cell1 = new html_table_cell(get_string('gradedon', 'assign'));
-        $cell2 = new html_table_cell(userdate($status->gradeddate));
-        $row->cells = array($cell1, $cell2);
-        $t->data[] = $row;
+            $row = new html_table_row();
+            $cell1 = new html_table_cell(get_string('gradedon', 'assign'));
+            $cell2 = new html_table_cell(userdate($status->gradeddate));
+            $row->cells = array($cell1, $cell2);
+            $t->data[] = $row;
+        }
 
         if ($status->grader) {
             $row = new html_table_row();
index 182de24..c45ed5b 100644 (file)
@@ -1322,10 +1322,11 @@ function scorm_get_toc($user,$scorm,$cmid,$toclink=TOCJSLINK,$currentorg='',$sco
     //
     // If not specified retrieve the last attempt number
     //
+    $attemptsmade = scorm_get_attempt_count($user->id, $scorm);
     if (empty($attempt)) {
-        $attempt = scorm_get_attempt_count($user->id, $scorm);
+        $attempt = $attemptsmade;
     }
-    $result->attemptleft = $scorm->maxattempt == 0 ? 1 : $scorm->maxattempt - $attempt;
+    $result->attemptleft = $scorm->maxattempt == 0 ? 1 : $scorm->maxattempt - $attemptsmade;
     if ($scoes = scorm_get_scoes($scorm->id, $currentorg)){
         //
         // Retrieve user tracking data for each learning object
index f2cc2bf..9ea6c7b 100644 (file)
@@ -26,6 +26,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->dirroot . '/question/type/numerical/questiontype.php');
+
 
 /**
  * Form for editing multi-answer questions.
@@ -35,11 +37,11 @@ defined('MOODLE_INTERNAL') || die();
  */
 class qtype_multianswer_edit_form extends question_edit_form {
 
-    //  $questiondisplay will contain the qtype_multianswer_extract_question from
-    // the questiontext
+    // The variable $questiondisplay will contain the qtype_multianswer_extract_question from
+    // the questiontext.
     public $questiondisplay;
-    //  $savedquestiondisplay will contain the qtype_multianswer_extract_question
-    // from the questiontext in database
+    // The variable $savedquestiondisplay will contain the qtype_multianswer_extract_question
+    // from the questiontext in database.
     public $savedquestion;
     public $savedquestiondisplay;
     public $used_in_quiz = false;
@@ -49,6 +51,9 @@ class qtype_multianswer_edit_form extends question_edit_form {
     public $nb_of_attempts = 0;
     public $confirm = 0;
     public $reload = false;
+    /** @var qtype_numerical_answer_processor used when validating numerical answers. */
+    protected $ap = null;
+
 
     public function __construct($submiturl, $question, $category, $contexts, $formeditable = true) {
         global $SESSION, $CFG, $DB;
@@ -86,14 +91,14 @@ class qtype_multianswer_edit_form extends question_edit_form {
         // Make questiontext a required field for this question type.
         $mform->addRule('questiontext', null, 'required', null, 'client');
 
-        // display the questions from questiontext;
+        // Display the questions from questiontext.
         if ($questiontext = optional_param_array('questiontext', false, PARAM_RAW)) {
             $this->questiondisplay = fullclone(qtype_multianswer_extract_question($questiontext));
 
         } else {
             if (!$this->reload && !empty($this->savedquestiondisplay->id)) {
-                // use database data as this is first pass
-                // question->id == 0 so no stored datasets
+                // Use database data as this is first pass
+                // question->id == 0 so no stored datasets.
                 $this->questiondisplay = fullclone($this->savedquestiondisplay);
                 foreach ($this->questiondisplay->options->questions as $subquestion) {
                     if (!empty($subquestion)) {
@@ -261,7 +266,7 @@ class qtype_multianswer_edit_form extends question_edit_form {
             foreach ($question->options->questions as $key => $wrapped) {
                 if (!empty($wrapped)) {
                     // The old way of restoring the definitions is kept to gradually
-                    // update all multianswer questions
+                    // update all multianswer questions.
                     if (empty($wrapped->questiontext)) {
                         $parsableanswerdef = '{' . $wrapped->defaultmark . ':';
                         switch ($wrapped->qtype) {
@@ -288,7 +293,7 @@ class qtype_multianswer_edit_form extends question_edit_form {
                                 $parsableanswerdef .= $subanswer->answer;
                             }
                             if (!empty($wrapped->options->tolerance)) {
-                                // Special for numerical answers:
+                                // Special for numerical answers.
                                 $parsableanswerdef .= ":{$wrapped->options->tolerance}";
                                 // We only want tolerance for the first alternative, it will
                                 // be applied to all of the alternatives.
@@ -300,7 +305,7 @@ class qtype_multianswer_edit_form extends question_edit_form {
                             $separator = '~';
                         }
                         $parsableanswerdef .= '}';
-                        // Fix the questiontext fields of old questions
+                        // Fix the questiontext fields of old questions.
                         $DB->set_field('question', 'questiontext', $parsableanswerdef,
                                 array('id' => $wrapped->id));
                     } else {
@@ -312,7 +317,7 @@ class qtype_multianswer_edit_form extends question_edit_form {
             }
         }
 
-        // set default to $questiondisplay questions elements
+        // Set default to $questiondisplay questions elements.
         if ($this->reload) {
             if (isset($this->questiondisplay->options->questions)) {
                 $subquestions = fullclone($this->questiondisplay->options->questions);
@@ -321,7 +326,7 @@ class qtype_multianswer_edit_form extends question_edit_form {
                     foreach ($subquestions as $subquestion) {
                         $prefix = 'sub_'.$sub.'_';
 
-                        // validate parameters
+                        // Validate parameters.
                         $answercount = 0;
                         $maxgrade = false;
                         $maxfraction = -1;
@@ -370,7 +375,7 @@ class qtype_multianswer_edit_form extends question_edit_form {
                             if ($trimmedanswer !== '') {
                                 $answercount++;
                                 if ($subquestion->qtype == 'numerical' &&
-                                        !(is_numeric($trimmedanswer) || $trimmedanswer == '*')) {
+                                        !($this->is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
                                     $this->_form->setElementError($prefix.'answer['.$key.']',
                                             get_string('answermustbenumberorstar',
                                                     'qtype_numerical'));
@@ -423,6 +428,22 @@ class qtype_multianswer_edit_form extends question_edit_form {
         parent::set_data($question);
     }
 
+    /**
+     * Validate that a string is a nubmer formatted correctly for the current locale.
+     * @param string $x a string
+     * @return bool whether $x is a number that the numerical question type can interpret.
+     */
+    protected function is_valid_number($x) {
+        if (is_null($this->ap)) {
+            $this->ap = new qtype_numerical_answer_processor(array());
+        }
+
+        list($value, $unit) = $this->ap->apply_units($x);
+
+        return !is_null($value) && !$unit;
+    }
+
+
     public function validation($data, $files) {
         $errors = parent::validation($data, $files);
 
@@ -451,7 +472,7 @@ class qtype_multianswer_edit_form extends question_edit_form {
                         if ($trimmedanswer !== '') {
                             $answercount++;
                             if ($subquestion->qtype == 'numerical' &&
-                                    !(is_numeric($trimmedanswer) || $trimmedanswer == '*')) {
+                                    !($this->is_valid_number($trimmedanswer) || $trimmedanswer == '*')) {
                                 $errors[$prefix.'answer['.$key.']'] =
                                         get_string('answermustbenumberorstar', 'qtype_numerical');
                             }
index a24a4fd..a959080 100644 (file)
@@ -194,7 +194,7 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
             $size = max($size, strlen(trim($ans->answer)));
         }
         $size = min(60, round($size + rand(0, $size*0.15)));
-        // The rand bit is to make guessing harder
+        // The rand bit is to make guessing harder.
 
         $inputattributes = array(
             'type' => 'text',
index f7070d8..1cdcf8c 100644 (file)
@@ -87,6 +87,11 @@ class qtype_shortanswer_question extends question_graded_by_strategy
     }
 
     public static function compare_string_with_wildcard($string, $pattern, $ignorecase) {
+
+        // Normalise any non-canonical UTF-8 characters before we start.
+        $pattern = self::safe_normalize($pattern);
+        $string = self::safe_normalize($string);
+
         // Break the string on non-escaped asterisks.
         $bits = preg_split('/(?<!\\\\)\*/', $pattern);
         // Escape regexp special characters in the bits.
@@ -102,12 +107,32 @@ class qtype_shortanswer_question extends question_graded_by_strategy
             $regexp .= 'i';
         }
 
-        if (function_exists('normalizer_normalize')) {
-            $regexp = normalizer_normalize($regexp, Normalizer::FORM_C);
-            $string = normalizer_normalize($string, Normalizer::FORM_C);
+        return preg_match($regexp, trim($string));
+    }
+
+    /**
+     * Normalise a UTf-8 string to FORM_C, avoiding the pitfalls in PHP's
+     * normalizer_normalize function.
+     * @param string $string the input string.
+     * @return string the normalised string.
+     */
+    protected static function safe_normalize($string) {
+        if (!$string) {
+            return '';
         }
 
-        return preg_match($regexp, trim($string));
+        if (!function_exists('normalizer_normalize')) {
+            return $string;
+        }
+
+        $normalised = normalizer_normalize($string, Normalizer::FORM_C);
+        if (!$normalised) {
+            // An error occurred in normalizer_normalize, but we have no idea what.
+            debugging('Failed to normalise string: ' . $string, DEBUG_DEVELOPER);
+            return $string; // Return the original string, since it is the best we have.
+        }
+
+        return $normalised;
     }
 
     public function get_correct_response() {
index 3a88306..6d29995 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2012062504.03;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2012062504.04;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes
 
-$release  = '2.3.4+ (Build: 20130125)';  // Human-friendly version name
+$release  = '2.3.4+ (Build: 20130131)'; // Human-friendly version name
 
 $branch   = '23';                       // this version's branch
 $maturity = MATURITY_STABLE;            // this version's maturity level