Merge branch 'MDL-33572' of git://github.com/netspotau/moodle-mod_assign
authorDan Poltawski <dan@moodle.com>
Tue, 12 Jun 2012 05:38:34 +0000 (13:38 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 12 Jun 2012 05:38:34 +0000 (13:38 +0800)
70 files changed:
backup/cc/cc2moodle.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/restore_controller_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/dbops/tests/dbops_test.php
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/backup_helper.class.php
course/renderer.php
course/view.php
course/yui/modchooser/modchooser.js
files/externallib.php
install/lang/es_mx/admin.php
install/lang/sv_fi/install.php [new file with mode: 0644]
lib/cronlib.php
lib/db/log.php
lib/filestorage/stored_file.php
lib/filestorage/tests/file_storage_test.php
lib/googleapi.php
lib/moodlelib.php
lib/navigationlib.php
lib/phpunit/bootstraplib.php
lib/pluginlib.php
lib/setup.php
lib/setuplib.php
lib/yui/chooserdialogue/chooserdialogue.js
local/readme.txt
mod/assign/feedback/comments/db/install.xml
mod/assign/feedback/file/db/install.xml
mod/assign/lib.php
mod/assign/module.js
mod/assignment/lib.php
mod/data/field/file/mod.html
mod/data/field/picture/mod.html
mod/data/lib.php
mod/data/locallib.php
mod/folder/lib.php
mod/glossary/formats/entrylist/entrylist_format.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/locallib.php
mod/glossary/styles.css
mod/page/lib.php
mod/quiz/comment.php
mod/quiz/cronlib.php
mod/quiz/edit.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/mod_form.php
mod/quiz/processattempt.php
mod/quiz/report/grading/report.php
mod/quiz/summary.php
mod/resource/lib.php
mod/resource/pix/icon.gif
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/url/lib.php
question/behaviour/behaviourbase.php
question/previewlib.php
question/type/multianswer/renderer.php
repository/draftfiles_manager.php
repository/equella/callback.php [new file with mode: 0644]
repository/equella/db/access.php [new file with mode: 0644]
repository/equella/lang/en/repository_equella.php [new file with mode: 0644]
repository/equella/lib.php [new file with mode: 0644]
repository/equella/pix/icon.png [new file with mode: 0644]
repository/equella/version.php [new file with mode: 0644]
repository/filepicker.php
theme/base/style/core.css
version.php

index cf73d77..013d891 100644 (file)
@@ -71,6 +71,11 @@ class cc2moodle {
             return false;
         }
 
+        // Before iterate over directories, try to find one manifest at top level
+        if (file_exists($folder . '/imsmanifest.xml')) {
+            return $folder . '/imsmanifest.xml';
+        }
+
         $result = false;
         try {
             $dirIter = new RecursiveDirectoryIterator($folder, RecursiveDirectoryIterator::KEY_AS_PATHNAME);
index 377011e..3f627bf 100644 (file)
@@ -135,7 +135,7 @@ class restore_final_task extends restore_task {
         $rules[] = new restore_log_rule('course', 'report outline', 'report/outline/index.php?id={course}', '{course}');
         $rules[] = new restore_log_rule('course', 'report participation', 'report/participation/index.php?id={course}', '{course}');
         $rules[] = new restore_log_rule('course', 'report stats', 'report/stats/index.php?id={course}', '{course}');
-        $rules[] = new restore_log_rule('course', 'view section', 'view.php?id={course}&section={course_sectionnumber}', '{course_section}');
+        $rules[] = new restore_log_rule('course', 'view section', 'view.php?id={course}&sectionid={course_section}', '{course_section}');
 
         // module 'user' rules
         $rules[] = new restore_log_rule('user', 'view', 'view.php?id={user}&course={course}', '{user}');
index e6fc1d1..9ee8f5a 100644 (file)
@@ -207,13 +207,13 @@ class restore_gradebook_structure_step extends restore_structure_step {
         global $DB;
 
         $data = (object)$data;
-        $oldid = $data->id;
+        $olduserid = $data->userid;
 
         $data->itemid = $this->get_new_parentid('grade_item');
 
-        $data->userid = $this->get_mappingid('user', $data->userid, NULL);
-        if (!is_null($data->userid)) {
-            $data->usermodified = $this->get_mappingid('user', $data->usermodified, NULL);
+        $data->userid = $this->get_mappingid('user', $data->userid, null);
+        if (!empty($data->userid)) {
+            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
             $data->locktime     = $this->apply_date_offset($data->locktime);
             // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
             $data->overridden = $this->apply_date_offset($data->overridden);
@@ -222,9 +222,10 @@ class restore_gradebook_structure_step extends restore_structure_step {
 
             $newitemid = $DB->insert_record('grade_grades', $data);
         } else {
-            debugging("Mapped user id not found for grade item id '{$data->itemid}'");
+            debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
         }
     }
+
     protected function process_grade_category($data) {
         global $DB;
 
@@ -1050,7 +1051,6 @@ class restore_section_structure_step extends restore_structure_step {
         global $CFG, $DB;
         $data = (object)$data;
         $oldid = $data->id; // We'll need this later
-        $oldsection = $data->number;
 
         $restorefiles = false;
 
@@ -1103,12 +1103,10 @@ class restore_section_structure_step extends restore_structure_step {
 
             $DB->update_record('course_sections', $section);
             $newitemid = $secrec->id;
-            $oldsection = $secrec->section;
         }
 
         // Annotate the section mapping, with restorefiles option if needed
         $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
-        $this->set_mapping('course_sectionnumber', $oldsection, $section->section);
 
         // set the new course_section id in the task
         $this->task->set_sectionid($newitemid);
@@ -2373,18 +2371,24 @@ class restore_activity_grades_structure_step extends restore_structure_step {
 
     protected function process_grade_grade($data) {
         $data = (object)($data);
-
+        $olduserid = $data->userid;
         unset($data->id);
+
         $data->itemid = $this->get_new_parentid('grade_item');
-        $data->userid = $this->get_mappingid('user', $data->userid);
-        $data->usermodified = $this->get_mappingid('user', $data->usermodified);
-        $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
-        // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
-        $data->overridden = $this->apply_date_offset($data->overridden);
 
-        $grade = new grade_grade($data, false);
-        $grade->insert('restore');
-        // no need to save any grade_grade mapping
+        $data->userid = $this->get_mappingid('user', $data->userid, null);
+        if (!empty($data->userid)) {
+            $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
+            $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
+            // TODO: Ask, all the rest of locktime/exported... work with time... to be rolled?
+            $data->overridden = $this->apply_date_offset($data->overridden);
+
+            $grade = new grade_grade($data, false);
+            $grade->insert('restore');
+            // no need to save any grade_grade mapping
+        } else {
+            debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
+        }
     }
 
     /**
@@ -2560,7 +2564,6 @@ class restore_module_structure_step extends restore_structure_step {
 
         $data = (object)$data;
         $oldid = $data->id;
-        $oldsection = $data->sectionnumber;
         $this->task->set_old_moduleversion($data->version);
 
         $data->course = $this->task->get_courseid();
@@ -2587,7 +2590,6 @@ class restore_module_structure_step extends restore_structure_step {
                 'course' => $this->get_courseid(),
                 'section' => 1);
             $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
-            $this->set_mapping('course_sectionnumber', $oldsection, 1); // Assign unmatching sections to section 1.
         }
         $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
         if (!$CFG->enablegroupmembersonly) {                                         // observe groupsmemberonly
index 0b47c5f..94674e7 100644 (file)
@@ -125,5 +125,7 @@ abstract class restore_controller_dbops extends restore_dbops {
             $table = new xmldb_table($targettablename);
             $dbman->drop_table($table); // And drop it
         }
+        // Invalidate the backup_ids caches.
+        restore_dbops::reset_backup_ids_cached();
     }
 }
index 81dbaf6..4254e69 100644 (file)
@@ -312,6 +312,31 @@ abstract class restore_dbops {
         }
     }
 
+    /**
+     * Reset the ids caches completely
+     *
+     * Any destructive operation (partial delete, truncate, drop or recreate) performed
+     * with the backup_ids table must cause the backup_ids caches to be
+     * invalidated by calling this method. See MDL-33630.
+     *
+     * Note that right now, the only operation of that type is the recreation
+     * (drop & restore) of the table that may happen once the prechecks have ended. All
+     * the rest of operations are always routed via {@link set_backup_ids_record()}, 1 by 1,
+     * keeping the caches on sync.
+     *
+     * @todo MDL-25290 static should be replaced with MUC code.
+     */
+    public static function reset_backup_ids_cached() {
+        // Reset the ids cache.
+        $cachetoadd = count(self::$backupidscache);
+        self::$backupidscache = array();
+        self::$backupidscachesize = self::$backupidscachesize + $cachetoadd;
+        // Reset the exists cache.
+        $existstoadd = count(self::$backupidsexist);
+        self::$backupidsexist = array();
+        self::$backupidsexistsize = self::$backupidsexistsize + $existstoadd;
+    }
+
     /**
      * Given one role, as loaded from XML, perform the best possible matching against the assignable
      * roles, using different fallback alternatives (shortname, archetype, editingteacher => teacher, defaultcourseroleid)
index dc02ad9..e088084 100644 (file)
@@ -26,10 +26,104 @@ defined('MOODLE_INTERNAL') || die();
 // Include all the needed stuff
 global $CFG;
 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
 
+/**
+ * Restore dbops tests (all).
+ */
+class restore_dbops_testcase extends advanced_testcase {
+
+    /**
+     * Verify the xxx_ids_cached (in-memory backup_ids cache) stuff works as expected.
+     *
+     * Note that those private implementations are tested here by using the public
+     * backup_ids API and later performing low-level tests.
+     */
+    public function test_backup_ids_cached() {
+        global $DB;
+        $dbman = $DB->get_manager(); // We are going to use database_manager services.
+
+        $this->resetAfterTest(true); // Playing with temp tables, better reset once finished.
+
+        // Some variables and objects for testing.
+        $restoreid = 'testrestoreid';
+
+        $mapping = new stdClass();
+        $mapping->itemname = 'user';
+        $mapping->itemid = 1;
+        $mapping->newitemid = 2;
+        $mapping->parentitemid = 3;
+        $mapping->info = 'info';
+
+        // Create the backup_ids temp tables used by restore.
+        restore_controller_dbops::create_restore_temp_tables($restoreid);
+
+        // Send one mapping using the public api with defaults.
+        restore_dbops::set_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
+        // Get that mapping and verify everything is returned as expected.
+        $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
+        $this->assertSame($mapping->itemname, $result->itemname);
+        $this->assertSame($mapping->itemid, $result->itemid);
+        $this->assertSame(0, $result->newitemid);
+        $this->assertSame(null, $result->parentitemid);
+        $this->assertSame(null, $result->info);
+
+        // Drop the backup_xxx_temp temptables manually, so memory cache won't be invalidated.
+        $dbman->drop_table(new xmldb_table('backup_ids_temp'));
+        $dbman->drop_table(new xmldb_table('backup_files_temp'));
+
+        // Verify the mapping continues returning the same info,
+        // now from cache (the table does not exist).
+        $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
+        $this->assertSame($mapping->itemname, $result->itemname);
+        $this->assertSame($mapping->itemid, $result->itemid);
+        $this->assertSame(0, $result->newitemid);
+        $this->assertSame(null, $result->parentitemid);
+        $this->assertSame(null, $result->info);
 
-/*
- * dbops tests (all)
+        // Recreate the temp table, just to drop it using the restore API in
+        // order to check that, then, the cache becomes invalid for the same request.
+        restore_controller_dbops::create_restore_temp_tables($restoreid);
+        restore_controller_dbops::drop_restore_temp_tables($restoreid);
+
+        // No cached info anymore, so the mapping request will arrive to
+        // DB leading to error (temp table does not exist).
+        try {
+            $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
+            $this->fail('Expecting an exception, none occurred');
+        } catch (Exception $e) {
+            $this->assertTrue($e instanceof dml_exception);
+            $this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
+        }
+
+        // Create the backup_ids temp tables once more.
+        restore_controller_dbops::create_restore_temp_tables($restoreid);
+
+        // Send one mapping using the public api with complete values.
+        restore_dbops::set_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid,
+                $mapping->newitemid, $mapping->parentitemid, $mapping->info);
+        // Get that mapping and verify everything is returned as expected.
+        $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
+        $this->assertSame($mapping->itemname, $result->itemname);
+        $this->assertSame($mapping->itemid, $result->itemid);
+        $this->assertSame($mapping->newitemid, $result->newitemid);
+        $this->assertSame($mapping->parentitemid, $result->parentitemid);
+        $this->assertSame($mapping->info, $result->info);
+
+        // Finally, drop the temp tables properly and get the DB error again (memory caches empty).
+        restore_controller_dbops::drop_restore_temp_tables($restoreid);
+        try {
+            $result = restore_dbops::get_backup_ids_record($restoreid, $mapping->itemname, $mapping->itemid);
+            $this->fail('Expecting an exception, none occurred');
+        } catch (Exception $e) {
+            $this->assertTrue($e instanceof dml_exception);
+            $this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
+        }
+    }
+}
+
+/**
+ * Backup dbops tests (all).
  */
 class backup_dbops_testcase extends advanced_testcase {
 
index 94a7a87..65e56ee 100644 (file)
@@ -350,13 +350,13 @@ abstract class backup_cron_automated_helper {
 
             $bc->execute_plan();
             $results = $bc->get_results();
-            $file = $results['backup_destination'];
+            $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;
             if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
                 $dir = null;
             }
-            if (!empty($dir) && $storage !== 0) {
+            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) {
@@ -461,6 +461,11 @@ abstract class backup_cron_automated_helper {
         $storage =  $config->backup_auto_storage;
         $dir =      $config->backup_auto_destination;
 
+        if ($keep == 0) {
+            // means keep all backup files
+            return true;
+        }
+
         $backupword = str_replace(' ', '_', textlib::strtolower(get_string('backupfilename')));
         $backupword = trim(clean_filename($backupword), '_');
 
index 67bd78e..5904751 100644 (file)
@@ -176,8 +176,17 @@ abstract class backup_helper {
     /**
      * Given one backupid and the (FS) final generated file, perform its final storage
      * into Moodle file storage. For stored files it returns the complete file_info object
+     *
+     * Note: the $filepath is deleted if the backup file is created successfully
+     *
+     * @param int $backupid
+     * @param string $filepath zip file containing the backup
+     * @return stored_file if created, null otherwise
+     *
+     * @throws moodle_exception in case of any problems
      */
     static public function store_backup_file($backupid, $filepath) {
+        global $CFG;
 
         // First of all, get some information from the backup_controller to help us decide
         list($dinfo, $cinfo, $sinfo) = backup_controller_dbops::get_moodle_backup_information($backupid);
@@ -191,6 +200,7 @@ abstract class backup_helper {
         $userid    = $dinfo[0]->userid;                // User->id executing the backup
         $id        = $dinfo[0]->id;                    // Id of activity/section/course (depends of type)
         $courseid  = $dinfo[0]->courseid;              // Id of the course
+        $format    = $dinfo[0]->format;                // Type of backup file
 
         // Quick hack. If for any reason, filename is blank, fix it here.
         // TODO: This hack will be out once MDL-22142 - P26 gets fixed
@@ -200,7 +210,13 @@ abstract class backup_helper {
 
         // Backups of type IMPORT aren't stored ever
         if ($backupmode == backup::MODE_IMPORT) {
-            return false;
+            return null;
+        }
+
+        if (!is_readable($filepath)) {
+            // we have a problem if zip file does not exist
+            throw new coding_exception('backup_helper::store_backup_file() expects valid $filepath parameter');
+
         }
 
         // Calculate file storage options of id being backup
@@ -232,6 +248,25 @@ abstract class backup_helper {
         if ($backupmode == backup::MODE_AUTOMATED) {
             // Automated backups have there own special area!
             $filearea  = 'automated';
+
+            // If we're keeping the backup only in a chosen path, just move it there now
+            // this saves copying from filepool to here later and filling trashdir.
+            $config = get_config('backup');
+            $dir = $config->backup_auto_destination;
+            if ($config->backup_auto_storage == 1 and $dir and is_dir($dir) and is_writable($dir)) {
+                $filedest = $dir.'/'.backup_plan_dbops::get_default_backup_filename($format, $backuptype, $courseid, $hasusers, $isannon, !$config->backup_shortname);
+                // first try to move the file, if it is not possible copy and delete instead
+                if (@rename($filepath, $filedest)) {
+                    return null;
+                }
+                umask(0000);
+                if (copy($filepath, $filedest)) {
+                    @chmod($filedest, $CFG->filepermissions); // may fail because the permissions may not make sense outside of dataroot
+                    unlink($filepath);
+                    return null;
+                }
+                // bad luck, try to deal with the file the old way - keep backup in file area if we can not copy to ext system
+            }
         }
 
         // Backups of type HUB (by definition never have user info)
@@ -275,7 +310,9 @@ abstract class backup_helper {
             $sf = $fs->get_file_by_hash($pathnamehash);
             $sf->delete();
         }
-        return $fs->create_file_from_pathname($fr, $filepath);
+        $file = $fs->create_file_from_pathname($fr, $filepath);
+        unlink($filepath);
+        return $file;
     }
 
     /**
index 2831fed..22e5e2c 100644 (file)
@@ -168,7 +168,7 @@ class core_course_renderer extends plugin_renderer_base {
 
         // Add the header
         $header = html_writer::tag('div', get_string('addresourceoractivity', 'moodle'),
-                array('id' => 'choosertitle', 'class' => 'hd'));
+                array('class' => 'hd choosertitle'));
 
         $formcontent = html_writer::start_tag('form', array('action' => new moodle_url('/course/jumpto.php'),
                 'id' => 'chooserform', 'method' => 'post'));
@@ -221,8 +221,8 @@ class core_course_renderer extends plugin_renderer_base {
         // Put all of the content together
         $content = $formcontent;
 
-        $content = html_writer::tag('div', $content, array('id' => 'choosercontainer'));
-        return $header . html_writer::tag('div', $content, array('id' => 'chooserdialogue'));
+        $content = html_writer::tag('div', $content, array('class' => 'choosercontainer'));
+        return $header . html_writer::tag('div', $content, array('class' => 'chooserdialoguebody'));
     }
 
     /**
index 2b6a908..9667832 100644 (file)
     $loglabel = 'view';
     $infoid = $course->id;
     if(!empty($section)) {
-        $logparam .= '&section='. $section;
         $loglabel = 'view section';
         $sectionparams = array('course' => $course->id, 'section' => $section);
-        if ($coursesections = $DB->get_record('course_sections', $sectionparams, 'id', MUST_EXIST)) {
-            $infoid = $coursesections->id;
-    }
+        $coursesections = $DB->get_record('course_sections', $sectionparams, 'id', MUST_EXIST);
+        $infoid = $coursesections->id;
+        $logparam .= '&sectionid='. $infoid;
     }
     add_to_log($course->id, 'course', $loglabel, "view.php?". $logparam, $infoid);
 
index 5d95a02..4a691a2 100644 (file)
@@ -21,13 +21,12 @@ YUI.add('moodle-course-modchooser', function(Y) {
         jumplink : null,
 
         initializer : function(config) {
-            var dialogue = Y.one('#chooserdialogue');
-            var header = Y.one('#choosertitle');
+            var dialogue = Y.one('.chooserdialoguebody');
+            var header = Y.one('.choosertitle');
             var params = {
                 width: '540px'
             };
             this.setup_chooser_dialogue(dialogue, header, params);
-            this.overlay.get('boundingBox').addClass('modchooser');
 
             this.jumplink = this.container.one('#jump');
 
index ec16724..f6001d2 100644 (file)
@@ -52,7 +52,8 @@ class core_files_external extends external_api {
                 'filearea'  => new external_value(PARAM_TEXT, 'file area'),
                 'itemid'    => new external_value(PARAM_INT, 'associated id'),
                 'filepath'  => new external_value(PARAM_PATH, 'file path'),
-                'filename'  => new external_value(PARAM_FILE, 'file name')
+                'filename'  => new external_value(PARAM_FILE, 'file name'),
+                'modified' => new external_value(PARAM_INT, 'timestamp to return files changed after this time.', VALUE_DEFAULT, null)
             )
         );
     }
@@ -66,12 +67,15 @@ class core_files_external extends external_api {
      * @param int $itemid item id
      * @param string $filepath file path
      * @param string $filename file name
+     * @param int $modified timestamp to return files changed after this time.
      * @return array
      * @since Moodle 2.2
      */
-    public static function get_files($contextid, $component, $filearea, $itemid, $filepath, $filename) {
+    public static function get_files($contextid, $component, $filearea, $itemid, $filepath, $filename, $modified = null) {
         global $CFG, $USER, $OUTPUT;
-        $fileinfo = self::validate_parameters(self::get_files_parameters(), array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'filename'=>$filename));
+        $fileinfo = self::validate_parameters(self::get_files_parameters(), array(
+                    'contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea,
+                    'itemid'=>$itemid, 'filepath'=>$filepath, 'filename'=>$filename, 'modified'=>$modified));
 
         $browser = get_file_browser();
 
@@ -99,7 +103,10 @@ class core_files_external extends external_api {
         $return = array();
         $return['parents'] = array();
         $return['files'] = array();
-        if ($file = $browser->get_file_info($context, $fileinfo['component'], $fileinfo['filearea'], $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename'])) {
+        $list = array();
+        if ($file = $browser->get_file_info(
+            $context, $fileinfo['component'], $fileinfo['filearea'], $fileinfo['itemid'],
+                $fileinfo['filepath'], $fileinfo['filename'])) {
             $level = $file->get_parent();
             while ($level) {
                 $params = $level->get_params();
@@ -107,36 +114,42 @@ class core_files_external extends external_api {
                 array_unshift($return['parents'], $params);
                 $level = $level->get_parent();
             }
-            $list = array();
             $children = $file->get_children();
             foreach ($children as $child) {
 
                 $params = $child->get_params();
+                $timemodified = $child->get_timemodified();
 
                 if ($child->is_directory()) {
-                    $node = array(
-                        'contextid' => $params['contextid'],
-                        'component' => $params['component'],
-                        'filearea'  => $params['filearea'],
-                        'itemid'    => $params['itemid'],
-                        'filepath'  => $params['filepath'],
-                        'filename'  => $child->get_visible_name(),
-                        'url'       => null,
-                        'isdir'     => true
-                    );
-                    $list[] = $node;
+                    if ((is_null($modified)) or ($modified < $timemodified)) {
+                        $node = array(
+                            'contextid' => $params['contextid'],
+                            'component' => $params['component'],
+                            'filearea'  => $params['filearea'],
+                            'itemid'    => $params['itemid'],
+                            'filepath'  => $params['filepath'],
+                            'filename'  => $child->get_visible_name(),
+                            'url'       => null,
+                            'isdir'     => true,
+                            'timemodified' => $timemodified
+                           );
+                           $list[] = $node;
+                    }
                 } else {
-                    $node = array(
-                        'contextid' => $params['contextid'],
-                        'component' => $params['component'],
-                        'filearea'  => $params['filearea'],
-                        'itemid'    => $params['itemid'],
-                        'filepath'  => $params['filepath'],
-                        'filename'  => $child->get_visible_name(),
-                        'url'       => $child->get_url(),
-                        'isdir'     => false
-                    );
-                    $list[] = $node;
+                    if ((is_null($modified)) or ($modified < $timemodified)) {
+                        $node = array(
+                            'contextid' => $params['contextid'],
+                            'component' => $params['component'],
+                            'filearea'  => $params['filearea'],
+                            'itemid'    => $params['itemid'],
+                            'filepath'  => $params['filepath'],
+                            'filename'  => $child->get_visible_name(),
+                            'url'       => $child->get_url(),
+                            'isdir'     => false,
+                            'timemodified' => $timemodified
+                        );
+                           $list[] = $node;
+                    }
                 }
             }
         }
@@ -176,6 +189,7 @@ class core_files_external extends external_api {
                             'filename' => new external_value(PARAM_FILE, ''),
                             'isdir'    => new external_value(PARAM_BOOL, ''),
                             'url'      => new external_value(PARAM_TEXT, ''),
+                            'timemodified' => new external_value(PARAM_INT, ''),
                         )
                     )
                 )
@@ -219,12 +233,14 @@ class core_files_external extends external_api {
     public static function upload($contextid, $component, $filearea, $itemid, $filepath, $filename, $filecontent) {
         global $USER, $CFG;
 
-        $fileinfo = self::validate_parameters(self::upload_parameters(), array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'filename'=>$filename, 'filecontent'=>$filecontent));
+        $fileinfo = self::validate_parameters(self::upload_parameters(), array(
+            'contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid,
+            'filepath'=>$filepath, 'filename'=>$filename, 'filecontent'=>$filecontent));
 
         if (!isset($fileinfo['filecontent'])) {
             throw new moodle_exception('nofile');
         }
-        // saving file
+        // Saving file.
         $dir = make_temp_directory('wsupload');
 
         if (empty($fileinfo['filename'])) {
@@ -239,7 +255,6 @@ class core_files_external extends external_api {
             $savedfilepath = $dir.$filename;
         }
 
-
         file_put_contents($savedfilepath, base64_decode($fileinfo['filecontent']));
         unset($fileinfo['filecontent']);
 
@@ -250,7 +265,7 @@ class core_files_external extends external_api {
         }
 
         if (isset($fileinfo['itemid'])) {
-            // TODO MDL-31116 in user private area, itemid is always 0
+            // TODO MDL-31116 in user private area, itemid is always 0.
             $itemid = 0;
         } else {
             throw new coding_exception('itemid cannot be empty');
@@ -265,19 +280,19 @@ class core_files_external extends external_api {
         if (!($fileinfo['component'] == 'user' and $fileinfo['filearea'] == 'private')) {
             throw new coding_exception('File can be uploaded to user private area only');
         } else {
-            // TODO MDL-31116 hard-coded to use user_private area
+            // TODO MDL-31116 hard-coded to use user_private area.
             $component = 'user';
             $filearea = 'private';
         }
 
         $browser = get_file_browser();
 
-        // check existing file
+        // Check existing file.
         if ($file = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename)) {
             throw new moodle_exception('fileexist');
         }
 
-        // move file to filepool
+        // Move file to filepool.
         if ($dir = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, '.')) {
             $info = $dir->create_file_from_pathname($filename, $savedfilepath);
             $params = $info->get_params();
index c50bbe7..350e39c 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['clianswerno'] = 'n';
+$string['cliansweryes'] = 's';
+$string['cliincorrectvalueerror'] = 'Error, valor incorrecto  "{$a->value}" para "{$a->option}"';
+$string['cliincorrectvalueretry'] = 'Valor incorrecto, por favor, inténtelo de nuevo';
+$string['clitypevalue'] = 'valor del tipo';
+$string['clitypevaluedefault'] = 'valor del tipo, pulse Enter para utilizar el valor por defecto ({$a})';
+$string['cliunknowoption'] = 'Opciones no reconocidas:
+{$a}
+Por favor, utilice la opción Ayuda.';
+$string['cliyesnoprompt'] = 'escriba s (sí) o n (no)';
 $string['environmentrequireinstall'] = 'debe estar instalado y activado';
+$string['environmentrequireversion'] = 'versión {$a->needed} es obligatoria y está ejecutando {$a->current}';
diff --git a/install/lang/sv_fi/install.php b/install/lang/sv_fi/install.php
new file mode 100644 (file)
index 0000000..2b93e10
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle 2.3dev installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['databasename'] = 'Databasens namn';
index b79057d..b034760 100644 (file)
@@ -711,11 +711,11 @@ function notify_login_failures() {
             //emailing the admins directly rather than putting these through the messaging system
             email_to_user($admin,get_admin(), $subject, $body);
         }
-
-        // Update lastnotifyfailure with current time
-        set_config('lastnotifyfailure', time());
     }
 
+    // Update lastnotifyfailure with current time
+    set_config('lastnotifyfailure', time());
+
     // Finally, delete all the temp records we have created in cache_flags
     $DB->delete_records_select('cache_flags', "flagtype IN ('login_failure_by_ip', 'login_failure_by_info')");
 
index f0475f6..94abc81 100644 (file)
@@ -38,7 +38,7 @@ global $DB; // TODO: this is a hack, we should really do something with the SQL
 $logs = array(
     array('module'=>'course', 'action'=>'user report', 'mtable'=>'user', 'field'=>$DB->sql_concat('firstname', "' '" , 'lastname')),
     array('module'=>'course', 'action'=>'view', 'mtable'=>'course', 'field'=>'fullname'),
-    array('module'=>'course', 'action'=>'view section', 'mtable'=>'course_sections', 'field'=>'COALESCE(name, section)'),
+    array('module'=>'course', 'action'=>'view section', 'mtable'=>'course_sections', 'field'=>'name'),
     array('module'=>'course', 'action'=>'update', 'mtable'=>'course', 'field'=>'fullname'),
     array('module'=>'course', 'action'=>'enrol', 'mtable'=>'course', 'field'=>'fullname'), // there should be some way to store user id of the enrolled user!
     array('module'=>'course', 'action'=>'unenrol', 'mtable'=>'course', 'field'=>'fullname'), // there should be some way to store user id of the enrolled user!
index 75ab483..4b7bf09 100644 (file)
@@ -142,8 +142,10 @@ class stored_file {
                     }
                 }
 
-                if ($field == 'referencefileid' or $field == 'referencelastsync' or $field == 'referencelifetime') {
-                    $value = clean_param($value, PARAM_INT);
+                if ($field === 'referencefileid' or $field === 'referencelastsync' or $field === 'referencelifetime') {
+                    if (!is_null($value) and !is_number($value)) {
+                        throw new file_exception('storedfileproblem', 'Invalid reference info');
+                    }
                 }
 
                 // adding the field
@@ -196,36 +198,46 @@ class stored_file {
     }
 
     /**
-     * Delete file reference
+     * Unlink the stored file from the referenced file
      *
+     * This methods destroys the link to the record in files_reference table. This effectively
+     * turns the stored file from being an alias to a plain copy. However, the caller has
+     * to make sure that the actual file's content has beed synced prior to calling this method.
      */
     public function delete_reference() {
         global $DB;
 
-        // Remove repository info.
-        $this->repository = null;
+        if (!$this->is_external_file()) {
+            throw new coding_exception('An attempt to unlink a non-reference file.');
+        }
 
         $transaction = $DB->start_delegated_transaction();
 
-        // Remove reference info from DB.
-        $DB->delete_records('files_reference', array('id'=>$this->file_record->referencefileid));
+        // Are we the only one referring to the original file? If so, delete the
+        // referenced file record. Note we do not use file_storage::search_references_count()
+        // here because we want to count draft files too and we are at a bit lower access level here.
+        $countlinks = $DB->count_records('files',
+            array('referencefileid' => $this->file_record->referencefileid));
+        if ($countlinks == 1) {
+            $DB->delete_records('files_reference', array('id' => $this->file_record->referencefileid));
+        }
 
-        // Must refresh $this->file_record form DB
-        $filerecord = $DB->get_record('files', array('id'=>$this->get_id()));
-        // Update DB
-        $filerecord->referencelastsync = null;
-        $filerecord->referencelifetime = null;
-        $filerecord->referencefileid = null;
-        $this->update($filerecord);
+        // 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();
 
-        // unset object variable
-        unset($this->file_record->repositoryid);
-        unset($this->file_record->reference);
-        unset($this->file_record->referencelastsync);
-        unset($this->file_record->referencelifetime);
-        unset($this->file_record->referencefileid);
+        // Update our properties and the record in the memory.
+        $this->repository = null;
+        $this->file_record->repositoryid = null;
+        $this->file_record->reference = null;
+        $this->file_record->referencefileid = null;
+        $this->file_record->referencelastsync = null;
+        $this->file_record->referencelifetime = null;
     }
 
     /**
@@ -254,15 +266,20 @@ class stored_file {
 
         $transaction = $DB->start_delegated_transaction();
 
-        // If other files referring to this file, we need convert them
+        // If there are other files referring to this file, convert them to copies.
         if ($files = $this->fs->get_references_by_storedfile($this)) {
             foreach ($files as $file) {
                 $this->fs->import_external_file($file);
             }
         }
-        // Now delete file records in DB
+
+        // If this file is a reference (alias) to another file, unlink it first.
+        if ($this->is_external_file()) {
+            $this->delete_reference();
+        }
+
+        // Now delete the file record.
         $DB->delete_records('files', array('id'=>$this->file_record->id));
-        $DB->delete_records('files_reference', array('id'=>$this->file_record->referencefileid));
 
         $transaction->allow_commit();
 
index ad3fb49..818d47b 100644 (file)
@@ -1212,4 +1212,90 @@ class filestoragelib_testcase extends advanced_testcase {
         $this->setExpectedException('stored_file_creation_exception');
         $file2 = $fs->create_file_from_pathname($filerecord, $path);
     }
+
+    /**
+     * Calling stored_file::delete_reference() on a non-reference file throws coding_exception
+     */
+    public function test_delete_reference_on_nonreference() {
+
+        $this->resetAfterTest(true);
+        $user = $this->setup_three_private_files();
+        $fs = get_file_storage();
+        $repos = repository::get_instances(array('type'=>'user'));
+        $repo = reset($repos);
+
+        $file = null;
+        foreach ($fs->get_area_files($user->ctxid, 'user', 'private') as $areafile) {
+            if (!$areafile->is_directory()) {
+                $file = $areafile;
+                break;
+            }
+        }
+        $this->assertInstanceOf('stored_file', $file);
+        $this->assertFalse($file->is_external_file());
+
+        $this->setExpectedException('coding_exception');
+        $file->delete_reference();
+    }
+
+    /**
+     * Calling stored_file::delete_reference() on a reference file does not affect other
+     * symlinks to the same original
+     */
+    public function test_delete_reference_one_symlink_does_not_rule_them_all() {
+
+        $this->resetAfterTest(true);
+        $user = $this->setup_three_private_files();
+        $fs = get_file_storage();
+        $repos = repository::get_instances(array('type'=>'user'));
+        $repo = reset($repos);
+
+        // create two aliases linking the same original
+
+        $originalfile = null;
+        foreach ($fs->get_area_files($user->ctxid, 'user', 'private') as $areafile) {
+            if (!$areafile->is_directory()) {
+                $originalfile = $areafile;
+                break;
+            }
+        }
+        $this->assertInstanceOf('stored_file', $originalfile);
+
+        // calling delete_reference() on a non-reference file
+
+        $originalrecord = array(
+            'contextid' => $originalfile->get_contextid(),
+            'component' => $originalfile->get_component(),
+            'filearea'  => $originalfile->get_filearea(),
+            'itemid'    => $originalfile->get_itemid(),
+            'filepath'  => $originalfile->get_filepath(),
+            'filename'  => $originalfile->get_filename(),
+        );
+
+        $aliasrecord = $this->generate_file_record();
+        $aliasrecord->filepath = '/A/';
+        $aliasrecord->filename = 'symlink.txt';
+
+        $ref = $fs->pack_reference($originalrecord);
+        $aliasfile1 = $fs->create_file_from_reference($aliasrecord, $repo->id, $ref);
+
+        $aliasrecord->filepath = '/B/';
+        $aliasrecord->filename = 'symlink.txt';
+        $ref = $fs->pack_reference($originalrecord);
+        $aliasfile2 = $fs->create_file_from_reference($aliasrecord, $repo->id, $ref);
+
+        // refetch A/symlink.txt
+        $symlink1 = $fs->get_file($aliasrecord->contextid, $aliasrecord->component,
+            $aliasrecord->filearea, $aliasrecord->itemid, '/A/', 'symlink.txt');
+        $this->assertTrue($symlink1->is_external_file());
+
+        // unlink the A/symlink.txt
+        $symlink1->delete_reference();
+        $this->assertFalse($symlink1->is_external_file());
+
+        // make sure that B/symlink.txt has not been affected
+        $symlink2 = $fs->get_file($aliasrecord->contextid, $aliasrecord->component,
+            $aliasrecord->filearea, $aliasrecord->itemid, '/B/', 'symlink.txt');
+        $this->assertTrue($symlink2->is_external_file());
+    }
 }
index 5994dd3..fe2b933 100644 (file)
@@ -56,6 +56,15 @@ class google_docs {
      */
     public function __construct(google_oauth $googleoauth) {
         $this->googleoauth = $googleoauth;
+        $this->reset_curl_state();
+    }
+
+    /**
+     * Resets state on oauth curl object and set GData protocol
+     * version
+     */
+    private function reset_curl_state() {
+        $this->googleoauth->reset_state();
         $this->googleoauth->setHeader('GData-Version: 3.0');
     }
 
@@ -147,7 +156,7 @@ class google_docs {
         }
 
         // Reset the curl object for actually sending the file.
-        $this->googleoauth->clear_headers();
+        $this->reset_curl_state();
         $this->googleoauth->setHeader("Content-Length: ". $file->get_filesize());
         $this->googleoauth->setHeader("Content-Type: ". $file->get_mimetype());
 
@@ -163,6 +172,8 @@ class google_docs {
         unlink($tmpfilepath);
 
         if ($this->googleoauth->info['http_code'] === 201) {
+            // Clear headers for further requests.
+            $this->reset_curl_state();
             return true;
         } else {
             return false;
@@ -398,9 +409,10 @@ class google_oauth extends oauth2_client {
     }
 
     /**
-     * Clear any headers in the curl object
+     * Resets headers and response for multiple requests
      */
-    public function clear_headers() {
+    public function reset_state() {
         $this->header = array();
+        $this->response = array();
     }
 }
index 91cb5f4..266a67d 100644 (file)
@@ -132,6 +132,14 @@ define('PARAM_FILE',   'file');
 
 /**
  * PARAM_FLOAT - a real/floating point number.
+ *
+ * Note that you should not use PARAM_FLOAT for numbers typed in by the user.
+ * It does not work for languages that use , as a decimal separator.
+ * Instead, do something like
+ *     $rawvalue = required_param('name', PARAM_RAW);
+ *     // ... other code including require_login, which sets current lang ...
+ *     $realvalue = unformat_float($rawvalue);
+ *     // ... then use $realvalue
  */
 define('PARAM_FLOAT',  'float');
 
index eaf5218..54ab6cc 100644 (file)
@@ -3308,6 +3308,9 @@ class settings_navigation extends navigation_node {
             $this->add(get_string('returntooriginaluser', 'moodle', fullname($realuser, true)), $url, self::TYPE_SETTING, null, null, new pix_icon('t/left', ''));
         }
 
+        // At this point we give any local plugins the ability to extend/tinker with the navigation settings.
+        $this->load_local_plugin_settings();
+
         foreach ($this->children as $key=>$node) {
             if ($node->nodetype != self::NODETYPE_BRANCH || $node->children->count()===0) {
                 $node->remove();
@@ -4392,6 +4395,17 @@ class settings_navigation extends navigation_node {
         return $frontpage;
     }
 
+    /**
+     * This function gives local plugins an opportunity to modify the settings navigation.
+     */
+    protected function load_local_plugin_settings() {
+        // Get all local plugins with an extend_settings_navigation function in their lib.php file
+        foreach (get_plugin_list_with_function('local', 'extends_settings_navigation') as $function) {
+            // Call each function providing this (the settings navigation) and the current context.
+            $function($this, $this->context);
+        }
+    }
+
     /**
      * This function marks the cache as volatile so it is cleared during shutdown
      */
index 3c191dc..d28de8d 100644 (file)
@@ -63,10 +63,12 @@ function phpunit_bootstrap_error($errorcode, $text = '') {
             $text = "Moodle PHPUnit environment configuration warning:\n".$text;
             break;
         case PHPUNIT_EXITCODE_INSTALL:
-            $text = "Moodle PHPUnit environment is not initialised, please use:\n php admin/tool/phpunit/cli/init.php";
+            $path = phpunit_bootstrap_cli_argument_path('/admin/tool/phpunit/cli/init.php');
+            $text = "Moodle PHPUnit environment is not initialised, please use:\n php $path";
             break;
         case PHPUNIT_EXITCODE_REINSTALL:
-            $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php admin/tool/phpunit/cli/init.php";
+            $path = phpunit_bootstrap_cli_argument_path('/admin/tool/phpunit/cli/init.php');
+            $text = "Moodle PHPUnit environment was initialised for different version, please use:\n php $path";
             break;
         default:
             $text = empty($text) ? '' : ': '.$text;
@@ -79,6 +81,32 @@ function phpunit_bootstrap_error($errorcode, $text = '') {
     exit($errorcode);
 }
 
+/**
+ * Returns relative path against current working directory,
+ * to be used for shell execution hints.
+ * @param string $moodlepath starting with "/", ex: "/admin/tool/cli/init.php"
+ * @return string path relative to current directory or absolute path
+ */
+function phpunit_bootstrap_cli_argument_path($moodlepath) {
+    global $CFG;
+
+    if (isset($CFG->admin) and $CFG->admin !== 'admin') {
+        $moodlepath = preg_replace('|^/admin/|', "/$CFG->admin/", $moodlepath);
+    }
+
+    $cwd = getcwd();
+    if (substr($cwd, -1) !== DIRECTORY_SEPARATOR) {
+        $cwd .= DIRECTORY_SEPARATOR;
+    }
+    $path = realpath($CFG->dirroot.$moodlepath);
+
+    if (strpos($path, $cwd) === 0) {
+        return substr($path, strlen($cwd));
+    }
+
+    return $path;
+}
+
 /**
  * Mark empty dataroot to be used for testing.
  * @param string $dataroot The dataroot directory
index 20f8a34..75a8186 100644 (file)
@@ -506,7 +506,7 @@ class plugin_manager {
             ),
 
             'repository' => array(
-                'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'filesystem',
+                'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
                 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
                 'wikimedia', 'youtube'
index 23cf9e1..74f6520 100644 (file)
@@ -488,8 +488,6 @@ if (PHPUNIT_TEST and !PHPUNIT_UTIL) {
         if ($dbhash) {
             // we ned to reinit if reset fails
             $DB->set_field('config', 'value', 'na', array('name'=>'phpunittest'));
-        } else {
-            throw $e;
         }
     }
     unset($dbhash);
index 2c471f6..4fec92d 100644 (file)
@@ -133,7 +133,7 @@ class moodle_exception extends Exception {
         $this->module    = $module;
         $this->link      = $link;
         $this->a         = $a;
-        $this->debuginfo = $debuginfo;
+        $this->debuginfo = is_null($debuginfo) ? null : (string)$debuginfo;
 
         if (get_string_manager()->string_exists($errorcode, $module)) {
             $message = get_string($errorcode, $module, $a);
@@ -486,7 +486,7 @@ function get_exception_info($ex) {
         $module = 'error';
         $a = $ex->getMessage();
         $link = '';
-        $debuginfo = null;
+        $debuginfo = '';
     }
 
     // Append the error code to the debug info to make grepping and googling easier
index 71c77a3..7f720d2 100644 (file)
@@ -45,8 +45,11 @@ YUI.add('moodle-core-chooserdialogue', function(Y) {
             this.overlay.render();
 
             // Set useful links
-            this.container = this.overlay.get('boundingBox').one('#choosercontainer');
+            this.container = this.overlay.get('boundingBox').one('.choosercontainer');
             this.options = this.container.all('.option input[type=radio]');
+
+            // Add the chooserdialogue class to the container for styling
+            this.overlay.get('boundingBox').addClass('chooserdialogue');
         },
 
         /**
index 9e09311..b7ec6e3 100644 (file)
@@ -205,6 +205,25 @@ You will need to write the /local/nicehack/externallib.php - external functions
 description and code. See some examples from the core files (/user/externallib.php,
 /group/externallib.php...).
 
+Local plugin navigation hooks
+-----------------------------
+There are two functions that your plugin can define that allow it to extend the main
+navigation and the settings navigation.
+These two functions both need to be defined within /local/nicehack/lib.php.
+
+sample code
+<?php
+function nicehack_extends_navigation(global_navigation $nav) {
+    // $nav is the global navigation instance.
+    // Here you can add to and manipulate the navigation structure as you like.
+    // This callback was introduced in 2.0
+}
+function local_nicehack_extends_settings_navigation(settings_navigation $nav, context $context) {
+    // $nav is the settings navigation instance.
+    // $context is the context the settings have been loaded for (settings is context specific)
+    // Here you can add to and manipulate the settings structure as you like.
+    // This callback was introduced in 2.3
+}
 
 Other local customisation files
 ===============================
index 3096e18..9435982 100644 (file)
@@ -15,7 +15,7 @@
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="The unique id for this feedback" NEXT="assignment"/>
         <KEY NAME="assignment" TYPE="foreign" FIELDS="assignment" REFTABLE="assign" REFFIELDS="id" COMMENT="The assignment instance this feedback relates to." PREVIOUS="primary" NEXT="grade"/>
-        <KEY NAME="grade" TYPE="foreign" FIELDS="grade" REFTABLE="assign_grade" REFFIELDS="id" COMMENT="The grade instance this feedback relates to." PREVIOUS="assignment"/>
+        <KEY NAME="grade" TYPE="foreign" FIELDS="grade" REFTABLE="assign_grades" REFFIELDS="id" COMMENT="The grade instance this feedback relates to." PREVIOUS="assignment"/>
       </KEYS>
     </TABLE>
   </TABLES>
index 7aa96c5..aecb0cc 100644 (file)
@@ -14,7 +14,7 @@
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="Unique id for this feedback value." NEXT="assignment"/>
         <KEY NAME="assignment" TYPE="foreign" FIELDS="assignment" REFTABLE="assign" REFFIELDS="id" COMMENT="The assignment instance this feedback relates to." PREVIOUS="primary" NEXT="grade"/>
-        <KEY NAME="grade" TYPE="foreign" FIELDS="grade" REFTABLE="assign_grade" REFFIELDS="id" COMMENT="The grade instance this feedback relates to." PREVIOUS="assignment"/>
+        <KEY NAME="grade" TYPE="foreign" FIELDS="grade" REFTABLE="assign_grades" REFFIELDS="id" COMMENT="The grade instance this feedback relates to." PREVIOUS="assignment"/>
       </KEYS>
     </TABLE>
   </TABLES>
index b9e71a1..caccf10 100644 (file)
@@ -795,7 +795,7 @@ function assign_update_grades($assign, $userid=0, $nullifnone=true) {
  * @param stdClass $context
  * @return array
  */
-function mod_assign_get_file_areas($course, $cm, $context) {
+function assign_get_file_areas($course, $cm, $context) {
     global $CFG;
     require_once($CFG->dirroot . '/mod/assign/locallib.php');
     $areas = array();
@@ -837,7 +837,7 @@ function mod_assign_get_file_areas($course, $cm, $context) {
  * @param string $filename
  * @return object file_info instance or null if not found
  */
-function mod_assign_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
+function assign_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
     global $CFG;
     require_once($CFG->dirroot . '/mod/assign/locallib.php');
 
index d361b32..5786e1e 100644 (file)
@@ -140,9 +140,11 @@ M.mod_assign.init_grading_options = function(Y) {
             Y.one('form.gradingoptionsform').submit();
         });
         var quickgradingelement = Y.one('#id_quickgrading');
-        quickgradingelement.on('change', function(e) {
-            Y.one('form.gradingoptionsform').submit();
-        });
+        if (quickgradingelement) {
+            quickgradingelement.on('change', function(e) {
+                Y.one('form.gradingoptionsform').submit();
+            });
+        }
 
     });
 
index f82005d..8f8dcf3 100644 (file)
@@ -1113,7 +1113,7 @@ class assignment_base {
             }
         }
 
-        $submitform = new mod_assignment_grading_form( null, $mformdata );
+        $submitform = new assignment_grading_form( null, $mformdata );
 
          if (!$display) {
             $ret_data = new stdClass();
@@ -2422,7 +2422,7 @@ class assignment_base {
 } ////// End of the assignment_base class
 
 
-class mod_assignment_grading_form extends moodleform {
+class assignment_grading_form extends moodleform {
     /** @var stores the advaned grading instance (if used in grading) */
     private $advancegradinginstance;
 
@@ -3994,7 +3994,7 @@ function assignment_get_file_areas($course, $cm, $context) {
  * @param string $filename
  * @return file_info_stored file_info_stored instance or null if not found
  */
-function mod_assignment_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
+function assignment_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
     global $CFG, $DB, $USER;
 
     if ($context->contextlevel != CONTEXT_MODULE || $filearea != 'submission') {
index 73815b5..7a67a16 100644 (file)
@@ -12,7 +12,7 @@
             <?php echo get_string('maxsize', 'data'); ?></label></td>
         <td class="c1">
             <?php
-                $course->maxbytes = $DB->get_field('course', 'maxbytes', array('id'=>$this->data->course));
+                $course = $DB->get_record('course', array('id'=>$this->data->course));
                 $choices = get_max_upload_sizes($CFG->maxbytes, $course->maxbytes);
                 echo html_writer::select($choices, 'param3', $this->field->param3, false, array('id' => 'param3'));
             ?>
index 9dd1583..4b32474 100644 (file)
@@ -44,7 +44,7 @@
             <?php echo get_string('maxsize', 'data'); ?></label></td>
         <td class="c1">
             <?php
-                $course->maxbytes = $DB->get_field('course', 'maxbytes', array('id'=>$this->data->course));
+                $course = $DB->get_record('course', array('id'=>$this->data->course));
                 $choices = get_max_upload_sizes($CFG->maxbytes, $course->maxbytes);
                 echo html_writer::select($choices, 'param3', $this->field->param3, false, array('id'=>'param3'));
             ?>
index d21aff3..c23094b 100644 (file)
@@ -2832,7 +2832,7 @@ function data_get_file_areas($course, $cm, $context) {
  * @param string $filename
  * @return file_info_stored file_info_stored instance or null if not found
  */
-function mod_data_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
+function data_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
     global $CFG, $DB;
 
     if ($context->contextlevel != CONTEXT_MODULE) {
index 25f5ed9..68a14e4 100644 (file)
@@ -23,7 +23,7 @@
 
 require_once($CFG->dirroot . '/mod/data/lib.php');
 require_once($CFG->libdir . '/portfolio/caller.php');
-require_once($CFG->libdir . '/filebrowser/file_info.php');
+require_once($CFG->libdir . '/filelib.php');
 
 /**
  * The class to handle entry exports of a database module
index 607e78f..fde1587 100644 (file)
@@ -363,7 +363,7 @@ function folder_export_contents($cm, $baseurl) {
  * Register the ability to handle drag and drop file uploads
  * @return array containing details of the files / types the mod can handle
  */
-function mod_folder_dndupload_register() {
+function folder_dndupload_register() {
     return array('files' => array(
                      array('extension' => 'zip', 'message' => get_string('dnduploadmakefolder', 'mod_folder'))
                  ));
@@ -374,7 +374,7 @@ function mod_folder_dndupload_register() {
  * @param object $uploadinfo details of the file / content that has been uploaded
  * @return int instance id of the newly created mod
  */
-function mod_folder_dndupload_handle($uploadinfo) {
+function folder_dndupload_handle($uploadinfo) {
     global $DB, $USER;
 
     // Gather the required info.
index 4aa37c8..6a35248 100644 (file)
@@ -42,25 +42,25 @@ function glossary_show_entry_entrylist($course, $cm, $glossary, $entry, $mode=''
 }
 
 function glossary_print_entry_entrylist($course, $cm, $glossary, $entry, $mode='', $hook='', $printicons=1) {
-
-    //The print view for this format is different from the normal view, so we implement it here completely
-    global $CFG, $USER;
-
-
     //Take out autolinking in definitions un print view
+    // TODO use <nolink> tags MDL-15555.
     $entry->definition = '<span class="nolink">'.$entry->definition.'</span>';
 
-    echo '<table class="glossarypost entrylist">';
-    echo '<tr valign="top">';
-    echo '<td class="entry">';
-    echo '<b>';
+    echo html_writer::start_tag('table', array('class' => 'glossarypost entrylist mod-glossary-entrylist'));
+    echo html_writer::start_tag('tr');
+    echo html_writer::start_tag('td', array('class' => 'entry mod-glossary-entry'));
+    echo html_writer::start_tag('div', array('class' => 'mod-glossary-concept'));
     glossary_print_entry_concept($entry);
-    echo ':</b> ';
+    echo html_writer::end_tag('div');
+    echo html_writer::start_tag('div', array('class' => 'mod-glossary-definition'));
     glossary_print_entry_definition($entry, $glossary, $cm);
+    echo html_writer::end_tag('div');
+    echo html_writer::start_tag('div', array('class' => 'mod-glossary-lower-section'));
     glossary_print_entry_lower_section($course, $cm, $glossary, $entry, $mode, $hook, false, false);
-    echo '</td>';
-    echo '</tr>';
-    echo "</table>\n";
+    echo html_writer::end_tag('div');
+    echo html_writer::end_tag('td');
+    echo html_writer::end_tag('tr');
+    echo html_writer::end_tag('table');
 }
 
 
index a8d67f9..c094dfe 100644 (file)
@@ -224,7 +224,7 @@ Glossaries have many uses, such as
 * A ‘handy tips’ resource of best practice in a practical subject
 * A sharing area of useful videos, images or sound files
 * A revision resource of facts to remember';
-$string['modulename_link'] = 'mod/glosssary/view';
+$string['modulename_link'] = 'mod/glossary/view';
 $string['modulenameplural'] = 'Glossaries';
 $string['newentries'] = 'New glossary entries';
 $string['newglossary'] = 'New glossary';
index 5fafc28..0a05ec7 100644 (file)
@@ -1108,9 +1108,8 @@ function glossary_print_entry_default ($entry, $glossary, $cm) {
  */
 function  glossary_print_entry_concept($entry, $return=false) {
     global $OUTPUT;
-    $options = new stdClass();
-    $options->para = false;
-    $text = format_text($OUTPUT->heading('<span class="nolink">' . $entry->concept . '</span>', 3, 'nolink'), FORMAT_MOODLE, $options);
+
+    $text = html_writer::tag('h3', format_string($entry->concept));
     if (!empty($entry->highlight)) {
         $text = highlight($entry->highlight, $text);
     }
@@ -1152,6 +1151,7 @@ function glossary_print_entry_definition($entry, $glossary, $cm) {
     $options->trusted = $entry->definitiontrust;
     $options->context = $context;
     $options->overflowdiv = true;
+
     $text = format_text($definition, $entry->definitionformat, $options);
 
     // Stop excluding concepts from autolinking
@@ -1620,7 +1620,7 @@ function glossary_get_file_areas($course, $cm, $context) {
  * @param string $filename
  * @return file_info_stored file_info_stored instance or null if not found
  */
-function mod_glossary_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
+function glossary_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
     global $CFG, $DB;
 
     if ($context->contextlevel != CONTEXT_MODULE) {
index 9e24706..2f0b448 100644 (file)
@@ -25,6 +25,7 @@
  */
 
 require_once($CFG->libdir . '/portfolio/caller.php');
+require_once($CFG->libdir . '/filelib.php');
 
 /**
  * class to handle exporting an entire glossary database
index 43839f4..f9a3ba1 100644 (file)
@@ -30,3 +30,5 @@
 #page-mod-glossary-view table.glossarycategoryheader th {padding:0px;}
 
 #page-mod-glossary-showentry #page-content {min-width:600px;}
+
+#page-mod-glossary-print .mod-glossary-entrylist .mod-glossary-entry { vertical-align: top; }
index 68b813c..3242bd6 100644 (file)
@@ -477,7 +477,7 @@ function page_export_contents($cm, $baseurl) {
  * Register the ability to handle drag and drop file uploads
  * @return array containing details of the files / types the mod can handle
  */
-function mod_page_dndupload_register() {
+function page_dndupload_register() {
     return array('types' => array(
                      array('identifier' => 'text/html', 'message' => get_string('createpage', 'page')),
                      array('identifier' => 'text', 'message' => get_string('createpage', 'page'))
@@ -489,7 +489,7 @@ function mod_page_dndupload_register() {
  * @param object $uploadinfo details of the file / content that has been uploaded
  * @return int instance id of the newly created mod
  */
-function mod_page_dndupload_handle($uploadinfo) {
+function page_dndupload_handle($uploadinfo) {
     // Gather the required info.
     $data = new stdClass();
     $data->course = $uploadinfo->course->id;
index 8ac1f10..85e6d45 100644 (file)
@@ -55,7 +55,7 @@ echo $OUTPUT->heading(format_string($attemptobj->get_question_name($slot)));
 
 // Process any data that was submitted.
 if (data_submitted() && confirm_sesskey()) {
-    if (optional_param('submit', false, PARAM_BOOL)) {
+    if (optional_param('submit', false, PARAM_BOOL) && question_behaviour::is_manual_grade_in_range($attemptobj->get_uniqueid(), $slot)) {
         $transaction = $DB->start_delegated_transaction();
         $attemptobj->process_submitted_actions(time());
         $transaction->allow_commit();
index 7b14735..eab7b0f 100644 (file)
@@ -43,6 +43,7 @@ class mod_quiz_overdue_attempt_updater {
      * @param int $processfrom the value of $processupto the last time update_overdue_attempts was
      *      called called and completed successfully.
      * @param int $processto only process attempt modifed longer ago than this.
+     * @return array with two elements, the number of attempt considered, and how many different quizzes that was.
      */
     public function update_overdue_attempts($timenow, $processfrom, $processto) {
         global $DB;
@@ -53,11 +54,14 @@ class mod_quiz_overdue_attempt_updater {
         $quiz = null;
         $cm = null;
 
+        $count = 0;
+        $quizcount = 0;
         foreach ($attemptstoprocess as $attempt) {
             // If we have moved on to a different quiz, fetch the new data.
             if (!$quiz || $attempt->quiz != $quiz->id) {
                 $quiz = $DB->get_record('quiz', array('id' => $attempt->quiz), '*', MUST_EXIST);
                 $cm = get_coursemodule_from_instance('quiz', $attempt->quiz);
+                $quizcount += 1;
             }
 
             // If we have moved on to a different course, fetch the new data.
@@ -73,9 +77,11 @@ class mod_quiz_overdue_attempt_updater {
             // Trigger any transitions that are required.
             $attemptobj = new quiz_attempt($attempt, $quizforuser, $cm, $course);
             $attemptobj->handle_if_time_expired($timenow, false);
+            $count += 1;
         }
 
         $attemptstoprocess->close();
+        return array($count, $quizcount);
     }
 
     /**
index 503999f..e2f24d2 100644 (file)
@@ -318,7 +318,7 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
         if (preg_match('!^g([0-9]+)$!', $key, $matches)) {
             // Parse input for question -> grades.
             $questionid = $matches[1];
-            $quiz->grades[$questionid] = clean_param($value, PARAM_FLOAT);
+            $quiz->grades[$questionid] = unformat_float($value);
             quiz_update_question_instance($quiz->grades[$questionid], $questionid, $quiz);
             $deletepreviews = true;
             $recomputesummarks = true;
@@ -385,7 +385,7 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
     }
 
     // If rescaling is required save the new maximum.
-    $maxgrade = optional_param('maxgrade', -1, PARAM_FLOAT);
+    $maxgrade = unformat_float(optional_param('maxgrade', -1, PARAM_RAW));
     if ($maxgrade >= 0) {
         quiz_set_grade($maxgrade, $quiz);
     }
index dd4645f..af3f104 100644 (file)
@@ -335,6 +335,7 @@ $string['graceperiod_desc'] = 'If what to do when time expires is set to \'Allow
 $string['graceperiod_help'] = 'If what to do when time expires is set to \'Allow a grace period to submit, but not change any responses\', the amount of extra time that is allowed.';
 $string['graceperiodmin'] = 'Last submission grace period';
 $string['graceperiodmin_desc'] = 'There is a potential problem right at the end of the quiz. On the one hand, we want to let students continue working right up until the last second - with the help of the timer that automatically submits the quiz when time runs out. On the other hand, the server may then be overloaded, and take some time to get to process the responses. Therefore, we will accept responses for up to this many seconds after time expires, so they are not penalised for the server being slow. However, the student could cheat and get this many seconds to answer the quiz. You have to make a trade-off based on how much you trust the performance of your server during quizzes.';
+$string['graceperiodtoosmall'] = 'The grace period must be more than {$a}.';
 $string['grade'] = 'Grade';
 $string['gradeall'] = 'Grade all';
 $string['gradeaverage'] = 'Average grade';
@@ -808,7 +809,7 @@ $string['timecompleted'] = 'Completed';
 $string['timedelay'] = 'You are not allowed to do the quiz since you have not passed the time delay before attempting another quiz';
 $string['timeleft'] = 'Time left';
 $string['timelimit'] = 'Time limit';
-$string['timelimit_help'] = 'If enabled, a floating timer window (requiring JavaScript) is shown with a countdown. When the time limit is up, the quiz is submitted automatically with whatever answers have been filled in so far.';
+$string['timelimit_help'] = 'If enabled, the time limit is stated on the initial quiz page and a countdown timer is displayed in the quiz navigation block.';
 $string['timelimitexeeded'] = 'Sorry! Quiz time limit exceeded!';
 $string['timelimitmin'] = 'Time limit (minutes)';
 $string['timelimitsec'] = 'Time limit (seconds)';
index 8175374..97bd198 100644 (file)
@@ -446,6 +446,7 @@ function quiz_user_complete($course, $user, $mod, $quiz) {
  */
 function quiz_cron() {
     global $CFG;
+    mtrace('');
 
     // Since the quiz specifies $module->cron = 60, so that the subplugins can
     // have frequent cron if they need it, we now need to do our own scheduling.
@@ -461,10 +462,15 @@ function quiz_cron() {
         $overduehander = new mod_quiz_overdue_attempt_updater();
 
         $processto = $timenow - $quizconfig->graceperiodmin;
-        $overduehander->update_overdue_attempts($timenow, $quizconfig->overduedoneto, $processto);
 
+        mtrace('  Looking for quiz overdue quiz attempts between ' .
+                userdate($quizconfig->overduedoneto) . ' and ' . userdate($processto) . '...');
+
+        list($count, $quizcount) = $overduehander->update_overdue_attempts($timenow, $quizconfig->overduedoneto, $processto);
         set_config('overduelastrun', $timenow, 'quiz');
         set_config('overduedoneto', $processto, 'quiz');
+
+        mtrace('  Considered ' . $count . ' attempts in ' . $quizcount . ' quizzes.');
     }
 
     // Run cron for our sub-plugin types.
@@ -1735,7 +1741,7 @@ function quiz_pluginfile($course, $cm, $context, $filearea, $args, $forcedownloa
  * @param array $options additional options affecting the file serving
  * @return bool false if file not found, does not return if found - justsend the file
  */
-function mod_quiz_question_pluginfile($course, $context, $component,
+function quiz_question_pluginfile($course, $context, $component,
         $filearea, $qubaid, $slot, $args, $forcedownload, array $options=array()) {
     global $CFG;
     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
index 94336a6..0b101ec 100644 (file)
@@ -502,6 +502,14 @@ class mod_quiz_mod_form extends moodleform_mod {
             $errors['timeclose'] = get_string('closebeforeopen', 'quiz');
         }
 
+        // Check that the grace period is not too short.
+        if ($data['overduehandling'] == 'graceperiod') {
+            $graceperiodmin = get_config('quiz', 'graceperiodmin');
+            if ($data['graceperiod'] <= $graceperiodmin) {
+                $errors['graceperiod'] = get_string('graceperiodtoosmall', 'quiz', format_time($graceperiodmin));
+            }
+        }
+
         // Check the boundary value is a number or a percentage, and in range.
         $i = 0;
         while (!empty($data['feedbackboundaries'][$i] )) {
index 6d1fc60..1757eb5 100644 (file)
@@ -65,12 +65,14 @@ if ($page == -1) {
 
 // If there is only a very small amount of time left, there is no point trying
 // to show the student another page of the quiz. Just finish now.
+$graceperiodmin = null;
 $accessmanager = $attemptobj->get_access_manager($timenow);
 $timeleft = $accessmanager->get_time_left($attemptobj->get_attempt(), $timenow);
 $toolate = false;
 if ($timeleft !== false && $timeleft < QUIZ_MIN_TIME_TO_CONTINUE) {
     $timeup = true;
-    if ($timeleft < -get_config('quiz', 'graceperiodmin')) {
+    $graceperiodmin = get_config('quiz', 'graceperiodmin');
+    if ($timeleft < -$graceperiodmin) {
         $toolate = true;
     }
 }
@@ -97,9 +99,19 @@ if ($attemptobj->is_finished()) {
 
 // If time is running out, trigger the appropriate action.
 $becomingoverdue = false;
+$becomingabandoned = false;
 if ($timeup) {
     if ($attemptobj->get_quiz()->overduehandling == 'graceperiod') {
-        $becomingoverdue = true;
+        if (is_null($graceperiodmin)) {
+            $graceperiodmin = get_config('quiz', 'graceperiodmin');
+        }
+        if ($timeleft < -$attemptobj->get_quiz()->graceperiod - $graceperiodmin) {
+            // Grace period has run out.
+            $finishattempt = true;
+            $becomingabandoned = true;
+        } else {
+            $becomingoverdue = true;
+        }
     } else {
         $finishattempt = true;
     }
@@ -150,7 +162,11 @@ add_to_log($attemptobj->get_courseid(), 'quiz', 'close attempt',
 
 // Update the quiz attempt record.
 try {
-    $attemptobj->process_finish($timenow, !$toolate);
+    if ($becomingabandoned) {
+        $attemptobj->process_abandon($timenow, true);
+    } else {
+        $attemptobj->process_finish($timenow, !$toolate);
+    }
 
 } catch (question_out_of_sequence_exception $e) {
     print_error('submissionoutofsequencefriendlymessage', 'question',
index 9fb94bb..6d20d2a 100644 (file)
@@ -461,11 +461,7 @@ class quiz_grading_report extends quiz_default_report {
 
         foreach ($qubaids as $qubaid) {
             foreach ($slots as $slot) {
-                $prefix = 'q' . $qubaid . ':' . $slot . '_';
-                $mark = optional_param($prefix . '-mark', null, PARAM_NUMBER);
-                $maxmark = optional_param($prefix . '-maxmark', null, PARAM_NUMBER);
-                $minfraction = optional_param($prefix . ':minfraction', null, PARAM_NUMBER);
-                if (!is_null($mark) && ($mark < $minfraction * $maxmark || $mark > $maxmark)) {
+                if (!question_behaviour::is_manual_grade_in_range($qubaid, $slot)) {
                     return false;
                 }
             }
index 0b1cdb0..303638f 100644 (file)
@@ -46,11 +46,6 @@ if (!$attemptobj->is_preview_user()) {
     $attemptobj->require_capability('mod/quiz:attempt');
 }
 
-// If the attempt is already closed, redirect them to the review page.
-if ($attemptobj->is_finished()) {
-    redirect($attemptobj->review_url());
-}
-
 if ($attemptobj->is_preview_user()) {
     navigation_node::override_active_url($attemptobj->start_attempt_url());
 }
@@ -69,6 +64,14 @@ if ($accessmanager->is_preflight_check_required($attemptobj->get_attemptid())) {
 
 $displayoptions = $attemptobj->get_display_options(false);
 
+// If the attempt is now overdue, or abandoned, deal with that.
+$attemptobj->handle_if_time_expired(time(), true);
+
+// If the attempt is already closed, redirect them to the review page.
+if ($attemptobj->is_finished()) {
+    redirect($attemptobj->review_url());
+}
+
 // Log this page view.
 add_to_log($attemptobj->get_courseid(), 'quiz', 'view summary',
         'summary.php?attempt=' . $attemptobj->get_attemptid(),
index d5f2081..8243c5b 100644 (file)
@@ -484,7 +484,7 @@ function resource_export_contents($cm, $baseurl) {
  * Register the ability to handle drag and drop file uploads
  * @return array containing details of the files / types the mod can handle
  */
-function mod_resource_dndupload_register() {
+function resource_dndupload_register() {
     return array('files' => array(
                      array('extension' => '*', 'message' => get_string('dnduploadresource', 'mod_resource'))
                  ));
@@ -495,7 +495,7 @@ function mod_resource_dndupload_register() {
  * @param object $uploadinfo details of the file / content that has been uploaded
  * @return int instance id of the newly created mod
  */
-function mod_resource_dndupload_handle($uploadinfo) {
+function resource_dndupload_handle($uploadinfo) {
     // Gather the required info.
     $data = new stdClass();
     $data->course = $uploadinfo->course->id;
index 41edc0e..dc97482 100644 (file)
Binary files a/mod/resource/pix/icon.gif and b/mod/resource/pix/icon.gif differ
index 7b92eda..f1cb721 100644 (file)
@@ -92,6 +92,7 @@ $string['displaycoursestructure_help'] = 'If enabled, the table of contents is d
 $string['displaycoursestructuredesc'] = 'This preference sets the default value for the display course structure on entry page setting';
 $string['displaydesc'] = 'This preference sets the default of whether to display the package or not for an activity';
 $string['displaysettings'] = 'Display Settings';
+$string['dnduploadscorm'] = 'Create SCORM package';
 $string['domxml'] = 'DOMXML external library';
 $string['duedate'] = 'Due date';
 $string['element'] = 'Element';
@@ -180,7 +181,7 @@ $string['invalidhacpsession'] = 'Invalid HACP Session';
 $string['invalidmanifestresource'] = 'WARNING: The following resources were referenced in your manifest but couldn\'t be found:';
 $string['last'] = 'Last accessed on';
 $string['lastaccess'] = 'Last access';
-$string['lastattempt'] = 'Last attempt';
+$string['lastattempt'] = 'Last completed attempt';
 $string['lastattemptlock'] = 'Lock after final attempt';
 $string['lastattemptlock_help'] = 'If enabled, a student is prevented from launching the SCORM player after using up all their allocated attempts.';
 $string['lastattemptlockdesc'] = 'This preference sets the default value for the lock after final attempt setting';
@@ -334,7 +335,7 @@ $string['versionwarning'] = 'The manifest version is older than 1.3, warning at
 $string['viewallreports'] = 'View reports for {$a} attempts';
 $string['viewalluserreports'] = 'View reports for {$a} users';
 $string['whatgrade'] = 'Attempts grading';
-$string['whatgrade_help'] = 'If multiple attempts are allowed, this setting specifies whether the highest, average (mean), first or last attempt is recorded in the gradebook.
+$string['whatgrade_help'] = 'If multiple attempts are allowed, this setting specifies whether the highest, average (mean), first or last completed attempt is recorded in the gradebook.
 
 Handling of Multiple Attempts
 
index 9849031..7b395f5 100644 (file)
@@ -1258,3 +1258,68 @@ function scorm_get_completion_state($course, $cm, $userid, $type) {
 
     return $result;
 }
+
+/**
+ * Register the ability to handle drag and drop file uploads
+ * @return array containing details of the files / types the mod can handle
+ */
+function scorm_dndupload_register() {
+    return array('files' => array(
+        array('extension' => 'zip', 'message' => get_string('dnduploadscorm', 'scorm'))
+    ));
+}
+
+/**
+ * Handle a file that has been uploaded
+ * @param object $uploadinfo details of the file / content that has been uploaded
+ * @return int instance id of the newly created mod
+ */
+function scorm_dndupload_handle($uploadinfo) {
+
+    $context = context_module::instance($uploadinfo->coursemodule);
+    file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_scorm', 'package', 0);
+    $fs = get_file_storage();
+    $files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, 'sortorder, itemid, filepath, filename', false);
+    $file = reset($files);
+
+    // Validate the file, make sure it's a valid SCORM package!
+    $packer = get_file_packer('application/zip');
+    $filelist = $file->list_files($packer);
+
+    if (!is_array($filelist)) {
+        return false;
+    } else {
+        $manifestpresent = false;
+        $aiccfound = false;
+
+        foreach ($filelist as $info) {
+            if ($info->pathname == 'imsmanifest.xml') {
+                $manifestpresent = true;
+                break;
+            }
+
+            if (preg_match('/\.cst$/', $info->pathname)) {
+                $aiccfound = true;
+                break;
+            }
+        }
+
+        if (!$manifestpresent && !$aiccfound) {
+            return false;
+        }
+    }
+
+    // Create a default scorm object to pass to scorm_add_instance()!
+    $scorm = get_config('scorm');
+    $scorm->course = $uploadinfo->course->id;
+    $scorm->coursemodule = $uploadinfo->coursemodule;
+    $scorm->cmidnumber = '';
+    $scorm->name = $uploadinfo->displayname;
+    $scorm->scormtype = SCORM_TYPE_LOCAL;
+    $scorm->reference = $file->get_filename();
+    $scorm->intro = '';
+    $scorm->width = $scorm->framewidth;
+    $scorm->height = $scorm->frameheight;
+
+    return scorm_add_instance($scorm, null);
+}
index 65c21cf..8fdfee1 100644 (file)
@@ -336,7 +336,7 @@ function url_export_contents($cm, $baseurl) {
  * Register the ability to handle drag and drop file uploads
  * @return array containing details of the files / types the mod can handle
  */
-function mod_url_dndupload_register() {
+function url_dndupload_register() {
     return array('types' => array(
                      array('identifier' => 'url', 'message' => get_string('createurl', 'url'))
                  ));
@@ -347,7 +347,7 @@ function mod_url_dndupload_register() {
  * @param object $uploadinfo details of the file / content that has been uploaded
  * @return int instance id of the newly created mod
  */
-function mod_url_dndupload_handle($uploadinfo) {
+function url_dndupload_handle($uploadinfo) {
     // Gather all the required data.
     $data = new stdClass();
     $data->course = $uploadinfo->course->id;
index ca09117..cd527ef 100644 (file)
@@ -458,7 +458,8 @@ abstract class question_behaviour {
                 $fraction = null;
             } else if ($fraction > 1 || $fraction < $this->qa->get_min_fraction()) {
                 throw new coding_exception('Score out of range when processing ' .
-                        'a manual grading action.', $pendingstep);
+                        'a manual grading action.', 'Question ' . $this->qa->get_question()->id .
+                                ', slot ' . $this->qa->get_slot() . ', fraction ' . $fraction);
             }
             $pendingstep->set_fraction($fraction);
         }
@@ -468,6 +469,20 @@ abstract class question_behaviour {
         return question_attempt::KEEP;
     }
 
+    /**
+     * Validate that the manual grade submitted for a particular question is in range.
+     * @param int $qubaid the question_usage id.
+     * @param int $slot the slot number within the usage.
+     * @return bool whether the submitted data is in range.
+     */
+    public static function is_manual_grade_in_range($qubaid, $slot) {
+        $prefix = 'q' . $qubaid . ':' . $slot . '_';
+        $mark = optional_param($prefix . '-mark', null, PARAM_NUMBER);
+        $maxmark = optional_param($prefix . '-maxmark', null, PARAM_NUMBER);
+        $minfraction = optional_param($prefix . ':minfraction', null, PARAM_NUMBER);
+        return is_null($mark) || ($mark >= $minfraction * $maxmark && $mark <= $maxmark);
+    }
+
     /**
      * @param $comment the comment text to format. If omitted,
      *      $this->qa->get_manual_comment() is used.
index c3ffc19..f763557 100644 (file)
@@ -228,7 +228,7 @@ class question_preview_options extends question_display_options {
  * @return bool false if file not found, does not return if found - justsend the file
  */
 function question_preview_question_pluginfile($course, $context, $component,
-        $filearea, $qubaid, $slot, $args, $forcedownload, $options) {
+        $filearea, $qubaid, $slot, $args, $forcedownload, $fileoptions) {
     global $USER, $DB, $CFG;
 
     $quba = question_engine::load_questions_usage_by_activity($qubaid);
@@ -256,7 +256,7 @@ function question_preview_question_pluginfile($course, $context, $component,
         send_file_not_found();
     }
 
-    send_stored_file($file, 0, 0, $forcedownload, $options);
+    send_stored_file($file, 0, 0, $forcedownload, $fileoptions);
 }
 
 /**
index 29c23d6..547a370 100644 (file)
@@ -279,7 +279,8 @@ class qtype_multianswer_multichoice_inline_renderer
                 $response, array('' => ''), $inputattributes);
 
         $order = $subq->get_order($qa);
-        $rightanswer = $subq->answers[$order[reset($subq->get_correct_response())]];
+        $correctresponses = $subq->get_correct_response();
+        $rightanswer = $subq->answers[$order[reset($correctresponses)]];
         $feedbackpopup = $this->feedback_popup($subq, $matchinganswer->fraction,
                 $subq->format_text($matchinganswer->feedback, $matchinganswer->feedbackformat,
                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
index b3f1dde..0bbc3a3 100644 (file)
@@ -333,7 +333,8 @@ default:
 
                 $home_url->param('action', 'browse');
                 $home_url->param('draftpath', $file->filepath);
-                $foldername = trim(array_pop(explode('/', trim($file->filepath, '/'))), '/');
+                $filepathchunks = explode('/', trim($file->filepath, '/'));
+                $foldername = trim(array_pop($filepathchunks), '/');
                 echo html_writer::link($home_url, $foldername);
 
                 $home_url->param('draftpath', $file->filepath);
diff --git a/repository/equella/callback.php b/repository/equella/callback.php
new file mode 100644 (file)
index 0000000..0a5b083
--- /dev/null
@@ -0,0 +1,79 @@
+<?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/>.
+
+/**
+ * Callback for equella repository.
+ *
+ * @since 2.3
+ * @package   repository_equella
+ * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+require_once(dirname(dirname(dirname(__FILE__))).'/config.php');
+$json = required_param('tlelinks', PARAM_RAW);
+
+require_login();
+
+$decodedinfo = json_decode($json);
+$info = array_pop($decodedinfo);
+
+$url = '';
+if (isset($info->url)) {
+    $url = s(clean_param($info->url, PARAM_URL));
+}
+
+$filename = '';
+if (isset($info->name)) {
+    $filename  = s(clean_param($info->name, PARAM_FILE));
+}
+
+$thumbnail = '';
+if (isset($info->thumbnail)) {
+    $thumbnail = s(clean_param($info->thumbnail, PARAM_URL));
+}
+
+$author = '';
+if (isset($info->owner)) {
+    $author = s(clean_param($info->owner, PARAM_NOTAGS));
+}
+
+$license = '';
+if (isset($info->license)) {
+    $license = s(clean_param($info->license, PARAM_ALPHAEXT));
+}
+
+$js =<<<EOD
+<html>
+<head>
+   <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+    <script type="text/javascript">
+    window.onload = function() {
+        var resource = {};
+        resource.title = "$filename";
+        resource.source = "$url";
+        resource.thumbnail = '$thumbnail';
+        resource.author = "$author";
+        resource.license = "$license";
+        parent.M.core_filepicker.select_file(resource);
+    }
+    </script>
+</head>
+<body><noscript></noscript></body>
+</html>
+EOD;
+
+header('Content-Type: text/html; charset=utf-8');
+die($js);
diff --git a/repository/equella/db/access.php b/repository/equella/db/access.php
new file mode 100644 (file)
index 0000000..4a7d6ab
--- /dev/null
@@ -0,0 +1,36 @@
+<?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/>.
+
+/**
+ * Capabilities for equella repository.
+ *
+ * @package    repository_equella
+ * @copyright  2012 Dongsheng Cai {@link http://dongsheng.org}
+ * @author     Dongsheng Cai <dongsheng@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+    'repository/equella:view' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'user' => CAP_ALLOW
+        )
+    )
+);
diff --git a/repository/equella/lang/en/repository_equella.php b/repository/equella/lang/en/repository_equella.php
new file mode 100644 (file)
index 0000000..f24ac9a
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * Strings for equella repository.
+ *
+ * @package   repository_equella
+ * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'EQUELLA repository';
+$string['configplugin'] = 'Configuration for EQUELLA repository';
+$string['search'] = 'Search EQUELLA';
+$string['breadcrumb'] = 'EQUELLA';
+
+$string['equellaurl'] = 'EQUELLA URL';
+$string['equellaaction'] = 'EQUELLA action';
+$string['equellaoptions'] = 'EQUELLA options';
+$string['sharedid'] = 'Shared secret ID';
+$string['sharedsecrets'] = 'Shared secret';
+
+$string['selectrestriction'] = 'Restrict selection';
+$string['selectrestriction.desc'] = 'Choose whether course editors should only be able to select an item summary, an attached resources or either';
+$string['restrictionnone'] = 'No restriction';
+$string['restrictionitemsonly'] = 'Item summary only';
+$string['restrictionattachmentsonly'] = 'Attached resource only';
+
+$string['sharedsecretsheading'] = 'Shared Secret Settings';
+$string['sharedsecretshelp'] =  '<p>Below you can set a default EQUELLA shared secret for single signing-on users.  You can configure different shared secrets for general (read) usage, and a specialised role based shared secret for each <em>write</em> role in your Moodle site.  If a shared secret ID is not configured for a role then the default shared secret ID and shared secret are used.</p><p>All shared secret IDs and shared secrets must also be configured within EQUELLA and the shared secret module enabled.  This configuration is found in the EQUELLA Administration Console under User Management > Shared Secrets.</p>';
+$string['group'] = '{$a} role settings';
+$string['groupdefault'] = 'Default';
+$string['sharedidtitle'] = 'Shared secret ID';
+$string['sharedsecrettitle'] = 'Shared secret';
diff --git a/repository/equella/lib.php b/repository/equella/lib.php
new file mode 100644 (file)
index 0000000..25a544f
--- /dev/null
@@ -0,0 +1,333 @@
+<?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/>.
+
+/**
+ * This plugin is used to access equella repositories.
+ *
+ * @since 2.3
+ * @package    repository_equella
+ * @copyright  2012 Dongsheng Cai {@link http://dongsheng.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/repository/lib.php');
+
+/**
+ * repository_equella class implements equella_client
+ *
+ * @since 2.3
+ * @package    repository_equella
+ * @copyright  2012 Dongsheng Cai {@link http://dongsheng.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class repository_equella extends repository {
+    /** @var array mimetype filter */
+    private $mimetypes = array();
+
+    /**
+     * Constructor
+     *
+     * @param int $repositoryid repository instance id
+     * @param int|stdClass $context a context id or context object
+     * @param array $options repository options
+     */
+    public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
+        parent::__construct($repositoryid, $context, $options);
+
+        if (isset($this->options['mimetypes'])) {
+            $mt = $this->options['mimetypes'];
+            if (!empty($mt) && is_array($mt) && !in_array('*', $mt)) {
+                $this->mimetypes = array_unique(array_map(array($this, 'to_mime_type'), $mt));
+            }
+        }
+    }
+
+    /**
+     * Display embedded equella interface
+     *
+     * @param string $path
+     * @param mixed $page
+     * @return array
+     */
+    public function get_listing($path = null, $page = null) {
+        global $COURSE;
+        $callbackurl = new moodle_url('/repository/equella/callback.php', array('repo_id'=>$this->id));
+
+        $mimetypesstr = '';
+        $restrict = '';
+        if (!empty($this->mimetypes)) {
+            $mimetypesstr = '&mimeTypes=' . implode(',', $this->mimetypes);
+            // We're restricting to a mime type, so we always restrict to selecting resources only.
+            $restrict = '&attachmentonly=true';
+        } else if ($this->get_option('equella_select_restriction') != 'none') {
+            // The option value matches the EQUELLA paramter name.
+            $restrict = '&' . $this->get_option('equella_select_restriction') . '=true';
+        }
+
+        $url = $this->get_option('equella_url')
+                . '?method=lms'
+                . '&returnurl='.urlencode($callbackurl)
+                . '&returnprefix=tle'
+                . '&template=standard'
+                . '&token='.urlencode($this->getssotoken('write'))
+                . '&courseId='.urlencode($COURSE->id)
+                . '&courseCode='.urlencode($COURSE->shortname)
+                . '&action=searchThin'
+                . '&forcePost=true'
+                . '&cancelDisabled=true'
+                . '&attachmentUuidUrls=true'
+                . '&options='.urlencode($this->get_option('equella_options') . $mimetypesstr)
+                . $restrict;
+        $list = array();
+        $list['object'] = array();
+        $list['object']['type'] = 'text/html';
+        $list['object']['src'] = $url;
+        $list['nologin']  = true;
+        $list['nosearch'] = true;
+        $list['norefresh'] = true;
+        return $list;
+    }
+
+    /**
+     * Supported equella file types
+     *
+     * @return int
+     */
+    public function supported_returntypes() {
+        return FILE_REFERENCE;
+    }
+
+    /**
+     * Prepare file reference information
+     *
+     * @param string $source
+     * @return string file referece
+     */
+    public function get_file_reference($source) {
+        return base64_encode($source);
+    }
+
+    /**
+     * Download a file, this function can be overridden by subclass. {@link curl}
+     *
+     * @param string $url the url of file
+     * @param string $filename save location
+     * @return string the location of the file
+     */
+    public function get_file($url, $filename = '') {
+        global $USER;
+        $cookiename = uniqid('', true) . '.cookie';
+        $dir = make_temp_directory('repository/equella/' . $USER->id);
+        $cookiepathname = $dir . '/' . $cookiename;
+        $path = $this->prepare_file($filename);
+        $fp = fopen($path, 'w');
+        $c = new curl(array('cookie'=>$cookiepathname));
+        $c->download(array(array('url'=>$url, 'file'=>$fp)), array('CURLOPT_FOLLOWLOCATION'=>true));
+        // Close file handler.
+        fclose($fp);
+        // Delete cookie jar.
+        unlink($cookiepathname);
+        return array('path'=>$path, 'url'=>$url);
+    }
+
+    /**
+     * Get file from external repository by reference
+     * {@link repository::get_file_reference()}
+     * {@link repository::get_file()}
+     *
+     * @param stdClass $reference file reference db record
+     * @return stdClass|null|false
+     */
+    public function get_file_by_reference($reference) {
+        $ref = base64_decode($reference->reference);
+        $url = $this->appendtoken($ref);
+
+        if (!$url) {
+            // Occurs when the user isn't known..
+            return false;
+        }
+
+        // We use this cache to get the correct file size.
+        $cachedfilepath = cache_file::get($url, array('ttl' => 0));
+        if ($cachedfilepath === false) {
+            // Cache the file.
+            $path = $this->get_file($url);
+            $cachedfilepath = cache_file::create_from_file($url, $path['path']);
+        }
+
+        $fileinfo = new stdClass;
+        $fileinfo->filepath = $cachedfilepath;
+
+        return $fileinfo;
+    }
+
+    /**
+     * Send equella file to browser
+     *
+     * @param stored_file $stored_file
+     */
+    public function send_file($stored_file, $lifetime=86400 , $filter=0, $forcedownload=false, array $options = null) {
+        $reference = base64_decode($stored_file->get_reference());
+        $url = $this->appendtoken($reference);
+        if ($url) {
+            header('Location: ' . $url);
+        }
+        die;
+    }
+
+    /**
+     * Add Instance settings input to Moodle form
+     *
+     * @param moodleform $mform
+     */
+    public function instance_config_form($mform) {
+        $mform->addElement('text', 'equella_url', get_string('equellaurl', 'repository_equella'));
+        $mform->setType('equella_url', PARAM_URL);
+
+        $strrequired = get_string('required');
+        $mform->addRule('equella_url', $strrequired, 'required', null, 'client');
+
+        $mform->addElement('text', 'equella_options', get_string('equellaoptions', 'repository_equella'));
+        $mform->setType('equella_options', PARAM_NOTAGS);
+
+        $choices = array(
+            'none' => get_string('restrictionnone', 'repository_equella'),
+            'itemonly' => get_string('restrictionitemsonly', 'repository_equella'),
+            'attachmentonly' => get_string('restrictionattachmentsonly', 'repository_equella'),
+        );
+        $mform->addElement('select', 'equella_select_restriction', get_string('selectrestriction', 'repository_equella'), $choices);
+
+        $mform->addElement('header', '',
+            get_string('group', 'repository_equella', get_string('groupdefault', 'repository_equella')));
+        $mform->addElement('text', 'equella_shareid', get_string('sharedid', 'repository_equella'));
+        $mform->setType('equella_shareid', PARAM_RAW);
+        $mform->addRule('equella_shareid', $strrequired, 'required', null, 'client');
+
+        $mform->addElement('text', 'equella_sharedsecret', get_string('sharedsecrets', 'repository_equella'));
+        $mform->setType('equella_sharedsecret', PARAM_RAW);
+        $mform->addRule('equella_sharedsecret', $strrequired, 'required', null, 'client');
+
+        foreach (self::get_all_editing_roles() as $role) {
+            $mform->addElement('header', '', get_string('group', 'repository_equella', format_string($role->name)));
+            $mform->addElement('text', "equella_{$role->shortname}_shareid", get_string('sharedid', 'repository_equella'));
+            $mform->setType("equella_{$role->shortname}_shareid", PARAM_RAW);
+            $mform->addElement('text', "equella_{$role->shortname}_sharedsecret",
+                get_string('sharedsecrets', 'repository_equella'));
+            $mform->setType("equella_{$role->shortname}_sharedsecret", PARAM_RAW);
+        }
+    }
+
+    /**
+     * Names of the instance settings
+     *
+     * @return array
+     */
+    public static function get_instance_option_names() {
+        $rv = array('equella_url', 'equella_select_restriction', 'equella_options',
+            'equella_shareid', 'equella_sharedsecret'
+        );
+
+        foreach (self::get_all_editing_roles() as $role) {
+            array_push($rv, "equella_{$role->shortname}_shareid");
+            array_push($rv, "equella_{$role->shortname}_sharedsecret");
+        }
+
+        return $rv;
+    }
+
+    /**
+     * Generate equella token
+     *
+     * @param string $username
+     * @param string $shareid
+     * @param string $sharedsecret
+     * @return string
+     */
+    private static function getssotoken_raw($username, $shareid, $sharedsecret) {
+        $time = time() . '000';
+        return urlencode($username)
+            . ':'
+            . $shareid
+            . ':'
+            . $time
+            . ':'
+            . base64_encode(pack('H*', md5($username . $shareid . $time . $sharedsecret)));
+    }
+
+    /**
+     * Append token
+     *
+     * @param string $url
+     * @param $readwrite
+     * @return string
+     */
+    private function appendtoken($url, $readwrite = null) {
+        $ssotoken = $this->getssotoken($readwrite);
+        if (!$ssotoken) {
+            return false;
+        }
+        return $url . (strpos($url, '?') != false ? '&' : '?') . 'token=' . urlencode($ssotoken);
+    }
+
+    /**
+     * Generate equella sso token
+     *
+     * @param string $readwrite
+     * @return string
+     */
+    private function getssotoken($readwrite = 'read') {
+        global $USER;
+
+        if (empty($USER->username)) {
+            return false;
+        }
+
+        if ($readwrite == 'write') {
+
+            foreach (self::get_all_editing_roles() as $role) {
+                if (user_has_role_assignment($USER->id, $role->id, $this->context->id)) {
+                    // See if the user has a role that is linked to an equella role.
+                    $shareid = $this->get_option("equella_{$role->shortname}_shareid");
+                    if (!empty($shareid)) {
+                        return $this->getssotoken_raw($USER->username, $shareid,
+                            $this->get_option("equella_{$role->shortname}_sharedsecret"));
+                    }
+                }
+            }
+        }
+        // If we are only reading, use the unadorned shareid and secret.
+        $shareid = $this->get_option('equella_shareid');
+        if (!empty($shareid)) {
+            return $this->getssotoken_raw($USER->username, $shareid, $this->get_option('equella_sharedsecret'));
+        }
+    }
+
+    private static function get_all_editing_roles() {
+        return get_roles_with_capability('moodle/course:manageactivities', CAP_ALLOW);
+    }
+
+    /**
+     * Convert moodle mimetypes list to equella format
+     *
+     * @param string $value
+     * @return string
+     */
+    private static function to_mime_type($value) {
+        return mimeinfo('type', $value);
+    }
+}
diff --git a/repository/equella/pix/icon.png b/repository/equella/pix/icon.png
new file mode 100644 (file)
index 0000000..9020c2d
Binary files /dev/null and b/repository/equella/pix/icon.png differ
diff --git a/repository/equella/version.php b/repository/equella/version.php
new file mode 100644 (file)
index 0000000..5b866ef
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Version information for equella repository.
+ *
+ * @since 2.3
+ * @package    repository_equella
+ * @copyright  2012 Dongsheng Cai {@link http://dongsheng.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version   = 2012060100;        // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2012052500;        // Requires this Moodle version.
+$plugin->component = 'repository_equella'; // Full name of the plugin (used for diagnostics).
index 407b3dd..4dab067 100644 (file)
@@ -67,7 +67,7 @@ $draftpath = optional_param('draftpath', '/',    PARAM_PATH);
 
 
 // user context
-$user_context = get_context_instance(CONTEXT_USER, $USER->id);
+$user_context = context_user::instance($USER->id);
 
 $PAGE->set_context($user_context);
 if (!$course = $DB->get_record('course', array('id'=>$courseid))) {
@@ -298,7 +298,14 @@ case 'download':
         $record->itemid   = $itemid;
         $record->license  = '';
         $record->author   = '';
-        $record->source   = $thefile['url'];
+
+        $now = time();
+        $record->timecreated  = $now;
+        $record->timemodified = $now;
+        $record->userid       = $USER->id;
+        $record->contextid = $user_context->id;
+
+        $record->source = serialize((object)array('source' => $thefile['url']));
         try {
             $info = repository::move_to_filepool($thefile['path'], $record);
             redirect($home_url, get_string('downloadsucc', 'repository'));
index ee0f8c0..7e349d0 100644 (file)
@@ -813,78 +813,120 @@ sup {vertical-align: super;}
  * without javascript enabled
  */
 /* Hide the dialog and it's title */
-#chooserdialogue,
-#choosertitle {
+.chooserdialoguebody,
+.choosertitle {
     display:none;
 }
 
-.modchooser .moodle-dialogue-hd {
-    text-align: center;
+.moodle-dialogue-base .moodle-dialogue {
+    background-color: transparent;
+    border: 0px solid transparent!important;
+}
+
+.chooserdialogue .moodle-dialogue-wrap {
+    height: auto;
+    background-color: #FFFFFF;
+    border: 1px solid #CCCCCC!important;
+    border-radius:10px;
+    box-shadow: 5px 5px 20px 0px #666666;
+    -webkit-box-shadow: 5px 5px 20px 0px #666666;
+    -moz-box-shadow: 5px 5px 20px 0px #666666;
+}
+
+.chooserdialogue .moodle-dialogue-hd {
+    font-size:12px!important;
+    font-weight: normal!important;
+    letter-spacing: 1px;
+    color:#333333!important;
+    text-align: center!important;
+    text-shadow: 1px 1px 1px #FFFFFF;
+    padding:5px 5px 5px 5px;
+    border-radius: 10px 10px 0px 0px;
+    border-bottom: 1px solid #BBBBBB!important;
+    background: #CCCCCC;
+    background: -webkit-gradient(linear, left top, left bottom, from(#FFFFFF), to(#CCCCCC));
+    background: -moz-linear-gradient(top,  #FFFFFF,  #CCCCCC);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFF', endColorstr='#CCCCCC')!important;
+    filter: dropshadow(color=#FFFFFF, offx=1, offy=1);
+}
+
+.chooserdialogue .moodle-dialogue-bd {
+    font-size: 12px;
+    color: #555555;
+    overflow: auto;
+    padding: 0px;
+    background: #F2F2F2;
+    border-bottom-left-radius: 10px;
+    border-bottom-right-radius: 10px;
 }
 
 /* Center the submit buttons within the area */
-#choosercontainer #chooseform .submitbuttons {
+.choosercontainer #chooseform .submitbuttons {
     margin: 0.7em 0;
     text-align: center;
 }
 
-#choosercontainer #chooseform .submitbuttons input {
+.choosercontainer #chooseform .submitbuttons input {
     min-width: 100px;
     margin: 0px 0.5em;
 }
 
 /* Various settings for the options area */
-#choosercontainer #chooseform .options {
+.choosercontainer #chooseform .options {
     position: relative;
-    border-bottom: 1px solid grey;
-    padding: 0.24em 0;
+    border-bottom: 1px solid #BBBBBB;
 }
 
 /* Only set these options if we're showing the js container */
-.jsenabled #choosercontainer #chooseform .alloptions {
-    max-height: 530px;
+.jsenabled .choosercontainer #chooseform .alloptions {
+    max-height: 550px;
     overflow-x: hidden;
     overflow-y: auto;
-    max-width: 18.15em;
+    max-width: 18.5em;
+    box-shadow: inset 0px 0px 30px 0px #CCCCCC;
+    -webkit-box-shadow: inset 0px 0px 30px 0px #CCCCCC;
+    -moz-box-shadow: inset 0px 0px 30px 0px #CCCCCC;
 }
 
 /* Settings for option rows and option subtypes */
-#choosercontainer #chooseform .moduletypetitle,
-#choosercontainer #chooseform .option,
-#choosercontainer #chooseform .nonoption {
+.choosercontainer #chooseform .moduletypetitle,
+.choosercontainer #chooseform .option,
+.choosercontainer #chooseform .nonoption {
     margin-bottom: 0;
-    padding: 0 0 0 0.3em;
+    padding: 0 1.6em 0 1.6em;
 }
 
-#choosercontainer #chooseform .moduletypetitle {
-    font-weight: bold;
-    text-align : center;
+.choosercontainer #chooseform .moduletypetitle {
+    text-transform: uppercase;
+    padding-top: 1.2em;
+    padding-bottom: 0.4em;
 }
 
-#choosercontainer #chooseform .subtype {
+.choosercontainer #chooseform .subtype {
     margin-bottom: 0;
     padding: 0 0 0 1em;
 }
 
-#choosercontainer #chooseform .option .typename,
-#choosercontainer #chooseform .option span.modicon img.icon {
-    padding: 0 0 0 0.3em;
+.choosercontainer #chooseform .option .typename,
+.choosercontainer #chooseform .option span.modicon img.icon {
+    padding: 0 0 0 0.5em;
 }
 
-#choosercontainer #chooseform .option input[type=radio],
-#choosercontainer #chooseform .option span.typename,
-#choosercontainer #chooseform .option span.modicon {
+.choosercontainer #chooseform .option input[type=radio],
+.choosercontainer #chooseform .option span.typename,
+.choosercontainer #chooseform .option span.modicon {
     vertical-align: middle;
 }
 
-#choosercontainer #chooseform .option label {
+.choosercontainer #chooseform .option label {
     display: block;
-    padding: 0.2em 0 0.2em 0;
+    padding: 0.3em 0 0.1em 0;
+    border-bottom: 1px solid #FFFFFF;
 }
 
 /* The instruction/help area */
-#choosercontainer #chooseform .instruction,
-.jsenabled #choosercontainer #chooseform .typesummary {
+.choosercontainer #chooseform .instruction,
+.jsenabled .choosercontainer #chooseform .typesummary {
     display: none;
     position: absolute;
     top: 0px;
@@ -892,20 +934,24 @@ sup {vertical-align: super;}
     bottom: 0px;
     left: 18.5em;
     margin: 0;
-    border-left: 1px solid grey;
-    padding: 0.3em 0.5em;
+    padding: 2em 2em 2em 2.4em;
     background-color: white;
     overflow-x: hidden;
     overflow-y: auto;
     max-height: 550px;
+    line-height: 2em;
 }
 
 /* Selected option settings */
-.jsenabled #choosercontainer #chooseform .instruction,
-#choosercontainer #chooseform .selected .typesummary {
+.jsenabled .choosercontainer #chooseform .instruction,
+.choosercontainer #chooseform .selected .typesummary {
     display: block;
 }
-#choosercontainer #chooseform .selected {
-    background-color: #ddd;
+
+.choosercontainer #chooseform .selected {
+    background-color: #FFFFFF;
+    box-shadow: 0px 0px 10px 0px #CCCCCC;
+    -webkit-box-shadow: 0px 0px 10px 0px #CCCCCC;
+    -moz-box-shadow: 0px 0px 10px 0px #CCCCCC;
 }
 
index f6ee588..8aef9ad 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2012060600.04;              // YYYYMMDD      = weekly release date of this DEV branch
+$version  = 2012061200.00;              // YYYYMMDD      = weekly release date of this DEV branch
                                         //         RR    = release increments - 00 in DEV branches
                                         //           .XX = incremental changes
 
-$release  = '2.3dev (Build: 20120601)'; // Human-friendly version name
+$release  = '2.3dev (Build: 20120612)'; // Human-friendly version name
 
-$branch = '23';                         // this version's branch
+$branch   = '23';                       // this version's branch
 $maturity = MATURITY_ALPHA;             // this version's maturity level