Merge branch 'w34_MDL-34901_m23_useraccess' of git://github.com/skodak/moodle into...
authorDan Poltawski <dan@moodle.com>
Tue, 21 Aug 2012 04:02:25 +0000 (12:02 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 21 Aug 2012 04:02:25 +0000 (12:02 +0800)
53 files changed:
admin/settings/location.php
admin/tool/phpunit/cli/util.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]
blocks/tags/block_tags.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/form/tests/dateselector_test.php
lib/form/tests/datetimeselector_test.php
lib/messagelib.php
lib/moodlelib.php
lib/phpunit/classes/data_generator.php
lib/phpunit/classes/util.php
lib/phpunit/tests/generator_test.php
lib/tests/backup_test.php [deleted file]
lib/tests/moodlelib_test.php
mod/assign/gradingtable.php
mod/assign/index.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
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

index 3d3ccb8..9f74d51 100644 (file)
@@ -14,7 +14,8 @@ 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('googlemapkey', new lang_string('googlemapkey', 'admin'), new lang_string('configgooglemapkey', 'admin', $CFG->wwwroot), '', PARAM_RAW, 60));
+    $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 2ce9434..30714ed 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 9715a5d..b6d81ab 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 7ce4a4a..6183a9b 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 b79e443..dc7fa19 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 cb899f0..09fb15b 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 afb5383..5eff878 100644 (file)
@@ -80,7 +80,7 @@ $PAGE->set_title(get_string('iplookup', 'admin').': '.$title);
 $PAGE->set_heading($title);
 echo $OUTPUT->header();
 
-if (empty($CFG->googlemapkey)) {
+if (empty($CFG->googlemapkey) and empty($CFG->googlemapkey3)) {
     $imgwidth  = 620;
     $imgheight = 310;
     $dotwidth  = 18;
@@ -95,11 +95,23 @@ if (empty($CFG->googlemapkey)) {
     echo '</div>';
     echo '<div id="note">'.$info['note'].'</div>';
 
-} else {
+} else if (empty($CFG->googlemapkey3)) {
     $PAGE->requires->js(new moodle_url("http://maps.google.com/maps?file=api&v=2&key=$CFG->googlemapkey"));
     $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);
 
+    echo '<div id="map" style="width: 650px; height: 360px"></div>';
+    echo '<div id="note">'.$info['note'].'</div>';
+
+} else {
+    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.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..004b7d2 100644 (file)
@@ -41,3 +41,21 @@ M.core_iplookup.init = function(Y, latitude, longitude) {
         }, document.body);
     }
 };
+
+M.core_iplookup.init3 = function(Y, latitude, longitude, ip) {
+    var ipLatlng = new google.maps.LatLng(latitude, longitude);
+
+    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 6b571c4..26fbe3b 100644 (file)
@@ -214,11 +214,11 @@ $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['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="https://developers.google.com/maps/documentation/javascript/v2/introduction#Obtaining_Key">https://developers.google.com/maps/documentation/javascript/v2/introduction#Obtaining_Key</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.';
@@ -545,7 +545,9 @@ $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['googlemapkey'] = 'Google Maps API V2 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 9a9d10c..f7e0829 100644 (file)
@@ -71,6 +71,7 @@ $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 1c7097e..b41b67c 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 4df3bc0..cd07c45 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 7155373..1e8b1c8 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 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 f4674ce..1fee1b0 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 104da29..ce53c0c 100644 (file)
@@ -2324,10 +2324,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;
 }
 
@@ -4838,12 +4838,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 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);
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 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 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..84a1638 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   = 2012061701; // 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 c8936ec..483f0dc 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) {
@@ -929,7 +937,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();
@@ -940,8 +948,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;
@@ -969,26 +978,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;
     }
 
@@ -1034,7 +1050,6 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     }
 }
 
-
 /**
  * @global object $DB
  * @global object $CFG
index daf6f02..7c4ed9d 100644 (file)
@@ -2091,8 +2091,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 8c7b4d9..dc3495b 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 b2414a7..e688add 100644 (file)
@@ -1639,12 +1639,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;
@@ -1672,28 +1668,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']);
@@ -1702,6 +1682,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 e7f570e..6fbccbf 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 0ccdbce..a0e9df4 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 f763557..e228245 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 f8548c4..1b7fbec 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 b71c72f..ee17bc0 100644 (file)
@@ -111,7 +111,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 cdbe37f..8e6b397 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 fb7f9a9..589b6dc 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 a91000e..9661206 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]]);}
 
 /**