Merge branch 'wip-mdl-34293' of git://github.com/rajeshtaneja/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 21 Aug 2012 05:56:58 +0000 (13:56 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 21 Aug 2012 05:56:58 +0000 (13:56 +0800)
82 files changed:
admin/settings/location.php
admin/tool/phpunit/cli/util.php
auth/email/auth.php
auth/ldap/auth.php
auth/manual/auth.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/backup_plan_dbops.class.php
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/tests/cronhelper_test.php [new file with mode: 0644]
backup/util/structure/restore_path_element.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/yui/backupselectall/backupselectall.js [new file with mode: 0644]
blocks/tags/block_tags.php
config-dist.php
course/recent_form.php
course/reset_form.php
course/tests/externallib_test.php
iplookup/index.php
iplookup/module.js
lang/en/admin.php
lang/en/completion.php
lib/blocklib.php
lib/completionlib.php
lib/cronlib.php
lib/db/upgrade.php
lib/editor/tinymce/extra/tools/.gitignore [deleted file]
lib/form/tests/dateselector_test.php
lib/form/tests/datetimeselector_test.php
lib/messagelib.php
lib/moodlelib.php
lib/pagelib.php
lib/phpunit/classes/data_generator.php
lib/phpunit/classes/util.php
lib/phpunit/tests/generator_test.php
lib/pluginlib.php
lib/setup.php
lib/tests/backup_test.php [deleted file]
lib/tests/moodlelib_test.php
lib/upgrade.txt
lib/weblib.php
mod/assign/backup/moodle2/backup_assign_stepslib.php
mod/assign/db/install.xml
mod/assign/db/upgrade.php
mod/assign/gradingtable.php
mod/assign/index.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/version.php
mod/book/backup/moodle2/restore_book_activity_task.class.php
mod/book/version.php
mod/chat/lib.php
mod/forum/lib.php
mod/lesson/lib.php
mod/quiz/report/responses/responses_table.php
mod/upgrade.txt
mod/wiki/editors/wikieditor.php
mod/wiki/lib.php
mod/wiki/mod_form.php
question/editlib.php
question/engine/bank.php
question/engine/datalib.php
question/format.php
question/format/blackboard/format.php
question/format/blackboard/lang/en/qformat_blackboard.php
question/format/blackboard/tests/blackboardformat_test.php [new file with mode: 0644]
question/format/blackboard/tests/fixtures/sample_blackboard.dat [new file with mode: 0644]
question/format/blackboard/version.php
question/format/examview/format.php
question/format/examview/lang/en/qformat_examview.php
question/format/examview/tests/examviewformat_test.php [new file with mode: 0644]
question/format/examview/tests/fixtures/examview_sample.xml [new file with mode: 0644]
question/format/examview/version.php
question/previewlib.php
report/stats/locallib.php
tag/edit.php
tag/lib.php
tag/locallib.php
theme/base/style/core.css
version.php

index 3d3ccb8..607090a 100644 (file)
@@ -14,7 +14,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $temp->add(new admin_setting_heading('iplookup', new lang_string('iplookup', 'admin'), new lang_string('iplookupinfo', 'admin')));
     $temp->add(new admin_setting_configfile('geoipfile', new lang_string('geoipfile', 'admin'), new lang_string('configgeoipfile', 'admin', $CFG->dataroot.'/geoip/'), $CFG->dataroot.'/geoip/GeoLiteCity.dat'));
-    $temp->add(new admin_setting_configtext('googlemapkey', new lang_string('googlemapkey', 'admin'), new lang_string('configgooglemapkey', 'admin', $CFG->wwwroot), ''));
+    $temp->add(new admin_setting_configtext('googlemapkey3', new lang_string('googlemapkey3', 'admin'), new lang_string('googlemapkey3_help', 'admin'), '', PARAM_RAW, 60));
 
     $temp->add(new admin_setting_configtext('allcountrycodes', new lang_string('allcountrycodes', 'admin'), new lang_string('configallcountrycodes', 'admin'), '', '/^(?:\w+(?:,\w+)*)?$/'));
 
