Merge branch 'MDL-37375' of git://github.com/rwijaya/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 22 Jan 2013 04:43:47 +0000 (12:43 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 22 Jan 2013 04:43:47 +0000 (12:43 +0800)
116 files changed:
admin/mnet/index.php
admin/settings/appearance.php
admin/tool/customlang/filter_form.php
admin/tool/phpunit/cli/init.php
admin/tool/phpunit/cli/util.php
admin/tool/xmldb/actions/XMLDBCheckAction.class.php
admin/tool/xmldb/actions/check_bigints/check_bigints.class.php
admin/tool/xmldb/actions/check_defaults/check_defaults.class.php
admin/tool/xmldb/actions/check_foreign_keys/check_foreign_keys.class.php
admin/tool/xmldb/actions/check_indexes/check_indexes.class.php
admin/tool/xmldb/actions/check_oracle_semantics/check_oracle_semantics.class.php
admin/tool/xmldb/actions/delete_field/delete_field.class.php
admin/tool/xmldb/actions/delete_index/delete_index.class.php
admin/tool/xmldb/actions/delete_key/delete_key.class.php
admin/tool/xmldb/actions/delete_table/delete_table.class.php
admin/tool/xmldb/actions/delete_xml_file/delete_xml_file.class.php
admin/tool/xmldb/actions/revert_changes/revert_changes.class.php
blocks/online_users/tests/generator/lib.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/managesubscriptions_form.php
calendar/renderer.php
calendar/upgrade.txt
course/lib.php
course/modedit.php
course/rest.php
course/yui/dragdrop/dragdrop.js
course/yui/toolboxes/toolboxes.js
enrol/locallib.php
files/renderer.php
grade/report/grader/lib.php
grade/report/grader/styles.css
lang/en/admin.php
lang/en/calendar.php
lang/en/question.php
lib/ajax/blocks.php
lib/completionlib.php
lib/db/caches.php
lib/dml/moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/editor/tinymce/lib.php
lib/filelib.php
lib/form/filemanager.js
lib/javascript-static.js
lib/moodlelib.php
lib/outputrequirementslib.php
lib/phpunit/bootstrap.php
lib/phpunit/bootstraplib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/hint_resultprinter.php
lib/phpunit/classes/util.php
lib/phpunit/generatorlib.php
lib/phpunit/lib.php
lib/phpunit/tests/advanced_test.php
lib/phpunit/tests/basic_test.php
lib/resourcelib.php
lib/setup.php
lib/testing/classes/test_lock.php [new file with mode: 0644]
lib/testing/classes/tests_finder.php [new file with mode: 0644]
lib/testing/classes/util.php [new file with mode: 0644]
lib/testing/generator/block_generator.php [moved from lib/phpunit/classes/block_generator.php with 73% similarity]
lib/testing/generator/data_generator.php [moved from lib/phpunit/classes/data_generator.php with 94% similarity]
lib/testing/generator/lib.php [new file with mode: 0644]
lib/testing/generator/module_generator.php [moved from lib/phpunit/classes/module_generator.php with 74% similarity]
lib/testing/lib.php [new file with mode: 0644]
lib/testing/tests/generator_test.php [moved from lib/phpunit/tests/generator_test.php with 98% similarity]
lib/tests/moodlelib_test.php
lib/tests/weblib_test.php
lib/weblib.php
lib/yui/blocks/blocks.js
lib/yui/formchangechecker/formchangechecker.js
login/index.php
login/index_form.html
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/file/locallib.php
mod/assign/lib.php
mod/assign/submission/comments/locallib.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/tests/generator/lib.php
mod/assign/tests/upgradelib_test.php
mod/assign/upgradelib.php
mod/assignment/tests/generator/lib.php
mod/data/tests/generator/lib.php
mod/forum/backup/moodle2/restore_forum_activity_task.class.php
mod/forum/tests/generator/lib.php
mod/lesson/locallib.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/matching.php
mod/lti/tests/generator/lib.php
mod/page/tests/generator/lib.php
mod/quiz/cronlib.php
mod/quiz/locallib.php
mod/quiz/report/statistics/report.php
mod/quiz/tests/generator/lib.php
mod/scorm/locallib.php
phpunit.xml.dist
question/format.php
question/format/gift/tests/giftformat_test.php
question/type/essay/db/upgrade.php
question/type/essay/questiontype.php
question/type/essay/version.php
question/type/multianswer/questiontype.php
question/type/shortanswer/backup/moodle2/backup_qtype_shortanswer_plugin.class.php
question/type/shortanswer/backup/moodle2/restore_qtype_shortanswer_plugin.class.php
question/type/shortanswer/db/install.xml
question/type/shortanswer/db/upgrade.php [new file with mode: 0755]
question/type/shortanswer/questiontype.php
question/type/shortanswer/tests/upgradelibnewqe_test.php [moved from question/type/shortanswer/tests/tupgradelibnewqe_test.php with 100% similarity]
question/type/shortanswer/version.php
repository/wikimedia/lang/en/repository_wikimedia.php
repository/wikimedia/lib.php
repository/wikimedia/wikimedia.php
theme/base/style/calendar.css
version.php
webservice/rest/locallib.php

index d81e8a9..fb4e2c4 100644 (file)
@@ -85,7 +85,7 @@
     echo $OUTPUT->header();
 ?>
 <form method="post" action="index.php">
-    <table align="center" width="635" class="generalbox" border="0" cellpadding="5" cellspacing="0">
+    <table align="center" width="635" class="generaltable" border="0" cellpadding="5" cellspacing="0">
         <tr>
             <td  class="generalboxcontent">
             <table cellpadding="9" cellspacing="0" >
     </table>
 </form>
 <form method="post" action="index.php">
-    <table align="center" width="635" class="generalbox" border="0" cellpadding="5" cellspacing="0">
+    <table align="center" width="635" class="generaltable" border="0" cellpadding="5" cellspacing="0">
         <tr>
             <td  class="generalboxcontent">
             <table cellpadding="9" cellspacing="0" >
index 6b527ec..da078d7 100644 (file)
@@ -81,6 +81,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $temp->add(new admin_setting_configselect('calendar_exportlookahead', new lang_string('configexportlookahead','admin'), new lang_string('helpexportlookahead', 'admin'), 365, $days));
     $temp->add(new admin_setting_configselect('calendar_exportlookback', new lang_string('configexportlookback','admin'), new lang_string('helpexportlookback', 'admin'), 5, $days));
     $temp->add(new admin_setting_configtext('calendar_exportsalt', new lang_string('calendarexportsalt','admin'), new lang_string('configcalendarexportsalt', 'admin'), random_string(60)));
+    $temp->add(new admin_setting_configcheckbox('calendar_showicalsource', new lang_string('configshowicalsource', 'admin'), new lang_string('helpshowicalsource','admin'), 1));
     $ADMIN->add('appearance', $temp);
 
     // blog
index 4bfcd58..547a8a4 100644 (file)
@@ -60,8 +60,8 @@ class tool_customlang_filter_form extends moodleform {
 
         // Modified only
         $mform->addElement('advcheckbox', 'modified', get_string('filtermodified', 'tool_customlang'));
-        $mform->setType('filtermodified', PARAM_BOOL);
-        $mform->setDefault('filtermodified', 0);
+        $mform->setType('modified', PARAM_BOOL);
+        $mform->setDefault('modified', 0);
 
         // Substring
         $mform->addElement('text', 'substring', get_string('filtersubstring', 'tool_customlang'));
index ca00d56..bf026a4 100644 (file)
@@ -28,6 +28,7 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
 
 require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
+require_once(__DIR__.'/../../../../lib/testing/lib.php');
 
 echo "Initialising Moodle PHPUnit test environment...\n";
 
index 5cfd128..111be5d 100644 (file)
@@ -30,6 +30,7 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
 
 require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
+require_once(__DIR__.'/../../../../lib/testing/lib.php');
 
 // now get cli options
 list($options, $unrecognized) = cli_get_params(
@@ -110,7 +111,7 @@ Options:
 -h, --help     Print out this help
 
 Example:
-\$ php ".phpunit_bootstrap_cli_argument_path('/admin/tool/phpunit/cli/util.php')." --install
+\$ php ".testing_cli_argument_path('/admin/tool/phpunit/cli/util.php')." --install
 ";
     echo $help;
     exit(0);
@@ -136,7 +137,7 @@ if ($diag) {
 
 } else if ($drop) {
     // make sure tests do not run in parallel
-    phpunit_util::acquire_test_lock();
+    test_lock::acquire('phpunit');
     phpunit_util::drop_site(true);
     // note: we must stop here because $CFG is messed up and we can not reinstall, sorry
     exit(0);
index 06787a6..2514c61 100644 (file)
@@ -90,7 +90,7 @@ abstract class XMLDBCheckAction extends XMLDBAction {
 
         // If  not confirmed, show confirmation box
         if (!$confirmed) {
-            $o = '<table class="generalbox" border="0" cellpadding="5" cellspacing="0" id="notice">';
+            $o = '<table class="generaltable" border="0" cellpadding="5" cellspacing="0" id="notice">';
             $o.= '  <tr><td class="generalboxcontent">';
             $o.= '    <p class="centerpara">' . $this->str[$this->introstr] . '</p>';
             $o.= '    <table class="boxaligncenter" cellpadding="20"><tr><td>';
index 69bfb84..1118906 100644 (file)
@@ -99,7 +99,7 @@ class check_bigints extends XMLDBCheckAction {
         $dbman = $DB->get_manager();
 
         $s = '';
-        $r = '<table class="generalbox boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
+        $r = '<table class="generaltable boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
         $r.= '  <tr><td class="generalboxcontent">';
         $r.= '    <h2 class="main">' . $this->str['searchresults'] . '</h2>';
         $r.= '    <p class="centerpara">' . $this->str['wrongints'] . ': ' . count($wrong_fields) . '</p>';
index 15a9365..73306ea 100644 (file)
@@ -111,7 +111,7 @@ class check_defaults extends XMLDBCheckAction {
         $dbman = $DB->get_manager();
 
         $s = '';
-        $r = '<table class="generalbox boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
+        $r = '<table class="generaltable boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
         $r.= '  <tr><td class="generalboxcontent">';
         $r.= '    <h2 class="main">' . $this->str['searchresults'] . '</h2>';
         $r.= '    <p class="centerpara">' . $this->str['wrongdefaults'] . ': ' . count($wrong_fields) . '</p>';
index cd897d4..67e886f 100644 (file)
@@ -130,7 +130,7 @@ class check_foreign_keys extends XMLDBCheckAction {
     }
 
     protected function display_results(array $violatedkeys) {
-        $r = '<table class="generalbox boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
+        $r = '<table class="generaltable boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
         $r.= '  <tr><td class="generalboxcontent">';
         $r.= '    <h2 class="main">' . $this->str['searchresults'] . '</h2>';
         $r.= '    <p class="centerpara">' . $this->str['violatedforeignkeys'] . ': ' . count($violatedkeys) . '</p>';
index dc6ec90..ba03bac 100644 (file)
@@ -130,7 +130,7 @@ class check_indexes extends XMLDBCheckAction {
         $dbman = $DB->get_manager();
 
         $s = '';
-        $r = '<table class="generalbox boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
+        $r = '<table class="generaltable boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
         $r.= '  <tr><td class="generalboxcontent">';
         $r.= '    <h2 class="main">' . $this->str['searchresults'] . '</h2>';
         $r.= '    <p class="centerpara">' . $this->str['missingindexes'] . ': ' . count($missing_indexes) . '</p>';
index f7488c1..8041672 100644 (file)
@@ -110,7 +110,7 @@ class check_oracle_semantics extends XMLDBCheckAction {
         $dbman = $DB->get_manager();
 
         $s = '';
-        $r = '<table class="generalbox boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
+        $r = '<table class="generaltable boxaligncenter boxwidthwide" border="0" cellpadding="5" cellspacing="0" id="results">';
         $r.= '  <tr><td class="generalboxcontent">';
         $r.= '    <h2 class="main">' . $this->str['searchresults'] . '</h2>';
         $r.= '    <p class="centerpara">' . $this->str['wrongoraclesemantics'] . ': ' . count($wrong_fields) . '</p>';
index 1d8d414..66096f4 100644 (file)
@@ -73,7 +73,7 @@ class delete_field extends XMLDBAction {
 
         // If  not confirmed, show confirmation box
         if (!$confirmed) {
-            $o = '<table width="60" class="generalbox" border="0" cellpadding="5" cellspacing="0" id="notice">';
+            $o = '<table width="60" class="generaltable" border="0" cellpadding="5" cellspacing="0" id="notice">';
             $o.= '  <tr><td class="generalboxcontent">';
             $o.= '    <p class="centerpara">' . $this->str['confirmdeletefield'] . '<br /><br />' . $fieldparam . '</p>';
             $o.= '    <table class="boxaligncenter" cellpadding="20"><tr><td>';
index 7dadedc..ad6f801 100644 (file)
@@ -73,7 +73,7 @@ class delete_index extends XMLDBAction {
 
         // If  not confirmed, show confirmation box
         if (!$confirmed) {
-            $o = '<table width="60" class="generalbox" border="0" cellpadding="5" cellspacing="0" id="notice">';
+            $o = '<table width="60" class="generaltable" border="0" cellpadding="5" cellspacing="0" id="notice">';
             $o.= '  <tr><td class="generalboxcontent">';
             $o.= '    <p class="centerpara">' . $this->str['confirmdeleteindex'] . '<br /><br />' . $indexparam . '</p>';
             $o.= '    <table class="boxaligncenter" cellpadding="20"><tr><td>';
index 3c1b232..ea55010 100644 (file)
@@ -73,7 +73,7 @@ class delete_key extends XMLDBAction {
 
         // If  not confirmed, show confirmation box
         if (!$confirmed) {
-            $o = '<table width="60" class="generalbox" border="0" cellpadding="5" cellspacing="0" id="notice">';
+            $o = '<table width="60" class="generaltable" border="0" cellpadding="5" cellspacing="0" id="notice">';
             $o.= '  <tr><td class="generalboxcontent">';
             $o.= '    <p class="centerpara">' . $this->str['confirmdeletekey'] . '<br /><br />' . $keyparam . '</p>';
             $o.= '    <table class="boxaligncenter" cellpadding="20"><tr><td>';
index 9d32eec..e577b96 100644 (file)
@@ -72,7 +72,7 @@ class delete_table extends XMLDBAction {
 
         // If  not confirmed, show confirmation box
         if (!$confirmed) {
-            $o = '<table width="60" class="generalbox" border="0" cellpadding="5" cellspacing="0" id="notice">';
+            $o = '<table width="60" class="generaltable" border="0" cellpadding="5" cellspacing="0" id="notice">';
             $o.= '  <tr><td class="generalboxcontent">';
             $o.= '    <p class="centerpara">' . $this->str['confirmdeletetable'] . '<br /><br />' . $tableparam . '</p>';
             $o.= '    <table class="boxaligncenter" cellpadding="20"><tr><td>';
index 02a2bc4..05cdd79 100644 (file)
@@ -71,7 +71,7 @@ class delete_xml_file extends XMLDBAction {
 
         // If  not confirmed, show confirmation box
         if (!$confirmed) {
-            $o = '<table width="60" class="generalbox" border="0" cellpadding="5" cellspacing="0" id="notice">';
+            $o = '<table width="60" class="generaltable" border="0" cellpadding="5" cellspacing="0" id="notice">';
             $o.= '  <tr><td class="generalboxcontent">';
             $o.= '    <p class="centerpara">' . $this->str['confirmdeletexmlfile'] . '<br /><br />' . $dirpath . '/install.php</p>';
             $o.= '    <table class="boxaligncenter" cellpadding="20"><tr><td>';
index a9d1ef9..1649d36 100644 (file)
@@ -71,7 +71,7 @@ class revert_changes extends XMLDBAction {
 
         // If  not confirmed, show confirmation box
         if (!$confirmed) {
-            $o = '<table width="60" class="generalbox boxaligncenter" border="0" cellpadding="5" cellspacing="0" id="notice">';
+            $o = '<table width="60" class="generaltable boxaligncenter" border="0" cellpadding="5" cellspacing="0" id="notice">';
             $o.= '  <tr><td class="generalboxcontent">';
             $o.= '    <p class="centerpara">' . $this->str['confirmrevertchanges'] . '<br /><br />' . $dirpath . '</p>';
             $o.= '    <table class="boxaligncenter" cellpadding="20"><tr><td>';
index d762f42..dce490b 100644 (file)
@@ -18,7 +18,7 @@
  * block_online_users data generator
  *
  * @package    block_online_users
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -27,14 +27,14 @@ defined('MOODLE_INTERNAL') || die();
 
 
 /**
- * Page module PHPUnit data generator class
+ * Online users block data generator class
  *
- * @package    mod_page
- * @category   phpunit
+ * @package    block_online_users
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class block_online_users_generator extends phpunit_block_generator {
+class block_online_users_generator extends testing_block_generator {
 
     /**
      * Create new block instance
index dc56f91..ecc1af2 100644 (file)
@@ -119,6 +119,27 @@ function calendar_get_days() {
     return array('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday');
 }
 
+/**
+ * Get the subscription from a given id
+ *
+ * @since Moodle 2.5
+ * @param int $id id of the subscription
+ * @return stdClass Subscription record from DB
+ * @throws moodle_exception for an invalid id
+ */
+function calendar_get_subscription($id) {
+    global $DB;
+
+    $cache = cache::make('core', 'calendar_subscriptions');
+    $subscription = $cache->get($id);
+    if (empty($subscription)) {
+        $subscription = $DB->get_record('event_subscriptions', array('id' => $id), '*', MUST_EXIST);
+        // cache the data.
+        $cache->set($id, $subscription);
+    }
+    return $subscription;
+}
+
 /**
  * Gets the first day of the week
  *
@@ -314,7 +335,7 @@ function calendar_get_mini($courses, $groups, $users, $cal_month = false, $cal_y
                 if (!isset($events[$eventid])) {
                     continue;
                 }
-                $event = $events[$eventid];
+                $event = new calendar_event($events[$eventid]);
                 $popupalt  = '';
                 $component = 'moodle';
                 if(!empty($event->modulename)) {
@@ -335,7 +356,15 @@ function calendar_get_mini($courses, $groups, $users, $cal_month = false, $cal_y
 
                 $popupcontent .= html_writer::start_tag('div');
                 $popupcontent .= $OUTPUT->pix_icon($popupicon, $popupalt, $component);
-                $popupcontent .= html_writer::link($dayhref, format_string($event->name, true));
+                $name = format_string($event->name, true);
+                // Show ical source if needed.
+                if (!empty($event->subscription) && $CFG->calendar_showicalsource) {
+                    $a = new stdClass();
+                    $a->name = $name;
+                    $a->source = $event->subscription->name;
+                    $name = get_string('namewithsource', 'calendar', $a);
+                }
+                $popupcontent .= html_writer::link($dayhref, $name);
                 $popupcontent .= html_writer::end_tag('div');
             }
 
@@ -1910,6 +1939,10 @@ class calendar_event {
             $data->id = null;
         }
 
+        if (!empty($data->subscriptionid)) {
+            $data->subscription = calendar_get_subscription($data->subscriptionid);
+        }
+
         // Default to a user event
         if (empty($data->eventtype)) {
             $data->eventtype = 'user';
@@ -2743,12 +2776,18 @@ function calendar_add_subscription($sub) {
         $sub->pollinterval = 0;
     }
 
+    $cache = cache::make('core', 'calendar_subscriptions');
+
     if (!empty($sub->name)) {
         if (empty($sub->id)) {
             $id = $DB->insert_record('event_subscriptions', $sub);
+            // we cannot cache the data here because $sub is not complete.
             return $id;
         } else {
+            // Why are we doing an update here?
             $DB->update_record('event_subscriptions', $sub);
+            // update cache.
+            $cache->set($sub->id, $sub);
             return $sub->id;
         }
     } else {
@@ -2762,9 +2801,10 @@ function calendar_add_subscription($sub) {
  * @param object $event The RFC-2445 iCalendar event
  * @param int $courseid The course ID
  * @param int $subscriptionid The iCalendar subscription ID
+ * @throws dml_exception A DML specific exception is thrown for invalid subscriptionids.
  * @return int Code: 1=updated, 2=inserted, 0=error
  */
-function calendar_add_icalendar_event($event, $courseid, $subscriptionid = null) {
+function calendar_add_icalendar_event($event, $courseid, $subscriptionid) {
     global $DB, $USER;
 
     // Probably an unsupported X-MICROSOFT-CDO-BUSYSTATUS event.
@@ -2805,16 +2845,13 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid = null)
     $eventrecord->timemodified = time();
 
     // Add the iCal subscription details if required.
-    if ($sub = $DB->get_record('event_subscriptions', array('id' => $subscriptionid))) {
-        $eventrecord->subscriptionid = $subscriptionid;
-        $eventrecord->userid = $sub->userid;
-        $eventrecord->groupid = $sub->groupid;
-        $eventrecord->courseid = $sub->courseid;
-        $eventrecord->eventtype = $sub->eventtype;
-    } else {
-        // We should never do anything with an event without a subscription reference.
-        return 0;
-    }
+    // We should never do anything with an event without a subscription reference.
+    $sub = calendar_get_subscription($subscriptionid);
+    $eventrecord->subscriptionid = $subscriptionid;
+    $eventrecord->userid = $sub->userid;
+    $eventrecord->groupid = $sub->groupid;
+    $eventrecord->courseid = $sub->courseid;
+    $eventrecord->eventtype = $sub->eventtype;
 
     if ($updaterecord = $DB->get_record('event', array('uuid' => $eventrecord->uuid))) {
         $eventrecord->id = $updaterecord->id;
@@ -2838,20 +2875,18 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid = null)
  * @param int $subscriptionid The ID of the subscription we are acting upon.
  * @param int $pollinterval The poll interval to use.
  * @param int $action The action to be performed. One of update or remove.
+ * @throws dml_exception if invalid subscriptionid is provided
  * @return string A log of the import progress, including errors
  */
 function calendar_process_subscription_row($subscriptionid, $pollinterval, $action) {
     global $DB;
 
-    if (empty($subscriptionid)) {
-        return '';
-    }
-
     // Fetch the subscription from the database making sure it exists.
-    $sub = $DB->get_record('event_subscriptions', array('id' => $subscriptionid), '*', MUST_EXIST);
+    $sub = calendar_get_subscription($subscriptionid);
 
     $strupdate = get_string('update');
     $strremove = get_string('remove');
+
     // Update or remove the subscription, based on action.
     switch ($action) {
         case $strupdate:
@@ -2862,12 +2897,15 @@ function calendar_process_subscription_row($subscriptionid, $pollinterval, $acti
             $sub->pollinterval = $pollinterval;
             $DB->update_record('event_subscriptions', $sub);
 
+            // update the cache.
+            $cache = cache::make('core', 'calendar_subscriptions');
+            $cache->set($sub->id, $sub);
+
             // Update the events.
             return "<p>".get_string('subscriptionupdated', 'calendar', $sub->name)."</p>" . calendar_update_subscription_events($subscriptionid);
 
         case $strremove:
-            $DB->delete_records('event', array('subscriptionid' => $subscriptionid));
-            $DB->delete_records('event_subscriptions', array('id' => $subscriptionid));
+            calendar_delete_subscription($subscriptionid);
             return get_string('subscriptionremoved', 'calendar', $sub->name);
             break;
 
@@ -2877,6 +2915,22 @@ function calendar_process_subscription_row($subscriptionid, $pollinterval, $acti
     return '';
 }
 
+/**
+ * Delete subscription and all related events.
+ *
+ * @param int|stdClass $subscription subscription or it's id, which needs to be deleted.
+ */
+function calendar_delete_subscription($subscription) {
+    global $DB;
+
+    if (is_object($subscription)) {
+        $subscription = $subscription->id;
+    }
+    // Delete subscription and related events.
+    $DB->delete_records('event', array('subscriptionid' => $subscription));
+    $DB->delete_records('event_subscriptions', array('id' => $subscription));
+    cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription));
+}
 /**
  * From a URL, fetch the calendar and return an iCalendar object.
  *
@@ -2962,10 +3016,7 @@ function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = nu
 function calendar_update_subscription_events($subscriptionid) {
     global $DB;
 
-    $sub = $DB->get_record('event_subscriptions', array('id' => $subscriptionid));
-    if (empty($sub)) {
-        print_error('errorbadsubscription', 'calendar');
-    }
+    $sub = calendar_get_subscription($subscriptionid);
     // Don't update a file subscription. TODO: Update from a new uploaded file.
     if (empty($sub->url)) {
         return 'File subscription not updated.';
@@ -2974,6 +3025,9 @@ function calendar_update_subscription_events($subscriptionid) {
     $return = calendar_import_icalendar_events($ical, $sub->courseid, $subscriptionid);
     $sub->lastupdated = time();
     $DB->update_record('event_subscriptions', $sub);
+    // Update the cache.
+    $cache = cache::make('core', 'calendar_subscriptions');
+    $cache->set($subscriptionid, $sub);
     return $return;
 }
 
@@ -2991,7 +3045,7 @@ function calendar_can_edit_subscription($subscriptionorid) {
     } else if (is_object($subscriptionorid)) {
         $subscription = $subscriptionorid;
     } else {
-        $subscription = $DB->get_record('event_subscriptions', array('id' => $subscriptionorid), '*', MUST_EXIST);
+        $subscription = calendar_get_subscription($subscriptionorid);
     }
     $allowed = new stdClass;
     $courseid = $subscription->courseid;
index cc726f1..07265bc 100644 (file)
@@ -75,7 +75,13 @@ if (!empty($formdata)) {
         $ical->unserialize($calendar);
         $importresults = calendar_import_icalendar_events($ical, $courseid, $subscriptionid);
     } else {
-        $importresults = calendar_update_subscription_events($subscriptionid);
+        try {
+            $importresults = calendar_update_subscription_events($subscriptionid);
+        } catch (moodle_exception $e) {
+            // Delete newly added subscription and show invalid url error.
+            calendar_delete_subscription($subscriptionid);
+            print_error($e->errorcode, $e->module, $PAGE->url);
+        }
     }
     // Redirect to prevent refresh issues.
     redirect($PAGE->url, $importresults);
@@ -83,7 +89,12 @@ if (!empty($formdata)) {
     // The user is wanting to perform an action upon an existing subscription.
     require_sesskey(); // Must have sesskey for all actions.
     if (calendar_can_edit_subscription($subscriptionid)) {
-        $importresults = calendar_process_subscription_row($subscriptionid, $pollinterval, $action);
+        try {
+            $importresults = calendar_process_subscription_row($subscriptionid, $pollinterval, $action);
+        } catch (moodle_exception $e) {
+            // If exception caught, then user should be redirected to page where he/she came from.
+            print_error($e->errorcode, $e->module, $PAGE->url);
+        }
     } else {
         print_error('nopermissions', 'error', $PAGE->url, get_string('managesubscriptions', 'calendar'));
     }
index 28344cd..a969fbd 100644 (file)
@@ -123,7 +123,9 @@ class calendar_addsubscription_form extends moodleform {
                 }
             }
         } else if (($data['importfrom'] == CALENDAR_IMPORT_FROM_URL)) {
-            if (clean_param($data['url'], PARAM_URL) !== $data['url']) {
+            // Clean input calendar url.
+            $url = clean_param($data['url'], PARAM_URL);
+            if (empty($url) || ($url !== $data['url'])) {
                 $errors['url']  = get_string('invalidurl', 'error');
             }
         } else {
index f5e6885..838a92a 100644 (file)
@@ -292,6 +292,8 @@ class core_calendar_renderer extends plugin_renderer_base {
      * @return string
      */
     public function event(calendar_event $event, $showactions=true) {
+        global $CFG;
+
         $event = calendar_add_event_metadata($event);
 
         $anchor  = html_writer::tag('a', '', array('name'=>'event_'.$event->id));
@@ -320,6 +322,16 @@ class core_calendar_renderer extends plugin_renderer_base {
         if (!empty($event->courselink)) {
             $table->data[0]->cells[1]->text .= html_writer::tag('div', $event->courselink, array('class'=>'course'));
         }
+        // Show subscription source if needed.
+        if (!empty($event->subscription) && $CFG->calendar_showicalsource) {
+            if (!empty($event->subscription->url)) {
+                $source = html_writer::link($event->subscription->url, get_string('subsource', 'calendar', $event->subscription));
+            } else {
+                // File based ical.
+                $source = get_string('subsource', 'calendar', $event->subscription);
+            }
+            $table->data[0]->cells[1]->text .= html_writer::tag('div', $source, array('class' => 'subscription'));
+        }
         if (!empty($event->time)) {
             $table->data[0]->cells[1]->text .= html_writer::tag('span', $event->time, array('class'=>'date'));
         } else {
index ff3607c..673f73d 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /calendar/* ,
 information provided here is intended especially for developers.
 
+=== 2.5 ===
+required changes in code:
+* calendar_add_icalendar_event() now requires a valid subscriptionid
+* calendar_process_subscription_row() throws exception for invalid subscriptionid
+* calendar_update_subscription_events() now throws a dml_exception instead of moodle_exception for bad subscriptions
 
 === 2.4 ===
 
index 92268fd..64dde6a 100644 (file)
@@ -2149,6 +2149,13 @@ function set_coursemodule_visible($id, $visible) {
     if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) {
         return false;
     }
+
+    // Create events and propagate visibility to associated grade items if the value has changed.
+    // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
+    if ($cm->visible == $visible) {
+        return true;
+    }
+
     if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) {
         return false;
     }
@@ -2162,7 +2169,7 @@ function set_coursemodule_visible($id, $visible) {
         }
     }
 
-    // hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there
+    // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
     $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course));
     if ($grade_items) {
         foreach ($grade_items as $grade_item) {
@@ -2418,7 +2425,7 @@ function reorder_sections($sections, $origin_position, $target_position) {
  * All parameters are objects
  */
 function moveto_module($mod, $section, $beforemod=NULL) {
-    global $OUTPUT;
+    global $OUTPUT, $DB;
 
 /// Remove original module from original section
     if (! delete_mod_from_section($mod->id, $mod->section)) {
@@ -2427,7 +2434,16 @@ function moveto_module($mod, $section, $beforemod=NULL) {
 
     // if moving to a hidden section then hide module
     if (!$section->visible && $mod->visible) {
+        // Set this in the object because it is sent as a response to ajax calls.
+        $mod->visible = 0;
         set_coursemodule_visible($mod->id, 0);
+        // Set visibleold to 1 so module will be visible when section is made visible.
+        $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id));
+    }
+    if ($section->visible && !$mod->visible) {
+        set_coursemodule_visible($mod->id, $mod->visibleold);
+        // Set this in the object because it is sent as a response to ajax calls.
+        $mod->visible = $mod->visibleold;
     }
 
 /// Add the module into the new section
@@ -3753,16 +3769,6 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
             )), null, true);
     }
 
-    // Include blocks dragdrop
-    $params = array(
-        'courseid' => $course->id,
-        'pagetype' => $PAGE->pagetype,
-        'pagelayout' => $PAGE->pagelayout,
-        'subpage' => $PAGE->subpage,
-        'regions' => $PAGE->blocks->get_regions(),
-    );
-    $PAGE->requires->yui_module('moodle-core-blocks', 'M.core_blocks.init_dragdrop', array($params), null, true);
-
     // Require various strings for the command toolbox
     $PAGE->requires->strings_for_js(array(
             'moveleft',
index 56bf28a..c08108a 100644 (file)
@@ -480,6 +480,7 @@ if ($mform->is_cancelled()) {
         // make sure visibility is set correctly (in particular in calendar)
         // note: allow them to set it even without moodle/course:activityvisibility
         set_coursemodule_visible($fromform->coursemodule, $fromform->visible);
+        $DB->set_field('course_modules', 'visibleold', 1, array('id' => $fromform->coursemodule));
 
         if (isset($fromform->cmidnumber)) { //label
             // set cm idnumber - uniqueness is already verified by form validation
index 8056c57..6272903 100644 (file)
@@ -134,6 +134,7 @@ switch($requestmethod) {
                         }
 
                         moveto_module($cm, $section, $beforemod);
+                        echo json_encode(array('visible' => $cm->visible));
                         break;
                     case 'gettitle':
                         require_capability('moodle/course:manageactivities', $modcontext);
index 96b8f1b..6872ece 100644 (file)
@@ -385,6 +385,9 @@ YUI.add('moodle-course-dragdrop', function(Y) {
                         spinner.show();
                     },
                     success: function(tid, response) {
+                        var responsetext = Y.JSON.parse(response.responseText);
+                        var params = {element: dragnode, visible: responsetext.visible};
+                        M.course.coursebase.invoke_function('set_visibility_resource_ui', params);
                         this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
                         window.setTimeout(function(e) {
                             spinner.hide();
index 23a61a3..8c652e0 100644 (file)
@@ -592,6 +592,26 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 }
             }, this);
             listenevents.push(thisevent);
+        },
+        /**
+         * Set the visibility of the current resource (identified by the element)
+         * to match the hidden parameter (this is not a toggle).
+         * Only changes the visibility in the browser (no ajax update).
+         * @param args An object with 'element' being the A node containing the resource
+         *             and 'visible' being the state that the visiblity should be set to.
+         * @return void
+         */
+        set_visibility_resource_ui: function(args) {
+            var element = args.element;
+            var shouldbevisible = args.visible;
+            var buttonnode = element.one(CSS.SHOW);
+            var visible = (buttonnode === null);
+            if (visible) {
+                buttonnode = element.one(CSS.HIDE);
+            }
+            if (visible != shouldbevisible) {
+                this.toggle_hide_resource_ui(buttonnode);
+            }
         }
     }, {
         NAME : 'course-resource-toolbox',
index c4a222e..5e947b6 100644 (file)
@@ -875,7 +875,9 @@ class course_enrolment_manager {
                         break;
                 }
             }
-            $users[$userrole->id]['roles'] = array();
+            if (!isset($users[$userrole->id]['roles'])) {
+                $users[$userrole->id]['roles'] = array();
+            }
             $users[$userrole->id]['roles'][$userrole->roleid] = array(
                 'text' => $roletext,
                 'unchangeable' => !$changeable
index 0715c6b..53c74f0 100644 (file)
@@ -305,7 +305,10 @@ class core_files_renderer extends plugin_renderer_base {
     private function fm_js_template_mkdir() {
         $rv = '
 <div class="filemanager fp-mkdir-dlg">
-    <div class="fp-mkdir-dlg-text">'.get_string('newfoldername','repository').'<br/><input type="text" /></div>
+    <div class="fp-mkdir-dlg-text">
+        <label>' . get_string('newfoldername', 'repository') . '</label><br/>
+        <input type="text" />
+    </div>
     <button class="{!}fp-dlg-butcreate">'.get_string('makeafolder').'</button>
     <button class="{!}fp-dlg-butcancel">'.get_string('cancel').'</button>
 </div>';
index 089b168..2890159 100644 (file)
@@ -181,7 +181,7 @@ class grade_report_grader extends grade_report {
         // Were any changes made?
         $changedgrades = false;
 
-        foreach ($data as $varname => $postedvalue) {
+        foreach ($data as $varname => $students) {
 
             $needsupdate = false;
 
@@ -194,113 +194,116 @@ class grade_report_grader extends grade_report {
                 continue;
             }
 
-            $gradeinfo = explode("_", $varname);
-            $userid = clean_param($gradeinfo[1], PARAM_INT);
-            $itemid = clean_param($gradeinfo[2], PARAM_INT);
+            foreach ($students as $userid => $items) {
+                $userid = clean_param($userid, PARAM_INT);
+                foreach ($items as $itemid => $postedvalue) {
+                    $itemid = clean_param($itemid, PARAM_INT);
+
+                    // Was change requested?
+                    $oldvalue = $this->grades[$userid][$itemid];
+                    if ($datatype === 'grade') {
+                        // If there was no grade and there still isn't
+                        if (is_null($oldvalue->finalgrade) && $postedvalue == -1) {
+                            // -1 means no grade
+                            continue;
+                        }
 
-            // Was change requested?
-            $oldvalue = $this->grades[$userid][$itemid];
-            if ($datatype === 'grade') {
-                // If there was no grade and there still isn't
-                if (is_null($oldvalue->finalgrade) && $postedvalue == -1) {
-                    // -1 means no grade
-                    continue;
-                }
+                        // If the grade item uses a custom scale
+                        if (!empty($oldvalue->grade_item->scaleid)) {
 
-                // If the grade item uses a custom scale
-                if (!empty($oldvalue->grade_item->scaleid)) {
+                            if ((int)$oldvalue->finalgrade === (int)$postedvalue) {
+                                continue;
+                            }
+                        } else {
+                            // The grade item uses a numeric scale
 
-                    if ((int)$oldvalue->finalgrade === (int)$postedvalue) {
-                        continue;
-                    }
-                } else {
-                    // The grade item uses a numeric scale
+                            // Format the finalgrade from the DB so that it matches the grade from the client
+                            if ($postedvalue === format_float($oldvalue->finalgrade, $oldvalue->grade_item->get_decimals())) {
+                                continue;
+                            }
+                        }
 
-                    // Format the finalgrade from the DB so that it matches the grade from the client
-                    if ($postedvalue === format_float($oldvalue->finalgrade, $oldvalue->grade_item->get_decimals())) {
-                        continue;
+                        $changedgrades = true;
+
+                    } else if ($datatype === 'feedback') {
+                        if (($oldvalue->feedback === $postedvalue) or ($oldvalue->feedback === NULL and empty($postedvalue))) {
+                            continue;
+                        }
                     }
-                }
 
-                $changedgrades = true;
+                    if (!$gradeitem = grade_item::fetch(array('id'=>$itemid, 'courseid'=>$this->courseid))) {
+                        print_error('invalidgradeitemid');
+                    }
 
-            } else if ($datatype === 'feedback') {
-                if (($oldvalue->feedback === $postedvalue) or ($oldvalue->feedback === NULL and empty($postedvalue))) {
-                    continue;
-                }
-            }
+                    // Pre-process grade
+                    if ($datatype == 'grade') {
+                        $feedback = false;
+                        $feedbackformat = false;
+                        if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+                            if ($postedvalue == -1) { // -1 means no grade
+                                $finalgrade = null;
+                            } else {
+                                $finalgrade = $postedvalue;
+                            }
+                        } else {
+                            $finalgrade = unformat_float($postedvalue);
+                        }
 
-            if (!$gradeitem = grade_item::fetch(array('id'=>$itemid, 'courseid'=>$this->courseid))) { // we must verify course id here!
-                print_error('invalidgradeitemid');
-            }
+                        $errorstr = '';
+                        // Warn if the grade is out of bounds.
+                        if (is_null($finalgrade)) {
+                            // ok
+                        } else {
+                            $bounded = $gradeitem->bounded_grade($finalgrade);
+                            if ($bounded > $finalgrade) {
+                            $errorstr = 'lessthanmin';
+                            } else if ($bounded < $finalgrade) {
+                                $errorstr = 'morethanmax';
+                            }
+                        }
+                        if ($errorstr) {
+                            $user = $DB->get_record('user', array('id' => $userid), 'id, firstname, lastname');
+                            $gradestr = new stdClass();
+                            $gradestr->username = fullname($user);
+                            $gradestr->itemname = $gradeitem->get_name();
+                            $warnings[] = get_string($errorstr, 'grades', $gradestr);
+                        }
 
-            // Pre-process grade
-            if ($datatype == 'grade') {
-                $feedback = false;
-                $feedbackformat = false;
-                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
-                    if ($postedvalue == -1) { // -1 means no grade
-                        $finalgrade = null;
-                    } else {
-                        $finalgrade = $postedvalue;
+                    } else if ($datatype == 'feedback') {
+                        $finalgrade = false;
+                        $trimmed = trim($postedvalue);
+                        if (empty($trimmed)) {
+                             $feedback = NULL;
+                        } else {
+                             $feedback = $postedvalue;
+                        }
                     }
-                } else {
-                    $finalgrade = unformat_float($postedvalue);
-                }
 
-                $errorstr = '';
-                // Warn if the grade is out of bounds.
-                if (is_null($finalgrade)) {
-                    // ok
-                } else {
-                    $bounded = $gradeitem->bounded_grade($finalgrade);
-                    if ($bounded > $finalgrade) {
-                    $errorstr = 'lessthanmin';
-                    } else if ($bounded < $finalgrade) {
-                        $errorstr = 'morethanmax';
+                    // group access control
+                    if ($separategroups) {
+                        // note: we can not use $this->currentgroup because it would fail badly
+                        //       when having two browser windows each with different group
+                        $sharinggroup = false;
+                        foreach($mygroups as $groupid) {
+                            if (groups_is_member($groupid, $userid)) {
+                                $sharinggroup = true;
+                                break;
+                            }
+                        }
+                        if (!$sharinggroup) {
+                            // either group membership changed or somebody is hacking grades of other group
+                            $warnings[] = get_string('errorsavegrade', 'grades');
+                            continue;
+                        }
                     }
-                }
-                if ($errorstr) {
-                    $user = $DB->get_record('user', array('id' => $userid), 'id, firstname, lastname');
-                    $gradestr = new stdClass();
-                    $gradestr->username = fullname($user);
-                    $gradestr->itemname = $gradeitem->get_name();
-                    $warnings[] = get_string($errorstr, 'grades', $gradestr);
-                }
 
-            } else if ($datatype == 'feedback') {
-                $finalgrade = false;
-                $trimmed = trim($postedvalue);
-                if (empty($trimmed)) {
-                     $feedback = NULL;
-                } else {
-                     $feedback = $postedvalue;
-                }
-            }
+                    $gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE);
 
-            // group access control
-            if ($separategroups) {
-                // note: we can not use $this->currentgroup because it would fail badly
-                //       when having two browser windows each with different group
-                $sharinggroup = false;
-                foreach($mygroups as $groupid) {
-                    if (groups_is_member($groupid, $userid)) {
-                        $sharinggroup = true;
-                        break;
+                    // We can update feedback without reloading the grade item as it doesn't affect grade calculations
+                    if ($datatype === 'feedback') {
+                        $this->grades[$userid][$itemid]->feedback = $feedback;
                     }
                 }
-                if (!$sharinggroup) {
-                    // either group membership changed or somebody is hacking grades of other group
-                    $warnings[] = get_string('errorsavegrade', 'grades');
-                    continue;
-                }
-            }
-
-            $gradeitem->update_final_grade($userid, $finalgrade, 'gradebook', $feedback, FORMAT_MOODLE);
-
-            // We can update feedback without reloading the grade item as it doesn't affect grade calculations
-            if ($datatype === 'feedback') {
-                $this->grades[$userid][$itemid]->feedback = $feedback;
             }
         }
 
@@ -1018,7 +1021,7 @@ class grade_report_grader extends grade_report {
                             }
                             $attributes = array('tabindex' => $tabindices[$item->id]['grade'], 'id'=>'grade_'.$userid.'_'.$item->id);
                             $itemcell->text .= html_writer::label(get_string('typescale', 'grades'), $attributes['id'], false, array('class' => 'accesshide'));
-                            $itemcell->text .= html_writer::select($scaleopt, 'grade_'.$userid.'_'.$item->id, $gradeval, array(-1=>$nogradestr), $attributes);
+                            $itemcell->text .= html_writer::select($scaleopt, 'grade['.$userid.']['.$item->id.']', $gradeval, array(-1=>$nogradestr), $attributes);
                         } elseif(!empty($scale)) {
                             $scales = explode(",", $scale->scale);
 
@@ -1040,8 +1043,8 @@ class grade_report_grader extends grade_report {
                             $itemcell->text .= '<label class="accesshide" for="grade_'.$userid.'_'.$item->id.'">'
                                           .get_string('useractivitygrade', 'gradereport_grader', $gradelabel).'</label>';
                             $itemcell->text .= '<input size="6" tabindex="' . $tabindices[$item->id]['grade']
-                                          . '" type="text" class="text" title="'. $strgrade .'" name="grade_'
-                                          .$userid.'_' .$item->id.'" id="grade_'.$userid.'_'.$item->id.'" value="'.$value.'" />';
+                                          . '" type="text" class="text" title="'. $strgrade .'" name="grade['
+                                          .$userid.'][' .$item->id.']" id="grade_'.$userid.'_'.$item->id.'" value="'.$value.'" />';
                         } else {
                             $itemcell->text .= html_writer::tag('span', format_float($gradeval, $decimalpoints), array('class'=>"gradevalue$hidden$gradepass"));
                         }
@@ -1054,7 +1057,7 @@ class grade_report_grader extends grade_report {
                         $itemcell->text .= '<label class="accesshide" for="feedback_'.$userid.'_'.$item->id.'">'
                                       .get_string('useractivityfeedback', 'gradereport_grader', $feedbacklabel).'</label>';
                         $itemcell->text .= '<input class="quickfeedback" tabindex="' . $tabindices[$item->id]['feedback'].'" id="feedback_'.$userid.'_'.$item->id
-                                      . '" size="6" title="' . $strfeedback . '" type="text" name="feedback_'.$userid.'_'.$item->id.'" value="' . s($grade->feedback) . '" />';
+                                      . '" size="6" title="' . $strfeedback . '" type="text" name="feedback['.$userid.']['.$item->id.']" value="' . s($grade->feedback) . '" />';
                     }
 
                 } else { // Not editing
@@ -1738,25 +1741,21 @@ class grade_report_grader extends grade_report {
             // Will this number of students result in more fields that we are allowed?
             $maxinputvars = ini_get('max_input_vars');
             if ($maxinputvars !== false) {
-                $fieldspergradeitem = 0; // The number of fields output per grade item for each student
+                // We can't do anything about there being more grade items than max_input_vars,
+                // but we can decrease number of students per page if there are >= max_input_vars
+                $fieldsperstudent = 0; // The number of fields output per student
 
-                if ($this->get_pref('quickgrading')) {
-                    // One grade field
-                    $fieldspergradeitem ++;
-                }
-                if ($this->get_pref('showquickfeedback')) {
-                    // One feedback field
-                    $fieldspergradeitem ++;
+                if ($this->get_pref('quickgrading') || $this->get_pref('showquickfeedback')) {
+                    // Each array (grade, feedback) will gain one element
+                    $fieldsperstudent ++;
                 }
 
-                $fieldsperstudent = $fieldspergradeitem * count($this->gtree->get_items());
                 $fieldsrequired = $studentsperpage * $fieldsperstudent;
-                if ($fieldsrequired > $maxinputvars) {
-                    $studentsperpage = floor($maxinputvars / $fieldsperstudent);
+                if ($fieldsrequired >= $maxinputvars) {
+                    $studentsperpage = $maxinputvars - 1; // Subtract one to be on the safe side
                     if ($studentsperpage<1) {
-                        // Make sure students per page doesn't fall below 1
-                        // PHP max_input_vars could potentially be reached with 1 student
-                        // if there are >500 grade items and quickgrading and showquickfeedback are on
+                        // Make sure students per page doesn't fall below 1, though if your
+                        // max_input_vars is only 1 you've got bigger problems!
                         $studentsperpage = 1;
                     }
 
index 977e305..e5c8380 100644 (file)
@@ -586,7 +586,7 @@ background-color:#f3ead8;
 
 .path-grade-report-grader .grade_icons { margin-bottom: .3em;}
 
-.path-grade-report-grader .yui-overlay {
+.path-grade-report-grader .yui3-overlay {
     background-color: #FFEE69;
     border-color: #D4C237 #A6982B #A6982B;
     border-style: solid;
@@ -596,15 +596,15 @@ background-color:#f3ead8;
     font-size: 0.7em;
 }
 
-.path-grade-report-grader .yui-overlay .fullname {
+.path-grade-report-grader .yui3-overlay .fullname {
     color: #5F3E00;
     font-weight: bold;
 }
-.path-grade-report-grader .yui-overlay .itemname {
+.path-grade-report-grader .yui3-overlay .itemname {
     color: #194F3E;
     font-weight: bold;
 }
-.path-grade-report-grader .yui-overlay .feedback {
+.path-grade-report-grader .yui3-overlay .feedback {
     color: #5F595E;
 }
 /* table#user-grades td */
@@ -613,7 +613,7 @@ background-color:#f3ead8;
   text-align: left;
 }
 
-.path-grade-report-grader .yui-overlay a.container-close {
+.path-grade-report-grader .yui3-overlay a.container-close {
   margin-top: -3px;
 }
 
index 8178322..df948f3 100644 (file)
@@ -310,6 +310,7 @@ $string['configsessioncookie'] = 'This setting customises the name of the cookie
 $string['configsessioncookiedomain'] = 'This allows you to change the domain that the Moodle cookies are available from. This is useful for Moodle customisations (e.g. authentication or enrolment plugins) that need to share Moodle session information with a web application on another subdomain. <strong>WARNING: it is strongly recommended to leave this setting at the default (empty) - an incorrect value will prevent all logins to the site.</strong>';
 $string['configsessioncookiepath'] = 'If you need to change where browsers send the Moodle cookies, you can change this setting to specify a subdirectory of your web site.  Otherwise the default \'/\' should be fine.';
 $string['configsessiontimeout'] = 'If people logged in to this site are idle for a long time (without loading pages) then they are automatically logged out (their session is ended).  This variable specifies how long this time should be.';
+$string['configshowicalsource'] = 'Show source information for ical events.';
 $string['configshowcommentscount'] = 'Show comments count, it will cost one more query when display comments link';
 $string['configshowsiteparticipantslist'] = 'All of these site students and site teachers will be listed on the site participants list. Who shall be allowed to see this site participants list?';
 $string['configsitedefaultlicense'] = 'Default site licence';
@@ -568,6 +569,7 @@ $string['helpcalendarcustomexport'] = 'Enable custom date range export option in
 $string['helpexportlookahead'] = 'How many days in the future does the calendar look for events during export for the custom export option?';
 $string['helpexportlookback'] = 'How many days in the past does the calendar look for events during export for the custom export option?';
 $string['helpforcetimezone'] = 'You can allow users to individually select their timezone, or force a timezone for everyone.';
+$string['helpshowicalsource'] = 'Enable this setting if you want to show the ical subscription name and link for ical imported events.';
 $string['helpsitemaintenance'] = 'For upgrades and other work';
 $string['helpstartofweek'] = 'Which day starts the week in the calendar?';
 $string['helpupcominglookahead'] = 'How many days in the future does the calendar look for upcoming events by default?';
index 78c2fb9..59ea201 100644 (file)
@@ -126,6 +126,7 @@ $string['monthly'] = 'Monthly';
 $string['monthlyview'] = 'Monthly view';
 $string['monthnext'] = 'Next month';
 $string['monththis'] = 'This month';
+$string['namewithsource'] = '{$a->name}({$a->source})';
 $string['never'] = 'Never';
 $string['newevent'] = 'New event';
 $string['notitle'] = 'no title';
@@ -168,6 +169,7 @@ $string['subscriptions'] = 'Subscriptions';
 $string['subscriptionname'] = 'Calendar name';
 $string['subscriptionremoved'] = 'Calendar subscription {$a} removed';
 $string['subscriptionupdated'] = 'Calendar subscription {$a} updated';
+$string['subsource'] = 'Event source: {$a->name}';
 $string['sun'] = 'Sun';
 $string['sunday'] = 'Sunday';
 $string['thu'] = 'Thu';
index 135c3fb..3dea4dd 100644 (file)
@@ -183,13 +183,13 @@ $string['invalidcategoryidforparent'] = 'Invalid category id for parent!';
 $string['invalidcategoryidtomove'] = 'Invalid category id to move!';
 $string['invalidconfirm'] = 'Confirmation string was incorrect';
 $string['invalidcontextinhasanyquestions'] = 'Invalid context passed to question_context_has_any_questions.';
+$string['invalidgrade'] = 'Grades ({$a}) do not match grade options - question skipped.';
 $string['invalidpenalty'] = 'Invalid penalty';
 $string['invalidwizardpage'] = 'Incorrect or no wizard page specified!';
 $string['lastmodifiedby'] = 'Last modified by';
 $string['linkedfiledoesntexist'] = 'Linked file {$a} doesn\'t exist';
 $string['makechildof'] = 'Make child of \'{$a}\'';
 $string['maketoplevelitem'] = 'Move to top level';
-$string['matcherror'] = 'Grades do not match grade options - question skipped';
 $string['matchgrades'] = 'Match grades';
 $string['matchgradeserror'] = 'Error if grade not listed';
 $string['matchgradesnearest'] = 'Nearest grade if not listed';
index 470106d..8615405 100644 (file)
@@ -53,6 +53,20 @@ require_sesskey();
 // Setting layout to replicate blocks configuration for the page we edit
 $PAGE->set_pagelayout($pagelayout);
 $PAGE->set_subpage($subpage);
+$pagetype = explode('-', $pagetype);
+switch ($pagetype[0]) {
+    case 'admin':
+        // Admin pages need to be in the system context, not Site Course context.
+        $PAGE->set_context(context_system::instance());
+        break;
+    case 'my':
+        // My Home page needs to be in user context, and to have 'content' block region set up.
+        $PAGE->set_context(context_user::instance($USER->id));
+        $PAGE->set_blocks_editing_capability('moodle/my:manageblocks');
+        $PAGE->blocks->add_region('content');
+        break;
+}
+
 echo $OUTPUT->header(); // send headers
 
 switch ($action) {
index 4da22fa..235c4be 100644 (file)
@@ -1088,7 +1088,7 @@ class completion_info {
      * @return bool
      */
     public function is_tracked_user($userid) {
-        return is_enrolled(context_course::instance($this->course->id), $userid, '', true);
+        return is_enrolled(context_course::instance($this->course->id), $userid, 'moodle/course:isincompletionreports', true);
     }
 
     /**
@@ -1105,7 +1105,7 @@ class completion_info {
         global $DB;
 
         list($enrolledsql, $enrolledparams) = get_enrolled_sql(
-                context_course::instance($this->course->id), '', $groupid, true);
+                context_course::instance($this->course->id), 'moodle/course:isincompletionreports', $groupid, true);
         $sql  = 'SELECT COUNT(eu.id) FROM (' . $enrolledsql . ') eu JOIN {user} u ON u.id = eu.id';
         if ($where) {
             $sql .= " WHERE $where";
index 09a00fb..6299390 100644 (file)
@@ -104,5 +104,12 @@ $definitions = array(
         'simpledata' => true, // Array of stdClass objects containing only strings.
         'persist' => true, // Likely there will be a couple of calls to this.
         'persistmaxsize' => 2, // The original cache used 1, we've increased that to two.
+    ),
+     // Used to cache calendar subscriptions.
+    'calendar_subscriptions' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'persistent' => true,
     )
 );
index 6ffbcbe..3dc9c55 100644 (file)
@@ -408,6 +408,7 @@ abstract class moodle_database {
             // free memory
             $this->last_sql    = null;
             $this->last_params = null;
+            $this->print_debug_time();
             return;
         }
 
@@ -415,7 +416,6 @@ abstract class moodle_database {
         $type   = $this->last_type;
         $sql    = $this->last_sql;
         $params = $this->last_params;
-        $time   = microtime(true) - $this->last_time;
         $error  = $this->get_last_error();
 
         $this->query_log($error);
@@ -520,6 +520,25 @@ abstract class moodle_database {
         }
     }
 
+    /**
+     * Prints the time a query took to run.
+     * @return void
+     */
+    protected function print_debug_time() {
+        if (!$this->get_debug()) {
+            return;
+        }
+        $time = microtime(true) - $this->last_time;
+        $message = "Query took: {$time} seconds.\n";
+        if (CLI_SCRIPT) {
+            echo $message;
+            echo "--------------------------------\n";
+        } else {
+            echo s($message);
+            echo "<hr />\n";
+        }
+    }
+
     /**
      * Returns the SQL WHERE conditions.
      * @param string $table The table name that these conditions will be validated against.
index 0127895..e819824 100644 (file)
@@ -377,6 +377,9 @@ class mysqli_native_moodle_database extends moodle_database {
         if (empty($dbport)) {
             $dbport = 3306;
         }
+        if ($dbhost and !empty($this->dboptions['dbpersist'])) {
+            $dbhost = "p:$dbhost";
+        }
         ob_start();
         $this->mysqli = new mysqli($dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbsocket);
         $dberr = ob_get_contents();
index 0c4569e..ede90a5 100644 (file)
@@ -95,12 +95,12 @@ class tinymce_texteditor extends texteditor {
      * @param null $fpoptions
      */
     public function use_editor($elementid, array $options=null, $fpoptions=null) {
-        global $PAGE;
-        // Note: use full moodle_url instance to prevent standard JS loader.
+        global $PAGE, $CFG;
+        // Note: use full moodle_url instance to prevent standard JS loader, make sure we are using https on profile page if required.
         if (debugging('', DEBUG_DEVELOPER)) {
-            $PAGE->requires->js(new moodle_url('/lib/editor/tinymce/tiny_mce/'.$this->version.'/tiny_mce_src.js'));
+            $PAGE->requires->js(new moodle_url($CFG->httpswwwroot.'/lib/editor/tinymce/tiny_mce/'.$this->version.'/tiny_mce_src.js'));
         } else {
-            $PAGE->requires->js(new moodle_url('/lib/editor/tinymce/tiny_mce/'.$this->version.'/tiny_mce.js'));
+            $PAGE->requires->js(new moodle_url($CFG->httpswwwroot.'/lib/editor/tinymce/tiny_mce/'.$this->version.'/tiny_mce.js'));
         }
         $PAGE->requires->js_init_call('M.editor_tinymce.init_editor', array($elementid, $this->get_init_params($elementid, $options)), true);
         if ($fpoptions) {
index 328b695..013f6c5 100644 (file)
@@ -558,11 +558,11 @@ function file_get_user_used_space() {
  * @param string $str
  * @return string path
  */
-function file_correct_filepath($str) { //TODO: what is this? (skodak)
+function file_correct_filepath($str) { //TODO: what is this? (skodak) - No idea (Fred)
     if ($str == '/' or empty($str)) {
         return '/';
     } else {
-        return '/'.trim($str, './@#$ ').'/';
+        return '/'.trim($str, '/').'/';
     }
 }
 
index 86fc03a..19080d9 100644 (file)
@@ -319,6 +319,7 @@ M.form_filemanager.init = function(Y, options) {
                             on('keydown', function(e){
                                 if (e.keyCode == 13) {Y.bind(perform_action, this)(e);}
                             }, this);
+                        node.one('label').set('for', 'fm-newname-' + this.client_id);
                         node.all('.fp-dlg-butcancel').on('click', function(e){e.preventDefault();this.mkdir_dialog.hide();}, this);
                         node.all('.fp-dlg-curpath').set('id', 'fm-curpath-'+this.client_id);
                     }
index 6572ecd..2a5712e 100644 (file)
@@ -854,6 +854,16 @@ M.util.focus_login_form = function(Y) {
     }
 }
 
+/**
+ * Set focus on login error message
+ */
+M.util.focus_login_error = function(Y) {
+    var errorlog = Y.one('#loginerrormessage');
+
+    if (errorlog) {
+        errorlog.focus();
+    }
+}
 /**
  * Adds lightbox hidden element that covers the whole node.
  *
index a98062a..f09d6ec 100644 (file)
@@ -920,8 +920,7 @@ function clean_param($param, $type) {
         case PARAM_FILE:         // Strip all suspicious characters from filename
             $param = fix_utf8($param);
             $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':\\\\/]~u', '', $param);
-            $param = preg_replace('~\.\.+~', '', $param);
-            if ($param === '.') {
+            if ($param === '.' || $param === '..') {
                 $param = '';
             }
             return $param;
@@ -929,10 +928,23 @@ function clean_param($param, $type) {
         case PARAM_PATH:         // Strip all suspicious characters from file path
             $param = fix_utf8($param);
             $param = str_replace('\\', '/', $param);
-            $param = preg_replace('~[[:cntrl:]]|[&<>"`\|\':]~u', '', $param);
-            $param = preg_replace('~\.\.+~', '', $param);
+
+            // Explode the path and clean each element using the PARAM_FILE rules.
+            $breadcrumb = explode('/', $param);
+            foreach ($breadcrumb as $key => $crumb) {
+                if ($crumb === '.' && $key === 0) {
+                    // Special condition to allow for relative current path such as ./currentdirfile.txt.
+                } else {
+                    $crumb = clean_param($crumb, PARAM_FILE);
+                }
+                $breadcrumb[$key] = $crumb;
+            }
+            $param = implode('/', $breadcrumb);
+
+            // Remove multiple current path (./././) and multiple slashes (///).
             $param = preg_replace('~//+~', '/', $param);
-            return preg_replace('~/(\./)+~', '/', $param);
+            $param = preg_replace('~/(\./)+~', '/', $param);
+            return $param;
 
         case PARAM_HOST:         // allow FQDN or IPv4 dotted quad
             $param = preg_replace('/[^\.\d\w-]/','', $param ); // only allowed chars
index d4eff1c..008d091 100644 (file)
@@ -282,6 +282,21 @@ class page_requirements_manager {
         if ($page->pagelayout === 'frametop') {
             $this->js_init_call('M.util.init_frametop');
         }
+
+        // Include block drag/drop if editing is on
+        if ($page->user_is_editing()) {
+            $params = array(
+                'courseid' => $page->course->id,
+                'pagetype' => $page->pagetype,
+                'pagelayout' => $page->pagelayout,
+                'subpage' => $page->subpage,
+                'regions' => $page->blocks->get_regions(),
+            );
+            if (!empty($page->cm->id)) {
+                $params['cmid'] = $page->cm->id;
+            }
+            $page->requires->yui_module('moodle-core-blocks', 'M.core_blocks.init_dragdrop', array($params), null, true);
+        }
     }
 
     /**
index f94c295..f63fdb0 100644 (file)
@@ -32,6 +32,7 @@ ini_set('display_errors', '1');
 ini_set('log_errors', '1');
 
 require_once(__DIR__.'/bootstraplib.php');
+require_once(__DIR__.'/../testing/lib.php');
 
 if (isset($_SERVER['REMOTE_ADDR'])) {
     phpunit_bootstrap_error(1, 'Unit tests can be executed only from command line!');
@@ -133,7 +134,7 @@ if (!file_exists("$CFG->phpunit_dataroot/phpunittestdir.txt")) {
     }
 
     // now we are 100% sure this dir is used only for phpunit tests
-    phpunit_bootstrap_initdataroot($CFG->phpunit_dataroot);
+    testing_initdataroot($CFG->phpunit_dataroot, 'phpunit');
 }
 
 // verify db prefix
index b7c9fde..28f4345 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * PHPUnit bootstrap function
  *
- * Note: these functions must be self contained and must not rely on any library or include
+ * Note: these functions must be self contained and must not rely on any other library or include
  *
  * @package    core
  * @category   phpunit
@@ -25,6 +25,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+require_once(__DIR__ . '/../testing/lib.php');
+
 define('PHPUNIT_EXITCODE_PHPUNITMISSING', 129);
 define('PHPUNIT_EXITCODE_PHPUNITWRONG', 130);
 define('PHPUNIT_EXITCODE_PHPUNITEXTMISSING', 131);
@@ -63,11 +65,11 @@ function phpunit_bootstrap_error($errorcode, $text = '') {
             $text = "Moodle PHPUnit environment configuration warning:\n".$text;
             break;
         case PHPUNIT_EXITCODE_INSTALL:
-            $path = phpunit_bootstrap_cli_argument_path('/admin/tool/phpunit/cli/init.php');
+            $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php');
             $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path";
             break;
         case PHPUNIT_EXITCODE_REINSTALL:
-            $path = phpunit_bootstrap_cli_argument_path('/admin/tool/phpunit/cli/init.php');
+            $path = testing_cli_argument_path('/admin/tool/phpunit/cli/init.php');
             $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path";
             break;
         default:
@@ -76,90 +78,5 @@ function phpunit_bootstrap_error($errorcode, $text = '') {
             break;
     }
 
-    // do not write to error stream because we need the error message in PHP exec result from web ui
-    echo($text."\n");
-    exit($errorcode);
-}
-
-/**
- * Returns relative path against current working directory,
- * to be used for shell execution hints.
- * @param string $moodlepath starting with "/", ex: "/admin/tool/cli/init.php"
- * @return string path relative to current directory or absolute path
- */
-function phpunit_bootstrap_cli_argument_path($moodlepath) {
-    global $CFG;
-
-    if (isset($CFG->admin) and $CFG->admin !== 'admin') {
-        $moodlepath = preg_replace('|^/admin/|', "/$CFG->admin/", $moodlepath);
-    }
-
-    $cwd = getcwd();
-    if (substr($cwd, -1) !== DIRECTORY_SEPARATOR) {
-        $cwd .= DIRECTORY_SEPARATOR;
-    }
-    $path = realpath($CFG->dirroot.$moodlepath);
-
-    if (strpos($path, $cwd) === 0) {
-        $path = substr($path, strlen($cwd));
-    }
-
-    if (phpunit_bootstrap_is_cygwin()) {
-        $path = str_replace('\\', '/', $path);
-    }
-
-    return $path;
-}
-
-/**
- * Mark empty dataroot to be used for testing.
- * @param string $dataroot The dataroot directory
- * @return void
- */
-function phpunit_bootstrap_initdataroot($dataroot) {
-    global $CFG;
-    umask(0);
-    if (!file_exists("$dataroot/phpunittestdir.txt")) {
-        file_put_contents("$dataroot/phpunittestdir.txt", 'Contents of this directory are used during tests only, do not delete this file!');
-    }
-    phpunit_boostrap_fix_file_permissions("$dataroot/phpunittestdir.txt");
-    if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
-        mkdir("$CFG->phpunit_dataroot/phpunit", $CFG->directorypermissions);
-    }
-}
-
-/**
- * Try to change permissions to $CFG->dirroot or $CFG->dataroot if possible
- * @param string $file
- * @return bool success
- */
-function phpunit_boostrap_fix_file_permissions($file) {
-    global $CFG;
-
-    $permissions = fileperms($file);
-    if ($permissions & $CFG->filepermissions != $CFG->filepermissions) {
-        $permissions = $permissions | $CFG->filepermissions;
-        return chmod($file, $permissions);
-    }
-
-    return true;
-}
-
-/**
- * Find out if running under Cygwin on Windows.
- * @return bool
- */
-function phpunit_bootstrap_is_cygwin() {
-    if (empty($_SERVER['OS']) or $_SERVER['OS'] !== 'Windows_NT') {
-        return false;
-
-    } else if (!empty($_SERVER['SHELL']) and $_SERVER['SHELL'] === '/bin/bash') {
-        return true;
-
-    } else if (!empty($_SERVER['TERM']) and $_SERVER['TERM'] === 'cygwin') {
-        return true;
-
-    } else {
-        return false;
-    }
+    testing_error($errorcode, $text);
 }
index c63eed9..9b9bb99 100644 (file)
@@ -386,7 +386,7 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
     /**
      * Get data generator
      * @static
-     * @return phpunit_data_generator
+     * @return testing_data_generator
      */
     public static function getDataGenerator() {
         return phpunit_util::get_data_generator();
index 4f68206..a04b326 100644 (file)
@@ -106,7 +106,7 @@ class Hint_ResultPrinter extends PHPUnit_TextUI_ResultPrinter {
 
         if (!$executable) {
             $executable = 'phpunit';
-            if (phpunit_bootstrap_is_cygwin()) {
+            if (testing_is_cygwin()) {
                 $file = str_replace('\\', '/', $file);
                 $executable = 'phpunit.bat';
             }
index fc41b2f..23a34d9 100644 (file)
@@ -23,6 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+require_once(__DIR__.'/../../testing/classes/util.php');
 
 /**
  * Collection of utility methods.
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class phpunit_util {
-    /** @var string current version hash from php files */
-    protected static $versionhash = null;
-
-    /** @var array original content of all database tables*/
-    protected static $tabledata = null;
-
-    /** @var array original structure of all database tables */
-    protected static $tablestructure = null;
-
-    /** @var array original structure of all database tables */
-    protected static $sequencenames = null;
-
+class phpunit_util extends testing_util {
     /** @var array An array of original globals, restored after each test */
     protected static $globals = array();
 
-    /** @var int last value of db writes counter, used for db resetting */
-    public static $lastdbwrites = null;
-
-    /** @var phpunit_data_generator */
-    protected static $generator = null;
-
-    /** @var resource used for prevention of parallel test execution */
-    protected static $lockhandle = null;
-
     /** @var array list of debugging messages triggered during the last test execution */
     protected static $debuggings = array();
 
@@ -64,54 +44,14 @@ class phpunit_util {
     protected static $messagesink = null;
 
     /**
-     * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
-     *
-     * Note: do not call manually!
-     *
-     * @internal
-     * @static
-     * @return void
+     * @var array Files to skip when resetting dataroot folder
      */
-    public static function acquire_test_lock() {
-        global $CFG;
-        if (!file_exists("$CFG->phpunit_dataroot/phpunit")) {
-            // dataroot not initialised yet
-            return;
-        }
-        if (!file_exists("$CFG->phpunit_dataroot/phpunit/lock")) {
-            file_put_contents("$CFG->phpunit_dataroot/phpunit/lock", 'This file prevents concurrent execution of Moodle PHPUnit tests');
-            phpunit_boostrap_fix_file_permissions("$CFG->phpunit_dataroot/phpunit/lock");
-        }
-        if (self::$lockhandle = fopen("$CFG->phpunit_dataroot/phpunit/lock", 'r')) {
-            $wouldblock = null;
-            $locked = flock(self::$lockhandle, (LOCK_EX | LOCK_NB), $wouldblock);
-            if (!$locked) {
-                if ($wouldblock) {
-                    echo "Waiting for other test execution to complete...\n";
-                }
-                $locked = flock(self::$lockhandle, LOCK_EX);
-            }
-            if (!$locked) {
-                fclose(self::$lockhandle);
-                self::$lockhandle = null;
-            }
-        }
-        register_shutdown_function(array('phpunit_util', 'release_test_lock'));
-    }
+    protected static $datarootskiponreset = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
 
     /**
-     * Note: do not call manually!
-     * @internal
-     * @static
-     * @return void
+     * @var array Files to skip when dropping dataroot folder
      */
-    public static function release_test_lock() {
-        if (self::$lockhandle) {
-            flock(self::$lockhandle, LOCK_UN);
-            fclose(self::$lockhandle);
-            self::$lockhandle = null;
-        }
-    }
+    protected static $datarootskipondrop = array('.', '..', 'lock', 'webrunner.xml');
 
     /**
      * Load global $CFG;
@@ -129,7 +69,7 @@ class phpunit_util {
             initialise_cfg();
             return;
         }
-        if ($dbhash !== phpunit_util::get_version_hash()) {
+        if ($dbhash !== self::get_version_hash()) {
             // do not set CFG - the only way forward is to drop and reinstall
             return;
         }
@@ -137,413 +77,6 @@ class phpunit_util {
         initialise_cfg();
     }
 
-    /**
-     * Get data generator
-     * @static
-     * @return phpunit_data_generator
-     */
-    public static function get_data_generator() {
-        if (is_null(self::$generator)) {
-            require_once(__DIR__.'/../generatorlib.php');
-            self::$generator = new phpunit_data_generator();
-        }
-        return self::$generator;
-    }
-
-    /**
-     * Returns contents of all tables right after installation.
-     * @static
-     * @return array $table=>$records
-     */
-    protected static function get_tabledata() {
-        global $CFG;
-
-        if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser")) {
-            // not initialised yet
-            return array();
-        }
-
-        if (!isset(self::$tabledata)) {
-            $data = file_get_contents("$CFG->dataroot/phpunit/tabledata.ser");
-            self::$tabledata = unserialize($data);
-        }
-
-        if (!is_array(self::$tabledata)) {
-            phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tabledata.ser or invalid format, reinitialize test database.');
-        }
-
-        return self::$tabledata;
-    }
-
-    /**
-     * Returns structure of all tables right after installation.
-     * @static
-     * @return array $table=>$records
-     */
-    public static function get_tablestructure() {
-        global $CFG;
-
-        if (!file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
-            // not initialised yet
-            return array();
-        }
-
-        if (!isset(self::$tablestructure)) {
-            $data = file_get_contents("$CFG->dataroot/phpunit/tablestructure.ser");
-            self::$tablestructure = unserialize($data);
-        }
-
-        if (!is_array(self::$tablestructure)) {
-            phpunit_bootstrap_error(1, 'Can not read dataroot/phpunit/tablestructure.ser or invalid format, reinitialize test database.');
-        }
-
-        return self::$tablestructure;
-    }
-
-    /**
-     * Returns the names of sequences for each autoincrementing id field in all standard tables.
-     * @static
-     * @return array $table=>$sequencename
-     */
-    public static function get_sequencenames() {
-        global $DB;
-
-        if (isset(self::$sequencenames)) {
-            return self::$sequencenames;
-        }
-
-        if (!$structure = self::get_tablestructure()) {
-            return array();
-        }
-
-        self::$sequencenames = array();
-        foreach ($structure as $table=>$ignored) {
-            $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
-            if ($name !== false) {
-                self::$sequencenames[$table] = $name;
-            }
-        }
-
-        return self::$sequencenames;
-    }
-
-    /**
-     * Returns list of tables that are unmodified and empty.
-     *
-     * @static
-     * @return array of table names, empty if unknown
-     */
-    protected static function guess_unmodified_empty_tables() {
-        global $DB;
-
-        $dbfamily = $DB->get_dbfamily();
-
-        if ($dbfamily === 'mysql') {
-            $empties = array();
-            $prefix = $DB->get_prefix();
-            $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
-            foreach ($rs as $info) {
-                $table = strtolower($info->name);
-                if (strpos($table, $prefix) !== 0) {
-                    // incorrect table match caused by _
-                    continue;
-                }
-                if (!is_null($info->auto_increment)) {
-                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
-                    if ($info->auto_increment == 1) {
-                        $empties[$table] = $table;
-                    }
-                }
-            }
-            $rs->close();
-            return $empties;
-
-        } else if ($dbfamily === 'mssql') {
-            $empties = array();
-            $prefix = $DB->get_prefix();
-            $sql = "SELECT t.name
-                      FROM sys.identity_columns i
-                      JOIN sys.tables t ON t.object_id = i.object_id
-                     WHERE t.name LIKE ?
-                       AND i.name = 'id'
-                       AND i.last_value IS NULL";
-            $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
-            foreach ($rs as $info) {
-                $table = strtolower($info->name);
-                if (strpos($table, $prefix) !== 0) {
-                    // incorrect table match caused by _
-                    continue;
-                }
-                $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
-                $empties[$table] = $table;
-            }
-            $rs->close();
-            return $empties;
-
-        } else if ($dbfamily === 'oracle') {
-            $sequences = phpunit_util::get_sequencenames();
-            $sequences = array_map('strtoupper', $sequences);
-            $lookup = array_flip($sequences);
-            $empties = array();
-            list($seqs, $params) = $DB->get_in_or_equal($sequences);
-            $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $seq) {
-                $table = $lookup[$seq->sequence_name];
-                $empties[$table] = $table;
-            }
-            $rs->close();
-            return $empties;
-
-        } else {
-            return array();
-        }
-    }
-
-    /**
-     * Reset all database sequences to initial values.
-     *
-     * @static
-     * @param array $empties tables that are known to be unmodified and empty
-     * @return void
-     */
-    public static function reset_all_database_sequences(array $empties = null) {
-        global $DB;
-
-        if (!$data = self::get_tabledata()) {
-            // not initialised yet
-            return;
-        }
-        if (!$structure = self::get_tablestructure()) {
-            // not initialised yet
-            return;
-        }
-
-        $dbfamily = $DB->get_dbfamily();
-        if ($dbfamily === 'postgres') {
-            $queries = array();
-            $prefix = $DB->get_prefix();
-            foreach ($data as $table=>$records) {
-                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    if (empty($records)) {
-                        $nextid = 1;
-                    } else {
-                        $lastrecord = end($records);
-                        $nextid = $lastrecord->id + 1;
-                    }
-                    $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
-                }
-            }
-            if ($queries) {
-                $DB->change_database_structure(implode(';', $queries));
-            }
-
-        } else if ($dbfamily === 'mysql') {
-            $sequences = array();
-            $prefix = $DB->get_prefix();
-            $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
-            foreach ($rs as $info) {
-                $table = strtolower($info->name);
-                if (strpos($table, $prefix) !== 0) {
-                    // incorrect table match caused by _
-                    continue;
-                }
-                if (!is_null($info->auto_increment)) {
-                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
-                    $sequences[$table] = $info->auto_increment;
-                }
-            }
-            $rs->close();
-            $prefix = $DB->get_prefix();
-            foreach ($data as $table=>$records) {
-                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    if (isset($sequences[$table])) {
-                        if (empty($records)) {
-                            $nextid = 1;
-                        } else {
-                            $lastrecord = end($records);
-                            $nextid = $lastrecord->id + 1;
-                        }
-                        if ($sequences[$table] != $nextid) {
-                            $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
-                        }
-
-                    } else {
-                        // some problem exists, fallback to standard code
-                        $DB->get_manager()->reset_sequence($table);
-                    }
-                }
-            }
-
-        } else if ($dbfamily === 'oracle') {
-            $sequences = phpunit_util::get_sequencenames();
-            $sequences = array_map('strtoupper', $sequences);
-            $lookup = array_flip($sequences);
-
-            $current = array();
-            list($seqs, $params) = $DB->get_in_or_equal($sequences);
-            $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
-            $rs = $DB->get_recordset_sql($sql, $params);
-            foreach ($rs as $seq) {
-                $table = $lookup[$seq->sequence_name];
-                $current[$table] = $seq->last_number;
-            }
-            $rs->close();
-
-            foreach ($data as $table=>$records) {
-                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    $lastrecord = end($records);
-                    if ($lastrecord) {
-                        $nextid = $lastrecord->id + 1;
-                    } else {
-                        $nextid = 1;
-                    }
-                    if (!isset($current[$table])) {
-                        $DB->get_manager()->reset_sequence($table);
-                    } else if ($nextid == $current[$table]) {
-                        continue;
-                    }
-                    // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
-                    $seqname = $sequences[$table];
-                    $cachesize = $DB->get_manager()->generator->sequence_cache_size;
-                    $DB->change_database_structure("DROP SEQUENCE $seqname");
-                    $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
-                }
-            }
-
-        } else {
-            // note: does mssql support any kind of faster reset?
-            if (is_null($empties)) {
-                $empties = self::guess_unmodified_empty_tables();
-            }
-            foreach ($data as $table=>$records) {
-                if (isset($empties[$table])) {
-                    continue;
-                }
-                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    $DB->get_manager()->reset_sequence($table);
-                }
-            }
-        }
-    }
-
-    /**
-     * Reset all database tables to default values.
-     * @static
-     * @return bool true if reset done, false if skipped
-     */
-    public static function reset_database() {
-        global $DB;
-
-        if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
-            return false;
-        }
-
-        $tables = $DB->get_tables(false);
-        if (!$tables or empty($tables['config'])) {
-            // not installed yet
-            return false;
-        }
-
-        if (!$data = self::get_tabledata()) {
-            // not initialised yet
-            return false;
-        }
-        if (!$structure = self::get_tablestructure()) {
-            // not initialised yet
-            return false;
-        }
-
-        $empties = self::guess_unmodified_empty_tables();
-
-        foreach ($data as $table=>$records) {
-            if (empty($records)) {
-                if (isset($empties[$table])) {
-                    // table was not modified and is empty
-                } else {
-                    $DB->delete_records($table, array());
-                }
-                continue;
-            }
-
-            if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                $currentrecords = $DB->get_records($table, array(), 'id ASC');
-                $changed = false;
-                foreach ($records as $id=>$record) {
-                    if (!isset($currentrecords[$id])) {
-                        $changed = true;
-                        break;
-                    }
-                    if ((array)$record != (array)$currentrecords[$id]) {
-                        $changed = true;
-                        break;
-                    }
-                    unset($currentrecords[$id]);
-                }
-                if (!$changed) {
-                    if ($currentrecords) {
-                        $lastrecord = end($records);
-                        $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
-                        continue;
-                    } else {
-                        continue;
-                    }
-                }
-            }
-
-            $DB->delete_records($table, array());
-            foreach ($records as $record) {
-                $DB->import_record($table, $record, false, true);
-            }
-        }
-
-        // reset all next record ids - aka sequences
-        self::reset_all_database_sequences($empties);
-
-        // remove extra tables
-        foreach ($tables as $table) {
-            if (!isset($data[$table])) {
-                $DB->get_manager()->drop_table(new xmldb_table($table));
-            }
-        }
-
-        self::$lastdbwrites = $DB->perf_get_writes();
-
-        return true;
-    }
-
-    /**
-     * Purge dataroot directory
-     * @static
-     * @return void
-     */
-    public static function reset_dataroot() {
-        global $CFG;
-
-        $handle = opendir($CFG->dataroot);
-        $skip = array('.', '..', 'phpunittestdir.txt', 'phpunit', '.htaccess');
-        while (false !== ($item = readdir($handle))) {
-            if (in_array($item, $skip)) {
-                continue;
-            }
-            if (is_dir("$CFG->dataroot/$item")) {
-                remove_dir("$CFG->dataroot/$item", false);
-            } else {
-                unlink("$CFG->dataroot/$item");
-            }
-        }
-        closedir($handle);
-        make_temp_directory('');
-        make_cache_directory('');
-        make_cache_directory('htmlpurifier');
-        // Reset the cache API so that it recreates it's required directories as well.
-        cache_factory::reset();
-        // Purge all data from the caches. This is required for consistency.
-        // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
-        // and now we will purge any other caches as well.
-        cache_helper::purge_all();
-    }
-
     /**
      * Reset contents of all database tables to initial values, reset caches, etc.
      *
@@ -724,35 +257,6 @@ class phpunit_util {
         return null;
     }
 
-    /**
-     * Does this site (db and dataroot) appear to be used for production?
-     * We try very hard to prevent accidental damage done to production servers!!
-     *
-     * @static
-     * @return bool
-     */
-    public static function is_test_site() {
-        global $DB, $CFG;
-
-        if (!file_exists("$CFG->dataroot/phpunittestdir.txt")) {
-            // this is already tested in bootstrap script,
-            // but anyway presence of this file means the dataroot is for testing
-            return false;
-        }
-
-        $tables = $DB->get_tables(false);
-        if ($tables) {
-            if (!$DB->get_manager()->table_exists('config')) {
-                return false;
-            }
-            if (!get_config('core', 'phpunittest')) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
     /**
      * Is this site initialised to run unit tests?
      *
@@ -760,36 +264,19 @@ class phpunit_util {
      * @return int array errorcode=>message, 0 means ok
      */
     public static function testing_ready_problem() {
-        global $CFG, $DB;
-
-        $tables = $DB->get_tables(false);
+        global $DB;
 
         if (!self::is_test_site()) {
             // dataroot was verified in bootstrap, so it must be DB
             return array(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not use database for testing, try different prefix');
         }
 
+        $tables = $DB->get_tables(false);
         if (empty($tables)) {
             return array(PHPUNIT_EXITCODE_INSTALL, '');
         }
 
-        if (!file_exists("$CFG->dataroot/phpunit/tabledata.ser") or !file_exists("$CFG->dataroot/phpunit/tablestructure.ser")) {
-            return array(PHPUNIT_EXITCODE_REINSTALL, '');
-        }
-
-        if (!file_exists("$CFG->dataroot/phpunit/versionshash.txt")) {
-            return array(PHPUNIT_EXITCODE_REINSTALL, '');
-        }
-
-        $hash = phpunit_util::get_version_hash();
-        $oldhash = file_get_contents("$CFG->dataroot/phpunit/versionshash.txt");
-
-        if ($hash !== $oldhash) {
-            return array(PHPUNIT_EXITCODE_REINSTALL, '');
-        }
-
-        $dbhash = get_config('core', 'phpunittest');
-        if ($hash !== $dbhash) {
+        if (!self::is_test_data_updated()) {
             return array(PHPUNIT_EXITCODE_REINSTALL, '');
         }
 
@@ -816,52 +303,13 @@ class phpunit_util {
         if ($displayprogress) {
             echo "Purging dataroot:\n";
         }
+
         self::reset_dataroot();
-        phpunit_bootstrap_initdataroot($CFG->dataroot);
-        $keep = array('.', '..', 'lock', 'webrunner.xml');
-        $files = scandir("$CFG->dataroot/phpunit");
-        foreach ($files as $file) {
-            if (in_array($file, $keep)) {
-                continue;
-            }
-            $path = "$CFG->dataroot/phpunit/$file";
-            if (is_dir($path)) {
-                remove_dir($path, false);
-            } else {
-                unlink($path);
-            }
-        }
+        testing_initdataroot($CFG->dataroot, 'phpunit');
+        self::drop_dataroot();
 
         // drop all tables
-        $tables = $DB->get_tables(false);
-        if (isset($tables['config'])) {
-            // config always last to prevent problems with interrupted drops!
-            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";
-        }
+        self::drop_database($displayprogress);
     }
 
     /**
@@ -899,84 +347,11 @@ class phpunit_util {
         $timezones = get_records_csv($CFG->libdir.'/timezone.txt', 'timezone');
         update_timezone_records($timezones);
 
-        // add test db flag
-        $hash = phpunit_util::get_version_hash();
-        set_config('phpunittest', $hash);
-
-        // store data for all tables
-        $data = array();
-        $structure = array();
-        $tables = $DB->get_tables();
-        foreach ($tables as $table) {
-            $columns = $DB->get_columns($table);
-            $structure[$table] = $columns;
-            if (isset($columns['id']) and $columns['id']->auto_increment) {
-                $data[$table] = $DB->get_records($table, array(), 'id ASC');
-            } else {
-                // there should not be many of these
-                $data[$table] = $DB->get_records($table, array());
-            }
-        }
-        $data = serialize($data);
-        file_put_contents("$CFG->dataroot/phpunit/tabledata.ser", $data);
-        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tabledata.ser");
-
-        $structure = serialize($structure);
-        file_put_contents("$CFG->dataroot/phpunit/tablestructure.ser", $structure);
-        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/tablestructure.ser");
-
-        // hash all plugin versions - helps with very fast detection of db structure changes
-        file_put_contents("$CFG->dataroot/phpunit/versionshash.txt", $hash);
-        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/versionshash.txt", $hash);
-    }
-
-    /**
-     * Calculate unique version hash for all plugins and core.
-     * @static
-     * @return string sha1 hash
-     */
-    public static function get_version_hash() {
-        global $CFG;
-
-        if (self::$versionhash) {
-            return self::$versionhash;
-        }
-
-        $versions = array();
-
-        // main version first
-        $version = null;
-        include($CFG->dirroot.'/version.php');
-        $versions['core'] = $version;
-
-        // modules
-        $mods = get_plugin_list('mod');
-        ksort($mods);
-        foreach ($mods as $mod => $fullmod) {
-            $module = new stdClass();
-            $module->version = null;
-            include($fullmod.'/version.php');
-            $versions[$mod] = $module->version;
-        }
-
-        // now the rest of plugins
-        $plugintypes = get_plugin_types();
-        unset($plugintypes['mod']);
-        ksort($plugintypes);
-        foreach ($plugintypes as $type=>$unused) {
-            $plugs = get_plugin_list($type);
-            ksort($plugs);
-            foreach ($plugs as $plug=>$fullplug) {
-                $plugin = new stdClass();
-                $plugin->version = null;
-                @include($fullplug.'/version.php');
-                $versions[$plug] = $plugin->version;
-            }
-        }
-
-        self::$versionhash = sha1(serialize($versions));
+        // Store version hash in the database and in a file.
+        self::store_versions_hash();
 
-        return self::$versionhash;
+        // Store database data and structure.
+        self::store_database_state();
     }
 
     /**
@@ -1020,7 +395,7 @@ class phpunit_util {
         $result = false;
         if (is_writable($CFG->dirroot)) {
             if ($result = file_put_contents("$CFG->dirroot/phpunit.xml", $data)) {
-                phpunit_boostrap_fix_file_permissions("$CFG->dirroot/phpunit.xml");
+                testing_fix_file_permissions("$CFG->dirroot/phpunit.xml");
             }
         }
 
@@ -1030,7 +405,7 @@ class phpunit_util {
             '<directory suffix="_test.php">'.$CFG->dirroot.(DIRECTORY_SEPARATOR === '\\' ? '\\\\' : DIRECTORY_SEPARATOR).'$1</directory>',
             $data);
         file_put_contents("$CFG->dataroot/phpunit/webrunner.xml", $data);
-        phpunit_boostrap_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
+        testing_fix_file_permissions("$CFG->dataroot/phpunit/webrunner.xml");
 
         return (bool)$result;
     }
@@ -1055,17 +430,8 @@ class phpunit_util {
         $ftemplate = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
         $ftemplate = preg_replace('|<!--All core suites.*</testsuites>|s', '<!--@component_suite@-->', $ftemplate);
 
-        // Get all the components
-        $components = self::get_all_plugins_with_tests() + self::get_all_subsystems_with_tests();
-
-        // Get all the directories having tests
-        $directories = self::get_all_directories_with_tests();
-
-        // Find any directory not covered by proper components
-        $remaining = array_diff($directories, $components);
-
-        // Add them to the list of components
-        $components += $remaining;
+        // Gets all the components with tests
+        $components = tests_finder::get_components_with_tests('phpunit');
 
         // Create the corresponding phpunit.xml file for each component
         foreach ($components as $cname => $cpath) {
@@ -1084,7 +450,7 @@ class phpunit_util {
             $result = false;
             if (is_writable($cpath)) {
                 if ($result = (bool)file_put_contents("$cpath/phpunit.xml", $fcontents)) {
-                    phpunit_boostrap_fix_file_permissions("$cpath/phpunit.xml");
+                    testing_fix_file_permissions("$cpath/phpunit.xml");
                 }
             }
             // Problems writing file, throw error
@@ -1094,112 +460,6 @@ class phpunit_util {
         }
     }
 
-    /**
-     * Returns all the plugins having PHPUnit tests
-     *
-     * @return array all the plugins having PHPUnit tests
-     *
-     */
-    private static function get_all_plugins_with_tests() {
-        $pluginswithtests = array();
-
-        $plugintypes = get_plugin_types();
-        ksort($plugintypes);
-        foreach ($plugintypes as $type => $unused) {
-            $plugs = get_plugin_list($type);
-            ksort($plugs);
-            foreach ($plugs as $plug => $fullplug) {
-                // Look for tests recursively
-                if (self::directory_has_tests($fullplug)) {
-                    $pluginswithtests[$type . '_' . $plug] = $fullplug;
-                }
-            }
-        }
-        return $pluginswithtests;
-    }
-
-    /**
-     * Returns all the subsystems having PHPUnit tests
-     *
-     * Note we are hacking here the list of subsystems
-     * to cover some well-known subsystems that are not properly
-     * returned by the {@link get_core_subsystems()} function.
-     *
-     * @return array all the subsystems having PHPUnit tests
-     */
-    private static function get_all_subsystems_with_tests() {
-        global $CFG;
-
-        $subsystemswithtests = array();
-
-        $subsystems = get_core_subsystems();
-
-        // Hack the list a bit to cover some well-known ones
-        $subsystems['backup'] = 'backup';
-        $subsystems['db-dml'] = 'lib/dml';
-        $subsystems['db-ddl'] = 'lib/ddl';
-
-        ksort($subsystems);
-        foreach ($subsystems as $subsys => $relsubsys) {
-            if ($relsubsys === null) {
-                continue;
-            }
-            $fullsubsys = $CFG->dirroot . '/' . $relsubsys;
-            if (!is_dir($fullsubsys)) {
-                continue;
-            }
-            // Look for tests recursively
-            if (self::directory_has_tests($fullsubsys)) {
-                $subsystemswithtests['core_' . $subsys] = $fullsubsys;
-            }
-        }
-        return $subsystemswithtests;
-    }
-
-    /**
-     * Returns all the directories having tests
-     *
-     * @return array all directories having tests
-     */
-    private static function get_all_directories_with_tests() {
-        global $CFG;
-
-        $dirs = array();
-        $dirite = new RecursiveDirectoryIterator($CFG->dirroot);
-        $iteite = new RecursiveIteratorIterator($dirite);
-        $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
-        $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|');
-        foreach ($regite as $path => $element) {
-            $key = dirname(dirname($path));
-            $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_');
-            $dirs[$key] = $value;
-        }
-        ksort($dirs);
-        return array_flip($dirs);
-    }
-
-    /**
-     * Returns if a given directory has tests (recursively)
-     *
-     * @param $dir string full path to the directory to look for phpunit tests
-     * @return bool if a given directory has tests (true) or no (false)
-     */
-    private static function directory_has_tests($dir) {
-        if (!is_dir($dir)) {
-            return false;
-        }
-
-        $dirite = new RecursiveDirectoryIterator($dir);
-        $iteite = new RecursiveIteratorIterator($dirite);
-        $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
-        $regite = new RegexIterator($iteite, '|'.$sep.'tests'.$sep.'.*_test\.php$|');
-        $regite->rewind();
-        if ($regite->valid()) {
-            return true;
-        }
-        return false;
-    }
-
     /**
      * To be called from debugging() only.
      * @param string $message
index f6c8897..6dfbdd4 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * PHPUnit data generator support
+ * Adds data generator support
  *
+ * Deprecated file in favour of lib/testing/generator/lib.php, keeping
+ * file for backwards reference just in case 3rd party applications are
+ * using them.
+ *
+ * @deprecated
+ * @todo       MDL-37517 This will be deleted in Moodle 2.7
+ * @see        lib/testing/generator/lib.php
  * @package    core
  * @category   phpunit
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-// NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php!
 
-require_once(__DIR__.'/classes/data_generator.php');
-require_once(__DIR__.'/classes/module_generator.php');
-require_once(__DIR__.'/classes/block_generator.php');
+debugging('File lib/phpunit/generatorlib.php is deprecated, please use lib/testing/generator/lib.php instead', DEBUG_DEVELOPER);
 
+require_once(__DIR__ . '/../testing/generator/lib.php');
index 95b7f73..2ceaf3e 100644 (file)
@@ -35,5 +35,6 @@ require_once(__DIR__.'/classes/database_driver_testcase.php');
 require_once(__DIR__.'/classes/arraydataset.php');
 require_once(__DIR__.'/classes/advanced_testcase.php');
 require_once(__DIR__.'/classes/unittestcase.php');
-require_once(__DIR__.'/classes/hint_resultprinter.php'); // Loaded here because phpunit.xml does not support relative links for printerFile
-
+require_once(__DIR__.'/classes/hint_resultprinter.php'); // Loaded here because phpunit.xml does not support relative links for printerFile.
+require_once(__DIR__.'/../testing/classes/test_lock.php');
+require_once(__DIR__.'/../testing/classes/tests_finder.php');
index d1d1db8..60e058c 100644 (file)
@@ -249,7 +249,7 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
 
     public function test_getDataGenerator() {
         $generator = $this->getDataGenerator();
-        $this->assertInstanceOf('phpunit_data_generator', $generator);
+        $this->assertInstanceOf('testing_data_generator', $generator);
     }
 
     public function test_database_mock1() {
index 2ad03cb..4b8799f 100644 (file)
@@ -35,6 +35,15 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class core_phpunit_basic_testcase extends basic_testcase {
+    protected $testassertexecuted = false;
+
+    public function setUp() {
+        if ($this->getName() === 'test_setup_assert') {
+            $this->assertTrue(true);
+            $this->testassertexecuted = true;
+            return;
+        }
+    }
 
     /**
      * Tests that bootstrapping has occurred correctly
@@ -113,6 +122,14 @@ class core_phpunit_basic_testcase extends basic_testcase {
         $this->assertNotEmpty(new stdClass());
     }
 
+    /**
+     * Make sure asserts in setUp() do not create problems.
+     */
+    public function test_setup_assert() {
+        $this->assertTrue($this->testassertexecuted);
+        $this->testassertexecuted = false;
+    }
+
 // Uncomment following tests to see logging of unexpected changes in global state and database
     /*
         public function test_db_modification() {
index 6cf51b4..10b0ea9 100644 (file)
@@ -262,35 +262,17 @@ function resourcelib_embed_general($fullurl, $title, $clicktoopen, $mimetype) {
         $fullurl = $fullurl->out();
     }
 
-    $iframe = false;
-
     $param = '<param name="src" value="'.$fullurl.'" />';
 
-    // IE can not embed stuff properly, that is why we use iframe instead.
-    // Unfortunately this tag does not validate in xhtml strict mode,
-    // but in any case it is undeprecated in HTML 5 - we will use it everywhere soon!
-    if ($mimetype === 'text/html' and check_browser_version('MSIE', 5)) {
-        $iframe = true;
-    }
-
-    if ($iframe) {
-        $code = <<<EOT
+    // Always use iframe embedding because object tag does not work much,
+    // this is ok in HTML5.
+    $code = <<<EOT
 <div class="resourcecontent resourcegeneral">
   <iframe id="resourceobject" src="$fullurl">
     $clicktoopen
   </iframe>
 </div>
 EOT;
-    } else {
-        $code = <<<EOT
-<div class="resourcecontent resourcegeneral">
-  <object id="resourceobject" data="$fullurl" type="$mimetype"  width="800" height="600">
-    $param
-    $clicktoopen
-  </object>
-</div>
-EOT;
-    }
 
     // the size is hardcoded in the boject obove intentionally because it is adjusted by the following function on-the-fly
     $PAGE->requires->js_init_call('M.util.init_maximised_embed', array('resourceobject'), true);
index fdcfc04..f25301f 100644 (file)
@@ -480,7 +480,7 @@ setup_DB();
 
 if (PHPUNIT_TEST and !PHPUNIT_UTIL) {
     // make sure tests do not run in parallel
-    phpunit_util::acquire_test_lock();
+    test_lock::acquire('phpunit');
     $dbhash = null;
     try {
         if ($dbhash = $DB->get_field('config', 'value', array('name'=>'phpunittest'))) {
diff --git a/lib/testing/classes/test_lock.php b/lib/testing/classes/test_lock.php
new file mode 100644 (file)
index 0000000..710cdea
--- /dev/null
@@ -0,0 +1,98 @@
+<?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/>.
+
+/**
+ * Tests lock
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__.'/../lib.php');
+
+/**
+ * Tests lock to prevent concurrent executions of the same test suite
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_lock {
+
+    /**
+     * @var array Array of resource used for prevention of parallel test execution
+     */
+    protected static $lockhandles = array();
+
+    /**
+     * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
+     *
+     * Note: do not call manually!
+     *
+     * @internal
+     * @static
+     * @param    string  $framework Test framework
+     * @return   void
+     */
+    public static function acquire($framework) {
+        global $CFG;
+
+        $datarootpath = $CFG->{$framework . '_dataroot'} . '/' . $framework;
+        $lockfile = $datarootpath . '/lock';
+        if (!file_exists($datarootpath)) {
+            // Dataroot not initialised yet.
+            return;
+        }
+        if (!file_exists($lockfile)) {
+            file_put_contents($lockfile, 'This file prevents concurrent execution of Moodle ' . $framework . ' tests');
+            testing_fix_file_permissions($lockfile);
+        }
+        if (self::$lockhandles[$framework] = fopen($lockfile, 'r')) {
+            $wouldblock = null;
+            $locked = flock(self::$lockhandles[$framework], (LOCK_EX | LOCK_NB), $wouldblock);
+            if (!$locked) {
+                if ($wouldblock) {
+                    echo "Waiting for other test execution to complete...\n";
+                }
+                $locked = flock(self::$lockhandles[$framework], LOCK_EX);
+            }
+            if (!$locked) {
+                fclose(self::$lockhandles[$framework]);
+                self::$lockhandles[$framework] = null;
+            }
+        }
+        register_shutdown_function(array('test_lock', 'release'), $framework);
+    }
+
+    /**
+     * Note: do not call manually!
+     * @internal
+     * @static
+     * @param    string  $framework phpunit|behat
+     * @return   void
+     */
+    public static function release($framework) {
+        if (self::$lockhandles[$framework]) {
+            flock(self::$lockhandles[$framework], LOCK_UN);
+            fclose(self::$lockhandles[$framework]);
+            self::$lockhandles[$framework] = null;
+        }
+    }
+
+}
diff --git a/lib/testing/classes/tests_finder.php b/lib/testing/classes/tests_finder.php
new file mode 100644 (file)
index 0000000..0a89cfa
--- /dev/null
@@ -0,0 +1,190 @@
+<?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/>.
+
+/**
+ * Tests finder
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Finds components and plugins with tests
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tests_finder {
+
+    /**
+     * Returns all the components with tests of the specified type
+     * @param string $testtype The kind of test we are looking for
+     * @return array
+     */
+    public static function get_components_with_tests($testtype) {
+
+        // Get all the components
+        $components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype);
+
+        // Get all the directories having tests
+        $directories = self::get_all_directories_with_tests($testtype);
+
+        // Find any directory not covered by proper components
+        $remaining = array_diff($directories, $components);
+
+        // Add them to the list of components
+        $components += $remaining;
+
+        return $components;
+    }
+
+    /**
+     * Returns all the plugins having tests
+     * @param string $testtype The kind of test we are looking for
+     * @return array  all the plugins having tests
+     */
+    private static function get_all_plugins_with_tests($testtype) {
+        $pluginswithtests = array();
+
+        $plugintypes = get_plugin_types();
+        ksort($plugintypes);
+        foreach ($plugintypes as $type => $unused) {
+            $plugs = get_plugin_list($type);
+            ksort($plugs);
+            foreach ($plugs as $plug => $fullplug) {
+                // Look for tests recursively
+                if (self::directory_has_tests($fullplug, $testtype)) {
+                    $pluginswithtests[$type . '_' . $plug] = $fullplug;
+                }
+            }
+        }
+        return $pluginswithtests;
+    }
+
+    /**
+     * Returns all the subsystems having tests
+     *
+     * Note we are hacking here the list of subsystems
+     * to cover some well-known subsystems that are not properly
+     * returned by the {@link get_core_subsystems()} function.
+     *
+     * @param string $testtype The kind of test we are looking for
+     * @return array all the subsystems having tests
+     */
+    private static function get_all_subsystems_with_tests($testtype) {
+        global $CFG;
+
+        $subsystemswithtests = array();
+
+        $subsystems = get_core_subsystems();
+
+        // Hack the list a bit to cover some well-known ones
+        $subsystems['backup'] = 'backup';
+        $subsystems['db-dml'] = 'lib/dml';
+        $subsystems['db-ddl'] = 'lib/ddl';
+
+        ksort($subsystems);
+        foreach ($subsystems as $subsys => $relsubsys) {
+            if ($relsubsys === null) {
+                continue;
+            }
+            $fullsubsys = $CFG->dirroot . '/' . $relsubsys;
+            if (!is_dir($fullsubsys)) {
+                continue;
+            }
+            // Look for tests recursively
+            if (self::directory_has_tests($fullsubsys, $testtype)) {
+                $subsystemswithtests['core_' . $subsys] = $fullsubsys;
+            }
+        }
+        return $subsystemswithtests;
+    }
+
+    /**
+     * Returns all the directories having tests
+     *
+     * @param string $testtype The kind of test we are looking for
+     * @return array all directories having tests
+     */
+    private static function get_all_directories_with_tests($testtype) {
+        global $CFG;
+
+        $dirs = array();
+        $dirite = new RecursiveDirectoryIterator($CFG->dirroot);
+        $iteite = new RecursiveIteratorIterator($dirite);
+        $regexp = self::get_regexp($testtype);
+        $regite = new RegexIterator($iteite, $regexp);
+        foreach ($regite as $path => $element) {
+            $key = dirname(dirname($path));
+            $value = trim(str_replace('/', '_', str_replace($CFG->dirroot, '', $key)), '_');
+            $dirs[$key] = $value;
+        }
+        ksort($dirs);
+        return array_flip($dirs);
+    }
+
+    /**
+     * Returns if a given directory has tests (recursively)
+     *
+     * @param string $dir full path to the directory to look for phpunit tests
+     * @param string $testtype phpunit|behat
+     * @return bool if a given directory has tests (true) or no (false)
+     */
+    private static function directory_has_tests($dir, $testtype) {
+        if (!is_dir($dir)) {
+            return false;
+        }
+
+        $dirite = new RecursiveDirectoryIterator($dir);
+        $iteite = new RecursiveIteratorIterator($dirite);
+        $regexp = self::get_regexp($testtype);
+        $regite = new RegexIterator($iteite, $regexp);
+        $regite->rewind();
+        if ($regite->valid()) {
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * Returns the regular expression to match by the test files
+     * @param string $testtype
+     * @return string
+     */
+    private static function get_regexp($testtype) {
+
+        $sep = preg_quote(DIRECTORY_SEPARATOR, '|');
+
+        switch ($testtype) {
+            case 'phpunit':
+                $regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|';
+                break;
+            case 'features':
+                $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|';
+                break;
+            case 'stepsdefinitions':
+                $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.php$|';
+                break;
+        }
+
+        return $regexp;
+    }
+}
diff --git a/lib/testing/classes/util.php b/lib/testing/classes/util.php
new file mode 100644 (file)
index 0000000..8588b1b
--- /dev/null
@@ -0,0 +1,727 @@
+<?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/>.
+
+/**
+ * Testing util classes
+ *
+ * @abstract
+ * @package    core
+ * @category   test
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Utils for test sites creation
+ *
+ * @package   core
+ * @category  test
+ * @copyright 2012 Petr Skoda {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class testing_util {
+
+    /**
+     * @var int last value of db writes counter, used for db resetting
+     */
+    public static $lastdbwrites = null;
+
+    /**
+     * @var testing_data_generator
+     */
+    protected static $generator = null;
+
+    /**
+     * @var string current version hash from php files
+     */
+    protected static $versionhash = null;
+
+    /**
+     * @var array original content of all database tables
+     */
+    protected static $tabledata = null;
+
+    /**
+     * @var array original structure of all database tables
+     */
+    protected static $tablestructure = null;
+
+    /**
+     * @var array original structure of all database tables
+     */
+    protected static $sequencenames = null;
+
+    /**
+     * Returns the testing framework name
+     * @static
+     * @return string
+     */
+    protected static final function get_framework() {
+        $classname = get_called_class();
+        return substr($classname, 0, strpos($classname, '_'));
+    }
+
+    /**
+     * Get data generator
+     * @static
+     * @return testing_data_generator
+     */
+    public static function get_data_generator() {
+        if (is_null(self::$generator)) {
+            require_once(__DIR__.'/../generator/lib.php');
+            self::$generator = new testing_data_generator();
+        }
+        return self::$generator;
+    }
+
+    /**
+     * Does this site (db and dataroot) appear to be used for production?
+     * We try very hard to prevent accidental damage done to production servers!!
+     *
+     * @static
+     * @return bool
+     */
+    public static function is_test_site() {
+        global $DB, $CFG;
+
+        $framework = self::get_framework();
+
+        if (!file_exists($CFG->dataroot . '/' . $framework . 'testdir.txt')) {
+            // this is already tested in bootstrap script,
+            // but anyway presence of this file means the dataroot is for testing
+            return false;
+        }
+
+        $tables = $DB->get_tables(false);
+        if ($tables) {
+            if (!$DB->get_manager()->table_exists('config')) {
+                return false;
+            }
+            if (!get_config('core', $framework . 'test')) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns whether test database and dataroot were created using the current version codebase
+     *
+     * @return boolean
+     */
+    protected static function is_test_data_updated() {
+        global $CFG;
+
+        $framework = self::get_framework();
+
+        $datarootpath = $CFG->dataroot . '/' . $framework;
+        if (!file_exists($datarootpath . '/tabledata.ser') or !file_exists($datarootpath . '/tablestructure.ser')) {
+            return false;
+        }
+
+        if (!file_exists($datarootpath . '/versionshash.txt')) {
+            return false;
+        }
+
+        $hash = self::get_version_hash();
+        $oldhash = file_get_contents($datarootpath . '/versionshash.txt');
+
+        if ($hash !== $oldhash) {
+            return false;
+        }
+
+        $dbhash = get_config('core', $framework . 'test');
+        if ($hash !== $dbhash) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Stores the status of the database
+     *
+     * Serializes the contents and the structure and
+     * stores it in the test framework space in dataroot
+     */
+    protected static function store_database_state() {
+        global $DB, $CFG;
+
+        $framework = self::get_framework();
+
+        // store data for all tables
+        $data = array();
+        $structure = array();
+        $tables = $DB->get_tables();
+        foreach ($tables as $table) {
+            $columns = $DB->get_columns($table);
+            $structure[$table] = $columns;
+            if (isset($columns['id']) and $columns['id']->auto_increment) {
+                $data[$table] = $DB->get_records($table, array(), 'id ASC');
+            } else {
+                // there should not be many of these
+                $data[$table] = $DB->get_records($table, array());
+            }
+        }
+        $data = serialize($data);
+        $datafile = $CFG->dataroot . '/' . $framework . '/tabledata.ser';
+        file_put_contents($datafile, $data);
+        testing_fix_file_permissions($datafile);
+
+        $structure = serialize($structure);
+        $structurefile = $CFG->dataroot . '/' . $framework . '/tablestructure.ser';
+        file_put_contents($structurefile, $structure);
+        testing_fix_file_permissions($structurefile);
+    }
+
+    /**
+     * Stores the version hash in both database and dataroot
+     */
+    protected static function store_versions_hash() {
+        global $CFG;
+
+        $framework = self::get_framework();
+        $hash = self::get_version_hash();
+
+        // add test db flag
+        set_config($framework . 'test', $hash);
+
+        // hash all plugin versions - helps with very fast detection of db structure changes
+        $hashfile = $CFG->dataroot . '/' . $framework . '/versionshash.txt';
+        file_put_contents($hashfile, $hash);
+        testing_fix_file_permissions($hashfile);
+    }
+
+    /**
+     * Returns contents of all tables right after installation.
+     * @static
+     * @return array  $table=>$records
+     */
+    protected static function get_tabledata() {
+        global $CFG;
+
+        $framework = self::get_framework();
+
+        $datafile = $CFG->dataroot . '/' . $framework . '/tabledata.ser';
+        if (!file_exists($datafile)) {
+            // Not initialised yet.
+            return array();
+        }
+
+        if (!isset(self::$tabledata)) {
+            $data = file_get_contents($datafile);
+            self::$tabledata = unserialize($data);
+        }
+
+        if (!is_array(self::$tabledata)) {
+            testing_error(1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.');
+        }
+
+        return self::$tabledata;
+    }
+
+    /**
+     * Returns structure of all tables right after installation.
+     * @static
+     * @return array $table=>$records
+     */
+    public static function get_tablestructure() {
+        global $CFG;
+
+        $framework = self::get_framework();
+
+        $structurefile = $CFG->dataroot . '/' . $framework . '/tablestructure.ser';
+        if (!file_exists($structurefile)) {
+            // Not initialised yet.
+            return array();
+        }
+
+        if (!isset(self::$tablestructure)) {
+            $data = file_get_contents($structurefile);
+            self::$tablestructure = unserialize($data);
+        }
+
+        if (!is_array(self::$tablestructure)) {
+            testing_error(1, 'Can not read dataroot/' . $framework . '/tablestructure.ser or invalid format, reinitialize test database.');
+        }
+
+        return self::$tablestructure;
+    }
+
+    /**
+     * Returns the names of sequences for each autoincrementing id field in all standard tables.
+     * @static
+     * @return array $table=>$sequencename
+     */
+    public static function get_sequencenames() {
+        global $DB;
+
+        if (isset(self::$sequencenames)) {
+            return self::$sequencenames;
+        }
+
+        if (!$structure = self::get_tablestructure()) {
+            return array();
+        }
+
+        self::$sequencenames = array();
+        foreach ($structure as $table => $ignored) {
+            $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table));
+            if ($name !== false) {
+                self::$sequencenames[$table] = $name;
+            }
+        }
+
+        return self::$sequencenames;
+    }
+
+    /**
+     * Returns list of tables that are unmodified and empty.
+     *
+     * @static
+     * @return array of table names, empty if unknown
+     */
+    protected static function guess_unmodified_empty_tables() {
+        global $DB;
+
+        $dbfamily = $DB->get_dbfamily();
+
+        if ($dbfamily === 'mysql') {
+            $empties = array();
+            $prefix = $DB->get_prefix();
+            $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
+            foreach ($rs as $info) {
+                $table = strtolower($info->name);
+                if (strpos($table, $prefix) !== 0) {
+                    // incorrect table match caused by _
+                    continue;
+                }
+                if (!is_null($info->auto_increment)) {
+                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                    if ($info->auto_increment == 1) {
+                        $empties[$table] = $table;
+                    }
+                }
+            }
+            $rs->close();
+            return $empties;
+
+        } else if ($dbfamily === 'mssql') {
+            $empties = array();
+            $prefix = $DB->get_prefix();
+            $sql = "SELECT t.name
+                      FROM sys.identity_columns i
+                      JOIN sys.tables t ON t.object_id = i.object_id
+                     WHERE t.name LIKE ?
+                       AND i.name = 'id'
+                       AND i.last_value IS NULL";
+            $rs = $DB->get_recordset_sql($sql, array($prefix.'%'));
+            foreach ($rs as $info) {
+                $table = strtolower($info->name);
+                if (strpos($table, $prefix) !== 0) {
+                    // incorrect table match caused by _
+                    continue;
+                }
+                $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                $empties[$table] = $table;
+            }
+            $rs->close();
+            return $empties;
+
+        } else if ($dbfamily === 'oracle') {
+            $sequences = self::get_sequencenames();
+            $sequences = array_map('strtoupper', $sequences);
+            $lookup = array_flip($sequences);
+            $empties = array();
+            list($seqs, $params) = $DB->get_in_or_equal($sequences);
+            $sql = "SELECT sequence_name FROM user_sequences WHERE last_number = 1 AND sequence_name $seqs";
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $seq) {
+                $table = $lookup[$seq->sequence_name];
+                $empties[$table] = $table;
+            }
+            $rs->close();
+            return $empties;
+
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Reset all database sequences to initial values.
+     *
+     * @static
+     * @param array $empties tables that are known to be unmodified and empty
+     * @return void
+     */
+    public static function reset_all_database_sequences(array $empties = null) {
+        global $DB;
+
+        if (!$data = self::get_tabledata()) {
+            // Not initialised yet.
+            return;
+        }
+        if (!$structure = self::get_tablestructure()) {
+            // Not initialised yet.
+            return;
+        }
+
+        $dbfamily = $DB->get_dbfamily();
+        if ($dbfamily === 'postgres') {
+            $queries = array();
+            $prefix = $DB->get_prefix();
+            foreach ($data as $table => $records) {
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    if (empty($records)) {
+                        $nextid = 1;
+                    } else {
+                        $lastrecord = end($records);
+                        $nextid = $lastrecord->id + 1;
+                    }
+                    $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
+                }
+            }
+            if ($queries) {
+                $DB->change_database_structure(implode(';', $queries));
+            }
+
+        } else if ($dbfamily === 'mysql') {
+            $sequences = array();
+            $prefix = $DB->get_prefix();
+            $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", array($prefix.'%'));
+            foreach ($rs as $info) {
+                $table = strtolower($info->name);
+                if (strpos($table, $prefix) !== 0) {
+                    // incorrect table match caused by _
+                    continue;
+                }
+                if (!is_null($info->auto_increment)) {
+                    $table = preg_replace('/^'.preg_quote($prefix, '/').'/', '', $table);
+                    $sequences[$table] = $info->auto_increment;
+                }
+            }
+            $rs->close();
+            $prefix = $DB->get_prefix();
+            foreach ($data as $table => $records) {
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    if (isset($sequences[$table])) {
+                        if (empty($records)) {
+                            $nextid = 1;
+                        } else {
+                            $lastrecord = end($records);
+                            $nextid = $lastrecord->id + 1;
+                        }
+                        if ($sequences[$table] != $nextid) {
+                            $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
+                        }
+
+                    } else {
+                        // some problem exists, fallback to standard code
+                        $DB->get_manager()->reset_sequence($table);
+                    }
+                }
+            }
+
+        } else if ($dbfamily === 'oracle') {
+            $sequences = self::get_sequencenames();
+            $sequences = array_map('strtoupper', $sequences);
+            $lookup = array_flip($sequences);
+
+            $current = array();
+            list($seqs, $params) = $DB->get_in_or_equal($sequences);
+            $sql = "SELECT sequence_name, last_number FROM user_sequences WHERE sequence_name $seqs";
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $seq) {
+                $table = $lookup[$seq->sequence_name];
+                $current[$table] = $seq->last_number;
+            }
+            $rs->close();
+
+            foreach ($data as $table => $records) {
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    $lastrecord = end($records);
+                    if ($lastrecord) {
+                        $nextid = $lastrecord->id + 1;
+                    } else {
+                        $nextid = 1;
+                    }
+                    if (!isset($current[$table])) {
+                        $DB->get_manager()->reset_sequence($table);
+                    } else if ($nextid == $current[$table]) {
+                        continue;
+                    }
+                    // reset as fast as possible - alternatively we could use http://stackoverflow.com/questions/51470/how-do-i-reset-a-sequence-in-oracle
+                    $seqname = $sequences[$table];
+                    $cachesize = $DB->get_manager()->generator->sequence_cache_size;
+                    $DB->change_database_structure("DROP SEQUENCE $seqname");
+                    $DB->change_database_structure("CREATE SEQUENCE $seqname START WITH $nextid INCREMENT BY 1 NOMAXVALUE CACHE $cachesize");
+                }
+            }
+
+        } else {
+            // note: does mssql support any kind of faster reset?
+            if (is_null($empties)) {
+                $empties = self::guess_unmodified_empty_tables();
+            }
+            foreach ($data as $table => $records) {
+                if (isset($empties[$table])) {
+                    continue;
+                }
+                if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                    $DB->get_manager()->reset_sequence($table);
+                }
+            }
+        }
+    }
+
+    /**
+     * Resets the database
+     * @static
+     * @return boolean Returns whether database has been modified or not
+     */
+    public static function reset_database() {
+        global $DB;
+
+        if (!is_null(self::$lastdbwrites) and self::$lastdbwrites == $DB->perf_get_writes()) {
+            return false;
+        }
+
+        $tables = $DB->get_tables(false);
+        if (!$tables or empty($tables['config'])) {
+            // not installed yet
+            return false;
+        }
+
+        if (!$data = self::get_tabledata()) {
+            // not initialised yet
+            return false;
+        }
+        if (!$structure = self::get_tablestructure()) {
+            // not initialised yet
+            return false;
+        }
+
+        $empties = self::guess_unmodified_empty_tables();
+
+        foreach ($data as $table => $records) {
+            if (empty($records)) {
+                if (isset($empties[$table])) {
+                    // table was not modified and is empty
+                } else {
+                    $DB->delete_records($table, array());
+                }
+                continue;
+            }
+
+            if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
+                $currentrecords = $DB->get_records($table, array(), 'id ASC');
+                $changed = false;
+                foreach ($records as $id => $record) {
+                    if (!isset($currentrecords[$id])) {
+                        $changed = true;
+                        break;
+                    }
+                    if ((array)$record != (array)$currentrecords[$id]) {
+                        $changed = true;
+                        break;
+                    }
+                    unset($currentrecords[$id]);
+                }
+                if (!$changed) {
+                    if ($currentrecords) {
+                        $lastrecord = end($records);
+                        $DB->delete_records_select($table, "id > ?", array($lastrecord->id));
+                        continue;
+                    } else {
+                        continue;
+                    }
+                }
+            }
+
+            $DB->delete_records($table, array());
+            foreach ($records as $record) {
+                $DB->import_record($table, $record, false, true);
+            }
+        }
+
+        // reset all next record ids - aka sequences
+        self::reset_all_database_sequences($empties);
+
+        // remove extra tables
+        foreach ($tables as $table) {
+            if (!isset($data[$table])) {
+                $DB->get_manager()->drop_table(new xmldb_table($table));
+            }
+        }
+
+        self::$lastdbwrites = $DB->perf_get_writes();
+
+        return true;
+    }
+
+    /**
+     * Purge dataroot directory
+     * @static
+     * @return void
+     */
+    public static function reset_dataroot() {
+        global $CFG;
+
+        $childclassname = self::get_framework() . '_util';
+
+        $handle = opendir($CFG->dataroot);
+        while (false !== ($item = readdir($handle))) {
+            if (in_array($item, $childclassname::$datarootskiponreset)) {
+                continue;
+            }
+            if (is_dir("$CFG->dataroot/$item")) {
+                remove_dir("$CFG->dataroot/$item", false);
+            } else {
+                unlink("$CFG->dataroot/$item");
+            }
+        }
+        closedir($handle);
+        make_temp_directory('');
+        make_cache_directory('');
+        make_cache_directory('htmlpurifier');
+        // Reset the cache API so that it recreates it's required directories as well.
+        cache_factory::reset();
+        // Purge all data from the caches. This is required for consistency.
+        // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
+        // and now we will purge any other caches as well.
+        cache_helper::purge_all();
+    }
+
+    /**
+     * Drop the whole test database
+     * @static
+     * @param boolean $displayprogress
+     */
+    protected static function drop_database($displayprogress = false) {
+        global $DB;
+
+        $tables = $DB->get_tables(false);
+        if (isset($tables['config'])) {
+            // config always last to prevent problems with interrupted drops!
+            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";
+        }
+    }
+
+    /**
+     * Drops the test framework dataroot
+     * @static
+     */
+    protected static function drop_dataroot() {
+        global $CFG;
+
+        $framework = self::get_framework();
+        $childclassname = $framework . '_util';
+
+        $files = scandir($CFG->dataroot . '/' . $framework);
+        foreach ($files as $file) {
+            if (in_array($file, $childclassname::$datarootskipondrop)) {
+                continue;
+            }
+            $path = $CFG->dataroot . '/' . $framework . '/' . $file;
+            if (is_dir($path)) {
+                remove_dir($path, false);
+            } else {
+                unlink($path);
+            }
+        }
+    }
+
+    /**
+     * Reset all database tables to default values.
+     * @static
+     * @return bool true if reset done, false if skipped
+     */
+    /**
+     * Calculate unique version hash for all plugins and core.
+     * @static
+     * @return string sha1 hash
+     */
+    public static function get_version_hash() {
+        global $CFG;
+
+        if (self::$versionhash) {
+            return self::$versionhash;
+        }
+
+        $versions = array();
+
+        // main version first
+        $version = null;
+        include($CFG->dirroot.'/version.php');
+        $versions['core'] = $version;
+
+        // modules
+        $mods = get_plugin_list('mod');
+        ksort($mods);
+        foreach ($mods as $mod => $fullmod) {
+            $module = new stdClass();
+            $module->version = null;
+            include($fullmod.'/version.php');
+            $versions[$mod] = $module->version;
+        }
+
+        // now the rest of plugins
+        $plugintypes = get_plugin_types();
+        unset($plugintypes['mod']);
+        ksort($plugintypes);
+        foreach ($plugintypes as $type => $unused) {
+            $plugs = get_plugin_list($type);
+            ksort($plugs);
+            foreach ($plugs as $plug => $fullplug) {
+                $plugin = new stdClass();
+                $plugin->version = null;
+                @include($fullplug.'/version.php');
+                $versions[$plug] = $plugin->version;
+            }
+        }
+
+        self::$versionhash = sha1(serialize($versions));
+
+        return self::$versionhash;
+    }
+
+}
similarity index 73%
rename from lib/phpunit/classes/block_generator.php
rename to lib/testing/generator/block_generator.php
index a1cb635..02a2288 100644 (file)
@@ -18,7 +18,7 @@
  * Block generator base class.
  *
  * @package    core
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
  * Extend in blocks/xxxx/tests/generator/lib.php as class block_xxxx_generator.
  *
  * @package    core
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-abstract class phpunit_block_generator {
-    /** @var phpunit_data_generator@var  */
+abstract class testing_block_generator {
+    /** @var testing_data_generator */
     protected $datagenerator;
 
     /** @var number of created instances */
     protected $instancecount = 0;
 
-    public function __construct(phpunit_data_generator $datagenerator) {
+    /**
+     * Dumb constructor to throw the deprecated notification
+     * @param testing_data_generator $datagenerator
+     */
+    public function __construct(testing_data_generator $datagenerator) {
         $this->datagenerator = $datagenerator;
     }
 
@@ -110,3 +114,26 @@ abstract class phpunit_block_generator {
      */
     abstract public function create_instance($record = null, array $options = null);
 }
+
+/**
+ * Deprecated in favour of testing_block_generator
+ *
+ * @deprecated since Moodle 2.5 MDL-37457 - please do not use this function any more.
+ * @todo       MDL-37517 This will be deleted in Moodle 2.7
+ * @see        testing_block_generator
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class phpunit_block_generator extends testing_block_generator {
+
+    /**
+     * Dumb constructor to throw the deprecated notification
+     * @param testing_data_generator $datagenerator
+     */
+    public function __construct(testing_data_generator $datagenerator) {
+        debugging('Class phpunit_block_generator is deprecated, please use class testing_block_generator instead', DEBUG_DEVELOPER);
+        parent::__construct($datagenerator);
+    }
+}
similarity index 94%
rename from lib/phpunit/classes/data_generator.php
rename to lib/testing/generator/data_generator.php
index 7e43c10..7f99a93 100644 (file)
  * Data generator.
  *
  * @package    core
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 
 /**
- * Data generator class for unit tests and other tools
- * that need to create fake test sites.
+ * Data generator class for unit tests and other tools that need to create fake test sites.
  *
  * @package    core
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class phpunit_data_generator {
+class testing_data_generator {
     protected $usercounter = 0;
     protected $categorycount = 0;
     protected $cohortcount = 0;
@@ -84,7 +83,7 @@ EOD;
         $this->coursecount = 0;
         $this->scalecount = 0;
 
-        foreach($this->generators as $generator) {
+        foreach ($this->generators as $generator) {
             $generator->reset();
         }
     }
@@ -395,11 +394,11 @@ EOD;
         $record = (array)$record;
 
         if (empty($record['course'])) {
-            throw new coding_exception('course must be present in phpunit_util::create_course_section() $record');
+            throw new coding_exception('course must be present in testing_data_generator::create_course_section() $record');
         }
 
         if (!isset($record['section'])) {
-            throw new coding_exception('section must be present in phpunit_util::create_course_section() $record');
+            throw new coding_exception('section must be present in testing_data_generator::create_course_section() $record');
         }
 
         course_create_sections_if_missing($record['course'], $record['section']);
@@ -437,8 +436,8 @@ EOD;
      * At the very least it needs to contain courseid.
      * Default values are added for name, description, and descriptionformat if they are not present.
      *
-     * This function calls {@see groups_create_group()} to create the group within the database.
-     *
+     * This function calls groups_create_group() to create the group within the database.
+     * @see groups_create_group
      * @param array|stdClass $record
      * @return stdClass group record
      */
@@ -453,7 +452,7 @@ EOD;
         $record = (array)$record;
 
         if (empty($record['courseid'])) {
-            throw new coding_exception('courseid must be present in phpunit_util::create_group() $record');
+            throw new coding_exception('courseid must be present in testing_data_generator::create_group() $record');
         }
 
         if (!isset($record['name'])) {
@@ -480,8 +479,8 @@ EOD;
      * At the very least it needs to contain courseid.
      * Default values are added for name, description, and descriptionformat if they are not present.
      *
-     * This function calls {@see groups_create_grouping()} to create the grouping within the database.
-     *
+     * This function calls groups_create_grouping() to create the grouping within the database.
+     * @see groups_create_grouping
      * @param array|stdClass $record
      * @return stdClass grouping record
      */
@@ -496,7 +495,7 @@ EOD;
         $record = (array)$record;
 
         if (empty($record['courseid'])) {
-            throw new coding_exception('courseid must be present in phpunit_util::create_grouping() $record');
+            throw new coding_exception('courseid must be present in testing_data_generator::create_grouping() $record');
         }
 
         if (!isset($record['name'])) {
@@ -602,3 +601,24 @@ EOD;
         return true;
     }
 }
+
+/**
+ * Deprecated in favour of testing_data_generator
+ *
+ * @deprecated since Moodle 2.5 MDL-37457 - please do not use this function any more.
+ * @todo       MDL-37517 This will be deleted in Moodle 2.7
+ * @see        testing_data_generator
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class phpunit_data_generator extends testing_data_generator {
+
+    /**
+     * Dumb constructor to throw the deprecated notification
+     */
+    public function __construct() {
+        debugging('Class phpunit_data_generator is deprecated, please use class testing_module_generator instead', DEBUG_DEVELOPER);
+    }
+}
diff --git a/lib/testing/generator/lib.php b/lib/testing/generator/lib.php
new file mode 100644 (file)
index 0000000..3afbff2
--- /dev/null
@@ -0,0 +1,31 @@
+<?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/>.
+
+/**
+ * Adds data generator support
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php!
+
+require_once(__DIR__.'/data_generator.php');
+require_once(__DIR__.'/module_generator.php');
+require_once(__DIR__.'/block_generator.php');
+
similarity index 74%
rename from lib/phpunit/classes/module_generator.php
rename to lib/testing/generator/module_generator.php
index ffa20f9..c499f96 100644 (file)
@@ -18,7 +18,7 @@
  * Module generator base class.
  *
  * @package    core
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
  * Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator.
  *
  * @package    core
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-abstract class phpunit_module_generator {
-    /** @var phpunit_data_generator@var  */
+abstract class testing_module_generator {
+
+    /**
+     * @var testing_data_generator
+     */
     protected $datagenerator;
 
-    /** @var number of created instances */
+    /**
+     * @var number of created instances
+     */
     protected $instancecount = 0;
 
-    public function __construct(phpunit_data_generator $datagenerator) {
+    /**
+     * Dumb constructor to throw the deprecated notification
+     * @param testing_data_generator $datagenerator
+     */
+    public function __construct(testing_data_generator $datagenerator) {
         $this->datagenerator = $datagenerator;
     }
 
@@ -73,9 +82,9 @@ abstract class phpunit_module_generator {
 
     /**
      * Create course module and link it to course
-     * @param int $courseid
-     * @param array $options: section, visible
-     * @return int $cm instance id
+     * @param integer $courseid
+     * @param array $options section, visible
+     * @return integer $cm instance id
      */
     protected function precreate_course_module($courseid, array $options) {
         global $DB, $CFG;
@@ -94,7 +103,7 @@ abstract class phpunit_module_generator {
         $cm->added              = time();
 
         $columns = $DB->get_columns('course_modules');
-        foreach ($options as $key=>$value) {
+        foreach ($options as $key => $value) {
             if ($key === 'id' or !isset($columns[$key])) {
                 continue;
             }
@@ -140,3 +149,26 @@ abstract class phpunit_module_generator {
      */
     abstract public function create_instance($record = null, array $options = null);
 }
+
+/**
+ * Deprecated in favour of testing_module_generator
+ *
+ * @deprecated since Moodle 2.5 MDL-37457 - please do not use this function any more.
+ * @todo       MDL-37517 This will be deleted in Moodle 2.7
+ * @see        testing_module_generator
+ * @package    core
+ * @category   test
+ * @copyright  2012 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class phpunit_module_generator extends testing_module_generator {
+
+    /**
+     * Dumb constructor to throw the deprecated notification
+     * @param testing_data_generator $datagenerator
+     */
+    public function __construct(testing_data_generator $datagenerator) {
+        debugging('Class phpunit_module_generator is deprecated, please use class testing_module_generator instead', DEBUG_DEVELOPER);
+        parent::__construct($datagenerator);
+    }
+}
diff --git a/lib/testing/lib.php b/lib/testing/lib.php
new file mode 100644 (file)
index 0000000..0713d6b
--- /dev/null
@@ -0,0 +1,130 @@
+<?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/>.
+
+/**
+ * Testing general functions
+ *
+ * Note: these functions must be self contained and must not rely on any library or include
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Returns relative path against current working directory,
+ * to be used for shell execution hints.
+ * @param string $moodlepath starting with "/", ex: "/admin/tool/cli/init.php"
+ * @return string path relative to current directory or absolute path
+ */
+function testing_cli_argument_path($moodlepath) {
+    global $CFG;
+
+    if (isset($CFG->admin) and $CFG->admin !== 'admin') {
+        $moodlepath = preg_replace('|^/admin/|', "/$CFG->admin/", $moodlepath);
+    }
+
+    $cwd = getcwd();
+    if (substr($cwd, -1) !== DIRECTORY_SEPARATOR) {
+        $cwd .= DIRECTORY_SEPARATOR;
+    }
+    $path = realpath($CFG->dirroot.$moodlepath);
+
+    if (strpos($path, $cwd) === 0) {
+        $path = substr($path, strlen($cwd));
+    }
+
+    if (testing_is_cygwin()) {
+        $path = str_replace('\\', '/', $path);
+    }
+
+    return $path;
+}
+
+/**
+ * Try to change permissions to $CFG->dirroot or $CFG->dataroot if possible
+ * @param string $file
+ * @return bool success
+ */
+function testing_fix_file_permissions($file) {
+    global $CFG;
+
+    $permissions = fileperms($file);
+    if ($permissions & $CFG->filepermissions != $CFG->filepermissions) {
+        $permissions = $permissions | $CFG->filepermissions;
+        return chmod($file, $permissions);
+    }
+
+    return true;
+}
+
+/**
+ * Find out if running under Cygwin on Windows.
+ * @return bool
+ */
+function testing_is_cygwin() {
+    if (empty($_SERVER['OS']) or $_SERVER['OS'] !== 'Windows_NT') {
+        return false;
+
+    } else if (!empty($_SERVER['SHELL']) and $_SERVER['SHELL'] === '/bin/bash') {
+        return true;
+
+    } else if (!empty($_SERVER['TERM']) and $_SERVER['TERM'] === 'cygwin') {
+        return true;
+
+    } else {
+        return false;
+    }
+}
+
+/**
+ * Mark empty dataroot to be used for testing.
+ * @param string $dataroot  The dataroot directory
+ * @param string $framework The test framework
+ * @return void
+ */
+function testing_initdataroot($dataroot, $framework) {
+    global $CFG;
+
+    $filename = $dataroot . '/' . $framework . 'testdir.txt';
+
+    umask(0);
+    if (!file_exists($filename)) {
+        file_put_contents($filename, 'Contents of this directory are used during tests only, do not delete this file!');
+    }
+    testing_fix_file_permissions($filename);
+
+    $varname = $framework . '_dataroot';
+    $datarootdir = $CFG->{$varname} . '/' . $framework;
+    if (!file_exists($datarootdir)) {
+        mkdir($datarootdir, $CFG->directorypermissions);
+    }
+}
+
+/**
+ * Prints an error and stops execution
+ *
+ * @param integer $errorcode
+ * @param string $text
+ * @return void exits
+ */
+function testing_error($errorcode, $text = '') {
+
+    // do not write to error stream because we need the error message in PHP exec result from web ui
+    echo($text."\n");
+    exit($errorcode);
+}
similarity index 98%
rename from lib/phpunit/tests/generator_test.php
rename to lib/testing/tests/generator_test.php
index 0213875..19acca6 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * PHPUnit integration tests
+ * Data generators tests
  *
  * @package    core
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -30,11 +30,11 @@ defined('MOODLE_INTERNAL') || die();
  * Test data generator
  *
  * @package    core
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_phpunit_generator_testcase extends advanced_testcase {
+class core_test_generator_testcase extends advanced_testcase {
     public function test_create() {
         global $DB;
 
index 915e0bb..dec5fdd 100644 (file)
@@ -872,7 +872,26 @@ class moodlelib_testcase extends advanced_testcase {
     function test_clean_param_file() {
         $this->assertEquals(clean_param('correctfile.txt', PARAM_FILE), 'correctfile.txt');
         $this->assertEquals(clean_param('b\'a<d`\\/fi:l>e.t"x|t', PARAM_FILE), 'badfile.txt');
-        $this->assertEquals(clean_param('../parentdirfile.txt', PARAM_FILE), 'parentdirfile.txt');
+        $this->assertEquals(clean_param('../parentdirfile.txt', PARAM_FILE), '..parentdirfile.txt');
+        $this->assertEquals(clean_param('../../grandparentdirfile.txt', PARAM_FILE), '....grandparentdirfile.txt');
+        $this->assertEquals(clean_param('..\winparentdirfile.txt', PARAM_FILE), '..winparentdirfile.txt');
+        $this->assertEquals(clean_param('../../wingrandparentdir.txt', PARAM_FILE), '....wingrandparentdir.txt');
+        $this->assertEquals(clean_param('myfile.a.b.txt', PARAM_FILE), 'myfile.a.b.txt');
+        $this->assertEquals(clean_param('myfile..a..b.txt', PARAM_FILE), 'myfile..a..b.txt');
+        $this->assertEquals(clean_param('myfile.a..b...txt', PARAM_FILE), 'myfile.a..b...txt');
+        $this->assertEquals(clean_param('myfile.a.txt', PARAM_FILE), 'myfile.a.txt');
+        $this->assertEquals(clean_param('myfile...txt', PARAM_FILE), 'myfile...txt');
+        $this->assertEquals(clean_param('...jpg', PARAM_FILE), '...jpg');
+        $this->assertEquals(clean_param('.a.b.', PARAM_FILE), '.a.b.');
+        $this->assertEquals(clean_param('.', PARAM_FILE), '');
+        $this->assertEquals(clean_param('..', PARAM_FILE), '');
+        $this->assertEquals(clean_param('...', PARAM_FILE), '...');
+        $this->assertEquals(clean_param('. . . .', PARAM_FILE), '. . . .');
+        $this->assertEquals(clean_param('dontrtrim.me. .. .. . ', PARAM_FILE), 'dontrtrim.me. .. .. . ');
+        $this->assertEquals(clean_param(' . .dontltrim.me', PARAM_FILE), ' . .dontltrim.me');
+        $this->assertEquals(clean_param("here is a tab\t.txt", PARAM_FILE), 'here is a tab.txt');
+        $this->assertEquals(clean_param("here is a line\r\nbreak.txt", PARAM_FILE), 'here is a linebreak.txt');
+
         //The following behaviours have been maintained although they seem a little odd
         $this->assertEquals(clean_param('funny:thing', PARAM_FILE), 'funnything');
         $this->assertEquals(clean_param('./currentdirfile.txt', PARAM_FILE), '.currentdirfile.txt');
@@ -881,6 +900,26 @@ class moodlelib_testcase extends advanced_testcase {
         $this->assertEquals(clean_param('~/myfile.txt', PARAM_FILE), '~myfile.txt');
     }
 
+    function test_clean_param_path() {
+        $this->assertEquals(clean_param('correctfile.txt', PARAM_PATH), 'correctfile.txt');
+        $this->assertEquals(clean_param('b\'a<d`\\/fi:l>e.t"x|t', PARAM_PATH), 'bad/file.txt');
+        $this->assertEquals(clean_param('../parentdirfile.txt', PARAM_PATH), '/parentdirfile.txt');
+        $this->assertEquals(clean_param('../../grandparentdirfile.txt', PARAM_PATH), '/grandparentdirfile.txt');
+        $this->assertEquals(clean_param('..\winparentdirfile.txt', PARAM_PATH), '/winparentdirfile.txt');
+        $this->assertEquals(clean_param('../../wingrandparentdir.txt', PARAM_PATH), '/wingrandparentdir.txt');
+        $this->assertEquals(clean_param('funny:thing', PARAM_PATH), 'funnything');
+        $this->assertEquals(clean_param('./././here', PARAM_PATH), './here');
+        $this->assertEquals(clean_param('./currentdirfile.txt', PARAM_PATH), './currentdirfile.txt');
+        $this->assertEquals(clean_param('c:\temp\windowsfile.txt', PARAM_PATH), 'c/temp/windowsfile.txt');
+        $this->assertEquals(clean_param('/home/user/linuxfile.txt', PARAM_PATH), '/home/user/linuxfile.txt');
+        $this->assertEquals(clean_param('/home../user ./.linuxfile.txt', PARAM_PATH), '/home../user ./.linuxfile.txt');
+        $this->assertEquals(clean_param('~/myfile.txt', PARAM_PATH), '~/myfile.txt');
+        $this->assertEquals(clean_param('~/../myfile.txt', PARAM_PATH), '~/myfile.txt');
+        $this->assertEquals(clean_param('/..b../.../myfile.txt', PARAM_PATH), '/..b../.../myfile.txt');
+        $this->assertEquals(clean_param('..b../.../myfile.txt', PARAM_PATH), '..b../.../myfile.txt');
+        $this->assertEquals(clean_param('/super//slashes///', PARAM_PATH), '/super/slashes/');
+    }
+
     function test_clean_param_username() {
         global $CFG;
         $currentstatus =  $CFG->extendedusernamechars;
index f7f2fca..5e29d5d 100644 (file)
@@ -179,8 +179,23 @@ class web_testcase extends advanced_testcase {
     }
 
     function test_out_as_local_url() {
+        global $CFG;
+        // Test http url.
         $url1 = new moodle_url('/lib/tests/weblib_test.php');
         $this->assertEquals('/lib/tests/weblib_test.php', $url1->out_as_local_url());
+
+        // Test https url.
+        $httpswwwroot = str_replace("http://", "https://", $CFG->wwwroot);
+        $url2 = new moodle_url($httpswwwroot.'/login/profile.php');
+        $this->assertEquals('/login/profile.php', $url2->out_as_local_url());
+
+        // Test http url matching wwwroot.
+        $url3 = new moodle_url($CFG->wwwroot);
+        $this->assertEquals('', $url3->out_as_local_url());
+
+        // Test http url matching wwwroot ending with slash (/).
+        $url3 = new moodle_url($CFG->wwwroot.'/');
+        $this->assertEquals('/', $url3->out_as_local_url());
     }
 
     /**
@@ -192,6 +207,31 @@ class web_testcase extends advanced_testcase {
         $url2->out_as_local_url();
     }
 
+    /**
+     * You should get error with modified url
+     *
+     * @expectedException coding_exception
+     * @return void
+     */
+    public function test_modified_url_out_as_local_url_error() {
+        global $CFG;
+
+        $modifiedurl = $CFG->wwwroot.'1';
+        $url3 = new moodle_url($modifiedurl.'/login/profile.php');
+        $url3->out_as_local_url();
+    }
+
+    /**
+     * Try get local url from external https url and you should get error
+     *
+     * @expectedException coding_exception
+     * @return void
+     */
+    public function test_https_out_as_local_url_error() {
+        $url4 = new moodle_url('https://www.google.com/lib/tests/weblib_test.php');
+        $url4->out_as_local_url();
+    }
+
     public function test_clean_text() {
         $text = "lala <applet>xx</applet>";
         $this->assertEquals($text, clean_text($text, FORMAT_PLAIN));
index 9b93a3a..4ee520b 100644 (file)
@@ -747,12 +747,18 @@ class moodle_url {
         global $CFG;
 
         $url = $this->out($escaped, $overrideparams);
-
-        if (strpos($url, $CFG->wwwroot) !== 0) {
+        $httpswwwroot = str_replace("http://", "https://", $CFG->wwwroot);
+
+        // $url should be equal to wwwroot or httpswwwroot. If not then throw exception.
+        if (($url === $CFG->wwwroot) || (strpos($url, $CFG->wwwroot.'/') === 0)) {
+            $localurl = substr($url, strlen($CFG->wwwroot));
+            return !empty($localurl) ? $localurl : '';
+        } else if (($url === $httpswwwroot) || (strpos($url, $httpswwwroot.'/') === 0)) {
+            $localurl = substr($url, strlen($httpswwwroot));
+            return !empty($localurl) ? $localurl : '';
+        } else {
             throw new coding_exception('out_as_local_url called on a non-local URL');
         }
-
-        return str_replace($CFG->wwwroot, '', $url);
     }
 
     /**
index edb655f..1c4ba83 100644 (file)
@@ -10,7 +10,9 @@ YUI.add('moodle-core-blocks', function(Y) {
         LIGHTBOX : 'lightbox',
         REGIONCONTENT : 'region-content',
         SKIPBLOCK : 'skip-block',
-        SKIPBLOCKTO : 'skip-block-to'
+        SKIPBLOCKTO : 'skip-block-to',
+        MYINDEX : 'page-my-index',
+        REGIONMAIN : 'region-main'
     }
 
     var DRAGBLOCK = function() {
@@ -26,6 +28,15 @@ YUI.add('moodle-core-blocks', function(Y) {
             this.samenodeclass = CSS.BLOCK;
             this.parentnodeclass = CSS.REGIONCONTENT;
 
+            // Add relevant classes and ID to 'content' block region on My Home page.
+            var myhomecontent = Y.Node.all('body#'+CSS.MYINDEX+' #'+CSS.REGIONMAIN+' > .'+CSS.REGIONCONTENT);
+            if (myhomecontent.size() > 0) {
+                var contentregion = myhomecontent.item(0);
+                contentregion.addClass(CSS.BLOCKREGION);
+                contentregion.set('id', CSS.REGIONCONTENT);
+                contentregion.one('div').addClass(CSS.REGIONCONTENT);
+            }
+
             // Initialise blocks dragging
             // Find all block regions on the page
             var blockregionlist = Y.Node.all('div.'+CSS.BLOCKREGION);
index 46cdc4e..033cd7d 100644 (file)
@@ -1,14 +1,10 @@
 YUI.add('moodle-core-formchangechecker',
     function(Y) {
-        // The CSS selectors we use
-        var CSS = {
-        };
-
-        var FORMCHANGECHECKERNAME = 'core-formchangechecker';
+        var FORMCHANGECHECKERNAME = 'core-formchangechecker',
 
-        var FORMCHANGECHECKER = function() {
-            FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
-        }
+            FORMCHANGECHECKER = function() {
+                FORMCHANGECHECKER.superclass.constructor.apply(this, arguments);
+            };
 
         Y.extend(FORMCHANGECHECKER, Y.Base, {
 
@@ -18,11 +14,11 @@ YUI.add('moodle-core-formchangechecker',
                 /**
                  * Initialize the module
                  */
-                initializer : function(config) {
-                    var formid = 'form#' + this.get('formid');
+                initializer : function() {
+                    var formid = 'form#' + this.get('formid'),
+                        currentform = Y.one(formid);
 
                     // Add change events to the form elements
-                    var currentform = Y.one(formid);
                     currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'input', this);
                     currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'textarea', this);
                     currentform.delegate('change', M.core_formchangechecker.set_form_changed, 'select', this);
@@ -48,18 +44,15 @@ YUI.add('moodle-core-formchangechecker',
                  * get_form_dirty_state function later.
                  */
                 store_initial_value : function(e) {
+                    var thisevent;
                     if (e.target.hasClass('ignoredirty')) {
                         // Don't warn on elements with the ignoredirty class
                         return;
                     }
                     if (M.core_formchangechecker.get_form_dirty_state()) {
-                        // Clear the store_initial_value listeners as the form is already dirty so
-                        // we no longer need to call this function
-                        var formid = 'form#' + this.get('formid');
-
                         // Detach all listen events to prevent duplicate initial value setting
-                        var thisevent;
-                        while (thisevent = this.initialvaluelisteners.shift()) {
+                        while (this.initialvaluelisteners.length) {
+                            thisevent = this.initialvaluelisteners.shift();
                             thisevent.detach();
                         }
 
@@ -71,7 +64,7 @@ YUI.add('moodle-core-formchangechecker',
                     M.core_formchangechecker.stateinformation.focused_element = {
                         element : e.target,
                         initial_value : e.target.get('value')
-                    }
+                    };
                 }
             },
             {
@@ -92,7 +85,7 @@ YUI.add('moodle-core-formchangechecker',
             var formchangechecker = new FORMCHANGECHECKER(config);
             M.core_formchangechecker.instances.push(formchangechecker);
             return formchangechecker;
-        }
+        };
 
         // Store state information
         M.core_formchangechecker.stateinformation = [];
@@ -110,14 +103,14 @@ YUI.add('moodle-core-formchangechecker',
             // Once the form has been marked as dirty, we no longer need to keep track of form elements
             // which haven't yet blurred
             delete M.core_formchangechecker.stateinformation.focused_element;
-        }
+        };
 
         /**
          * Set the form submitted state to true
          */
         M.core_formchangechecker.set_form_submitted = function() {
             M.core_formchangechecker.stateinformation.formsubmitted = 1;
-        }
+        };
 
         /**
          * Attempt to determine whether the form has been modified in any way and
@@ -126,7 +119,8 @@ YUI.add('moodle-core-formchangechecker',
          * @return Integer 1 is the form is dirty; 0 if not
          */
         M.core_formchangechecker.get_form_dirty_state = function() {
-            var state = M.core_formchangechecker.stateinformation;
+            var state = M.core_formchangechecker.stateinformation,
+                editor;
 
             // If the form was submitted, then return a non-dirty state
             if (state.formsubmitted) {
@@ -141,7 +135,7 @@ YUI.add('moodle-core-formchangechecker',
             // If a field has been focused and changed, but still has focus then the browser won't fire the
             // onChange event. We check for this eventuality here
             if (state.focused_element) {
-                if (state.focused_element.element.get('value') != state.focused_element.initial_value) {
+                if (state.focused_element.element.get('value') !== state.focused_element.initial_value) {
                     return 1;
                 }
             }
@@ -149,8 +143,8 @@ YUI.add('moodle-core-formchangechecker',
             // Handle TinyMCE editor instances
             // We can't add a listener in the initializer as the editors may not have been created by that point
             // so we do so here instead
-            if (typeof tinyMCE != 'undefined') {
-                for (var editor in tinyMCE.editors) {
+            if (typeof tinyMCE !== 'undefined') {
+                for (editor in tinyMCE.editors) {
                     if (tinyMCE.editors[editor].isDirty()) {
                         return 1;
                     }
index cd3b558..fde60a4 100644 (file)
@@ -341,7 +341,9 @@ if (isloggedin() and !isguestuser()) {
     echo $OUTPUT->box_end();
 } else {
     include("index_form.html");
-    if (!empty($CFG->loginpageautofocus)) {
+    if ($errormsg) {
+        $PAGE->requires->js_init_call('M.util.focus_login_error', null, true);
+    } else if (!empty($CFG->loginpageautofocus)) {
         //focus username or password
         $PAGE->requires->js_init_call('M.util.focus_login_form', null, true);
     }
index 5c9f34d..c5f75e8 100644 (file)
@@ -30,9 +30,10 @@ if (!empty($CFG->loginpasswordautocomplete)) {
         </div>
         <?php
           if (!empty($errormsg)) {
-              echo '<div class="loginerrors">';
+              echo html_writer::start_tag('div', array('class' => 'loginerrors'));
+              echo html_writer::link('#', $errormsg, array('id' => 'loginerrormessage', 'class' => 'accesshide'));
               echo $OUTPUT->error_text($errormsg);
-              echo '</div>';
+              echo html_writer::end_tag('div');
           }
         ?>
         <form action="<?php echo $CFG->httpswwwroot; ?>/login/index.php" method="post" id="login" <?php echo $autocomplete; ?> >
index 2c56aa7..9ed302c 100644 (file)
@@ -203,7 +203,9 @@ class assign_feedback_comments extends assign_feedback_plugin {
             }
         }
 
-        $mform->addElement('editor', 'assignfeedbackcomments_editor', '', null, null);
+        $mform->addElement('editor', 'assignfeedbackcomments_editor', html_writer::tag('span', $this->get_name(),
+            array('class' => 'accesshide')), null, null);
+
         return true;
     }
 
index 63fd674..d79053a 100644 (file)
@@ -143,8 +143,8 @@ class assign_feedback_file extends assign_feedback_plugin {
                                                   'assignfeedback_file',
                                                   ASSIGNFEEDBACK_FILE_FILEAREA,
                                                   $gradeid);
-
-        $mform->addElement('filemanager', $elementname . '_filemanager', '', null, $fileoptions);
+        $mform->addElement('filemanager', $elementname . '_filemanager', html_writer::tag('span', $this->get_name(),
+            array('class' => 'accesshide')), null, $fileoptions);
 
         return true;
     }
index b933f0a..07e04c2 100644 (file)
@@ -358,7 +358,7 @@ function assign_print_overview($courses, &$htmlarray) {
     $rs->close();
 
     // Get all user submissions, indexed by assignment id.
-    $dbparams = array_merge(array($USER->id, $USER->id), $assignmentidparams);
+    $dbparams = array($USER->id, $USER->id);
     $mysubmissions = $DB->get_records_sql('SELECT
                                                a.id AS assignment,
                                                a.nosubmissions AS nosubmissions,
@@ -372,8 +372,8 @@ function assign_print_overview($courses, &$htmlarray) {
                                                g.userid = ?
                                            LEFT JOIN {assign_submission} s ON
                                                s.assignment = a.id AND
-                                               s.userid = ? AND
-                                               a.id ' . $sqlassignmentids, $dbparams);
+                                               s.userid = ?
+                                           WHERE a.id ' . $sqlassignmentids, $dbparams);
 
     foreach ($assignments as $assignment) {
         // Do not show assignments that are not open.
index 16364e2..33f97be 100644 (file)
@@ -110,7 +110,12 @@ class assign_submission_comments extends assign_submission_plugin {
      * @return bool was it a success? (false will trigger a rollback)
      */
     public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) {
-        // No settings to upgrade.
+        if ($oldassignment->assignmenttype == 'upload') {
+            // Disable if allow notes was not enabled.
+            if (!$oldassignment->var2) {
+                $this->disable();
+            }
+        }
         return true;
     }
 
index 39c3561..f6e1ba2 100644 (file)
@@ -166,7 +166,9 @@ class assign_submission_file extends assign_submission_plugin {
                                                   'assignsubmission_file',
                                                   ASSIGNSUBMISSION_FILE_FILEAREA,
                                                   $submissionid);
-        $mform->addElement('filemanager', 'files_filemanager', '', null, $fileoptions);
+        $mform->addElement('filemanager', 'files_filemanager', html_writer::tag('span', $this->get_name(),
+            array('class' => 'accesshide')), null, $fileoptions);
+
         return true;
     }
 
@@ -342,13 +344,31 @@ class assign_submission_file extends assign_submission_plugin {
      * @return bool Was it a success? (false will trigger rollback)
      */
     public function upgrade_settings(context $oldcontext, stdClass $oldassignment, & $log) {
+        global $DB;
+
         if ($oldassignment->assignmenttype == 'uploadsingle') {
             $this->set_config('maxfilesubmissions', 1);
             $this->set_config('maxsubmissionsizebytes', $oldassignment->maxbytes);
             return true;
-        } else {
+        } else if ($oldassignment->assignmenttype == 'upload') {
             $this->set_config('maxfilesubmissions', $oldassignment->var1);
             $this->set_config('maxsubmissionsizebytes', $oldassignment->maxbytes);
+
+            // Advanced file upload uses a different setting to do the same thing.
+            $DB->set_field('assign',
+                           'submissiondrafts',
+                           $oldassignment->var4,
+                           array('id'=>$this->assignment->get_instance()->id));
+
+            // Convert advanced file upload "hide description before due date" setting.
+            $alwaysshow = 0;
+            if (!$oldassignment->var3) {
+                $alwaysshow = 1;
+            }
+            $DB->set_field('assign',
+                           'alwaysshowdescription',
+                           $alwaysshow,
+                           array('id'=>$this->assignment->get_instance()->id));
             return true;
         }
     }
index edbe85a..57e51ab 100644 (file)
@@ -95,7 +95,9 @@ class assign_submission_onlinetext extends assign_submission_plugin {
                                              'assignsubmission_onlinetext',
                                              ASSIGNSUBMISSION_ONLINETEXT_FILEAREA,
                                              $submissionid);
-        $mform->addElement('editor', 'onlinetext_editor', '', null, $editoroptions);
+        $mform->addElement('editor', 'onlinetext_editor', html_writer::tag('span', $this->get_name(),
+            array('class' => 'accesshide')), null, $editoroptions);
+
         return true;
     }
 
index 43eb7d0..9e8e8ed 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * assign module PHPUnit data generator class
+ * assign module data generator class
  *
  * @package mod_assign
- * @category phpunit
+ * @category test
  * @copyright 2012 Paul Charsley
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mod_assign_generator extends phpunit_module_generator {
+class mod_assign_generator extends testing_module_generator {
 
     /**
      * Create new assign module instance
index 8454229..51fb6eb 100644 (file)
@@ -136,7 +136,7 @@ class mod_assign_upgradelib_testcase extends advanced_testcase {
         $plugin = $assign->get_submission_plugin_by_type('onlinetext');
         $this->assertEmpty($plugin->is_enabled());
         $plugin = $assign->get_submission_plugin_by_type('comments');
-        $this->assertNotEmpty($plugin->is_enabled());
+        $this->assertEmpty($plugin->is_enabled());
         $plugin = $assign->get_submission_plugin_by_type('file');
         $this->assertNotEmpty($plugin->is_enabled());
         $plugin = $assign->get_feedback_plugin_by_type('comments');
index 055457b..abffb4e 100644 (file)
@@ -173,6 +173,24 @@ class assign_upgrade_manager {
                                                        array('areaid'=>$gradingarea->id));
             }
 
+            // Upgrade availability data.
+            $DB->set_field('course_modules_avail_fields',
+                           'coursemoduleid',
+                           $newcoursemodule->id,
+                           array('coursemoduleid'=>$oldcoursemodule->id));
+            $DB->set_field('course_modules_availability',
+                           'coursemoduleid',
+                           $newcoursemodule->id,
+                           array('coursemoduleid'=>$oldcoursemodule->id));
+            $DB->set_field('course_modules_availability',
+                           'sourcecmid',
+                           $newcoursemodule->id,
+                           array('sourcecmid'=>$oldcoursemodule->id));
+            $DB->set_field('course_sections_availability',
+                           'sourcecmid',
+                           $newcoursemodule->id,
+                           array('sourcecmid'=>$oldcoursemodule->id));
+
             // Upgrade completion data.
             $DB->set_field('course_modules_completion',
                            'coursemoduleid',
index ea07132..9bd8b22 100644 (file)
@@ -18,7 +18,7 @@
  * mod_assignment data generator
  *
  * @package    mod_assignment
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -27,14 +27,14 @@ defined('MOODLE_INTERNAL') || die();
 
 
 /**
- * Assignment module PHPUnit data generator class
+ * Assignment module data generator class
  *
  * @package    mod_assignment
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mod_assignment_generator extends phpunit_module_generator {
+class mod_assignment_generator extends testing_module_generator {
 
     /**
      * Create new assignment module instance
index 4ff538d..389769d 100644 (file)
@@ -18,7 +18,7 @@
  * mod_data data generator
  *
  * @package    mod_data
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -27,14 +27,14 @@ defined('MOODLE_INTERNAL') || die();
 
 
 /**
- * Page module PHPUnit data generator class
+ * Database module data generator class
  *
  * @package    mod_data
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mod_data_generator extends phpunit_module_generator {
+class mod_data_generator extends testing_module_generator {
 
     /**
      * Create new data module instance
index ea00ab2..903c9a1 100644 (file)
@@ -112,6 +112,7 @@ class restore_forum_activity_task extends restore_activity_task {
         $rules[] = new restore_log_rule('forum', 'delete discussion', 'view.php?id={course_module}', '{forum}');
         $rules[] = new restore_log_rule('forum', 'add post', 'discuss.php?d={forum_discussion}&parent={forum_post}', '{forum_post}');
         $rules[] = new restore_log_rule('forum', 'update post', 'discuss.php?d={forum_discussion}#p{forum_post}&parent={forum_post}', '{forum_post}');
+        $rules[] = new restore_log_rule('forum', 'update post', 'discuss.php?d={forum_discussion}&parent={forum_post}', '{forum_post}');
         $rules[] = new restore_log_rule('forum', 'prune post', 'discuss.php?d={forum_discussion}', '{forum_post}');
         $rules[] = new restore_log_rule('forum', 'delete post', 'discuss.php?d={forum_discussion}', '[post]');
 
index 369b593..7f98768 100644 (file)
@@ -18,7 +18,7 @@
  * mod_forum data generator
  *
  * @package    mod_forum
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -27,14 +27,14 @@ defined('MOODLE_INTERNAL') || die();
 
 
 /**
- * Assignment module PHPUnit data generator class
+ * Forum module data generator class
  *
  * @package    mod_forum
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mod_forum_generator extends phpunit_module_generator {
+class mod_forum_generator extends testing_module_generator {
 
     /**
      * Create new forum module instance
index f4addcc..10b8541 100644 (file)
@@ -2309,9 +2309,14 @@ abstract class lesson_page extends lesson_base {
         }
         if (count($this->answers)>0) {
             $count = 0;
+            $qtype = $properties->qtype;
             foreach ($this->answers as $answer) {
-                $properties->{'answer_editor['.$count.']'} = array('text'=>$answer->answer, 'format'=>$answer->answerformat);
-                $properties->{'response_editor['.$count.']'} = array('text'=>$answer->response, 'format'=>$answer->responseformat);
+                $properties->{'answer_editor['.$count.']'} = array('text' => $answer->answer, 'format' => $answer->answerformat);
+                if ($qtype != LESSON_PAGE_MATCHING) {
+                    $properties->{'response_editor['.$count.']'} = array('text' => $answer->response, 'format' => $answer->responseformat);
+                } else {
+                    $properties->{'response_editor['.$count.']'} = $answer->response;
+                }
                 $properties->{'jumpto['.$count.']'} = $answer->jumpto;
                 $properties->{'score['.$count.']'} = $answer->score;
                 $count++;
index 9e475a5..0944e3e 100644 (file)
@@ -213,6 +213,7 @@ class lesson_page_type_essay extends lesson_page {
             } else {
                 $essayinfo = new stdClass();
                 $essayinfo->answer = get_string("didnotanswerquestion", "lesson");
+                $essayinfo->answerformat = null;
             }
 
             if (isset($pagestats[$this->properties->id])) {
index 82769f6..00b0dcb 100644 (file)
@@ -72,16 +72,17 @@ class lesson_page_type_matching extends lesson_page {
         foreach ($answers as $answer) {
             // get all the response
             if ($answer->response != NULL) {
-                $responses[] = trim($answer->response);
+                $responses[$answer->id] = trim($answer->response);
             }
         }
 
         $responseoptions = array(''=>get_string('choosedots'));
         if (!empty($responses)) {
-            shuffle($responses);
-            $responses = array_unique($responses);
-            foreach ($responses as $response) {
-                $responseoptions[htmlspecialchars(trim($response))] = $response;
+            $shuffleresponses = $responses;
+            shuffle($shuffleresponses);
+            foreach ($shuffleresponses as  $response) {
+                $key = array_search($response, $responses);
+                $responseoptions[$key] = $response;
             }
         }
         if (isset($USER->modattempts[$this->lesson->id]) && !empty($attempt->useranswer)) {
@@ -115,9 +116,9 @@ class lesson_page_type_matching extends lesson_page {
                 $answer->answer = $properties->answer_editor[$i]['text'];
                 $answer->answerformat = $properties->answer_editor[$i]['format'];
             }
-            if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
-                $answer->response = $properties->response_editor[$i]['text'];
-                $answer->responseformat = $properties->response_editor[$i]['format'];
+            if (!empty($properties->response_editor[$i])) {
+                $answer->response = $properties->response_editor[$i];
+                $answer->responseformat = 0;
             }
 
             if (isset($properties->jumpto[$i])) {
@@ -171,27 +172,26 @@ class lesson_page_type_matching extends lesson_page {
         $wrong   = array_shift($answers);
 
         foreach ($answers as $key=>$answer) {
-            if ($answer->answer === '' or $answer->response === '') {
-                // incomplete option!
-                unset($answers[$key]);
+            if ($answer->answer !== '' or $answer->response !== '') {
+                $answers[$answer->id] = $answer;
             }
+            unset($answers[$key]);
         }
         // get he users exact responses for record keeping
         $hits = 0;
         $userresponse = array();
-        foreach ($response as $key => $value) {
-            foreach($answers as $answer) {
-                if ($value === $answer->response) {
-                    $userresponse[] = $answer->id;
-                }
-                if ((int)$answer->id === (int)$key) {
-                    $result->studentanswer .= '<br />'.format_text($answer->answer, $answer->answerformat, $formattextdefoptions).' = '.$value;
-                }
-                if ((int)$answer->id === (int)$key and $value === $answer->response) {
+        foreach ($response as $id => $value) {
+            $userresponse[] = $value;
+            // Make sure the user's answer is exist in question's answer
+            if (array_key_exists($id, $answers)) {
+                $answer = $answers[$id];
+                $result->studentanswer .= '<br />'.format_text($answer->answer, $answer->answerformat, $formattextdefoptions).' = '.$answers[$value]->response;
+                if ($id == $value) {
                     $hits++;
                 }
             }
         }
+
         $result->userresponse = implode(",", $userresponse);
 
         if ($hits == count($answers)) {
@@ -315,9 +315,9 @@ class lesson_page_type_matching extends lesson_page {
                 $this->answers[$i]->answer = $properties->answer_editor[$i]['text'];
                 $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format'];
             }
-            if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
-                $this->answers[$i]->response = $properties->response_editor[$i]['text'];
-                $this->answers[$i]->responseformat = $properties->response_editor[$i]['format'];
+            if (!empty($properties->response_editor[$i])) {
+                $this->answers[$i]->response = $properties->response_editor[$i];
+                $this->answers[$i]->responseformat = 0;
             }
 
             if (isset($properties->jumpto[$i])) {
@@ -473,12 +473,18 @@ class lesson_add_page_form_matching extends lesson_add_page_form_base {
         for ($i = 2; $i < $this->_customdata['lesson']->maxanswers+2; $i++) {
             $this->_form->addElement('header', 'matchingpair'.($i-1), get_string('matchingpair', 'lesson', $i-1));
             $this->add_answer($i, NULL, ($i < 4));
-            $this->add_response($i, get_string('matchesanswer','lesson'), ($i < 4));
+            $required = ($i < 4);
+            $label = get_string('matchesanswer','lesson');
+            $count = $i;
+            $this->_form->addElement('text', 'response_editor['.$count.']', $label, array('size'=>'50'));
+            $this->_form->setDefault('response_editor['.$count.']', '');
+            if ($required) {
+                $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
+            }
         }
     }
 }
 
-
 class lesson_display_answer_form_matching extends moodleform {
 
     public function definition() {
index b3d3480..5fc7a09 100644 (file)
@@ -18,7 +18,7 @@
  * mod_lti data generator
  *
  * @package    mod_lti
- * @category   phpunit
+ * @category   test
  * @copyright  Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com)
  * @author     Mark Nielsen
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Assignment module PHPUnit data generator class
+ * LTI module data generator class
  *
  * @package    mod_lti
- * @category   phpunit
+ * @category   test
  * @copyright  Copyright (c) 2012 Moodlerooms Inc. (http://www.moodlerooms.com)
  * @author     Mark Nielsen
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mod_lti_generator extends phpunit_module_generator {
+class mod_lti_generator extends testing_module_generator {
 
     /**
      * Create new lti module instance
@@ -98,4 +98,4 @@ class mod_lti_generator extends phpunit_module_generator {
         $id = lti_add_instance($record, null);
         return $this->post_add_instance($id, $record->coursemodule);
     }
-}
\ No newline at end of file
+}
index d2ae2fa..533c9ff 100644 (file)
@@ -18,7 +18,7 @@
  * mod_page data generator
  *
  * @package    mod_page
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -27,14 +27,14 @@ defined('MOODLE_INTERNAL') || die();
 
 
 /**
- * Page module PHPUnit data generator class
+ * Page module data generator class
  *
  * @package    mod_page
- * @category   phpunit
+ * @category   test
  * @copyright  2012 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mod_page_generator extends phpunit_module_generator {
+class mod_page_generator extends testing_module_generator {
 
     /**
      * Create new page module instance
index c8776de..175e5c6 100644 (file)
@@ -100,7 +100,8 @@ class mod_quiz_overdue_attempt_updater {
 
 
         // SQL to compute timeclose and timelimit for each attempt:
-        $quizausersql = quiz_get_attempt_usertime_sql();
+        $quizausersql = quiz_get_attempt_usertime_sql(
+                "iquiza.state IN ('inprogress', 'overdue') AND iquiza.timecheckstate <= :iprocessto");
 
         // This query should have all the quiz_attempts columns.
         return $DB->get_recordset_sql("
@@ -116,6 +117,6 @@ class mod_quiz_overdue_attempt_updater {
             AND quiza.timecheckstate <= :processto
        ORDER BY quiz.course, quiza.quiz",
 
-                array('processto' => $processto));
+                array('processto' => $processto, 'iprocessto' => $processto));
     }
 }
index 8bacae7..a366a84 100644 (file)
@@ -783,34 +783,48 @@ function quiz_update_open_attempts(array $conditions) {
     }
 
     $params = array();
-    $coursecond = '';
-    $usercond = '';
-    $quizcond = '';
-    $groupcond = '';
+    $wheres = array("quiza.state IN ('inprogress', 'overdue')");
+    $iwheres = array("iquiza.state IN ('inprogress', 'overdue')");
 
     if (isset($conditions['courseid'])) {
         list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'cid');
         $params = array_merge($params, $inparams);
-        $coursecond = "AND quiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)";
+        $wheres[] = "quiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)";
+        list ($incond, $inparams) = $DB->get_in_or_equal($conditions['courseid'], SQL_PARAMS_NAMED, 'icid');
+        $params = array_merge($params, $inparams);
+        $iwheres[] = "iquiza.quiz IN (SELECT q.id FROM {quiz} q WHERE q.course $incond)";
     }
+
     if (isset($conditions['userid'])) {
         list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'uid');
         $params = array_merge($params, $inparams);
-        $usercond = "AND quiza.userid $incond";
+        $wheres[] = "quiza.userid $incond";
+        list ($incond, $inparams) = $DB->get_in_or_equal($conditions['userid'], SQL_PARAMS_NAMED, 'iuid');
+        $params = array_merge($params, $inparams);
+        $iwheres[] = "iquiza.userid $incond";
     }
+
     if (isset($conditions['quizid'])) {
         list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'qid');
         $params = array_merge($params, $inparams);
-        $quizcond = "AND quiza.quiz $incond";
+        $wheres[] = "quiza.quiz $incond";
+        list ($incond, $inparams) = $DB->get_in_or_equal($conditions['quizid'], SQL_PARAMS_NAMED, 'iqid');
+        $params = array_merge($params, $inparams);
+        $iwheres[] = "iquiza.quiz $incond";
     }
+
     if (isset($conditions['groupid'])) {
         list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'gid');
         $params = array_merge($params, $inparams);
-        $groupcond = "AND quiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)";
+        $wheres[] = "quiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)";
+        list ($incond, $inparams) = $DB->get_in_or_equal($conditions['groupid'], SQL_PARAMS_NAMED, 'igid');
+        $params = array_merge($params, $inparams);
+        $iwheres[] = "iquiza.quiz IN (SELECT qo.quiz FROM {quiz_overrides} qo WHERE qo.groupid $incond)";
     }
 
     // SQL to compute timeclose and timelimit for each attempt:
-    $quizausersql = quiz_get_attempt_usertime_sql();
+    $quizausersql = quiz_get_attempt_usertime_sql(
+            implode("\n                AND ", $iwheres));
 
     // SQL to compute the new timecheckstate
     $timecheckstatesql = "
@@ -822,11 +836,7 @@ function quiz_update_open_attempts(array $conditions) {
           CASE WHEN quiza.state = 'overdue' THEN quiz.graceperiod ELSE 0 END";
 
     // SQL to select which attempts to process
-    $attemptselect = " quiza.state IN ('inprogress', 'overdue')
-                       $coursecond
-                       $usercond
-                       $quizcond
-                       $groupcond";
+    $attemptselect = implode("\n                         AND ", $wheres);
 
    /*
     * Each database handles updates with inner joins differently:
@@ -876,9 +886,14 @@ function quiz_update_open_attempts(array $conditions) {
 /**
  * Returns SQL to compute timeclose and timelimit for every attempt, taking into account user and group overrides.
  *
- * @return string         SQL select with columns attempt.id, usertimeclose, usertimelimit
+ * @param string $redundantwhereclauses extra where clauses to add to the subquery
+ *      for performance. These can use the table alias iquiza for the quiz attempts table.
+ * @return string SQL select with columns attempt.id, usertimeclose, usertimelimit.
  */
-function quiz_get_attempt_usertime_sql() {
+function quiz_get_attempt_usertime_sql($redundantwhereclauses = '') {
+    if ($redundantwhereclauses) {
+        $redundantwhereclauses = 'WHERE ' . $redundantwhereclauses;
+    }
     // The multiple qgo JOINS are necessary because we want timeclose/timelimit = 0 (unlimited) to supercede
     // any other group override
     $quizausersql = "
@@ -894,6 +909,7 @@ function quiz_get_attempt_usertime_sql() {
       LEFT JOIN {quiz_overrides} qgo2 ON qgo2.quiz = iquiza.quiz AND qgo2.groupid = gm.groupid AND qgo2.timeclose > 0
       LEFT JOIN {quiz_overrides} qgo3 ON qgo3.quiz = iquiza.quiz AND qgo3.groupid = gm.groupid AND qgo3.timelimit = 0
       LEFT JOIN {quiz_overrides} qgo4 ON qgo4.quiz = iquiza.quiz AND qgo4.groupid = gm.groupid AND qgo4.timelimit > 0
+          $redundantwhereclauses
        GROUP BY iquiza.id, iquiz.id, iquiz.timeclose, iquiz.timelimit";
     return $quizausersql;
 }
index e3153a1..244d82d 100644 (file)
@@ -318,20 +318,6 @@ class quiz_statistics_report extends quiz_default_report {
         echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
         echo html_writer::table($questionstatstable);
     }
-    public function format_text($text, $format, $qa, $component, $filearea, $itemid,
-            $clean = false) {
-        $formatoptions = new stdClass();
-        $formatoptions->noclean = !$clean;
-        $formatoptions->para = false;
-        $text = $qa->rewrite_pluginfile_urls($text, $component, $filearea, $itemid);
-        return format_text($text, $format, $formatoptions);
-    }
-
-    /** @return the result of applying {@link format_text()} to the question text. */
-    public function format_questiontext($qa) {
-        return $this->format_text($this->questiontext, $this->questiontextformat,
-        $qa, 'question', 'questiontext', $this->id);
-    }
 
     /**
      * @param object $question question data.
index b889d3d..fa2e1db 100644 (file)
@@ -23,7 +23,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright 2012 The Open University
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class mod_quiz_generator extends phpunit_module_generator {
+class mod_quiz_generator extends testing_module_generator {
 
     /**
      * Create new quiz module instance.
index e5e4fbc..7c0a814 100644 (file)
@@ -834,7 +834,7 @@ function scorm_course_format_display($user, $course) {
 }
 
 function scorm_view_display ($user, $scorm, $action, $cm) {
-    global $CFG, $DB, $PAGE, $OUTPUT;
+    global $CFG, $DB, $PAGE, $OUTPUT, $COURSE;
 
     if ($scorm->scormtype != SCORM_TYPE_LOCAL && $scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
         scorm_parse($scorm, false);
@@ -913,7 +913,7 @@ function scorm_view_display ($user, $scorm, $action, $cm) {
                       <label for="a"><?php print_string('newattempt', 'scorm') ?></label>
             <?php
         }
-        if (!empty($scorm->popup)) {
+        if ($COURSE->format != 'scorm' && !empty($scorm->popup)) {
             echo '<input type="hidden" name="display" value="popup" />'."\n";
         }
         ?>
index 5325606..9b23f51 100644 (file)
@@ -27,6 +27,9 @@
         <testsuite name="core_phpunit">
             <directory suffix="_test.php">lib/phpunit/tests</directory>
         </testsuite>
+        <testsuite name="core_test">
+            <directory suffix="_test.php">lib/testing/tests</directory>
+        </testsuite>
         <testsuite name="core_db">
             <directory suffix="_test.php">lib/ddl/tests</directory>
             <directory suffix="_test.php">lib/dml/tests</directory>
index 7abdbcb..c4ff0de 100644 (file)
@@ -325,18 +325,19 @@ class qformat_default {
         foreach ($questions as $question) {
             if (!empty($question->fraction) and (is_array($question->fraction))) {
                 $fractions = $question->fraction;
-                $answersvalid = true; // in case they are!
+                $invalidfractions = array();
                 foreach ($fractions as $key => $fraction) {
                     $newfraction = match_grade_options($gradeoptionsfull, $fraction,
                             $this->matchgrades);
                     if ($newfraction === false) {
-                        $answersvalid = false;
+                        $invalidfractions[] = $fraction;
                     } else {
                         $fractions[$key] = $newfraction;
                     }
                 }
-                if (!$answersvalid) {
-                    echo $OUTPUT->notification(get_string('invalidgrade', 'question'));
+                if ($invalidfractions) {
+                    echo $OUTPUT->notification(get_string('invalidgrade', 'question',
+                            implode(', ', $invalidfractions)));
                     ++$gradeerrors;
                     continue;
                 } else {
index 54fcdb3..514b177 100644 (file)
@@ -743,7 +743,7 @@ class qformat_gift_test extends question_testcase {
             'qtype' => 'shortanswer',
             'options' => (object) array(
                 'id' => 123,
-                'question' => 666,
+                'questionid' => 666,
                 'usecase' => 1,
                 'answers' => array(
                     1 => (object) array(
@@ -803,7 +803,7 @@ class qformat_gift_test extends question_testcase {
             'qtype' => 'shortanswer',
             'options' => (object) array(
                 'id' => 123,
-                'question' => 666,
+                'questionid' => 666,
                 'usecase' => 1,
                 'answers' => array(
                     1 => (object) array(
index 69ecb56..6f3a66f 100644 (file)
@@ -107,6 +107,15 @@ function xmldb_qtype_essay_upgrade($oldversion) {
     // Moodle v2.4.0 release upgrade line
     // Put any upgrade step following this
 
+    if ($oldversion < 2013011800) {
+        // Then we delete the old question_answers rows for essay questions.
+        $DB->delete_records_select('qtype_essay_options', "NOT EXISTS (
+                SELECT 1 FROM {question} WHERE qtype = 'essay' AND
+                    {question}.id = {qtype_essay_options}.questionid)");
+
+        // Essay savepoint reached.
+        upgrade_plugin_savepoint(true, 2013011800, 'qtype', 'essay');
+    }
 
     return true;
 }
index dac965e..10a1346 100644 (file)
@@ -80,6 +80,13 @@ class qtype_essay extends question_type {
         $question->graderinfoformat = $questiondata->options->graderinfoformat;
     }
 
+    public function delete_question($questionid, $contextid) {
+        global $DB;
+
+        $DB->delete_records('qtype_essay_options', array('questionid' => $questionid));
+        parent::delete_question($questionid, $contextid);
+    }
+
     /**
      * @return array the different response formats that the question type supports.
      * internal name => human-readable name.
index 997941c..67a0d8e 100644 (file)
@@ -26,7 +26,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'qtype_essay';
-$plugin->version   = 2012112900;
+$plugin->version   = 2013011800;
 
 $plugin->requires  = 2012112900;
 
index 032364e..7df126e 100644 (file)
@@ -113,8 +113,8 @@ class qtype_multianswer extends question_type {
                                         array('question' => $oldwrappedquestion->id));
                                 break;
                             case 'shortanswer':
-                                $DB->delete_records('question_shortanswer',
-                                        array('question' => $oldwrappedquestion->id));
+                                $DB->delete_records('qtype_shortanswer_options',
+                                        array('questionid' => $oldwrappedquestion->id));
                                 break;
                             case 'numerical':
                                 $DB->delete_records('question_numerical',
index 99c094a..39570e2 100644 (file)
@@ -52,15 +52,14 @@ class backup_qtype_shortanswer_plugin extends backup_qtype_plugin {
         $this->add_question_question_answers($pluginwrapper);
 
         // Now create the qtype own structures
-        $shortanswer = new backup_nested_element('shortanswer', array('id'), array(
-            'answers', 'usecase'));
+        $shortanswer = new backup_nested_element('shortanswer', array('id'), array('usecase'));
 
         // Now the own qtype tree
         $pluginwrapper->add_child($shortanswer);
 
         // set source to populate the data
-        $shortanswer->set_source_table('question_shortanswer',
-                array('question' => backup::VAR_PARENTID));
+        $shortanswer->set_source_table('qtype_shortanswer_options',
+                array('questionid' => backup::VAR_PARENTID));
 
         // don't need to annotate ids nor files
 
index 9f1b67b..665a5cd 100644 (file)
@@ -68,20 +68,11 @@ class restore_qtype_shortanswer_plugin extends restore_qtype_plugin {
         $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
 
         // If the question has been created by restore, we need to create its
-        // question_shortanswer too, if they are defined (the gui should ensure this).
-        if ($questioncreated && !empty($data->answers)) {
-            // Adjust some columns
-            $data->question = $newquestionid;
-            // Map sequence of question_answer ids
-            $answersarr = explode(',', $data->answers);
-            foreach ($answersarr as $key => $answer) {
-                $answersarr[$key] = $this->get_mappingid('question_answer', $answer);
-            }
-            $data->answers = implode(',', $answersarr);
-            // Insert record
-            $newitemid = $DB->insert_record('question_shortanswer', $data);
-            // Create mapping
-            $this->set_mapping('question_shortanswer', $oldid, $newitemid);
+        // qtype_shortanswer_options too, if they are defined (the gui should ensure this).
+        if ($questioncreated) {
+            $data->questionid = $newquestionid;
+            $newitemid = $DB->insert_record('qtype_shortanswer_options', $data);
+            $this->set_mapping('qtype_shortanswer_options', $oldid, $newitemid);
         }
     }
 }
index eb410f6..97df0fd 100644 (file)
@@ -1,19 +1,18 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="question/type/shortanswer/db" VERSION="20120122" COMMENT="XMLDB file for Moodle question/type/shortanswer"
+<XMLDB PATH="question/type/shortanswer/db" VERSION="20130118" COMMENT="XMLDB file for Moodle question/type/shortanswer"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
 >
   <TABLES>
-    <TABLE NAME="question_shortanswer" COMMENT="Options for short answer questions">
+    <TABLE NAME="qtype_shortanswer_options" COMMENT="Options for short answer questions">
       <FIELDS>
-        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="question"/>
-        <FIELD NAME="question" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id." PREVIOUS="id" NEXT="answers"/>
-        <FIELD NAME="answers" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="Redundant. Comma-separated list of question_answer ids. SELECT id FROM question_answers WHERE question = ? ORDER BY id." PREVIOUS="question" NEXT="usecase"/>
-        <FIELD NAME="usecase" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether answers are matched case-sensitively." PREVIOUS="answers"/>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="questionid"/>
+        <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id." PREVIOUS="id" NEXT="usecase"/>
+        <FIELD NAME="usecase" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether answers are matched case-sensitively." PREVIOUS="questionid"/>
       </FIELDS>
       <KEYS>
-        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="question"/>
-        <KEY NAME="question" TYPE="foreign" FIELDS="question" REFTABLE="question" REFFIELDS="id" PREVIOUS="primary"/>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="questionid"/>
+        <KEY NAME="questionid" TYPE="foreign-unique" FIELDS="questionid" REFTABLE="question" REFFIELDS="id" PREVIOUS="primary"/>
       </KEYS>
     </TABLE>
   </TABLES>
diff --git a/question/type/shortanswer/db/upgrade.php b/question/type/shortanswer/db/upgrade.php
new file mode 100755 (executable)
index 0000000..cadf88a
--- /dev/null
@@ -0,0 +1,109 @@
+<?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/>.
+
+/**
+ * Short-answer question type upgrade code.
+ *
+ * @package    qtype
+ * @subpackage shortanswer
+ * @copyright  2011 The Open University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Upgrade code for the essay question type.
+ * @param int $oldversion the version we are upgrading from.
+ */
+function xmldb_qtype_shortanswer_upgrade($oldversion) {
+    global $CFG, $DB;
+
+    $dbman = $DB->get_manager();
+
+    // Moodle v2.4.0 release upgrade line
+    // Put any upgrade step following this
+
+    if ($oldversion < 2013011800) {
+
+        // Define field answers to be dropped from question_shortanswer.
+        $table = new xmldb_table('question_shortanswer');
+        $field = new xmldb_field('answers');
+
+        // Conditionally launch drop field answers.
+        if ($dbman->field_exists($table, $field)) {
+            $dbman->drop_field($table, $field);
+        }
+
+        // Shortanswer savepoint reached.
+        upgrade_plugin_savepoint(true, 2013011800, 'qtype', 'shortanswer');
+    }
+
+    if ($oldversion < 2013011801) {
+
+        // Define key question (foreign) to be dropped form question_shortanswer
+        $table = new xmldb_table('question_shortanswer');
+        $key = new xmldb_key('question', XMLDB_KEY_FOREIGN, array('question'), 'question', array('id'));
+
+        // Launch drop key question
+        $dbman->drop_key($table, $key);
+
+        // shortanswer savepoint reached
+        upgrade_plugin_savepoint(true, 2013011801, 'qtype', 'shortanswer');
+    }
+
+    if ($oldversion < 2013011802) {
+
+        // Rename field question on table question_shortanswer to questionid.
+        $table = new xmldb_table('question_shortanswer');
+        $field = new xmldb_field('question', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'id');
+
+        // Launch rename field question.
+        $dbman->rename_field($table, $field, 'questionid');
+
+        // Shortanswer savepoint reached.
+        upgrade_plugin_savepoint(true, 2013011802, 'qtype', 'shortanswer');
+    }
+
+    if ($oldversion < 2013011803) {
+
+        // Define key questionid (foreign-unique) to be added to question_shortanswer
+        $table = new xmldb_table('question_shortanswer');
+        $key = new xmldb_key('questionid', XMLDB_KEY_FOREIGN_UNIQUE, array('questionid'), 'question', array('id'));
+
+        // Launch add key questionid
+        $dbman->add_key($table, $key);
+
+        // shortanswer savepoint reached
+        upgrade_plugin_savepoint(true, 2013011803, 'qtype', 'shortanswer');
+    }
+
+    if ($oldversion < 2013011804) {
+
+        // Define table qtype_shortanswer_options to be renamed to qtype_shortanswer_options
+        $table = new xmldb_table('question_shortanswer');
+
+        // Launch rename table for qtype_shortanswer_options
+        $dbman->rename_table($table, 'qtype_shortanswer_options');
+
+        // shortanswer savepoint reached
+        upgrade_plugin_savepoint(true, 2013011804, 'qtype', 'shortanswer');
+    }
+
+    return true;
+}
index 2cf77d7..5aa5d3d 100644 (file)
@@ -39,11 +39,7 @@ require_once($CFG->dirroot . '/question/type/shortanswer/question.php');
  */
 class qtype_shortanswer extends question_type {
     public function extra_question_fields() {
-        return array('question_shortanswer', 'answers', 'usecase');
-    }
-
-    public function questionid_column_name() {
-        return 'question';
+        return array('qtype_shortanswer_options', 'usecase');
     }
 
     public function move_files($questionid, $oldcontextid, $newcontextid) {
@@ -67,7 +63,6 @@ class qtype_shortanswer extends question_type {
         $oldanswers = $DB->get_records('question_answers',
                 array('question' => $question->id), 'id ASC');
 
-        $answers = array();
         $maxfraction = -1;
 
         // Insert all the new answers
@@ -95,13 +90,11 @@ class qtype_shortanswer extends question_type {
             $answer->feedbackformat = $question->feedback[$key]['format'];
             $DB->update_record('question_answers', $answer);
 
-            $answers[] = $answer->id;
             if ($question->fraction[$key] > $maxfraction) {
                 $maxfraction = $question->fraction[$key];
             }
         }
 
-        $question->answers = implode(',', $answers);
         $parentresult = parent::save_question_options($question);
         if ($parentresult !== null) {
             // Parent function returns null if all is OK
index a7ef9c2..196e9be 100644 (file)
@@ -26,7 +26,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'qtype_shortanswer';
-$plugin->version   = 2012112900;
+$plugin->version   = 2013011804;
 
 $plugin->requires  = 2012112900;
 
index d0c152f..3114683 100644 (file)
@@ -23,7 +23,9 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['keyword'] = 'Full text';
+$string['keyword'] = 'Search for';
 $string['pluginname'] = 'Wikimedia';
 $string['wikimedia:view'] = 'View wikimedia repository';
 $string['configplugin'] = 'Wikimedia repository type configuration';
+$string['maxwidth'] = 'Max image width (px)';
+$string['maxheight'] = 'Max image height (px)';
index cc4bf2b..09a00fb 100644 (file)
@@ -55,6 +55,43 @@ class repository_wikimedia extends repository {
             $SESSION->{$sess_keyword} = $this->keyword;
         }
     }
+
+    /**
+     * Returns maximum width for images
+     *
+     * Takes the maximum width for images eithre from search form or from
+     * user preferences, updates user preferences if needed
+     *
+     * @return int
+     */
+    public function get_maxwidth() {
+        $param = optional_param('wikimedia_maxwidth', 0, PARAM_INT);
+        $pref = get_user_preferences('repository_wikimedia_maxwidth', WIKIMEDIA_IMAGE_SIDE_LENGTH);
+        if ($param > 0 && $param != $pref) {
+            $pref = $param;
+            set_user_preference('repository_wikimedia_maxwidth', $pref);
+        }
+        return $pref;
+    }
+
+    /**
+     * Returns maximum height for images
+     *
+     * Takes the maximum height for images eithre from search form or from
+     * user preferences, updates user preferences if needed
+     *
+     * @return int
+     */
+    public function get_maxheight() {
+        $param = optional_param('wikimedia_maxheight', 0, PARAM_INT);
+        $pref = get_user_preferences('repository_wikimedia_maxheight', WIKIMEDIA_IMAGE_SIDE_LENGTH);
+        if ($param > 0 && $param != $pref) {
+            $pref = $param;
+            set_user_preference('repository_wikimedia_maxheight', $pref);
+        }
+        return $pref;
+    }
+
     public function get_listing($path = '', $page = '') {
         $client = new wikimedia;
         $list = array();
@@ -62,7 +99,9 @@ class repository_wikimedia extends repository {
         if ($list['page'] < 1) {
             $list['page'] = 1;
         }
-        $list['list'] = $client->search_images($this->keyword, $list['page'] - 1);
+        $list['list'] = $client->search_images($this->keyword, $list['page'] - 1,
+                array('iiurlwidth' => $this->get_maxwidth(),
+                    'iiurlheight' => $this->get_maxheight()));
         $list['nologin'] = true;
         $list['norefresh'] = true;
         $list['nosearch'] = true;
@@ -88,13 +127,26 @@ class repository_wikimedia extends repository {
         $keyword->type  = 'text';
         $keyword->name  = 'wikimedia_keyword';
         $keyword->value = '';
+        $maxwidth = array(
+            'label' => get_string('maxwidth', 'repository_wikimedia').': ',
+            'type' => 'text',
+            'name' => 'wikimedia_maxwidth',
+            'value' => get_user_preferences('repository_wikimedia_maxwidth', WIKIMEDIA_IMAGE_SIDE_LENGTH),
+        );
+        $maxheight = array(
+            'label' => get_string('maxheight', 'repository_wikimedia').': ',
+            'type' => 'text',
+            'name' => 'wikimedia_maxheight',
+            'value' => get_user_preferences('repository_wikimedia_maxheight', WIKIMEDIA_IMAGE_SIDE_LENGTH),
+        );
         if ($this->options['ajax']) {
             $form = array();
-            $form['login'] = array($keyword);
+            $form['login'] = array($keyword, (object)$maxwidth, (object)$maxheight);
             $form['nologin'] = true;
             $form['norefresh'] = true;
             $form['nosearch'] = true;
-            $form['allowcaching'] = true; // indicates that login form can be cached in filepicker.js
+            $form['allowcaching'] = false; // indicates that login form can NOT
+            // be cached in filepicker.js (maxwidth and maxheight are dynamic)
             return $form;
         } else {
             echo <<<EOD
index d02b181..36950ca 100644 (file)
@@ -26,6 +26,7 @@
 define('WIKIMEDIA_THUMBS_PER_PAGE', 24);
 define('WIKIMEDIA_FILE_NS', 6);
 define('WIKIMEDIA_IMAGE_SIDE_LENGTH', 1024);
+define('WIKIMEDIA_THUMB_SIZE', 120);
 
 class wikimedia {
     private $_conn  = null;
@@ -135,13 +136,17 @@ class wikimedia {
             return $thumb_url;
         }
     }
+
     /**
      * Search for images and return photos array.
      *
      * @param string $keyword
+     * @param int $page
+     * @param array $params additional query params
      * @return array
      */
-    public function search_images($keyword, $page = 0) {
+    public function search_images($keyword, $page = 0, $params = array()) {
+        global $OUTPUT;
         $files_array = array();
         $this->_param['action'] = 'query';
         $this->_param['generator'] = 'search';
@@ -150,9 +155,10 @@ class wikimedia {
         $this->_param['gsrlimit'] = WIKIMEDIA_THUMBS_PER_PAGE;
         $this->_param['gsroffset'] = $page * WIKIMEDIA_THUMBS_PER_PAGE;
         $this->_param['prop']   = 'imageinfo';
-        $this->_param['iiprop'] = 'url|dimensions|mime';
-        $this->_param['iiurlwidth'] = WIKIMEDIA_IMAGE_SIDE_LENGTH;
-        $this->_param['iiurlheight'] = WIKIMEDIA_IMAGE_SIDE_LENGTH;
+        $this->_param['iiprop'] = 'url|dimensions|mime|timestamp|size|user';
+        $this->_param += $params;
+        $this->_param += array('iiurlwidth' => WIKIMEDIA_IMAGE_SIDE_LENGTH,
+            'iiurlheight' => WIKIMEDIA_IMAGE_SIDE_LENGTH);
         //didn't work with POST
         $content = $this->_conn->get($this->api, $this->_param);
         $result = unserialize($content);
@@ -162,26 +168,50 @@ class wikimedia {
                 $file_type = $page['imageinfo'][0]['mime'];
                 $image_types = array('image/jpeg', 'image/png', 'image/gif', 'image/svg+xml');
                 if (in_array($file_type, $image_types)) {  //is image
-                    $thumbnail = $this->get_thumb_url($page['imageinfo'][0]['url'], $page['imageinfo'][0]['width'], $page['imageinfo'][0]['height']);
-                    $source = $page['imageinfo'][0]['thumburl'];        //upload scaled down image
                     $extension = pathinfo($title, PATHINFO_EXTENSION);
                     if (strcmp($extension, 'svg') == 0) {               //upload png version of svg-s
                         $title .= '.png';
                     }
-                } else {                                   //other file types
-                    $thumbnail = '';
-                    $source = $page['imageinfo'][0]['url'];
+                    if ($page['imageinfo'][0]['thumbwidth'] < $page['imageinfo'][0]['width']) {
+                        $attrs = array(
+                            //upload scaled down image
+                            'source' => $page['imageinfo'][0]['thumburl'],
+                            'image_width' => $page['imageinfo'][0]['thumbwidth'],
+                            'image_height' => $page['imageinfo'][0]['thumbheight']
+                        );
+                        if ($attrs['image_width'] <= WIKIMEDIA_THUMB_SIZE && $attrs['image_height'] <= WIKIMEDIA_THUMB_SIZE) {
+                            $attrs['realthumbnail'] = $attrs['source'];
+                        }
+                        if ($attrs['image_width'] <= 24 && $attrs['image_height'] <= 24) {
+                            $attrs['realicon'] = $attrs['source'];
+                        }
+                    } else {
+                        $attrs = array(
+                            //upload full size image
+                            'source' => $page['imageinfo'][0]['url'],
+                            'image_width' => $page['imageinfo'][0]['width'],
+                            'image_height' => $page['imageinfo'][0]['height'],
+                            'size' => $page['imageinfo'][0]['size']
+                        );
+                    }
+                    $attrs += array(
+                        'realthumbnail' => $this->get_thumb_url($page['imageinfo'][0]['url'], $page['imageinfo'][0]['width'], $page['imageinfo'][0]['height'], WIKIMEDIA_THUMB_SIZE),
+                        'realicon' => $this->get_thumb_url($page['imageinfo'][0]['url'], $page['imageinfo'][0]['width'], $page['imageinfo'][0]['height'], 24),
+                        'author' => $page['imageinfo'][0]['user'],
+                        'datemodified' => strtotime($page['imageinfo'][0]['timestamp']),
+                        );
+                } else {  // other file types
+                    $attrs = array('source' => $page['imageinfo'][0]['url']);
                 }
                 $files_array[] = array(
                     'title'=>substr($title, 5),         //chop off 'File:'
-                    'thumbnail'=>$thumbnail,
-                    'thumbnail_width'=>120,
-                    'thumbnail_height'=>120,
-                    // plugin-dependent unique path to the file (id, url, path, etc.)
-                    'source'=>$source,
+                    'thumbnail' => $OUTPUT->pix_url(file_extension_icon(substr($title, 5), WIKIMEDIA_THUMB_SIZE))->out(false),
+                    'thumbnail_width' => WIKIMEDIA_THUMB_SIZE,
+                    'thumbnail_height' => WIKIMEDIA_THUMB_SIZE,
+                    'license' => 'cc-sa',
                     // the accessible url of the file
                     'url'=>$pa