$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+)*)?$/'));
} 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);
} else if ($user->secret == $confirmsecret) { // They have provided the secret key to get in
$DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
- $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+ if ($user->firstaccess == 0) {
+ $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+ }
return AUTH_CONFIRM_OK;
}
} else {
return AUTH_CONFIRM_FAIL;
}
$DB->set_field('user', 'confirmed', 1, array('id'=>$user->id));
- $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id));
+ if ($user->firstaccess == 0) {
+ $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id));
+ }
return AUTH_CONFIRM_OK;
}
} else {
return AUTH_CONFIRM_ALREADY;
} else {
$DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
- $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+ if ($user->firstaccess == 0) {
+ $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+ }
return AUTH_CONFIRM_OK;
}
} else {
// 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;
}
$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');
}
// 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
$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;
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)) {
/**
* 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;
$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
--- /dev/null
+<?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));
+
+ }
+}
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');
$mform->setAdvanced('user');
}
- $sectiontitle = get_string('sectionname', 'format_'.$COURSE->format);
-
$options = array(''=>get_string('allactivities'));
$modsused = array();
$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');
$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;
$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;
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>';
}
}, 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
+ });
+};
$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.';
$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';
$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.)';
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) {
$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.
*
}
+ // 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');
'year' => 2011,
'usertimezone' => 0.0,
'timezone' => 0.0,
- 'timestamp' => 1309449600
+ 'timestamp' => 1309478400 // 6am at UTC+0
),
array (
'day' => 1,
'year' => 2011,
'usertimezone' => 0.0,
'timezone' => 99,
- 'timestamp' => 1309449600
+ 'timestamp' => 1309478400 // 6am at UTC+0
)
);
}
'year' => 2011,
'usertimezone' => 0.0,
'timezone' => 0.0,
- 'timestamp' => 1309449600
+ 'timestamp' => 1309478400 // 6am at UTC+0
),
array (
'minute' => 0,
'year' => 2011,
'usertimezone' => 0.0,
'timezone' => 99,
- 'timestamp' => 1309449600
+ 'timestamp' => 1309478400 // 6am at UTC+0
)
);
}
//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)) {
$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;
}
function update_user_login_times() {
global $USER, $DB;
+ $now = time();
+
$user = new stdClass();
+ $user->id = $USER->id;
+
+ // Make sure all users that logged in have some firstaccess.
+ if ($USER->firstaccess == 0) {
+ $USER->firstaccess = $user->firstaccess = $now;
+ }
+
+ // Store the previous current as lastlogin.
$USER->lastlogin = $user->lastlogin = $USER->currentlogin;
- $USER->currentlogin = $user->lastaccess = $user->currentlogin = time();
- $user->id = $USER->id;
+ $USER->currentlogin = $user->currentlogin = $now;
+
+ // Function user_accesstime_log() may not update immediately, better do it here.
+ $USER->lastaccess = $user->lastaccess = $now;
+ $USER->lastip = $user->lastip = getremoteaddr();
$DB->update_record('user', $user);
return true;
$DB->set_field('user', 'auth', $auth, array('username'=>$username));
$user->auth = $auth;
}
- if (empty($user->firstaccess)) { //prevent firstaccess from remaining 0 for manual account that never required confirmation
- $DB->set_field('user','firstaccess', $user->timemodified, array('id' => $user->id));
- $user->firstaccess = $user->timemodified;
- }
update_internal_user_password($user, $password); // just in case salt or encoding were changed (magic quotes too one day)
$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');
}
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'])) {
$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'])) {
* 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');
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";
}
}
$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);
+++ /dev/null
-<?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));
-
- }
-}
'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',
}
/**
- * 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));
}
/**
$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;
$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&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;
}
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)
}
// 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 ') {
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) {
}
// It's not a system event
- $text = $message->message;
+ $text = trim($message->message);
/// Parse the text to clean and filter it
$options = new stdClass();
$special = false;
$outtime = $message->strtime;
- //Initilise output variable.
+ // Initialise variables.
$outmain = '';
+ $patternTo = '#^\s*To\s([^:]+):(.*)#';
if (substr($text, 0, 5) == 'beep ') {
$special = true;
} 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;
}
}
}
-
/**
* @global object $DB
* @global object $CFG
u.lastname,
u.email,
u.picture,
- u.imagealt,
- u.email
+ u.imagealt
FROM $fromsql
WHERE $selectsql
ORDER BY p.modified DESC";
$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";
}
public function col_sumgrades($attempt) {
- if ($attempt->state == quiz_attempt::FINISHED) {
+ if ($attempt->state != quiz_attempt::FINISHED) {
return '-';
}
$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;
$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']);
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.
*/
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();
+ }
}
* @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
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());
}
$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;
+ }
+}
/**
* 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
*/
defined('MOODLE_INTERNAL') || die();
-require_once ($CFG->libdir . '/xmlize.php');
+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(
+ "'" => "'",
+ "’" => "'",
+ "[" => "[",
+ "“" => "\"",
+ "”" => "\"",
+ "]" => "]",
+ "'" => "'",
+ "–" => "-",
+ "—" => "-",
+ );
+ $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();
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;
+ }
}
}
/**
* 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.';
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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
/**
* 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_blackboard';
-$plugin->version = 2012061700;
+$plugin->version = 2012061701;
$plugin->requires = 2012061700;
/**
* 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
*/
* @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,
'es' => ESSAY,
'ca' => 99,
'ot' => 99,
- 'sa' => SHORTANSWER
- );
+ 'sa' => SHORTANSWER,
+ );
public $matching_questions = array();
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(
+ "'" => "'",
+ "’" => "'",
+ "“" => "\"",
+ "”" => "\"",
+ "–" => "-",
+ "—" => "-",
+ );
+ $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
$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'] = '';
return $question;
}
- protected function parse_matching_groups($matching_groups) {
+ public function parse_matching_groups($matching_groups) {
if (empty($matching_groups)) {
return;
}
$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;
}
if (empty($this->matching_questions)) {
return;
}
+
foreach ($this->matching_questions as $match_group) {
$question = $this->defaultquestion();
$htmltext = s($match_group->questiontext);
$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;
}
return str_replace('’', "'", $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().
$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 );
$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;
}
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'));
}
}
}
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;
}
$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;
/**
* 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
*/
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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
/**
* 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
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'qformat_examview';
-$plugin->version = 2012061700;
+$plugin->version = 2012061701;
$plugin->requires = 2012061700;
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.');
+}
echo "(".get_string("gdneed").")";
} else {
if ($mode == STATS_MODE_DETAILED) {
- echo '<div class="graph"><img src="'.$CFG->wwwroot.'/report/stats/graph.php?mode='.$mode.'&course='.$course->id.'&time='.$time.'&report='.$report.'&userid='.$userid.'" alt="'.get_string('statisticsgraph').'" /></div';
+ echo '<div class="graph"><img src="'.$CFG->wwwroot.'/report/stats/graph.php?mode='.$mode.'&course='.$course->id.'&time='.$time.'&report='.$report.'&userid='.$userid.'" alt="'.get_string('statisticsgraph').'" /></div>';
} else {
echo '<div class="graph"><img src="'.$CFG->wwwroot.'/report/stats/graph.php?mode='.$mode.'&course='.$course->id.'&time='.$time.'&report='.$report.'&roleid='.$roleid.'" alt="'.get_string('statisticsgraph').'" /></div>';
}
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
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;
}
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
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();
#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]]);}
/**