Merge branch 'MDL-33588-master-1' of git://git.luns.net.uk/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 12 Jun 2012 09:16:40 +0000 (17:16 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 12 Jun 2012 09:16:40 +0000 (17:16 +0800)
136 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
enrol/mnet/addinstance_form.php
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/install.xml
lib/db/log.php
lib/db/upgrade.php
lib/filestorage/file_exceptions.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/filestorage/tests/file_storage_test.php
lib/form/editor.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/db/messages.php
mod/assign/feedback/comments/db/install.xml
mod/assign/feedback/file/db/install.xml
mod/assign/gradeform.php
mod/assign/gradingtable.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/module.js
mod/assign/submission/onlinetext/locallib.php
mod/assign/version.php
mod/assignment/lib.php
mod/assignment/type/online/assignment.class.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/forum/post_form.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/mod_form.php
mod/resource/pix/icon.gif
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/url/lib.php
mod/workshop/submission.php
pix/f/archive.png [new file with mode: 0644]
pix/f/audio.png
pix/f/avi.png
pix/f/base.png [new file with mode: 0644]
pix/f/bmp.png
pix/f/calc.png [new file with mode: 0644]
pix/f/chart.png [new file with mode: 0644]
pix/f/database.png
pix/f/document.png
pix/f/draw.png [new file with mode: 0644]
pix/f/eps.png
pix/f/flash.png
pix/f/gif.png
pix/f/image-128.png
pix/f/image-24.png
pix/f/image-256.png
pix/f/image-32.png
pix/f/image-48.png
pix/f/image-64.png
pix/f/image-72.png
pix/f/image-80.png
pix/f/image-96.png
pix/f/image.png
pix/f/impress.png [new file with mode: 0644]
pix/f/isf.gif [deleted file]
pix/f/isf.png [new file with mode: 0644]
pix/f/jpeg.png
pix/f/markup.png [new file with mode: 0644]
pix/f/math.png [new file with mode: 0644]
pix/f/moodle.png
pix/f/mp3.png
pix/f/mpeg.png
pix/f/oth.png
pix/f/pdf.png
pix/f/png.png
pix/f/powerpoint.png
pix/f/psd.png
pix/f/quicktime.png [new file with mode: 0644]
pix/f/sourcecode.png [new file with mode: 0644]
pix/f/spreadsheet.png
pix/f/text.png
pix/f/tiff.png
pix/f/video.png
pix/f/wav.png
pix/f/wmv.png
pix/f/writer.png [new file with mode: 0644]
question/behaviour/behaviourbase.php
question/previewlib.php
question/type/essay/renderer.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/pix/fp/dnd_arrow.gif [new file with mode: 0644]
theme/base/pix/fp/dnd_arrow.png [deleted file]
theme/base/style/core.css
theme/base/style/filemanager.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 f002f54..9e30ce1 100644 (file)
@@ -72,9 +72,10 @@ class enrol_mnet_addinstance_form extends moodleform {
      * Do not allow multiple instances for single remote host
      *
      * @param array $data raw form data
-     * @return array
+     * @param array $files
+     * @return array of errors
      */
-    function validation($data) {
+    function validation($data, $files) {
         global $DB;
 
         $errors = array();
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 d4b7077..2cbd0e9 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20120521" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20120531" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="repositoryid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" PREVIOUS="id" NEXT="lastsync"/>
         <FIELD NAME="lastsync" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Last time the proxy file was synced with repository" PREVIOUS="repositoryid" NEXT="lifetime"/>
         <FIELD NAME="lifetime" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="How often do we have to sync proxy file with repository" PREVIOUS="lastsync" NEXT="reference"/>
-        <FIELD NAME="reference" TYPE="text" NOTNULL="false" SEQUENCE="false" PREVIOUS="lifetime"/>
+        <FIELD NAME="reference" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Identification of the external file. Repository plugins are interpreting it to locate the external file." PREVIOUS="lifetime" NEXT="referencehash"/>
+        <FIELD NAME="referencehash" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="Internal implementation detail, contains SHA1 hash of the reference field. Can be indexed and used for comparison. Not meant to be used by a non-core code." PREVIOUS="reference"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="repositoryid"/>
         <KEY NAME="repositoryid" TYPE="foreign" FIELDS="repositoryid" REFTABLE="repository_instances" REFFIELDS="id" PREVIOUS="primary"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="uq_external_file" UNIQUE="true" FIELDS="repositoryid, referencehash" COMMENT="The combination of repositoryid and reference field is supposed to be a unique identification of an external file. Because the reference is a TEXT field, we can't use to compose the index. So we use the referencehash instead and the file API is responsible to keep it up-to-date"/>
+      </INDEXES>
     </TABLE>
     <TABLE NAME="repository" COMMENT="This table contains one entry for every configured external repository instance." PREVIOUS="files_reference" NEXT="repository_instances">
       <FIELDS>
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 ba53f99..f0c8ba1 100644 (file)
@@ -769,5 +769,66 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2012052900.05);
     }
 
+    if ($oldversion < 2012060600.01) {
+        // Add field referencehash to files_reference
+        $table = new xmldb_table('files_reference');
+        $field = new xmldb_field('referencehash', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null, 'reference');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+        upgrade_main_savepoint(true, 2012060600.01);
+    }
+
+    if ($oldversion < 2012060600.02) {
+        // Populate referencehash field with SHA1 hash of the reference - this shoudl affect only 2.3dev sites
+        // that were using the feature for testing. Production sites have the table empty.
+        $rs = $DB->get_recordset('files_reference', null, '', 'id, reference');
+        foreach ($rs as $record) {
+            $hash = sha1($record->reference);
+            $DB->set_field('files_reference', 'referencehash', $hash, array('id' => $record->id));
+        }
+        $rs->close();
+
+        upgrade_main_savepoint(true, 2012060600.02);
+    }
+
+    if ($oldversion < 2012060600.03) {
+        // Merge duplicate records in files_reference that were created during the development
+        // phase at 2.3dev sites. This is needed so we can create the unique index over
+        // (repositoryid, referencehash) fields.
+        $sql = "SELECT repositoryid, referencehash, MIN(id) AS minid
+                  FROM {files_reference}
+              GROUP BY repositoryid, referencehash
+                HAVING COUNT(*) > 1";
+        $duprs = $DB->get_recordset_sql($sql);
+        foreach ($duprs as $duprec) {
+            // get the list of all ids in {files_reference} that need to be remapped
+            $dupids = $DB->get_records_select('files_reference', "repositoryid = ? AND referencehash = ? AND id > ?",
+                array($duprec->repositoryid, $duprec->referencehash, $duprec->minid), '', 'id');
+            $dupids = array_keys($dupids);
+            // relink records in {files} that are now referring to a duplicate record
+            // in {files_reference} to refer to the first one
+            list($subsql, $subparams) = $DB->get_in_or_equal($dupids);
+            $DB->set_field_select('files', 'referencefileid', $duprec->minid, "referencefileid $subsql", $subparams);
+            // and finally remove all orphaned records from {files_reference}
+            $DB->delete_records_list('files_reference', 'id', $dupids);
+        }
+        $duprs->close();
+
+        upgrade_main_savepoint(true, 2012060600.03);
+    }
+
+    if ($oldversion < 2012060600.04) {
+        // Add a unique index over repositoryid and referencehash fields in files_reference table
+        $table = new xmldb_table('files_reference');
+        $index = new xmldb_index('uq_external_file', XMLDB_INDEX_UNIQUE, array('repositoryid', 'referencehash'));
+
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        upgrade_main_savepoint(true, 2012060600.04);
+    }
+
     return true;
-}
\ No newline at end of file
+}
index 68c01de..425e304 100644 (file)
@@ -116,23 +116,31 @@ class file_pool_content_exception extends file_exception {
     }
 }
 