index 2986d64..5afd3ba 100644 (file)
@@ -150,7 +150,7 @@ if ($diag) {
 } else if ($drop) {
     // make sure tests do not run in parallel
     phpunit_util::acquire_test_lock();
-    phpunit_util::drop_site();
+    phpunit_util::drop_site(true);
     // note: we must stop here because $CFG is messed up and we can not reinstall, sorry
     exit(0);
 
index 0051aaf..e50c09e 100644 (file)
@@ -132,7 +132,9 @@ class auth_plugin_email extends auth_plugin_base {
 
             } else if ($user->secret == $confirmsecret) {   // They have provided the secret key to get in
                 $DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
-                $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+                if ($user->firstaccess == 0) {
+                    $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+                }
                 return AUTH_CONFIRM_OK;
             }
         } else {
index 53f867f..049fd97 100644 (file)
@@ -546,7 +546,9 @@ class auth_plugin_ldap extends auth_plugin_base {
                     return AUTH_CONFIRM_FAIL;
                 }
                 $DB->set_field('user', 'confirmed', 1, array('id'=>$user->id));
-                $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id));
+                if ($user->firstaccess == 0) {
+                    $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id));
+                }
                 return AUTH_CONFIRM_OK;
             }
         } else {
index e3df78a..29cb59a 100644 (file)
@@ -170,7 +170,9 @@ class auth_plugin_manual extends auth_plugin_base {
                 return AUTH_CONFIRM_ALREADY;
             } else {
                 $DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
-                $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+                if ($user->firstaccess == 0) {
+                    $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+                }
                 return AUTH_CONFIRM_OK;
             }
         } else  {
index 24d7da9..b4399f2 100644 (file)
@@ -182,8 +182,17 @@ abstract class backup_questions_activity_structure_step extends backup_activity_
     /**
      * Attach to $element (usually attempts) the needed backup structures
      * for question_usages and all the associated data.
+     *
+     * @param backup_nested_element $element the element that will contain all the question_usages data.
+     * @param string $usageidname the name of the element that holds the usageid.
+     *      This must be child of $element, and must be a final element.
+     * @param string $nameprefix this prefix is added to all the element names we create.
+     *      Element names in the XML must be unique, so if you are using usages in
+     *      two different ways, you must give a prefix to at least one of them. If
+     *      you only use one sort of usage, then you can just use the default empty prefix.
+     *      This should include a trailing underscore. For example "myprefix_"
      */
-    protected function add_question_usages($element, $usageidname) {
+    protected function add_question_usages($element, $usageidname, $nameprefix = '') {
         global $CFG;
         require_once($CFG->dirroot . '/question/engine/lib.php');
 
@@ -195,21 +204,21 @@ abstract class backup_questions_activity_structure_step extends backup_activity_
             throw new backup_step_exception('question_states_bad_question_attempt_element', $usageidname);
         }
 
-        $quba = new backup_nested_element('question_usage', array('id'),
+        $quba = new backup_nested_element($nameprefix . 'question_usage', array('id'),
                 array('component', 'preferredbehaviour'));
 
-        $qas = new backup_nested_element('question_attempts');
-        $qa = new backup_nested_element('question_attempt', array('id'), array(
+        $qas = new backup_nested_element($nameprefix . 'question_attempts');
+        $qa = new backup_nested_element($nameprefix . 'question_attempt', array('id'), array(
                 'slot', 'behaviour', 'questionid', 'maxmark', 'minfraction',
                 'flagged', 'questionsummary', 'rightanswer', 'responsesummary',
                 'timemodified'));
 
-        $steps = new backup_nested_element('steps');
-        $step = new backup_nested_element('step', array('id'), array(
+        $steps = new backup_nested_element($nameprefix . 'steps');
+        $step = new backup_nested_element($nameprefix . 'step', array('id'), array(
                 'sequencenumber', 'state', 'fraction', 'timecreated', 'userid'));
 
-        $response = new backup_nested_element('response');
-        $variable = new backup_nested_element('variable', null,  array('name', 'value'));
+        $response = new backup_nested_element($nameprefix . 'response');
+        $variable = new backup_nested_element($nameprefix . 'variable', null,  array('name', 'value'));
 
         // Build the tree
         $element->add_child($quba);
@@ -1835,7 +1844,7 @@ class backup_annotate_all_user_files extends backup_execution_step {
             'backupid' => $this->get_backupid(), 'itemname' => 'userfinal'));
         foreach ($rs as $record) {
             $userid = $record->itemid;
-            $userctx = context_user::instance($userid);
+            $userctx = context_user::instance($userid, IGNORE_MISSING);
             if (!$userctx) {
                 continue; // User has not context, sure it's a deleted user, so cannot have files
             }
index 520274c..3677d7b 100644 (file)
@@ -3478,31 +3478,74 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     /**
      * Attach below $element (usually attempts) the needed restore_path_elements
      * to restore question_usages and all they contain.
+     *
+     * If you use the $nameprefix parameter, then you will need to implement some
+     * extra methods in your class, like
+     *
+     * protected function process_{nameprefix}question_attempt($data) {
+     *     $this->restore_question_usage_worker($data, '{nameprefix}');
+     * }
+     * protected function process_{nameprefix}question_attempt($data) {
+     *     $this->restore_question_attempt_worker($data, '{nameprefix}');
+     * }
+     * protected function process_{nameprefix}question_attempt_step($data) {
+     *     $this->restore_question_attempt_step_worker($data, '{nameprefix}');
+     * }
+     *
+     * @param restore_path_element $element the parent element that the usages are stored inside.
+     * @param array $paths the paths array that is being built.
+     * @param string $nameprefix should match the prefix passed to the corresponding
+     *      backup_questions_activity_structure_step::add_question_usages call.
      */
-    protected function add_question_usages($element, &$paths) {
+    protected function add_question_usages($element, &$paths, $nameprefix = '') {
         // Check $element is restore_path_element
         if (! $element instanceof restore_path_element) {
             throw new restore_step_exception('element_must_be_restore_path_element', $element);
         }
+
         // Check $paths is one array
         if (!is_array($paths)) {
             throw new restore_step_exception('paths_must_be_array', $paths);
         }
-        $paths[] = new restore_path_element('question_usage',
-                $element->get_path() . '/question_usage');
-        $paths[] = new restore_path_element('question_attempt',
-                $element->get_path() . '/question_usage/question_attempts/question_attempt');
-        $paths[] = new restore_path_element('question_attempt_step',
-                $element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step',
+        $paths[] = new restore_path_element($nameprefix . 'question_usage',
+                $element->get_path() . "/{$nameprefix}question_usage");
+        $paths[] = new restore_path_element($nameprefix . 'question_attempt',
+                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt");
+        $paths[] = new restore_path_element($nameprefix . 'question_attempt_step',
+                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step",
                 true);
-        $paths[] = new restore_path_element('question_attempt_step_data',
-                $element->get_path() . '/question_usage/question_attempts/question_attempt/steps/step/response/variable');
+        $paths[] = new restore_path_element($nameprefix . 'question_attempt_step_data',
+                $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step/{$nameprefix}response/{$nameprefix}variable");
     }
 
     /**
      * Process question_usages
      */
     protected function process_question_usage($data) {
+        $this->restore_question_usage_worker($data, '');
+    }
+
+    /**
+     * Process question_attempts
+     */
+    protected function process_question_attempt($data) {
+        $this->restore_question_attempt_worker($data, '');
+    }
+
+    /**
+     * Process question_attempt_steps
+     */
+    protected function process_question_attempt_step($data) {
+        $this->restore_question_attempt_step_worker($data, '');
+    }
+
+    /**
+     * This method does the acutal work for process_question_usage or
+     * process_{nameprefix}_question_usage.
+     * @param array $data the data from the XML file.
+     * @param string $nameprefix the element name prefix.
+     */
+    protected function restore_question_usage_worker($data, $nameprefix) {
         global $DB;
 
         // Clear our caches.
@@ -3520,7 +3563,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
 
         $this->inform_new_usage_id($newitemid);
 
-        $this->set_mapping('question_usage', $oldid, $newitemid, false);
+        $this->set_mapping($nameprefix . 'question_usage', $oldid, $newitemid, false);
     }
 
     /**
@@ -3532,30 +3575,36 @@ abstract class restore_questions_activity_structure_step extends restore_activit
     abstract protected function inform_new_usage_id($newusageid);
 
     /**
-     * Process question_attempts
+     * This method does the acutal work for process_question_attempt or
+     * process_{nameprefix}_question_attempt.
+     * @param array $data the data from the XML file.
+     * @param string $nameprefix the element name prefix.
      */
-    protected function process_question_attempt($data) {
+    protected function restore_question_attempt_worker($data, $nameprefix) {
         global $DB;
 
         $data = (object)$data;
         $oldid = $data->id;
         $question = $this->get_mapping('question', $data->questionid);
 
-        $data->questionusageid = $this->get_new_parentid('question_usage');
+        $data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage');
         $data->questionid      = $question->newitemid;
         $data->timemodified    = $this->apply_date_offset($data->timemodified);
 
         $newitemid = $DB->insert_record('question_attempts', $data);
 
-        $this->set_mapping('question_attempt', $oldid, $newitemid);
+        $this->set_mapping($nameprefix . 'question_attempt', $oldid, $newitemid);
         $this->qtypes[$newitemid] = $question->info->qtype;
         $this->newquestionids[$newitemid] = $data->questionid;
     }
 
     /**
-     * Process question_attempt_steps
+     * This method does the acutal work for process_question_attempt_step or
+     * process_{nameprefix}_question_attempt_step.
+     * @param array $data the data from the XML file.
+     * @param string $nameprefix the element name prefix.
      */
-    protected function process_question_attempt_step($data) {
+    protected function restore_question_attempt_step_worker($data, $nameprefix) {
         global $DB;
 
         $data = (object)$data;
@@ -3563,14 +3612,14 @@ abstract class restore_questions_activity_structure_step extends restore_activit
 
         // Pull out the response data.
         $response = array();
-        if (!empty($data->response['variable'])) {
-            foreach ($data->response['variable'] as $variable) {
+        if (!empty($data->{$nameprefix . 'response'}[$nameprefix . 'variable'])) {
+            foreach ($data->{$nameprefix . 'response'}[$nameprefix . 'variable'] as $variable) {
                 $response[$variable['name']] = $variable['value'];
             }
         }
         unset($data->response);
 
-        $data->questionattemptid = $this->get_new_parentid('question_attempt');
+        $data->questionattemptid = $this->get_new_parentid($nameprefix . 'question_attempt');
         $data->timecreated = $this->apply_date_offset($data->timecreated);
         $data->userid      = $this->get_mappingid('user', $data->userid);
 
@@ -3583,6 +3632,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
                 $this->qtypes[$data->questionattemptid],
                 $this->newquestionids[$data->questionattemptid],
                 $data->sequencenumber, $response);
+
         foreach ($response as $name => $value) {
             $row = new stdClass();
             $row->attemptstepid = $newitemid;
index 4ff5d8c..e169a8a 100644 (file)
@@ -112,7 +112,7 @@ abstract class backup_plan_dbops extends backup_dbops {
 
         // Get all sections belonging to requested course
         $sectionsarr = array();
-        $sections = $DB->get_records('course_sections', array('course' => $courseid));
+        $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section');
         foreach ($sections as $section) {
             $sectionsarr[] = $section->id;
         }
index 68e9298..ea8fda0 100644 (file)
@@ -110,7 +110,7 @@ abstract class backup_cron_automated_helper {
             $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup($admin->timezone, $now);
             $showtime = "undefined";
             if ($nextstarttime > 0) {
-                $showtime = userdate($nextstarttime,"",$admin->timezone);
+                $showtime = date('r', $nextstarttime);
             }
 
             $rs = $DB->get_recordset('course');
@@ -124,7 +124,14 @@ abstract class backup_cron_automated_helper {
                 }
 
                 // Skip courses that do not yet need backup
-                $skipped = !(($backupcourse->nextstarttime >= 0 && $backupcourse->nextstarttime < $now) || $rundirective == self::RUN_IMMEDIATELY);
+                $skipped = !(($backupcourse->nextstarttime > 0 && $backupcourse->nextstarttime < $now) || $rundirective == self::RUN_IMMEDIATELY);
+                if ($skipped && $backupcourse->nextstarttime != $nextstarttime) {
+                    $backupcourse->nextstarttime = $nextstarttime;
+                    $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED;
+                    $DB->update_record('backup_courses', $backupcourse);
+                    mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime);
+                }
+
                 // Skip backup of unavailable courses that have remained unmodified in a month
                 if (!$skipped && empty($course->visible) && ($now - $course->timemodified) > 31*24*60*60) {  //Hidden + settings were unmodified last month
                     //Check log if there were any modifications to the course content
@@ -139,9 +146,10 @@ abstract class backup_cron_automated_helper {
                         $skipped = true;
                     }
                 }
+
                 //Now we backup every non-skipped course
                 if (!$skipped) {
-                    mtrace('Backing up '.$course->fullname'...');
+                    mtrace('Backing up '.$course->fullname.'...');
 
                     //We have to send a email because we have included at least one backup
                     $emailpending = true;
@@ -255,7 +263,7 @@ abstract class backup_cron_automated_helper {
             self::BACKUP_STATUS_SKIPPED => 0,
         );
 
-        $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) statuscount FROM {backup_courses} bc GROUP BY bc.laststatus');
+        $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) AS statuscount FROM {backup_courses} bc GROUP BY bc.laststatus');
 
         foreach ($statuses as $status) {
             if (empty($status->statuscount)) {
@@ -270,38 +278,47 @@ abstract class backup_cron_automated_helper {
     /**
      * Works out the next time the automated backup should be run.
      *
-     * @param mixed $timezone
-     * @param int $now
-     * @return int
+     * @param mixed $timezone user timezone
+     * @param int $now timestamp, should not be in the past, most likely time()
+     * @return int timestamp of the next execution at server time
      */
     public static function calculate_next_automated_backup($timezone, $now) {
 
-        $result = -1;
+        $result = 0;
         $config = get_config('backup');
-        $midnight = usergetmidnight($now, $timezone);
+        $autohour = $config->backup_auto_hour;
+        $automin = $config->backup_auto_minute;
+
+        // Gets the user time relatively to the server time.
         $date = usergetdate($now, $timezone);
+        $usertime = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']);
+        $diff = $now - $usertime;
 
-        // Get number of days (from today) to execute backups
+        // Get number of days (from user's today) to execute backups.
         $automateddays = substr($config->backup_auto_weekdays, $date['wday']) . $config->backup_auto_weekdays;
-        $daysfromtoday = strpos($automateddays, "1", 1);
+        $daysfromnow = strpos($automateddays, "1");
 
-        // If we can't find the next day, we set it to tomorrow
-        if (empty($daysfromtoday)) {
-            $daysfromtoday = 1;
+        // Error, there are no days to schedule the backup for.
+        if ($daysfromnow === false) {
+            return 0;
         }
 
-        // If some day has been found
-        if ($daysfromtoday !== false) {
-            // Calculate distance
-            $dist = ($daysfromtoday * 86400) +                // Days distance
-                    ($config->backup_auto_hour * 3600) +      // Hours distance
-                    ($config->backup_auto_minute * 60);       // Minutes distance
-            $result = $midnight + $dist;
+        // Checks if the date would happen in the future (of the user).
+        $userresult = mktime($autohour, $automin, 0, $date['mon'], $date['mday'] + $daysfromnow, $date['year']);
+        if ($userresult <= $usertime) {
+            // If not, we skip the first scheduled day, that should fix it.
+            $daysfromnow = strpos($automateddays, "1", 1);
+            $userresult = mktime($autohour, $automin, 0, $date['mon'], $date['mday'] + $daysfromnow, $date['year']);
         }
 
-        // If that time is past, call the function recursively to obtain the next valid day
-        if ($result > 0 && $result < time()) {
-            $result = self::calculate_next_automated_backup($timezone, $result);
+        // Now we generate the time relative to the server.
+        $result = $userresult + $diff;
+
+        // If that time is past, call the function recursively to obtain the next valid day.
+        if ($result <= $now) {
+            // Checking time() in here works, but makes PHPUnit Tests extremely hard to predict.
+            // $now should never be earlier than time() anyway...
+            $result = self::calculate_next_automated_backup($timezone, $now + DAYSECS);
         }
 
         return $result;
@@ -411,7 +428,12 @@ abstract class backup_cron_automated_helper {
 
         $config = get_config('backup');
         $active = (int)$config->backup_auto_active;
-        if ($active === self::AUTO_BACKUP_DISABLED || ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL)) {
+        $weekdays = (string)$config->backup_auto_weekdays;
+
+        // In case of automated backup also check that it is scheduled for at least one weekday.
+        if ($active === self::AUTO_BACKUP_DISABLED ||
+                ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL) ||
+                ($rundirective == self::RUN_ON_SCHEDULE && strpos($weekdays, '1') === false)) {
             return self::STATE_DISABLED;
         } else if (!empty($config->backup_auto_running)) {
             // Detect if the backup_auto_running semaphore is a valid one
diff --git a/backup/util/helper/tests/cronhelper_test.php b/backup/util/helper/tests/cronhelper_test.php
new file mode 100644 (file)
index 0000000..7afd1be
--- /dev/null
@@ -0,0 +1,442 @@
+<?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/>.
+
+/**
+ * Unit tests for backups cron helper.
+ *
+ * @package   core_backup
+ * @category  phpunit
+ * @copyright 2012 Frédéric Massart <fred@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php');
+
+/**
+ * Unit tests for backup cron helper
+ */
+class backup_cron_helper_testcase extends advanced_testcase {
+
+    /**
+     * Test {@link backup_cron_automated_helper::calculate_next_automated_backup}.
+     */
+    public function test_next_automated_backup() {
+        $this->resetAfterTest();
+        set_config('backup_auto_active', '1', 'backup');
+
+        // Notes
+        // - backup_auto_weekdays starts on Sunday
+        // - Tests cannot be done in the past
+        // - Only the DST on the server side is handled.
+
+        // Every Tue and Fri at 11pm.
+        set_config('backup_auto_weekdays', '0010010', 'backup');
+        set_config('backup_auto_hour', '23', 'backup');
+        set_config('backup_auto_minute', '0', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 18:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('5-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('5-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('5-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-23:00', date('w-H:i', $next));
+
+        // Every Sun and Sat at 12pm.
+        set_config('backup_auto_weekdays', '1000001', 'backup');
+        set_config('backup_auto_hour', '0', 'backup');
+        set_config('backup_auto_minute', '0', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        // Every Sun at 4am.
+        set_config('backup_auto_weekdays', '1000000', 'backup');
+        set_config('backup_auto_hour', '4', 'backup');
+        set_config('backup_auto_minute', '0', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        // Every day but Wed at 8:30pm.
+        set_config('backup_auto_weekdays', '1110111', 'backup');
+        set_config('backup_auto_hour', '20', 'backup');
+        set_config('backup_auto_minute', '30', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('1-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('4-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('4-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('5-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-20:30', date('w-H:i', $next));
+
+        // Sun, Tue, Thu, Sat at 12pm.
+        set_config('backup_auto_weekdays', '1010101', 'backup');
+        set_config('backup_auto_hour', '0', 'backup');
+        set_config('backup_auto_minute', '0', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('4-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('4-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-00:00', date('w-H:i', $next));
+
+        // None.
+        set_config('backup_auto_weekdays', '0000000', 'backup');
+        set_config('backup_auto_hour', '15', 'backup');
+        set_config('backup_auto_minute', '30', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Sunday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0', $next);
+
+        // Playing with timezones.
+        set_config('backup_auto_weekdays', '1111111', 'backup');
+        set_config('backup_auto_hour', '20', 'backup');
+        set_config('backup_auto_minute', '00', 'backup');
+
+        $timezone = 99;
+        date_default_timezone_set('Australia/Perth');
+        $now = strtotime('18:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
+
+        $timezone = 99;
+        date_default_timezone_set('Europe/Brussels');
+        $now = strtotime('18:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
+
+        $timezone = 99;
+        date_default_timezone_set('America/New_York');
+        $now = strtotime('18:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
+
+        // Viva Australia! (UTC+8).
+        date_default_timezone_set('Australia/Perth');
+        $now = strtotime('18:00:00');
+
+        $timezone = -10.0; // 12am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-14:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        $timezone = -5.0; // 5am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-09:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        $timezone = 0.0;  // 10am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-04:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        $timezone = 3.0; // 1pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-01:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        $timezone = 8.0; // 6pm for the user (same than the server).
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
+
+        $timezone = 9.0; // 7pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-19:00'), date('w-H:i', $next));
+
+        $timezone = 13.0; // 12am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-15:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        // Let's have a Belgian beer! (UTC+1 / UTC+2 DST).
+        date_default_timezone_set('Europe/Brussels');
+        $now = strtotime('18:00:00');
+        $dst = date('I');
+
+        $timezone = -10.0; // 7am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-07:00', strtotime('tomorrow')) : date('w-08:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = -5.0; // 12pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-02:00', strtotime('tomorrow')) : date('w-03:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 0.0;  // 5pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-21:00') : date('w-22:00');
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 3.0; // 8pm for the user (note the expected time is today while in DST).
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-18:00', strtotime('tomorrow')) : date('w-19:00');
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 8.0; // 1am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-13:00', strtotime('tomorrow')) : date('w-14:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 9.0; // 2am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-12:00', strtotime('tomorrow')) : date('w-13:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 13.0; // 6am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-08:00', strtotime('tomorrow')) : date('w-09:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        // The big apple! (UTC-5 / UTC-4 DST).
+        date_default_timezone_set('America/New_York');
+        $now = strtotime('18:00:00');
+        $dst = date('I');
+
+        $timezone = -10.0; // 1pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-01:00', strtotime('tomorrow')) : date('w-02:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = -5.0; // 6pm for the user (server time).
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-20:00') : date('w-21:00');
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 0.0;  // 11pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-15:00', strtotime('tomorrow')) : date('w-16:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 3.0; // 2am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-12:00', strtotime('tomorrow')) : date('w-13:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 8.0; // 7am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-07:00', strtotime('tomorrow')) : date('w-08:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 9.0; // 8am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-06:00', strtotime('tomorrow')) : date('w-07:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 13.0; // 6am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-02:00', strtotime('tomorrow')) : date('w-03:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        // Some more timezone tests
+        set_config('backup_auto_weekdays', '0100001', 'backup');
+        set_config('backup_auto_hour', '20', 'backup');
+        set_config('backup_auto_minute', '00', 'backup');
+
+        date_default_timezone_set('Europe/Brussels');
+        $now = strtotime('next Monday 18:00:00');
+        $dst = date('I');
+
+        $timezone = -12.0;  // 1pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '2-09:00' : '2-10:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = -4.0;  // 1pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '2-01:00' : '2-02:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 0.0;  // 5pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '1-21:00' : '1-22:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 2.0;  // 7pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '1-19:00' : '1-20:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 4.0;  // 9pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '6-17:00' : '6-18:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 12.0;  // 6am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '6-09:00' : '6-10:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        // Some more timezone tests
+        set_config('backup_auto_weekdays', '0100001', 'backup');
+        set_config('backup_auto_hour', '02', 'backup');
+        set_config('backup_auto_minute', '00', 'backup');
+
+        date_default_timezone_set('America/New_York');
+        $now = strtotime('next Monday 04:00:00');
+        $dst = date('I');
+
+        $timezone = -12.0;  // 8pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '1-09:00' : '1-10:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = -4.0;  // 4am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '6-01:00' : '6-02:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 0.0;  // 8am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '5-21:00' : '5-22:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 2.0;  // 10am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '5-19:00' : '5-20:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 4.0;  // 12pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '5-17:00' : '5-18:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 12.0;  // 8pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '5-09:00' : '5-10:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+    }
+}
index 9914f98..716ac69 100644 (file)
@@ -47,9 +47,9 @@ class restore_path_element {
     /**
      * Constructor - instantiates one restore_path_element, specifying its basic info.
      *
-     * @param string $name name of the element
-     * @param string $path path of the element
-     * @param bool $grouped to gather information in grouped mode or no
+     * @param string $name name of the thing being restored. This determines the name of the process_... method called.
+     * @param string $path path of the element.
+     * @param bool $grouped to gather information in grouped mode or no.
      */
     public function __construct($name, $path, $grouped = false) {
 
index 3a72edc..ce95e7a 100644 (file)
@@ -324,6 +324,9 @@ abstract class base_moodleform extends moodleform {
         $config->noLabel = get_string('confirmcancelno', 'backup');
         $PAGE->requires->yui_module('moodle-backup-confirmcancel', 'M.core_backup.watch_cancel_buttons', array($config));
 
+        $PAGE->requires->yui_module('moodle-backup-backupselectall', 'M.core_backup.select_all_init',
+                array(array('select' => get_string('select'), 'all' => get_string('all'), 'none' => get_string('none'))));
+
         parent::display();
     }
 
diff --git a/backup/util/ui/yui/backupselectall/backupselectall.js b/backup/util/ui/yui/backupselectall/backupselectall.js
new file mode 100644 (file)
index 0000000..9f05b7f
--- /dev/null
@@ -0,0 +1,80 @@
+YUI.add('moodle-backup-backupselectall', function(Y) {
+
+// Namespace for the backup
+M.core_backup = M.core_backup || {};
+
+/**
+ * Adds select all/none links to the top of the backup/restore/import schema page.
+ */
+M.core_backup.select_all_init = function(str) {
+    var formid = null;
+
+    var helper = function(e, check, type) {
+        e.preventDefault();
+
+        var len = type.length;
+        Y.all('input[type="checkbox"]').each(function(checkbox) {
+            var name = checkbox.get('name');
+            if (name.substring(name.length - len) == type) {
+                checkbox.set('checked', check);
+            }
+        });
+
+        // At this point, we really need to persuade the form we are part of to
+        // update all of its disabledIf rules. However, as far as I can see,
+        // given the way that lib/form/form.js is written, that is impossible.
+        if (formid && M.form) {
+            M.form.updateFormState(formid);
+        }
+    };
+
+    var html_generator = function(classname, idtype) {
+        return '<div class="' + classname + '">' +
+                    '<div class="fitem fitem_fcheckbox">' +
+                        '<div class="fitemtitle">' + str.select + '</div>' +
+                        '<div class="felement">' +
+                            '<a id="backup-all-' + idtype + '" href="#">' + str.all + '</a> / ' +
+                            '<a id="backup-none-' + idtype + '" href="#">' + str.none + '</a>' +
+                        '</div>' +
+                    '</div>' +
+                '</div>';
+    };
+
+    var firstsection = Y.one('fieldset#coursesettings .fcontainer.clearfix .grouped_settings.section_level');
+    if (!firstsection) {
+        // This is not a relevant page.
+        return;
+    }
+    if (!firstsection.one('.felement.fcheckbox')) {
+        // No checkboxes.
+        return;
+    }
+
+    formid = firstsection.ancestor('form').getAttribute('id');
+
+    var withuserdata = false;
+    Y.all('input[type="checkbox"]').each(function(checkbox) {
+        var name = checkbox.get('name');
+        if (name.substring(name.length - 9) == '_userdata') {
+            withuserdata = '_userdata';
+        } else if (name.substring(name.length - 9) == '_userinfo') {
+            withuserdata = '_userinfo';
+        }
+    });
+
+    var html = html_generator('include_setting section_level', 'included');
+    if (withuserdata) {
+        html += html_generator('normal_setting', 'userdata');
+    }
+    var links = Y.Node.create('<div class="grouped_settings section_level">' + html + '</div>');
+    firstsection.insert(links, 'before');
+
+    Y.one('#backup-all-included').on('click',  function(e) { helper(e, true,  '_included'); });
+    Y.one('#backup-none-included').on('click', function(e) { helper(e, false, '_included'); });
+    if (withuserdata) {
+        Y.one('#backup-all-userdata').on('click',  function(e) { helper(e, true,  withuserdata); });
+        Y.one('#backup-none-userdata').on('click', function(e) { helper(e, false, withuserdata); });
+    }
+}
+
+}, '@VERSION@', {'requires':['base','node','event', 'node-event-simulate']});
index 0e2e341..0e429f3 100644 (file)
@@ -40,6 +40,7 @@ class block_tags extends block_base {
         global $CFG, $COURSE, $SITE, $USER, $SCRIPT, $OUTPUT;
 
         if (empty($CFG->usetags)) {
+            $this->content = new stdClass();
             $this->content->text = '';
             if ($this->page->user_is_editing()) {
                 $this->content->text = get_string('disabledtags', 'block_tags');
index eb0ab29..feb2902 100644 (file)
@@ -211,10 +211,12 @@ $CFG->admin = 'admin';
 // You can specify a different class to be created for the $PAGE global, and to
 // compute which blocks appear on each page. However, I cannot think of any good
 // reason why you would need to change that. It just felt wrong to hard-code the
-// the class name. You are stronly advised not to use these to settings unless
+// the class name. You are strongly advised not to use these to settings unless
 // you are absolutely sure you know what you are doing.
 //      $CFG->moodlepageclass = 'moodle_page';
+//      $CFG->moodlepageclassfile = "$CFG->dirroot/local/myplugin/mypageclass.php";
 //      $CFG->blockmanagerclass = 'block_manager';
+//      $CFG->blockmanagerclassfile = "$CFG->dirroot/local/myplugin/myblockamanagerclass.php";
 //
 // Seconds for files to remain in caches. Decrease this if you are worried
 // about students being served outdated versions of uploaded files.
index 642a24d..46a3137 100644 (file)
@@ -101,8 +101,6 @@ class recent_form extends moodleform {
             $mform->setAdvanced('user');
         }
 
-        $sectiontitle = get_string('sectionname', 'format_'.$COURSE->format);
-
         $options = array(''=>get_string('allactivities'));
         $modsused = array();
 
index f340725..751c0c7 100644 (file)
@@ -19,7 +19,7 @@ class course_reset_form extends moodleform {
         $mform->addElement('checkbox', 'reset_logs', get_string('deletelogs'));
         $mform->addElement('checkbox', 'reset_notes', get_string('deletenotes', 'notes'));
         $mform->addElement('checkbox', 'reset_comments', get_string('deleteallcomments', 'moodle'));
-        $mform->addElement('checkbox', 'reset_course_completion', get_string('deletecoursecompletiondata', 'completion'));
+        $mform->addElement('checkbox', 'reset_completion', get_string('deletecompletiondata', 'completion'));
         $mform->addElement('checkbox', 'delete_blog_associations', get_string('deleteblogassociations', 'blog'));
         $mform->addHelpButton('delete_blog_associations', 'deleteblogassociations', 'blog');
 
index ac3c4d9..08f8b4a 100644 (file)
@@ -315,7 +315,7 @@ class core_course_external_testcase extends externallib_advanced_testcase {
         $course2['format'] = 'weeks';
         $course2['showgrades'] = 1;
         $course2['newsitems'] = 3;
-        $course2['startdate'] = 32882306400; // 01/01/3012
+        $course2['startdate'] = 1420092000; // 01/01/2015
         $course2['numsections'] = 4;
         $course2['maxbytes'] = 100000;
         $course2['showreports'] = 1;
index e876e8f..92dd51e 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -20,8 +19,7 @@
  *
  * This script is not compatible with IPv6.
  *
- * @package    core
- * @subpackage iplookup
+ * @package    core_iplookup
  * @copyright  2008 Petr Skoda (http://skodak.org)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -35,7 +33,7 @@ $ip   = optional_param('ip', getremoteaddr(), PARAM_HOST);
 $user = optional_param('user', 0, PARAM_INT);
 
 if (isset($CFG->iplookup)) {
-    //clean up of old settings
+    // Clean up of old settings.
     set_config('iplookup', NULL);
 }
 
@@ -61,7 +59,7 @@ if ($match[1] == '127' or $match[1] == '10' or ($match[1] == '172' and $match[2]
 $info = iplookup_find_location($ip);
 
 if ($info['error']) {
-    // can not display
+    // Can not display.
     notice($info['error']);
 }
 
@@ -80,7 +78,7 @@ $PAGE->set_title(get_string('iplookup', 'admin').': '.$title);
 $PAGE->set_heading($title);
 echo $OUTPUT->header();
 
-if (empty($CFG->googlemapkey)) {
+if (empty($CFG->googlemapkey3)) {
     $imgwidth  = 620;
     $imgheight = 310;
     $dotwidth  = 18;
@@ -96,9 +94,13 @@ if (empty($CFG->googlemapkey)) {
     echo '<div id="note">'.$info['note'].'</div>';
 
 } else {
-    $PAGE->requires->js(new moodle_url("http://maps.google.com/maps?file=api&v=2&key=$CFG->googlemapkey"));
+    if (strpos($CFG->wwwroot, 'https:') === 0) {
+        $PAGE->requires->js(new moodle_url('https://maps.googleapis.com/maps/api/js', array('key'=>$CFG->googlemapkey3, 'sensor'=>'false')));
+    } else {
+        $PAGE->requires->js(new moodle_url('http://maps.googleapis.com/maps/api/js', array('key'=>$CFG->googlemapkey3, 'sensor'=>'false')));
+    }
     $module = array('name'=>'core_iplookup', 'fullpath'=>'/iplookup/module.js');
-    $PAGE->requires->js_init_call('M.core_iplookup.init', array($info['latitude'], $info['longitude']), true, $module);
+    $PAGE->requires->js_init_call('M.core_iplookup.init3', array($info['latitude'], $info['longitude'], $ip), true, $module);
 
     echo '<div id="map" style="width: 650px; height: 360px"></div>';
     echo '<div id="note">'.$info['note'].'</div>';
index dba71cc..d706caf 100644 (file)
 /**
  * Iplookup utility functions
  *
- * @package    core
- * @subpackage iplookup
+ * @package    core_iplookup
  * @copyright  2008 Petr Skoda (http://skodak.org)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 M.core_iplookup = {};
 
-M.core_iplookup.init = function(Y, latitude, longitude) {
-    if (GBrowserIsCompatible()) {
-        var map = new GMap2(document.getElementById("map"));
-        map.addControl(new GSmallMapControl());
-        map.addControl(new GMapTypeControl());
-        var point = new GLatLng(latitude, longitude);
-        map.setCenter(point, 4);
-        map.addOverlay(new GMarker(point));
-        map.setMapType(G_HYBRID_MAP);
+M.core_iplookup.init3 = function(Y, latitude, longitude, ip) {
+    var ipLatlng = new google.maps.LatLng(latitude, longitude);
 
-        Y.on('unload', function() {
-            if (GBrowserIsCompatible()) {
-                GUnload();
-            }
-        }, document.body);
-    }
+    var mapOptions = {
+        center: ipLatlng,
+        zoom: 6,
+        mapTypeId: google.maps.MapTypeId.ROADMAP
+    };
+
+    var map = new google.maps.Map(document.getElementById("map"), mapOptions);
+
+    var marker = new google.maps.Marker({
+        position: ipLatlng,
+        map: map,
+        title: ip
+    });
 };
index 14fec09..7f8972a 100644 (file)
@@ -215,11 +215,10 @@ $string['configforcelogin'] = 'Normally, the front page of the site and the cour
 $string['configforceloginforprofiles'] = 'This setting forces people to login as a real (non-guest) account before viewing any user\'s profile. If you disabled this setting, you may find that some users post advertising (spam) or other inappropriate content in their profiles, which is then visible to the whole world.';
 $string['configfrontpage'] = 'The items selected above will be displayed on the site\'s front page.';
 $string['configfrontpageloggedin'] = 'The items selected above will be displayed on the site\'s front page when a user is logged in.';
-$string['configfullnamedisplay'] = 'This defines how names are shown when they are displayed in full. For most mono-lingual sites the most efficient setting is the default "First name + Surname", but you may choose to hide surnames altogether, or to leave it up to the current language pack to decide (some languages have different conventions).';
+$string['configfullnamedisplay'] = 'This defines how names are shown when they are displayed in full. For most mono-lingual sites the most efficient setting is "First name + Surname", but you may choose to hide surnames altogether, or to leave it up to the current language pack to decide (some languages have different conventions).';
 $string['configgdversion'] = 'Indicate the version of GD that is installed.  The version shown by default is the one that has been auto-detected.  Don\'t change this unless you really know what you\'re doing.';
 $string['configgeoipfile'] = 'Location of GeoIP City binary data file. This file is not part of Moodle distribution and must be obtained separately from <a href="http://www.maxmind.com/">MaxMind</a>. You can either buy a commercial version or use the free version.<br />Simply download <a href="http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz" >http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz</a> and extract it into "{$a}" directory on your server.';
 $string['configgetremoteaddrconf'] = 'If your server is behind a reverse proxy, you can use this setting to specify which HTTP headers can be trusted to contain the remote IP address. The headers are read in order, using the first one that is available.';
-$string['configgooglemapkey'] = 'You need to enter a special key to use Google Maps for IP address lookup visualization. You can obtain the key free of charge at <a href="http://code.google.com/apis/maps/signup.html" >http://code.google.com/apis/maps/signup.html</a>.<br />Your web site URL is: {$a}';
 $string['configgradebookroles'] = 'This setting allows you to control who appears on the gradebook.  Users need to have at least one of these roles in a course to be shown in the gradebook for that course.';
 $string['configgradeexport'] = 'Choose which gradebook export formats are your primary methods for exporting grades.  Chosen plugins will then set and use a "last exported" field for every grade.  For example, this might result in exported records being identified as being "new" or "updated".  If you are not sure about this then leave everything unchecked.';
 $string['confighiddenuserfields'] = 'Select which user information fields you wish to hide from other users other than course teachers/admins. This will increase student privacy. Hold CTRL key to select multiple fields.';
@@ -546,7 +545,8 @@ $string['globalsquoteswarning'] = '<p><strong>Security Warning</strong>: to oper
 $string['globalswarning'] = '<p><strong>SECURITY WARNING!</strong></p><p> To operate properly, Moodle requires <br />that you make certain changes to your current PHP settings.</p><p>You <em>must</em> set <code>register_globals=off</code>.</p><p>This setting is controlled by editing your <code>php.ini</code>, Apache/IIS <br />configuration or <code>.htaccess</code> file.</p>';
 $string['groupenrolmentkeypolicy'] = 'Group enrolment key policy';
 $string['groupenrolmentkeypolicy_desc'] = 'Turning this on will make Moodle check group enrolment keys against a valid password policy.';
-$string['googlemapkey'] = 'Google Maps API key';
+$string['googlemapkey3'] = 'Google Maps API V3 key';
+$string['googlemapkey3_help'] = 'You need to enter a special key to use Google Maps for IP address lookup visualization. You can obtain the key free of charge at <a href="https://developers.google.com/maps/documentation/javascript/tutorial#api_key" target="_blank">https://developers.google.com/maps/documentation/javascript/tutorial#api_key</a>';
 $string['gotofirst'] = 'Go to first missing string';
 $string['gradebook'] = 'Gradebook';
 $string['gradebookroles'] = 'Graded roles';
index 8b73c59..a488b9d 100644 (file)
@@ -70,7 +70,7 @@ $string['completionview'] = 'Require view';
 $string['completionview_desc'] = 'Student must view this activity to complete it';
 $string['configenablecompletion'] = 'When enabled, this lets you turn on completion tracking (progress) features at course level.';
 $string['csvdownload'] = 'Download in spreadsheet format (UTF-8 .csv)';
-$string['deletecoursecompletiondata'] = 'Delete course completion data';
+$string['deletecompletiondata'] = 'Delete completion data';
 $string['enablecompletion'] = 'Enable completion tracking';
 $string['err_noactivities'] = 'Completion information is not enabled for any activity, so none can be displayed. You can enable completion information by editing the settings for an activity.';
 $string['err_nousers'] = 'There are no students on this course or group for whom completion information is displayed. (By default, completion information is displayed only for students, so if there are no students, you will see this error. Administrators can alter this option via the admin screens.)';
index b74272f..79ec70d 100644 (file)
@@ -457,6 +457,12 @@ class block_manager {
         if (!$this->page->theme->enable_dock) {
             return false;
         }
+
+        // Do not dock the region when the user attemps to move a block.
+        if ($this->movingblock) {
+            return false;
+        }
+
         $this->check_is_loaded();
         $this->ensure_content_created($region, $output);
         foreach($this->visibleblockcontent[$region] as $instance) {
index f30b4f0..4f15263 100644 (file)
@@ -711,6 +711,31 @@ class completion_info {
         $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id));
     }
 
+    /**
+     * Deletes all activity and course completion data for an entire course
+     * (the below delete_all_state function does this for a single activity).
+     *
+     * Used by course reset page.
+     */
+    public function delete_all_completion_data() {
+        global $DB;
+
+        // Delete from database.
+        $DB->delete_records_select('course_modules_completion',
+                'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=?)',
+                array($this->course_id));
+
+        // Reset cache for current user.
+        if (isset($SESSION->completioncache) &&
+            array_key_exists($this->course_id, $SESSION->completioncache)) {
+
+            unset($SESSION->completioncache[$this->course_id]);
+        }
+
+        // Wipe course completion data too.
+        $this->delete_course_completion_data();
+    }
+
     /**
      * Deletes completion state related to an activity for all users.
      *
index 7ec99e5..457336a 100644 (file)
@@ -373,6 +373,13 @@ function cron_run() {
     }
 
 
+    // Run question bank clean-up.
+    mtrace("Starting the question bank cron...", '');
+    require_once($CFG->libdir . '/questionlib.php');
+    question_bank::cron();
+    mtrace('done.');
+
+
     //Run registration updated cron
     mtrace(get_string('siteupdatesstart', 'hub'));
     require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php');
index 6315ca1..d5b9856 100644 (file)
@@ -1113,5 +1113,12 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012081400.01);
     }
 
+    if ($oldversion < 2012081600.01) {
+        // Delete removed setting - Google Maps API V2 will not work in 2013.
+        unset_config('googlemapkey');
+        upgrade_main_savepoint(true, 2012081600.01);
+    }
+
+
     return true;
 }
diff --git a/lib/editor/tinymce/extra/tools/.gitignore b/lib/editor/tinymce/extra/tools/.gitignore
deleted file mode 100644 (file)
index 9c595a6..0000000
+++ /dev/null
@@ -1 +0,0 @@
-temp
index ab8cb9a..8db9155 100644 (file)
@@ -128,7 +128,7 @@ class dateselector_form_element_testcase extends basic_testcase {
                 'year' => 2011,
                 'usertimezone' => 0.0,
                 'timezone' => 0.0,
-                'timestamp' => 1309449600
+                'timestamp' => 1309478400 // 6am at UTC+0
             ),
             array (
                 'day' => 1,
@@ -136,7 +136,7 @@ class dateselector_form_element_testcase extends basic_testcase {
                 'year' => 2011,
                 'usertimezone' => 0.0,
                 'timezone' => 99,
-                'timestamp' => 1309449600
+                'timestamp' => 1309478400 // 6am at UTC+0
             )
         );
     }
index b0ce16e..abd5cea 100644 (file)
@@ -138,7 +138,7 @@ class datetimeselector_form_element_testcase extends basic_testcase {
                 'year' => 2011,
                 'usertimezone' => 0.0,
                 'timezone' => 0.0,
-                'timestamp' => 1309449600
+                'timestamp' => 1309478400 // 6am at UTC+0
             ),
             array (
                 'minute' => 0,
@@ -148,7 +148,7 @@ class datetimeselector_form_element_testcase extends basic_testcase {
                 'year' => 2011,
                 'usertimezone' => 0.0,
                 'timezone' => 99,
-                'timestamp' => 1309449600
+                'timestamp' => 1309478400 // 6am at UTC+0
             )
         );
     }
index b184f0d..140ae0e 100644 (file)
@@ -60,7 +60,7 @@ function message_send($eventdata) {
     //TODO: we need to solve problems with database transactions here somehow, for now we just prevent transactions - sorry
     $DB->transactions_forbidden();
 
-    if (is_int($eventdata->userto)) {
+    if (is_number($eventdata->userto)) {
         $eventdata->userto = $DB->get_record('user', array('id' => $eventdata->userto));
     }
     if (is_int($eventdata->userfrom)) {
index 24f452f..5640dbf 100644 (file)
@@ -2330,10 +2330,10 @@ function get_user_timezone($tz = 99) {
 
     $tz = 99;
 
-    while(($tz == '' || $tz == 99 || $tz == NULL) && $next = each($timezones)) {
+    // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array
+    while(((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) {
         $tz = $next['value'];
     }
-
     return is_numeric($tz) ? (float) $tz : $tz;
 }
 
@@ -3289,11 +3289,24 @@ function get_user_key($script, $userid, $instance=null, $iprestriction=null, $va
 function update_user_login_times() {
     global $USER, $DB;
 
+    $now = time();
+
     $user = new stdClass();
+    $user->id = $USER->id;
+
+    // Make sure all users that logged in have some firstaccess.
+    if ($USER->firstaccess == 0) {
+        $USER->firstaccess = $user->firstaccess = $now;
+    }
+
+    // Store the previous current as lastlogin.
     $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
-    $USER->currentlogin = $user->lastaccess = $user->currentlogin = time();
 
-    $user->id = $USER->id;
+    $USER->currentlogin = $user->currentlogin = $now;
+
+    // Function user_accesstime_log() may not update immediately, better do it here.
+    $USER->lastaccess = $user->lastaccess = $now;
+    $USER->lastip = $user->lastip = getremoteaddr();
 
     $DB->update_record('user', $user);
     return true;
@@ -4093,10 +4106,6 @@ function authenticate_user_login($username, $password) {
                 $DB->set_field('user', 'auth', $auth, array('username'=>$username));
                 $user->auth = $auth;
             }
-            if (empty($user->firstaccess)) { //prevent firstaccess from remaining 0 for manual account that never required confirmation
-                $DB->set_field('user','firstaccess', $user->timemodified, array('id' => $user->id));
-                $user->firstaccess = $user->timemodified;
-            }
 
             update_internal_user_password($user, $password); // just in case salt or encoding were changed (magic quotes too one day)
 
@@ -4850,12 +4859,13 @@ function reset_course_userdata($data) {
         $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteblogassociations', 'blog'), 'error'=>false);
     }
 
-    if (!empty($data->reset_course_completion)) {
-        // Delete course completion information
+    if (!empty($data->reset_completion)) {
+        // Delete course and activity completion information.
         $course = $DB->get_record('course', array('id'=>$data->courseid));
         $cc = new completion_info($course);
-        $cc->delete_course_completion_data();
-        $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecoursecompletiondata', 'completion'), 'error'=>false);
+        $cc->delete_all_completion_data();
+        $status[] = array('component' => $componentstr,
+                'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
     }
 
     $componentstr = get_string('roles');
index 107911b..f62f8da 100644 (file)
@@ -578,6 +578,9 @@ class moodle_page {
         global $CFG;
         if (is_null($this->_blocks)) {
             if (!empty($CFG->blockmanagerclass)) {
+                if (!empty($CFG->blockmanagerclassfile)) {
+                    require_once($CFG->blockmanagerclassfile);
+                }
                 $classname = $CFG->blockmanagerclass;
             } else {
                 $classname = 'block_manager';
index 48a459f..8225f23 100644 (file)
@@ -240,11 +240,11 @@ EOD;
         }
 
         if (!isset($record['descriptionformat'])) {
-            $record['description'] = FORMAT_MOODLE;
+            $record['descriptionformat'] = FORMAT_MOODLE;
         }
 
         if (!isset($record['parent'])) {
-            $record['descriptionformat'] = 0;
+            $record['parent'] = 0;
         }
 
         if (empty($record['parent'])) {
@@ -310,12 +310,12 @@ EOD;
             $record['numsections'] = 5;
         }
 
-        if (!isset($record['description'])) {
-            $record['description'] = "Test course $i\n$this->loremipsum";
+        if (!isset($record['summary'])) {
+            $record['summary'] = "Test course $i\n$this->loremipsum";
         }
 
-        if (!isset($record['descriptionformat'])) {
-            $record['description'] = FORMAT_MOODLE;
+        if (!isset($record['summaryformat'])) {
+            $record['summaryformat'] = FORMAT_MOODLE;
         }
 
         if (!isset($record['category'])) {
index 3380549..b580259 100644 (file)
@@ -762,16 +762,20 @@ class phpunit_util {
      * Note: To be used from CLI scripts only.
      *
      * @static
+     * @param bool $displayprogress if true, this method will echo progress information.
      * @return void may terminate execution with exit code
      */
-    public static function drop_site() {
+    public static function drop_site($displayprogress = false) {
         global $DB, $CFG;
 
         if (!self::is_test_site()) {
             phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!');
         }
 
-        // purge dataroot
+        // Purge dataroot
+        if ($displayprogress) {
+            echo "Purging dataroot:\n";
+        }
         self::reset_dataroot();
         phpunit_bootstrap_initdataroot($CFG->dataroot);
         $keep = array('.', '..', 'lock', 'webrunner.xml');
@@ -795,9 +799,28 @@ class phpunit_util {
             unset($tables['config']);
             $tables['config'] = 'config';
         }
+
+        if ($displayprogress) {
+            echo "Dropping tables:\n";
+        }
+        $dotsonline = 0;
         foreach ($tables as $tablename) {
             $table = new xmldb_table($tablename);
             $DB->get_manager()->drop_table($table);
+
+            if ($dotsonline == 60) {
+                if ($displayprogress) {
+                    echo "\n";
+                }
+                $dotsonline = 0;
+            }
+            if ($displayprogress) {
+                echo '.';
+            }
+            $dotsonline += 1;
+        }
+        if ($displayprogress) {
+            echo "\n";
         }
     }
 
index eb5df6b..316ee70 100644 (file)
@@ -48,10 +48,22 @@ class core_phpunit_generator_testcase extends advanced_testcase {
         $count = $DB->count_records('course_categories');
         $category = $generator->create_category();
         $this->assertEquals($count+1, $DB->count_records('course_categories'));
+        $this->assertRegExp('/^Course category \d/', $category->name);
+        $this->assertSame('', $category->idnumber);
+        $this->assertRegExp('/^Test course category \d/', $category->description);
+        $this->assertSame(FORMAT_MOODLE, $category->descriptionformat);
 
         $count = $DB->count_records('course');
         $course = $generator->create_course();
         $this->assertEquals($count+1, $DB->count_records('course'));
+        $this->assertRegExp('/^Test course \d/', $course->fullname);
+        $this->assertRegExp('/^tc_\d/', $course->shortname);
+        $this->assertSame('', $course->idnumber);
+        $this->assertSame('topics', $course->format);
+        $this->assertEquals(0, $course->newsitems);
+        $this->assertEquals(5, $course->numsections);
+        $this->assertRegExp('/^Test course \d/', $course->summary);
+        $this->assertSame(FORMAT_MOODLE, $course->summaryformat);
 
         $section = $generator->create_course_section(array('course'=>$course->id, 'section'=>3));
         $this->assertEquals($course->id, $section->course);
index fcca051..61d2a6e 100644 (file)
@@ -30,8 +30,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once($CFG->libdir.'/filelib.php');  // curl class needed here
-
 /**
  * Singleton class providing general plugins management functionality
  */
@@ -100,6 +98,16 @@ class plugin_manager {
         global $CFG;
 
         if ($disablecache or is_null($this->pluginsinfo)) {
+            // Hack: include mod and editor subplugin management classes first,
+            //       the adminlib.php is supposed to contain extra admin settings too.
+            require_once($CFG->libdir.'/adminlib.php');
+            foreach(array('mod', 'editor') as $type) {
+                foreach (get_plugin_list($type) as $dir) {
+                    if (file_exists("$dir/adminlib.php")) {
+                        include_once("$dir/adminlib.php");
+                    }
+                }
+            }
             $this->pluginsinfo = array();
             $plugintypes = get_plugin_types();
             $plugintypes = $this->reorder_plugin_types($plugintypes);
@@ -148,10 +156,11 @@ class plugin_manager {
         if ($disablecache or is_null($this->subpluginsinfo)) {
             $this->subpluginsinfo = array();
             foreach (array('mod', 'editor') as $type) {
-                $owners = get_plugin_list('type');
+                $owners = get_plugin_list($type);
                 foreach ($owners as $component => $ownerdir) {
                     $componentsubplugins = array();
                     if (file_exists($ownerdir . '/db/subplugins.php')) {
+                        $subplugins = array();
                         include($ownerdir . '/db/subplugins.php');
                         foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
                             $subplugin = new stdClass();
@@ -785,6 +794,9 @@ class available_update_checker {
      * @throws available_update_checker_exception
      */
     protected function get_response() {
+        global $CFG;
+        require_once($CFG->libdir.'/filelib.php');
+
         $curl = new curl(array('proxy' => true));
         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
         $curlinfo = $curl->get_info();
@@ -961,6 +973,9 @@ class available_update_checker {
             return;
         }
 
+        $version = null;
+        $release = null;
+
         require($CFG->dirroot.'/version.php');
         $this->currentversion = $version;
         $this->currentrelease = $release;
index 6f73e0f..c26b81b 100644 (file)
@@ -791,6 +791,9 @@ moodle_setlocale();
 
 // Create the $PAGE global - this marks the PAGE and OUTPUT fully initialised, this MUST be done at the end of setup!
 if (!empty($CFG->moodlepageclass)) {
+    if (!empty($CFG->moodlepageclassfile)) {
+        require_once($CFG->moodlepageclassfile);
+    }
     $classname = $CFG->moodlepageclass;
 } else {
     $classname = 'moodle_page';
diff --git a/lib/tests/backup_test.php b/lib/tests/backup_test.php
deleted file mode 100644 (file)
index 1830f56..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-<?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/>.
-
-/**
- * Unit tests for backups.
- *
- * @package   core
- * @category  phpunit
- * @copyright 2012 Frédéric Massart <fred@moodle.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php');
-
-/**
- * Unit tests for backup system
- */
-class backup_testcase extends advanced_testcase {
-
-    public function test_next_automated_backup() {
-
-        $this->resetAfterTest();
-        $admin = get_admin();
-        $timezone = $admin->timezone;
-
-        // Notes
-        // - The next automated backup will never be on the same date than $now
-        // - backup_auto_weekdays starts on Sunday
-        // - Tests cannot be done in the past.
-
-        // Every Wed and Sat at 11pm.
-        set_config('backup_auto_active', '1', 'backup');
-        set_config('backup_auto_weekdays', '0010010', 'backup');
-        set_config('backup_auto_hour', '23', 'backup');
-        set_config('backup_auto_minute', '0', 'backup');
-
-        $now = strtotime('next Monday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Tuesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('5-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Wednesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('5-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Thursday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('5-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Friday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Saturday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Sunday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-23:00', date('w-H:i', $next));
-
-        // Every Sun and Sat at 12pm.
-        set_config('backup_auto_active', '1', 'backup');
-        set_config('backup_auto_weekdays', '1000001', 'backup');
-        set_config('backup_auto_hour', '0', 'backup');
-        set_config('backup_auto_minute', '0', 'backup');
-
-        $now = strtotime('next Monday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Tuesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Wednesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Thursday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Friday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Saturday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Sunday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        // Every Sun at 4am.
-        set_config('backup_auto_active', '1', 'backup');
-        set_config('backup_auto_weekdays', '1000000', 'backup');
-        set_config('backup_auto_hour', '4', 'backup');
-        set_config('backup_auto_minute', '0', 'backup');
-
-        $now = strtotime('next Monday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Tuesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Wednesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Thursday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Friday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Saturday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Sunday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        // Every day but Wed at 8:30pm.
-        set_config('backup_auto_active', '1', 'backup');
-        set_config('backup_auto_weekdays', '1110111', 'backup');
-        set_config('backup_auto_hour', '20', 'backup');
-        set_config('backup_auto_minute', '30', 'backup');
-
-        $now = strtotime('next Monday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Tuesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('4-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Wednesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('4-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Thursday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('5-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Friday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Saturday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Sunday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('1-20:30', date('w-H:i', $next));
-
-    }
-}
index 2d03ffc..dd9665a 100644 (file)
@@ -1549,9 +1549,9 @@ class moodlelib_testcase extends advanced_testcase {
                 'hour' => '10',
                 'minutes' => '00',
                 'seconds' => '00',
-                'timezone' => '0.0', //no dst offset
-                'applydst' => false,
-                'expectedoutput' => '1309528800'
+                'timezone' => '0.0',
+                'applydst' => false, //no dst offset
+                'expectedoutput' => '1309514400' // 6pm at UTC+0
             ),
             array(
                 'usertimezone' => 'America/Moncton',
index 36e54e4..34db5a0 100644 (file)
@@ -5,6 +5,7 @@ information provided here is intended especially for developers.
 
 * Pagelib: Numerous deprecated functions were removed as classes page_base, page_course
   and page_generic_activity.
+* use $CFG->googlemapkey3 instead of removed $CFG->googlemapkey and migrate to Google Maps API V3
 
 YUI changes:
 * moodle-enrol-notification has been renamed to moodle-core-notification
index 8384f37..1295b00 100644 (file)
@@ -2551,23 +2551,38 @@ function obfuscate_text($plaintext) {
  * @param string $email The email address to display
  * @param string $label The text to displayed as hyperlink to $email
  * @param boolean $dimmed If true then use css class 'dimmed' for hyperlink
+ * @param string $subject The subject of the email in the mailto link
+ * @param string $body The content of the email in the mailto link
  * @return string The obfuscated mailto link
  */
-function obfuscate_mailto($email, $label='', $dimmed=false) {
+function obfuscate_mailto($email, $label='', $dimmed=false, $subject = '', $body = '') {
 
     if (empty($label)) {
         $label = $email;
     }
+
+    $label = obfuscate_text($label);
+    $email = obfuscate_email($email);
+    $mailto = obfuscate_text('mailto');
+    $url = new moodle_url("mailto:$email");
+    $attrs = array();
+
+    if (!empty($subject)) {
+        $url->param('subject', format_string($subject));
+    }
+    if (!empty($body)) {
+        $url->param('body', format_string($body));
+    }
+
+    // Use the obfuscated mailto
+    $url = preg_replace('/^mailto/', $mailto, $url->out());
+
     if ($dimmed) {
-        $title = get_string('emaildisable');
-        $dimmed = ' class="dimmed"';
-    } else {
-        $title = '';
-        $dimmed = '';
+        $attrs['title'] = get_string('emaildisable');
+        $attrs['class'] = 'dimmed';
     }
-    return sprintf("<a href=\"%s:%s\" $dimmed title=\"$title\">%s</a>",
-                    obfuscate_text('mailto'), obfuscate_email($email),
-                    obfuscate_text($label));
+
+    return html_writer::link($url, $label, $attrs);
 }
 
 /**
index fa97208..06a8fdc 100644 (file)
@@ -55,7 +55,8 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
                                                   'duedate',
                                                   'allowsubmissionsfromdate',
                                                   'grade',
-                                                  'timemodified'));
+                                                  'timemodified',
+                                                  'completionsubmit'));
 
         $submissions = new backup_nested_element('submissions');
 
index c075dbd..213a62b 100644 (file)
@@ -21,7 +21,8 @@
         <FIELD NAME="allowsubmissionsfromdate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If set, submissions will only be accepted after this date." PREVIOUS="duedate" NEXT="grade"/>
         <FIELD NAME="grade" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The maximum grade for this assignment. Can be negative to indicate the use of a scale." PREVIOUS="allowsubmissionsfromdate" NEXT="timemodified"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time the settings for this assign module instance were last modified." PREVIOUS="grade" NEXT="requiresubmissionstatement"/>
-        <FIELD NAME="requiresubmissionstatement" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Forces the student to accept a submission statement when submitting an assignment" PREVIOUS="timemodified"/>
+        <FIELD NAME="requiresubmissionstatement" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Forces the student to accept a submission statement when submitting an assignment" PREVIOUS="timemodified" NEXT="completionsubmit"/>
+        <FIELD NAME="completionsubmit" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If this field is set to 1, then the activity will be automatically marked as 'complete' once the user submits their assignment." PREVIOUS="requiresubmissionstatement"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="The unique id for this assignment instance."/>
index e12e781..b80a60e 100644 (file)
@@ -65,6 +65,21 @@ function xmldb_assign_upgrade($oldversion) {
         upgrade_mod_savepoint(true, 2012071800, 'assign');
     }
 
+    if ($oldversion < 2012081600) {
+
+        // Define field sendlatenotifications to be added to assign.
+        $table = new xmldb_table('assign');
+        $field = new xmldb_field('completionsubmit', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'timemodified');
+
+        // Conditionally launch add field sendlatenotifications.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Assign savepoint reached.
+        upgrade_mod_savepoint(true, 2012081600, 'assign');
+    }
+
     return true;
 }
 
index be0eb3d..fb5e4ff 100644 (file)
@@ -287,13 +287,15 @@ class assign_grading_table extends table_sql implements renderable {
     }
 
     /**
-     * Format a user record for display (don't link to profile)
+     * Format a user record for display (link to profile)
      *
      * @param stdClass $row
      * @return string
      */
     function col_fullname($row) {
-        return fullname($row);
+        $courseid = $this->assignment->get_course()->id;
+        $link= new moodle_url('/user/view.php', array('id' =>$row->id, 'course'=>$courseid));
+        return $this->output->action_link($link, fullname($row));
     }
 
     /**
index cde25a4..b90c8ac 100644 (file)
@@ -56,8 +56,13 @@ foreach ($assignments as $assignment) {
     $cm = get_coursemodule_from_instance('assign', $assignment->id, 0, false, MUST_EXIST);
 
     $link = html_writer::link(new moodle_url('/mod/assign/view.php', array('id' => $cm->id)), $assignment->name);
-    $date = userdate($assignment->duedate);
-    $submissions = $DB->count_records('assign_submission', array('assignment'=>$cm->instance));
+    $date = '-';
+    if (!empty($assignment->duedate)) {
+        $date = userdate($assignment->duedate);
+    }
+
+    $params = array('assignment'=>$cm->instance, 'status'=>ASSIGN_SUBMISSION_STATUS_SUBMITTED);
+    $submissions = $DB->count_records('assign_submission', $params);
     $row = array($link, $date, $submissions);
     $table->data[] = $row;
 
index 9ab4132..3b8a6f9 100644 (file)
@@ -66,6 +66,7 @@ $string['batchoperationlock'] = 'lock submissions';
 $string['batchoperationunlock'] = 'unlock submissions';
 $string['batchoperationreverttodraft'] = 'revert submissions to draft';
 $string['comment'] = 'Comment';
+$string['completionsubmit'] = 'Student must submit to this activity to complete it';
 $string['conversionexception'] = 'Could not convert assignment. Exception was: {$a}.';
 $string['configshowrecentsubmissions'] = 'Everyone can see notifications of submissions in recent activity reports.';
 $string['confirmsubmission'] = 'Are you sure you want to submit your work for grading? You will not be able to make any more changes';
index 1c98958..18a1b11 100644 (file)
@@ -83,6 +83,7 @@ function assign_supports($feature) {
         case FEATURE_GROUPMEMBERSONLY:        return true;
         case FEATURE_MOD_INTRO:               return true;
         case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
+        case FEATURE_COMPLETION_HAS_RULES:    return true;
         case FEATURE_GRADE_HAS_GRADE:         return true;
         case FEATURE_GRADE_OUTCOMES:          return true;
         case FEATURE_BACKUP_MOODLE2:          return true;
@@ -937,3 +938,29 @@ function assign_user_outline($course, $user, $coursemodule, $assignment) {
 
     return $result;
 }
+
+/**
+ * Obtains the automatic completion state for this module based on any conditions
+ * in assign settings.
+ *
+ * @param object $course Course
+ * @param object $cm Course-module
+ * @param int $userid User ID
+ * @param bool $type Type of comparison (or/and; can be used as return value if no conditions)
+ * @return bool True if completed, false if not, $type if conditions not set.
+ */
+function assign_get_completion_state($course, $cm, $userid, $type) {
+    global $CFG,$DB;
+    require_once($CFG->dirroot . '/mod/assign/locallib.php');
+
+    $assign = new assign(null, $cm, $course);
+
+    // If completion option is enabled, evaluate it and return true/false.
+    if ($assign->get_instance()->completionsubmit) {
+        $submission = $DB->get_record('assign_submission', array('assignment'=>$assign->get_instance()->id, 'userid'=>$userid), '*', IGNORE_MISSING);
+        return $submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+    } else {
+        // Completion option is not enabled so just return $type.
+        return $type;
+    }
+}
index 94d1871..60a3f92 100644 (file)
@@ -417,6 +417,7 @@ class assign {
         $update->duedate = $formdata->duedate;
         $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
         $update->grade = $formdata->grade;
+        $update->completionsubmit = $formdata->completionsubmit;
         $returnid = $DB->insert_record('assign', $update);
         $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
         // cache the course record
@@ -636,6 +637,7 @@ class assign {
         $update->duedate = $formdata->duedate;
         $update->allowsubmissionsfromdate = $formdata->allowsubmissionsfromdate;
         $update->grade = $formdata->grade;
+        $update->completionsubmit = $formdata->completionsubmit;
 
         $result = $DB->update_record('assign', $update);
         $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
@@ -2561,6 +2563,11 @@ class assign {
 
                 $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
                 $this->update_submission($submission);
+                $completion = new completion_info($this->get_course());
+                if ($completion->is_enabled($this->get_course_module()) && $this->get_instance()->completionsubmit) {
+                    $completion->update_state($this->get_course_module(), COMPLETION_COMPLETE, $USER->id);
+                }
+
                 if (isset($data->submissionstatement)) {
                     $this->add_to_log('submission statement accepted', get_string('submissionstatementacceptedlog', 'mod_assign', fullname($USER)));
                 }
@@ -2838,6 +2845,15 @@ class assign {
             }
             $this->add_to_log('submit', $this->format_submission_for_log($submission));
 
+            $complete = COMPLETION_INCOMPLETE;
+            if ($submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED) {
+                $complete = COMPLETION_COMPLETE;
+            }
+            $completion = new completion_info($this->get_course());
+            if ($completion->is_enabled($this->get_course_module()) && $this->get_instance()->completionsubmit) {
+                $completion->update_state($this->get_course_module(), $complete, $USER->id);
+            }
+
             if (!$this->get_instance()->submissiondrafts) {
                 $this->notify_student_submission_receipt($submission);
                 $this->notify_graders($submission);
@@ -3140,7 +3156,7 @@ class assign {
      * @return void
      */
     private function process_revert_to_draft($userid = 0) {
-        global $USER, $DB;
+        global $DB;
 
         // Need grade permission
         require_capability('mod/assign:grade', $this->context);
@@ -3163,6 +3179,10 @@ class assign {
 
         $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
 
+        $completion = new completion_info($this->get_course());
+        if ($completion->is_enabled($this->get_course_module()) && $this->get_instance()->completionsubmit) {
+            $completion->update_state($this->get_course_module(), COMPLETION_INCOMPLETE, $userid);
+        }
         $this->add_to_log('revert submission to draft', get_string('reverttodraftforstudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user))));
 
     }
index 89d0159..1465075 100644 (file)
@@ -161,5 +161,15 @@ class mod_assign_mod_form extends moodleform_mod {
         $assignment->plugin_data_preprocessing($defaultvalues);
     }
 
+    function add_completion_rules() {
+        $mform =& $this->_form;
+
+        $mform->addElement('checkbox', 'completionsubmit', '', get_string('completionsubmit', 'assign'));
+        return array('completionsubmit');
+    }
+
+    function completion_rule_enabled($data) {
+        return !empty($data['completionsubmit']);
+    }
 
 }
index 99ef647..b9a29a3 100644 (file)
@@ -25,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $module->component = 'mod_assign'; // Full name of the plugin (used for diagnostics)
-$module->version  = 2012071800;    // The current module version (Date: YYYYMMDDXX)
+$module->version  = 2012081600;    // The current module version (Date: YYYYMMDDXX)
 $module->requires = 2012061700;    // Requires this Moodle version
 $module->cron     = 60;
 
index da1b75f..d9ee50e 100644 (file)
@@ -82,6 +82,9 @@ class restore_book_activity_task extends restore_activity_task {
         $rules[] = new restore_decode_rule('BOOKVIEWBYB', '/mod/book/view.php?b=$1', 'book');
         $rules[] = new restore_decode_rule('BOOKVIEWBYBCH', '/mod/book/view.php?b=$1&amp;chapterid=$2', array('book', 'book_chapter'));
 
+        // Convert old book links MDL-33362\r
+        $rules[] = new restore_decode_rule('BOOKSTART', '/mod/book/view.php?id=$1', 'course_module');
+
         return $rules;
     }
 
index cc4ad8f..edbbd9d 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die;
 
 $module->component = 'mod_book'; // Full name of the plugin (used for diagnostics)
-$module->version   = 2012061700; // The current module version (Date: YYYYMMDDXX)
+$module->version   = 2012081600; // The current module version (Date: YYYYMMDDXX)
 $module->requires  = 2012061700; // Requires this Moodle version
 $module->cron      = 0;          // Period for cron to check this module (secs)
index f5e52ae..9b56981 100644 (file)
@@ -767,16 +767,15 @@ function chat_format_message_manually($message, $courseid, $sender, $currentuser
     }
 
     // It's not a system event
-
-    $text = $message->message;
+    $text = trim($message->message);
 
     /// Parse the text to clean and filter it
-
     $options = new stdClass();
     $options->para = false;
     $text = format_text($text, FORMAT_MOODLE, $options, $courseid);
 
     // And now check for special cases
+    $patternTo = '#^\s*To\s([^:]+):(.*)#';
     $special = false;
 
     if (substr($text, 0, 5) == 'beep ') {
@@ -799,23 +798,32 @@ function chat_format_message_manually($message, $courseid, $sender, $currentuser
             return false;
         }
     } else if (substr($text, 0, 1) == '/') {     /// It's a user command
-        // support some IRC commands
+        $special = true;
         $pattern = '#(^\/)(\w+).*#';
-        preg_match($pattern, trim($text), $matches);
-        $command = $matches[2];
+        preg_match($pattern, $text, $matches);
+        $command = isset($matches[2]) ? $matches[2] : false;
+        // Support some IRC commands.
         switch ($command){
-        case 'me':
-            $special = true;
-            $outinfo = $message->strtime;
-            $outmain = '*** <b>'.$sender->firstname.' '.substr($text, 4).'</b>';
-            break;
+            case 'me':
+                $outinfo = $message->strtime;
+                $outmain = '*** <b>'.$sender->firstname.' '.substr($text, 4).'</b>';
+                break;
+            default:
+                // Error, we set special back to false to use the classic message output.
+                $special = false;
+                break;
         }
-    } elseif (substr($text, 0, 2) == 'To') {
-        $pattern = '#To[[:space:]](.*):(.*)#';
-        preg_match($pattern, trim($text), $matches);
+    } else if (preg_match($patternTo, $text)) {
         $special = true;
-        $outinfo = $message->strtime;
-        $outmain = $sender->firstname.' '.get_string('saidto', 'chat').' <i>'.$matches[1].'</i>: '.$matches[2];
+        $matches = array();
+        preg_match($patternTo, $text, $matches);
+        if (isset($matches[1]) && isset($matches[2])) {
+            $outinfo = $message->strtime;
+            $outmain = $sender->firstname.' '.get_string('saidto', 'chat').' <i>'.$matches[1].'</i>: '.$matches[2];
+        } else {
+            // Error, we set special back to false to use the classic message output.
+            $special = false;
+        }
     }
 
     if(!$special) {
@@ -924,7 +932,7 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     }
 
     // It's not a system event
-    $text = $message->message;
+    $text = trim($message->message);
 
     /// Parse the text to clean and filter it
     $options = new stdClass();
@@ -935,8 +943,9 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     $special = false;
     $outtime = $message->strtime;
 
-    //Initilise output variable.
+    // Initialise variables.
     $outmain = '';
+    $patternTo = '#^\s*To\s([^:]+):(.*)#';
 
     if (substr($text, 0, 5) == 'beep ') {
         $special = true;
@@ -964,26 +973,33 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     } else if (substr($text, 0, 1) == '/') {     /// It's a user command
         $special = true;
         $result->type = 'command';
-        // support some IRC commands
         $pattern = '#(^\/)(\w+).*#';
-        preg_match($pattern, trim($text), $matches);
-        $command = $matches[2];
-        $special = true;
+        preg_match($pattern, $text, $matches);
+        $command = isset($matches[2]) ? $matches[2] : false;
+        // Support some IRC commands.
         switch ($command){
-        case 'me':
-            $outmain = '*** <b>'.$sender->firstname.' '.substr($text, 4).'</b>';
-            break;
+            case 'me':
+                $outmain = '*** <b>'.$sender->firstname.' '.substr($text, 4).'</b>';
+                break;
+            default:
+                // Error, we set special back to false to use the classic message output.
+                $special = false;
+                break;
         }
-    } elseif (substr($text, 0, 2) == 'To') {
+    } else if (preg_match($patternTo, $text)) {
         $special = true;
         $result->type = 'dialogue';
-        $pattern = '#To[[:space:]](.*):(.*)#';
-        preg_match($pattern, trim($text), $matches);
-        $special = true;
-        $outmain = $sender->firstname.' <b>'.get_string('saidto', 'chat').'</b> <i>'.$matches[1].'</i>: '.$matches[2];
+        $matches = array();
+        preg_match($patternTo, $text, $matches);
+        if (isset($matches[1]) && isset($matches[2])) {
+            $outmain = $sender->firstname.' <b>'.get_string('saidto', 'chat').'</b> <i>'.$matches[1].'</i>: '.$matches[2];
+        } else {
+            // Error, we set special back to false to use the classic message output.
+            $special = false;
+        }
     }
 
-    if(!$special) {
+    if (!$special) {
         $outmain = $text;
     }
 
@@ -1008,7 +1024,6 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     }
 }
 
-
 /**
  * @global object $DB
  * @global object $CFG
index 7236bd0..405bc95 100644 (file)
@@ -2092,8 +2092,7 @@ function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=5
                          u.lastname,
                          u.email,
                          u.picture,
-                         u.imagealt,
-                         u.email
+                         u.imagealt
                     FROM $fromsql
                    WHERE $selectsql
                 ORDER BY p.modified DESC";
index 31daa38..89f4eda 100644 (file)
@@ -345,7 +345,7 @@ function lesson_get_user_grades($lesson, $userid=0) {
 
     $params = array("lessonid" => $lesson->id,"lessonid2" => $lesson->id);
 
-    if (isset($userid)) {
+    if (!empty($userid)) {
         $params["userid"] = $userid;
         $params["userid2"] = $userid;
         $user = "AND u.id = :userid";
index ed6ada4..a5a0c09 100644 (file)
@@ -63,7 +63,7 @@ class quiz_responses_table extends quiz_attempts_report_table {
     }
 
     public function col_sumgrades($attempt) {
-        if ($attempt->state == quiz_attempt::FINISHED) {
+        if ($attempt->state != quiz_attempt::FINISHED) {
             return '-';
         }
 
index 72411c8..c5b2335 100644 (file)
@@ -2,6 +2,13 @@ This files describes API changes in /mod/* - activity modules,
 information provided here is intended especially for developers.
 
 
+=== 2.4 ===
+
+new features:
+
+* mod/xxx/adminlib.php may now include 'plugininfo_yoursubplugintype' class definition
+  used by plugin_manager; it is recommended to store extra admin settings classes in this file
+
 === 2.3 ===
 
 required changes in code:
index e880493..4865a7c 100644 (file)
@@ -120,7 +120,8 @@ class MoodleQuickForm_wikieditor extends MoodleQuickForm_textarea {
             $html .= html_writer::empty_tag('img', array('alt' => $button[1], 'src' => $CFG->wwwroot . '/mod/wiki/editors/wiki/images/' . $button[0]));
             $html .= "</a>";
         }
-        $html .= "<select onchange=\"insertTags('{$imagetag[0]}', '{$imagetag[1]}', this.value)\">";
+        $html .= "<label class='accesshide' for='addtags'>" . get_string('insertimage', 'wiki')  . "</label>";
+        $html .= "<select id='addtags' onchange=\"insertTags('{$imagetag[0]}', '{$imagetag[1]}', this.value)\">";
         $html .= "<option value='" . s(get_string('wikiimage', 'wiki')) . "'>" . get_string('insertimage', 'wiki') . '</option>';
         foreach ($this->files as $filename) {
             $html .= "<option value='".s($filename)."'>";
index 8b9e955..f5d6e3f 100644 (file)
@@ -474,7 +474,8 @@ function wiki_search_form($cm, $search = '') {
     $output = '<div class="wikisearch">';
     $output .= '<form method="post" action="' . $CFG->wwwroot . '/mod/wiki/search.php" style="display:inline">';
     $output .= '<fieldset class="invisiblefieldset">';
-    $output .= '<input name="searchstring" type="text" size="18" value="' . s($search, true) . '" alt="search" />';
+    $output .= '<label class="accesshide" for="searchwiki">' . get_string("searchwikis", "wiki") . '</label>';
+    $output .= '<input id="searchwiki" name="searchstring" type="text" size="18" value="' . s($search, true) . '" alt="search" />';
     $output .= '<input name="courseid" type="hidden" value="' . $cm->course . '" />';
     $output .= '<input name="cmid" type="hidden" value="' . $cm->id . '" />';
     $output .= '<input name="searchwikicontent" type="hidden" value="1" />';
index ef5b269..fd1196e 100644 (file)
@@ -65,8 +65,6 @@ class mod_wiki_mod_form extends moodleform_mod {
         $attr = array('size' => '20');
         if (!empty($this->_instance)) {
             $attr['disabled'] = 'disabled';
-        } else {
-            $attr['value'] = get_string('firstpagetitle', 'wiki');
         }
 
         $mform->addElement('text', 'firstpagetitle', get_string('firstpagetitle', 'wiki'), $attr);
index b779de6..f28b9c2 100644 (file)
@@ -1642,12 +1642,8 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $requirec
         $pagevars['qpage'] = 0;
     }
 
-    $pagevars['qperpage'] = optional_param('qperpage', -1, PARAM_INT);
-    if ($pagevars['qperpage'] > -1) {
-        $thispageurl->param('qperpage', $pagevars['qperpage']);
-    } else {
-        $pagevars['qperpage'] = DEFAULT_QUESTIONS_PER_PAGE;
-    }
+    $pagevars['qperpage'] = question_get_display_preference(
+            'qperpage', DEFAULT_QUESTIONS_PER_PAGE, PARAM_INT, $thispageurl);
 
     for ($i = 1; $i <= question_bank_view::MAX_SORTS; $i++) {
         $param = 'qbs' . $i;
@@ -1675,28 +1671,12 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $requirec
         $pagevars['cat'] = "$category->id,$category->contextid";
     }
 
-    if(($recurse = optional_param('recurse', -1, PARAM_BOOL)) != -1) {
-        $pagevars['recurse'] = $recurse;
-        $thispageurl->param('recurse', $recurse);
-    } else {
-        $pagevars['recurse'] = 1;
-    }
+    // Display options.
+    $pagevars['recurse']    = question_get_display_preference('recurse',    1, PARAM_BOOL, $thispageurl);
+    $pagevars['showhidden'] = question_get_display_preference('showhidden', 0, PARAM_BOOL, $thispageurl);
+    $pagevars['qbshowtext'] = question_get_display_preference('qbshowtext', 0, PARAM_BOOL, $thispageurl);
 
-    if(($showhidden = optional_param('showhidden', -1, PARAM_BOOL)) != -1) {
-        $pagevars['showhidden'] = $showhidden;
-        $thispageurl->param('showhidden', $showhidden);
-    } else {
-        $pagevars['showhidden'] = 0;
-    }
-
-    if(($showquestiontext = optional_param('qbshowtext', -1, PARAM_BOOL)) != -1) {
-        $pagevars['qbshowtext'] = $showquestiontext;
-        $thispageurl->param('qbshowtext', $showquestiontext);
-    } else {
-        $pagevars['qbshowtext'] = 0;
-    }
-
-    //category list page
+    // Category list page.
     $pagevars['cpage'] = optional_param('cpage', 1, PARAM_INT);
     if ($pagevars['cpage'] != 1){
         $thispageurl->param('cpage', $pagevars['cpage']);
@@ -1705,6 +1685,32 @@ function question_edit_setup($edittab, $baseurl, $requirecmid = false, $requirec
     return array($thispageurl, $contexts, $cmid, $cm, $module, $pagevars);
 }
 
+/**
+ * Get a particular question preference that is also stored as a user preference.
+ * If the the value is given in the GET/POST request, then that value is used,
+ * and the user preference is updated to that value. Otherwise, the last set
+ * value of the user preference is used, or if it has never been set the default
+ * passed to this function.
+ *
+ * @param string $param the param name. The URL parameter set, and the GET/POST
+ *      parameter read. The user_preference name is 'question_bank_' . $param.
+ * @param mixed $default The default value to use, if not otherwise set.
+ * @param int $type one of the PARAM_... constants.
+ * @param moodle_url $thispageurl if the value has been explicitly set, we add
+ *      it to this URL.
+ * @return mixed the parameter value to use.
+ */
+function question_get_display_preference($param, $default, $type, $thispageurl) {
+    $submittedvalue = optional_param($param, null, $type);
+    if (is_null($submittedvalue)) {
+        return get_user_preferences('question_bank_' . $param, $default);
+    }
+
+    set_user_preference('question_bank_' . $param, $submittedvalue);
+    $thispageurl->param($param, $submittedvalue);
+    return $submittedvalue;
+}
+
 /**
  * Make sure user is logged in as required in this context.
  */
index 2864f04..29277ee 100644 (file)
@@ -398,6 +398,17 @@ abstract class question_bank {
         self::ensure_fraction_options_initialised();
         return self::$fractionoptionsfull;
     }
+
+    /**
+     * Perform scheduled maintenance tasks relating to the question bank.
+     */
+    public static function cron() {
+        global $CFG;
+
+        // Delete any old question preview that got left in the database.
+        require_once($CFG->dirroot . '/question/previewlib.php');
+        question_preview_cron();
+    }
 }
 
 
index 667b78e..b79fe5e 100644 (file)
@@ -758,6 +758,14 @@ ORDER BY
      * @param qubaid_condition $qubaids identifies which question useages to delete.
      */
     protected function delete_usage_records_for_mysql(qubaid_condition $qubaids) {
+        $qubaidtest = $qubaids->usage_id_in();
+        if (strpos($qubaidtest, 'question_usages') !== false &&
+                strpos($qubaidtest, 'IN (SELECT') === 0) {
+            // This horrible hack is required by MDL-29847. It comes from
+            // http://www.xaprb.com/blog/2006/06/23/how-to-select-from-an-update-target-in-mysql/
+            $qubaidtest = 'IN (SELECT * FROM ' . substr($qubaidtest, 3) . ' AS hack_subquery_alias)';
+        }
+
         // TODO once MDL-29589 is fixed, eliminate this method, and instead use the new $DB API.
         $this->db->execute('
                 DELETE qu, qa, qas, qasd
@@ -765,7 +773,7 @@ ORDER BY
                   JOIN {question_attempts}          qa   ON qa.questionusageid = qu.id
              LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
              LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
-                 WHERE qu.id ' . $qubaids->usage_id_in(),
+                 WHERE qu.id ' . $qubaidtest,
                 $qubaids->usage_id_in_params());
     }
 
index 9b0bf37..e1cc7db 100644 (file)
@@ -898,3 +898,57 @@ class qformat_default {
                 $question->questiontextformat, $formatoptions), 0, false);
     }
 }
+
+class qformat_based_on_xml extends qformat_default {
+
+    /**
+     * Return the array moodle is expecting
+     * for an HTML text. No processing is done on $text.
+     * qformat classes that want to process $text
+     * for instance to import external images files
+     * and recode urls in $text must overwrite this method.
+     * @param array $text some HTML text string
+     * @return array with keys text, format and files.
+     */
+    public function text_field($text) {
+        return array(
+            'text' => trim($text),
+            'format' => FORMAT_HTML,
+            'files' => array(),
+        );
+    }
+
+    /**
+     * Return the value of a node, given a path to the node
+     * if it doesn't exist return the default value.
+     * @param array xml data to read
+     * @param array path path to node expressed as array
+     * @param mixed default
+     * @param bool istext process as text
+     * @param string error if set value must exist, return false and issue message if not
+     * @return mixed value
+     */
+    public function getpath($xml, $path, $default, $istext=false, $error='') {
+        foreach ($path as $index) {
+            if (!isset($xml[$index])) {
+                if (!empty($error)) {
+                    $this->error($error);
+                    return false;
+                } else {
+                    return $default;
+                }
+            }
+
+            $xml = $xml[$index];
+        }
+
+        if ($istext) {
+            if (!is_string($xml)) {
+                $this->error(get_string('invalidxml', 'qformat_xml'));
+            }
+            $xml = trim($xml);
+        }
+
+        return $xml;
+    }
+}
index 9ce23d0..1626489 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Blackboard question importer.
  *
- * @package    qformat
- * @subpackage blackboard
+ * @package qformat_blackboard
  * @copyright  2003 Scott Elliott
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -26,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once ($CFG->libdir . '/xmlize.php');
+require_once($CFG->libdir . '/xmlize.php');
 
 
 /**
@@ -35,19 +34,67 @@ require_once ($CFG->libdir . '/xmlize.php');
  * @copyright  2003 Scott Elliott
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class qformat_blackboard extends qformat_default {
+class qformat_blackboard extends qformat_based_on_xml {
+    // Is the current question's question text escaped HTML (true for most if not all Blackboard files).
+    public $ishtml = true;
+
 
     public function provide_import() {
         return true;
     }
 
-    function readquestions($lines) {
-        /// Parses an array of lines into an array of questions,
-        /// where each item is a question object as defined by
-        /// readquestion().
+    public function mime_type() {
+        return mimeinfo('type', '.dat');
+    }
+
+    /**
+     * Some softwares put entities in exported files.
+     * This method try to clean up known problems.
+     * @param string str string to correct
+     * @return string the corrected string
+     */
+    public function cleaninput($str) {
+        if (!$this->ishtml) {
+            return $str;
+        }
+        $html_code_list = array(
+            "&#039;" => "'",
+            "&#8217;" => "'",
+            "&#091;" => "[",
+            "&#8220;" => "\"",
+            "&#8221;" => "\"",
+            "&#093;" => "]",
+            "&#039;" => "'",
+            "&#8211;" => "-",
+            "&#8212;" => "-",
+        );
+        $str = strtr($str, $html_code_list);
+        // Use textlib entities_to_utf8 function to convert only numerical entities.
+        $str = textlib::entities_to_utf8($str, false);
+        return $str;
+    }
 
-        $text = implode($lines, " ");
-        $xml = xmlize($text, 0);
+    /**
+     * Parse the array of lines into an array of questions
+     * this *could* burn memory - but it won't happen that much
+     * so fingers crossed!
+     * @param array of lines from the input file.
+     * @param stdClass $context
+     * @return array (of objects) question objects.
+     */
+    protected function readquestions($lines) {
+
+        $text = implode($lines, ' ');
+        unset($lines);
+
+        // This converts xml to big nasty data structure,
+        // the 0 means keep white space as it is.
+        try {
+            $xml = xmlize($text, 0, 'UTF-8', true);
+        } catch (xml_format_exception $e) {
+            $this->error($e->getMessage(), '');
+            return false;
+        }
 
         $questions = array();
 
@@ -61,341 +108,405 @@ class qformat_blackboard extends qformat_default {
         return $questions;
     }
 
-//----------------------------------------
-// Process Essay Questions
-//----------------------------------------
-    function process_essay($xml, &$questions ) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_ESSAY"])) {
-            $essayquestions = $xml["POOL"]["#"]["QUESTION_ESSAY"];
+    /**
+     * Do question import processing common to every qtype.
+     * @param array $questiondata the xml tree related to the current question
+     * @return object initialized question object.
+     */
+    public function process_common($questiondata) {
+        global $CFG;
+
+        // This routine initialises the question object.
+        $question = $this->defaultquestion();
+
+        // Determine if the question is already escaped html.
+        $this->ishtml = $this->getpath($questiondata,
+                array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'),
+                false, false);
+
+        // Put questiontext in question object.
+        $text = $this->getpath($questiondata,
+                array('#', 'BODY', 0, '#', 'TEXT', 0, '#'),
+                '', true, get_string('importnotext', 'qformat_blackboard'));
+
+        if ($this->ishtml) {
+            $question->questiontext = $this->cleaninput($text);
+            $question->questiontextformat = FORMAT_HTML;
+            $question->questiontextfiles = array();
+
+        } else {
+            $question->questiontext = $text;
         }
-        else {
-            return;
+        // Put name in question object. We must ensure it is not empty and it is less than 250 chars.
+        $question->name = shorten_text(strip_tags($question->questiontext), 200);
+        $question->name = substr($question->name, 0, 250);
+        if (!$question->name) {
+            $id = $this->getpath($questiondata,
+                    array('@', 'id'), '',  true);
+            $question->name = get_string('defaultname', 'qformat_blackboard' , $id);
         }
 
-        foreach ($essayquestions as $essayquestion) {
+        $question->generalfeedback = '';
+        $question->generalfeedbackformat = FORMAT_HTML;
+        $question->generalfeedbackfiles = array();
 
-            $question = $this->defaultquestion();
+        // TODO : read the mark from the POOL TITLE QUESTIONLIST section.
+        $question->defaultmark = 1;
+        return $question;
+    }
+
+    /**
+     * Process Essay Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_essay($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) {
+            $essayquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_ESSAY'), false, false);
+        } else {
+            return;
+        }
 
-            $question->qtype = ESSAY;
+        foreach ($essayquestions as $thisquestion) {
 
-            // determine if the question is already escaped html
-            $ishtml = $essayquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
+            $question = $this->process_common($thisquestion);
 
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($essayquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]));
-            }
+            $question->qtype = 'essay';
 
-            // put name in question object
-            $question->name = substr($question->questiontext, 0, 254);
             $question->answer = '';
+            $answer = $this->getpath($thisquestion,
+                    array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true);
+            $question->graderinfo =  $this->text_field($this->cleaninput($answer));
             $question->feedback = '';
+            $question->responseformat = 'editor';
+            $question->responsefieldlines = 15;
+            $question->attachments = 0;
             $question->fraction = 0;
 
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process True / False Questions
-    //----------------------------------------
-    function process_tf($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_TRUEFALSE"])) {
-            $tfquestions = $xml["POOL"]["#"]["QUESTION_TRUEFALSE"];
-        }
-        else {
+    /**
+     * Process True / False Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_tf($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) {
+            $tfquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($tfquestions); $i++) {
-
-            $question = $this->defaultquestion();
+        foreach ($tfquestions as $thisquestion) {
 
-            $question->qtype = TRUEFALSE;
-            $question->single = 1; // Only one answer is allowed
+            $question = $this->process_common($thisquestion);
 
-            $thisquestion = $tfquestions[$i];
+            $question->qtype = 'truefalse';
+            $question->single = 1; // Only one answer is allowed.
 
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false);
 
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-            // put name in question object
-            $question->name = shorten_text($question->questiontext, 254);
+            $correct_answer = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
+                    '', true);
 
-            $choices = $thisquestion["#"]["ANSWER"];
-
-            $correct_answer = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"][0]["@"]["answer_id"];
-
-            // first choice is true, second is false.
-            $id = $choices[0]["@"]["id"];
-
-            if (strcmp($id, $correct_answer) == 0) {  // true is correct
+            // First choice is true, second is false.
+            $id = $this->getpath($choices[0], array('@', 'id'), '', true);
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            if (strcmp($id,  $correct_answer) == 0) {  // True is correct.
                 $question->answer = 1;
-                $question->feedbacktrue = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
-                $question->feedbackfalse = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-            } else {  // false is correct
+                $question->feedbacktrue = $this->text_field($this->cleaninput($correctfeedback));
+                $question->feedbackfalse = $this->text_field($this->cleaninput($incorrectfeedback));
+            } else {  // False is correct.
                 $question->answer = 0;
-                $question->feedbacktrue = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-                $question->feedbackfalse = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
+                $question->feedbacktrue = $this->text_field($this->cleaninput($incorrectfeedback));
+                $question->feedbackfalse = $this->text_field($this->cleaninput($correctfeedback));
             }
             $question->correctanswer = $question->answer;
             $questions[] = $question;
-          }
+        }
     }
 
-    //----------------------------------------
-    // Process Multiple Choice Questions
-    //----------------------------------------
-    function process_mc($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MULTIPLECHOICE"])) {
-            $mcquestions = $xml["POOL"]["#"]["QUESTION_MULTIPLECHOICE"];
-        }
-        else {
+    /**
+     * Process Multiple Choice Questions with single answer
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_mc($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) {
+            $mcquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($mcquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MULTICHOICE;
-            $question->single = 1; // Only one answer is allowed
-
-            $thisquestion = $mcquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-
-            // put name of question in question object, careful of length
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $choices = $thisquestion["#"]["ANSWER"];
-            for ($j = 0; $j < sizeof ($choices); $j++) {
-
-                $choice = trim($choices[$j]["#"]["TEXT"][0]["#"]);
-                // put this choice in the question object.
-                if ($ishtml) {
-                    $question->answer[$j] = html_entity_decode($choice,ENT_QUOTES,'UTF-8');
-                }
-                $question->answer[$j] = $question->answer[$j];
-
-                $id = $choices[$j]["@"]["id"];
-                $correct_answer_id = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"][0]["@"]["answer_id"];
-                // if choice is the answer, give 100%, otherwise give 0%
-                if (strcmp ($id, $correct_answer_id) == 0) {
-                    $question->fraction[$j] = 1;
-                    if ($ishtml) {
-                        $question->feedback[$j] = html_entity_decode(trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]),ENT_QUOTES,'UTF-8');
-                    }
-                    $question->feedback[$j] = $question->feedback[$j];
+        foreach ($mcquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            $question->partiallycorrectfeedback = $this->text_field('');
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $question->qtype = 'multichoice';
+            $question->single = 1; // Only one answer is allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            $correct_answer_id = $this->getpath($thisquestion,
+                        array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
+                        '', true);
+            foreach ($choices as $choice) {
+                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
+                // Put this choice in the question object.
+                $question->answer[] =  $this->text_field($this->cleaninput($choicetext));
+
+                $choice_id = $this->getpath($choice, array('@', 'id'), '', true);
+                // If choice is the right answer, give 100% mark, otherwise give 0%.
+                if (strcmp ($choice_id, $correct_answer_id) == 0) {
+                    $question->fraction[] = 1;
                 } else {
-                    $question->fraction[$j] = 0;
-                    if ($ishtml) {
-                        $question->feedback[$j] = html_entity_decode(trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]),ENT_QUOTES,'UTF-8');
-                    }
-                    $question->feedback[$j] = $question->feedback[$j];
+                    $question->fraction[] = 0;
                 }
+                // There is never feedback specific to each choice.
+                $question->feedback[] =  $this->text_field('');
             }
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Multiple Choice Questions With Multiple Answers
-    //----------------------------------------
-    function process_ma($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MULTIPLEANSWER"])) {
-            $maquestions = $xml["POOL"]["#"]["QUESTION_MULTIPLEANSWER"];
-        }
-        else {
+    /**
+     * Process Multiple Choice Questions With Multiple Answers
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_ma($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) {
+            $maquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($maquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MULTICHOICE;
+        foreach ($maquestions as $thisquestion) {
+            $question = $this->process_common($thisquestion);
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            // As there is no partially correct feedback we use incorrect one.
+            $question->partiallycorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $question->qtype = 'multichoice';
             $question->defaultmark = 1;
-            $question->single = 0; // More than one answers allowed
-            $question->image = ""; // No images with this format
-
-            $thisquestion = $maquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
+            $question->single = 0; // More than one answers allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            $correct_answer_ids = array();
+            foreach ($this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) {
+                if ($correctanswer) {
+                    $correct_answer_ids[] = $this->getpath($correctanswer,
+                            array('@', 'answer_id'),
+                            '', true);
+                }
             }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
+            $fraction = 1/count($correct_answer_ids);
 
-            $choices = $thisquestion["#"]["ANSWER"];
-            $correctanswers = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"];
+            foreach ($choices as $choice) {
+                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
+                // Put this choice in the question object.
+                $question->answer[] =  $this->text_field($this->cleaninput($choicetext));
 
-            for ($j = 0; $j < sizeof ($choices); $j++) {
+                $choice_id = $this->getpath($choice, array('@', 'id'), '', true);
 
-                $choice = trim($choices[$j]["#"]["TEXT"][0]["#"]);
-                // put this choice in the question object.
-                $question->answer[$j] = $choice;
+                $iscorrect = in_array($choice_id, $correct_answer_ids);
 
-                $correctanswercount = sizeof($correctanswers);
-                $id = $choices[$j]["@"]["id"];
-                $iscorrect = 0;
-                for ($k = 0; $k < $correctanswercount; $k++) {
-
-                    $correct_answer_id = trim($correctanswers[$k]["@"]["answer_id"]);
-                    if (strcmp ($id, $correct_answer_id) == 0) {
-                        $iscorrect = 1;
-                    }
-
-                }
                 if ($iscorrect) {
-                    $question->fraction[$j] = floor(100000/$correctanswercount)/100000; // strange behavior if we have more than 5 decimal places
-                    $question->feedback[$j] = trim($thisquestion["#"]["GRADABLE"][$j]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
+                    $question->fraction[] = $fraction;
                 } else {
-                    $question->fraction[$j] = 0;
-                    $question->feedback[$j] = trim($thisquestion["#"]["GRADABLE"][$j]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
+                    $question->fraction[] = 0;
                 }
+                // There is never feedback specific to each choice.
+                $question->feedback[] =  $this->text_field('');
             }
-
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Fill in the Blank Questions
-    //----------------------------------------
-    function process_fib($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_FILLINBLANK"])) {
-            $fibquestions = $xml["POOL"]["#"]["QUESTION_FILLINBLANK"];
-        }
-        else {
+    /**
+     * Process Fill in the Blank Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_fib($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) {
+            $fibquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($fibquestions); $i++) {
-            $question = $this->defaultquestion();
-
-            $question->qtype = SHORTANSWER;
-            $question->usecase = 0; // Ignore case
-
-            $thisquestion = $fibquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $answer = trim($thisquestion["#"]["ANSWER"][0]["#"]["TEXT"][0]["#"]);
-
-            $question->answer[] = $answer;
-            $question->fraction[] = 1;
-            $question->feedback = array();
-
-            if (is_array( $thisquestion['#']['GRADABLE'][0]['#'] )) {
-                $question->feedback[0] = trim($thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
-            }
-            else {
-                $question->feedback[0] = '';
-            }
-            if (is_array( $thisquestion["#"]["GRADABLE"][0]["#"] )) {
-                $question->feedback[1] = trim($thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-            }
-            else {
-                $question->feedback[1] = '';
+        foreach ($fibquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $question->qtype = 'shortanswer';
+            $question->usecase = 0; // Ignore case.
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            foreach ($answers as $answer) {
+                $question->answer[] = $this->getpath($answer,
+                        array('#', 'TEXT', 0, '#'), '', true);
+                $question->fraction[] = 1;
+                $question->feedback[] = $this->text_field($this->cleaninput($correctfeedback));
             }
+            $question->answer[] = '*';
+            $question->fraction[] = 0;
+            $question->feedback[] = $this->text_field($this->cleaninput($incorrectfeedback));
 
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Matching Questions
-    //----------------------------------------
-    function process_matching($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MATCH"])) {
-            $matchquestions = $xml["POOL"]["#"]["QUESTION_MATCH"];
-        }
-        else {
+    /**
+     * Process Matching Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_matching($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) {
+            $matchquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MATCH'), false, false);
+        } else {
             return;
         }
-
-        for ($i = 0; $i < sizeof ($matchquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MATCH;
-
-            $thisquestion = $matchquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
+        // Blackboard questions can't be imported in core Moodle without a loss in data,
+        // as core match question don't allow HTML in subanswers. The contributed ddmatch
+        // question type support HTML in subanswers.
+        // The ddmatch question type is not part of core, so we need to check if it is defined.
+        $ddmatch_is_installed = question_bank::is_qtype_installed('ddmatch');
+
+        foreach ($matchquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+            if ($ddmatch_is_installed) {
+                $question->qtype = 'ddmatch';
+            } else {
+                $question->qtype = 'match';
             }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $choices = $thisquestion["#"]["CHOICE"];
-            for ($j = 0; $j < sizeof ($choices); $j++) {
-
-                $subquestion = NULL;
-
-                $choice = $choices[$j]["#"]["TEXT"][0]["#"];
-                $choice_id = $choices[$j]["@"]["id"];
-
-                $question->subanswers[] = trim($choice);
-
-                $correctanswers = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"];
-                for ($k = 0; $k < sizeof ($correctanswers); $k++) {
-
-                    if (strcmp($choice_id, $correctanswers[$k]["@"]["choice_id"]) == 0) {
-
-                        $answer_id = $correctanswers[$k]["@"]["answer_id"];
 
-                        $answers = $thisquestion["#"]["ANSWER"];
-                        for ($m = 0; $m < sizeof ($answers); $m++) {
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            // As there is no partially correct feedback we use incorrect one.
+            $question->partiallycorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $choices = $this->getpath($thisquestion,
+                    array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers.
+            $answers = $this->getpath($thisquestion,
+                    array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions.
+            $correctanswers = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers.
+            $mappings = array();
+            foreach ($correctanswers as $correctanswer) {
+                if ($correctanswer) {
+                    $correct_choice_id = $this->getpath($correctanswer,
+                                array('@', 'choice_id'), '', true);
+                    $correct_answer_id = $this->getpath($correctanswer,
+                            array('@', 'answer_id'),
+                            '', true);
+                    $mappings[$correct_answer_id] = $correct_choice_id;
+                }
+            }
 
-                            $answer = $answers[$m];
-                            $current_ans_id = $answer["@"]["id"];
-                            if (strcmp ($current_ans_id, $answer_id) == 0) {
+            foreach ($choices as $choice) {
+                if ($ddmatch_is_installed) {
+                    $choicetext = $this->text_field($this->cleaninput($this->getpath($choice,
+                            array('#', 'TEXT', 0, '#'), '', true)));
+                } else {
+                    $choicetext = trim(strip_tags($this->getpath($choice,
+                            array('#', 'TEXT', 0, '#'), '', true)));
+                }
 
-                                $answer = $answer["#"]["TEXT"][0]["#"];
-                                $question->subquestions[] = trim($answer);
+                if ($choicetext != '') { // Only import non empty subanswers.
+                    $subquestion = '';
+                    $choice_id = $this->getpath($choice,
+                            array('@', 'id'), '', true);
+                    $fiber = array_search($choice_id, $mappings);
+                    $fiber = array_keys ($mappings, $choice_id);
+                    foreach ($fiber as $correct_answer_id) {
+                        // We have found a correspondance for this choice so we need to take the associated answer.
+                        foreach ($answers as $answer) {
+                            $current_ans_id = $this->getpath($answer,
+                                    array('@', 'id'), '', true);
+                            if (strcmp ($current_ans_id, $correct_answer_id) == 0) {
+                                $subquestion = $this->getpath($answer,
+                                        array('#', 'TEXT', 0, '#'), '', true);
                                 break;
                             }
                         }
-                        break;
+                        $question->subquestions[] = $this->text_field($this->cleaninput($subquestion));
+                        $question->subanswers[] = $choicetext;
+                    }
+
+                    if ($subquestion == '') { // Then in this case, $choice is a distractor.
+                        $question->subquestions[] = $this->text_field('');
+                        $question->subanswers[] = $choicetext;
                     }
                 }
             }
 
-            $questions[] = $question;
+            // Verify that this matching question has enough subquestions and subanswers.
+            $subquestioncount = 0;
+            $subanswercount = 0;
+            $subanswers = $question->subanswers;
+            foreach ($question->subquestions as $key => $subquestion) {
+                $subquestion = $subquestion['text'];
+                $subanswer = $subanswers[$key];
+                if ($subquestion != '') {
+                    $subquestioncount++;
+                }
+                $subanswercount++;
+            }
+            if ($subquestioncount < 2 || $subanswercount < 3) {
+                    $this->error(get_string('notenoughtsubans', 'qformat_blackboard', $question->questiontext));
+            } else {
+                $questions[] = $question;
+            }
 
         }
     }
index 6f68e21..f5fa957 100644 (file)
 /**
  * Strings for component 'qformat_blackboard', language 'en', branch 'MOODLE_20_STABLE'
  *
- * @package    qformat
- * @subpackage blackboard
+ * @package    qformat_blackboard
  * @copyright  2010 Helen Foster
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['defaultname'] = 'Imported question {$a}';
+$string['importnotext'] = 'Missing question text in XML file';
+$string['notenoughtsubans'] = 'Unable to import matching question \'{$a}\' because a matching question must comprise at least two questions and three answers.';
 $string['pluginname'] = 'Blackboard';
 $string['pluginname_help'] = 'Blackboard format enables questions saved in the Blackboard version 5 "POOL" type export format to be imported.';
diff --git a/question/format/blackboard/tests/blackboardformat_test.php b/question/format/blackboard/tests/blackboardformat_test.php
new file mode 100644 (file)
index 0000000..d67e526
--- /dev/null
@@ -0,0 +1,328 @@
+<?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/>.
+
+/**
+ * Unit tests for the Moodle Blackboard format.
+ *
+ * @package    qformat_blackboard
+ * @copyright  2012 Jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/format.php');
+require_once($CFG->dirroot . '/question/format/blackboard/format.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the blackboard question import format.
+ *
+ * @copyright  2012 Jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qformat_blackboard_test extends question_testcase {
+
+    public function make_test_xml() {
+        $xml = file_get_contents(__DIR__ . '/fixtures/sample_blackboard.dat');
+        return $xml;
+    }
+
+    public function test_import_match() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_matching($xmldata, $questions);
+        $q = $questions[0];
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'match';
+        $expectedq->name = 'Classify the animals.';
+        $expectedq->questiontext = '<i>Classify the animals.</i>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->correctfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->partiallycorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->incorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->subquestions = array(
+            array('text' => 'cat', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => '', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => 'frog', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => 'newt', 'format' => FORMAT_HTML, 'files' => array()));
+        $expectedq->subanswers = array('mammal', 'insect', 'amphibian', 'amphibian');
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_multichoice_single() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_mc($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'multichoice';
+        $expectedq->single = 1;
+        $expectedq->name = 'What\'s between orange and green in the spectrum?';
+        $expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->correctfeedback = array('text' => 'You gave the right answer.',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->partiallycorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->incorrectfeedback = array('text' => 'Only yellow is between orange and green in the spectrum.',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->answer = array(
+                0 => array(
+                    'text' => '<span style="font-size:12pt">red</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '<span style="font-size:12pt">yellow</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => '<span style="font-size:12pt">blue</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+        $expectedq->fraction = array(0, 1, 0);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_multichoice_multi() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_ma($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'multichoice';
+        $expectedq->single = 0;
+        $expectedq->name = 'What\'s between orange and green in the spectrum?';
+        $expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->correctfeedback = array(
+                'text' => 'You gave the right answer.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+        $expectedq->partiallycorrectfeedback = array(
+                'text' => 'Only yellow and off-beige are between orange and green in the spectrum.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+        $expectedq->incorrectfeedback = array(
+                'text' => 'Only yellow and off-beige are between orange and green in the spectrum.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->answer = array(
+                0 => array(
+                    'text' => '<span style="font-size:12pt">yellow</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '<span style="font-size:12pt">red</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => '<span style="font-size:12pt">off-beige</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                3 => array(
+                    'text' => '<span style="font-size:12pt">blue</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+        $expectedq->fraction = array(0.5, 0, 0.5, 0);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                3 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_truefalse() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_tf($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'truefalse';
+        $expectedq->name = '42 is the Absolute Answer to everything.';
+        $expectedq->questiontext = '<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->correctanswer = 0;
+        $expectedq->feedbacktrue = array(
+                'text' => '42 is the Ultimate Answer.',
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            );
+        $expectedq->feedbackfalse = array(
+                'text' => 'You gave the right answer.',
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            );
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_fill_in_the_blank() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_fib($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'shortanswer';
+        $expectedq->name = 'Name an amphibian: __________.';
+        $expectedq->questiontext = '<span style="font-size:12pt">Name an amphibian: __________.</span>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->usecase = 0;
+        $expectedq->answer = array('frog', '*');
+        $expectedq->fraction = array(1, 0);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_essay() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_essay($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'essay';
+        $expectedq->name = 'How are you?';
+        $expectedq->questiontext = 'How are you?';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->responseformat = 'editor';
+        $expectedq->responsefieldlines = 15;
+        $expectedq->attachments = 0;
+        $expectedq->graderinfo = array(
+                'text' => 'Blackboard answer for essay questions will be imported as informations for graders.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+}
diff --git a/question/format/blackboard/tests/fixtures/sample_blackboard.dat b/question/format/blackboard/tests/fixtures/sample_blackboard.dat
new file mode 100644 (file)
index 0000000..93bb583
--- /dev/null
@@ -0,0 +1,142 @@
+<?xml version='1.0' encoding='utf-8'?>\r
+<POOL>\r
+    <TITLE value='exam 3 2008-9'/>\r
+    <QUESTIONLIST>\r
+        <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>\r
+        <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>\r
+        <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>\r
+        <QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/>\r
+        <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>\r
+        <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>\r
+    </QUESTIONLIST>\r
+    <QUESTION_TRUEFALSE id='q1'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q1_a1'>\r
+            <TEXT>False</TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q1_a2'>\r
+            <TEXT>True</TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+            <CORRECTANSWER answer_id='q1_a2'/>\r
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>\r
+        </GRADABLE>\r
+    </QUESTION_TRUEFALSE>\r
+    <QUESTION_MULTIPLECHOICE id='q7'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q7_a1' position='1'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q7_a2' position='2'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q7_a3' position='3'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+            <CORRECTANSWER answer_id='q7_a2'/>\r
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>\r
+        </GRADABLE>\r
+    </QUESTION_MULTIPLECHOICE>\r
+    <QUESTION_MULTIPLEANSWER id='q8'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q8_a1' position='1'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q8_a2' position='2'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q8_a3' position='3'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q8_a4' position='4'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+            <CORRECTANSWER answer_id='q8_a1'/>\r
+            <CORRECTANSWER answer_id='q8_a3'/>\r
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>\r
+        </GRADABLE>\r
+    </QUESTION_MULTIPLEANSWER>\r
+    <QUESTION_MATCH id='q39-44'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q39-44_a1' position='1'>\r
+            <TEXT><![CDATA[frog]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q39-44_a2' position='2'>\r
+            <TEXT><![CDATA[cat]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q39-44_a3' position='3'>\r
+            <TEXT><![CDATA[newt]]></TEXT>\r
+        </ANSWER>\r
+        <CHOICE id='q39-44_c1' position='1'>\r
+            <TEXT><![CDATA[mammal]]></TEXT>\r
+        </CHOICE>\r
+        <CHOICE id='q39-44_c2' position='2'>\r
+            <TEXT><![CDATA[insect]]></TEXT>\r
+        </CHOICE>\r
+        <CHOICE id='q39-44_c3' position='3'>\r
+            <TEXT><![CDATA[amphibian]]></TEXT>\r
+        </CHOICE>\r
+        <GRADABLE>\r
+            <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>\r
+            <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>\r
+            <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>\r
+        </GRADABLE>\r
+    </QUESTION_MATCH>\r
+    <QUESTION_ESSAY id='q9'>\r
+        <BODY>\r
+            <TEXT><![CDATA[How are you?]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q9_a1'>\r
+            <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+        </GRADABLE>\r
+    </QUESTION_ESSAY>\r
+    <QUESTION_FILLINBLANK id='q27'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<span style="font-size:12pt">Name an amphibian: __________.</span>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q27_a1' position='1'>\r
+            <TEXT>frog</TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+        </GRADABLE>\r
+    </QUESTION_FILLINBLANK>\r
+</POOL>\r
index f2334f3..67e6e42 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Version information for the calculated question type.
  *
- * @package    qformat
- * @subpackage blackboard
+ * @package    qformat_blackboard
  * @copyright  2011 The Open University
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -26,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'qformat_blackboard';
-$plugin->version   = 2012061700;
+$plugin->version   = 2012061701;
 
 $plugin->requires  = 2012061700;
 
index f1acd34..7cea482 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Examview question importer.
  *
- * @package    qformat
- * @subpackage examview
+ * @package    qformat_examview
  * @copyright  2005 Howard Miller
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -35,7 +34,7 @@ require_once($CFG->libdir . '/xmlize.php');
  * @copyright  2005 Howard Miller
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class qformat_examview extends qformat_default {
+class qformat_examview extends qformat_based_on_xml {
 
     public $qtypes = array(
         'tf' => TRUEFALSE,
@@ -49,8 +48,8 @@ class qformat_examview extends qformat_default {
         'es' => ESSAY,
         'ca' => 99,
         'ot' => 99,
-        'sa' => SHORTANSWER
-        );
+        'sa' => SHORTANSWER,
+    );
 
     public $matching_questions = array();
 
@@ -62,6 +61,28 @@ class qformat_examview extends qformat_default {
         return 'application/xml';
     }
 
+    /**
+     * Some softwares put entities in exported files.
+     * This method try to clean up known problems.
+     * @param string str string to correct
+     * @return string the corrected string
+     */
+    public function cleaninput($str) {
+
+        $html_code_list = array(
+            "&#039;" => "'",
+            "&#8217;" => "'",
+            "&#8220;" => "\"",
+            "&#8221;" => "\"",
+            "&#8211;" => "-",
+            "&#8212;" => "-",
+        );
+        $str = strtr($str, $html_code_list);
+        // Use textlib entities_to_utf8 function to convert only numerical entities.
+        $str = textlib::entities_to_utf8( $str, false);
+        return $str;
+    }
+
     /**
      * unxmlise reconstructs part of the xml data structure in order
      * to identify the actual data therein
@@ -87,13 +108,6 @@ class qformat_examview extends qformat_default {
         $text = strip_tags($text);
         return $text;
     }
-    protected function text_field($text) {
-        return array(
-            'text' => htmlspecialchars(trim($text), ENT_NOQUOTES),
-            'format' => FORMAT_HTML,
-            'files' => array(),
-        );
-    }
 
     protected function add_blank_combined_feedback($question) {
         $question->correctfeedback['text'] = '';
@@ -108,7 +122,7 @@ class qformat_examview extends qformat_default {
         return $question;
     }
 
-    protected function parse_matching_groups($matching_groups) {
+    public function parse_matching_groups($matching_groups) {
         if (empty($matching_groups)) {
             return;
         }
@@ -136,8 +150,7 @@ class qformat_examview extends qformat_default {
         $phrase = trim($this->unxmlise($qrec['text']['0']['#']));
         $answer = trim($this->unxmlise($qrec['answer']['0']['#']));
         $answer = strip_tags( $answer );
-        $match_group->subquestions[] = $phrase;
-        $match_group->subanswers[] = $match_group->subchoices[$answer];
+        $match_group->mappings[$phrase] = $match_group->subchoices[$answer];
         $this->matching_questions[$groupname] = $match_group;
         return null;
     }
@@ -146,6 +159,7 @@ class qformat_examview extends qformat_default {
         if (empty($this->matching_questions)) {
             return;
         }
+
         foreach ($this->matching_questions as $match_group) {
             $question = $this->defaultquestion();
             $htmltext = s($match_group->questiontext);
@@ -157,12 +171,17 @@ class qformat_examview extends qformat_default {
             $question = $this->add_blank_combined_feedback($question);
             $question->subquestions = array();
             $question->subanswers = array();
-            foreach ($match_group->subquestions as $key => $value) {
-                $htmltext = s($value);
-                $question->subquestions[] = $this->text_field($htmltext);
-
-                $htmltext = s($match_group->subanswers[$key]);
-                $question->subanswers[] = $htmltext;
+            foreach ($match_group->subchoices as $subchoice) {
+                $fiber = array_keys ($match_group->mappings, $subchoice);
+                $subquestion = '';
+                foreach ($fiber as $subquestion) {
+                    $question->subquestions[] = $this->text_field($subquestion);
+                    $question->subanswers[] = $subchoice;
+                }
+                if ($subquestion == '') { // Then in this case, $subchoice is a distractor.
+                    $question->subquestions[] = $this->text_field('');
+                    $question->subanswers[] = $subchoice;
+                }
             }
             $questions[] = $question;
         }
@@ -172,7 +191,7 @@ class qformat_examview extends qformat_default {
         return str_replace('&#x2019;', "'", $text);
     }
 
-    protected function readquestions($lines) {
+    public function readquestions($lines) {
         // Parses an array of lines into an array of questions,
         // where each item is a question object as defined by
         // readquestion().
@@ -209,9 +228,11 @@ class qformat_examview extends qformat_default {
             $question->qtype = null;
         }
         $question->single = 1;
+
         // Only one answer is allowed.
         $htmltext = $this->unxmlise($qrec['#']['text'][0]['#']);
-        $question->questiontext = $htmltext;
+
+        $question->questiontext = $this->cleaninput($htmltext);
         $question->questiontextformat = FORMAT_HTML;
         $question->questiontextfiles = array();
         $question->name = shorten_text( $question->questiontext, 250 );
@@ -251,11 +272,11 @@ class qformat_examview extends qformat_default {
         $question->answer = $choices[$answer];
         $question->correctanswer = $question->answer;
         if ($question->answer == 1) {
-            $question->feedbacktrue = $this->text_field('Correct');
-            $question->feedbackfalse = $this->text_field('Incorrect');
+            $question->feedbacktrue = $this->text_field(get_string('correct', 'question'));
+            $question->feedbackfalse = $this->text_field(get_string('incorrect', 'question'));
         } else {
-            $question->feedbacktrue = $this->text_field('Incorrect');
-            $question->feedbackfalse = $this->text_field('Correct');
+            $question->feedbacktrue = $this->text_field(get_string('incorrect', 'question'));
+            $question->feedbackfalse = $this->text_field(get_string('correct', 'question'));
         }
         return $question;
     }
@@ -268,13 +289,13 @@ class qformat_examview extends qformat_default {
         foreach ($choices as $key => $value) {
             if (strpos(trim($key), 'choice-') !== false) {
 
-                $question->answer[$key] = $this->text_field(s($this->unxmlise($value[0]['#'])));
+                $question->answer[] = $this->text_field(s($this->unxmlise($value[0]['#'])));
                 if (strcmp($key, $answer) == 0) {
-                    $question->fraction[$key] = 1;
-                    $question->feedback[$key] = $this->text_field('Correct');
+                    $question->fraction[] = 1;
+                    $question->feedback[] = $this->text_field(get_string('correct', 'question'));
                 } else {
-                    $question->fraction[$key] = 0;
-                    $question->feedback[$key] = $this->text_field('Incorrect');
+                    $question->fraction[] = 0;
+                    $question->feedback[] = $this->text_field(get_string('incorrect', 'question'));
                 }
             }
         }
@@ -290,11 +311,15 @@ class qformat_examview extends qformat_default {
         foreach ($answers as $key => $value) {
             $value = trim($value);
             if (strlen($value) > 0) {
-                $question->answer[$key] = $value;
-                $question->fraction[$key] = 1;
-                $question->feedback[$key] = $this->text_field("Correct");
+                $question->answer[] = $value;
+                $question->fraction[] = 1;
+                $question->feedback[] = $this->text_field(get_string('correct', 'question'));
             }
         }
+        $question->answer[] = '*';
+        $question->fraction[] = 0;
+        $question->feedback[] = $this->text_field(get_string('incorrect', 'question'));
+
         return $question;
     }
 
@@ -318,10 +343,10 @@ class qformat_examview extends qformat_default {
             $value = trim($value);
             if (is_numeric($value)) {
                 $errormargin = 0;
-                $question->answer[$key] = $value;
-                $question->fraction[$key] = 1;
-                $question->feedback[$key] = $this->text_field("Correct");
-                $question->tolerance[$key] = $errormargin;
+                $question->answer[] = $value;
+                $question->fraction[] = 1;
+                $question->feedback[] = $this->text_field(get_string('correct', 'question'));
+                $question->tolerance[] = $errormargin;
             }
         }
         return $question;
index 970778f..ce51ff1 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Strings for component 'qformat_examview', language 'en', branch 'MOODLE_20_STABLE'
  *
- * @package    qformat
- * @subpackage examview
+ * @package    qformat_examview
  * @copyright  2010 Helen Foster
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
diff --git a/question/format/examview/tests/examviewformat_test.php b/question/format/examview/tests/examviewformat_test.php
new file mode 100644 (file)
index 0000000..601650f
--- /dev/null
@@ -0,0 +1,305 @@
+<?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/>.
+
+/**
+ * Unit tests for the Moodle Examview format.
+ *
+ * @package    qformat_examview
+ * @copyright  2012 jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/format.php');
+require_once($CFG->dirroot . '/question/format/examview/format.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the examview question import format.
+ *
+ * @copyright  2012 Jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qformat_examview_test extends question_testcase {
+
+    public function make_test_xml() {
+        $xml = $xml = file_get_contents(__DIR__ . '/fixtures/examview_sample.xml');
+        return array(0=>$xml);
+    }
+
+    public function test_import_truefalse() {
+
+        $xml = $this->make_test_xml();
+        $questions = array();
+
+        $importer = new qformat_examview();
+        $questions = $importer->readquestions($xml);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'truefalse';
+        $expectedq->name = '42 is the Absolute Answer to everything.';
+        $expectedq->questiontext = "42 is the Absolute Answer to everything.";
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_MOODLE;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->correctanswer = 0;
+        $expectedq->feedbacktrue = array(
+                'text' => get_string('incorrect', 'question'),
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            );
+        $expectedq->feedbackfalse = array(
+                'text' => get_string('correct', 'question'),
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+    public function test_import_multichoice_single() {
+        $xml = $this->make_test_xml();
+        $questions = array();
+
+        $importer = new qformat_examview();
+        $questions = $importer->readquestions($xml);
+        $q = $questions[1];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'multichoice';
+        $expectedq->single = 1;
+        $expectedq->name = "What's between orange and green in the spectrum?";
+        $expectedq->questiontext = "What's between orange and green in the spectrum?";
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->correctfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->partiallycorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->incorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_MOODLE;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->answer = array(
+                0 => array(
+                    'text' => 'red',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => 'yellow',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => 'blue',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+        $expectedq->fraction = array(0, 1, 0);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => get_string('incorrect', 'question'),
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => get_string('correct', 'question'),
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => get_string('incorrect', 'question'),
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_numerical() {
+
+        $xml = $this->make_test_xml();
+        $questions = array();
+
+        $importer = new qformat_examview();
+        $questions = $importer->readquestions($xml);
+        $q = $questions[2];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'numerical';
+        $expectedq->name = 'This is a numeric response question.  How much is 12 * 2?';
+        $expectedq->questiontext = 'This is a numeric response question.  How much is 12 * 2?';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_MOODLE;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->answer = array('24');
+        $expectedq->fraction = array(1);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => get_string('correct', 'question'),
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+
+
+    public function test_import_fill_in_the_blank() {
+
+        $xml = $this->make_test_xml();
+        $questions = array();
+
+        $importer = new qformat_examview();
+        $questions = $importer->readquestions($xml);
+        $q = $questions[3];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'shortanswer';
+        $expectedq->name = 'Name an amphibian: __________.';
+        $expectedq->questiontext = 'Name an amphibian: __________.';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_MOODLE;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->usecase = 0;
+        $expectedq->answer = array('frog', '*');
+        $expectedq->fraction = array(1, 0);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => get_string('correct', 'question'),
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => get_string('incorrect', 'question'),
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_essay() {
+
+        $xml = $this->make_test_xml();
+        $questions = array();
+
+        $importer = new qformat_examview();
+        $questions = $importer->readquestions($xml);
+        $q = $questions[4];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'essay';
+        $expectedq->name = 'How are you?';
+        $expectedq->questiontext = 'How are you?';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_MOODLE;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->responseformat = 'editor';
+        $expectedq->responsefieldlines = 15;
+        $expectedq->attachments = 0;
+        $expectedq->graderinfo = array(
+                'text' => 'Examview answer for essay questions will be imported as informations for graders.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    // Due to the way matching questions are parsed,
+    // the test for matching questions is somewhat different.
+    // First we test the parse_matching_groups method alone.
+    // Then we test the whole process wich involve parse_matching_groups,
+    // parse_ma and process_matches methods.
+    public function test_parse_matching_groups() {
+        $lines = $this->make_test_xml();
+
+        $importer = new qformat_examview();
+        $text = implode($lines, ' ');
+
+        $xml = xmlize($text, 0);
+        $importer->parse_matching_groups($xml['examview']['#']['matching-group']);
+        $matching = $importer->matching_questions;
+        $group = new stdClass();
+        $group->questiontext = 'Classify the animals.';
+        $group->subchoices = array('A' => 'insect', 'B' => 'amphibian', 'C' =>'mammal');
+            $group->subquestions = array();
+            $group->subanswers = array();
+        $expectedmatching = array( 'Matching 1' => $group);
+
+        $this->assertEquals($matching, $expectedmatching);
+    }
+
+    public function test_import_match() {
+
+        $xml = $this->make_test_xml();
+        $questions = array();
+
+        $importer = new qformat_examview();
+        $questions = $importer->readquestions($xml);
+        $q = $questions[5];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'match';
+        $expectedq->name = 'Classify the animals.';
+        $expectedq->questiontext = 'Classify the animals.';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->correctfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->partiallycorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->incorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_MOODLE;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->subquestions = array(
+            array('text' => '', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => 'frog', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => 'newt', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => 'cat', 'format' => FORMAT_HTML, 'files' => array()));
+        $expectedq->subanswers = array('insect', 'amphibian', 'amphibian', 'mammal');
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+}
diff --git a/question/format/examview/tests/fixtures/examview_sample.xml b/question/format/examview/tests/fixtures/examview_sample.xml
new file mode 100644 (file)
index 0000000..7627e46
--- /dev/null
@@ -0,0 +1,161 @@
+<?xml version='1.0' encoding='utf-8' standalone='yes'?>
+<examview type='test' platform='Windows' app-version='4.0.2'>
+    <header>
+        <title>Moodle Example</title>
+        <version>A</version>
+    </header>
+    <font-table>
+        <font-entry number='1'>
+            <charset>ansi</charset>
+            <name>Times New Roman</name>
+            <pitch>variable</pitch>
+            <family>roman</family>
+        </font-entry>
+    </font-table>
+    <preferences>
+        <show>
+            <show-answer value='yes'/>
+            <show-difficulty value='yes'/>
+            <show-reference value='yes'/>
+            <show-text-objective value='yes'/>
+            <show-state-objective value='yes'/>
+            <show-topic value='yes'/>
+            <show-keywords value='yes'/>
+            <show-misc value='yes'/>
+            <show-notes value='yes'/>
+            <show-rationale value='yes'/>
+        </show>
+        <leave-answer-space>
+            <tf value='yes'/>
+            <mtf value='yes'/>
+            <mc value='yes'/>
+            <yn value='yes'/>
+            <nr value='no'/>
+            <co value='no'/>
+            <ma value='yes'/>
+            <sa value='no'/>
+            <pr value='no'/>
+            <es value='no'/>
+            <ca value='no'/>
+            <ot value='no'/>
+        </leave-answer-space>
+        <question-type-order value='tf,mtf,mc,yn,nr,co,ma,sa,pr,es,ca,ot'/>
+        <section-page-break value='no'/>
+        <bi-display-mode value='mc'/>
+        <tf-show-choices value='no'/>
+        <mc-conserve-paper value='no'/>
+        <mc-choice-sequence value='abcde'/>
+        <show-answer-lines value='no'/>
+        <question-numbering value='continuous'/>
+        <answer-style template='a.' style='none'/>
+        <number-style template='1.' style='none'/>
+        <max-question-id value='9'/>
+        <max-narrative-id value='0'/>
+        <max-group-id value='1'/>
+        <default-font target='title'>
+            <number>1</number>
+            <size>13</size>
+            <style>bold</style>
+            <text-rgb>#000000</text-rgb>
+        </default-font>
+        <default-font target='sectiontitle'>
+            <number>1</number>
+            <size>11</size>
+            <style>bold</style>
+            <text-rgb>#000000</text-rgb>
+        </default-font>
+        <default-font target='questionnumber'>
+            <number>1</number>
+            <size>11</size>
+            <text-rgb>#000000</text-rgb>
+        </default-font>
+        <default-font target='answerchoice'>
+            <number>1</number>
+            <size>11</size>
+            <text-rgb>#000000</text-rgb>
+        </default-font>
+        <default-font target='newquestiondefault'>
+            <number>1</number>
+            <size>11</size>
+            <text-rgb>#000000</text-rgb>
+        </default-font>
+    </preferences>
+    <test-header page='first'><para tabs='R10440'><b>Name: ________________________  Class: ___________________  Date: __________    ID: <field field-type='version'/></b></para></test-header>
+    <test-header page='subsequent'><para tabs='R10440'><b>Name: ________________________    ID: <field field-type='version'/></b></para></test-header>
+    <test-footer page='first'><para justify='center'><field field-type='pageNumber'/>
+</para></test-footer>
+    <test-footer page='subsequent'><para justify='center'><field field-type='pageNumber'/>
+</para></test-footer>
+    <instruction type='tf'><b>True/False</b><font size='10'>
+</font><i>Indicate whether the sentence or statement is true or false.</i></instruction>
+    <instruction type='mtf'><b>Modified True/False</b><font size='10'>
+</font><i>Indicate whether the sentence or statement is true or false.  If false, change the identified word or phrase to make the sentence or statement true.</i></instruction>
+    <instruction type='mc'><b>Multiple Choice</b><font size='10'>
+</font><i>Identify the letter of the choice that best completes the statement or answers the question.</i></instruction>
+    <instruction type='yn'><b>Yes/No</b><font size='10'>
+</font><i>Indicate whether you agree with the sentence or statement.</i></instruction>
+    <instruction type='nr'><b>Numeric Response</b></instruction>
+    <instruction type='co'><b>Completion</b><font size='10'>
+</font><i>Complete each sentence or statement.</i></instruction>
+    <instruction type='ma'><b>Matching</b></instruction>
+    <instruction type='sa'><b>Short Answer</b></instruction>
+    <instruction type='pr'><b>Problem</b></instruction>
+    <instruction type='es'><b>Essay</b></instruction>
+    <instruction type='ca'><b>Case</b></instruction>
+    <instruction type='ot'><b>Other</b></instruction>
+    <question type='tf' question-id='1' bank-id='0'>
+        <text>42 is the Absolute Answer to everything.</text>
+        <rationale>42 is the Ultimate Answer.</rationale>
+        <answer>F</answer>
+    </question>
+    <question type='mc' question-id='2' bank-id='0'>
+        <text><font size='10'>What's between orange and green in the spectrum?</font></text>
+        <choices columns='2'>
+            <choice-a><font size='10'>red</font></choice-a>
+            <choice-b><font size='10'>yellow</font></choice-b>
+            <choice-c><font size='10'>blue</font></choice-c>
+        </choices>
+        <answer>B</answer>
+    </question>
+    <question type='nr' question-id='3' bank-id='0'>
+        <text>This is a numeric response question.  How much is 12 * 2?</text>
+        <answer>24</answer>
+        <info>
+            <answer-space>-1</answer-space>
+        </info>
+    </question>
+    <matching-group group-id='1' bank-id='0' name='Matching 1'>
+        <text>Classify the animals.</text>
+        <choices columns='2'>
+            <choice-a>insect</choice-a>
+            <choice-b>amphibian</choice-b>
+            <choice-c>mammal</choice-c>
+        </choices>
+    </matching-group>
+    <question type='ma' question-id='6' bank-id='0' group='Matching 1'>
+        <text>cat</text>
+        <answer>C</answer>
+    </question>
+    <question type='ma' question-id='7' bank-id='0' group='Matching 1'>
+        <text>frog</text>
+        <answer>B</answer>
+    </question>
+    <question type='ma' question-id='8' bank-id='0' group='Matching 1'>
+        <text>newt</text>
+        <answer>B</answer>
+    </question>
+    <question type='sa' question-id='5' bank-id='0'>
+        <text>Name an amphibian: __________.</text>
+        <answer>frog</answer>
+        <info>
+            <answer-space>-1</answer-space>
+        </info>
+    </question>
+    <question type='es' question-id='4' bank-id='0'>
+        <text>How are you?</text>
+        <answer>Examview answer for essay questions will be imported as informations for graders.</answer>
+        <info>
+            <answer-space>-1</answer-space>
+        </info>
+    </question>
+</examview>
\ No newline at end of file
index e32474d..af82376 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Version information for the calculated question type.
  *
- * @package    qformat
- * @subpackage examview
+ * @package    qformat_examview
  * @copyright  2011 The Open University
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -26,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'qformat_examview';
-$plugin->version   = 2012061700;
+$plugin->version   = 2012061701;
 
 $plugin->requires  = 2012061700;
 
index 9683cae..6509d68 100644 (file)
@@ -316,3 +316,30 @@ function restart_preview($previewid, $questionid, $displayoptions, $context) {
     redirect(question_preview_url($questionid, $displayoptions->behaviour,
             $displayoptions->maxmark, $displayoptions, $displayoptions->variant, $context));
 }
+
+/**
+ * Scheduled tasks relating to question preview. Specifically, delete any old
+ * previews that are left over in the database.
+ */
+function question_preview_cron() {
+    $maxage = 24*60*60; // We delete previews that have not been touched for 24 hours.
+    $lastmodifiedcutoff = time() - $maxage;
+
+    mtrace("\n  Cleaning up old question previews...", '');
+    $oldpreviews = new qubaid_join('{question_usages} quba', 'quba.id',
+            'quba.component = :qubacomponent
+                    AND NOT EXISTS (
+                        SELECT 1
+                          FROM {question_attempts} qa
+                          JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
+                         WHERE qa.questionusageid = quba.id
+                           AND (qa.timemodified > :qamodifiedcutoff
+                                    OR qas.timecreated > :stepcreatedcutoff)
+                    )
+            ',
+            array('qubacomponent' => 'core_question_preview',
+                'qamodifiedcutoff' => $lastmodifiedcutoff, 'stepcreatedcutoff' => $lastmodifiedcutoff));
+
+    question_engine::delete_questions_usage_by_activities($oldpreviews);
+    mtrace('done.');
+}
index d576fd7..c144bbf 100644 (file)
@@ -197,7 +197,7 @@ function report_stats_report($course, $report, $mode, $user, $roleid, $time) {
                 echo "(".get_string("gdneed").")";
             } else {
                 if ($mode == STATS_MODE_DETAILED) {
-                    echo '<div class="graph"><img src="'.$CFG->wwwroot.'/report/stats/graph.php?mode='.$mode.'&amp;course='.$course->id.'&amp;time='.$time.'&amp;report='.$report.'&amp;userid='.$userid.'" alt="'.get_string('statisticsgraph').'" /></div';
+                    echo '<div class="graph"><img src="'.$CFG->wwwroot.'/report/stats/graph.php?mode='.$mode.'&amp;course='.$course->id.'&amp;time='.$time.'&amp;report='.$report.'&amp;userid='.$userid.'" alt="'.get_string('statisticsgraph').'" /></div>';
                 } else {
                     echo '<div class="graph"><img src="'.$CFG->wwwroot.'/report/stats/graph.php?mode='.$mode.'&amp;course='.$course->id.'&amp;time='.$time.'&amp;report='.$report.'&amp;roleid='.$roleid.'" alt="'.get_string('statisticsgraph').'" /></div>';
                 }
index dd41f63..e0c80f8 100644 (file)
@@ -106,7 +106,8 @@ if ($tagnew = $tagform->get_data()) {
         unset($tagnew->rawname);
 
     } else {  // They might be trying to change the rawname, make sure it's a change that doesn't affect name
-        $tagnew->name = array_shift(tag_normalize($tagnew->rawname, TAG_CASE_LOWER));
+        $norm = tag_normalize($tagnew->rawname, TAG_CASE_LOWER);
+        $tagnew->name = array_shift($norm);
 
         if ($tag->name != $tagnew->name) {  // The name has changed, let's make sure it's not another existing tag
             if (tag_get_id($tagnew->name)) {   // Something exists already, so flag an error
index d2a6f66..0afc563 100644 (file)
@@ -558,7 +558,8 @@ function tag_get_related_tags_csv($related_tags, $html=TAG_RETURN_HTML) {
 function tag_rename($tagid, $newrawname) {
     global $DB;
 
-    if (! $newrawname_clean = array_shift(tag_normalize($newrawname, TAG_CASE_ORIGINAL)) ) {
+    $norm = tag_normalize($newrawname, TAG_CASE_ORIGINAL);
+    if (! $newrawname_clean = array_shift($norm) ) {
         return false;
     }
 
@@ -1020,7 +1021,8 @@ function tag_cron() {
 function tag_find_tags($text, $ordered=true, $limitfrom='', $limitnum='') {
     global $DB;
 
-    $text = array_shift(tag_normalize($text, TAG_CASE_LOWER));
+    $norm = tag_normalize($text, TAG_CASE_LOWER);
+    $text = array_shift($norm);
 
     if ($ordered) {
         $query = "SELECT tg.id, tg.name, tg.rawname, COUNT(ti.id) AS count
index 81afc01..095bbda 100644 (file)
@@ -261,7 +261,8 @@ function tag_print_search_results($query,  $page, $perpage, $return=false) {
 
     global $CFG, $USER, $OUTPUT;
 
-    $query = array_shift(tag_normalize($query, TAG_CASE_ORIGINAL));
+    $norm = tag_normalize($query, TAG_CASE_ORIGINAL);
+    $query = array_shift($norm);
 
     $count = sizeof(tag_find_tags($query, false));
     $tags = array();
index 15659a9..69be86e 100644 (file)
@@ -508,7 +508,7 @@ body.tag .managelink {padding: 5px;}
 #custommenu .yui3-menu-horizontal .yui3-menu-label,
 #custommenu .yui3-menu-horizontal .yui3-menu-content {background-image:none;background-position:right center;background-repeat:no-repeat;}
 #custommenu .yui3-menu-label,
-#custommenu .yui3-menu .yui3-menu .yui3-menu-label {background-image:url([[pix:theme|vertical-menu-submenu-indicator]]);}
+#custommenu .yui3-menu .yui3-menu .yui3-menu-label {background-image:url([[pix:theme|vertical-menu-submenu-indicator]]); padding-right: 20px;}
 #custommenu .yui3-menu .yui3-menu .yui3-menu-label-menuvisible {background-image:url([[pix:theme|horizontal-menu-submenu-indicator]]);}
 
 /**
index f1e652e..782b4a6 100644 (file)
@@ -30,7 +30,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2012081600.00;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2012081600.01;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes