Merge branch 'wip-mdl-34606-m23' of git://github.com/rajeshtaneja/moodle into MOODLE_...
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 4 Sep 2012 00:09:25 +0000 (02:09 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 4 Sep 2012 00:09:25 +0000 (02:09 +0200)
126 files changed:
admin/environment.xml
admin/tool/assignmentupgrade/module.js
backup/moodle2/backup_custom_fields.php
backup/moodle2/restore_stepslib.php
backup/upgrade.txt [new file with mode: 0644]
backup/util/dbops/backup_plan_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/helper/backup_cron_helper.class.php
backup/util/plan/backup_structure_step.class.php
backup/util/plan/restore_structure_step.class.php
backup/util/structure/backup_nested_element.class.php
backup/util/ui/backup_ui_stage.class.php
backup/util/ui/restore_ui_stage.class.php
blocks/completionstatus/block_completionstatus.php
blocks/completionstatus/details.php
blocks/completionstatus/lang/en/block_completionstatus.php
blog/locallib.php
course/editsection_form.php
course/reset_form.php
filter/algebra/filter.php
filter/algebra/tests/filter_test.php [new file with mode: 0644]
grade/lib.php
install/lang/lt/admin.php
lang/en/admin.php
lang/en/backup.php
lang/en/moodle.php
lang/en/repository.php
lib/accesslib.php
lib/boxlib.php
lib/completionlib.php
lib/cronlib.php
lib/db/upgradelib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/googleapi.php
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/oauthlib.php
lib/pluginlib.php
lib/questionlib.php
lib/tests/completionlib_test.php
lib/tests/moodlelib_test.php
lib/tests/pluginlib_test.php
lib/tests/questionlib_test.php
login/index.php
message/edit.php
mod/assign/gradingtable.php
mod/assignment/type/upload/assignment.class.php
mod/choice/backup/moodle2/restore_choice_stepslib.php
mod/data/field/checkbox/mod.html
mod/data/field/latlong/field.class.php
mod/data/field/menu/mod.html
mod/data/field/multimenu/mod.html
mod/data/field/picture/field.class.php
mod/data/field/picture/mod.html
mod/data/field/radiobutton/mod.html
mod/data/field/textarea/mod.html
mod/data/lib.php
mod/data/styles.css
mod/data/templates.php
mod/feedback/item/multichoice/lib.php
mod/feedback/item/multichoicerated/lib.php
mod/feedback/lang/en/feedback.php
mod/lesson/format.php
mod/quiz/lib.php
mod/quiz/report/upgrade.txt
mod/quiz/styles.css
mod/workshop/form/comments/backup/moodle1/lib.php
mod/workshop/form/numerrors/backup/moodle1/lib.php
mod/workshop/form/rubric/backup/moodle1/lib.php
question/category_class.php
question/format.php
question/format/blackboard/format.php
question/format/blackboard_six/format.php
question/format/blackboard_six/formatbase.php [new file with mode: 0644]
question/format/blackboard_six/formatpool.php [new file with mode: 0644]
question/format/blackboard_six/formatqti.php [new file with mode: 0644]
question/format/blackboard_six/lang/en/qformat_blackboard_six.php
question/format/blackboard_six/tests/blackboardformatpool_test.php [new file with mode: 0644]
question/format/blackboard_six/tests/blackboardsixformatqti_test.php [new file with mode: 0644]
question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat [new file with mode: 0644]
question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat [new file with mode: 0644]
question/format/blackboard_six/version.php
question/format/examview/format.php
question/format/gift/format.php
question/format/multianswer/format.php
question/format/multianswer/tests/fixtures/questions.multianswer.txt [new file with mode: 0644]
question/format/multianswer/tests/multianswerformat_test.php [new file with mode: 0644]
question/type/calculated/questiontype.php
question/type/edit_question_form.php
question/type/essay/db/upgrade.php
question/type/match/lang/en/qtype_match.php
question/type/multianswer/lang/en/qtype_multianswer.php
question/type/multianswer/questiontype.php
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/styles.css
question/type/numerical/question.php
question/type/numerical/tests/answer_test.php
question/type/shortanswer/lang/en/qtype_shortanswer.php
report/backups/index.php
report/stats/lib.php
repository/alfresco/lib.php
repository/boxnet/lib.php
repository/dropbox/db/upgrade.php [new file with mode: 0644]
repository/dropbox/lang/en/repository_dropbox.php
repository/dropbox/lib.php
repository/dropbox/locallib.php
repository/dropbox/thumbnail.php [new file with mode: 0644]
repository/dropbox/version.php
repository/equella/lib.php
repository/filepicker.php
repository/filesystem/lib.php
repository/flickr/lib.php
repository/flickr_public/lib.php
repository/googledocs/lib.php
repository/lib.php
repository/repository_ajax.php
theme/afterburner/style/afterburner_styles.css
theme/anomaly/style/general.css
theme/base/style/admin.css
theme/formal_white/style/formal_white.css
theme/yui_combo.php
user/profile.php
version.php

index 004cab0..584a9b0 100644 (file)
         </FEEDBACK>
       </PHP_SETTING>
     </PHP_SETTINGS>
-</MOODLE>
+  </MOODLE>
+  <MOODLE version="2.4" requires="2.2">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mysql" version="5.1.33" />
+      <VENDOR name="postgres" version="8.3" />
+      <VENDOR name="mssql" version="9.0" />
+      <VENDOR name="odbc_mssql" version="9.0" />
+      <VENDOR name="mssql_n" version="9.0" />
+      <VENDOR name="oracle" version="10.2" />
+      <VENDOR name="sqlite" version="2.0" />
+    </DATABASE>
+    <PHP version="5.3.2" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_CHECK message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_CHECK message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opensslrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="gdrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_CHECK message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_CHECK message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="intlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="40M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="safe_mode" value="0" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingsafemode" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index edee839..59df8c5 100644 (file)
@@ -42,8 +42,8 @@ M.tool_assignmentupgrade = {
             }
         });
 
-        var batchform = Y.one('.tool_assignmentupgrade_batchform form');
-        batchform.on('submit', function(e) {
+        var upgradeselectedbutton = Y.one('#id_upgradeselected');
+        upgradeselectedbutton.on('click', function(e) {
             checkboxes = Y.all('td.c0 input');
             var selectedassignments = [];
             checkboxes.each(function(node) {
index e0ceb33..12929de 100644 (file)
@@ -96,14 +96,19 @@ class file_nested_element extends backup_nested_element {
         if (is_null($this->backupid)) {
             $this->backupid = $processor->get_var(backup::VAR_BACKUPID);
         }
-        parent::process($processor);
+        return parent::process($processor);
     }
 
     public function fill_values($values) {
         // Fill values
         parent::fill_values($values);
         // Do our own tasks (copy file from moodle to backup)
-        backup_file_manager::copy_file_moodle2backup($this->backupid, $values);
+        try {
+            backup_file_manager::copy_file_moodle2backup($this->backupid, $values);
+        } catch (file_exception $e) {
+            $this->add_result(array('missing_files_in_pool' => true));
+            $this->add_log('missing file in pool: ' . $e->debuginfo, backup::LOG_WARNING);
+        }
     }
 }
 
index 19d183a..dbd2c14 100644 (file)
@@ -270,7 +270,14 @@ class restore_gradebook_structure_step extends restore_structure_step {
 
         $data->contextid = get_context_instance(CONTEXT_COURSE, $this->get_courseid())->id;
 
-        $newitemid = $DB->insert_record('grade_letters', $data);
+        $gradeletter = (array)$data;
+        unset($gradeletter['id']);
+        if (!$DB->record_exists('grade_letters', $gradeletter)) {
+            $newitemid = $DB->insert_record('grade_letters', $data);
+        } else {
+            $newitemid = $data->id;
+        }
+
         $this->set_mapping('grade_letter', $oldid, $newitemid);
     }
     protected function process_grade_setting($data) {
@@ -2401,17 +2408,21 @@ class restore_activity_grades_structure_step extends restore_structure_step {
 
     /**
      * process activity grade_letters. Note that, while these are possible,
-     * because grade_letters are contextid based, in proctice, only course
+     * because grade_letters are contextid based, in practice, only course
      * context letters can be defined. So we keep here this method knowing
      * it won't be executed ever. gradebook restore will restore course letters.
      */
     protected function process_grade_letter($data) {
         global $DB;
 
-        $data = (object)$data;
+        $data['contextid'] = $this->task->get_contextid();
+        $gradeletter = (object)$data;
 
-        $data->contextid = $this->task->get_contextid();
-        $newitemid = $DB->insert_record('grade_letters', $data);
+        // Check if it exists before adding it
+        unset($data['id']);
+        if (!$DB->record_exists('grade_letters', $data)) {
+            $newitemid = $DB->insert_record('grade_letters', $gradeletter);
+        }
         // no need to save any grade_letter mapping
     }
 }
diff --git a/backup/upgrade.txt b/backup/upgrade.txt
new file mode 100644 (file)
index 0000000..cb02095
--- /dev/null
@@ -0,0 +1,17 @@
+This files describes API changes in /backup/*,
+information provided here is intended especially for developers.
+
+=== 2.4 ===
+
+* Since 2.3.1+ the backup file name schema has changed. The ID of the course will always be part of
+    the filename regardless of the setting 'backup_shortname'. See MDL-33812.
+
+=== 2.3 ===
+
+* Since 2.3.1+ the backup file name schema has changed. The ID of the course will always be part of
+    the filename regardless of the setting 'backup_shortname'. See MDL-33812.
+
+=== 2.2 ===
+
+* Since 2.2.4+ the backup file name schema has changed. The ID of the course will always be part of
+    the filename regardless of the setting 'backup_shortname'. See MDL-33812.
\ No newline at end of file
index 30714ed..0752f0d 100644 (file)
@@ -197,19 +197,19 @@ abstract class backup_plan_dbops extends backup_dbops {
     * @param int $courseid/$sectionid/$cmid
     * @param bool $users Should be true is users were included in the backup
     * @param bool $anonymised Should be true is user information was anonymized.
-    * @param bool $useidasname true to use id, false to use strings (default)
+    * @param bool $useidonly only use the ID in the file name
     * @return string The filename to use
     */
-    public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $useidasname = false) {
+    public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $useidonly = false) {
         global $DB;
 
         // Calculate backup word
         $backupword = str_replace(' ', '_', textlib::strtolower(get_string('backupfilename')));
         $backupword = trim(clean_filename($backupword), '_');
 
+        // Not $useidonly, lets fetch the name
         $shortname = '';
-        // Not $useidasname, lets calculate it, else $id will be used
-        if (!$useidasname) {
+        if (!$useidonly) {
             // Calculate proper name element (based on type)
             switch ($type) {
                 case backup::TYPE_1COURSE:
@@ -231,7 +231,11 @@ abstract class backup_plan_dbops extends backup_dbops {
             $shortname = textlib::strtolower(trim(clean_filename($shortname), '_'));
         }
 
-        $name = empty($shortname) ? $id : $shortname;
+        // The name will always contain the ID, but we append the course short name if requested.
+        $name = $id;
+        if (!$useidonly && $shortname != '') {
+            $name .= '-' . $shortname;
+        }
 
         // Calculate date
         $backupdateformat = str_replace(' ', '_', get_string('backupnameformat', 'langconfig'));
index 2e73e8f..fb7beb3 100644 (file)
@@ -819,10 +819,13 @@ abstract class restore_dbops {
      * @param int|null $olditemid
      * @param int|null $forcenewcontextid explicit value for the new contextid (skip mapping)
      * @param bool $skipparentitemidctxmatch
+     * @return array of result object
      */
     public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) {
         global $DB;
 
+        $results = array();
+
         if ($forcenewcontextid) {
             // Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings,
             // with questions originally at system/coursecat context in source being restored to course context in target). So we need
@@ -902,8 +905,14 @@ abstract class restore_dbops {
                 // this is a regular file, it must be present in the backup pool
                 $backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash);
 
+                // The file is not found in the backup.
                 if (!file_exists($backuppath)) {
-                    throw new restore_dbops_exception('file_not_found_in_pool', $file);
+                    $result = new stdClass();
+                    $result->code = 'file_missing_in_backup';
+                    $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
+                    $result->level = backup::LOG_WARNING;
+                    $results[] = $result;
+                    continue;
                 }
 
                 // create the file in the filepool if it does not exist yet
@@ -960,6 +969,7 @@ abstract class restore_dbops {
             }
         }
         $rs->close();
+        return $results;
     }
 
     /**
index b6d81ab..f7c6bc1 100644 (file)
@@ -46,6 +46,8 @@ abstract class backup_cron_automated_helper {
     const BACKUP_STATUS_UNFINISHED = 2;
     /** Course automated backup was skipped */
     const BACKUP_STATUS_SKIPPED = 3;
+    /** Course automated backup had warnings */
+    const BACKUP_STATUS_WARNING = 4;
 
     /** Run if required by the schedule set in config. Default. **/
     const RUN_ON_SCHEDULE = 0;
@@ -139,7 +141,7 @@ abstract class backup_cron_automated_helper {
                     $params = array('courseid' => $course->id, 'time' => $now-31*24*60*60, 'action' => '%view%');
                     $logexists = $DB->record_exists_select('log', $sqlwhere, $params);
                     if (!$logexists) {
-                        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED;
+                        $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED;
                         $backupcourse->nextstarttime = $nextstarttime;
                         $DB->update_record('backup_courses', $backupcourse);
                         mtrace('Skipping unchanged course '.$course->fullname);
@@ -160,7 +162,7 @@ abstract class backup_cron_automated_helper {
                         $starttime = time();
 
                         $backupcourse->laststarttime = time();
-                        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED;
+                        $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED;
                         $DB->update_record('backup_courses', $backupcourse);
 
                         $backupcourse->laststatus = backup_cron_automated_helper::launch_automated_backup($course, $backupcourse->laststarttime, $admin->id);
@@ -169,7 +171,7 @@ abstract class backup_cron_automated_helper {
 
                         $DB->update_record('backup_courses', $backupcourse);
 
-                        if ($backupcourse->laststatus) {
+                        if ($backupcourse->laststatus === self::BACKUP_STATUS_OK) {
                             // Clean up any excess course backups now that we have
                             // taken a successful backup.
                             $removedcount = backup_cron_automated_helper::remove_excess_backups($course);
@@ -188,17 +190,18 @@ abstract class backup_cron_automated_helper {
             $message = "";
 
             $count = backup_cron_automated_helper::get_backup_status_array();
-            $haserrors = ($count[backup_cron_automated_helper::BACKUP_STATUS_ERROR] != 0 || $count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED] != 0);
+            $haserrors = ($count[self::BACKUP_STATUS_ERROR] != 0 || $count[self::BACKUP_STATUS_UNFINISHED] != 0);
 
             //Build the message text
             //Summary
             $message .= get_string('summary')."\n";
             $message .= "==================================================\n";
             $message .= "  ".get_string('courses').": ".array_sum($count)."\n";
-            $message .= "  ".get_string('ok').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_OK]."\n";
-            $message .= "  ".get_string('skipped').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_SKIPPED]."\n";
-            $message .= "  ".get_string('error').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_ERROR]."\n";
-            $message .= "  ".get_string('unfinished').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED]."\n\n";
+            $message .= "  ".get_string('ok').": ".$count[self::BACKUP_STATUS_OK]."\n";
+            $message .= "  ".get_string('skipped').": ".$count[self::BACKUP_STATUS_SKIPPED]."\n";
+            $message .= "  ".get_string('error').": ".$count[self::BACKUP_STATUS_ERROR]."\n";
+            $message .= "  ".get_string('unfinished').": ".$count[self::BACKUP_STATUS_UNFINISHED]."\n";
+            $message .= "  ".get_string('warning').": ".$count[self::BACKUP_STATUS_WARNING]."\n\n";
 
             //Reference
             if ($haserrors) {
@@ -261,6 +264,7 @@ abstract class backup_cron_automated_helper {
             self::BACKUP_STATUS_OK => 0,
             self::BACKUP_STATUS_UNFINISHED => 0,
             self::BACKUP_STATUS_SKIPPED => 0,
+            self::BACKUP_STATUS_WARNING => 0
         );
 
         $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) AS statuscount FROM {backup_courses} bc GROUP BY bc.laststatus');
@@ -334,7 +338,7 @@ abstract class backup_cron_automated_helper {
      */
     public static function launch_automated_backup($course, $starttime, $userid) {
 
-        $outcome = true;
+        $outcome = self::BACKUP_STATUS_OK;
         $config = get_config('backup');
         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_AUTOMATED, $userid);
 
@@ -369,6 +373,7 @@ abstract class backup_cron_automated_helper {
 
             $bc->execute_plan();
             $results = $bc->get_results();
+            $outcome = self::outcome_from_results($results);
             $file = $results['backup_destination']; // may be empty if file already moved to target location
             $dir = $config->backup_auto_destination;
             $storage = (int)$config->backup_auto_storage;
@@ -377,8 +382,10 @@ abstract class backup_cron_automated_helper {
             }
             if ($file && !empty($dir) && $storage !== 0) {
                 $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised, !$config->backup_shortname);
-                $outcome = $file->copy_content_to($dir.'/'.$filename);
-                if ($outcome && $storage === 1) {
+                if (!$file->copy_content_to($dir.'/'.$filename)) {
+                    $outcome = self::BACKUP_STATUS_ERROR;
+                }
+                if ($outcome != self::BACKUP_STATUS_ERROR && $storage === 1) {
                     $file->delete();
                 }
             }
@@ -387,7 +394,7 @@ abstract class backup_cron_automated_helper {
             $bc->log('backup_auto_failed_on_course', backup::LOG_ERROR, $course->shortname); // Log error header.
             $bc->log('Exception: ' . $e->errorcode, backup::LOG_ERROR, $e->a, 1); // Log original exception problem.
             $bc->log('Debug: ' . $e->debuginfo, backup::LOG_DEBUG, null, 1); // Log original debug information.
-            $outcome = false;
+            $outcome = self::BACKUP_STATUS_ERROR;
         }
 
         $bc->destroy();
@@ -396,6 +403,30 @@ abstract class backup_cron_automated_helper {
         return $outcome;
     }
 
+    /**
+     * Returns the backup outcome by analysing its results.
+     *
+     * @param array $results returned by a backup
+     * @return int {@link self::BACKUP_STATUS_OK} and other constants
+     */
+    public static function outcome_from_results($results) {
+        $outcome = self::BACKUP_STATUS_OK;
+        foreach ($results as $code => $value) {
+            // Each possible error and warning code has to be specified in this switch
+            // which basically analyses the results to return the correct backup status.
+            switch ($code) {
+                case 'missing_files_in_pool':
+                    $outcome = self::BACKUP_STATUS_WARNING;
+                    break;
+            }
+            // If we found the highest error level, we exit the loop.
+            if ($outcome == self::BACKUP_STATUS_ERROR) {
+                break;
+            }
+        }
+        return $outcome;
+    }
+
     /**
      * Removes deleted courses fromn the backup_courses table so that we don't
      * waste time backing them up.
@@ -530,18 +561,7 @@ abstract class backup_cron_automated_helper {
         if (!empty($dir) && ($storage == 1 || $storage == 2)) {
             // Calculate backup filename regex, ignoring the date/time/info parts that can be
             // variable, depending of languages, formats and automated backup settings
-
-
-            // MDL-33531: use different filenames depending on backup_shortname option
-            if ( !empty($config->backup_shortname) ) {
-                $context = get_context_instance(CONTEXT_COURSE, $course->id);
-                $courseref = format_string($course->shortname, true, array('context' => $context));
-                $courseref = str_replace(' ', '_', $courseref);
-                $courseref = textlib::strtolower(trim(clean_filename($courseref), '_'));
-            } else {
-                $courseref = $course->id;
-            }
-            $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' .$courseref . '-';
+            $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' .$course->id . '-';
             $regex = '#^'.preg_quote($filename, '#').'.*\.mbz$#';
 
             // Store all the matching files into fullpath => timemodified array
index f62fee9..964dd3f 100644 (file)
@@ -94,11 +94,22 @@ abstract class backup_structure_step extends backup_step {
         // Process structure definition
         $structure->process($pr);
 
+        // Get the results from the nested elements
+        $results = $structure->get_results();
+
+        // Get the log messages to append to the log
+        $logs = $structure->get_logs();
+        foreach ($logs as $log) {
+            $this->log($log->message, $log->level, $log->a, $log->depth, $log->display);
+        }
+
         // Close everything
         $xw->stop();
 
         // Destroy the structure. It helps PHP 5.2 memory a lot!
         $structure->destroy();
+
+        return $results;
     }
 
     /**
index 4b0ff58..b6c0ead 100644 (file)
@@ -218,8 +218,14 @@ abstract class restore_structure_step extends restore_step {
      */
     public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) {
         $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid;
-        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
-                                          $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid);
+        $results = restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
+                $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid);
+        $resultstoadd = array();
+        foreach ($results as $result) {
+            $this->log($result->message, $result->level);
+            $resultstoadd[$result->code] = true;
+        }
+        $this->task->add_result($resultstoadd);
     }
 
     /**
index 9a47901..8557ec8 100644 (file)
@@ -37,6 +37,8 @@ class backup_nested_element extends base_nested_element implements processable {
     protected $aliases;   // Define DB->final element aliases
     protected $fileannotations;   // array of file areas to be searched by file annotations
     protected $counter;   // Number of instances of this element that have been processed
+    protected $results;  // Logs the results we encounter during the process.
+    protected $logs;     // Some log messages that could be retrieved later.
 
     /**
      * Constructor - instantiates one backup_nested_element, specifying its basic info.
@@ -55,8 +57,16 @@ class backup_nested_element extends base_nested_element implements processable {
         $this->aliases   = array();
         $this->fileannotations = array();
         $this->counter   = 0;
+        $this->results  = array();
+        $this->logs     = array();
     }
 
+    /**
+     * Process the nested element
+     *
+     * @param object $processor the processor
+     * @return void
+     */
     public function process($processor) {
         if (!$processor instanceof base_processor) { // No correct processor, throw exception
             throw new base_element_struct_exception('incorrect_processor');
@@ -113,6 +123,69 @@ class backup_nested_element extends base_nested_element implements processable {
         $iterator->close();
     }
 
+    /**
+     * Saves a log message to an array
+     *
+     * @see backup_helper::log()
+     * @param string $message to add to the logs
+     * @param int $level level of importance {@link backup::LOG_DEBUG} and other constants
+     * @param mixed $a to be included in $message
+     * @param int $depth of the message
+     * @param display $bool supporting translation via get_string() if true
+     * @return void
+     */
+    protected function add_log($message, $level, $a = null, $depth = null, $display = false) {
+        // Adding the result to the oldest parent.
+        if ($this->get_parent()) {
+            $parent = $this->get_grandparent();
+            $parent->add_log($message, $level, $a, $depth, $display);
+        } else {
+            $log = new stdClass();
+            $log->message = $message;
+            $log->level = $level;
+            $log->a = $a;
+            $log->depth = $depth;
+            $log->display = $display;
+            $this->logs[] = $log;
+        }
+    }
+
+    /**
+     * Saves the results to an array
+     *
+     * @param array $result associative array
+     * @return void
+     */
+    protected function add_result($result) {
+        if (is_array($result)) {
+            // Adding the result to the oldest parent.
+            if ($this->get_parent()) {
+                $parent = $this->get_grandparent();
+                $parent->add_result($result);
+            } else {
+                $this->results = array_merge($this->results, $result);
+            }
+        }
+    }
+
+    /**
+     * Returns the logs
+     *
+     * @return array of log objects
+     */
+    public function get_logs() {
+        return $this->logs;
+    }
+
+    /**
+     * Returns the results
+     *
+     * @return associative array of results
+     */
+    public function get_results() {
+        return $this->results;
+    }
+
     public function set_source_array($arr) {
         // TODO: Only elements having final elements can set source
         $this->var_array = $arr;
index 7995561..0b90e15 100644 (file)
@@ -487,6 +487,9 @@ class backup_ui_stage_complete extends backup_ui_stage_final {
         if (!empty($this->results['include_file_references_to_external_content'])) {
             $output .= $renderer->notification(get_string('filereferencesincluded', 'backup'), 'notifyproblem');
         }
+        if (!empty($this->results['missing_files_in_pool'])) {
+            $output .= $renderer->notification(get_string('missingfilesinpool', 'backup'), 'notifyproblem');
+        }
         $output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess');
         $output .= $renderer->continue_button($restorerul);
         $output .= $renderer->box_end();
index 7e92069..34bff92 100644 (file)
@@ -772,6 +772,9 @@ class restore_ui_stage_complete extends restore_ui_stage_process {
             $html .= $renderer->box_end();
         }
         $html .= $renderer->box_start();
+        if (array_key_exists('file_missing_in_backup', $this->results)) {
+            $html .= $renderer->notification(get_string('restorefileweremissing', 'backup'), 'notifyproblem');
+        }
         $html .= $renderer->notification(get_string('restoreexecutionsuccess', 'backup'), 'notifysuccess');
         $html .= $renderer->continue_button(new moodle_url('/course/view.php', array(
             'id' => $this->get_ui()->get_controller()->get_courseid())), 'get');
index 0daec7c..449e632 100644 (file)
  *
  * @package    block
  * @subpackage completion
- * @copyright  2009 Catalyst IT Ltd
+ * @copyright  2009-2012 Catalyst IT Ltd
  * @author     Aaron Barnes <aaronb@catalyst.net.nz>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-
-require_once($CFG->libdir.'/completionlib.php');
+require_once("{$CFG->libdir}/completionlib.php");
 
 /**
  * Course completion status
@@ -36,25 +35,27 @@ require_once($CFG->libdir.'/completionlib.php');
 class block_completionstatus extends block_base {
 
     public function init() {
-        $this->title   = get_string('pluginname', 'block_completionstatus');
+        $this->title = get_string('pluginname', 'block_completionstatus');
     }
 
     public function get_content() {
-        global $USER, $CFG, $DB, $COURSE;
+        global $USER;
 
         // If content is cached
         if ($this->content !== NULL) {
             return $this->content;
         }
 
+        $course  = $this->page->course;
+
         // Create empty content
-        $this->content = new stdClass;
+        $this->content = new stdClass();
 
         // Can edit settings?
-        $can_edit = has_capability('moodle/course:update', get_context_instance(CONTEXT_COURSE, $this->page->course->id));
+        $can_edit = has_capability('moodle/course:update', get_context_instance(CONTEXT_COURSE, $course->id));
 
         // Get course completion data
-        $info = new completion_info($this->page->course);
+        $info = new completion_info($course);
 
         // Don't display if completion isn't enabled!
         if (!completion_info::is_enabled_for_site()) {
@@ -84,9 +85,9 @@ class block_completionstatus extends block_base {
         // Check this user is enroled
         if (!$info->is_tracked_user($USER->id)) {
             // If not enrolled, but are can view the report:
-            if (has_capability('report/completion:view', get_context_instance(CONTEXT_COURSE, $COURSE->id))) {
-                $this->content->text = '<a href="'.$CFG->wwwroot.'/report/completion/index.php?course='.$COURSE->id.
-                                       '">'.get_string('viewcoursereport', 'completion').'</a>';
+            if (has_capability('report/completion:view', get_context_instance(CONTEXT_COURSE, $course->id))) {
+                $report = new moodle_url('/report/completion/index.php', array('course' => $course->id));
+                $this->content->text = '<a href="'.$report->out().'">'.get_string('viewcoursereport', 'completion').'</a>';
                 return $this->content;
             }
 
@@ -187,7 +188,7 @@ class block_completionstatus extends block_base {
         // Load course completion
         $params = array(
             'userid' => $USER->id,
-            'course' => $COURSE->id
+            'course' => $course->id
         );
         $ccompletion = new completion_completion($params);
 
@@ -221,7 +222,8 @@ class block_completionstatus extends block_base {
         $this->content->text .= $shtml.'</tbody></table>';
 
         // Display link to detailed view
-        $this->content->footer = '<br><a href="'.$CFG->wwwroot.'/blocks/completionstatus/details.php?course='.$COURSE->id.'">'.get_string('moredetails', 'completion').'</a>';
+        $details = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id));
+        $this->content->footer = '<br><a href="'.$details->out().'">'.get_string('moredetails', 'completion').'</a>';
 
         return $this->content;
     }
index a1ac771..84406e3 100644 (file)
  *
  * @package    block
  * @subpackage completion
- * @copyright  2009 Catalyst IT Ltd
+ * @copyright  2009-2012 Catalyst IT Ltd
  * @author     Aaron Barnes <aaronb@catalyst.net.nz>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require_once('../../config.php');
-require_once($CFG->libdir.'/completionlib.php');
-
-
-// TODO:  Make this page Moodle 2.0 compliant
+require_once(dirname(__FILE__).'/../../config.php');
+require_once("{$CFG->libdir}/completionlib.php");
 
 
 ///
 /// Load data
 ///
 $id = required_param('course', PARAM_INT);
-// User id
 $userid = optional_param('user', 0, PARAM_INT);
 
 // Load course
-$course = $DB->get_record('course', array('id' => $id));
+$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST);
 
 // Load user
 if ($userid) {
@@ -76,21 +72,13 @@ if (!$can_view) {
 // Load completion data
 $info = new completion_info($course);
 
-$returnurl = "{$CFG->wwwroot}/course/view.php?id={$id}";
+$returnurl = new moodle_url('/course/view.php', array('id' => $id));
 
 // Don't display if completion isn't enabled!
 if (!$info->is_enabled()) {
     print_error('completionnotenabled', 'completion', $returnurl);
 }
 
-// Load criteria to display
-$completions = $info->get_completions($user->id);
-
-// Check if this course has any criteria
-if (empty($completions)) {
-    print_error('nocriteriaset', 'completion', $returnurl);
-}
-
 // Check this user is enroled
 if (!$info->is_tracked_user($user->id)) {
     if ($USER->id == $user->id) {
@@ -104,6 +92,7 @@ if (!$info->is_tracked_user($user->id)) {
 ///
 /// Display page
 ///
+$PAGE->set_context(context_course::instance($course->id));
 
 // Print header
 $page = get_string('completionprogressdetails', 'block_completionstatus');
@@ -111,7 +100,7 @@ $title = format_string($course->fullname) . ': ' . $page;
 
 $PAGE->navbar->add($page);
 $PAGE->set_pagelayout('standard');
-$PAGE->set_url('/blocks/completionstatus/details.php', array('course' => $course->id));
+$PAGE->set_url('/blocks/completionstatus/details.php', array('course' => $course->id, 'user' => $user->id));
 $PAGE->set_title(get_string('course') . ': ' . $course->fullname);
 $PAGE->set_heading($title);
 echo $OUTPUT->header();
@@ -135,122 +124,148 @@ $coursecomplete = $info->is_course_complete($user->id);
 // Has this user completed any criteria?
 $criteriacomplete = $info->count_course_user_data($user->id);
 
+// Load course completion
+$params = array(
+    'userid' => $user->id,
+    'course' => $course->id,
+);
+$ccompletion = new completion_completion($params);
+
 if ($coursecomplete) {
     echo get_string('complete');
-} else if (!$criteriacomplete) {
+} else if (!$criteriacomplete && !$ccompletion->timestarted) {
     echo '<i>'.get_string('notyetstarted', 'completion').'</i>';
 } else {
     echo '<i>'.get_string('inprogress','completion').'</i>';
 }
 
 echo '</td></tr>';
-echo '<tr><td colspan="2"><b>'.get_string('required').':</b> ';
 
-// Get overall aggregation method
-$overall = $info->get_aggregation_method();
+// Load criteria to display
+$completions = $info->get_completions($user->id);
 
-if ($overall == COMPLETION_AGGREGATION_ALL) {
-    echo get_string('criteriarequiredall', 'completion');
+// Check if this course has any criteria
+if (empty($completions)) {
+    echo '<tr><td colspan="2"><br />';
+    echo $OUTPUT->box(get_string('err_nocriteria', 'completion'), 'noticebox');
+    echo '</td></tr></tbody></table>';
 } else {
-    echo get_string('criteriarequiredany', 'completion');
-}
+    echo '<tr><td colspan="2"><b>'.get_string('required').':</b> ';
 
-echo '</td></tr></tbody></table>';
-
-// Generate markup for criteria statuses
-echo '<table class="generalbox boxaligncenter" cellpadding="3"><tbody>';
-echo '<tr class="ccheader">';
-echo '<th class="c0 header" scope="col">'.get_string('criteriagroup', 'block_completionstatus').'</th>';
-echo '<th class="c1 header" scope="col">'.get_string('criteria', 'completion').'</th>';
-echo '<th class="c2 header" scope="col">'.get_string('requirement', 'block_completionstatus').'</th>';
-echo '<th class="c3 header" scope="col">'.get_string('status').'</th>';
-echo '<th class="c4 header" scope="col">'.get_string('complete').'</th>';
-echo '<th class="c5 header" scope="col">'.get_string('completiondate', 'report_completion').'</th>';
-echo '</tr>';
-
-// Save row data
-$rows = array();
-
-global $COMPLETION_CRITERIA_TYPES;
-
-// Loop through course criteria
-foreach ($completions as $completion) {
-    $criteria = $completion->get_criteria();
-    $complete = $completion->is_complete();
-
-    $row = array();
-    $row['type'] = $criteria->criteriatype;
-    $row['title'] = $criteria->get_title();
-    $row['status'] = $completion->get_status();
-    $row['timecompleted'] = $completion->timecompleted;
-    $row['details'] = $criteria->get_details($completion);
-    $rows[] = $row;
-}
+    // Get overall aggregation method
+    $overall = $info->get_aggregation_method();
 
-// Print table
-$last_type = '';
-$agg_type = false;
+    if ($overall == COMPLETION_AGGREGATION_ALL) {
+        echo get_string('criteriarequiredall', 'completion');
+    } else {
+        echo get_string('criteriarequiredany', 'completion');
+    }
+
+    echo '</td></tr></tbody></table>';
+
+    // Generate markup for criteria statuses
+    echo '<table class="generalbox logtable boxaligncenter" id="criteriastatus" width="100%"><tbody>';
+    echo '<tr class="ccheader">';
+    echo '<th class="c0 header" scope="col">'.get_string('criteriagroup', 'block_completionstatus').'</th>';
+    echo '<th class="c1 header" scope="col">'.get_string('criteria', 'completion').'</th>';
+    echo '<th class="c2 header" scope="col">'.get_string('requirement', 'block_completionstatus').'</th>';
+    echo '<th class="c3 header" scope="col">'.get_string('status').'</th>';
+    echo '<th class="c4 header" scope="col">'.get_string('complete').'</th>';
+    echo '<th class="c5 header" scope="col">'.get_string('completiondate', 'report_completion').'</th>';
+    echo '</tr>';
 
-foreach ($rows as $row) {
+    // Save row data
+    $rows = array();
+
+    // Loop through course criteria
+    foreach ($completions as $completion) {
+        $criteria = $completion->get_criteria();
+
+        $row = array();
+        $row['type'] = $criteria->criteriatype;
+        $row['title'] = $criteria->get_title();
+        $row['status'] = $completion->get_status();
+        $row['complete'] = $completion->is_complete();
+        $row['timecompleted'] = $completion->timecompleted;
+        $row['details'] = $criteria->get_details($completion);
+        $rows[] = $row;
+    }
 
-    // Criteria group
-    echo '<td class="c0">';
-    if ($last_type !== $row['details']['type']) {
-        $last_type = $row['details']['type'];
-        echo $last_type;
+    // Print table
+    $last_type = '';
+    $agg_type = false;
+    $oddeven = 0;
 
-        // Reset agg type
-        $agg_type = true;
-    } else {
-        // Display aggregation type
-        if ($agg_type) {
-            $agg = $info->get_aggregation_method($row['type']);
+    foreach ($rows as $row) {
 
-            echo '(<i>';
+        echo '<tr class="r' . $oddeven . '">';
 
-            if ($agg == COMPLETION_AGGREGATION_ALL) {
-                echo strtolower(get_string('all', 'completion'));
-            } else {
-                echo strtolower(get_string('any', 'completion'));
-            }
+        // Criteria group
+        echo '<td class="cell c0">';
+        if ($last_type !== $row['details']['type']) {
+            $last_type = $row['details']['type'];
+            echo $last_type;
+
+            // Reset agg type
+            $agg_type = true;
+        } else {
+            // Display aggregation type
+            if ($agg_type) {
+                $agg = $info->get_aggregation_method($row['type']);
 
-            echo '</i> '.strtolower(get_string('required')).')';
-            $agg_type = false;
+                echo '(<i>';
+
+                if ($agg == COMPLETION_AGGREGATION_ALL) {
+                    echo strtolower(get_string('aggregateall', 'completion'));
+                } else {
+                    echo strtolower(get_string('aggregateany', 'completion'));
+                }
+
+                echo '</i> '.strtolower(get_string('required')).')';
+                $agg_type = false;
+            }
         }
+        echo '</td>';
+
+        // Criteria title
+        echo '<td class="cell c1">';
+        echo $row['details']['criteria'];
+        echo '</td>';
+
+        // Requirement
+        echo '<td class="cell c2">';
+        echo $row['details']['requirement'];
+        echo '</td>';
+
+        // Status
+        echo '<td class="cell c3">';
+        echo $row['details']['status'];
+        echo '</td>';
+
+        // Is complete
+        echo '<td class="cell c4">';
+        echo $row['complete'] ? get_string('yes') : get_string('no');
+        echo '</td>';
+
+        // Completion data
+        echo '<td class="cell c5">';
+        if ($row['timecompleted']) {
+            echo userdate($row['timecompleted'], get_string('strftimedate', 'langconfig'));
+        } else {
+            echo '-';
+        }
+        echo '</td>';
+        echo '</tr>';
+        // for row striping
+        $oddeven = $oddeven ? 0 : 1;
     }
-    echo '</td>';
-
-    // Criteria title
-    echo '<td class="c1">';
-    echo $row['details']['criteria'];
-    echo '</td>';
-
-    // Requirement
-    echo '<td class="c2">';
-    echo $row['details']['requirement'];
-    echo '</td>';
-
-    // Status
-    echo '<td class="c3">';
-    echo $row['details']['status'];
-    echo '</td>';
-
-    // Is complete
-    echo '<td class="c4">';
-    echo ($row['status'] === get_string('yes')) ? get_string('yes') : get_string('no');
-    echo '</td>';
-
-    // Completion data
-    echo '<td class="c5">';
-    if ($row['timecompleted']) {
-        echo userdate($row['timecompleted'], '%e %B %G');
-    } else {
-        echo '-';
-    }
-    echo '</td>';
-    echo '</tr>';
+
+    echo '</tbody></table>';
 }
 
-echo '</tbody></table>';
+echo '<div class="buttons">';
+$courseurl = new moodle_url("/course/view.php", array('id' => $course->id));
+echo $OUTPUT->single_button($courseurl, get_string('returntocourse', 'block_completionstatus'), 'get');
+echo '</div>';
 
 echo $OUTPUT->footer();
index fcc965a..6658c17 100644 (file)
@@ -5,3 +5,4 @@ $string['criteriagroup'] = 'Criteria group';
 $string['firstofsecond'] = '{$a->first} of {$a->second}';
 $string['pluginname'] = 'Course completion status';
 $string['requirement'] = 'Requirement';
+$string['returntocourse'] = 'Return to course';
index 6ac8944..a0d394e 100644 (file)
@@ -405,11 +405,10 @@ class blog_entry {
      * @return void
      */
     public function delete() {
-        global $DB, $USER;
-
-        $returnurl = '';
+        global $DB;
 
         $this->delete_attachments();
+        $this->remove_associations();
 
         $DB->delete_records('post', array('id' => $this->id));
         tag_set('post', $this->id, array());
index 9f33f83..73723af 100644 (file)
@@ -38,6 +38,7 @@ class editsection_form extends moodleform {
         $course = $this->_customdata['course'];
 
         if (!empty($CFG->enableavailability)) {
+            $mform->addElement('header', '', get_string('availabilityconditions', 'condition'));
             // Grouping conditions - only if grouping is enabled at site level
             if (!empty($CFG->enablegroupmembersonly)) {
                 $options = array();
@@ -49,7 +50,6 @@ class editsection_form extends moodleform {
                                 $grouping->name, true, array('context' => $context));
                     }
                 }
-                $mform->addElement('header', '', get_string('availabilityconditions', 'condition'));
                 $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options);
                 $mform->addHelpButton('groupingid', 'groupingsection', 'group');
             }
index 09fb15b..b4a86fe 100644 (file)
@@ -61,13 +61,13 @@ class course_reset_form extends moodleform {
         if ($allmods = $DB->get_records('modules') ) {
             foreach ($allmods as $mod) {
                 $modname = $mod->name;
-                if (!$DB->count_records($modname, array('course'=>$COURSE->id))) {
-                    continue; // skip mods with no instances
-                }
                 $modfile = $CFG->dirroot."/mod/$modname/lib.php";
                 $mod_reset_course_form_definition = $modname.'_reset_course_form_definition';
                 $mod_reset__userdata = $modname.'_reset_userdata';
                 if (file_exists($modfile)) {
+                    if (!$DB->count_records($modname, array('course'=>$COURSE->id))) {
+                        continue; // Skip mods with no instances
+                    }
                     include_once($modfile);
                     if (function_exists($mod_reset_course_form_definition)) {
                         $mod_reset_course_form_definition($mform);
index 54ffaff..def6b44 100644 (file)
@@ -89,7 +89,7 @@ function filter_algebra_image($imagefile, $tex= "", $height="", $width="", $alig
 }
 
 class filter_algebra extends moodle_text_filter {
-    function filter($text, array $options = array()){
+    public function filter($text, array $options = array()){
         global $CFG, $DB;
 
         /// Do a quick check using stripos to avoid unnecessary wor
@@ -114,9 +114,6 @@ class filter_algebra extends moodle_text_filter {
 #        return $text;
 #    }
 
-
-        $text .= ' ';
-
         preg_match_all('/@(@@+)([^@])/',$text,$matches);
         for ($i=0;$i<count($matches[0]);$i++) {
             $replacement = str_replace('@','&#x00040;',$matches[1][$i]).$matches[2][$i];
@@ -129,6 +126,17 @@ class filter_algebra extends moodle_text_filter {
         preg_match_all('/<algebra>(.+?)<\/algebra>|@@(.+?)@@/is', $text, $matches);
         for ($i=0; $i<count($matches[0]); $i++) {
             $algebra = $matches[1][$i] . $matches[2][$i];
+
+            // Look for some common false positives, and skip processing them.
+            if ($algebra == 'PLUGINFILE' || $algebra == 'DRAFTFILE') {
+                // Raw pluginfile URL.
+                continue;
+            }
+            if (preg_match('/^ -\d+(,\d+)? \+\d+(,\d+)? $/', $algebra)) {
+                // Part of a unified diff.
+                continue;
+            }
+
             $algebra = str_replace('<nolink>','',$algebra);
             $algebra = str_replace('</nolink>','',$algebra);
             $algebra = str_replace('<span class="nolink">','',$algebra);
@@ -159,7 +167,7 @@ class filter_algebra extends moodle_text_filter {
                $algebra = str_replace('upsilon','zupslon',$algebra);
                $algebra = preg_replace('!\r\n?!',' ',$algebra);
                $algebra = escapeshellarg($algebra);
-               if ( (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows") ) {
+               if ( (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
                   $cmd  = "cd $CFG->dirroot\\filter\\algebra & algebra2tex.pl $algebra";
                } else {
                   $cmd  = "cd $CFG->dirroot/filter/algebra; ./algebra2tex.pl $algebra";
@@ -220,6 +228,7 @@ class filter_algebra extends moodle_text_filter {
                   $texexp = preg_replace('/\\\int\\\left\((.+?d[a-z])\\\right\)/s','\int '. "\$1 ",$texexp);
                   $texexp = preg_replace('/\\\lim\\\left\((.+?),(.+?),(.+?)\\\right\)/s','\lim_'. "{\$2\\to \$3}\$1 ",$texexp);
                   $texexp = str_replace('\mbox', '', $texexp); // now blacklisted in tex, sorry
+                  $texcache = new stdClass();
                   $texcache->filter = 'algebra';
                   $texcache->version = 1;
                   $texcache->md5key = $md5;
diff --git a/filter/algebra/tests/filter_test.php b/filter/algebra/tests/filter_test.php
new file mode 100644 (file)
index 0000000..6c7db8f
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit test for the filter_algebra
+ *
+ * @package    filter_algebra
+ * @category   phpunit
+ * @copyright  2012 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/filter/algebra/filter.php');
+
+
+/**
+ * Unit tests for filter_algebra.
+ *
+ * Note that this only tests some of the filter logic. It does not acutally test
+ * the normal case of the filter working, because I cannot make it work on my
+ * test server, and if it does not work here, it probably does not also work
+ * for other people. A failing test will be irritating noise.
+ *
+ * @copyright  2012 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class filter_algebra_testcase extends basic_testcase {
+
+    protected $filter;
+
+    protected function setUp() {
+        parent::setUp();
+        $this->filter = new filter_algebra(context_system::instance(), array());
+    }
+
+    function test_algebra_filter_no_algebra() {
+        $this->assertEquals('<p>Look no algebra!</p>',
+                $this->filter->filter('<p>Look no algebra!</p>'));
+    }
+
+
+    function test_algebra_filter_pluginfile() {
+        $this->assertEquals('<img src="@@PLUGINFILE@@/photo.jpg">',
+                $this->filter->filter('<img src="@@PLUGINFILE@@/photo.jpg">'));
+    }
+
+    function test_algebra_filter_draftfile() {
+        $this->assertEquals('<img src="@@DRAFTFILE@@/photo.jpg">',
+                $this->filter->filter('<img src="@@DRAFTFILE@@/photo.jpg">'));
+    }
+
+    function test_algebra_filter_unified_diff() {
+        $diff = '
+diff -u -r1.1 Worksheet.php
+--- Worksheet.php   26 Sep 2003 04:18:02 -0000  1.1
++++ Worksheet.php   18 Nov 2009 03:58:50 -0000
+@@ -1264,10 +1264,10 @@
+         }
+
+         // Strip the = or @ sign at the beginning of the formula string
+-        if (ereg("^=",$formula)) {
++        if (preg_match("/^=/",$formula)) {
+             $formula = preg_replace("/(^=)/","",$formula);
+         }
+-        elseif(ereg("^@",$formula)) {
++        elseif(preg_match("/^@/",$formula)) {
+             $formula = preg_replace("/(^@)/","",$formula);
+         }
+         else {
+';
+        $this->assertEquals('<pre>' . $diff . '</pre>',
+                $this->filter->filter('<pre>' . $diff . '</pre>'));
+    }
+}
index b166e54..d7d3b5b 100644 (file)
@@ -2574,6 +2574,7 @@ abstract class grade_helper {
      * @return array
      */
     public static function get_info_letters($courseid) {
+        global $SITE;
         if (self::$letterinfo !== null) {
             return self::$letterinfo;
         }
@@ -2581,9 +2582,15 @@ abstract class grade_helper {
         $canmanage = has_capability('moodle/grade:manage', $context);
         $canmanageletters = has_capability('moodle/grade:manageletters', $context);
         if ($canmanage || $canmanageletters) {
+            // Redirect to system context when report is accessed from admin settings MDL-31633
+            if ($context->instanceid == $SITE->id) {
+                $param = array('edit' => 1);
+            } else {
+                $param = array('edit' => 1,'id' => $context->id);
+            }
             self::$letterinfo = array(
                 'view' => new grade_plugin_info('view', new moodle_url('/grade/edit/letter/index.php', array('id'=>$context->id)), get_string('view')),
-                'edit' => new grade_plugin_info('edit', new moodle_url('/grade/edit/letter/index.php', array('edit'=>1,'id'=>$context->id)), get_string('edit'))
+                'edit' => new grade_plugin_info('edit', new moodle_url('/grade/edit/letter/index.php', $param), get_string('edit'))
             );
         } else {
             self::$letterinfo = false;
index 3c005d8..9977e2b 100644 (file)
@@ -34,9 +34,11 @@ $string['clianswerno'] = 'n';
 $string['cliansweryes'] = 't';
 $string['cliincorrectvalueerror'] = 'Klaida, klaidinga "{$a->option}" reikšmė "{$a->value}"';
 $string['cliincorrectvalueretry'] = 'Klaidinga reikšmė, bandykite dar kartą';
-$string['clitypevalue'] = 'tipo reikšmė';
-$string['clitypevaluedefault'] = 'tipo reikšmė, paspauskite „Enter“, jei norite naudoti numatytąją reikšmę ({$a})';
-$string['cliunknowoption'] = 'Neatpažintos parinktys: {$a} naudokite --žinyno parinktį.';
+$string['clitypevalue'] = 'įveskite reikšmę';
+$string['clitypevaluedefault'] = 'įveskite reikšmę, paspauskite „Enter“, jei norite naudoti numatytąją reikšmę ({$a})';
+$string['cliunknowoption'] = 'Neatpažintos parinktys:
+{$a}
+Naudokite --help parinktį.';
 $string['cliyesnoprompt'] = 'įveskite t (taip) arba n (ne)';
 $string['environmentrequireinstall'] = 'turi būti įdiegta ir įgalinta';
 $string['environmentrequireversion'] = 'reikalinga {$a->needed} versija, o Jūs turite {$a->current}';
index 26fbe3b..5d26680 100644 (file)
@@ -68,7 +68,7 @@ $string['availablelicenses'] = 'Available licences';
 $string['backgroundcolour'] = 'Transparent colour';
 $string['backups'] = 'Backups';
 $string['backup_shortname'] = 'Use course name in backup filename';
-$string['backup_shortnamehelp'] = 'Use the course name as part of the backup filename instead of the course id number.';
+$string['backup_shortnamehelp'] = 'Use the course name as part of the backup filename.';
 $string['badwordsconfig'] = 'Enter your list of bad words separated by commas.';
 $string['badwordsdefault'] = 'If the custom list is empty, a default list from the language pack will be used.';
 $string['badwordslist'] = 'Custom bad words list';
index 5fde19e..e053c96 100644 (file)
@@ -163,6 +163,7 @@ $string['lockedbypermission'] = 'You don\'t have sufficient permissions to chang
 $string['lockedbyconfig'] = 'This setting has been locked by the default backup settings';
 $string['lockedbyhierarchy'] = 'Locked by dependencies';
 $string['managefiles'] = 'Manage backup files';
+$string['missingfilesinpool'] = 'Some files could not be saved during the backup, it won\'t be possible to restore them.';
 $string['moodleversion'] = 'Moodle version';
 $string['moreresults'] = 'There are too many results, enter a more specific search.';
 $string['nomatchingcourses'] = 'There are no courses to display';
@@ -177,6 +178,7 @@ $string['restoreactivity'] = 'Restore activity';
 $string['restorecourse'] = 'Restore course';
 $string['restorecoursesettings'] = 'Course settings';
 $string['restoreexecutionsuccess'] = 'The course was restored successfully, clicking the continue button below will take you to view the course you restored.';
+$string['restorefileweremissing'] = 'Some files could not be restored because they were missing in the backup.';
 $string['restorenewcoursefullname'] = 'New course name';
 $string['restorenewcourseshortname'] = 'New course short name';
 $string['restorenewcoursestartdate'] = 'New start date';
index 377b840..a3d28ab 100644 (file)
@@ -1803,6 +1803,7 @@ $string['virusfounduser'] = 'The file you have uploaded, {$a->filename}, has bee
 $string['virusplaceholder'] = 'This file that has been uploaded was found to contain a virus and has been moved or deleted and the user notified.';
 $string['visible'] = 'Visible';
 $string['visibletostudents'] = 'Visible to {$a}';
+$string['warning'] = 'Warning';
 $string['warningdeleteresource'] = 'Warning: {$a} is referred in a resource. Would you like to update the resource?';
 $string['webpage'] = 'Web page';
 $string['week'] = 'Week';
index 1245426..8682378 100644 (file)
@@ -100,6 +100,7 @@ $string['error'] = 'An unknown error occurred!';
 $string['errornotyourfile'] = 'You cannot pick file which is not added by your';
 $string['erroruniquename'] = 'Repository instance name should be unique';
 $string['errorpostmaxsize'] = 'The uploaded file may exceed max_post_size directive in php.ini.';
+$string['errorwhiledownload'] = 'An error occured while downloading the file: {$a}';
 $string['existingrepository'] = 'This repository already exists';
 $string['federatedsearch'] = 'Federated search';
 $string['fileexists'] = 'File name already being used, please use another name';
index c62dae4..50a9ee4 100644 (file)
@@ -6537,7 +6537,7 @@ class context_module extends context {
                     if ($withprefix){
                         $name = get_string('modulename', $cm->modname).': ';
                     }
-                    $name .= $mod->name;
+                    $name .= format_string($mod->name, true, array('context' => $this));
                 }
             }
         return $name;
index 0cee0da..974124f 100644 (file)
@@ -176,7 +176,7 @@ class boxclient {
         $params['action']     = 'get_account_tree';
         $params['onelevel']   = 1;
         $params['params[]']   = 'nozip';
-        $c = new curl(array('debug'=>$this->debug, 'cache'=>true, 'module_cache'=>'repository'));
+        $c = new curl(array('debug'=>$this->debug));
         $c->setopt(array('CURLOPT_FOLLOWLOCATION'=>1));
         try {
             $args = array();
@@ -196,23 +196,25 @@ class boxclient {
      * Get box.net file info
      *
      * @param string $fileid
-     * @return string|null
+     * @param int $timeout request timeout in seconds
+     * @return stdClass|null
      */
-    function get_file_info($fileid) {
+    function get_file_info($fileid, $timeout = 0) {
         $this->_clearErrors();
         $params = array();
         $params['action']     = 'get_file_info';
         $params['file_id']    = $fileid;
         $params['auth_token'] = $this->auth_token;
         $params['api_key']    = $this->api_key;
-        $http = new curl(array('debug'=>$this->debug, 'cache'=>true, 'module_cache'=>'repository'));
-        $xml = $http->get($this->_box_api_url, $params);
-        $o = simplexml_load_string(trim($xml));
-        if ($o->status == 's_get_file_info') {
-            return $o->info;
-        } else {
-            return null;
+        $http = new curl(array('debug'=>$this->debug));
+        $xml = $http->get($this->_box_api_url, $params, array('timeout' => $timeout));
+        if (!$http->get_errno()) {
+            $o = simplexml_load_string(trim($xml));
+            if ($o->status == 's_get_file_info') {
+                return $o->info;
+            }
         }
+        return null;
     }
 
     /**
index 54e9160..fcd5910 100644 (file)
@@ -621,16 +621,20 @@ class completion_info {
             debugging('set_module_viewed must be called before header is printed',
                     DEBUG_DEVELOPER);
         }
+
         // Don't do anything if view condition is not turned on
         if ($cm->completionview == COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) {
             return;
         }
+
         // Get current completion state
-        $data = $this->get_data($cm, $userid);
+        $data = $this->get_data($cm, false, $userid);
+
         // If we already viewed it, don't do anything
         if ($data->viewed == COMPLETION_VIEWED) {
             return;
         }
+
         // OK, change state, save it, and update completion
         $data->viewed = COMPLETION_VIEWED;
         $this->internal_set_data($cm, $data);
index 1e8b1c8..6562052 100644 (file)
@@ -466,10 +466,6 @@ function cron_run() {
     $fs = get_file_storage();
     $fs->cron();
 
-    mtrace("Clean up cached external files");
-    // 1 week
-    cache_file::cleanup(array(), 60 * 60 * 24 * 7);
-
     mtrace("Cron script completed correctly");
 
     $difftime = microtime_diff($starttime, microtime());
index 5103e0b..5247e49 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * Returns all non-view and non-temp tables with sane names.
+ * Prints list of non-supported tables using $OUTPUT->notification()
+ *
+ * @return array
+ */
+function upgrade_mysql_get_supported_tables() {
+    global $OUTPUT, $DB;
+
+    $tables = array();
+    $patprefix = str_replace('_', '\\_', $DB->get_prefix());
+    $pregprefix = preg_quote($DB->get_prefix(), '/');
+
+    $sql = "SHOW FULL TABLES LIKE '$patprefix%'";
+    $rs = $DB->get_recordset_sql($sql);
+    foreach ($rs as $record) {
+        $record = array_change_key_case((array)$record, CASE_LOWER);
+        $type = $record['table_type'];
+        unset($record['table_type']);
+        $fullname = array_shift($record);
+
+        if ($pregprefix === '') {
+            $name = $fullname;
+        } else {
+            $count = null;
+            $name = preg_replace("/^$pregprefix/", '', $fullname, -1, $count);
+            if ($count !== 1) {
+                continue;
+            }
+        }
+
+        if (!preg_match("/^[a-z][a-z0-9_]*$/", $name)) {
+            echo $OUTPUT->notification("Database table with invalid name '$fullname' detected, skipping.", 'notifyproblem');
+            continue;
+        }
+        if ($type === 'VIEW') {
+            echo $OUTPUT->notification("Unsupported database table view '$fullname' detected, skipping.", 'notifyproblem');
+            continue;
+        }
+        $tables[$name] = $name;
+    }
+    $rs->close();
+
+    return $tables;
+}
 
 /**
  * Remove all signed numbers from current database - mysql only.
@@ -50,7 +95,7 @@ function upgrade_mysql_fix_unsigned_columns() {
     $pbar = new progress_bar('mysqlconvertunsigned', 500, true);
 
     $prefix = $DB->get_prefix();
-    $tables = $DB->get_tables();
+    $tables = upgrade_mysql_get_supported_tables();
 
     $tablecount = count($tables);
     $i = 0;
@@ -115,7 +160,7 @@ function upgrade_mysql_fix_lob_columns() {
     $pbar = new progress_bar('mysqlconvertlobs', 500, true);
 
     $prefix = $DB->get_prefix();
-    $tables = $DB->get_tables();
+    $tables = upgrade_mysql_get_supported_tables();
     asort($tables);
 
     $tablecount = count($tables);
index 4f60f3c..df23d91 100644 (file)
@@ -804,10 +804,6 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
                 continue;
             }
 
-            // Replaced file content
-            if ($oldfile->get_contenthash() != $newfile->get_contenthash()) {
-                $oldfile->replace_content_with($newfile);
-            }
             // Updated author
             if ($oldfile->get_author() != $newfile->get_author()) {
                 $oldfile->set_author($newfile->get_author());
@@ -827,16 +823,18 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
                 $oldfile->set_sortorder($newfile->get_sortorder());
             }
 
-            // Update file size
-            if ($oldfile->get_filesize() != $newfile->get_filesize()) {
-                $oldfile->set_filesize($newfile->get_filesize());
-            }
-
             // Update file timemodified
             if ($oldfile->get_timemodified() != $newfile->get_timemodified()) {
                 $oldfile->set_timemodified($newfile->get_timemodified());
             }
 
+            // Replaced file content
+            if ($oldfile->get_contenthash() != $newfile->get_contenthash() || $oldfile->get_filesize() != $newfile->get_filesize()) {
+                $oldfile->replace_content_with($newfile);
+                // push changes to all local files that are referencing this file
+                $fs->update_references_to_storedfile($this);
+            }
+
             // unchanged file or directory - we keep it as is
             unset($newhashes[$oldhash]);
             if (!$oldfile->is_directory()) {
@@ -2328,7 +2326,7 @@ function send_stored_file($stored_file, $lifetime=86400 , $filter=0, $forcedownl
     }
 
     // handle external resource
-    if ($stored_file && $stored_file->is_external_file()) {
+    if ($stored_file && $stored_file->is_external_file() && !isset($options['sendcachedexternalfile'])) {
         $stored_file->send_file($lifetime, $filter, $forcedownload, $options);
         die;
     }
@@ -2752,6 +2750,8 @@ class curl {
     public  $info;
     /** @var string error */
     public  $error;
+    /** @var int error code */
+    public  $errno;
 
     /** @var array cURL options */
     private $options;
@@ -2895,6 +2895,14 @@ class curl {
         unset($this->options['CURLOPT_INFILE']);
         unset($this->options['CURLOPT_INFILESIZE']);
         unset($this->options['CURLOPT_CUSTOMREQUEST']);
+        unset($this->options['CURLOPT_FILE']);
+    }
+
+    /**
+     * Resets the HTTP Request headers (to prepare for the new request)
+     */
+    public function resetHeader() {
+        $this->header = array();
     }
 
     /**
@@ -3119,6 +3127,7 @@ class curl {
 
         $this->info  = curl_getinfo($curl);
         $this->error = curl_error($curl);
+        $this->errno = curl_errno($curl);
 
         if ($this->debug){
             echo '<h1>Return Data</h1>';
@@ -3202,6 +3211,62 @@ class curl {
         return $this->request($url, $options);
     }
 
+    /**
+     * Downloads one file and writes it to the specified file handler
+     *
+     * <code>
+     * $c = new curl();
+     * $file = fopen('savepath', 'w');
+     * $result = $c->download_one('http://localhost/', null,
+     *   array('file' => $file, 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));
+     * fclose($file);
+     * $download_info = $c->get_info();
+     * if ($result === true) {
+     *   // file downloaded successfully
+     * } else {
+     *   $error_text = $result;
+     *   $error_code = $c->get_errno();
+     * }
+     * </code>
+     *
+     * <code>
+     * $c = new curl();
+     * $result = $c->download_one('http://localhost/', null,
+     *   array('filepath' => 'savepath', 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));
+     * // ... see above, no need to close handle and remove file if unsuccessful
+     * </code>
+     *
+     * @param string $url
+     * @param array|null $params key-value pairs to be added to $url as query string
+     * @param array $options request options. Must include either 'file' or 'filepath'
+     * @return bool|string true on success or error string on failure
+     */
+    public function download_one($url, $params, $options = array()) {
+        $options['CURLOPT_HTTPGET'] = 1;
+        $options['CURLOPT_BINARYTRANSFER'] = true;
+        if (!empty($params)){
+            $url .= (stripos($url, '?') !== false) ? '&' : '?';
+            $url .= http_build_query($params, '', '&');
+        }
+        if (!empty($options['filepath']) && empty($options['file'])) {
+            // open file
+            if (!($options['file'] = fopen($options['filepath'], 'w'))) {
+                $this->errno = 100;
+                return get_string('cannotwritefile', 'error', $options['filepath']);
+            }
+            $filepath = $options['filepath'];
+        }
+        unset($options['filepath']);
+        $result = $this->request($url, $options);
+        if (isset($filepath)) {
+            fclose($options['file']);
+            if ($result !== true) {
+                unlink($filepath);
+            }
+        }
+        return $result;
+    }
+
     /**
      * HTTP PUT method
      *
@@ -3279,6 +3344,15 @@ class curl {
     public function get_info() {
         return $this->info;
     }
+
+    /**
+     * Get curl error code
+     *
+     * @return int
+     */
+    public function get_errno() {
+        return $this->errno;
+    }
 }
 
 /**
@@ -4197,170 +4271,3 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
     }
 
 }
-
-/**
- * Universe file cacheing class
- *
- * @package    core_files
- * @category   files
- * @copyright  2012 Dongsheng Cai {@link http://dongsheng.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class cache_file {
-    /** @var string */
-    public $cachedir = '';
-
-    /**
-     * static method to create cache_file class instance
-     *
-     * @param array $options caching ooptions
-     */
-    public static function get_instance($options = array()) {
-        return new cache_file($options);
-    }
-
-    /**
-     * Constructor
-     *
-     * @param array $options
-     */
-    private function __construct($options = array()) {
-        global $CFG;
-
-        // Path to file caches.
-        if (isset($options['cachedir'])) {
-            $this->cachedir = $options['cachedir'];
-        } else {
-            $this->cachedir = $CFG->cachedir . '/filedir';
-        }
-
-        // Create cache directory.
-        if (!file_exists($this->cachedir)) {
-            mkdir($this->cachedir, $CFG->directorypermissions, true);
-        }
-
-        // When use cache_file::get, it will check ttl.
-        if (isset($options['ttl']) && is_numeric($options['ttl'])) {
-            $this->ttl = $options['ttl'];
-        } else {
-            // One day.
-            $this->ttl = 60 * 60 * 24;
-        }
-    }
-
-    /**
-     * Get cached file, false if file expires
-     *
-     * @param mixed $param
-     * @param array $options caching options
-     * @return bool|string
-     */
-    public static function get($param, $options = array()) {
-        $instance = self::get_instance($options);
-        $filepath = $instance->generate_filepath($param);
-        if (file_exists($filepath)) {
-            $lasttime = filemtime($filepath);
-            if (time() - $lasttime > $instance->ttl) {
-                // Remove cache file.
-                unlink($filepath);
-                return false;
-            } else {
-                return $filepath;
-            }
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Static method to create cache from a file
-     *
-     * @param mixed $ref
-     * @param string $srcfile
-     * @param array $options
-     * @return string cached file path
-     */
-    public static function create_from_file($ref, $srcfile, $options = array()) {
-        $instance = self::get_instance($options);
-        $cachedfilepath = $instance->generate_filepath($ref);
-        copy($srcfile, $cachedfilepath);
-        return $cachedfilepath;
-    }
-
-    /**
-     * Static method to create cache from url
-     *
-     * @param mixed $ref file reference
-     * @param string $url file url
-     * @param array $options options
-     * @return string cached file path
-     */
-    public static function create_from_url($ref, $url, $options = array()) {
-        $instance = self::get_instance($options);
-        $cachedfilepath = $instance->generate_filepath($ref);
-        $fp = fopen($cachedfilepath, 'w');
-        $curl = new curl;
-        $curl->download(array(array('url'=>$url, 'file'=>$fp)));
-        // Must close file handler.
-        fclose($fp);
-        return $cachedfilepath;
-    }
-
-    /**
-     * Static method to create cache from string
-     *
-     * @param mixed $ref file reference
-     * @param string $url file url
-     * @param array $options options
-     * @return string cached file path
-     */
-    public static function create_from_string($ref, $string, $options = array()) {
-        $instance = self::get_instance($options);
-        $cachedfilepath = $instance->generate_filepath($ref);
-        $fp = fopen($cachedfilepath, 'w');
-        fwrite($fp, $string);
-        // Must close file handler.
-        fclose($fp);
-        return $cachedfilepath;
-    }
-
-    /**
-     * Build path to cache file
-     *
-     * @param mixed $ref
-     * @return string
-     */
-    private function generate_filepath($ref) {
-        global $CFG;
-        $hash = sha1(serialize($ref));
-        $l1 = $hash[0].$hash[1];
-        $l2 = $hash[2].$hash[3];
-        $dir = $this->cachedir . "/$l1/$l2";
-        if (!file_exists($dir)) {
-            mkdir($dir, $CFG->directorypermissions, true);
-        }
-        return "$dir/$hash";
-    }
-
-    /**
-     * Remove cache files
-     *
-     * @param array $options options
-     * @param int $expire The number of seconds before expiry
-     */
-    public static function cleanup($options = array(), $expire) {
-        global $CFG;
-        $instance = self::get_instance($options);
-        if ($dir = opendir($instance->cachedir)) {
-            while (($file = readdir($dir)) !== false) {
-                if (!is_dir($file) && $file != '.' && $file != '..') {
-                    $lasttime = @filemtime($instance->cachedir . $file);
-                    if(time() - $lasttime > $expire){
-                        @unlink($instance->cachedir . $file);
-                    }
-                }
-            }
-            closedir($dir);
-        }
-    }
-}
index 2b248d1..ae8df9c 100644 (file)
@@ -976,10 +976,6 @@ class file_storage {
             $filerecord->sortorder = 0;
         }
 
-        $filerecord->referencefileid   = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
-        $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
-        $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
-
         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
             // path must start and end with '/'
@@ -1094,9 +1090,6 @@ class file_storage {
         } else {
             $filerecord->sortorder = 0;
         }
-        $filerecord->referencefileid   = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
-        $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
-        $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
 
         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
@@ -1217,9 +1210,10 @@ class file_storage {
             $filerecord->sortorder = 0;
         }
 
-        $filerecord->referencefileid   = empty($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
-        $filerecord->referencelastsync = empty($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
-        $filerecord->referencelifetime = empty($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
+        // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
+        unset($filerecord->referencelastsync);
+        unset($filerecord->referencelifetime);
+
         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
@@ -1266,22 +1260,39 @@ class file_storage {
         $transaction = $DB->start_delegated_transaction();
 
         try {
-            $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference,
-                $filerecord->referencelastsync, $filerecord->referencelifetime);
+            $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
         } catch (Exception $e) {
             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
         }
 
-        // External file doesn't have content in moodle.
-        // So we create an empty file for it.
-        list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
+        if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
+            // there was specified the contenthash for a file already stored in moodle filepool
+            if (empty($filerecord->filesize)) {
+                $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
+                $filerecord->filesize = filesize($filepathname);
+            } else {
+                $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
+            }
+        } else {
+            // atempt to get the result of last synchronisation for this reference
+            $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
+                    'id, contenthash, filesize', IGNORE_MULTIPLE);
+            if ($lastcontent) {
+                $filerecord->contenthash = $lastcontent->contenthash;
+                $filerecord->filesize = $lastcontent->filesize;
+            } else {
+                // External file doesn't have content in moodle.
+                // So we create an empty file for it.
+                list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
+            }
+        }
 
         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
 
         try {
             $filerecord->id = $DB->insert_record('files', $filerecord);
         } catch (dml_exception $e) {
-            if ($newfile) {
+            if (!empty($newfile)) {
                 $this->deleted_file_cleanup($filerecord->contenthash);
             }
             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
@@ -1292,10 +1303,8 @@ class file_storage {
 
         $transaction->allow_commit();
 
-        // Adding repositoryid and reference to file record to create stored_file instance
-        $filerecord->repositoryid = $repositoryid;
-        $filerecord->reference = $reference;
-        return $this->get_file_instance($filerecord);
+        // this will retrieve all reference information from DB as well
+        return $this->get_file_by_id($filerecord->id);
     }
 
     /**
@@ -1805,18 +1814,51 @@ class file_storage {
         return $this->search_references_count(self::pack_reference($params));
     }
 
+    /**
+     * Updates all files that are referencing this file with the new contenthash
+     * and filesize
+     *
+     * @param stored_file $storedfile
+     */
+    public function update_references_to_storedfile(stored_file $storedfile) {
+        global $CFG;
+        $params = array();
+        $params['contextid'] = $storedfile->get_contextid();
+        $params['component'] = $storedfile->get_component();
+        $params['filearea']  = $storedfile->get_filearea();
+        $params['itemid']    = $storedfile->get_itemid();
+        $params['filename']  = $storedfile->get_filename();
+        $params['filepath']  = $storedfile->get_filepath();
+        $reference = self::pack_reference($params);
+        $referencehash = sha1($reference);
+
+        $sql = "SELECT repositoryid, id FROM {files_reference}
+                 WHERE referencehash = ? and reference = ?";
+        $rs = $DB->get_recordset_sql($sql, array($referencehash, $reference));
+
+        $now = time();
+        foreach ($rs as $record) {
+            require_once($CFG->dirroot.'/repository/lib.php');
+            $repo = repository::get_instance($record->repositoryid);
+            $lifetime = $repo->get_reference_file_lifetime($reference);
+            $this->update_references($record->id, $now, $lifetime,
+                    $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
+        }
+        $rs->close();
+    }
+
     /**
      * Convert file alias to local file
      *
+     * @throws moodle_exception if file could not be downloaded
+     *
      * @param stored_file $storedfile a stored_file instances
+     * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
      * @return stored_file stored_file
      */
-    public function import_external_file(stored_file $storedfile) {
+    public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
         global $CFG;
-        require_once($CFG->dirroot.'/repository/lib.php');
-        // sync external file
-        repository::sync_external_file($storedfile);
-        // Remove file references
+        $storedfile->import_external_file_contents($maxbytes);
         $storedfile->delete_reference();
         return $storedfile;
     }
@@ -1923,10 +1965,12 @@ class file_storage {
         // else problems like MDL-33172 occur.
         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
-            'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid',
-            'referencelastsync', 'referencelifetime');
+            'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
 
-        $referencefields = array('repositoryid', 'reference');
+        $referencefields = array('repositoryid' => 'repositoryid',
+            'reference' => 'reference',
+            'lastsync' => 'referencelastsync',
+            'lifetime' => 'referencelifetime');
 
         // id is specifically named to prevent overlaping between the two tables.
         $fields = array();
@@ -1935,8 +1979,8 @@ class file_storage {
             $fields[] = "{$filesprefix}.{$field}";
         }
 
-        foreach ($referencefields as $field) {
-            $fields[] = "{$filesreferenceprefix}.{$field}";
+        foreach ($referencefields as $field => $alias) {
+            $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
         }
 
         return implode(', ', $fields);
@@ -1997,4 +2041,40 @@ class file_storage {
         return $DB->get_field('files_reference', 'id',
             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
     }
+
+    /**
+     * Updates a reference to the external resource and all files that use it
+     *
+     * This function is called after synchronisation of an external file and updates the
+     * contenthash, filesize and status of all files that reference this external file
+     * as well as time last synchronised and sync lifetime (how long we don't need to call
+     * synchronisation for this reference).
+     *
+     * @param int $referencefileid
+     * @param int $lastsync
+     * @param int $lifetime
+     * @param string $contenthash
+     * @param int $filesize
+     * @param int $status 0 if ok or 666 if source is missing
+     */
+    public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
+        global $DB;
+        $referencefileid = clean_param($referencefileid, PARAM_INT);
+        $lastsync = clean_param($lastsync, PARAM_INT);
+        $lifetime = clean_param($lifetime, PARAM_INT);
+        validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
+        $filesize = clean_param($filesize, PARAM_INT);
+        $status = clean_param($status, PARAM_INT);
+        $params = array('contenthash' => $contenthash,
+                    'filesize' => $filesize,
+                    'status' => $status,
+                    'referencefileid' => $referencefileid,
+                    'lastsync' => $lastsync,
+                    'lifetime' => $lifetime);
+        $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
+            status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
+            WHERE referencefileid = :referencefileid', $params);
+        $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
+        $DB->update_record('files_reference', (object)$data);
+    }
 }
index e9410bd..fee7bfa 100644 (file)
@@ -70,6 +70,12 @@ class stored_file {
         } else {
             $this->repository = null;
         }
+        // make sure all reference fields exist in file_record even when it is not a reference
+        foreach (array('referencelastsync', 'referencelifetime', 'referencefileid', 'reference', 'repositoryid') as $key) {
+            if (empty($this->file_record->$key)) {
+                $this->file_record->$key = null;
+            }
+        }
     }
 
     /**
@@ -142,12 +148,18 @@ class stored_file {
                     }
                 }
 
-                if ($field === 'referencefileid' or $field === 'referencelastsync' or $field === 'referencelifetime') {
+                if ($field === 'referencefileid') {
                     if (!is_null($value) and !is_number($value)) {
                         throw new file_exception('storedfileproblem', 'Invalid reference info');
                     }
                 }
 
+                if ($field === 'referencelastsync' or $field === 'referencelifetime') {
+                    // do not update those fields
+                    // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
+                    continue;
+                }
+
                 // adding the field
                 $this->file_record->$field = $value;
             } else {
@@ -195,6 +207,7 @@ class stored_file {
     public function replace_content_with(stored_file $storedfile) {
         $contenthash = $storedfile->get_contenthash();
         $this->set_contenthash($contenthash);
+        $this->set_filesize($storedfile->get_filesize());
     }
 
     /**
@@ -225,8 +238,6 @@ class stored_file {
         // Update the underlying record in the database.
         $update = new stdClass();
         $update->referencefileid = null;
-        $update->referencelastsync = null;
-        $update->referencelifetime = null;
         $this->update($update);
 
         $transaction->allow_commit();
@@ -877,36 +888,43 @@ class stored_file {
      * We update contenthash, filesize and status in files table if changed
      * and we always update lastsync in files_reference table
      *
-     * @param type $contenthash
-     * @param type $filesize
+     * @param string $contenthash
+     * @param int $filesize
+     * @param int $status
+     * @param int $lifetime the life time of this synchronisation results
      */
-    public function set_synchronized($contenthash, $filesize, $status = 0) {
+    public function set_synchronized($contenthash, $filesize, $status = 0, $lifetime = null) {
         global $DB;
         if (!$this->is_external_file()) {
             return;
         }
         $now = time();
-        $filerecord = new stdClass();
-        if ($this->get_contenthash() !== $contenthash) {
-            $filerecord->contenthash = $contenthash;
-        }
-        if ($this->get_filesize() != $filesize) {
-            $filerecord->filesize = $filesize;
+        if ($contenthash != $this->file_record->contenthash) {
+            $oldcontenthash = $this->file_record->contenthash;
         }
-        if ($this->get_status() != $status) {
-            $filerecord->status = $status;
+        if ($lifetime === null) {
+            $lifetime = $this->file_record->referencelifetime;
         }
-        $filerecord->referencelastsync = $now; // TODO MDL-33416 remove this
-        if (!empty($filerecord)) {
-            $this->update($filerecord);
+        // this will update all entries in {files} that have the same filereference id
+        $this->fs->update_references($this->file_record->referencefileid, $now, $lifetime, $contenthash, $filesize, $status);
+        // we don't need to call update() for this object, just set the values of changed fields
+        $this->file_record->contenthash = $contenthash;
+        $this->file_record->filesize = $filesize;
+        $this->file_record->status = $status;
+        $this->file_record->referencelastsync = $now;
+        $this->file_record->referencelifetime = $lifetime;
+        if (isset($oldcontenthash)) {
+            $this->fs->deleted_file_cleanup($oldcontenthash);
         }
-
-        $DB->set_field('files_reference', 'lastsync', $now, array('id'=>$this->get_referencefileid()));
-        // $this->file_record->lastsync = $now; // TODO MDL-33416 uncomment or remove
     }
 
-    public function set_missingsource() {
-        $this->set_synchronized($this->get_contenthash(), 0, 666);
+    /**
+     * Sets the error status for a file that could not be synchronised
+     *
+     * @param int $lifetime the life time of this synchronisation results
+     */
+    public function set_missingsource($lifetime = null) {
+        $this->set_synchronized($this->get_contenthash(), $this->get_filesize(), 666, $lifetime);
     }
 
     /**
@@ -920,4 +938,16 @@ class stored_file {
     public function send_file($lifetime, $filter, $forcedownload, $options) {
         $this->repository->send_file($this, $lifetime, $filter, $forcedownload, $options);
     }
+
+    /**
+     * Imports the contents of an external file into moodle filepool.
+     *
+     * @throws moodle_exception if file could not be downloaded or is too big
+     * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
+     */
+    public function import_external_file_contents($maxbytes = 0) {
+        if ($this->repository) {
+            $this->repository->import_external_file_contents($this, $maxbytes);
+        }
+    }
 }
index fe2b933..c2d529f 100644 (file)
@@ -185,12 +185,21 @@ class google_docs {
      *
      * @param string $url url of file
      * @param string $path path to save file to
+     * @param int $timeout request timeout, default 0 which means no timeout
      * @return array stucture for repository download_file
      */
-    public function download_file($url, $path) {
-        $content = $this->googleoauth->get($url);
-        file_put_contents($path, $content);
-        return array('path'=>$path, 'url'=>$url);
+    public function download_file($url, $path, $timeout = 0) {
+        $result = $this->googleoauth->download_one($url, null, array('filepath' => $path, 'timeout' => $timeout));
+        if ($result === true) {
+            $info = $this->googleoauth->get_info();
+            if (isset($info['http_code']) && $info['http_code'] == 200) {
+                return array('path'=>$path, 'url'=>$url);
+            } else {
+                throw new moodle_exception('cannotdownload', 'repository');
+            }
+        } else {
+            throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
+        }
     }
 }
 
index db9b7f6..0739f76 100644 (file)
@@ -1196,6 +1196,10 @@ function get_fast_modinfo(&$course, $userid=0) {
         debugging('Coding problem - missing course modinfo property in get_fast_modinfo() call');
     }
 
+    if (!property_exists($course, 'sectioncache')) {
+        debugging('Coding problem - missing course sectioncache property in get_fast_modinfo() call');
+    }
+
     unset($cache[$course->id]); // prevent potential reference problems when switching users
 
     $cache[$course->id] = new course_modinfo($course, $userid);
index 55cf4e8..1c8f966 100644 (file)
@@ -3915,15 +3915,45 @@ function truncate_userinfo($info) {
  * Any plugin that needs to purge user data should register the 'user_deleted' event.
  *
  * @param stdClass $user full user object before delete
- * @return boolean always true
+ * @return boolean success
+ * @throws coding_exception if invalid $user parameter detected
  */
-function delete_user($user) {
+function delete_user(stdClass $user) {
     global $CFG, $DB;
     require_once($CFG->libdir.'/grouplib.php');
     require_once($CFG->libdir.'/gradelib.php');
     require_once($CFG->dirroot.'/message/lib.php');
     require_once($CFG->dirroot.'/tag/lib.php');
 
+    // Make sure nobody sends bogus record type as parameter.
+    if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
+        throw new coding_exception('Invalid $user parameter in delete_user() detected');
+    }
+
+    // Better not trust the parameter and fetch the latest info,
+    // this will be very expensive anyway.
+    if (!$user = $DB->get_record('user', array('id'=>$user->id))) {
+        debugging('Attempt to delete unknown user account.');
+        return false;
+    }
+
+    // There must be always exactly one guest record,
+    // originally the guest account was identified by username only,
+    // now we use $CFG->siteguest for performance reasons.
+    if ($user->username === 'guest' or isguestuser($user)) {
+        debugging('Guest user account can not be deleted.');
+        return false;
+    }
+
+    // Admin can be theoretically from different auth plugin,
+    // but we want to prevent deletion of internal accoutns only,
+    // if anything goes wrong ppl may force somebody to be admin via
+    // config.php setting $CFG->siteadmins.
+    if ($user->auth === 'manual' and is_siteadmin($user)) {
+        debugging('Local administrator accounts can not be deleted.');
+        return false;
+    }
+
     // delete all grades - backup is kept in grade_grades_history table
     grade_user_delete($user->id);
 
@@ -4767,7 +4797,7 @@ function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
     foreach ($fields as $field) {
         $updatesql = "UPDATE {".$modname."}
                           SET $field = $field + ?
-                        WHERE course=? AND $field<>0 AND $field<>0";
+                        WHERE course=? AND $field<>0";
         $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
     }
 
@@ -4961,12 +4991,12 @@ function reset_course_userdata($data) {
     if ($allmods = $DB->get_records('modules') ) {
         foreach ($allmods as $mod) {
             $modname = $mod->name;
-            if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
-                continue; // skip mods with no instances
-            }
             $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
             $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data
             if (file_exists($modfile)) {
+                if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
+                    continue; // Skip mods with no instances
+                }
                 include_once($modfile);
                 if (function_exists($moddeleteuserdata)) {
                     $modstatus = $moddeleteuserdata($data);
index 92f9029..84ca77f 100644 (file)
@@ -1131,7 +1131,13 @@ class global_navigation extends navigation_node {
                     $addedcategories[$category->id] = $categoryparent->add($category->name, $url, self::TYPE_CATEGORY, $category->name, $category->id);
 
                     if (!$category->visible) {
-                        if (!has_capability('moodle/category:viewhiddencategories', get_context_instance(CONTEXT_COURSECAT, $category->parent))) {
+                        // Let's decide the context where viewhidden cap checks will happen.
+                        if ($category->parent == '0') {
+                            $contexttocheck = context_system::instance();
+                        } else {
+                            $contexttocheck = context_coursecat::instance($category->parent);
+                        }
+                        if (!has_capability('moodle/category:viewhiddencategories', $contexttocheck)) {
                             $addedcategories[$category->id]->display = false;
                         } else {
                             $addedcategories[$category->id]->hidden = true;
index 587ba19..7f5ea64 100644 (file)
@@ -59,6 +59,8 @@ class oauth_helper {
     protected $access_token_api;
     /** @var curl */
     protected $http;
+    /** @var array options to pass to the next curl request */
+    protected $http_options;
 
     /**
      * Contructor for oauth_helper.
@@ -106,6 +108,7 @@ class oauth_helper {
             $this->access_token_secret = $args['access_token_secret'];
         }
         $this->http = new curl(array('debug'=>false));
+        $this->http_options = array();
     }
 
     /**
@@ -202,6 +205,15 @@ class oauth_helper {
         $this->http->setHeader($str);
     }
 
+    /**
+     * Sets the options for the next curl request
+     *
+     * @param array $options
+     */
+    public function setup_oauth_http_options($options) {
+        $this->http_options = $options;
+    }
+
     /**
      * Request token for authentication
      * This is the first step to use OAuth, it will return oauth_token and oauth_token_secret
@@ -210,7 +222,7 @@ class oauth_helper {
     public function request_token() {
         $this->sign_secret = $this->consumer_secret.'&';
         $params = $this->prepare_oauth_parameters($this->request_token_api, array(), 'GET');
-        $content = $this->http->get($this->request_token_api, $params);
+        $content = $this->http->get($this->request_token_api, $params, $this->http_options);
         // Including:
         //     oauth_token
         //     oauth_token_secret
@@ -252,7 +264,7 @@ class oauth_helper {
         $this->sign_secret = $this->consumer_secret.'&'.$secret;
         $params = $this->prepare_oauth_parameters($this->access_token_api, array('oauth_token'=>$token, 'oauth_verifier'=>$verifier), 'POST');
         $this->setup_oauth_http_header($params);
-        $content = $this->http->post($this->access_token_api, $params);
+        $content = $this->http->post($this->access_token_api, $params, $this->http_options);
         $keys = $this->parse_result($content);
         $this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']);
         return $keys;
@@ -274,9 +286,16 @@ class oauth_helper {
         }
         // to access protected resource, sign_secret will alwasy be consumer_secret+token_secret
         $this->sign_secret = $this->consumer_secret.'&'.$secret;
-        $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method);
+        if (strtolower($method) === 'post' && !empty($params)) {
+            $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token) + $params, $method);
+        } else {
+            $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method);
+        }
         $this->setup_oauth_http_header($oauth_params);
-        $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params));
+        $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params, $this->http_options));
+        // reset http header and options to prepare for the next request
+        $this->http->resetHeader();
+        // return request return value
         return $content;
     }
 
index 43de510..65e1c35 100644 (file)
@@ -1064,7 +1064,7 @@ class available_update_checker {
             return true;
         }
 
-        if ($now - $recent > HOURSECS) {
+        if ($now - $recent > 24 * HOURSECS) {
             return false;
         }
 
index db7be64..c8aeacd 100644 (file)
@@ -209,39 +209,44 @@ function get_grade_options() {
 }
 
 /**
- * match grade options
- * if no match return error or match nearest
+ * Check whether a given grade is one of a list of allowed options. If not,
+ * depending on $matchgrades, either return the nearest match, or return false
+ * to signal an error.
  * @param array $gradeoptionsfull list of valid options
  * @param int $grade grade to be tested
  * @param string $matchgrades 'error' or 'nearest'
- * @return mixed either 'fixed' value or false if erro
+ * @return mixed either 'fixed' value or false if error.
  */
-function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') {
+function match_grade_options($gradeoptionsfull, $grade, $matchgrades = 'error') {
+
     if ($matchgrades == 'error') {
-        // if we just need an error...
+        // (Almost) exact match, or an error.
         foreach ($gradeoptionsfull as $value => $option) {
-            // slightly fuzzy test, never check floats for equality :-)
+            // Slightly fuzzy test, never check floats for equality.
             if (abs($grade - $value) < 0.00001) {
-                return $grade;
+                return $value; // Be sure the return the proper value.
             }
         }
-        // didn't find a match so that's an error
+        // Didn't find a match so that's an error.
         return false;
+
     } else if ($matchgrades == 'nearest') {
-        // work out nearest value
-        $hownear = array();
+        // Work out nearest value
+        $best = false;
+        $bestmismatch = 2;
         foreach ($gradeoptionsfull as $value => $option) {
-            if ($grade==$value) {
-                return $grade;
+            $newmismatch = abs($grade - $value);
+            if ($newmismatch < $bestmismatch) {
+                $best = $value;
+                $bestmismatch = $newmismatch;
             }
-            $hownear[ $value ] = abs( $grade - $value );
         }
-        // reverse sort list of deltas and grab the last (smallest)
-        asort( $hownear, SORT_NUMERIC );
-        reset( $hownear );
-        return key( $hownear );
+        return $best;
+
     } else {
-        return false;
+        // Unknow option passed.
+        throw new coding_exception('Unknown $matchgrades ' . $matchgrades .
+                ' passed to match_grade_options');
     }
 }
 
@@ -780,14 +785,22 @@ function question_load_questions($questionids, $extrafields = '', $join = '') {
  */
 function _tidy_question($question, $loadtags = false) {
     global $CFG;
+
+    // Load question-type specific fields.
     if (!question_bank::is_qtype_installed($question->qtype)) {
         $question->questiontext = html_writer::tag('p', get_string('warningmissingtype',
                 'qtype_missingtype')) . $question->questiontext;
     }
     question_bank::get_qtype($question->qtype)->get_question_options($question);
+
+    // Convert numeric fields to float. (Prevents these being displayed as 1.0000000.)
+    $question->defaultmark += 0;
+    $question->penalty += 0;
+
     if (isset($question->_partiallyloaded)) {
         unset($question->_partiallyloaded);
     }
+
     if ($loadtags && !empty($CFG->usetags)) {
         require_once($CFG->dirroot . '/tag/lib.php');
         $question->tags = tag_get_tags_array('question', $question->id);
@@ -1104,16 +1117,18 @@ function question_category_options($contexts, $top = false, $currentcat = 0,
 
     // sort cats out into different contexts
     $categoriesarray = array();
-    foreach ($pcontexts as $pcontext) {
-        $contextstring = print_context_name(
-                get_context_instance_by_id($pcontext), true, true);
+    foreach ($pcontexts as $contextid) {
+        $context = context::instance_by_id($contextid);
+        $contextstring = $context->get_context_name(true, true);
         foreach ($categories as $category) {
-            if ($category->contextid == $pcontext) {
+            if ($category->contextid == $contextid) {
                 $cid = $category->id;
                 if ($currentcat != $cid || $currentcat == 0) {
                     $countstring = !empty($category->questioncount) ?
                             " ($category->questioncount)" : '';
-                    $categoriesarray[$contextstring][$cid] = $category->indentedname.$countstring;
+                    $categoriesarray[$contextstring][$cid] =
+                            format_string($category->indentedname, true,
+                                array('context' => $context)) . $countstring;
                 }
             }
         }
index 6e81cd7..5092831 100644 (file)
@@ -257,7 +257,7 @@ class completionlib_testcase extends basic_testcase {
             ->will($this->returnValue(true));
         $c->expects($this->at(1))
             ->method('get_data')
-            ->with($cm, 1337)
+            ->with($cm, false, 1337)
             ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
         $c->expects($this->at(2))
             ->method('internal_set_data')
index dd9665a..1abdf80 100644 (file)
@@ -1910,4 +1910,64 @@ class moodlelib_testcase extends advanced_testcase {
         $this->assertEquals('5.43000', format_float(5.43, 5, false));
         $this->assertEquals('5.43', format_float(5.43, 5, false, true));
     }
+
+    /**
+     * Test deleting of users.
+     */
+    public function test_delete_user() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest();
+
+        $guest = $DB->get_record('user', array('id'=>$CFG->siteguest), '*', MUST_EXIST);
+        $admin = $DB->get_record('user', array('id'=>$CFG->siteadmins), '*', MUST_EXIST);
+        $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
+
+        $user = $this->getDataGenerator()->create_user(array('idnumber'=>'abc'));
+        $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'xyz'));
+
+        $result = delete_user($user);
+        $this->assertTrue($result);
+        $deluser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
+        $this->assertEquals(1, $deluser->deleted);
+        $this->assertEquals(0, $deluser->picture);
+        $this->assertSame('', $deluser->idnumber);
+        $this->assertSame(md5($user->username), $deluser->email);
+        $this->assertRegExp('/^'.preg_quote($user->email, '/').'\.\d*$/', $deluser->username);
+
+        $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
+
+        // Try invalid params.
+
+        $record = new stdClass();
+        $record->grrr = 1;
+        try {
+            delete_user($record);
+            $this->fail('Expecting exception for invalid delete_user() $user parameter');
+        } catch (coding_exception $e) {
+            $this->assertTrue(true);
+        }
+        $record->id = 1;
+        try {
+            delete_user($record);
+            $this->fail('Expecting exception for invalid delete_user() $user parameter');
+        } catch (coding_exception $e) {
+            $this->assertTrue(true);
+        }
+
+        $CFG->debug = DEBUG_MINIMAL; // Prevent standard debug warnings.
+
+        $record = new stdClass();
+        $record->id = 666;
+        $record->username = 'xx';
+        $this->assertFalse($DB->record_exists('user', array('id'=>666))); // Any non-existent id is ok.
+        $result = delete_user($record);
+        $this->assertFalse($result);
+
+        $result = delete_user($guest);
+        $this->assertFalse($result);
+
+        $result = delete_user($admin);
+        $this->assertFalse($result);
+    }
 }
index eb29268..2ec4d0d 100644 (file)
@@ -103,7 +103,7 @@ class available_update_checker_test extends advanced_testcase {
      */
     public function test_cron_has_fresh_fetch() {
         $provider = testable_available_update_checker::instance();
-        $provider->fakerecentfetch = time() - 59 * MINSECS; // fetched an hour ago
+        $provider->fakerecentfetch = time() - 23 * HOURSECS; // fetched 23 hours ago
         $provider->fakecurrenttimestamp = -1;
         $provider->cron();
         $this->assertTrue(true); // we should get here with no exception thrown
@@ -127,23 +127,52 @@ class available_update_checker_test extends advanced_testcase {
      */
     public function test_cron_offset_execution_not_yet() {
         $provider = testable_available_update_checker::instance();
-        $provider->fakerecentfetch = time() - 24 * HOURSECS;
-        $provider->fakecurrenttimestamp = mktime(1, 40, 02); // 01:40:02 AM
+        $provider->fakecurrenttimestamp = mktime(1, 40, 02); // 01:40:02 AM today
+        $provider->fakerecentfetch = $provider->fakecurrenttimestamp - 24 * HOURSECS;
         $provider->cron();
         $this->assertTrue(true); // we should get here with no exception thrown
     }
 
     /**
-     * The first cron after 01:42 AM today should fetch the data
+     * The first cron after 01:42 AM today should fetch the data and then
+     * it is supposed to wait next 24 hours.
      *
      * @see testable_available_update_checker::cron_execution_offset()
      */
     public function test_cron_offset_execution() {
         $provider = testable_available_update_checker::instance();
-        $provider->fakerecentfetch = time() - 24 * HOURSECS;
-        $provider->fakecurrenttimestamp = mktime(1, 45, 02); // 01:45:02 AM
-        $this->setExpectedException('testable_available_update_checker_cron_executed');
-        $provider->cron();
+
+        // the cron at 01:45 should fetch the data
+        $provider->fakecurrenttimestamp = mktime(1, 45, 02); // 01:45:02 AM today
+        $provider->fakerecentfetch = $provider->fakecurrenttimestamp - 24 * HOURSECS - 1;
+        $executed = false;
+        try {
+            $provider->cron();
+        } catch (testable_available_update_checker_cron_executed $e) {
+            $executed = true;
+        }
+        $this->assertTrue($executed, 'Cron should be executed at 01:45:02 but it was not.');
+
+        // another cron at 06:45 should still consider data as fresh enough
+        $provider->fakerecentfetch = $provider->fakecurrenttimestamp;
+        $provider->fakecurrenttimestamp = mktime(6, 45, 03); // 06:45:03 AM
+        $executed = false;
+        try {
+            $provider->cron();
+        } catch (testable_available_update_checker_cron_executed $e) {
+            $executed = true;
+        }
+        $this->assertFalse($executed, 'Cron should not be executed at 06:45:03 but it was.');
+
+        // the next scheduled execution should happen the next day
+        $provider->fakecurrenttimestamp = $provider->fakerecentfetch + 24 * HOURSECS + 1;
+        $executed = false;
+        try {
+            $provider->cron();
+        } catch (testable_available_update_checker_cron_executed $e) {
+            $executed = true;
+        }
+        $this->assertTrue($executed, 'Cron should be executed the next night but it was not.');
     }
 
     public function test_compare_responses_both_empty() {
@@ -503,7 +532,7 @@ class testable_available_update_checker extends available_update_checker {
     }
 
     protected function cron_execute() {
-        throw new testable_available_update_checker_cron_executed('Cron executed but it should not!');
+        throw new testable_available_update_checker_cron_executed('Cron executed!');
     }
 }
 
index 2e5d431..d7d767d 100644 (file)
@@ -55,4 +55,19 @@ class questionlib_testcase extends basic_testcase {
             array(0 => 't1', 1 => 't2', 2 => 't3'));
     }
 
+    public function test_match_grade_options() {
+        $gradeoptions = question_bank::fraction_options_full();
+
+        $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.3333333, 'error'));
+        $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.333333, 'error'));
+        $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33333, 'error'));
+        $this->assertFalse(match_grade_options($gradeoptions, 0.3333, 'error'));
+
+        $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.3333333, 'nearest'));
+        $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.333333, 'nearest'));
+        $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33333, 'nearest'));
+        $this->assertEquals(0.3333333, match_grade_options($gradeoptions, 0.33, 'nearest'));
+
+        $this->assertEquals(-0.1428571, match_grade_options($gradeoptions, -0.15, 'nearest'));
+    }
 }
index 3f398a4..1847544 100644 (file)
@@ -204,16 +204,16 @@ if ($frm and isset($frm->username)) {                             // Login WITH
             // no wantsurl stored or external - go to homepage
             $urltogo = $CFG->wwwroot.'/';
             unset($SESSION->wantsurl);
-        }
 
-    /// Go to my-moodle page instead of site homepage if defaulthomepage set to homepage_my
-        if (!empty($CFG->defaulthomepage) && $CFG->defaulthomepage == HOMEPAGE_MY && !is_siteadmin() && !isguestuser()) {
-            if ($urltogo == $CFG->wwwroot or $urltogo == $CFG->wwwroot.'/' or $urltogo == $CFG->wwwroot.'/index.php') {
-                $urltogo = $CFG->wwwroot.'/my/';
+            $home_page = get_home_page();
+            // Go to my-moodle page instead of site homepage if defaulthomepage set to homepage_my
+            if ($home_page == HOMEPAGE_MY && !is_siteadmin() && !isguestuser()) {
+                if ($urltogo == $CFG->wwwroot or $urltogo == $CFG->wwwroot.'/' or $urltogo == $CFG->wwwroot.'/index.php') {
+                    $urltogo = $CFG->wwwroot.'/my/';
+                }
             }
         }
 
-
     /// check if user password has expired
     /// Currently supported only for ldap-authentication module
         $userauth = get_auth_plugin($USER->auth);
index c0b2cb4..be963fe 100644 (file)
@@ -180,7 +180,6 @@ $preferences->blocknoncontacts  =  get_user_preferences( 'message_blocknoncontac
 /// Display page header
 $streditmymessage = get_string('editmymessage', 'message');
 $strparticipants  = get_string('participants');
-$userfullname     = fullname($user, true);
 
 $PAGE->set_title("$course->shortname: $streditmymessage");
 if ($course->id != SITEID) {
index fb5e4ff..40bed66 100644 (file)
@@ -88,7 +88,7 @@ class assign_grading_table extends table_sql implements renderable {
         $params['assignmentid1'] = (int)$this->assignment->get_instance()->id;
         $params['assignmentid2'] = (int)$this->assignment->get_instance()->id;
 
-        $fields = user_picture::fields('u') . ', u.id as userid, u.firstname as firstname, u.lastname as lastname, ';
+        $fields = user_picture::fields('u') . ', u.id as userid, ';
         $fields .= 's.status as status, s.id as submissionid, s.timecreated as firstsubmission, s.timemodified as timesubmitted, ';
         $fields .= 'g.id as gradeid, g.grade as grade, g.timemodified as timemarked, g.timecreated as firstmarked, g.mailed as mailed, g.locked as locked';
         $from = '{user} u LEFT JOIN {assign_submission} s ON u.id = s.userid AND s.assignment = :assignmentid1' .
index 83afeb4..91c5f5f 100644 (file)
@@ -1150,7 +1150,7 @@ class assignment_upload extends assignment_base {
         require_once($CFG->libdir.'/filelib.php');
         $submissions = $this->get_submissions('','');
         if (empty($submissions)) {
-            print_error('errornosubmissions', 'assignment');
+            print_error('errornosubmissions', 'assignment', new moodle_url('/mod/assignment/submissions.php', array('id'=>$this->cm->id)));
         }
         $filesforzipping = array();
         $fs = get_file_storage();
@@ -1164,6 +1164,11 @@ class assignment_upload extends assignment_base {
         }
         $filename = str_replace(' ', '_', clean_filename($this->course->shortname.'-'.$this->assignment->name.'-'.$groupname.$this->assignment->id.".zip")); //name of new zip file.
         foreach ($submissions as $submission) {
+            // If assignment is open and submission is not finalized then don't add it to zip.
+            $submissionstatus = $this->is_finalized($submission);
+            if ($this->isopen() && empty($submissionstatus)) {
+                continue;
+            }
             $a_userid = $submission->userid; //get userid
             if ((groups_is_member($groupid,$a_userid)or !$groupmode or !$groupid)) {
                 $a_assignid = $submission->assignment; //get name of this assignment for use in the file names.
@@ -1180,6 +1185,12 @@ class assignment_upload extends assignment_base {
                 }
             }
         } // end of foreach loop
+
+        // Throw error if no files are added.
+        if (empty($filesforzipping)) {
+            print_error('errornosubmissions', 'assignment', new moodle_url('/mod/assignment/submissions.php', array('id'=>$this->cm->id)));
+        }
+
         if ($zipfile = assignment_pack_files($filesforzipping)) {
             send_temp_file($zipfile, $filename); //send file and delete after sending.
         }
index 75a56f8..337eb7f 100644 (file)
@@ -80,10 +80,9 @@ class restore_choice_activity_structure_step extends restore_activity_structure_
         global $DB;
 
         $data = (object)$data;
-        $oldid = $data->id;
 
         $data->choiceid = $this->get_new_parentid('choice');
-        $data->optionid = $this->get_mappingid('choice_option', $oldid);
+        $data->optionid = $this->get_mappingid('choice_option', $data->optionid);
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->timemodified = $this->apply_date_offset($data->timemodified);
 
index 6d8bef3..a881292 100644 (file)
@@ -9,6 +9,6 @@
     </tr>
     <tr>
         <td class="c0" valign="top"><label for="param1"><?php echo get_string('fieldoptions', 'data'); ?></label></td>
-        <td class="c1"><textarea style="width:300px; height:150px;" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
+        <td class="c1"><textarea class="optionstextarea" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
     </tr>
 </table>
index a6f6889..e941699 100644 (file)
@@ -130,7 +130,6 @@ class data_field_latlong extends data_field_base {
             } else {
                 $compasslong = sprintf('%01.4f', $long) . '°E';
             }
-            $str = '<form style="display:inline;">';
 
             // Now let's create the jump-to-services link
             $servicesshown = explode(',', $this->field->param1);
@@ -148,10 +147,11 @@ class data_field_latlong extends data_field_base {
             );
 
             if(sizeof($servicesshown)==1 && $servicesshown[0]) {
-                $str .= " <a href='"
+                $str = " <a href='"
                           . str_replace(array_keys($urlreplacements), array_values($urlreplacements), $this->linkoutservices[$servicesshown[0]])
                           ."' title='$servicesshown[0]'>$compasslat, $compasslong</a>";
             } elseif (sizeof($servicesshown)>1) {
+                $str = '<form id="latlongfieldbrowse">';
                 $str .= "$compasslat, $compasslong\n";
                 $str .= "<label class='accesshide' for='jumpto'>". get_string('jumpto') ."</label>";
                 $str .= "<select id='jumpto' name='jumpto'>";
@@ -164,10 +164,11 @@ class data_field_latlong extends data_field_base {
                 // NB! If you are editing this, make sure you don't break the javascript reference "previousSibling"
                 //   which allows the "Go" button to refer to the drop-down selector.
                 $str .= "\n</select><input type='button' value='" . get_string('go') . "' onclick='if(previousSibling.value){self.location=previousSibling.value}'/>";
+                $str .= '</form>';
             } else {
-                $str.= "$compasslat, $compasslong";
+                $str = "$compasslat, $compasslong";
             }
-            $str.= '</form>';
+
             return $str;
         }
         return false;
index 58fa709..f92fb13 100644 (file)
@@ -9,6 +9,6 @@
     </tr>
     <tr>
         <td class="c0" valign="top"><label for="param1"><?php echo get_string('fieldoptions', 'data'); ?></label></td>
-        <td class="c1"><textarea style="width:300px; height:150px;" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
+        <td class="c1"><textarea class="optionstextarea" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
     </tr>
 </table>
index 6d8bef3..a881292 100644 (file)
@@ -9,6 +9,6 @@
     </tr>
     <tr>
         <td class="c0" valign="top"><label for="param1"><?php echo get_string('fieldoptions', 'data'); ?></label></td>
-        <td class="c1"><textarea style="width:300px; height:150px;" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
+        <td class="c1"><textarea class="optionstextarea" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
     </tr>
 </table>
index 67d80f8..f569f53 100644 (file)
@@ -152,13 +152,13 @@ class data_field_picture extends data_field_base {
         if ($template == 'listtemplate') {
             $src = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$this->context->id.'/mod_data/content/'.$content->id.'/'.'thumb_'.$content->content);
             // no need to add width/height, because the thumb is resized properly
-            $str = '<a href="view.php?d='.$this->field->dataid.'&amp;rid='.$recordid.'"><img src="'.$src.'" alt="'.s($alt).'" title="'.s($title).'" style="border:0px" /></a>';
+            $str = '<a href="view.php?d='.$this->field->dataid.'&amp;rid='.$recordid.'"><img src="'.$src.'" alt="'.s($alt).'" title="'.s($title).'" class="list_picture"/></a>';
 
         } else {
             $src = file_encode_url($CFG->wwwroot.'/pluginfile.php', '/'.$this->context->id.'/mod_data/content/'.$content->id.'/'.$content->content);
             $width  = $this->field->param1 ? ' width="'.s($this->field->param1).'" ':' ';
             $height = $this->field->param2 ? ' height="'.s($this->field->param2).'" ':' ';
-            $str = '<a href="'.$src.'"><img '.$width.$height.' src="'.$src.'" alt="'.s($alt).'" title="'.s($title).'" style="border:0px" /></a>';
+            $str = '<a href="'.$src.'"><img '.$width.$height.' src="'.$src.'" alt="'.s($alt).'" title="'.s($title).'" class="list_picture"/></a>';
         }
 
         return $str;
index 4b32474..2ea0778 100644 (file)
         <td class="c0"><label for="param1">
             <?php echo get_string('fieldwidthsingleview', 'data');?></label></td>
         <td class="c1">
-            <input style="width:70px;" type="text" name="param1" id="param1" value="<?php if (!empty($this->field->param1)) p($this->field->param1); ?>" />
+            <input class="picturefieldsize" type="text" name="param1" id="param1" value="<?php if (!empty($this->field->param1)) p($this->field->param1); ?>" />
         </td>
     </tr>
     <tr>
         <td class="c0"><label for="param2">
             <?php echo get_string('fieldheightsingleview', 'data');?></label></td>
         <td class="c1">
-            <input style="width:70px;" type="text" name="param2" id="param2" value="<?php if (!empty($this->field->param2)) p($this->field->param2); ?>" />
+            <input class="picturefieldsize" type="text" name="param2" id="param2" value="<?php if (!empty($this->field->param2)) p($this->field->param2); ?>" />
         </td>
     </tr>
     <tr>
         <td class="c0"><label for="param4">
             <?php echo get_string('fieldwidthlistview', 'data');?></label></td>
-        <td class="c1"><input style="width:70px;" type="text" name="param4" id="param4" value="<?php if (!empty($this->field->param4)) p($this->field->param4); ?>" />
+        <td class="c1"><input class="picturefieldsize" type="text" name="param4" id="param4" value="<?php if (!empty($this->field->param4)) p($this->field->param4); ?>" />
         </td>
     </tr>
     <tr>
         <td class="c0"><label for="param5">
             <?php echo get_string('fieldheightlistview', 'data');?></label></td>
         <td class="c1">
-            <input style="width:70px;" type="text" name="param5" id="param5" value="<?php if (!empty($this->field->param5)) p($this->field->param5); ?>" />
+            <input class="picturefieldsize" type="text" name="param5" id="param5" value="<?php if (!empty($this->field->param5)) p($this->field->param5); ?>" />
         </td>
     </tr>
     <tr>
index 6d8bef3..a881292 100644 (file)
@@ -9,6 +9,6 @@
     </tr>
     <tr>
         <td class="c0" valign="top"><label for="param1"><?php echo get_string('fieldoptions', 'data'); ?></label></td>
-        <td class="c1"><textarea style="width:300px; height:150px;" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
+        <td class="c1"><textarea class="optionstextarea" name="param1" id="param1" cols="80" rows="10"><?php if($this->field->param1) {p($this->field->param1);} ?></textarea></td>
     </tr>
 </table>
index fb68424..e75365a 100644 (file)
@@ -14,7 +14,7 @@
         <td class="c0"><label for="param2">
             <?php echo get_string('fieldwidth', 'data'); ?></label></td>
         <td class="c1">
-            <input style="width:50px;" type="text" name="param2" id="param2" value=
+            <input class="textareafieldsize" type="text" name="param2" id="param2" value=
             <?php
                 if (empty($this->field->param2)) {
                     echo('"60"');
@@ -28,7 +28,7 @@
         <td class="c0"><label for="param3">
             <?php echo get_string('fieldheight', 'data'); ?></label></td>
         <td class="c1">
-            <input style="width:50px;" type="text" name="param3" id="param3" value=
+            <input class="textareafieldsize" type="text" name="param3" id="param3" value=
             <?php
                 if (empty($this->field->param3)) {
                     echo('"35"');
index 7b139af..fc26f78 100644 (file)
@@ -244,7 +244,7 @@ class data_field_base {     // Base class for Database Field Types (see field/*/
 
         $str = '<div title="'.s($this->field->description).'">';
         $str .= '<label class="accesshide" for="field_'.$this->field->id.'">'.$this->field->description.'</label>';
-        $str .= '<input style="width:300px;" type="text" name="field_'.$this->field->id.'" id="field_'.$this->field->id.'" value="'.s($content).'" />';
+        $str .= '<input class="basefieldinput" type="text" name="field_'.$this->field->id.'" id="field_'.$this->field->id.'" value="'.s($content).'" />';
         $str .= '</div>';
 
         return $str;
@@ -1480,14 +1480,16 @@ function data_print_preference_form($data, $perpage, $search, $sort='', $order='
     $pagesizes = array(2=>2,3=>3,4=>4,5=>5,6=>6,7=>7,8=>8,9=>9,10=>10,15=>15,
                        20=>20,30=>30,40=>40,50=>50,100=>100,200=>200,300=>300,400=>400,500=>500,1000=>1000);
     echo html_writer::select($pagesizes, 'perpage', $perpage, false, array('id'=>'pref_perpage'));
-    echo '<div id="reg_search" style="display: ';
+
     if ($advanced) {
-        echo 'none';
-    }
-    else {
-        echo 'inline';
+        $regsearchclass = 'search_none';
+        $advancedsearchclass = 'search_inline';
+    } else {
+        $regsearchclass = 'search_inline';
+        $advancedsearchclass = 'search_none';
     }
-    echo ';" >&nbsp;&nbsp;&nbsp;<label for="pref_search">'.get_string('search').'</label> <input type="text" size="16" name="search" id= "pref_search" value="'.s($search).'" /></div>';
+    echo '<div id="reg_search" class="' . $regsearchclass . '" >&nbsp;&nbsp;&nbsp;';
+    echo '<label for="pref_search">'.get_string('search').'</label> <input type="text" size="16" name="search" id= "pref_search" value="'.s($search).'" /></div>';
     echo '&nbsp;&nbsp;&nbsp;<label for="pref_sortby">'.get_string('sortby').'</label> ';
     // foreach field, print the option
     echo '<select name="sort" id="pref_sortby">';
@@ -1547,15 +1549,7 @@ function data_print_preference_form($data, $perpage, $search, $sort='', $order='
     echo '&nbsp;<input type="submit" value="'.get_string('savesettings','data').'" />';
 
     echo '<br />';
-    echo '<div class="dataadvancedsearch" id="data_adv_form" style="display: ';
-
-    if ($advanced) {
-        echo 'inline';
-    }
-    else {
-        echo 'none';
-    }
-    echo ';margin-left:auto;margin-right:auto;" >';
+    echo '<div class="' . $advancedsearchclass . '" id="data_adv_form">';
     echo '<table class="boxaligncenter">';
 
     // print ASC or DESC
@@ -1620,7 +1614,7 @@ function data_print_preference_form($data, $perpage, $search, $sort='', $order='
     echo format_text($newtext, FORMAT_HTML, $options);
     echo '</td></tr>';
 
-    echo '<tr><td colspan="4" style="text-align: center;"><br/><input type="submit" value="'.get_string('savesettings','data').'" /><input type="submit" name="resetadv" value="'.get_string('resetsettings','data').'" /></td></tr>';
+    echo '<tr><td colspan="4"><br/><input type="submit" value="'.get_string('savesettings','data').'" /><input type="submit" name="resetadv" value="'.get_string('resetsettings','data').'" /></td></tr>';
     echo '</table>';
     echo '</div>';
     echo '</div>';
index 4f68465..600c79d 100644 (file)
@@ -7,6 +7,24 @@
 .path-mod-data-field .c0,
 #page-mod-data-view #sortsearch .c0 {text-align: right;}
 #page-mod-data-view .approve img.icon {width:34px;height:34px;}
+#page-mod-data-view img.list_picture {
+    border:0px;
+}
+#page-mod-data-view div.search_none {
+    display: none;
+}
+#page-mod-data-view div.search_inline,
+#page-mod-data-view form#latlongfieldbrowse {
+    display: inline;
+}
+#page-mod-data-view div#data_adv_form {
+    margin-left:auto;
+    margin-right:auto;
+}
+
+#page-mod-data-edit .basefieldinput {
+    width:300px;
+}
 
 /** Styles for preset.php **/
 #page-mod-data-preset .presetmapping table {text-align: left;margin-left: auto;margin-right: auto;}
 .path-mod-data-field .sortdefault select {margin-left: 1em;}
 .path-mod-data-field .fieldname,
 .path-mod-data-field .fielddescription {width:300px;}
+.path-mod-data-field textarea.optionstextarea {
+    width:300px;
+    height:150px;
+}
+.path-mod-data-field input.textareafieldsize {
+    width:50px;
+}
+.path-mod-data-field input.picturefieldsize {
+    width:70px;
+}
 
 /** UI Usability Hacks **/
 #page-mod-data-export #notice span {padding:0 10px;}
 .mod-data-default-template .template-token {text-align:left;}
 .mod-data-default-template .controls {text-align:center;}
 .mod-data-default-template searchcontrols {text-align:right;}
+#page-mod-data-templates td.save_template,
+#page-mod-data-templates .template_heading {
+     text-align:center;
+}
 
 .dir-rtl .mod-data-default-template .template-field {text-align:left;}
 .dir-rtl .mod-data-default-template .template-token {text-align:right;}
index 404f28d..953fc89 100644 (file)
@@ -141,7 +141,7 @@ if (($mytemplate = data_submitted()) && confirm_sesskey()) {
         }
     }
 } else {
-    echo '<div class="littleintro" style="text-align:center">'.get_string('header'.$mode,'data').'</div>';
+    echo '<div class="template_heading">'.get_string('header'.$mode,'data').'</div>';
 }
 
 /// If everything is empty then generate some defaults
@@ -198,7 +198,7 @@ if ($mode == 'listtemplate'){
     echo '<tr>';
     echo '<td>&nbsp;</td>';
     echo '<td>';
-    echo '<div style="text-align:center"><label for="edit-listtemplateheader">'.get_string('header','data').'</label></div>';
+    echo '<div class="template_heading"><label for="edit-listtemplateheader">'.get_string('header','data').'</label></div>';
 
     $field = 'listtemplateheader';
     $editor->use_editor($field, $options);
@@ -290,9 +290,9 @@ echo '</td>';
 
 echo '<td valign="top">';
 if ($mode == 'listtemplate'){
-    echo '<div style="text-align:center"><label for="edit-template">'.get_string('multientry','data').'</label></div>';
+    echo '<div class="template_heading"><label for="edit-template">'.get_string('multientry','data').'</label></div>';
 } else {
-    echo '<div style="text-align:center"><label for="edit-template">'.get_string($mode,'data').'</label></div>';
+    echo '<div class="template_heading"><label for="edit-template">'.get_string($mode,'data').'</label></div>';
 }
 
 $field = 'template';
@@ -305,7 +305,7 @@ if ($mode == 'listtemplate'){
     echo '<tr>';
     echo '<td>&nbsp;</td>';
     echo '<td>';
-    echo '<div style="text-align:center"><label for="edit-listtemplatefooter">'.get_string('footer','data').'</label></div>';
+    echo '<div class="template_heading"><label for="edit-listtemplatefooter">'.get_string('footer','data').'</label></div>';
 
     $field = 'listtemplatefooter';
     $editor->use_editor($field, $options);
@@ -316,7 +316,7 @@ if ($mode == 'listtemplate'){
     echo '<tr>';
     echo '<td>&nbsp;</td>';
     echo '<td>';
-    echo '<div style="text-align:center"><label for="edit-rsstitletemplate">'.get_string('rsstitletemplate','data').'</label></div>';
+    echo '<div class="template_heading"><label for="edit-rsstitletemplate">'.get_string('rsstitletemplate','data').'</label></div>';
 
     $field = 'rsstitletemplate';
     $editor->use_editor($field, $options);
@@ -325,7 +325,7 @@ if ($mode == 'listtemplate'){
     echo '</tr>';
 }
 
-echo '<tr><td style="text-align:center" colspan="2">';
+echo '<tr><td class="save_template" colspan="2">';
 echo '<input type="submit" value="'.get_string('savetemplate','data').'" />&nbsp;';
 
 echo '</td></tr></table>';
index 4b7a841..c129b45 100644 (file)
@@ -609,7 +609,7 @@ class feedback_item_multichoice extends feedback_item_base {
         return 1;
     }
 
-    private function get_info($item) {
+    public function get_info($item) {
         $presentation = empty($item->presentation) ? '' : $item->presentation;
 
         $info = new stdClass();
index 00ee832..696afda 100644 (file)
@@ -465,7 +465,7 @@ class feedback_item_multichoicerated extends feedback_item_base {
         return 1;
     }
 
-    private function get_info($item) {
+    public function get_info($item) {
         $presentation = empty($item->presentation) ? '' : $item->presentation;
 
         $info = new stdClass();
index c35b24c..2e27645 100644 (file)
@@ -62,28 +62,26 @@ $string['delete_item'] = 'Delete question';
 $string['delete_old_items'] = 'Delete old items';
 $string['delete_template'] = 'Delete template';
 $string['delete_templates'] = 'Delete template...';
-$string['depending'] = 'depending items';
-$string['depending_help'] = 'Depending items allow you to show items depend on values from other items.<br />
-<strong>Here an build example to use it:</strong><br />
+$string['depending'] = 'Dependencies';
+$string['depending_help'] = 'It is possible to show an item depending on the value of another item.<br />
+<strong>Here is an example.</strong><br />
 <ul>
-<li>First create an item on which value other items depends.</li>
-<li>Next add a pagebreak.</li>
-<li>Next add the items depend on the item-value before<br />
-Choose in the item creation-form the item in the list "depend item" and put the needed value into the textbox "depend value".</li>
+<li>First, create an item on which another item will depend on.</li>
+<li>Next, add a pagebreak.</li>
+<li>Then add the items dependant on the value of the item created before. Choose the item from the list labelled "Dependence item" and write the required value in the textbox labelled "Dependence value".</li>
 </ul>
-<strong>The structure should looks like this:</strong>
+<strong>The item structure should look like this.</strong>
 <ol>
-<li>Item Q: do you have a car? A: yes/no</li>
+<li>Item Q: Do you have a car? A: yes/no</li>
 <li>Pagebreak</li>
-<li>Item Q: what color has your car?<br />
+<li>Item Q: What colour is your car?<br />
 (this item depends on item 1 with value = yes)</li>
-<li>Item Q: why you have not a car?<br />
+<li>Item Q: Why don\'t you have a car?<br />
 (this item depends on item 1 with value = no)</li>
 <li> ... other items</li>
-</ol>
-That is all. Have fun!';
-$string['dependitem'] = 'depend item';
-$string['dependvalue'] = 'depend value';
+</ol>';
+$string['dependitem'] = 'Dependence item';
+$string['dependvalue'] = 'Dependence value';
 $string['description'] = 'Description';
 $string['do_not_analyse_empty_submits'] = 'Do not analyse empty submits';
 $string['dropdown'] = 'Multiple choice - single answer allowed (dropdownlist)';
index 574bee1..95bb645 100644 (file)
@@ -384,6 +384,15 @@ class qformat_default {
                         $newpage->title = "Page $count";
                     }
                     $newpage->contents = $question->questiontext;
+                    $newpage->contentsformat = isset($question->questionformat) ? $question->questionformat : FORMAT_HTML;
+
+                    // Sometimes, questiontext is not a simple text, but one array
+                    // containing both text and format, so we need to support here
+                    // that case with the following dirty patch. MDL-35147
+                    if (is_array($question->questiontext)) {
+                        $newpage->contents = isset($question->questiontext['text']) ? $question->questiontext['text'] : '';
+                        $newpage->contentsformat = isset($question->questiontext['format']) ? $question->questiontext['format'] : FORMAT_HTML;
+                    }
 
                     // set up page links
                     if ($pageid) {
@@ -580,6 +589,93 @@ class qformat_default {
         return html_to_text(format_text($question->questiontext,
                 $question->questiontextformat, $formatoptions), 0, false);
     }
+
+    /**
+     * Since the lesson module tries to re-use the question bank import classes in
+     * a crazy way, this is necessary to stop things breaking.
+     */
+    protected function add_blank_combined_feedback($question) {
+        return $question;
+    }
 }
 
 
+/**
+ * Since the lesson module tries to re-use the question bank import classes in
+ * a crazy way, this is necessary to stop things breaking. This should be exactly
+ * the same as the class defined in question/format.php.
+ */
+class qformat_based_on_xml extends qformat_default {
+    /**
+     * A lot of imported files contain unwanted entities.
+     * This method tries to clean up all known problems.
+     * @param string str string to correct
+     * @return string the corrected string
+     */
+    public function cleaninput($str) {
+
+        $html_code_list = array(
+            "&#039;" => "'",
+            "&#8217;" => "'",
+            "&#8220;" => "\"",
+            "&#8221;" => "\"",
+            "&#8211;" => "-",
+            "&#8212;" => "-",
+        );
+        $str = strtr($str, $html_code_list);
+        // Use textlib entities_to_utf8 function to convert only numerical entities.
+        $str = textlib::entities_to_utf8($str, false);
+        return $str;
+    }
+
+    /**
+     * Return the array moodle is expecting
+     * for an HTML text. No processing is done on $text.
+     * qformat classes that want to process $text
+     * for instance to import external images files
+     * and recode urls in $text must overwrite this method.
+     * @param array $text some HTML text string
+     * @return array with keys text, format and files.
+     */
+    public function text_field($text) {
+        return array(
+            'text' => trim($text),
+            'format' => FORMAT_HTML,
+            'files' => array(),
+        );
+    }
+
+    /**
+     * Return the value of a node, given a path to the node
+     * if it doesn't exist return the default value.
+     * @param array xml data to read
+     * @param array path path to node expressed as array
+     * @param mixed default
+     * @param bool istext process as text
+     * @param string error if set value must exist, return false and issue message if not
+     * @return mixed value
+     */
+    public function getpath($xml, $path, $default, $istext=false, $error='') {
+        foreach ($path as $index) {
+            if (!isset($xml[$index])) {
+                if (!empty($error)) {
+                    $this->error($error);
+                    return false;
+                } else {
+                    return $default;
+                }
+            }
+
+            $xml = $xml[$index];
+        }
+
+        if ($istext) {
+            if (!is_string($xml)) {
+                $this->error(get_string('invalidxml', 'qformat_xml'));
+            }
+            $xml = trim($xml);
+        }
+
+        return $xml;
+    }
+}
index 993ca98..4bb7494 100644 (file)
@@ -1150,8 +1150,14 @@ function quiz_update_events($quiz, $override = null) {
         $addopen  = empty($current->id) || !empty($current->timeopen);
         $addclose = empty($current->id) || !empty($current->timeclose);
 
+        if (!empty($quiz->coursemodule)) {
+            $cmid = $quiz->coursemodule;
+        } else {
+            $cmid = get_coursemodule_from_instance('quiz', $quiz->id, $courseid)->id;
+        }
+
         $event = new stdClass();
-        $event->description = format_module_intro('quiz', $quiz, $quiz->coursemodule);
+        $event->description = format_module_intro('quiz', $quiz, $cmid);
         // Events module won't show user events when the courseid is nonzero.
         $event->courseid    = ($userid) ? 0 : $quiz->course;
         $event->groupid     = $groupid;
@@ -1341,8 +1347,18 @@ function quiz_reset_userdata($data) {
 
     // Updating dates - shift may be negative too.
     if ($data->timeshift) {
+        $DB->execute("UPDATE {quiz_overrides}
+                         SET timeopen = timeopen + ?
+                       WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?)
+                         AND timeopen <> 0", array($data->timeshift, $data->courseid));
+        $DB->execute("UPDATE {quiz_overrides}
+                         SET timeclose = timeclose + ?
+                       WHERE quiz IN (SELECT id FROM {quiz} WHERE course = ?)
+                         AND timeclose <> 0", array($data->timeshift, $data->courseid));
+
         shift_course_mod_dates('quiz', array('timeopen', 'timeclose'),
                 $data->timeshift, $data->courseid);
+
         $status[] = array(
             'component' => $componentstr,
             'item' => get_string('openclosedatesupdated', 'quiz'),
index c12e7ff..d1821ca 100644 (file)
@@ -3,9 +3,34 @@ This files describes API changes for quiz report plugins.
 Overview of this plugin type at http://docs.moodle.org/dev/Quiz_reports
 
 
-=== earlier versions ===
+=== 2.3 ===
 
-* ... API changes were not documented properly. Sorry. (There weren't many!)
+* Support for the old way of doing cron in a separate cron.php file has been removed.
+Instead, you need a lib.php file inside the plugin with a cron function
+called quiz_myreportname_cron(). The statistics report is an example of how
+it should be done.
+
+* There was a big refactor of the quiz reports, in issues MDL-32300, MDL-32322 and MDL-3030.
+It is difficult to explain the changes. Probably the best way to understand what
+happened is to look at
+    git log mod/quiz/report/overview
+    git log mod/quiz/report/responses
+and so on. Here are some notes on a few of the changes:
+
+The class quiz_attempt_report was renamed to quiz_attempts_report (with an extra s).
+
+Some globally defined constants with the prefix QUIZ_REPORT_ATTEMPTS_ moved into
+the quiz_attempts_report class. Specifically
+
+quiz_attempts_report::ALL_WITH         replaces QUIZ_REPORT_ATTEMPTS_ALL
+quiz_attempts_report::ENROLLED_ALL     replaces QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS
+quiz_attempts_report::ENROLLED_WITH    replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH
+quiz_attempts_report::ENROLLED_WITHOUT replaces QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO
+
+Your if you have a table class, it needs to be renamed like
+quiz_report_myreportname_table -> quiz_myreportname_table. That is, all the
+class names in your plugin should start with the frankenstyle plugin name
+quiz_myreportname.
 
 
 === 2.2 ===
@@ -17,6 +42,6 @@ This replaces the old way of having a separate cron.php file. Also, the cron
 frequency should be defined in version.php, not in the quiz_reports table.
 
 
-=== 2.3 ===
+=== earlier versions ===
 
-* Support for the old way of doing cron in a separate cron.php file has been removed.
+* ... API changes were not documented properly. Sorry. (There weren't many!)
index 6361024..d07442b 100644 (file)
@@ -178,6 +178,7 @@ table.quizreviewsummary td.cell {padding: 1px 1em 1px 0.5em;text-align: left;bac
 #page-mod-quiz-edit h2.main{display:inline;padding-right:1em;clear:left;}
 
 #categoryquestions .r1 {background: #e4e4e4;}
+#categoryquestions .r1.highlight {background-color:#AAFFAA;}
 #categoryquestions .header {text-align: center;padding: 0 2px;border: 0 none;}
 #categoryquestions th.modifiername .sorters,
 #categoryquestions th.creatorname .sorters {font-weight: normal;font-size: 0.8em;}
index 5d557a4..2e9a0c0 100644 (file)
@@ -32,8 +32,13 @@ class moodle1_workshopform_comments_handler extends moodle1_workshopform_handler
 
     /**
      * Converts <ELEMENT> into <workshopform_comments_dimension>
+     *
+     * @param array $data legacy element data
+     * @param array $raw raw element data
+     *
+     * @return array converted
      */
-    public function process_legacy_element($data, $raw) {
+    public function process_legacy_element(array $data, array $raw) {
         // prepare a fake record and re-use the upgrade logic
         $fakerecord = (object)$data;
         $converted = (array)workshopform_comments_upgrade_element($fakerecord, 12345678);
index d2c6310..e18a2f2 100644 (file)
@@ -49,9 +49,12 @@ class moodle1_workshopform_numerrors_handler extends moodle1_workshopform_handle
     /**
      * Converts <ELEMENT> into <workshopform_numerrors_dimension> and stores it for later writing
      *
+     * @param array $data legacy element data
+     * @param array $raw raw element data
+     *
      * @return array to be written to workshop.xml
      */
-    public function process_legacy_element($data, $raw) {
+    public function process_legacy_element(array $data, array $raw) {
 
         $workshop = $this->parenthandler->get_current_workshop();
 
index be0a6c6..fe604f8 100644 (file)
@@ -46,8 +46,11 @@ class moodle1_workshopform_rubric_handler extends moodle1_workshopform_handler {
 
     /**
      * Processes one <ELEMENT>
+     *
+     * @param array $data legacy element data
+     * @param array $raw raw element data
      */
-    public function process_legacy_element($data, $raw) {
+    public function process_legacy_element(array $data, array $raw) {
         $this->elements[] = $data;
         $this->rubrics[$data['id']] = array();
     }
index fd65dba..8103e96 100644 (file)
@@ -102,9 +102,12 @@ class question_category_list_item extends list_item {
         /// Each section adds html to be displayed as part of this list item
         $questionbankurl = new moodle_url("/question/edit.php", ($this->parentlist->pageurl->params() + array('category'=>"$category->id,$category->contextid")));
         $catediturl = $this->parentlist->pageurl->out(true, array('edit' => $this->id));
-        $item = "<b><a title=\"{$str->edit}\" href=\"$catediturl\">".$category->name ."</a></b> <a title=\"$editqestions\" href=\"$questionbankurl\">".'('.$category->questioncount.')</a>';
+        $item = "<b><a title=\"{$str->edit}\" href=\"$catediturl\">" .
+                format_string($category->name, true, array('context' => $this->parentlist->context)) .
+                "</a></b> <a title=\"$editqestions\" href=\"$questionbankurl\">".'('.$category->questioncount.')</a>';
 
-        $item .= '&nbsp;'. $category->info;
+        $item .= '&nbsp;' . format_text($category->info, $category->infoformat,
+                array('context' => $this->parentlist->context, 'noclean' => true));
 
         // don't allow delete if this is the last category in this context.
         if (count($this->parentlist->records) != 1) {
index a0e9df4..dd02cfc 100644 (file)
@@ -408,20 +408,44 @@ class qformat_default {
             $question->timecreated = time();
             $question->modifiedby = $USER->id;
             $question->timemodified = time();
+            $fileoptions = array(
+                    'subdirs' => false,
+                    'maxfiles' => -1,
+                    'maxbytes' => 0,
+                );
+            if (is_array($question->questiontext)) {
+                // Importing images from draftfile.
+                $questiontext = $question->questiontext;
+                $question->questiontext = $questiontext['text'];
+            }
+            if (is_array($question->generalfeedback)) {
+                $generalfeedback = $question->generalfeedback;
+                $question->generalfeedback = $generalfeedback['text'];
+            }
 
             $question->id = $DB->insert_record('question', $question);
-            if (isset($question->questiontextfiles)) {
+
+            if (!empty($questiontext['itemid'])) {
+                $question->questiontext = file_save_draft_area_files($questiontext['itemid'],
+                        $this->importcontext->id, 'question', 'questiontext', $question->id,
+                        $fileoptions, $question->questiontext);
+            } else if (isset($question->questiontextfiles)) {
                 foreach ($question->questiontextfiles as $file) {
                     question_bank::get_qtype($question->qtype)->import_file(
                             $this->importcontext, 'question', 'questiontext', $question->id, $file);
                 }
             }
-            if (isset($question->generalfeedbackfiles)) {
+            if (!empty($generalfeedback['itemid'])) {
+                $question->generalfeedback = file_save_draft_area_files($generalfeedback['itemid'],
+                        $this->importcontext->id, 'question', 'generalfeedback', $question->id,
+                        $fileoptions, $question->generalfeedback);
+            } else if (isset($question->generalfeedbackfiles)) {
                 foreach ($question->generalfeedbackfiles as $file) {
                     question_bank::get_qtype($question->qtype)->import_file(
                             $this->importcontext, 'question', 'generalfeedback', $question->id, $file);
                 }
             }
+            $DB->update_record('question', $question);
 
             $this->questionids[] = $question->id;
 
@@ -636,6 +660,24 @@ class qformat_default {
         return $question;
     }
 
+    /**
+     * Add a blank combined feedback to a question object.
+     * @param object question
+     * @return object question
+     */
+    protected function add_blank_combined_feedback($question) {
+        $question->correctfeedback['text'] = '';
+        $question->correctfeedback['format'] = $question->questiontextformat;
+        $question->correctfeedback['files'] = array();
+        $question->partiallycorrectfeedback['text'] = '';
+        $question->partiallycorrectfeedback['format'] = $question->questiontextformat;
+        $question->partiallycorrectfeedback['files'] = array();
+        $question->incorrectfeedback['text'] = '';
+        $question->incorrectfeedback['format'] = $question->questiontextformat;
+        $question->incorrectfeedback['files'] = array();
+        return $question;
+    }
+
     /**
      * Given the data known to define a question in
      * this format, this function converts it into a question
@@ -901,6 +943,28 @@ class qformat_default {
 
 class qformat_based_on_xml extends qformat_default {
 
+    /**
+     * A lot of imported files contain unwanted entities.
+     * This method tries to clean up all known problems.
+     * @param string str string to correct
+     * @return string the corrected string
+     */
+    public function cleaninput($str) {
+
+        $html_code_list = array(
+            "&#039;" => "'",
+            "&#8217;" => "'",
+            "&#8220;" => "\"",
+            "&#8221;" => "\"",
+            "&#8211;" => "-",
+            "&#8212;" => "-",
+        );
+        $str = strtr($str, $html_code_list);
+        // Use textlib entities_to_utf8 function to convert only numerical entities.
+        $str = textlib::entities_to_utf8($str, false);
+        return $str;
+    }
+
     /**
      * Return the array moodle is expecting
      * for an HTML text. No processing is done on $text.
index 88e0130..1e7fa0d 100644 (file)
@@ -59,33 +59,6 @@ class qformat_blackboard extends qformat_based_on_xml {
         return mimeinfo('type', '.dat');
     }
 
-    /**
-     * Some softwares put entities in exported files.
-     * This method try to clean up known problems.
-     * @param string str string to correct
-     * @return string the corrected string
-     */
-    public function cleaninput($str) {
-        if (!$this->ishtml) {
-            return $str;
-        }
-        $html_code_list = array(
-            "&#039;" => "'",
-            "&#8217;" => "'",
-            "&#091;" => "[",
-            "&#8220;" => "\"",
-            "&#8221;" => "\"",
-            "&#093;" => "]",
-            "&#039;" => "'",
-            "&#8211;" => "-",
-            "&#8212;" => "-",
-        );
-        $str = strtr($str, $html_code_list);
-        // Use textlib entities_to_utf8 function to convert only numerical entities.
-        $str = textlib::entities_to_utf8($str, false);
-        return $str;
-    }
-
     /**
      * Parse the array of lines into an array of questions
      * this *could* burn memory - but it won't happen that much
index 047bfa9..32652d1 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Blackboard 6.0 question importer.
+ * Blackboard V5 and V6 question importer.
  *
- * @package    qformat
- * @subpackage blackboard_six
+ * @package    qformat_blackboard_six
  * @copyright  2005 Michael Penney
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
 defined('MOODLE_INTERNAL') || die();
 
-require_once ($CFG->libdir . '/xmlize.php');
-
-
-/**
- * Blackboard 6.0 question importer.
- *
- * @copyright  2005 Michael Penney
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class qformat_blackboard_six extends qformat_default {
-    function provide_import() {
-        return true;
-    }
-
-    public function can_import_file($file) {
-        $mimetypes = array(
-            mimeinfo('type', '.dat'),
-            mimeinfo('type', '.zip')
-        );
-        return in_array($file->get_mimetype(), $mimetypes);
-    }
-
-
-    //Function to check and create the needed dir to unzip file to
-    function check_and_create_import_dir($unique_code) {
-
-        global $CFG;
-
-        $status = $this->check_dir_exists($CFG->tempdir."",true);
-        if ($status) {
-            $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import",true);
-        }
-        if ($status) {
-            $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import/".$unique_code,true);
-        }
-
-        return $status;
-    }
-
-    function clean_temp_dir($dir='') {
+require_once($CFG->libdir . '/xmlize.php');
+require_once($CFG->dirroot . '/question/format/blackboard_six/formatbase.php');
+require_once($CFG->dirroot . '/question/format/blackboard_six/formatqti.php');
+require_once($CFG->dirroot . '/question/format/blackboard_six/formatpool.php');
+
+class qformat_blackboard_six extends qformat_blackboard_six_base {
+    /** @var int Blackboard assessment qti files were always imported by the blackboard_six plugin. */
+    const FILETYPE_QTI = 1;
+    /** @var int Blackboard question pool files were previously handled by the blackboard plugin. */
+    const FILETYPE_POOL = 2;
+    /** @var int type of file being imported, one of the constants FILETYPE_QTI or FILETYPE_POOL. */
+    public $filetype;
+
+    public function get_filecontent($path) {
+        $fullpath = $this->tempdir . '/' . $path;
+        if (is_file($fullpath) && is_readable($fullpath)) {
+            return file_get_contents($fullpath);
+        }
+        return false;
+    }
+
+    /**
+     * Set the file type being imported
+     * @param int $type the imported file's type
+     */
+    public function set_filetype($type) {
+        $this->filetype = $type;
+    }
+
+    /**
+     * Return content of all files containing questions,
+     * as an array one element for each file found,
+     * For each file, the corresponding element is an array of lines.
+     * @param string filename name of file
+     * @return mixed contents array or false on failure
+     */
+    public function readdata($filename) {
         global $CFG;
 
-        // for now we will just say everything happened okay note
-        // that a mess may be piling up in $CFG->tempdir/bbquiz_import
-        // TODO return true at top of the function renders all the following code useless
-        return true;
-
-        if ($dir == '') {
-            $dir = $this->temp_dir;
-        }
-        $slash = "/";
-
-        // Create arrays to store files and directories
-        $dir_files      = array();
-        $dir_subdirs    = array();
-
-        // Make sure we can delete it
-        chmod($dir, $CFG->directorypermissions);
-
-        if ((($handle = opendir($dir))) == FALSE) {
-            // The directory could not be opened
-            return false;
-        }
-
-        // Loop through all directory entries, and construct two temporary arrays containing files and sub directories
-        while(false !== ($entry = readdir($handle))) {
-            if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != ".") {
-                $dir_subdirs[] = $dir. $slash .$entry;
-            }
-            else if ($entry != ".." && $entry != ".") {
-                $dir_files[] = $dir. $slash .$entry;
-            }
-        }
-
-        // Delete all files in the curent directory return false and halt if a file cannot be removed
-        $countdir_files = count($dir_files);
-        for($i=0; $i<$countdir_files; $i++) {
-            chmod($dir_files[$i], $CFG->directorypermissions);
-            if (((unlink($dir_files[$i]))) == FALSE) {
+        // Find if we are importing a .dat file.
+        if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'dat') {
+            if (!is_readable($filename)) {
+                $this->error(get_string('filenotreadable', 'error'));
                 return false;
             }
-        }
-
-        // Empty sub directories and then remove the directory
-        $countdir_subdirs = count($dir_subdirs);
-        for($i=0; $i<$countdir_subdirs; $i++) {
-            chmod($dir_subdirs[$i], $CFG->directorypermissions);
-            if ($this->clean_temp_dir($dir_subdirs[$i]) == FALSE) {
-                return false;
+            // As we are not importing a .zip file,
+            // there is no imsmanifest, and it is not possible
+            // to parse it to find the file type.
+            // So we need to guess the file type by looking at the content.
+            // For now we will do that searching for a required tag.
+            // This is certainly not bullet-proof but works for all usual files.
+            $text = file_get_contents($filename);
+            if (strpos($text, '<questestinterop>')) {
+                $this->set_filetype(self::FILETYPE_QTI);
             }
-            else {
-                if (rmdir($dir_subdirs[$i]) == FALSE) {
-                return false;
-                }
+            if (strpos($text, '<POOL>')) {
+                $this->set_filetype(self::FILETYPE_POOL);
             }
-        }
+            // In all other cases we are not able to handle this question file.
 
-        // Close directory
-        closedir($handle);
-        if (rmdir($this->temp_dir) == FALSE) {
-            return false;
+            // Readquestions is now expecting an array of strings.
+            return array($text);
         }
-        // Success, every thing is gone return true
-        return true;
-    }
-
-    //Function to check if a directory exists and, optionally, create it
-    function check_dir_exists($dir,$create=false) {
-
-        global $CFG;
-
-        $status = true;
-        if(!is_dir($dir)) {
-            if (!$create) {
-                $status = false;
-            } else {
-                umask(0000);
-                $status = mkdir ($dir,$CFG->directorypermissions);
+        // We are importing a zip file.
+        // Create name for temporary directory.
+        $unique_code = time();
+        $this->tempdir = make_temp_directory('bbquiz_import/' . $unique_code);
+        if (is_readable($filename)) {
+            if (!copy($filename, $this->tempdir . '/bboard.zip')) {
+                $this->error(get_string('cannotcopybackup', 'question'));
+                fulldelete($this->tempdir);
+                return false;
             }
-        }
-        return $status;
-    }
+            if (unzip_file($this->tempdir . '/bboard.zip', '', false)) {
+                $dom = new DomDocument();
 
-    function importpostprocess() {
-    /// Does any post-processing that may be desired
-    /// Argument is a simple array of question ids that
-    /// have just been added.
-
-        // need to clean up temporary directory
-        return $this->clean_temp_dir();
-    }
-
-    function copy_file_to_course($filename) {
-        global $CFG, $COURSE;
-        $filename = str_replace('\\','/',$filename);
-        $fullpath = $this->temp_dir.'/res00001/'.$filename;
-        $basename = basename($filename);
-
-        $copy_to = $CFG->dataroot.'/'.$COURSE->id.'/bb_import';
-
-        if ($this->check_dir_exists($copy_to,true)) {
-            if(is_readable($fullpath)) {
-                $copy_to.= '/'.$basename;
-                if (!copy($fullpath, $copy_to)) {
+                if (!$dom->load($this->tempdir . '/imsmanifest.xml')) {
+                    $this->error(get_string('errormanifest', 'qformat_blackboard_six'));
+                    fulldelete($this->tempdir);
                     return false;
                 }
-                else {
-                    return $copy_to;
-                }
-            }
-        }
-        else {
-            return false;
-        }
-    }
 
-    function readdata($filename) {
-    /// Returns complete file with an array, one item per line
-        global $CFG;
+                $xpath = new DOMXPath($dom);
 
-        // if the extension is .dat we just return that,
-        // if .zip we unzip the file and get the data
-        $ext = substr($this->realfilename, strpos($this->realfilename,'.'), strlen($this->realfilename)-1);
-        if ($ext=='.dat') {
-            if (!is_readable($filename)) {
-                print_error('filenotreadable', 'error');
-            }
-            return file($filename);
-        }
+                // We starts from the root element.
+                $query = '//resources/resource';
+                $this->filebase = $this->tempdir;
+                $q_file = array();
 
-        $unique_code = time();
-        $temp_dir = $CFG->tempdir."/bbquiz_import/".$unique_code;
-        $this->temp_dir = $temp_dir;
-        if ($this->check_and_create_import_dir($unique_code)) {
-            if(is_readable($filename)) {
-                if (!copy($filename, "$temp_dir/bboard.zip")) {
-                    print_error('cannotcopybackup', 'question');
-                }
-                if(unzip_file("$temp_dir/bboard.zip", '', false)) {
-                    // assuming that the information is in res0001.dat
-                    // after looking at 6 examples this was always the case
-                    $q_file = "$temp_dir/res00001.dat";
-                    if (is_file($q_file)) {
-                        if (is_readable($q_file)) {
-                            $filearray = file($q_file);
-                            /// Check for Macintosh OS line returns (ie file on one line), and fix
-                            if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) {
-                                return explode("\r", $filearray[0]);
-                            } else {
-                                return $filearray;
-                            }
+                $examfiles = $xpath->query($query);
+                foreach ($examfiles as $examfile) {
+                    if ($examfile->getAttribute('type') == 'assessment/x-bb-qti-test'
+                            || $examfile->getAttribute('type') == 'assessment/x-bb-qti-pool') {
+
+                        if ($content = $this->get_filecontent($examfile->getAttribute('bb:file'))) {
+                            $this->set_filetype(self::FILETYPE_QTI);
+                            $q_file[] = $content;
                         }
                     }
-                    else {
-                        print_error('cannotfindquestionfile', 'questioni');
-                    }
-                }
-                else {
-                    print "filename: $filename<br />tempdir: $temp_dir <br />";
-                    print_error('cannotunzip', 'question');
-                }
-            }
-            else {
-                print_error('cannotreaduploadfile');
-            }
-        }
-        else {
-            print_error('cannotcreatetempdir');
-        }
-    }
-
-    function save_question_options($question) {
-        return true;
-    }
-
-
-
-  protected function readquestions($lines) {
-    /// Parses an array of lines into an array of questions,
-    /// where each item is a question object as defined by
-    /// readquestion().
-
-    $text = implode($lines, " ");
-    $xml = xmlize($text, 0);
-
-    $raw_questions = $xml['questestinterop']['#']['assessment'][0]['#']['section'][0]['#']['item'];
-    $questions = array();
-
-    foreach($raw_questions as $quest) {
-        $question = $this->create_raw_question($quest);
-
-        switch($question->qtype) {
-            case "Matching":
-                $this->process_matching($question, $questions);
-                break;
-            case "Multiple Choice":
-                $this->process_mc($question, $questions);
-                break;
-            case "Essay":
-                $this->process_essay($question, $questions);
-                break;
-            case "Multiple Answer":
-                $this->process_ma($question, $questions);
-                break;
-            case "True/False":
-                $this->process_tf($question, $questions);
-                break;
-            case 'Fill in the Blank':
-                $this->process_fblank($question, $questions);
-                break;
-            case 'Short Response':
-                $this->process_essay($question, $questions);
-                break;
-            default:
-                print "Unknown or unhandled question type: \"$question->qtype\"<br />";
-                break;
-        }
-
-    }
-    return $questions;
-  }
-
-
-// creates a cleaner object to deal with for processing into moodle
-// the object created is NOT a moodle question object
-function create_raw_question($quest) {
-
-    $question = new stdClass();
-    $question->qtype = $quest['#']['itemmetadata'][0]['#']['bbmd_questiontype'][0]['#'];
-    $question->id = $quest['#']['itemmetadata'][0]['#']['bbmd_asi_object_id'][0]['#'];
-    $presentation->blocks = $quest['#']['presentation'][0]['#']['flow'][0]['#']['flow'];
-
-    foreach($presentation->blocks as $pblock) {
-
-        $block = NULL;
-        $block->type = $pblock['@']['class'];
-
-        switch($block->type) {
-            case 'QUESTION_BLOCK':
-                $sub_blocks = $pblock['#']['flow'];
-                foreach($sub_blocks as $sblock) {
-                    //echo "Calling process_block from line 263<br>";
-                    $this->process_block($sblock, $block);
-                }
-                break;
-
-            case 'RESPONSE_BLOCK':
-                $choices = NULL;
-                switch($question->qtype) {
-                    case 'Matching':
-                        $bb_subquestions = $pblock['#']['flow'];
-                        $sub_questions = array();
-                        foreach($bb_subquestions as $bb_subquestion) {
-                            $sub_question = NULL;
-                            $sub_question->ident = $bb_subquestion['#']['response_lid'][0]['@']['ident'];
-                            $this->process_block($bb_subquestion['#']['flow'][0], $sub_question);
-                            $bb_choices = $bb_subquestion['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label'];
-                            $choices = array();
-                            $this->process_choices($bb_choices, $choices);
-                            $sub_question->choices = $choices;
-                            if (!isset($block->subquestions)) {
-                                $block->subquestions = array();
-                            }
-                            $block->subquestions[] = $sub_question;
+                    if ($examfile->getAttribute('type') == 'assessment/x-bb-pool') {
+                        if ($examfile->getAttribute('baseurl')) {
+                            $this->filebase = $this->tempdir. '/' . $examfile->getAttribute('baseurl');
                         }
-                        break;
-                    case 'Multiple Answer':
-                        $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'];
-                        $choices = array();
-                        $this->process_choices($bb_choices, $choices);
-                        $block->choices = $choices;
-                        break;
-                    case 'Essay':
-                        // Doesn't apply since the user responds with text input
-                        break;
-                    case 'Multiple Choice':
-                        $mc_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'];
-                            foreach($mc_choices as $mc_choice) {
-                            $choices = NULL;
-                            $choices = $this->process_block($mc_choice, $choices);
-                            $block->choices[] = $choices;
+                        if ($content = $this->get_filecontent($examfile->getAttribute('file'))) {
+                            $this->set_filetype(self::FILETYPE_POOL);
+                            $q_file[] = $content;
                         }
-                        break;
-                    case 'Short Response':
-                        // do nothing?
-                        break;
-                    case 'Fill in the Blank':
-                        // do nothing?
-                        break;
-                    default:
-                        $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label'];
-                        $choices = array();
-                        $this->process_choices($bb_choices, $choices);
-                        $block->choices = $choices;
-                }
-                break;
-            case 'RIGHT_MATCH_BLOCK':
-                $matching_answerset = $pblock['#']['flow'];
-
-                $answerset = array();
-                foreach($matching_answerset as $answer) {
-                    // $answerset[] = $this->process_block($answer, $bb_answer);
-                    $bb_answer = null;
-                    $bb_answer->text = $answer['#']['flow'][0]['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'];
-                    $answerset[] = $bb_answer;
-                }
-                $block->matching_answerset = $answerset;
-                break;
-            default:
-                print "UNHANDLED PRESENTATION BLOCK";
-                break;
-        }
-        $question->{$block->type} = $block;
-    }
-
-    // determine response processing
-    // there is a section called 'outcomes' that I don't know what to do with
-    $resprocessing = $quest['#']['resprocessing'];
-    $respconditions = $resprocessing[0]['#']['respcondition'];
-    $reponses = array();
-    if ($question->qtype == 'Matching') {
-        $this->process_matching_responses($respconditions, $responses);
-    }
-    else {
-        $this->process_responses($respconditions, $responses);
-    }
-    $question->responses = $responses;
-    $feedbackset = $quest['#']['itemfeedback'];
-    $feedbacks = array();
-    $this->process_feedback($feedbackset, $feedbacks);
-    $question->feedback = $feedbacks;
-    return $question;
-}
-
-function process_block($cur_block, &$block) {
-    global $COURSE, $CFG;
-
-    $cur_type = $cur_block['@']['class'];
-    switch($cur_type) {
-        case 'FORMATTED_TEXT_BLOCK':
-            $block->text = $this->strip_applet_tags_get_mathml($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']);
-            break;
-        case 'FILE_BLOCK':
-            //revisit this to make sure it is working correctly
-            // Commented out ['matapplication']..., etc. because I
-            // noticed that when I imported a new Blackboard 6 file
-            // and printed out the block, the tree did not extend past ['material'][0]['#'] - CT 8/3/06
-            $block->file = $cur_block['#']['material'][0]['#'];//['matapplication'][0]['@']['uri'];
-            if ($block->file != '') {
-                // if we have a file copy it to the course dir and adjust its name to be visible over the web.
-                $block->file = $this->copy_file_to_course($block->file);
-                $block->file = $CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($block->file);
-            }
-            break;
-        case 'Block':
-            if (isset($cur_block['#']['material'][0]['#']['mattext'][0]['#'])) {
-            $block->text = $cur_block['#']['material'][0]['#']['mattext'][0]['#'];
-            }
-            else if (isset($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'])) {
-                $block->text = $cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'];
-            }
-            else if (isset($cur_block['#']['response_label'])) {
-                // this is a response label block
-                $sub_blocks = $cur_block['#']['response_label'][0];
-                if(!isset($block->ident)) {
-                    if(isset($sub_blocks['@']['ident'])) {
-                        $block->ident = $sub_blocks['@']['ident'];
                     }
                 }
-                foreach($sub_blocks['#']['flow_mat'] as $sub_block) {
-                    $this->process_block($sub_block, $block);
-                }
-            }
-            else {
-                if (isset($cur_block['#']['flow_mat']) || isset($cur_block['#']['flow'])) {
-                    if (isset($cur_block['#']['flow_mat'])) {
-                        $sub_blocks = $cur_block['#']['flow_mat'];
-                    }
-                    elseif (isset($cur_block['#']['flow'])) {
-                        $sub_blocks = $cur_block['#']['flow'];
-                    }
-                   foreach ($sub_blocks as $sblock) {
-                        // this will recursively grab the sub blocks which should be of one of the other types
-                        $this->process_block($sblock, $block);
-                    }
-                }
-            }
-            break;
-        case 'LINK_BLOCK':
-            // not sure how this should be included
-            if (!empty($cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'])) {
-                $block->link = $cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'];
-            }
-            else {
-               $block->link = '';
-            }
-            break;
-    }
-    return $block;
-}
 
-function process_choices($bb_choices, &$choices) {
-    foreach($bb_choices as $choice) {
-            if (isset($choice['@']['ident'])) {
-            $cur_choice = $choice['@']['ident'];
-        }
-        else { //for multiple answer
-            $cur_choice = $choice['#']['response_label'][0];//['@']['ident'];
-        }
-        if (isset($choice['#']['flow_mat'][0])) { //for multiple answer
-            $cur_block = $choice['#']['flow_mat'][0];
-            // Reset $cur_choice to NULL because process_block is expecting an object
-            // for the second argument and not a string, which is what is was set as
-            // originally - CT 8/7/06
-            $cur_choice = null;
-            $this->process_block($cur_block, $cur_choice);
-        }
-        elseif (isset($choice['#']['response_label'])) {
-            // Reset $cur_choice to NULL because process_block is expecting an object
-            // for the second argument and not a string, which is what is was set as
-            // originally - CT 8/7/06
-            $cur_choice = null;
-            $this->process_block($choice, $cur_choice);
-        }
-        $choices[] = $cur_choice;
-    }
-}
-
-function process_matching_responses($bb_responses, &$responses) {
-    foreach($bb_responses as $bb_response) {
-        $response = NULL;
-        if (isset($bb_response['#']['conditionvar'][0]['#']['varequal'])) {
-            $response->correct = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['#'];
-            $response->ident = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['@']['respident'];
-        }
-        else {
-            $response->correct =  'Broken Question?';
-            $response->ident = 'Broken Question?';
-        }
-        $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
-        $responses[] = $response;
-    }
-}
-
-function process_responses($bb_responses, &$responses) {
-    foreach($bb_responses as $bb_response) {
-        //Added this line to instantiate $response.
-        // Without instantiating the $response variable, the same object
-        // gets added to the array
-        $response = new stdClass();
-        if (isset($bb_response['@']['title'])) {
-                $response->title = $bb_response['@']['title'];
-            }
-            else {
-                $reponse->title = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
-            }
-            $reponse->ident = array();
-            if (isset($bb_response['#']['conditionvar'][0]['#'])){//['varequal'][0]['#'])) {
-                $response->ident[0] = $bb_response['#']['conditionvar'][0]['#'];//['varequal'][0]['#'];
-            }
-            else if (isset($bb_response['#']['conditionvar'][0]['#']['other'][0]['#'])) {
-                $response->ident[0] = $bb_response['#']['conditionvar'][0]['#']['other'][0]['#'];
-            }
-
-            if (isset($bb_response['#']['conditionvar'][0]['#']['and'])){//[0]['#'])) {
-                $responseset = $bb_response['#']['conditionvar'][0]['#']['and'];//[0]['#']['varequal'];
-                foreach($responseset as $rs) {
-                    $response->ident[] = $rs['#'];
-                    if(!isset($response->feedback) and isset( $rs['@'] ) ) {
-                        $response->feedback = $rs['@']['respident'];
-                    }
-                }
-            }
-            else {
-                $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
-            }
-
-            // determine what point value to give response
-            if (isset($bb_response['#']['setvar'])) {
-                switch ($bb_response['#']['setvar'][0]['#']) {
-                    case "SCORE.max":
-                        $response->fraction = 1;
-                        break;
-                    default:
-                        // I have only seen this being 0 or unset
-                        // there are probably fractional values of SCORE.max, but I'm not sure what they look like
-                        $response->fraction = 0;
-                        break;
-                }
-            }
-            else {
-               // just going to assume this is the case this is probably not correct.
-               $response->fraction = 0;
-            }
-
-            $responses[] = $response;
-        }
-}
-
-function process_feedback($feedbackset, &$feedbacks) {
-    foreach($feedbackset as $bb_feedback) {
-        // Added line $feedback=null so that $feedback does not get reused in the loop
-        // and added the the $feedbacks[] array multiple times
-        $feedback = null;
-        $feedback->ident = $bb_feedback['@']['ident'];
-        if (isset($bb_feedback['#']['flow_mat'][0])) {
-            $this->process_block($bb_feedback['#']['flow_mat'][0], $feedback);
-        }
-        elseif (isset($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0])) {
-            $this->process_block($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0], $feedback);
-        }
-        $feedbacks[] = $feedback;
-    }
-}
-
-/**
- * Create common parts of question
- */
-function process_common( $quest ) {
-    $question = $this->defaultquestion();
-    $question->questiontext = $quest->QUESTION_BLOCK->text;
-    $question->name = shorten_text( $quest->id, 250 );
-
-    return $question;
-}
-
-//----------------------------------------
-// Process True / False Questions
-//----------------------------------------
-function process_tf($quest, &$questions) {
-    $question = $this->process_common( $quest );
-
-    $question->qtype = TRUEFALSE;
-    $question->single = 1; // Only one answer is allowed
-    // 0th [response] is the correct answer.
-    $responses = $quest->responses;
-    $correctresponse = $responses[0]->ident[0]['varequal'][0]['#'];
-    if ($correctresponse != 'false') {
-        $correct = true;
-    }
-    else {
-        $correct = false;
-    }
-
-    foreach($quest->feedback as $fb) {
-        $fback->{$fb->ident} = $fb->text;
-    }
-
-    if ($correct) {  // true is correct
-        $question->answer = 1;
-        $question->feedbacktrue = $fback->correct;
-        $question->feedbackfalse = $fback->incorrect;
-    } else {  // false is correct
-        $question->answer = 0;
-        $question->feedbacktrue = $fback->incorrect;
-        $question->feedbackfalse = $fback->correct;
-    }
-    $question->correctanswer = $question->answer;
-    $questions[] = $question;
-}
-
-
-//----------------------------------------
-// Process Fill in the Blank
-//----------------------------------------
-function process_fblank($quest, &$questions) {
-    $question = $this->process_common( $quest );
-    $question->qtype = SHORTANSWER;
-    $question->single = 1;
-
-    $answers = array();
-    $fractions = array();
-    $feedbacks = array();
-
-    // extract the feedback
-    $feedback = array();
-    foreach($quest->feedback as $fback) {
-        if (isset($fback->ident)) {
-            if ($fback->ident == 'correct' || $fback->ident == 'incorrect') {
-                $feedback[$fback->ident] = $fback->text;
-            }
-        }
-    }
-
-    foreach($quest->responses as $response) {
-        if(isset($response->title)) {
-            if (isset($response->ident[0]['varequal'][0]['#'])) {
-                //for BB Fill in the Blank, only interested in correct answers
-                if ($response->feedback = 'correct') {
-                    $answers[] = $response->ident[0]['varequal'][0]['#'];
-                    $fractions[] = 1;
-                    if (isset($feedback['correct'])) {
-                        $feedbacks[] = $feedback['correct'];
-                    }
-                    else {
-                        $feedbacks[] = '';
-                    }
+                if ($q_file) {
+                    return $q_file;
+                } else {
+                    $this->error(get_string('cannotfindquestionfile', 'question'));
+                    fulldelete($this->tempdir);
                 }
-            }
-
-        }
-    }
-
-    //Adding catchall to so that students can see feedback for incorrect answers when they enter something the
-    //instructor did not enter
-    $answers[] = '*';
-    $fractions[] = 0;
-    if (isset($feedback['incorrect'])) {
-        $feedbacks[] = $feedback['incorrect'];
-    }
-    else {
-        $feedbacks[] = '';
-    }
-
-    $question->answer = $answers;
-    $question->fraction = $fractions;
-    $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of
-
-    if (!empty($question)) {
-        $questions[] = $question;
-    }
-
-}
-
-//----------------------------------------
-// Process Multiple Choice Questions
-//----------------------------------------
-function process_mc($quest, &$questions) {
-    $question = $this->process_common( $quest );
-    $question->qtype = MULTICHOICE;
-    $question->single = 1;
-
-    $feedback = array();
-    foreach($quest->feedback as $fback) {
-        $feedback[$fback->ident] = $fback->text;
-    }
-
-    foreach($quest->responses as $response) {
-        if (isset($response->title)) {
-            if ($response->title == 'correct') {
-                // only one answer possible for this qtype so first index is correct answer
-                $correct = $response->ident[0]['varequal'][0]['#'];
-            }
-        }
-        else {
-            // fallback method for when the title is not set
-            if ($response->feedback == 'correct') {
-               // only one answer possible for this qtype so first index is correct answer
-               $correct = $response->ident[0]['varequal'][0]['#']; // added [0]['varequal'][0]['#'] to $response->ident - CT 8/9/06
-            }
-        }
-    }
-
-    $i = 0;
-    foreach($quest->RESPONSE_BLOCK->choices as $response) {
-        $question->answer[$i] = $response->text;
-        if ($correct == $response->ident) {
-            $question->fraction[$i] = 1;
-            // this is a bit of a hack to catch the feedback... first we see if a 'correct' feedback exists
-            // then specific feedback for this question (maybe this should be switched?, but from my example
-            // question pools I have not seen response specific feedback, only correct or incorrect feedback
-            if (!empty($feedback['correct'])) {
-                $question->feedback[$i] = $feedback['correct'];
-            }
-            elseif (!empty($feedback[$i])) {
-                $question->feedback[$i] = $feedback[$i];
-            }
-            else {
-                // failsafe feedback (should be '' instead?)
-                $question->feedback[$i] = "correct";
-            }
-        }
-        else {
-            $question->fraction[$i] = 0;
-            if (!empty($feedback['incorrect'])) {
-                $question->feedback[$i] = $feedback['incorrect'];
-            }
-            elseif (!empty($feedback[$i])) {
-                $question->feedback[$i] = $feedback[$i];
-            }
-            else {
-                // failsafe feedback (should be '' instead?)
-                $question->feedback[$i] = 'incorrect';
-            }
-        }
-        $i++;
-    }
-
-    if (!empty($question)) {
-        $questions[] = $question;
-    }
-}
-
-//----------------------------------------
-// Process Multiple Choice Questions With Multiple Answers
-//----------------------------------------
-function process_ma($quest, &$questions) {
-    $question = $this->process_common( $quest ); // copied this from process_mc
-    $question->qtype = MULTICHOICE;
-    $question->single = 0; // More than one answer allowed
-
-    $answers = $quest->responses;
-    $correct_answers = array();
-    foreach($answers as $answer) {
-        if($answer->title == 'correct') {
-            $answerset = $answer->ident[0]['and'][0]['#']['varequal'];
-            foreach($answerset as $ans) {
-                $correct_answers[] = $ans['#'];
-            }
-        }
-    }
-
-    foreach ($quest->feedback as $fb) {
-        $feedback->{$fb->ident} = trim($fb->text);
-    }
-
-    $correct_answer_count = count($correct_answers);
-    $choiceset = $quest->RESPONSE_BLOCK->choices;
-    $i = 0;
-    foreach($choiceset as $choice) {
-        $question->answer[$i] = trim($choice->text);
-        if (in_array($choice->ident, $correct_answers)) {
-            // correct answer
-            $question->fraction[$i] = floor(100000/$correct_answer_count)/100000; // strange behavior if we have more than 5 decimal places
-            $question->feedback[$i] = $feedback->correct;
-        }
-        else {
-            // wrong answer
-            $question->fraction[$i] = 0;
-            $question->feedback[$i] = $feedback->incorrect;
-        }
-        $i++;
-    }
-
-    $questions[] = $question;
-}
-
-//----------------------------------------
-// Process Essay Questions
-//----------------------------------------
-function process_essay($quest, &$questions) {
-// this should be rewritten to accomodate moodle 1.6 essay question type eventually
-
-    if (defined("ESSAY")) {
-        // treat as short answer
-        $question = $this->process_common( $quest ); // copied this from process_mc
-        $question->qtype = ESSAY;
-
-        $question->feedback = array();
-        // not sure where to get the correct answer from
-        foreach($quest->feedback as $feedback) {
-            // Added this code to put the possible solution that the
-            // instructor gives as the Moodle answer for an essay question
-            if ($feedback->ident == 'solution') {
-                $question->feedback = $feedback->text;
-            }
-        }
-        //Added because essay/questiontype.php:save_question_option is expecting a
-        //fraction property - CT 8/10/06
-        $question->fraction[] = 1;
-        if (!empty($question)) {
-            $questions[]=$question;
-        }
-    }
-    else {
-        print "Essay question types are not handled because the quiz question type 'Essay' does not exist in this installation of Moodle<br/>";
-        print "&nbsp;&nbsp;&nbsp;&nbsp;Omitted Question: ".$quest->QUESTION_BLOCK->text.'<br/><br/>';
-    }
-}
-
-//----------------------------------------
-// Process Matching Questions
-//----------------------------------------
-function process_matching($quest, &$questions) {
-    // renderedmatch is an optional plugin, so we need to check if it is defined
-    if (question_bank::is_qtype_installed('renderedmatch')) {
-        $question = $this->process_common($quest);
-        $question->valid = true;
-        $question->qtype = 'renderedmatch';
-
-        foreach($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
-            foreach($quest->responses as $rid => $resp) {
-                if ($resp->ident == $subq->ident) {
-                    $correct = $resp->correct;
-                    $feedback = $resp->feedback;
-                }
-            }
-
-            foreach($subq->choices as $cid => $choice) {
-                if ($choice == $correct) {
-                    $question->subquestions[] = $subq->text;
-                    $question->subanswers[] = $quest->RIGHT_MATCH_BLOCK->matching_answerset[$cid]->text;
-                }
-            }
-        }
-
-        // check format
-        $status = true;
-        if ( count($quest->RESPONSE_BLOCK->subquestions) > count($quest->RIGHT_MATCH_BLOCK->matching_answerset) || count($question->subquestions) < 2) {
-            $status = false;
-        }
-        else {
-            // need to redo to make sure that no two questions have the same answer (rudimentary now)
-            foreach($question->subanswers as $qstn) {
-                if(isset($previous)) {
-                    if ($qstn == $previous) {
-                        $status = false;
-                    }
-                }
-                $previous = $qstn;
-                if ($qstn == '') {
-                    $status = false;
-                }
-            }
-        }
-
-        if ($status) {
-            $questions[] = $question;
-        }
-        else {
-            global $COURSE, $CFG;
-            print '<table class="boxaligncenter" border="1">';
-            print '<tr><td colspan="2" style="background-color:#FF8888;">This matching question is malformed. Please ensure there are no blank answers, no two questions have the same answer, and/or there are correct answers for each question. There must be at least as many subanswers as subquestions, and at least one subquestion.</td></tr>';
-
-            print "<tr><td>Question:</td><td>".$quest->QUESTION_BLOCK->text;
-            if (isset($quest->QUESTION_BLOCK->file)) {
-                print '<br/><font color="red">There is a subfile contained in the zipfile that has been copied to course files: bb_import/'.basename($quest->QUESTION_BLOCK->file).'</font>';
-                if (preg_match('/(gif|jpg|jpeg|png)$/i', $quest->QUESTION_BLOCK->file)) {
-                    print '<img src="'.$CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($quest->QUESTION_BLOCK->file).'" />';
-                }
-            }
-            print "</td></tr>";
-            print "<tr><td>Subquestions:</td><td><ul>";
-            foreach($quest->responses as $rs) {
-                $correct_responses->{$rs->ident} = $rs->correct;
-            }
-            foreach($quest->RESPONSE_BLOCK->subquestions as $subq) {
-                print '<li>'.$subq->text.'<ul>';
-                foreach($subq->choices as $id=>$choice) {
-                    print '<li>';
-                    if ($choice == $correct_responses->{$subq->ident}) {
-                        print '<font color="green">';
-                    }
-                    else {
-                        print '<font color="red">';
-                    }
-                    print $quest->RIGHT_MATCH_BLOCK->matching_answerset[$id]->text.'</font></li>';
-                }
-                print '</ul>';
-            }
-            print '</ul></td></tr>';
-
-            print '<tr><td>Feedback:</td><td><ul>';
-            foreach($quest->feedback as $fb) {
-                print '<li>'.$fb->ident.': '.$fb->text.'</li>';
-            }
-            print '</ul></td></tr></table>';
+            } else {
+                $this->error(get_string('cannotunzip', 'question'));
+                fulldelete($this->temp_dir);
+            }
+        } else {
+            $this->error(get_string('cannotreaduploadfile', 'error'));
+            fulldelete($this->tempdir);
+        }
+        return false;
+    }
+
+    /**
+     * Parse the array of strings into an array of questions.
+     * Each string is the content of a .dat questions file.
+     * This *could* burn memory - but it won't happen that much
+     * so fingers crossed!
+     * @param array of strings from the input file.
+     * @param stdClass $context
+     * @return array (of objects) question objects.
+     */
+    public function readquestions($lines) {
+
+        // Set up array to hold all our questions.
+        $questions = array();
+        if ($this->filetype == self::FILETYPE_QTI) {
+            $importer = new qformat_blackboard_six_qti();
+        } else if ($this->filetype == self::FILETYPE_POOL) {
+            $importer = new qformat_blackboard_six_pool();
+        } else {
+            // In all other cases we are not able to import the file.
+            return false;
         }
-    }
-    else {
-        print "Matching question types are not handled because the quiz question type 'Rendered Matching' does not exist in this installation of Moodle<br/>";
-        print "&nbsp;&nbsp;&nbsp;&nbsp;Omitted Question: ".$quest->QUESTION_BLOCK->text.'<br/><br/>';
-    }
-}
+        $importer->set_filebase($this->filebase);
 
-
-function strip_applet_tags_get_mathml($string) {
-    if(stristr($string, '</APPLET>') === FALSE) {
-        return $string;
-    }
-    else {
-        // strip all applet tags keeping stuff before/after and inbetween (if mathml) them
-        while (stristr($string, '</APPLET>') !== FALSE) {
-            preg_match("/(.*)\<applet.*value=\"(\<math\>.*\<\/math\>)\".*\<\/applet\>(.*)/i",$string, $mathmls);
-            $string = $mathmls[1].$mathmls[2].$mathmls[3];
+        // Each element of $lines is a string containing a complete xml document.
+        foreach ($lines as $text) {
+                $questions = array_merge($questions, $importer->readquestions($text));
         }
-        return $string;
+        return $questions;
     }
 }
-
-} // close object
-
diff --git a/question/format/blackboard_six/formatbase.php b/question/format/blackboard_six/formatbase.php
new file mode 100644 (file)
index 0000000..da08e91
--- /dev/null
@@ -0,0 +1,163 @@
+<?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/>.
+
+/**
+ * Blackboard V5 and V6 question importer.
+ *
+ * @package    qformat_blackboard_six
+ * @copyright  2012 Jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Base class question import format for zip files with images
+ *
+ */
+
+class qformat_blackboard_six_base extends qformat_based_on_xml {
+    /** @var string path to path to root of image tree in unzipped archive. */
+    public $filebase = '';
+    /** @var string path to the temporary directory. */
+    public $tempdir = '';
+    /**
+     * This plugin provide import
+     * @return bool true
+     */
+    public function provide_import() {
+        return true;
+    }
+
+    /**
+     * Check if the given file is capable of being imported by this plugin.
+     * As {@link file_storage::mimetype()} now uses finfo PHP extension if available,
+     * the value returned by $file->get_mimetype for a .dat file is not the same on all servers.
+     * So we must made 2 checks to verify if the plugin can import the file.
+     * @param stored_file $file the file to check
+     * @return bool whether this plugin can import the file
+     */
+    public function can_import_file($file) {
+        $mimetypes = array(
+            mimeinfo('type', '.dat'),
+            mimeinfo('type', '.zip')
+        );
+        return in_array($file->get_mimetype(), $mimetypes) || in_array(mimeinfo('type', $file->get_filename()), $mimetypes);
+    }
+
+    public function mime_type() {
+        return mimeinfo('type', '.zip');
+    }
+
+    /**
+     * Does any post-processing that may be desired
+     * Clean the temporary directory if a zip file was imported
+     * @return bool success
+     */
+    public function importpostprocess() {
+        if ($this->tempdir != '') {
+            fulldelete($this->tempdir);
+        }
+        return true;
+    }
+    /**
+     * Set the path to the root of images tree
+     * @param string $path path to images root
+     */
+    public function set_filebase($path) {
+        $this->filebase = $path;
+    }
+
+    /**
+     * Store an image file in a draft filearea
+     * @param array $text, if itemid element don't exists it will be created
+     * @param string tempdir path to root of image tree
+     * @param string filepathinsidetempdir path to image in the tree
+     * @param string filename image's name
+     * @return string new name of the image as it was stored
+     */
+    protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) {
+        global $USER;
+        $fs = get_file_storage();
+        if (empty($text['itemid'])) {
+            $text['itemid'] = file_get_unused_draft_itemid();
+        }
+        // As question file areas don't support subdirs,
+        // convert path to filename.
+        // So that images with same name can be imported.
+        $newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE);
+        $filerecord = array(
+            'contextid' => context_user::instance($USER->id)->id,
+            'component' => 'user',
+            'filearea'  => 'draft',
+            'itemid'    => $text['itemid'],
+            'filepath'  => '/',
+            'filename'  => $newfilename,
+        );
+        $fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename);
+        return $newfilename;
+    }
+
+    /**
+     * Given an HTML text with references to images files,
+     * store all images in a draft filearea,
+     * and return an array with all urls in text recoded,
+     * format set to FORMAT_HTML, and itemid set to filearea itemid
+     * @param string text text to parse and recode
+     * @return array with keys text, format, itemid.
+     */
+    public function text_field($text) {
+        $data = array();
+        // Step one, find all file refs then add to array.
+        preg_match_all('|<img[^>]+src="([^"]*)"|i', $text, $out); // Find all src refs.
+
+        foreach ($out[1] as $path) {
+            $fullpath = $this->filebase . '/' . $path;
+
+            if (is_readable($fullpath)) {
+                $dirpath = dirname($path);
+                $filename = basename($path);
+                $newfilename = $this->store_file_for_text_field($data, $this->filebase, $dirpath, $filename);
+                $text = preg_replace("|$path|", "@@PLUGINFILE@@/" . $newfilename, $text);
+            }
+
+        }
+        $data['text'] = $text;
+        $data['format'] = FORMAT_HTML;
+        return $data;
+    }
+
+    /**
+     * Same as text_field but text is cleaned.
+     * @param string text text to parse and recode
+     * @return array with keys text, format, itemid.
+     */
+    public function cleaned_text_field($text) {
+        return $this->text_field($this->cleaninput($text));
+    }
+
+    /**
+     * Convert the question text to plain text.
+     * We need to overwrite this function because questiontext is an array.
+     */
+    protected function format_question_text($question) {
+        global $DB;
+        $formatoptions = new stdClass();
+        $formatoptions->noclean = true;
+        return html_to_text(format_text($question->questiontext['text'],
+                $question->questiontext['format'], $formatoptions), 0, false);
+    }
+}
diff --git a/question/format/blackboard_six/formatpool.php b/question/format/blackboard_six/formatpool.php
new file mode 100644 (file)
index 0000000..80867f3
--- /dev/null
@@ -0,0 +1,467 @@
+<?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/>.
+
+/**
+ * Blackboard V5 and V6 question importer.
+ *
+ * @package    qformat_blackboard_six
+ * @copyright  2003 Scott Elliott
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/xmlize.php');
+
+/**
+ * Blackboard pool question importer.
+ *
+ * @copyright  2003 Scott Elliott
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class qformat_blackboard_six_pool extends qformat_blackboard_six_base {
+    // Is the current question's question text escaped HTML (true for most if not all Blackboard files).
+    public $ishtml = true;
+
+    /**
+     * Parse the xml document into an array of questions
+     * this *could* burn memory - but it won't happen that much
+     * so fingers crossed!
+     * @param array of lines from the input file.
+     * @param stdClass $context
+     * @return array (of objects) questions objects.
+     */
+    protected function readquestions($text) {
+
+        // This converts xml to big nasty data structure,
+        // the 0 means keep white space as it is.
+        try {
+            $xml = xmlize($text, 0, 'UTF-8', true);
+        } catch (xml_format_exception $e) {
+            $this->error($e->getMessage(), '');
+            return false;
+        }
+
+        $questions = array();
+
+        $this->process_tf($xml, $questions);
+        $this->process_mc($xml, $questions);
+        $this->process_ma($xml, $questions);
+        $this->process_fib($xml, $questions);
+        $this->process_matching($xml, $questions);
+        $this->process_essay($xml, $questions);
+
+        return $questions;
+    }
+
+    /**
+     * Do question import processing common to every qtype.
+     * @param array $questiondata the xml tree related to the current question
+     * @return object initialized question object.
+     */
+    public function process_common($questiondata) {
+
+        // This routine initialises the question object.
+        $question = $this->defaultquestion();
+
+        // Determine if the question is already escaped html.
+        $this->ishtml = $this->getpath($questiondata,
+                array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'),
+                false, false);
+
+        // Put questiontext in question object.
+        $text = $this->getpath($questiondata,
+                array('#', 'BODY', 0, '#', 'TEXT', 0, '#'),
+                '', true, get_string('importnotext', 'qformat_blackboard_six'));
+
+        $question->questiontext = $this->cleaned_text_field($text);
+        $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it.
+
+        // Put name in question object. We must ensure it is not empty and it is less than 250 chars.
+        $question->name = shorten_text(strip_tags($question->questiontext['text']), 200);
+        $question->name = substr($question->name, 0, 250);
+        if (!$question->name) {
+            $id = $this->getpath($questiondata,
+                    array('@', 'id'), '',  true);
+            $question->name = get_string('defaultname', 'qformat_blackboard_six' , $id);
+        }
+
+        $question->generalfeedback = '';
+        $question->generalfeedbackformat = FORMAT_HTML;
+        $question->generalfeedbackfiles = array();
+
+        // TODO : read the mark from the POOL TITLE QUESTIONLIST section.
+        $question->defaultmark = 1;
+        return $question;
+    }
+
+    /**
+     * Process Essay Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_essay($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) {
+            $essayquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_ESSAY'), false, false);
+        } else {
+            return;
+        }
+
+        foreach ($essayquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $question->qtype = 'essay';
+
+            $question->answer = '';
+            $answer = $this->getpath($thisquestion,
+                    array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true);
+            $question->graderinfo =  $this->cleaned_text_field($answer);
+            $question->feedback = '';
+            $question->responseformat = 'editor';
+            $question->responsefieldlines = 15;
+            $question->attachments = 0;
+            $question->fraction = 0;
+
+            $questions[] = $question;
+        }
+    }
+
+    /**
+     * Process True / False Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_tf($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) {
+            $tfquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false);
+        } else {
+            return;
+        }
+
+        foreach ($tfquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $question->qtype = 'truefalse';
+            $question->single = 1; // Only one answer is allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false);
+
+            $correctanswer = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
+                    '', true);
+
+            // First choice is true, second is false.
+            $id = $this->getpath($choices[0], array('@', 'id'), '', true);
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            if (strcmp($id,  $correctanswer) == 0) {  // True is correct.
+                $question->answer = 1;
+                $question->feedbacktrue = $this->cleaned_text_field($correctfeedback);
+                $question->feedbackfalse = $this->cleaned_text_field($incorrectfeedback);
+            } else {  // False is correct.
+                $question->answer = 0;
+                $question->feedbacktrue = $this->cleaned_text_field($incorrectfeedback);
+                $question->feedbackfalse = $this->cleaned_text_field($correctfeedback);
+            }
+            $question->correctanswer = $question->answer;
+            $questions[] = $question;
+        }
+    }
+
+    /**
+     * Process Multiple Choice Questions with single answer
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_mc($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) {
+            $mcquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false);
+        } else {
+            return;
+        }
+
+        foreach ($mcquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->cleaned_text_field($correctfeedback);
+            $question->partiallycorrectfeedback = $this->text_field('');
+            $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
+
+            $question->qtype = 'multichoice';
+            $question->single = 1; // Only one answer is allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            $correctanswerid = $this->getpath($thisquestion,
+                        array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
+                        '', true);
+            foreach ($choices as $choice) {
+                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
+                // Put this choice in the question object.
+                $question->answer[] =  $this->cleaned_text_field($choicetext);
+
+                $choiceid = $this->getpath($choice, array('@', 'id'), '', true);
+                // If choice is the right answer, give 100% mark, otherwise give 0%.
+                if (strcmp ($choiceid, $correctanswerid) == 0) {
+                    $question->fraction[] = 1;
+                } else {
+                    $question->fraction[] = 0;
+                }
+                // There is never feedback specific to each choice.
+                $question->feedback[] =  $this->text_field('');
+            }
+            $questions[] = $question;
+        }
+    }
+
+    /**
+     * Process Multiple Choice Questions With Multiple Answers
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_ma($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) {
+            $maquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false);
+        } else {
+            return;
+        }
+
+        foreach ($maquestions as $thisquestion) {
+            $question = $this->process_common($thisquestion);
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->cleaned_text_field($correctfeedback);
+            // As there is no partially correct feedback we use incorrect one.
+            $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
+            $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
+
+            $question->qtype = 'multichoice';
+            $question->defaultmark = 1;
+            $question->single = 0; // More than one answers allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            $correctanswerids = array();
+            foreach ($this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) {
+                if ($correctanswer) {
+                    $correctanswerids[] = $this->getpath($correctanswer,
+                            array('@', 'answer_id'),
+                            '', true);
+                }
+            }
+            $fraction = 1/count($correctanswerids);
+
+            foreach ($choices as $choice) {
+                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
+                // Put this choice in the question object.
+                $question->answer[] =  $this->cleaned_text_field($choicetext);
+
+                $choiceid = $this->getpath($choice, array('@', 'id'), '', true);
+
+                $iscorrect = in_array($choiceid, $correctanswerids);
+
+                if ($iscorrect) {
+                    $question->fraction[] = $fraction;
+                } else {
+                    $question->fraction[] = 0;
+                }
+                // There is never feedback specific to each choice.
+                $question->feedback[] =  $this->text_field('');
+            }
+            $questions[] = $question;
+        }
+    }
+
+    /**
+     * Process Fill in the Blank Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_fib($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) {
+            $fibquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false);
+        } else {
+            return;
+        }
+
+        foreach ($fibquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $question->qtype = 'shortanswer';
+            $question->usecase = 0; // Ignore case.
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            foreach ($answers as $answer) {
+                $question->answer[] = $this->getpath($answer,
+                        array('#', 'TEXT', 0, '#'), '', true);
+                $question->fraction[] = 1;
+                $question->feedback[] = $this->cleaned_text_field($correctfeedback);
+            }
+            $question->answer[] = '*';
+            $question->fraction[] = 0;
+            $question->feedback[] = $this->cleaned_text_field($incorrectfeedback);
+
+            $questions[] = $question;
+        }
+    }
+
+    /**
+     * Process Matching Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_matching($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) {
+            $matchquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MATCH'), false, false);
+        } else {
+            return;
+        }
+        // Blackboard questions can't be imported in core Moodle without a loss in data,
+        // as core match question don't allow HTML in subanswers. The contributed ddmatch
+        // question type support HTML in subanswers.
+        // The ddmatch question type is not part of core, so we need to check if it is defined.
+        $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch');
+
+        foreach ($matchquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+            if ($ddmatchisinstalled) {
+                $question->qtype = 'ddmatch';
+            } else {
+                $question->qtype = 'match';
+            }
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->cleaned_text_field($correctfeedback);
+            // As there is no partially correct feedback we use incorrect one.
+            $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
+            $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback);
+
+            $choices = $this->getpath($thisquestion,
+                    array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers.
+            $answers = $this->getpath($thisquestion,
+                    array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions.
+            $correctanswers = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers.
+            $mappings = array();
+            foreach ($correctanswers as $correctanswer) {
+                if ($correctanswer) {
+                    $correctchoiceid = $this->getpath($correctanswer,
+                                array('@', 'choice_id'), '', true);
+                    $correctanswerid = $this->getpath($correctanswer,
+                            array('@', 'answer_id'),
+                            '', true);
+                    $mappings[$correctanswerid] = $correctchoiceid;
+                }
+            }
+
+            foreach ($choices as $choice) {
+                if ($ddmatchisinstalled) {
+                    $choicetext = $this->cleaned_text_field($this->getpath($choice,
+                            array('#', 'TEXT', 0, '#'), '', true));
+                } else {
+                    $choicetext = trim(strip_tags($this->getpath($choice,
+                            array('#', 'TEXT', 0, '#'), '', true)));
+                }
+
+                if ($choicetext != '') { // Only import non empty subanswers.
+                    $subquestion = '';
+                    $choiceid = $this->getpath($choice,
+                            array('@', 'id'), '', true);
+                    $fiber = array_search($choiceid, $mappings);
+                    $fiber = array_keys ($mappings, $choiceid);
+                    foreach ($fiber as $correctanswerid) {
+                        // We have found a correspondance for this choice so we need to take the associated answer.
+                        foreach ($answers as $answer) {
+                            $currentanswerid = $this->getpath($answer,
+                                    array('@', 'id'), '', true);
+                            if (strcmp ($currentanswerid, $correctanswerid) == 0) {
+                                $subquestion = $this->getpath($answer,
+                                        array('#', 'TEXT', 0, '#'), '', true);
+                                break;
+                            }
+                        }
+                        $question->subquestions[] = $this->cleaned_text_field($subquestion);
+                        $question->subanswers[] = $choicetext;
+                    }
+
+                    if ($subquestion == '') { // Then in this case, $choice is a distractor.
+                        $question->subquestions[] = $this->text_field('');
+                        $question->subanswers[] = $choicetext;
+                    }
+                }
+            }
+
+            // Verify that this matching question has enough subquestions and subanswers.
+            $subquestioncount = 0;
+            $subanswercount = 0;
+            $subanswers = $question->subanswers;
+            foreach ($question->subquestions as $key => $subquestion) {
+                $subquestion = $subquestion['text'];
+                $subanswer = $subanswers[$key];
+                if ($subquestion != '') {
+                    $subquestioncount++;
+                }
+                $subanswercount++;
+            }
+            if ($subquestioncount < 2 || $subanswercount < 3) {
+                    $this->error(get_string('notenoughtsubans', 'qformat_blackboard_six', $question->questiontext['text']));
+            } else {
+                $questions[] = $question;
+            }
+
+        }
+    }
+}
diff --git a/question/format/blackboard_six/formatqti.php b/question/format/blackboard_six/formatqti.php
new file mode 100644 (file)
index 0000000..223d9f6
--- /dev/null
@@ -0,0 +1,894 @@
+<?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/>.
+
+/**
+ * Blackboard V5 and V6 question importer.
+ *
+ * @package    qformat_blackboard_six
+ * @copyright  2005 Michael Penney
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/xmlize.php');
+
+/**
+ * Blackboard 6.0 question importer.
+ *
+ * @copyright  2005 Michael Penney
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qformat_blackboard_six_qti extends qformat_blackboard_six_base {
+    /**
+     * Parse the xml document into an array of questions
+     * this *could* burn memory - but it won't happen that much
+     * so fingers crossed!
+     * @param array of lines from the input file.
+     * @param stdClass $context
+     * @return array (of objects) questions objects.
+     */
+    protected function readquestions($text) {
+
+        // This converts xml to big nasty data structure,
+        // the 0 means keep white space as it is.
+        try {
+            $xml = xmlize($text, 0, 'UTF-8', true);
+        } catch (xml_format_exception $e) {
+            $this->error($e->getMessage(), '');
+            return false;
+        }
+
+        $questions = array();
+        // First step : we are only interested in the <item> tags.
+        $rawquestions = $this->getpath($xml,
+                array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'),
+                array(), false);
+        // Each <item> tag contains data related to a single question.
+        foreach ($rawquestions as $quest) {
+            // Second step : parse each question data into the intermediate
+            // rawquestion structure array.
+            // Warning : rawquestions are not Moodle questions.
+            $question = $this->create_raw_question($quest);
+            // Third step : convert a rawquestion into a Moodle question.
+            switch($question->qtype) {
+                case "Matching":
+                    $this->process_matching($question, $questions);
+                    break;
+                case "Multiple Choice":
+                    $this->process_mc($question, $questions);
+                    break;
+                case "Essay":
+                    $this->process_essay($question, $questions);
+                    break;
+                case "Multiple Answer":
+                    $this->process_ma($question, $questions);
+                    break;
+                case "True/False":
+                    $this->process_tf($question, $questions);
+                    break;
+                case 'Fill in the Blank':
+                    $this->process_fblank($question, $questions);
+                    break;
+                case 'Short Response':
+                    $this->process_essay($question, $questions);
+                    break;
+                default:
+                    $this->error(get_string('unknownorunhandledtype', 'qformat_blackboard_six', $question->qtype));
+                    break;
+            }
+        }
+        return $questions;
+    }
+
+    /**
+     * Creates a cleaner object to deal with for processing into Moodle.
+     * The object returned is NOT a moodle question object.
+     * @param array $quest XML <item> question  data
+     * @return object rawquestion
+     */
+    public function create_raw_question($quest) {
+
+        $rawquestion = new stdClass();
+        $rawquestion->qtype = $this->getpath($quest,
+                array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'),
+                '', true);
+        $rawquestion->id = $this->getpath($quest,
+                array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'),
+                '', true);
+        $presentation = new stdClass();
+        $presentation->blocks = $this->getpath($quest,
+                array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'),
+                array(), false);
+
+        foreach ($presentation->blocks as $pblock) {
+            $block = new stdClass();
+            $block->type = $this->getpath($pblock,
+                    array('@', 'class'),
+                    '', true);
+
+            switch($block->type) {
+                case 'QUESTION_BLOCK':
+                    $subblocks = $this->getpath($pblock,
+                            array('#', 'flow'),
+                            array(), false);
+                    foreach ($subblocks as $sblock) {
+                        $this->process_block($sblock, $block);
+                    }
+                    break;
+
+                case 'RESPONSE_BLOCK':
+                    $choices = null;
+                    switch($rawquestion->qtype) {
+                        case 'Matching':
+                            $bbsubquestions = $this->getpath($pblock,
+                                    array('#', 'flow'),
+                                    array(), false);
+                            $sub_questions = array();
+                            foreach ($bbsubquestions as $bbsubquestion) {
+                                $sub_question = new stdClass();
+                                $sub_question->ident = $this->getpath($bbsubquestion,
+                                        array('#', 'response_lid', 0, '@', 'ident'),
+                                        '', true);
+                                $this->process_block($this->getpath($bbsubquestion,
+                                        array('#', 'flow', 0),
+                                        false, false), $sub_question);
+                                $bbchoices = $this->getpath($bbsubquestion,
+                                        array('#', 'response_lid', 0, '#', 'render_choice', 0,
+                                        '#', 'flow_label', 0, '#', 'response_label'),
+                                        array(), false);
+                                $choices = array();
+                                $this->process_choices($bbchoices, $choices);
+                                $sub_question->choices = $choices;
+                                if (!isset($block->subquestions)) {
+                                    $block->subquestions = array();
+                                }
+                                $block->subquestions[] = $sub_question;
+                            }
+                            break;
+                        case 'Multiple Answer':
+                            $bbchoices = $this->getpath($pblock,
+                                    array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
+                                    array(), false);
+                            $choices = array();
+                            $this->process_choices($bbchoices, $choices);
+                            $block->choices = $choices;
+                            break;
+                        case 'Essay':
+                            // Doesn't apply since the user responds with text input.
+                            break;
+                        case 'Multiple Choice':
+                            $mcchoices = $this->getpath($pblock,
+                                    array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'),
+                                    array(), false);
+                            foreach ($mcchoices as $mcchoice) {
+                                $choices = new stdClass();
+                                $choices = $this->process_block($mcchoice, $choices);
+                                $block->choices[] = $choices;
+                            }
+                            break;
+                        case 'Short Response':
+                            // Do nothing?
+                            break;
+                        case 'Fill in the Blank':
+                            // Do nothing?
+                            break;
+                        default:
+                            $bbchoices = $this->getpath($pblock,
+                                    array('#', 'response_lid', 0, '#', 'render_choice', 0, '#',
+                                    'flow_label', 0, '#', 'response_label'),
+                                    array(), false);
+                            $choices = array();
+                            $this->process_choices($bbchoices, $choices);
+                            $block->choices = $choices;
+                    }
+                    break;
+                case 'RIGHT_MATCH_BLOCK':
+                    $matchinganswerset = $this->getpath($pblock,
+                            array('#', 'flow'),
+                            false, false);
+
+                    $answerset = array();
+                    foreach ($matchinganswerset as $answer) {
+                        $bbanswer = new stdClass;
+                        $bbanswer->text =  $this->getpath($answer,
+                                array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension',
+                                0, '#', 'mat_formattedtext', 0, '#'),
+                                false, false);
+                        $answerset[] = $bbanswer;
+                    }
+                    $block->matchinganswerset = $answerset;
+                    break;
+                default:
+                    $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six'));
+                    break;
+            }
+            $rawquestion->{$block->type} = $block;
+        }
+
+        // Determine response processing.
+        // There is a section called 'outcomes' that I don't know what to do with.
+        $resprocessing = $this->getpath($quest,
+                array('#', 'resprocessing'),
+                array(), false);
+
+        $respconditions = $this->getpath($resprocessing[0],
+                array('#', 'respcondition'),
+                array(), false);
+        $responses = array();
+        if ($rawquestion->qtype == 'Matching') {
+            $this->process_matching_responses($respconditions, $responses);
+        } else {
+            $this->process_responses($respconditions, $responses);
+        }
+        $rawquestion->responses = $responses;
+        $feedbackset = $this->getpath($quest,
+                array('#', 'itemfeedback'),
+                array(), false);
+
+        $feedbacks = array();
+        $this->process_feedback($feedbackset, $feedbacks);
+        $rawquestion->feedback = $feedbacks;
+        return $rawquestion;
+    }
+
+    /**
+     * Helper function to process an XML block into an object.
+     * Can call himself recursively if necessary to parse this branch of the XML tree.
+     * @param array $curblock XML block to parse
+     * @return object $block parsed
+     */
+    public function process_block($curblock, $block) {
+
+        $curtype = $this->getpath($curblock,
+                array('@', 'class'),
+                '', true);
+
+        switch($curtype) {
+            case 'FORMATTED_TEXT_BLOCK':
+                $text = $this->getpath($curblock,
+                        array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
+                        '', true);
+                $block->text = $this->strip_applet_tags_get_mathml($text);
+                break;
+            case 'FILE_BLOCK':
+                $block->filename = $this->getpath($curblock,
+                        array('#', 'material', 0, '#'),
+                        '', true);
+                if ($block->filename != '') {
+                    // TODO : determine what to do with the file's content.
+                    $this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename));
+                }
+                break;
+            case 'Block':
+                if ($this->getpath($curblock,
+                        array('#', 'material', 0, '#', 'mattext'),
+                        false, false)) {
+                    $block->text = $this->getpath($curblock,
+                            array('#', 'material', 0, '#', 'mattext', 0, '#'),
+                            '', true);
+                } else if ($this->getpath($curblock,
+                        array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'),
+                        false, false)) {
+                    $block->text = $this->getpath($curblock,
+                            array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'),
+                            '', true);
+                } else if ($this->getpath($curblock,
+                        array('#', 'response_label'),
+                        false, false)) {
+                    // This is a response label block.
+                    $subblocks = $this->getpath($curblock,
+                            array('#', 'response_label', 0),
+                            array(), false);
+                    if (!isset($block->ident)) {
+
+                        if ($this->getpath($subblocks,
+                                array('@', 'ident'), '', true)) {
+                            $block->ident = $this->getpath($subblocks,
+                                array('@', 'ident'), '', true);
+                        }
+                    }
+                    foreach ($this->getpath($subblocks,
+                            array('#', 'flow_mat'), array(), false) as $subblock) {
+                        $this->process_block($subblock, $block);
+                    }
+                } else {
+                    if ($this->getpath($curblock,
+                                array('#', 'flow_mat'), false, false)
+                            || $this->getpath($curblock,
+                                array('#', 'flow'), false, false)) {
+                        if ($this->getpath($curblock,
+                                array('#', 'flow_mat'), false, false)) {
+                            $subblocks = $this->getpath($curblock,
+                                    array('#', 'flow_mat'), array(), false);
+                        } else if ($this->getpath($curblock,
+                                array('#', 'flow'), false, false)) {
+                            $subblocks = $this->getpath($curblock,
+                                    array('#', 'flow'), array(), false);
+                        }
+                        foreach ($subblocks as $sblock) {
+                            // This will recursively grab the sub blocks which should be of one of the other types.
+                            $this->process_block($sblock, $block);
+                        }
+                    }
+                }
+                break;
+            case 'LINK_BLOCK':
+                // Not sure how this should be included?
+                $link = $this->getpath($curblock,
+                            array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true);
+                if (!empty($link)) {
+                    $block->link = $link;
+                } else {
+                    $block->link = '';
+                }
+                break;
+        }
+        return $block;
+    }
+
+    /**
+     * Preprocess XML blocks containing data for questions' choices.
+     * Called by {@link create_raw_question()}
+     * for matching, multichoice and fill in the blank questions.
+     * @param array $bbchoices XML block to parse
+     * @param array $choices array of choices suitable for a rawquestion.
+     */
+    protected function process_choices($bbchoices, &$choices) {
+        foreach ($bbchoices as $choice) {
+            if ($this->getpath($choice,
+                    array('@', 'ident'), '', true)) {
+                $curchoice = $this->getpath($choice,
+                        array('@', 'ident'), '', true);
+            } else { // For multiple answers.
+                $curchoice = $this->getpath($choice,
+                         array('#', 'response_label', 0), array(), false);
+            }
+            if ($this->getpath($choice,
+                    array('#', 'flow_mat', 0), false, false)) { // For multiple answers.
+                $curblock = $this->getpath($choice,
+                    array('#', 'flow_mat', 0), false, false);
+                // Reset $curchoice to new stdClass because process_block is expecting an object
+                // for the second argument and not a string,
+                // which is what is was set as originally - CT 8/7/06.
+                $curchoice = new stdClass();
+                $this->process_block($curblock, $curchoice);
+            } else if ($this->getpath($choice,
+                    array('#', 'response_label'), false, false)) {
+                // Reset $curchoice to new stdClass because process_block is expecting an object
+                // for the second argument and not a string,
+                // which is what is was set as originally - CT 8/7/06.
+                $curchoice = new stdClass();
+                $this->process_block($choice, $curchoice);
+            }
+            $choices[] = $curchoice;
+        }
+    }
+
+    /**
+     * Preprocess XML blocks containing data for subanswers
+     * Called by {@link create_raw_question()}
+     * for matching questions only.
+     * @param array $bbresponses XML block to parse
+     * @param array $responses array of responses suitable for a matching rawquestion.
+     */
+    protected function process_matching_responses($bbresponses, &$responses) {
+        foreach ($bbresponses as $bbresponse) {
+            $response = new stdClass;
+            if ($this->getpath($bbresponse,
+                    array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) {
+                $response->correct = $this->getpath($bbresponse,
+                        array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true);
+                $response->ident = $this->getpath($bbresponse,
+                        array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true);
+            }
+            // Suppressed an else block because if the above if condition is false,
+            // the question is not necessary a broken one, most of the time it's an <other> tag.
+
+            $response->feedback = $this->getpath($bbresponse,
+                    array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
+            $responses[] = $response;
+        }
+    }
+
+    /**
+     * Preprocess XML blocks containing data for responses processing.
+     * Called by {@link create_raw_question()}
+     * for all questions types.
+     * @param array $bbresponses XML block to parse
+     * @param array $responses array of responses suitable for a rawquestion.
+     */
+    protected function process_responses($bbresponses, &$responses) {
+        foreach ($bbresponses as $bbresponse) {
+            $response = new stdClass();
+            if ($this->getpath($bbresponse,
+                    array('@', 'title'), '', true)) {
+                $response->title = $this->getpath($bbresponse,
+                        array('@', 'title'), '', true);
+            } else {
+                $response->title = $this->getpath($bbresponse,
+                        array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
+            }
+            $response->ident = array();
+            if ($this->getpath($bbresponse,
+                    array('#', 'conditionvar', 0, '#'), false, false)) {
+                $response->ident[0] = $this->getpath($bbresponse,
+                        array('#', 'conditionvar', 0, '#'), array(), false);
+            } else if ($this->getpath($bbresponse,
+                    array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) {
+                $response->ident[0] = $this->getpath($bbresponse,
+                        array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false);
+            }
+            if ($this->getpath($bbresponse,
+                    array('#', 'conditionvar', 0, '#', 'and'), false, false)) {
+                $responseset = $this->getpath($bbresponse,
+                    array('#', 'conditionvar', 0, '#', 'and'), array(), false);
+                foreach ($responseset as $rs) {
+                    $response->ident[] = $this->getpath($rs, array('#'), array(), false);
+                    if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) {
+                        $response->feedback = $this->getpath($rs,
+                                array('@', 'respident'), '', true);
+                    }
+                }
+            } else {
+                $response->feedback = $this->getpath($bbresponse,
+                        array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true);
+            }
+
+            // Determine what fraction to give response.
+            if ($this->getpath($bbresponse,
+                        array('#', 'setvar'), false, false)) {
+                switch ($this->getpath($bbresponse,
+                        array('#', 'setvar', 0, '#'), false, false)) {
+                    case "SCORE.max":
+                        $response->fraction = 1;
+                        break;
+                    default:
+                        // I have only seen this being 0 or unset.
+                        // There are probably fractional values of SCORE.max, but I'm not sure what they look like.
+                        $response->fraction = 0;
+                        break;
+                }
+            } else {
+                // Just going to assume this is the case this is probably not correct.
+                $response->fraction = 0;
+            }
+
+            $responses[] = $response;
+        }
+    }
+
+    /**
+     * Preprocess XML blocks containing data for responses feedbacks.
+     * Called by {@link create_raw_question()}
+     * for all questions types.
+     * @param array $feedbackset XML block to parse
+     * @param array $feedbacks array of feedbacks suitable for a rawquestion.
+     */
+    public function process_feedback($feedbackset, &$feedbacks) {
+        foreach ($feedbackset as $bb_feedback) {
+            $feedback = new stdClass();
+            $feedback->ident = $this->getpath($bb_feedback,
+                    array('@', 'ident'), '', true);
+            $feedback->text = '';
+            if ($this->getpath($bb_feedback,
+                    array('#', 'flow_mat', 0), false, false)) {
+                $this->process_block($this->getpath($bb_feedback,
+                        array('#', 'flow_mat', 0), false, false), $feedback);
+            } else if ($this->getpath($bb_feedback,
+                    array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) {
+                $this->process_block($this->getpath($bb_feedback,
+                        array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback);
+            }
+
+            $feedbacks[$feedback->ident] = $feedback;
+        }
+    }
+
+    /**
+     * Create common parts of question
+     * @param object $quest rawquestion
+     * @return object Moodle question.
+     */
+    public function process_common($quest) {
+        $question = $this->defaultquestion();
+        $text = $quest->QUESTION_BLOCK->text;
+
+        $question->questiontext = $this->cleaned_text_field($text);
+        $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it.
+
+        $question->name = shorten_text(strip_tags($question->questiontext['text']), 200);
+        $question->name = substr($question->name, 0, 250);
+        if (!$question->name) {
+            $question->name = get_string('defaultname', 'qformat_blackboard_six' , $quest->id);
+        }
+        $question->generalfeedback = '';
+        $question->generalfeedbackformat = FORMAT_HTML;
+        $question->generalfeedbackfiles = array();
+
+        return $question;
+    }
+
+