+
 /**
- * Exception related to external file support
+ * Problem with records in the {files_reference} table
  *
  * @package   core_files
- * @category  files
- * @copyright 2012 Dongsheng Cai {@link http://dongsheng.org}
+ * @catehory  files
+ * @copyright 2012 David Mudrak <david@moodle.com>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class external_file_exception extends file_exception {
+class file_reference_exception extends file_exception {
     /**
      * Constructor
      *
-     * @param string   $errorcode error code
-     * @param stdClass $a extra information
-     * @param string   $debuginfo extra debug info
+     * @param int $repositoryid the id of the repository that provides the referenced file
+     * @param string $reference the information for the repository to locate the file
+     * @param int|null $referencefileid the id of the record in {files_reference} if known
+     * @param int|null $fileid the id of the referrer's record in {files} if known
+     * @param string|null $debuginfo extra debug info
      */
-    public function __construct($errorcode, $a = null, $debuginfo = null) {
-        parent::__construct($errorcode, '', '', $a, $debuginfo);
+    function __construct($repositoryid, $reference, $referencefileid=null, $fileid=null, $debuginfo=null) {
+        $a = new stdClass();
+        $a->repositoryid = $repositoryid;
+        $a->reference = $reference;
+        $a->referencefileid = $referencefileid;
+        $a->fileid = $fileid;
+        parent::__construct('filereferenceproblem', $a, $debuginfo);
     }
 }
index d5ce7c1..203337a 100644 (file)
@@ -859,19 +859,13 @@ class file_storage {
             return $this->get_file_instance($newrecord);
         }
 
+        // note: referencefileid is copied from the original file so that
+        // creating a new file from an existing alias creates new alias implicitly.
+        // here we just check the database consistency.
         if (!empty($newrecord->repositoryid)) {
-            try {
-                $referencerecord = new stdClass;
-                $referencerecord->repositoryid = $newrecord->repositoryid;
-                $referencerecord->reference = $newrecord->reference;
-                $referencerecord->lastsync  = $newrecord->referencelastsync;
-                $referencerecord->lifetime  = $newrecord->referencelifetime;
-                $referencerecord->id = $DB->insert_record('files_reference', $referencerecord);
-            } catch (dml_exception $e) {
-                throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
-                                                         $newrecord->filepath, $newrecord->filename, $e->debuginfo);
+            if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
+                throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
             }
-            $newrecord->referencefileid = $referencerecord->id;
         }
 
         try {
@@ -1180,12 +1174,12 @@ class file_storage {
     }
 
     /**
-     * Create a moodle file from file reference information
+     * Create a new alias/shortcut file from file reference information
      *
-     * @param stdClass $filerecord
-     * @param int $repositoryid
-     * @param string $reference
-     * @param array $options options for creating external file
+     * @param stdClass|array $filerecord object or array describing the new file
+     * @param int $repositoryid the id of the repository that provides the original file
+     * @param string $reference the information required by the repository to locate the original file
+     * @param array $options options for creating the new file
      * @return stored_file
      */
     public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
@@ -1268,20 +1262,13 @@ class file_storage {
 
         $transaction = $DB->start_delegated_transaction();
 
-        // Insert file reference record.
         try {
-            $referencerecord = new stdClass;
-            $referencerecord->repositoryid = $repositoryid;
-            $referencerecord->reference = $reference;
-            $referencerecord->lastsync = $filerecord->referencelastsync;
-            $referencerecord->lifetime = $filerecord->referencelifetime;
-            $referencerecord->id = $DB->insert_record('files_reference', $referencerecord);
-        } catch (dml_exception $e) {
-            throw $e;
+            $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference,
+                $filerecord->referencelastsync, $filerecord->referencelifetime);
+        } catch (Exception $e) {
+            throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
         }
 
-        $filerecord->referencefileid = $referencerecord->id;
-
         // External file doesn't have content in moodle.
         // So we create an empty file for it.
         list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
@@ -1672,59 +1659,74 @@ class file_storage {
     }
 
     /**
-     * Search references by providing reference content
+     * Returns all aliases that link to an external file identified by the given reference
      *
-     * @param string $str
-     * @return array
+     * Aliases in user draft areas are excluded from the returned list.
+     *
+     * @param string $reference identification of the referenced file
+     * @return array of stored_file indexed by its pathnamehash
      */
-    public function search_references($str) {
+    public function search_references($reference) {
         global $DB;
+
+        if (is_null($reference)) {
+            throw new coding_exception('NULL is not a valid reference to an external file');
+        }
+
+        $referencehash = sha1($reference);
+
         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
                   FROM {files} f
-             LEFT JOIN {files_reference} r
-                       ON f.referencefileid = r.id
-                 WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
-                 AND (f.component <> ? OR f.filearea <> ?)";
+                  JOIN {files_reference} r ON f.referencefileid = r.id
+                  JOIN {repository_instances} ri ON r.repositoryid = ri.id
+                 WHERE r.referencehash = ?
+                       AND (f.component <> ? OR f.filearea <> ?)";
 
-        $rs = $DB->get_recordset_sql($sql, array($str, 'user', 'draft'));
+        $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
         $files = array();
         foreach ($rs as $filerecord) {
-            $file = $this->get_file_instance($filerecord);
-            if ($file->is_external_file()) {
-                $files[$filerecord->pathnamehash] = $file;
-            }
+            $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
         }
 
         return $files;
     }
 
     /**
-     * Search references count by providing reference content
+     * Returns the number of aliases that link to an external file identified by the given reference
      *
-     * @param string $str
+     * Aliases in user draft areas are not counted.
+     *
+     * @param string $reference identification of the referenced file
      * @return int
      */
-    public function search_references_count($str) {
+    public function search_references_count($reference) {
         global $DB;
+
+        if (is_null($reference)) {
+            throw new coding_exception('NULL is not a valid reference to an external file');
+        }
+
+        $referencehash = sha1($reference);
+
         $sql = "SELECT COUNT(f.id)
                   FROM {files} f
-             LEFT JOIN {files_reference} r
-                       ON f.referencefileid = r.id
-                 WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
-                 AND (f.component <> ? OR f.filearea <> ?)";
+                  JOIN {files_reference} r ON f.referencefileid = r.id
+                  JOIN {repository_instances} ri ON r.repositoryid = ri.id
+                 WHERE r.referencehash = ?
+                       AND (f.component <> ? OR f.filearea <> ?)";
 
-        $count = $DB->count_records_sql($sql, array($str, 'user', 'draft'));
-        return $count;
+        return $DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
     }
 
     /**
-     * Return all files referring to provided stored_file instance
-     * This won't work for draft files
+     * Returns all aliases that link to the given stored_file
+     *
+     * Aliases in user draft areas are excluded from the returned list.
      *
      * @param stored_file $storedfile
-     * @return array
+     * @return array of stored_file
      */
-    public function get_references_by_storedfile($storedfile) {
+    public function get_references_by_storedfile(stored_file $storedfile) {
         global $DB;
 
         $params = array();
@@ -1734,37 +1736,19 @@ class file_storage {
         $params['itemid']    = $storedfile->get_itemid();
         $params['filename']  = $storedfile->get_filename();
         $params['filepath']  = $storedfile->get_filepath();
-        $params['userid']    = $storedfile->get_userid();
-
-        $reference = self::pack_reference($params);
 
-        $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
-                  FROM {files} f
-             LEFT JOIN {files_reference} r
-                       ON f.referencefileid = r.id
-                 WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
-                   AND (f.component <> ? OR f.filearea <> ?)";
-
-        $rs = $DB->get_recordset_sql($sql, array($reference, 'user', 'draft'));
-        $files = array();
-        foreach ($rs as $filerecord) {
-            $file = $this->get_file_instance($filerecord);
-            if ($file->is_external_file()) {
-                $files[$filerecord->pathnamehash] = $file;
-            }
-        }
-
-        return $files;
+        return $this->search_references(self::pack_reference($params));
     }
 
     /**
-     * Return the count files referring to provided stored_file instance
-     * This won't work for draft files
+     * Returns the number of aliases that link to the given stored_file
+     *
+     * Aliases in user draft areas are not counted.
      *
      * @param stored_file $storedfile
      * @return int
      */
-    public function get_references_count_by_storedfile($storedfile) {
+    public function get_references_count_by_storedfile(stored_file $storedfile) {
         global $DB;
 
         $params = array();
@@ -1774,19 +1758,8 @@ class file_storage {
         $params['itemid']    = $storedfile->get_itemid();
         $params['filename']  = $storedfile->get_filename();
         $params['filepath']  = $storedfile->get_filepath();
-        $params['userid']    = $storedfile->get_userid();
-
-        $reference = self::pack_reference($params);
-
-        $sql = "SELECT COUNT(f.id)
-                  FROM {files} f
-             LEFT JOIN {files_reference} r
-                       ON f.referencefileid = r.id
-                 WHERE ".$DB->sql_compare_text('r.reference').' = '.$DB->sql_compare_text('?')."
-                 AND (f.component <> ? OR f.filearea <> ?)";
 
-        $count = $DB->count_records_sql($sql, array($reference, 'user', 'draft'));
-        return $count;
+        return $this->search_references_count(self::pack_reference($params));
     }
 
     /**
@@ -1795,7 +1768,7 @@ class file_storage {
      * @param stored_file $storedfile a stored_file instances
      * @return stored_file stored_file
      */
-    public function import_external_file($storedfile) {
+    public function import_external_file(stored_file $storedfile) {
         global $CFG;
         require_once($CFG->dirroot.'/repository/lib.php');
         // sync external file
@@ -1925,5 +1898,60 @@ class file_storage {
 
         return implode(', ', $fields);
     }
-}
 
+    /**
+     * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
+     *
+     * If the record already exists, its id is returned. If there is no such record yet,
+     * new one is created (using the lastsync and lifetime provided, too) and its id is returned.
+     *
+     * @param int $repositoryid
+     * @param string $reference
+     * @return int
+     */
+    private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
+        global $DB;
+
+        $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
+
+        if ($id !== false) {
+            // bah, that was easy
+            return $id;
+        }
+
+        // no such record yet, create one
+        try {
+            $id = $DB->insert_record('files_reference', array(
+                'repositoryid'  => $repositoryid,
+                'reference'     => $reference,
+                'referencehash' => sha1($reference),
+                'lastsync'      => $lastsync,
+                'lifetime'      => $lifetime));
+        } catch (dml_exception $e) {
+            // if inserting the new record failed, chances are that the race condition has just
+            // occured and the unique index did not allow to create the second record with the same
+            // repositoryid + reference combo
+            $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
+        }
+
+        return $id;
+    }
+
+    /**
+     * Returns the id of the record in {files_reference} that matches the passed parameters
+     *
+     * Depending on the required strictness, false can be returned. The behaviour is consistent
+     * with standard DML methods.
+     *
+     * @param int $repositoryid
+     * @param string $reference
+     * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
+     * @return int|bool
+     */
+    private function get_referencefileid($repositoryid, $reference, $strictness) {
+        global $DB;
+
+        return $DB->get_field('files_reference', 'id',
+            array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
+    }
+}
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 41feea1..818d47b 100644 (file)
@@ -297,7 +297,7 @@ class filestoragelib_testcase extends advanced_testcase {
             $this->assertEquals($key, $file->get_pathnamehash());
         }
 
-        // Get area files ordered by id (was breaking on oracle).
+        // Get area files ordered by id.
         $filesbyid  = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'id', false);
         // Should be the two files without folder.
         $this->assertEquals(3, count($filesbyid));
@@ -405,8 +405,6 @@ class filestoragelib_testcase extends advanced_testcase {
         $fs = get_file_storage();
 
         $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private');
-        //Test get_file_by_hash
-
         $testfile = reset($areafiles);
         $references = $fs->get_references_by_storedfile($testfile);
         // TODO MDL-33368 Verify result!!
@@ -420,9 +418,57 @@ class filestoragelib_testcase extends advanced_testcase {
         $userrepository = reset($repos);
         $this->assertInstanceOf('repository', $userrepository);
 
-        // This should break on oracle.
-        $fs->get_external_files($userrepository->id, 'id');
-        // TODO MDL-33368 Verify result!!
+        // no aliases yet
+        $exfiles = $fs->get_external_files($userrepository->id, 'id');
+        $this->assertEquals(array(), $exfiles);
+
+        // create three aliases linking the same original: $aliasfile1 and $aliasfile2 are
+        // created via create_file_from_reference(), $aliasfile3 created from $aliasfile2
+        $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);
+        $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 = '/foo/';
+        $aliasrecord->filename = 'one.txt';
+
+        $ref = $fs->pack_reference($originalrecord);
+        $aliasfile1 = $fs->create_file_from_reference($aliasrecord, $userrepository->id, $ref);
+
+        $aliasrecord->filepath = '/bar/';
+        $aliasrecord->filename = 'uno.txt';
+        // change the order of the items in the array to make sure that it does not matter
+        ksort($originalrecord);
+        $ref = $fs->pack_reference($originalrecord);
+        $aliasfile2 = $fs->create_file_from_reference($aliasrecord, $userrepository->id, $ref);
+
+        $aliasrecord->filepath = '/bar/';
+        $aliasrecord->filename = 'jedna.txt';
+        $aliasfile3 = $fs->create_file_from_storedfile($aliasrecord, $aliasfile2);
+
+        // make sure we get three aliases now
+        $exfiles = $fs->get_external_files($userrepository->id, 'id');
+        $this->assertEquals(3, count($exfiles));
+        foreach ($exfiles as $exfile) {
+            $this->assertTrue($exfile->is_external_file());
+        }
+        // make sure they all link the same original (thence that all are linked with the same
+        // record in {files_reference})
+        $this->assertEquals($aliasfile1->get_referencefileid(), $aliasfile2->get_referencefileid());
+        $this->assertEquals($aliasfile3->get_referencefileid(), $aliasfile2->get_referencefileid());
     }
 
     public function test_create_directory_contextid_negative() {
@@ -1166,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 f36580b..0f054a5 100644 (file)
@@ -52,7 +52,8 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element {
 
     /** @var array options provided to initalize filepicker */
     protected $_options    = array('subdirs'=>0, 'maxbytes'=>0, 'maxfiles'=>0, 'changeformat'=>0,
-                                   'context'=>null, 'noclean'=>0, 'trusttext'=>0);
+                                   'context'=>null, 'noclean'=>0, 'trusttext'=>0, 'return_types'=>7);
+    // $_options['return_types'] = FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE
 
     /** @var array values for editor */
     protected $_values     = array('text'=>null, 'format'=>null, 'itemid'=>null);
@@ -303,7 +304,7 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element {
             $args = new stdClass();
             // need these three to filter repositories list
             $args->accepted_types = array('web_image');
-            $args->return_types = (FILE_INTERNAL | FILE_EXTERNAL | FILE_REFERENCE);
+            $args->return_types = $this->_options['return_types'];
             $args->context = $ctx;
             $args->env = 'filepicker';
             // advimage plugin
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 77581d7..d4c71fd 100644 (file)
 $messageproviders = array (
 
     // Ordinary assignment submissions
-    'assign_student_notification' => array (
-        'capability' => 'mod/assign:submit'
+    'assign_student_notification' => array(
     ),
-    'assign_grader_notification' => array (
+    'assign_grader_notification' => array(
         'capability' => 'mod/assign:grade'
     )
 
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 f307a12..8f73ac1 100644 (file)
@@ -73,11 +73,11 @@ class mod_assign_grade_form extends moodleform {
         }
 
         if ($this->assignment->get_instance()->grade > 0) {
-            if (!is_numeric($data['grade']) and (!empty($data['grade']))) {
+            if (unformat_float($data['grade']) === null && (!empty($data['grade']))) {
                 $errors['grade'] = get_string('invalidfloatforgrade', 'assign', $data['grade']);
-            } else if ($data['grade'] > $this->assignment->get_instance()->grade) {
+            } else if (unformat_float($data['grade']) > $this->assignment->get_instance()->grade) {
                 $errors['grade'] = get_string('gradeabovemaximum', 'assign', $this->assignment->get_instance()->grade);
-            } else if ($data['grade'] < 0) {
+            } else if (unformat_float($data['grade']) < 0) {
                 $errors['grade'] = get_string('gradebelowzero', 'assign');
             }
         } else {
index db4432d..6d11be0 100644 (file)
@@ -414,7 +414,7 @@ class assign_grading_table extends table_sql implements renderable {
             if ($row->locked) {
                 $o .= $this->output->container(get_string('submissionslockedshort', 'assign'), 'lockedsubmission');
             }
-            if ($row->grade) {
+            if ($row->grade >= 0) {
                 $o .= $this->output->container(get_string('graded', 'assign'), 'submissiongraded');
             }
         }
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 bb203f0..86e8b92 100644 (file)
@@ -895,7 +895,12 @@ class assign {
         if ($this->get_instance()->grade >= 0) {
             // Normal number
             if ($editing) {
-                $o = '<input type="text" name="quickgrade_' . $userid . '" value="' . $grade . '" size="6" maxlength="10" class="quickgrade"/>';
+                if ($grade < 0) {
+                    $displaygrade = '';
+                } else {
+                    $displaygrade = format_float($grade);
+                }
+                $o = '<input type="text" name="quickgrade_' . $userid . '" value="' . $displaygrade . '" size="6" maxlength="10" class="quickgrade"/>';
                 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade,2);
                 $o .= '<input type="hidden" name="grademodified_' . $userid . '" value="' . $modified . '"/>';
                 return $o;
@@ -1636,7 +1641,7 @@ class assign {
         }
         if ($grade) {
             $data = new stdClass();
-            if ($grade->grade >= 0) {
+            if ($grade->grade !== NULL && $grade->grade >= 0) {
                 $data->grade = format_float($grade->grade,2);
             }
         } else {
@@ -1897,7 +1902,7 @@ class assign {
     private function is_graded($userid) {
         $grade = $this->get_user_grade($userid, false);
         if ($grade) {
-            return ($grade->grade != '');
+            return ($grade->grade !== NULL && $grade->grade >= 0);
         }
         return false;
     }
@@ -2531,7 +2536,7 @@ class assign {
                 // gather the userid, updated grade and last modified value
                 $record = new stdClass();
                 $record->userid = $userid;
-                $record->grade = required_param('quickgrade_' . $userid, PARAM_INT);
+                $record->grade = unformat_float(required_param('quickgrade_' . $record->userid, PARAM_TEXT));
                 $record->lastmodified = $modified;
                 $record->gradinginfo = grade_get_grades($this->get_course()->id, 'mod', 'assign', $this->get_instance()->id, array($userid));
                 $users[$userid] = $record;
@@ -2560,7 +2565,7 @@ class assign {
             if ($CFG->enableoutcomes) {
                 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
                     $oldoutcome = $outcome->grades[$modified->userid]->grade;
-                    $newoutcome = optional_param('outcome_' . $outcomeid . '_' . $modified->userid, -1, PARAM_INT);
+                    $newoutcome = optional_param('outcome_' . $outcomeid . '_' . $modified->userid, -1, PARAM_FLOAT);
                     if ($oldoutcome != $newoutcome) {
                         // can't check modified time for outcomes because it is not reported
                         $modifiedusers[$modified->userid] = $modified;
@@ -2605,7 +2610,7 @@ class assign {
         // ok - ready to process the updates
         foreach ($modifiedusers as $userid => $modified) {
             $grade = $this->get_user_grade($userid, true);
-            $grade->grade= grade_floatval($modified->grade);
+            $grade->grade= grade_floatval(unformat_float($modified->grade));
             $grade->grader= $USER->id;
 
             $this->update_grade($grade);
@@ -3180,7 +3185,7 @@ class assign {
             } else {
                 // handle the case when grade is set to No Grade
                 if (isset($formdata->grade)) {
-                    $grade->grade= grade_floatval($formdata->grade);
+                    $grade->grade= grade_floatval(unformat_float($formdata->grade));
                 }
             }
             $grade->grader= $USER->id;
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 0ab3777..cfb95ad 100644 (file)
@@ -106,7 +106,8 @@ class assign_submission_onlinetext extends assign_submission_plugin {
            'noclean' => false,
            'maxfiles' => EDITOR_UNLIMITED_FILES,
            'maxbytes' => $this->assignment->get_course()->maxbytes,
-           'context' => $this->assignment->get_context()
+           'context' => $this->assignment->get_context(),
+           'return_types' => FILE_INTERNAL | FILE_EXTERNAL
         );
         return $editoroptions;
     }
index b2b2bfa..af599d0 100644 (file)
@@ -25,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $module->component = 'mod_assign'; // Full name of the plugin (used for diagnostics)
-$module->version  = 2012051700;    // The current module version (Date: YYYYMMDDXX)
+$module->version  = 2012061100;    // The current module version (Date: YYYYMMDDXX)
 $module->requires = 2012050300;    // Requires this Moodle version
 $module->cron     = 60;
 
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 163bdab..48819c7 100644 (file)
@@ -42,7 +42,8 @@ class assignment_online extends assignment_base {
                 'noclean'  => false,
                 'maxfiles' => EDITOR_UNLIMITED_FILES,
                 'maxbytes' => $this->course->maxbytes,
-                'context'  => $this->context
+                'context'  => $this->context,
+                'return_types' => FILE_INTERNAL | FILE_EXTERNAL
             );
 
             $data = new stdClass();
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 e3caa53..b475684 100644 (file)
@@ -45,7 +45,8 @@ class mod_forum_post_form extends moodleform {
             $forum->maxbytes = $course->maxbytes;
         }
         // TODO: add max files and max size support
-        $editoroptions = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'trusttext'=>true, 'context'=>$modcontext);
+        $editoroptions = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'trusttext'=>true,
+            'context'=>$modcontext, 'return_types'=>FILE_INTERNAL | FILE_EXTERNAL);
 
         $mform->addElement('header', 'general', '');//fill in the data depending on page params later using set_data
         $mform->addElement('text', 'subject', get_string('subject', 'forum'), 'size="48"');
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..2fc0604 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';
@@ -521,9 +522,11 @@ $string['overallfeedback_help'] = 'Overall feedback is text that is shown after
 $string['overdue'] = 'Overdue';
 $string['overduehandling'] = 'When time expires';
 $string['overduehandling_desc'] = 'What should happen by default if a student does not submit the quiz before time expires.';
-$string['overduehandlingautosubmit'] = 'the attempt is submitted automatically';
-$string['overduehandlinggraceperiod'] = 'there is a grace period in which to submit the attempt, but not answer more questions';
-$string['overduehandlingautoabandon'] = 'that is it. The attempt must be submitted before time expires, or it is not counted';
+$string['overduehandling_help'] = 'This setting controls what happens if the Student fails to submit their quiz attempt before time expires. If the student is actively working on the quiz at the time, then the countdown timer will always automatically submit the attempt for them, but if they have logged out, then this setting controls what happens.';
+$string['overduehandling_link'] = 'mod/quiz/timing';
+$string['overduehandlingautosubmit'] = 'Open attempts are submitted automatically';
+$string['overduehandlinggraceperiod'] = 'There is a grace period when open attempts can be submitted, but no more questions answered';
+$string['overduehandlingautoabandon'] = 'Attempts must be submitted before time expires, or they are not counted';
 $string['overduemustbesubmittedby'] = 'This attempt is now overdue. It should already have been submitted. If you would like this quiz to be graded, you must submit it by {$a}. If you do not submit it by then, no marks from this attempt will be counted.';
 $string['override'] = 'Override';
 $string['overridedeletegroupsure'] = 'Are you sure you want to delete the override for group {$a}?';
@@ -608,6 +611,7 @@ $string['quiznavigation'] = 'Quiz navigation';
 $string['quizopen'] = 'Open the quiz';
 $string['quizopenclose'] = 'Open and close dates';
 $string['quizopenclose_help'] = 'Students can only start their attempt(s) after the open time and they must complete their attempts before the close time.';
+$string['quizopenclose_link'] = 'mod/quiz/timing';
 $string['quizopened'] = 'This quiz is open.';
 $string['quizopenedon'] = 'This quiz opened at {$a}';
 $string['quizopens'] = 'Quiz opens';
@@ -808,7 +812,8 @@ $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['timelimit_link'] = 'mod/quiz/timing';
 $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..a074514 100644 (file)
@@ -94,6 +94,7 @@ class mod_quiz_mod_form extends moodleform_mod {
         // What to do with overdue attempts.
         $mform->addElement('select', 'overduehandling', get_string('overduehandling', 'quiz'),
                 quiz_get_overdue_handling_options());
+        $mform->addHelpButton('overduehandling', 'overduehandling', 'quiz');
         $mform->setAdvanced('overduehandling', $quizconfig->overduehandling_adv);
         $mform->setDefault('overduehandling', $quizconfig->overduehandling);
         // TODO Formslib does OR logic on disableif, and we need AND logic here.
@@ -502,6 +503,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 01560dd..7b9d645 100644 (file)
@@ -61,9 +61,6 @@ class mod_resource_mod_form extends moodleform_mod {
         $mform->addElement('header', 'contentsection', get_string('contentheader', 'resource'));
 
         $filemanager_options = array();
-        // 3 == FILE_EXTERNAL | FILE_INTERNAL | FILE_REFERENCE
-        // These two constant names are defined in repository/lib.php
-        $filemanager_options['return_types'] = 7;
         $filemanager_options['accepted_types'] = '*';
         $filemanager_options['maxbytes'] = 0;
         $filemanager_options['maxfiles'] = -1;
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 efcff41..e17f13b 100644 (file)
@@ -144,7 +144,8 @@ if ($edit) {
                         'subdirs'   => false,
                         'maxfiles'  => $maxfiles,
                         'maxbytes'  => $maxbytes,
-                        'context'   => $workshop->context
+                        'context'   => $workshop->context,
+                        'return_types' => FILE_INTERNAL | FILE_EXTERNAL
                       );
 
     $attachmentopts = array('subdirs' => true, 'maxfiles' => $maxfiles, 'maxbytes' => $maxbytes, 'return_types' => FILE_INTERNAL);
diff --git a/pix/f/archive.png b/pix/f/archive.png
new file mode 100644 (file)
index 0000000..6b20595
Binary files /dev/null and b/pix/f/archive.png differ
index 6882b6f..88bf08d 100644 (file)
Binary files a/pix/f/audio.png and b/pix/f/audio.png differ
index 4491e22..593ffd5 100644 (file)
Binary files a/pix/f/avi.png and b/pix/f/avi.png differ
diff --git a/pix/f/base.png b/pix/f/base.png
new file mode 100644 (file)
index 0000000..9248c8b
Binary files /dev/null and b/pix/f/base.png differ
index a53c85f..b00e6a2 100644 (file)
Binary files a/pix/f/bmp.png and b/pix/f/bmp.png differ
diff --git a/pix/f/calc.png b/pix/f/calc.png
new file mode 100644 (file)
index 0000000..66e9833
Binary files /dev/null and b/pix/f/calc.png differ
diff --git a/pix/f/chart.png b/pix/f/chart.png
new file mode 100644 (file)
index 0000000..ef0b6ee
Binary files /dev/null and b/pix/f/chart.png differ
index 74a2d13..bd55b8b 100644 (file)
Binary files a/pix/f/database.png and b/pix/f/database.png differ
index 1dfb712..253a84c 100644 (file)
Binary files a/pix/f/document.png and b/pix/f/document.png differ
diff --git a/pix/f/draw.png b/pix/f/draw.png
new file mode 100644 (file)
index 0000000..3067ddc
Binary files /dev/null and b/pix/f/draw.png differ
index 8075e23..2c10d87 100644 (file)
Binary files a/pix/f/eps.png and b/pix/f/eps.png differ
index 2cff187..1cd9e63 100644 (file)
Binary files a/pix/f/flash.png and b/pix/f/flash.png differ
index 2e6d160..8f887fa 100644 (file)
Binary files a/pix/f/gif.png and b/pix/f/gif.png differ
index f70d02f..b40a733 100644 (file)
Binary files a/pix/f/image-128.png and b/pix/f/image-128.png differ
index 779415d..42820c9 100644 (file)
Binary files a/pix/f/image-24.png and b/pix/f/image-24.png differ
index ad1c5e7..b081d00 100644 (file)
Binary files a/pix/f/image-256.png and b/pix/f/image-256.png differ
index bfdf341..066ef6c 100644 (file)
Binary files a/pix/f/image-32.png and b/pix/f/image-32.png differ
index 4e46413..1f14a57 100644 (file)
Binary files a/pix/f/image-48.png and b/pix/f/image-48.png differ
index cbed5fb..c181eaa 100644 (file)
Binary files a/pix/f/image-64.png and b/pix/f/image-64.png differ
index 14b9623..72d3390 100644 (file)
Binary files a/pix/f/image-72.png and b/pix/f/image-72.png differ
index 5e23b6c..a20c56b 100644 (file)
Binary files a/pix/f/image-80.png and b/pix/f/image-80.png differ
index 270814b..cbf244f 100644 (file)
Binary files a/pix/f/image-96.png and b/pix/f/image-96.png differ
index a5cd485..dbdbe7f 100644 (file)
Binary files a/pix/f/image.png and b/pix/f/image.png differ
diff --git a/pix/f/impress.png b/pix/f/impress.png
new file mode 100644 (file)
index 0000000..fe4d72f
Binary files /dev/null and b/pix/f/impress.png differ
diff --git a/pix/f/isf.gif b/pix/f/isf.gif
deleted file mode 100644 (file)
index 003ac11..0000000
Binary files a/pix/f/isf.gif and /dev/null differ
diff --git a/pix/f/isf.png b/pix/f/isf.png
new file mode 100644 (file)
index 0000000..762a8a9
Binary files /dev/null and b/pix/f/isf.png differ
index c1fd299..98c868b 100644 (file)
Binary files a/pix/f/jpeg.png and b/pix/f/jpeg.png differ
diff --git a/pix/f/markup.png b/pix/f/markup.png
new file mode 100644 (file)
index 0000000..a067c73
Binary files /dev/null and b/pix/f/markup.png differ
diff --git a/pix/f/math.png b/pix/f/math.png
new file mode 100644 (file)
index 0000000..2d1adc0
Binary files /dev/null and b/pix/f/math.png differ
index 70e3f1b..cc40b7b 100644 (file)
Binary files a/pix/f/moodle.png and b/pix/f/moodle.png differ
index 36db610..3e3b58c 100644 (file)
Binary files a/pix/f/mp3.png and b/pix/f/mp3.png differ
index 3225907..35a7a1d 100644 (file)
Binary files a/pix/f/mpeg.png and b/pix/f/mpeg.png differ
index 57cbf55..24ffc97 100644 (file)
Binary files a/pix/f/oth.png and b/pix/f/oth.png differ
index 92bd3a6..b35e4a1 100644 (file)
Binary files a/pix/f/pdf.png and b/pix/f/pdf.png differ
index 41d52c6..bcc7eed 100644 (file)
Binary files a/pix/f/png.png and b/pix/f/png.png differ
index c127b1d..29b0b3c 100644 (file)
Binary files a/pix/f/powerpoint.png and b/pix/f/powerpoint.png differ
index a6657b2..86690db 100644 (file)
Binary files a/pix/f/psd.png and b/pix/f/psd.png differ
diff --git a/pix/f/quicktime.png b/pix/f/quicktime.png
new file mode 100644 (file)
index 0000000..97b96fb
Binary files /dev/null and b/pix/f/quicktime.png differ
diff --git a/pix/f/sourcecode.png b/pix/f/sourcecode.png
new file mode 100644 (file)
index 0000000..767dfde
Binary files /dev/null and b/pix/f/sourcecode.png differ
index 2fa2c9e..1f35f32 100644 (file)
Binary files a/pix/f/spreadsheet.png and b/pix/f/spreadsheet.png differ
index 91bd190..e9ec05b 100644 (file)
Binary files a/pix/f/text.png and b/pix/f/text.png differ
index 567679a..2c51c32 100644 (file)
Binary files a/pix/f/tiff.png and b/pix/f/tiff.png differ
index 726d1ba..bcefa46 100644 (file)
Binary files a/pix/f/video.png and b/pix/f/video.png differ
index c479357..8927684 100644 (file)
Binary files a/pix/f/wav.png and b/pix/f/wav.png differ
index 67fc572..bcefa46 100644 (file)
Binary files a/pix/f/wmv.png and b/pix/f/wmv.png differ
diff --git a/pix/f/writer.png b/pix/f/writer.png
new file mode 100644 (file)
index 0000000..94d30fe
Binary files /dev/null and b/pix/f/writer.png differ
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 288954f..ac2bac9 100644 (file)
@@ -109,6 +109,7 @@ class qtype_essay_renderer extends qtype_renderer {
         $pickeroptions->itemid = $qa->prepare_response_files_draft_itemid(
                 'attachments', $options->context->id);
         $pickeroptions->context = $options->context;
+        $pickeroptions->return_types = FILE_INTERNAL;
 
         $pickeroptions->itemid = $qa->prepare_response_files_draft_itemid(
                 'attachments', $options->context->id);
@@ -280,7 +281,7 @@ class qtype_essay_format_editor_renderer extends plugin_renderer_base {
      * @return array filepicker options for the editor.
      */
     protected function get_filepicker_options($context, $draftitemid) {
-        return array();
+        return array('return_types'  => FILE_INTERNAL | FILE_EXTERNAL);
     }
 
     /**
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'));
diff --git a/theme/base/pix/fp/dnd_arrow.gif b/theme/base/pix/fp/dnd_arrow.gif
new file mode 100644 (file)
index 0000000..9a474a5
Binary files /dev/null and b/theme/base/pix/fp/dnd_arrow.gif differ
diff --git a/theme/base/pix/fp/dnd_arrow.png b/theme/base/pix/fp/dnd_arrow.png
deleted file mode 100644 (file)
index 771f95a..0000000
Binary files a/theme/base/pix/fp/dnd_arrow.png and /dev/null differ
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 b6ed919..09f98df 100644 (file)
@@ -274,8 +274,7 @@ a.ygtvspacer:hover {color: transparent;text-decoration: none;}
 .dndsupported .dndupload-ready .dndupload-target {display:block;}
 .dndupload-uploadinprogress {display:none;text-align:center;}
 .dndupload-uploading .dndupload-uploadinprogress {display:block;}
-.dndupload-arrow {background:url('[[pix:theme|fp/dnd_arrow]]') center no-repeat;width:56px;height:47px;position:absolute;margin-left: -28px;/*right:46%;left:46%;*/animation:mymove 5s infinite;-moz-animation:mymove 5s infinite;-webkit-animation:mymove 5s infinite;}
-@keyframes mymove {0%{top:10px;} 12%{top:40px;} 30%{top:20px} 65%{top:35px;} 100%{top:9px;}}@-moz-keyframes mymove{0%{top:10px;} 12%{top:40px;} 30%{top:20px} 65%{top:35px;} 100%{top:9px;}}@-webkit-keyframes mymove {0%{top:10px;} 12%{top:40px;} 30%{top:20px} 65%{top:35px;} 100%{top:9px;}}
+.dndupload-arrow {background:url([[pix:theme|fp/dnd_arrow]]) center no-repeat;width:60px;height:80px;position:absolute;margin-left: -28px;top: 5px;}
 
 /*
  * Select Dialogue (File Manager only)
index 813b25b..8aef9ad 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 
-$version  = 2012060400.00;              // 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