diff --git a/course/dndupload.js b/course/dndupload.js
new file mode 100644
index 0000000000000..d5caff3207013
--- /dev/null
+++ b/course/dndupload.js
@@ -0,0 +1,884 @@
+// 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 .
+
+/**
+ * Javascript library for enableing a drag and drop upload to courses
+ *
+ * @package moodlecore
+ * @subpackage course
+ * @copyright 2012 Davo Smith
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+M.course_dndupload = {
+ // YUI object.
+ Y: null,
+ // URL for upload requests
+ url: M.cfg.wwwroot + '/course/dndupload.php',
+ // maximum size of files allowed in this form
+ maxbytes: 0,
+ // ID of the course we are on
+ courseid: null,
+ // Data about the different file/data handlers that are available
+ handlers: null,
+ // Nasty hack to distinguish between dragenter(first entry),
+ // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
+ entercount: 0,
+ // Used to keep track of the section we are dragging across - to make
+ // spotting movement between sections more reliable
+ currentsection: null,
+ // Used to store the pending uploads whilst the user is being asked for further input
+ uploadqueue: null,
+ // True if the there is currently a dialog being shown (asking for a name, or giving a
+ // choice of file handlers)
+ uploaddialog: false,
+ // An array containing the last selected file handler for each file type
+ lastselected: null,
+
+ // The following are used to identify specific parts of the course page
+
+ // The type of HTML element that is a course section
+ sectiontypename: 'li',
+ // The classes that an element must have to be identified as a course section
+ sectionclasses: ['section', 'main'],
+ // The ID of the main content area of the page (for adding the 'status' div)
+ pagecontentid: 'page-content',
+ // The selector identifying the list of modules within a section (note changing this may require
+ // changes to the get_mods_element function)
+ modslistselector: 'ul.section',
+
+ /**
+ * Initalise the drag and drop upload interface
+ * Note: one and only one of options.filemanager and options.formcallback must be defined
+ *
+ * @param Y the YUI object
+ * @param object options {
+ * courseid: ID of the course we are on
+ * maxbytes: maximum size of files allowed in this form
+ * handlers: Data about the different file/data handlers that are available
+ * }
+ */
+ init: function(Y, options) {
+ this.Y = Y;
+
+ if (!this.browser_supported()) {
+ return; // Browser does not support the required functionality
+ }
+
+ this.maxbytes = options.maxbytes;
+ this.courseid = options.courseid;
+ this.handlers = options.handlers;
+ this.uploadqueue = new Array();
+ this.lastselected = new Array();
+
+ var sectionselector = this.sectiontypename + '.' + this.sectionclasses.join('.');
+ var sections = this.Y.all(sectionselector);
+ if (sections.isEmpty()) {
+ return; // No sections - incompatible course format or front page.
+ }
+ sections.each( function(el) {
+ this.add_preview_element(el);
+ this.init_events(el);
+ }, this);
+
+ var div = this.add_status_div();
+ div.setContent(M.util.get_string('dndworking', 'moodle'));
+ },
+
+ /**
+ * Add a div element to tell the user that drag and drop upload
+ * is available (or to explain why it is not available)
+ * @return the DOM element to add messages to
+ */
+ add_status_div: function() {
+ var div = document.createElement('div');
+ div.id = 'dndupload-status';
+ var coursecontents = document.getElementById(this.pagecontentid);
+ if (coursecontents) {
+ coursecontents.insertBefore(div, coursecontents.firstChild);
+ }
+ return this.Y.one(div);
+ },
+
+ /**
+ * Check the browser has the required functionality
+ * @return true if browser supports drag/drop upload
+ */
+ browser_supported: function() {
+ if (typeof FileReader == 'undefined') {
+ return false;
+ }
+ if (typeof FormData == 'undefined') {
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Initialise drag events on node container, all events need
+ * to be processed for drag and drop to work
+ * @param el the element to add events to
+ */
+ init_events: function(el) {
+ this.Y.on('dragenter', this.drag_enter, el, this);
+ this.Y.on('dragleave', this.drag_leave, el, this);
+ this.Y.on('dragover', this.drag_over, el, this);
+ this.Y.on('drop', this.drop, el, this);
+ },
+
+ /**
+ * Work out which course section a given element is in
+ * @param el the child DOM element within the section
+ * @return the DOM element representing the section
+ */
+ get_section: function(el) {
+ var sectionclasses = this.sectionclasses;
+ return el.ancestor( function(test) {
+ var i;
+ for (i=0; i 2) {
+ this.entercount = 2;
+ return false;
+ }
+ }
+
+ this.show_preview_element(section, type);
+
+ return false;
+ },
+
+ /**
+ * Handle a dragleave event: remove the 'add here' message (if present)
+ * @param e event data
+ * @return false to prevent the event from continuing to be processed
+ */
+ drag_leave: function(e) {
+ if (!this.check_drag(e)) {
+ return false;
+ }
+
+ this.entercount--;
+ if (this.entercount == 1) {
+ return false;
+ }
+ this.entercount = 0;
+ this.currentsection = null;
+
+ this.hide_preview_element();
+ return false;
+ },
+
+ /**
+ * Handle a dragover event: just prevent the browser default (necessary
+ * to allow drag and drop handling to work)
+ * @param e event data
+ * @return false to prevent the event from continuing to be processed
+ */
+ drag_over: function(e) {
+ this.check_drag(e);
+ return false;
+ },
+
+ /**
+ * Handle a drop event: hide the 'add here' message, check the attached
+ * data type and start the upload process
+ * @param e event data
+ * @return false to prevent the event from continuing to be processed
+ */
+ drop: function(e) {
+ if (!(type = this.check_drag(e))) {
+ return false;
+ }
+
+ this.hide_preview_element();
+
+ // Work out the number of the section we are on (from its id)
+ var section = this.get_section(e.currentTarget);
+ var sectionnumber = this.get_section_number(section);
+
+ // Process the file or the included data
+ if (type.type == 'Files') {
+ var files = e._event.dataTransfer.files;
+ for (var i=0, f; f=files[i]; i++) {
+ this.handle_file(f, section, sectionnumber);
+ }
+ } else {
+ var contents = e._event.dataTransfer.getData(type.realtype);
+ if (contents) {
+ this.handle_item(type, contents, section, sectionnumber);
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Find or create the 'ul' element that contains all of the module
+ * instances in this section
+ * @param section the DOM element representing the section
+ * @return false to prevent the event from continuing to be processed
+ */
+ get_mods_element: function(section) {
+ // Find the 'ul' containing the list of mods
+ var modsel = section.one(this.modslistselector);
+ if (!modsel) {
+ // Create the above 'ul' if it doesn't exist
+ var modsel = document.createElement('ul');
+ modsel.className = 'section img-text';
+ var contentel = section.get('children').pop();
+ var brel = contentel.get('children').pop();
+ contentel.insertBefore(modsel, brel);
+ modsel = this.Y.one(modsel);
+ }
+
+ return modsel;
+ },
+
+ /**
+ * Add a new dummy item to the list of mods, to be replaced by a real
+ * item & link once the AJAX upload call has completed
+ * @param name the label to show in the element
+ * @param section the DOM element reperesenting the course section
+ * @return DOM element containing the new item
+ */
+ add_resource_element: function(name, section) {
+ var modsel = this.get_mods_element(section);
+
+ var resel = {
+ parent: modsel,
+ li: document.createElement('li'),
+ div: document.createElement('div'),
+ a: document.createElement('a'),
+ icon: document.createElement('img'),
+ namespan: document.createElement('span'),
+ progressouter: document.createElement('span'),
+ progress: document.createElement('span')
+ };
+
+ resel.li.className = 'activity resource modtype_resource';
+
+ resel.div.className = 'mod-indent';
+ resel.li.appendChild(resel.div);
+
+ resel.a.href = '#';
+ resel.div.appendChild(resel.a);
+
+ resel.icon.src = M.util.image_url('i/ajaxloader');
+ resel.a.appendChild(resel.icon);
+
+ resel.a.appendChild(document.createTextNode(' '));
+
+ resel.namespan.className = 'instancename';
+ resel.namespan.innerHTML = name;
+ resel.a.appendChild(resel.namespan);
+
+ resel.div.appendChild(document.createTextNode(' '));
+
+ resel.progressouter.className = 'dndupload-progress-outer';
+ resel.progress.className = 'dndupload-progress-inner';
+ resel.progress.innerHTML = ' ';
+ resel.progressouter.appendChild(resel.progress);
+ resel.div.appendChild(resel.progressouter);
+
+ modsel.insertBefore(resel.li, modsel.get('children').pop()); // Leave the 'preview element' at the bottom
+
+ return resel;
+ },
+
+ /**
+ * Hide any visible dndupload-preview elements on the page
+ */
+ hide_preview_element: function() {
+ this.Y.all('li.dndupload-preview').addClass('dndupload-hidden');
+ },
+
+ /**
+ * Unhide the preview element for the given section and set it to display
+ * the correct message
+ * @param section the YUI node representing the selected course section
+ * @param type the details of the data type detected in the drag (including the message to display)
+ */
+ show_preview_element: function(section, type) {
+ this.hide_preview_element();
+ var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden');
+ preview.one('span').setContent(type.addmessage);
+ },
+
+ /**
+ * Add the preview element to a course section. Note: this needs to be done before 'addEventListener'
+ * is called, otherwise Firefox will ignore events generated when the mouse is over the preview
+ * element (instead of passing them up to the parent element)
+ * @param section the YUI node representing the selected course section
+ */
+ add_preview_element: function(section) {
+ var modsel = this.get_mods_element(section);
+ var preview = {
+ li: document.createElement('li'),
+ div: document.createElement('div'),
+ icon: document.createElement('img'),
+ namespan: document.createElement('span')
+ };
+
+ preview.li.className = 'dndupload-preview activity resource modtype_resource dndupload-hidden';
+
+ preview.div.className = 'mod-indent';
+ preview.li.appendChild(preview.div);
+
+ preview.icon.src = M.util.image_url('t/addfile');
+ preview.div.appendChild(preview.icon);
+
+ preview.div.appendChild(document.createTextNode(' '));
+
+ preview.namespan.className = 'instancename';
+ preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle');
+ preview.div.appendChild(preview.namespan);
+
+ modsel.appendChild(preview.li);
+ },
+
+ /**
+ * Find the registered handler for the given file type. If there is more than one, ask the
+ * user which one to use. Then upload the file to the server
+ * @param file the details of the file, taken from the FileList in the drop event
+ * @param section the DOM element representing the selected course section
+ * @param sectionnumber the number of the selected course section
+ */
+ handle_file: function(file, section, sectionnumber) {
+ var handlers = new Array();
+ var filehandlers = this.handlers.filehandlers;
+ var extension = '';
+ var dotpos = file.name.lastIndexOf('.');
+ if (dotpos != -1) {
+ extension = file.name.substr(dotpos+1, file.name.length);
+ }
+
+ for (var i=0; i'+M.util.get_string('actionchoice', 'moodle', file.name)+'
';
+
+ var Y = this.Y;
+ var self = this;
+ var panel = new Y.Panel({
+ bodyContent: content,
+ width: 350,
+ zIndex: 5,
+ centered: true,
+ modal: true,
+ visible: true,
+ render: true,
+ buttons: [{
+ value: M.util.get_string('upload', 'core'),
+ action: function(e) {
+ e.preventDefault();
+ // Find out which module was selected
+ var module = false;
+ var div = Y.one('#dndupload_handlers'+uploadid);
+ div.all('input').each(function(input) {
+ if (input.get('checked')) {
+ module = input.get('value');
+ }
+ });
+ if (!module) {
+ return;
+ }
+ panel.hide();
+ // Remember this selection for next time
+ self.lastselected[extension] = module;
+ // Do the upload
+ self.upload_file(file, section, sectionnumber, module);
+ },
+ section: Y.WidgetStdMod.FOOTER
+ },{
+ value: M.util.get_string('cancel', 'core'),
+ action: function(e) {
+ e.preventDefault();
+ panel.hide();
+ },
+ section: Y.WidgetStdMod.FOOTER
+ }]
+ });
+ // When the panel is hidden - destroy it and then check for other pending uploads
+ panel.after("visibleChange", function(e) {
+ if (!panel.get('visible')) {
+ panel.destroy(true);
+ self.check_upload_queue();
+ }
+ });
+ },
+
+ /**
+ * Check to see if there are any other dialog boxes to show, now that the current one has
+ * been dealt with
+ */
+ check_upload_queue: function() {
+ this.uploaddialog = false;
+ if (this.uploadqueue.length == 0) {
+ return;
+ }
+
+ var details = this.uploadqueue.shift();
+ if (details.isfile) {
+ this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber);
+ } else {
+ this.handle_item(details.type, details.contents, details.section, details.sectionnumber);
+ }
+ },
+
+ /**
+ * Do the file upload: show the dummy element, use an AJAX call to send the data
+ * to the server, update the progress bar for the file, then replace the dummy
+ * element with the real information once the AJAX call completes
+ * @param file the details of the file, taken from the FileList in the drop event
+ * @param section the DOM element representing the selected course section
+ * @param sectionnumber the number of the selected course section
+ */
+ upload_file: function(file, section, sectionnumber, module) {
+
+ // This would be an ideal place to use the Y.io function
+ // however, this does not support data encoded using the
+ // FormData object, which is needed to transfer data from
+ // the DataTransfer object into an XMLHTTPRequest
+ // This can be converted when the YUI issue has been integrated:
+ // http://yuilibrary.com/projects/yui3/ticket/2531274
+ var xhr = new XMLHttpRequest();
+ var self = this;
+
+ if (file.size > this.maxbytes) {
+ alert("'"+file.name+"' "+M.util.get_string('filetoolarge', 'moodle'));
+ return;
+ }
+
+ // Add the file to the display
+ var resel = this.add_resource_element(file.name, section);
+
+ // Update the progress bar as the file is uploaded
+ xhr.upload.addEventListener('progress', function(e) {
+ if (e.lengthComputable) {
+ var percentage = Math.round((e.loaded * 100) / e.total);
+ resel.progress.style.width = percentage + '%';
+ }
+ }, false);
+
+ // Wait for the AJAX call to complete, then update the
+ // dummy element with the returned details
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ var result = JSON.parse(xhr.responseText);
+ if (result) {
+ if (result.error == 0) {
+ // All OK - update the dummy element
+ resel.icon.src = result.icon;
+ resel.a.href = result.link;
+ resel.namespan.innerHTML = result.name;
+ resel.div.removeChild(resel.progressouter);
+ resel.li.id = result.elementid;
+ resel.div.innerHTML += result.commands;
+ if (result.onclick) {
+ resel.a.onclick = result.onclick;
+ }
+ self.add_editing(result.elementid);
+ } else {
+ // Error - remove the dummy element
+ resel.parent.removeChild(resel.li);
+ alert(result.error);
+ }
+ }
+ } else {
+ alert(M.util.get_string('servererror', 'moodle'));
+ }
+ }
+ };
+
+ // Prepare the data to send
+ var formData = new FormData();
+ formData.append('repo_upload_file', file);
+ formData.append('sesskey', M.cfg.sesskey);
+ formData.append('course', this.courseid);
+ formData.append('section', sectionnumber);
+ formData.append('module', module);
+ formData.append('type', 'Files');
+
+ // Send the AJAX call
+ xhr.open("POST", this.url, true);
+ xhr.send(formData);
+ },
+
+ /**
+ * Show a dialog box to gather the name of the resource / activity to be created
+ * from the uploaded content
+ * @param type the details of the type of content
+ * @param contents the contents to be uploaded
+ * @section the DOM element for the section being uploaded to
+ * @sectionnumber the number of the section being uploaded to
+ */
+ handle_item: function(type, contents, section, sectionnumber) {
+ if (type.handlers.length == 0) {
+ // Nothing to handle this - should not have got here
+ return;
+ }
+
+ if (this.uploaddialog) {
+ var details = new Object();
+ details.isfile = false;
+ details.type = type;
+ details.contents = contents;
+ details.section = section;
+ details.setcionnumber = sectionnumber;
+ this.uploadqueue.push(details);
+ return;
+ }
+ this.uploaddialog = true;
+
+ var timestamp = new Date().getTime();
+ var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
+ var nameid = 'dndupload_handler_name'+uploadid;
+ var content = '';
+ content += '';
+ content += ' ';
+ if (type.handlers.length > 1) {
+ content += '
';
+ var sel = type.handlers[0].module;
+ for (var i=0; i';
+ content += ' ';
+ }
+ content += '
';
+ }
+
+ var Y = this.Y;
+ var self = this;
+ var panel = new Y.Panel({
+ bodyContent: content,
+ width: 350,
+ zIndex: 5,
+ centered: true,
+ modal: true,
+ visible: true,
+ render: true,
+ buttons: [{
+ value: M.util.get_string('upload', 'core'),
+ action: function(e) {
+ e.preventDefault();
+ var name = Y.one('#dndupload_handler_name'+uploadid).get('value');
+ name = name.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // Trim
+ if (name == '') {
+ return;
+ }
+ var module = false;
+ if (type.handlers.length > 1) {
+ // Find out which module was selected
+ var div = Y.one('#dndupload_handlers'+uploadid);
+ div.all('input').each(function(input) {
+ if (input.get('checked')) {
+ module = input.get('value');
+ }
+ });
+ if (!module) {
+ return;
+ }
+ } else {
+ module = type.handlers[0].module;
+ }
+ panel.hide();
+ // Do the upload
+ self.upload_item(name, type.type, contents, section, sectionnumber, module);
+ },
+ section: Y.WidgetStdMod.FOOTER
+ },{
+ value: M.util.get_string('cancel', 'core'),
+ action: function(e) {
+ e.preventDefault();
+ panel.hide();
+ },
+ section: Y.WidgetStdMod.FOOTER
+ }]
+ });
+ // When the panel is hidden - destroy it and then check for other pending uploads
+ panel.after("visibleChange", function(e) {
+ if (!panel.get('visible')) {
+ panel.destroy(true);
+ self.check_upload_queue();
+ }
+ });
+ // Focus on the 'name' box
+ Y.one('#'+nameid).focus();
+ },
+
+ /**
+ * Upload any data types that are not files: display a dummy resource element, send
+ * the data to the server, update the progress bar for the file, then replace the
+ * dummy element with the real information once the AJAX call completes
+ * @param name the display name for the resource / activity to create
+ * @param type the details of the data type found in the drop event
+ * @param contents the actual data that was dropped
+ * @param section the DOM element representing the selected course section
+ * @param sectionnumber the number of the selected course section
+ */
+ upload_item: function(name, type, contents, section, sectionnumber, module) {
+
+ // This would be an ideal place to use the Y.io function
+ // however, this does not support data encoded using the
+ // FormData object, which is needed to transfer data from
+ // the DataTransfer object into an XMLHTTPRequest
+ // This can be converted when the YUI issue has been integrated:
+ // http://yuilibrary.com/projects/yui3/ticket/2531274
+ var xhr = new XMLHttpRequest();
+ var self = this;
+
+ // Add the item to the display
+ var resel = this.add_resource_element(name, section);
+
+ // Wait for the AJAX call to complete, then update the
+ // dummy element with the returned details
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ var result = JSON.parse(xhr.responseText);
+ if (result) {
+ if (result.error == 0) {
+ // All OK - update the dummy element
+ resel.icon.src = result.icon;
+ resel.a.href = result.link;
+ resel.namespan.innerHTML = result.name;
+ resel.div.removeChild(resel.progressouter);
+ resel.li.id = result.elementid;
+ resel.div.innerHTML += result.commands;
+ if (result.onclick) {
+ resel.a.onclick = result.onclick;
+ }
+ self.add_editing(result.elementid, sectionnumber);
+ } else {
+ // Error - remove the dummy element
+ resel.parent.removeChild(resel.li);
+ alert(result.error);
+ }
+ }
+ } else {
+ alert(M.util.get_string('servererror', 'moodle'));
+ }
+ }
+ };
+
+ // Prepare the data to send
+ var formData = new FormData();
+ formData.append('contents', contents);
+ formData.append('displayname', name);
+ formData.append('sesskey', M.cfg.sesskey);
+ formData.append('course', this.courseid);
+ formData.append('section', sectionnumber);
+ formData.append('type', type);
+ formData.append('module', module);
+
+ // Send the data
+ xhr.open("POST", this.url, true);
+ xhr.send(formData);
+ },
+
+ /**
+ * Call the AJAX course editing initialisation to add the editing tools
+ * to the newly-created resource link
+ * @param elementid the id of the DOM element containing the new resource link
+ * @param sectionnumber the number of the selected course section
+ */
+ add_editing: function(elementid) {
+ YUI().use('moodle-course-coursebase', function(Y) {
+ M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
+ });
+ }
+};
diff --git a/course/dndupload.php b/course/dndupload.php
new file mode 100644
index 0000000000000..03f9f72c4023e
--- /dev/null
+++ b/course/dndupload.php
@@ -0,0 +1,39 @@
+.
+
+/**
+ * Starting point for drag and drop course uploads
+ *
+ * @package core
+ * @subpackage lib
+ * @copyright 2012 Davo smith
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('AJAX_SCRIPT', true);
+
+require_once(dirname(dirname(dirname(__FILE__))).'/config.php');
+require_once($CFG->dirroot.'/course/dnduploadlib.php');
+
+$courseid = required_param('course', PARAM_INT);
+$section = required_param('section', PARAM_INT);
+$type = required_param('type', PARAM_TEXT);
+$modulename = required_param('module', PARAM_PLUGIN);
+$displayname = optional_param('displayname', null, PARAM_TEXT);
+$contents = optional_param('contents', null, PARAM_RAW); // It will be up to each plugin to clean this data, before saving it.
+
+$dndproc = new dndupload_ajax_processor($courseid, $section, $type, $modulename);
+$dndproc->process($displayname, $contents);
diff --git a/course/dnduploadlib.php b/course/dnduploadlib.php
new file mode 100644
index 0000000000000..7c7756cd3a9eb
--- /dev/null
+++ b/course/dnduploadlib.php
@@ -0,0 +1,655 @@
+.
+
+/**
+ * Library to handle drag and drop course uploads
+ *
+ * @package core
+ * @subpackage lib
+ * @copyright 2012 Davo smith
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/repository/lib.php');
+require_once($CFG->dirroot.'/repository/upload/lib.php');
+require_once($CFG->dirroot.'/course/lib.php');
+
+/**
+ * Add the Javascript to enable drag and drop upload to a course page
+ *
+ * @param object $course The currently displayed course
+ * @param array $modnames The list of enabled (visible) modules on this site
+ * @return void
+ */
+function dndupload_add_to_course($course, $modnames) {
+ global $CFG, $PAGE;
+
+ // Get all handlers.
+ $handler = new dndupload_handler($course, $modnames);
+ $jsdata = $handler->get_js_data();
+ if (empty($jsdata->types) && empty($jsdata->filehandlers)) {
+ return; // No valid handlers - don't enable drag and drop.
+ }
+
+ // Add the javascript to the page.
+ $jsmodule = array(
+ 'name' => 'coursedndupload',
+ 'fullpath' => new moodle_url('/course/dndupload.js'),
+ 'strings' => array(
+ array('addfilehere', 'moodle'),
+ array('dndworking', 'moodle'),
+ array('filetoolarge', 'moodle'),
+ array('actionchoice', 'moodle'),
+ array('servererror', 'moodle'),
+ array('upload', 'moodle'),
+ array('cancel', 'moodle')
+ ),
+ 'requires' => array('node', 'event', 'panel', 'json')
+ );
+ $vars = array(
+ array('courseid' => $course->id,
+ 'maxbytes' => get_max_upload_file_size($CFG->maxbytes, $course->maxbytes),
+ 'handlers' => $handler->get_js_data())
+ );
+
+ $PAGE->requires->js_init_call('M.course_dndupload.init', $vars, true, $jsmodule);
+}
+
+
+/**
+ * Stores all the information about the available dndupload handlers
+ *
+ * @package core
+ * @copyright 2012 Davo Smith
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class dndupload_handler {
+
+ /**
+ * @var array A list of all registered mime types that can be dropped onto a course
+ * along with the modules that will handle them.
+ */
+ protected $types = array();
+
+ /**
+ * @var array A list of the different file types (extensions) that different modules
+ * will handle.
+ */
+ protected $filehandlers = array();
+
+ /**
+ * Gather a list of dndupload handlers from the different mods
+ *
+ * @param object $course The course this is being added to (to check course_allowed_module() )
+ */
+ public function __construct($course, $modnames = null) {
+ global $CFG;
+
+ // Add some default types to handle.
+ // Note: 'Files' type is hard-coded into the Javascript as this needs to be ...
+ // ... treated a little differently.
+ $this->add_type('url', array('url', 'text/uri-list'), get_string('addlinkhere', 'moodle'),
+ get_string('nameforlink', 'moodle'), 10);
+ $this->add_type('text/html', array('text/html'), get_string('addpagehere', 'moodle'),
+ get_string('nameforpage', 'moodle'), 20);
+ $this->add_type('text', array('text', 'text/plain'), get_string('addpagehere', 'moodle'),
+ get_string('nameforpage', 'moodle'), 30);
+
+ // Loop through all modules to find handlers.
+ $mods = get_plugin_list_with_function('mod', 'dndupload_register');
+ foreach ($mods as $component => $funcname) {
+ list($modtype, $modname) = normalize_component($component);
+ if ($modnames && !array_key_exists($modname, $modnames)) {
+ continue; // Module is deactivated (hidden) at the site level.
+ }
+ if (!course_allowed_module($course, $modname)) {
+ continue; // User does not have permission to add this module to the course.
+ }
+ $resp = $funcname();
+ if (!$resp) {
+ continue;
+ }
+ if (isset($resp['files'])) {
+ foreach ($resp['files'] as $file) {
+ $this->add_file_handler($file['extension'], $modname, $file['message']);
+ }
+ }
+ if (isset($resp['addtypes'])) {
+ foreach ($resp['addtypes'] as $type) {
+ if (isset($type['priority'])) {
+ $priority = $type['priority'];
+ } else {
+ $priority = 100;
+ }
+ $this->add_type($type['identifier'], $type['datatransfertypes'],
+ $type['addmessage'], $type['namemessage'], $priority);
+ }
+ }
+ if (isset($resp['types'])) {
+ foreach ($resp['types'] as $type) {
+ $this->add_type_handler($type['identifier'], $modname, $type['message']);
+ }
+ }
+ }
+ }
+
+ /**
+ * Used to add a new mime type that can be drag and dropped onto a
+ * course displayed in a browser window
+ *
+ * @param string $identifier The name that this type will be known as
+ * @param array $datatransfertypes An array of the different types in the browser
+ * 'dataTransfer.types' object that will map to this type
+ * @param string $addmessage The message to display in the browser when this type is being
+ * dragged onto the page
+ * @param string $namemessage The message to pop up when asking for the name to give the
+ * course module instance when it is created
+ * @param int $priority Controls the order in which types are checked by the browser (mainly
+ * needed to check for 'text' last as that is usually given as fallback)
+ */
+ public function add_type($identifier, $datatransfertypes, $addmessage, $namemessage, $priority=100) {
+ if ($this->is_known_type($identifier)) {
+ throw new coding_exception("Type $identifier is already registered");
+ }
+
+ $add = new stdClass;
+ $add->identifier = $identifier;
+ $add->datatransfertypes = $datatransfertypes;
+ $add->addmessage = $addmessage;
+ $add->namemessage = $namemessage;
+ $add->priority = $priority;
+ $add->handlers = array();
+
+ $this->types[$identifier] = $add;
+ }
+
+ /**
+ * Used to declare that a particular module will handle a particular type
+ * of dropped data
+ *
+ * @param string $type The name of the type (as declared in add_type)
+ * @param string $module The name of the module to handle this type
+ * @param string $message The message to show the user if more than one handler is registered
+ * for a type and the user needs to make a choice between them
+ */
+ public function add_type_handler($type, $module, $message) {
+ if (!$this->is_known_type($type)) {
+ throw new coding_exception("Trying to add handler for unknown type $type");
+ }
+
+ $add = new stdClass;
+ $add->type = $type;
+ $add->module = $module;
+ $add->message = $message;
+
+ $this->types[$type]->handlers[] = $add;
+ }
+
+ /**
+ * Used to declare that a particular module will handle a particular type
+ * of dropped file
+ *
+ * @param string $extension The file extension to handle ('*' for all types)
+ * @param string $module The name of the module to handle this type
+ * @param string $message The message to show the user if more than one handler is registered
+ * for a type and the user needs to make a choice between them
+ */
+ public function add_file_handler($extension, $module, $message) {
+ $extension = strtolower($extension);
+
+ $add = new stdClass;
+ $add->extension = $extension;
+ $add->module = $module;
+ $add->message = $message;
+
+ $this->filehandlers[] = $add;
+ }
+
+ /**
+ * Check to see if the type has been registered
+ *
+ * @param string $type The identifier of the type you are interested in
+ * @return bool True if the type is registered
+ */
+ public function is_known_type($type) {
+ return array_key_exists($type, $this->types);
+ }
+
+ /**
+ * Check to see if the module in question has registered to handle the
+ * type given
+ *
+ * @param string $module The name of the module
+ * @param string $type The identifier of the type
+ * @return bool True if the module has registered to handle that type
+ */
+ public function has_type_handler($module, $type) {
+ if (!$this->is_known_type($type)) {
+ throw new coding_exception("Checking for handler for unknown type $type");
+ }
+ foreach ($this->types[$type]->handlers as $handler) {
+ if ($handler->module == $module) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check to see if the module in question has registered to handle files
+ * with the given extension (or to handle all file types)
+ *
+ * @param string $module The name of the module
+ * @param string $extension The extension of the uploaded file
+ * @return bool True if the module has registered to handle files with
+ * that extension (or to handle all file types)
+ */
+ public function has_file_handler($module, $extension) {
+ foreach ($this->filehandlers as $handler) {
+ if ($handler->module == $module) {
+ if ($handler->extension == '*' || $handler->extension == $extension) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets a list of the file types that are handled by a particular module
+ *
+ * @param string $module The name of the module to check
+ * @return array of file extensions or string '*'
+ */
+ public function get_handled_file_types($module) {
+ $types = array();
+ foreach ($this->filehandlers as $handler) {
+ if ($handler->module == $module) {
+ if ($handler->extension == '*') {
+ return '*';
+ } else {
+ // Prepending '.' as otherwise mimeinfo fails.
+ $types[] = '.'.$handler->extension;
+ }
+ }
+ }
+ return $types;
+ }
+
+ /**
+ * Returns an object to pass onto the javascript code with data about all the
+ * registered file / type handlers
+ *
+ * @return object Data to pass on to Javascript code
+ */
+ public function get_js_data() {
+ $ret = new stdClass;
+
+ // Sort the types by priority.
+ uasort($this->types, array($this, 'type_compare'));
+
+ $ret->types = array();
+ foreach ($this->types as $type) {
+ if (empty($type->handlers)) {
+ continue; // Skip any types without registered handlers.
+ }
+ $ret->types[] = $type;
+ }
+
+ $ret->filehandlers = $this->filehandlers;
+ $uploadrepo = repository::get_instances(array('type' => 'upload'));
+ if (empty($uploadrepo)) {
+ $ret->filehandlers = array(); // No upload repo => no file handlers.
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Comparison function used when sorting types by priority
+ * @param object $type1 first type to compare
+ * @param object $type2 second type to compare
+ * @return integer -1 for $type1 < $type2; 1 for $type1 > $type2; 0 for equal
+ */
+ protected function type_compare($type1, $type2) {
+ if ($type1->priority < $type2->priority) {
+ return -1;
+ }
+ if ($type1->priority > $type2->priority) {
+ return 1;
+ }
+ return 0;
+ }
+
+}
+
+/**
+ * Processes the upload, creating the course module and returning the result
+ *
+ * @package core
+ * @copyright 2012 Davo Smith
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class dndupload_ajax_processor {
+
+ /** Returned when no error has occurred */
+ const ERROR_OK = 0;
+
+ /** @var object The course that we are uploading to */
+ protected $course = null;
+
+ /** @var context_course The course context for capability checking */
+ protected $context = null;
+
+ /** @var int The section number we are uploading to */
+ protected $section = null;
+
+ /** @var string The type of upload (e.g. 'Files', 'text/plain') */
+ protected $type = null;
+
+ /** @var object The details of the module type that will be created */
+ protected $module= null;
+
+ /** @var object The course module that has been created */
+ protected $cm = null;
+
+ /** @var dndupload_handler used to check the allowed file types */
+ protected $dnduploadhandler = null;
+
+ /** @var string The name to give the new activity instance */
+ protected $displayname = null;
+
+ /**
+ * Set up some basic information needed to handle the upload
+ *
+ * @param int $courseid The ID of the course we are uploading to
+ * @param int $section The section number we are uploading to
+ * @param string $type The type of upload (as reported by the browser)
+ * @param string $modulename The name of the module requested to handle this upload
+ */
+ public function __construct($courseid, $section, $type, $modulename) {
+ global $DB;
+
+ if (!defined('AJAX_SCRIPT')) {
+ throw new coding_exception('dndupload_ajax_processor should only be used within AJAX requests');
+ }
+
+ $this->course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+
+ require_login($this->course, false);
+ $this->context = context_course::instance($this->course->id);
+
+ if (!is_number($section) || $section < 0) {
+ throw new coding_exception("Invalid section number $section");
+ }
+ $this->section = $section;
+ $this->type = $type;
+
+ if (!$this->module = $DB->get_record('modules', array('name' => $modulename))) {
+ throw new coding_exception("Module $modulename does not exist");
+ }
+
+ $this->dnduploadhandler = new dndupload_handler($this->course);
+ }
+
+ /**
+ * Check if this upload is a 'file' upload
+ *
+ * @return bool true if it is a 'file' upload, false otherwise
+ */
+ protected function is_file_upload() {
+ return ($this->type == 'Files');
+ }
+
+ /**
+ * Process the upload - creating the module in the course and returning the result to the browser
+ *
+ * @param string $displayname optional the name (from the browser) to give the course module instance
+ * @param string $content optional the content of the upload (for non-file uploads)
+ */
+ public function process($displayname = null, $content = null) {
+ require_capability('moodle/course:manageactivities', $this->context);
+
+ if ($this->is_file_upload()) {
+ require_capability('moodle/course:managefiles', $this->context);
+ if ($content != null) {
+ throw new moodle_exception('fileuploadwithcontent', 'moodle');
+ }
+ }
+
+ require_sesskey();
+
+ $this->displayname = $displayname;
+
+ if ($this->is_file_upload()) {
+ $this->handle_file_upload();
+ } else {
+ $this->handle_other_upload($content);
+ }
+ }
+
+ /**
+ * Handle uploads containing files - create the course module, ask the upload repository
+ * to process the file, ask the mod to set itself up, then return the result to the browser
+ */
+ protected function handle_file_upload() {
+ global $CFG;
+
+ // Add the file to a draft file area.
+ $draftitemid = file_get_unused_draft_itemid();
+ $maxbytes = get_max_upload_file_size($CFG->maxbytes, $this->course->maxbytes);
+ $types = $this->dnduploadhandler->get_handled_file_types($this->module->name);
+ $repo = repository::get_instances(array('type' => 'upload'));
+ if (empty($repo)) {
+ throw new moodle_exception('errornouploadrepo', 'moodle');
+ }
+ $repo = reset($repo); // Get the first (and only) upload repo.
+ $details = $repo->process_upload(null, $maxbytes, $types, '/', $draftitemid);
+ if (empty($this->displayname)) {
+ $this->displayname = $this->display_name_from_file($details['file']);
+ }
+
+ // Create a course module to hold the new instance.
+ $this->create_course_module();
+
+ // Ask the module to set itself up.
+ $moduledata = $this->prepare_module_data($draftitemid);
+ $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction');
+ if ($instanceid === 'invalidfunction') {
+ throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function");
+ }
+
+ // Finish setting up the course module.
+ $this->finish_setup_course_module($instanceid);
+ }
+
+ /**
+ * Handle uploads not containing file - create the course module, ask the mod to
+ * set itself up, then return the result to the browser
+ *
+ * @param string $content the content uploaded to the browser
+ */
+ protected function handle_other_upload($content) {
+ // Check this plugin is registered to handle this type of upload
+ if (!$this->dnduploadhandler->has_type_handler($this->module->name, $this->type)) {
+ $info = (object)array('modname' => $this->module->name, 'type' => $this->type);
+ throw new moodle_exception('moddoesnotsupporttype', 'moodle', $info);
+ }
+
+ // Create a course module to hold the new instance.
+ $this->create_course_module();
+
+ // Ask the module to set itself up.
+ $moduledata = $this->prepare_module_data(null, $content);
+ $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction');
+ if ($instanceid === 'invalidfunction') {
+ throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function");
+ }
+
+ // Finish setting up the course module.
+ $this->finish_setup_course_module($instanceid);
+ }
+
+ /**
+ * Generate the name of the mod instance from the name of the file
+ * (remove the extension and convert underscore => space
+ *
+ * @param string $filename the filename of the uploaded file
+ * @return string the display name to use
+ */
+ protected function display_name_from_file($filename) {
+ $pos = textlib::strrpos($filename, '.');
+ if ($pos) { // Want to skip if $pos === 0 OR $pos === false.
+ $filename = textlib::substr($filename, 0, $pos);
+ }
+ return str_replace('_', ' ', $filename);
+ }
+
+ /**
+ * Create the coursemodule to hold the file/content that has been uploaded
+ */
+ protected function create_course_module() {
+ if (!course_allowed_module($this->course, $this->module->name)) {
+ throw new coding_exception("The module {$this->module->name} is not allowed to be added to this course");
+ }
+
+ $this->cm = new stdClass();
+ $this->cm->course = $this->course->id;
+ $this->cm->section = $this->section;
+ $this->cm->module = $this->module->id;
+ $this->cm->modulename = $this->module->name;
+ $this->cm->instance = 0; // This will be filled in after we create the instance.
+ $this->cm->visible = 1;
+ $this->cm->groupmode = $this->course->groupmode;
+ $this->cm->groupingid = $this->course->defaultgroupingid;
+
+ if (!$this->cm->id = add_course_module($this->cm)) {
+ throw new coding_exception("Unable to create the course module");
+ }
+ // The following are used inside some few core functions, so may as well set them here.
+ $this->cm->coursemodule = $this->cm->id;
+ $this->cm->groupmodelink = (!$this->course->groupmodeforce);
+ }
+
+ /**
+ * Gather together all the details to pass on to the mod, so that it can initialise it's
+ * own database tables
+ *
+ * @param int $draftitemid optional the id of the draft area containing the file (for file uploads)
+ * @param string $content optional the content dropped onto the course (for non-file uploads)
+ * @return object data to pass on to the mod, containing:
+ * string $type the 'type' as registered with dndupload_handler (or 'Files')
+ * object $course the course the upload was for
+ * int $draftitemid optional the id of the draft area containing the files
+ * int $coursemodule id of the course module that has already been created
+ * string $displayname the name to use for this activity (can be overriden by the mod)
+ */
+ protected function prepare_module_data($draftitemid = null, $content = null) {
+ $data = new stdClass();
+ $data->type = $this->type;
+ $data->course = $this->course;
+ if ($draftitemid) {
+ $data->draftitemid = $draftitemid;
+ } else if ($content) {
+ $data->content = $content;
+ }
+ $data->coursemodule = $this->cm->id;
+ $data->displayname = $this->displayname;
+ return $data;
+ }
+
+ /**
+ * Called after the mod has set itself up, to finish off any course module settings
+ * (set instance id, add to correct section, set visibility, etc.) and send the response
+ *
+ * @param int $instanceid id returned by the mod when it was created
+ */
+ protected function finish_setup_course_module($instanceid) {
+ global $DB, $USER;
+
+ if (!$instanceid) {
+ // Something has gone wrong - undo everything we can.
+ delete_course_module($this->cm->id);
+ throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name);
+ }
+
+ $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $this->cm->id));
+
+ $sectionid = add_mod_to_section($this->cm);
+ $DB->set_field('course_modules', 'section', $sectionid, array('id' => $this->cm->id));
+
+ set_coursemodule_visible($this->cm->id, true);
+
+ // Rebuild the course cache and retrieve the final info about this module.
+ rebuild_course_cache($this->course->id, true);
+ $this->course->modinfo = null; // Otherwise we will just get the old version back again.
+ $info = get_fast_modinfo($this->course);
+ if (!isset($info->cms[$this->cm->id])) {
+ // The course module has not been properly created in the course - undo everything.
+ delete_course_module($this->cm->id);
+ throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name);
+ }
+ $mod = $info->cms[$this->cm->id];
+
+ // Trigger mod_created event with information about this module.
+ $eventdata = new stdClass();
+ $eventdata->modulename = $mod->modname;
+ $eventdata->name = $mod->name;
+ $eventdata->cmid = $mod->id;
+ $eventdata->courseid = $this->course->id;
+ $eventdata->userid = $USER->id;
+ events_trigger('mod_created', $eventdata);
+
+ add_to_log($this->course->id, "course", "add mod",
+ "../mod/{$mod->modname}/view.php?id=$mod->id",
+ "{$mod->modname} $instanceid");
+ add_to_log($this->course->id, $mod->modname, "add",
+ "view.php?id=$mod->id",
+ "$instanceid", $mod->id);
+
+ if ($this->cm->groupmodelink && plugin_supports('mod', $mod->modname, FEATURE_GROUPS, 0)) {
+ $mod->groupmodelink = $this->cm->groupmodelink;
+ } else {
+ $mod->groupmodelink = false;
+ }
+
+ $this->send_response($mod);
+ }
+
+ /**
+ * Send the details of the newly created activity back to the client browser
+ *
+ * @param cm_info $mod details of the mod just created
+ */
+ protected function send_response($mod) {
+ global $OUTPUT;
+
+ $resp = new stdClass();
+ $resp->error = self::ERROR_OK;
+ $resp->icon = $mod->get_icon_url()->out();
+ $resp->name = $mod->name;
+ $resp->link = $mod->get_url()->out();
+ $resp->elementid = 'module-'.$mod->id;
+ $resp->commands = make_editing_buttons($mod, true, true, 0, $mod->sectionnum);
+ $resp->onclick = $mod->get_on_click();
+
+ echo $OUTPUT->header();
+ echo json_encode($resp);
+ die();
+ }
+}
\ No newline at end of file
diff --git a/course/lib.php b/course/lib.php
index 6fcfe4a461f68..8b78b51870109 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -28,6 +28,7 @@
require_once($CFG->libdir.'/completionlib.php');
require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->dirroot.'/course/dnduploadlib.php');
define('COURSE_MAX_LOGS_PER_PAGE', 1000); // records
define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds
@@ -4449,15 +4450,15 @@ function course_ajax_enabled($course) {
* toolbox YUI module
*
* @param integer $id The ID of the course being applied to
- * @param array $modules An array containing the names of the modules in
- * use on the page
+ * @param array $usedmodules An array containing the names of the modules in use on the page
+ * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site
* @param stdClass $config An object containing configuration parameters for ajax modules including:
* * resourceurl The URL to post changes to for resource changes
* * sectionurl The URL to post changes to for section changes
* * pageparams Additional parameters to pass through in the post
- * @return void
+ * @return bool
*/
-function include_course_ajax($course, $modules = array(), $config = null) {
+function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) {
global $PAGE, $SITE;
// Ensure that ajax should be included
@@ -4557,10 +4558,13 @@ function include_course_ajax($course, $modules = array(), $config = null) {
}
// For confirming resource deletion we need the name of the module in question
- foreach ($modules as $module => $modname) {
+ foreach ($usedmodules as $module => $modname) {
$PAGE->requires->string_for_js('pluginname', $module);
}
+ // Load drag and drop upload AJAX.
+ dndupload_add_to_course($course, $enabledmodules);
+
return true;
}
diff --git a/course/view.php b/course/view.php
index e6bc303923540..b14befe4cead7 100644
--- a/course/view.php
+++ b/course/view.php
@@ -241,6 +241,6 @@
echo html_writer::end_tag('div');
// Include the command toolbox YUI module
- include_course_ajax($course, $modnamesused);
+ include_course_ajax($course, $modnamesused, $modnames);
echo $OUTPUT->footer();
diff --git a/index.php b/index.php
index 721c40d38d8e5..dcb81b39073a0 100644
--- a/index.php
+++ b/index.php
@@ -150,7 +150,7 @@
echo $OUTPUT->box_end();
}
}
- include_course_ajax($SITE, $modnamesused);
+ include_course_ajax($SITE, $modnamesused, $modnames);
if (isloggedin() and !isguestuser() and isset($CFG->frontpageloggedin)) {
diff --git a/lang/en/moodle.php b/lang/en/moodle.php
index 0109f55dfe5ff..cf4b6981db19c 100644
--- a/lang/en/moodle.php
+++ b/lang/en/moodle.php
@@ -25,6 +25,7 @@
$string['abouttobeinstalled'] = 'about to be installed';
$string['action'] = 'Action';
+$string['actionchoice'] = 'What do you want to do with the file \'{$a}\'?';
$string['actions'] = 'Actions';
$string['active'] = 'Active';
$string['activeusers'] = 'Active users';
@@ -54,13 +55,16 @@
$string['addedtogroup'] = 'Added to group "{$a}"';
$string['addedtogroupnot'] = 'Not added to group "{$a}"';
$string['addedtogroupnotenrolled'] = 'Not added to group "{$a}", because not enrolled in course';
+$string['addfilehere'] = 'Add file(s) here';
$string['addinganew'] = 'Adding a new {$a}';
$string['addinganewto'] = 'Adding a new {$a->what} to {$a->to}';
$string['addingdatatoexisting'] = 'Adding data to existing';
+$string['addlinkhere'] = 'Add link here';
$string['addnewcategory'] = 'Add new category';
$string['addnewcourse'] = 'Add a new course';
$string['addnewuser'] = 'Add a new user';
$string['addnousersrecip'] = 'Add users who haven\'t accessed this {$a} to recipient list';
+$string['addpagehere'] = 'Add page here';
$string['addresource'] = 'Add a resource...';
$string['address'] = 'Address';
$string['addstudent'] = 'Add student';
@@ -462,6 +466,7 @@
$string['dndenabled_help'] = 'You can drag one or more files from your desktop and drop them onto the box below to upload them. Note: this may not work with other web browsers';
$string['dndenabled_insentence'] = 'drag and drop available';
$string['dndenabled_inbox'] = 'drag and drop files here to upload them';
+$string['dndworking'] = 'Drag and drop files, text or links onto course sections to upload them';
$string['documentation'] = 'Moodle documentation';
$string['down'] = 'Down';
$string['download'] = 'Download';
@@ -628,6 +633,9 @@
$string['enterusername'] = 'Enter your username';
$string['entries'] = 'Entries';
$string['error'] = 'Error';
+$string['errorcreatingactivity'] = 'Unable to create an instance of activity \'{$a}\'';
+$string['errorfiletoobig'] = 'The file was bigger than the limit of {$a} bytes';
+$string['errornouploadrepo'] = 'There is no upload repository enabled for this site';
$string['errortoomanylogins'] = 'Sorry, you have exceeded the allowed number of login attempts. Restart your browser.';
$string['errorwhenconfirming'] = 'You are not confirmed yet because an error occurred. If you clicked on a link in an email to get here, make sure that the line in your email wasn\'t broken or wrapped. You may have to use cut and paste to reconstruct the link properly.';
$string['everybody'] = 'Everybody';
@@ -669,8 +677,10 @@
$string['feedback'] = 'Feedback';
$string['file'] = 'File';
$string['filemissing'] = '{$a} is missing';
+$string['filetoolarge'] = 'is too large to upload';
$string['files'] = 'Files';
$string['filesfolders'] = 'Files/folders';
+$string['fileuploadwithcontent'] = 'File uploads should not include the content parameter';
$string['filloutallfields'] = 'Please fill out all fields in this form';
$string['filter'] = 'Filter';
$string['findmorecourses'] = 'Find more courses...';
@@ -1041,6 +1051,7 @@
$string['missingteacher'] = 'Must choose something';
$string['missingurl'] = 'Missing URL';
$string['missingusername'] = 'Missing username';
+$string['moddoesnotsupporttype'] = 'Module {$a->modname} does not support uploads of type {$a->type}';
$string['modified'] = 'Modified';
$string['moduledeleteconfirm'] = 'You are about to completely delete the module \'{$a}\'. This will completely delete everything in the database associated with this activity module. Are you SURE you want to continue?';
$string['moduledeletefiles'] = 'All data associated with the module \'{$a->module}\' has been deleted from the database. To complete the deletion (and prevent the module re-installing itself), you should now delete this directory from your server: {$a->directory}';
@@ -1081,6 +1092,8 @@
$string['mymoodledashboard'] = 'My Moodle dashboard';
$string['myprofile'] = 'My profile';
$string['name'] = 'Name';
+$string['nameforlink'] = 'What do you want to call this link?';
+$string['nameforpage'] = 'What do you want to call this page?';
$string['navigation'] = 'Navigation';
$string['needed'] = 'Needed';
$string['never'] = 'Never';
@@ -1482,6 +1495,7 @@
$string['separate'] = 'Separate';
$string['separateandconnected'] = 'Separate and Connected ways of knowing';
$string['separateandconnectedinfo'] = 'The scale based on the theory of separate and connected knowing. This theory describes two different ways that we can evaluate and learn about the things we see and hear.
Separate knowers remain as objective as possible without including feelings and emotions. In a discussion with other people, they like to defend their own ideas, using logic to find holes in opponent\'s ideas.
Connected knowers are more sensitive to other people. They are skilled at empathy and tends to listen and ask questions until they feel they can connect and "understand things from their point of view". They learn by trying to share the experiences that led to the knowledge they find in other people.
';
+$string['servererror'] = 'An error occurred whilst communicating with the server';
$string['serverlocaltime'] = 'Server\'s local time';
$string['setcategorytheme'] = 'Set category theme';
$string['settings'] = 'Settings';
diff --git a/mod/folder/lang/en/folder.php b/mod/folder/lang/en/folder.php
index 3a55366797b11..7196e3d3ec7be 100644
--- a/mod/folder/lang/en/folder.php
+++ b/mod/folder/lang/en/folder.php
@@ -25,6 +25,7 @@
*/
$string['contentheader'] = 'Content';
+$string['dnduploadmakefolder'] = 'Unzip files and create folder';
$string['folder:addinstance'] = 'Add a new folder';
$string['folder:managefiles'] = 'Manage files in folder module';
$string['folder:view'] = 'View folder content';
diff --git a/mod/folder/lib.php b/mod/folder/lib.php
index 61b17c8407d7b..607e78fb8f63a 100644
--- a/mod/folder/lib.php
+++ b/mod/folder/lib.php
@@ -358,3 +358,51 @@ function folder_export_contents($cm, $baseurl) {
return $contents;
}
+
+/**
+ * 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() {
+ return array('files' => array(
+ array('extension' => 'zip', 'message' => get_string('dnduploadmakefolder', 'mod_folder'))
+ ));
+}
+
+/**
+ * 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 mod_folder_dndupload_handle($uploadinfo) {
+ global $DB, $USER;
+
+ // Gather the required info.
+ $data = new stdClass();
+ $data->course = $uploadinfo->course->id;
+ $data->name = $uploadinfo->displayname;
+ $data->intro = '
'.$uploadinfo->displayname.'
';
+ $data->introformat = FORMAT_HTML;
+ $data->coursemodule = $uploadinfo->coursemodule;
+ $data->files = null; // We will unzip the file and sort out the contents below.
+
+ $data->id = folder_add_instance($data, null);
+
+ // Retrieve the file from the draft file area.
+ $context = context_module::instance($uploadinfo->coursemodule);
+ file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_folder', 'temp', 0, array('subdirs'=>true));
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($context->id, 'mod_folder', 'temp', 0, 'sortorder', false);
+ // Only ever one file - extract the contents.
+ $file = reset($files);
+
+ $success = $file->extract_to_storage(new zip_packer(), $context->id, 'mod_folder', 'content', 0, '/', $USER->id);
+ $fs->delete_area_files($context->id, 'mod_folder', 'temp', 0);
+
+ if ($success) {
+ return $data->id;
+ }
+
+ $DB->delete_records('folder', array('id' => $data->id));
+ return false;
+}
diff --git a/mod/page/lang/en/page.php b/mod/page/lang/en/page.php
index 98af5a64d2ea1..843b9d45fd4fa 100644
--- a/mod/page/lang/en/page.php
+++ b/mod/page/lang/en/page.php
@@ -26,6 +26,7 @@
$string['configdisplayoptions'] = 'Select all options that should be available, existing settings are not modified. Hold CTRL key to select multiple fields.';
$string['content'] = 'Page content';
$string['contentheader'] = 'Content';
+$string['createpage'] = 'Create a new page';
$string['displayoptions'] = 'Available display options';
$string['displayselect'] = 'Display';
$string['displayselectexplain'] = 'Select display type.';
diff --git a/mod/page/lib.php b/mod/page/lib.php
index cd027956165f3..68b813cfcd8d0 100644
--- a/mod/page/lib.php
+++ b/mod/page/lib.php
@@ -472,3 +472,46 @@ function page_export_contents($cm, $baseurl) {
return $contents;
}
+
+/**
+ * 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() {
+ return array('types' => array(
+ array('identifier' => 'text/html', 'message' => get_string('createpage', 'page')),
+ array('identifier' => 'text', 'message' => get_string('createpage', 'page'))
+ ));
+}
+
+/**
+ * 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 mod_page_dndupload_handle($uploadinfo) {
+ // Gather the required info.
+ $data = new stdClass();
+ $data->course = $uploadinfo->course->id;
+ $data->name = $uploadinfo->displayname;
+ $data->intro = '
'.$uploadinfo->displayname.'
';
+ $data->introformat = FORMAT_HTML;
+ if ($uploadinfo->type == 'text/html') {
+ $data->contentformat = FORMAT_HTML;
+ $data->content = clean_param($uploadinfo->content, PARAM_CLEANHTML);
+ } else {
+ $data->contentformat = FORMAT_PLAIN;
+ $data->content = clean_param($uploadinfo->content, PARAM_TEXT);
+ }
+ $data->coursemodule = $uploadinfo->coursemodule;
+
+ // Set the display options to the site defaults.
+ $config = get_config('page');
+ $data->display = $config->display;
+ $data->popupheight = $config->popupheight;
+ $data->popupwidth = $config->popupwidth;
+ $data->printheading = $config->printheading;
+ $data->printintro = $config->printintro;
+
+ return page_add_instance($data, null);
+}
diff --git a/mod/resource/lang/en/resource.php b/mod/resource/lang/en/resource.php
index ae0ef794e7f41..11ad55cc30911 100644
--- a/mod/resource/lang/en/resource.php
+++ b/mod/resource/lang/en/resource.php
@@ -53,6 +53,7 @@
* New window - The file is displayed in a new browser window with menus and an address bar';
$string['displayselect_link'] = 'mod/file/mod';
$string['displayselectexplain'] = 'Choose display type, unfortunately not all types are suitable for all files.';
+$string['dnduploadresource'] = 'Create file resource';
$string['encryptedcode'] = 'Encrypted code';
$string['filenotfound'] = 'File not found, sorry.';
$string['filterfiles'] = 'Use filters on file content';
diff --git a/mod/resource/lib.php b/mod/resource/lib.php
index 882c82f7ec36c..3e44d09f0dcf3 100644
--- a/mod/resource/lib.php
+++ b/mod/resource/lib.php
@@ -88,6 +88,7 @@ function resource_get_post_actions() {
function resource_add_instance($data, $mform) {
global $CFG, $DB;
require_once("$CFG->libdir/resourcelib.php");
+ require_once("$CFG->dirroot/mod/resource/locallib.php");
$cmid = $data->coursemodule;
$data->timemodified = time();
@@ -478,3 +479,39 @@ function resource_export_contents($cm, $baseurl) {
return $contents;
}
+
+/**
+ * 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() {
+ return array('files' => array(
+ array('extension' => '*', 'message' => get_string('dnduploadresource', 'mod_resource'))
+ ));
+}
+
+/**
+ * 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 mod_resource_dndupload_handle($uploadinfo) {
+ // Gather the required info.
+ $data = new stdClass();
+ $data->course = $uploadinfo->course->id;
+ $data->name = $uploadinfo->displayname;
+ $data->intro = '
'.$uploadinfo->displayname.'
';
+ $data->introformat = FORMAT_HTML;
+ $data->coursemodule = $uploadinfo->coursemodule;
+ $data->files = $uploadinfo->draftitemid;
+
+ // Set the display options to the site defaults.
+ $config = get_config('resource');
+ $data->display = $config->display;
+ $data->popupheight = $config->popupheight;
+ $data->popupwidth = $config->popupwidth;
+ $data->printheading = $config->printheading;
+ $data->printintro = $config->printintro;
+
+ return resource_add_instance($data, null);
+}
diff --git a/mod/url/lang/en/url.php b/mod/url/lang/en/url.php
index dacfb0cbe9c88..6837d6149b713 100644
--- a/mod/url/lang/en/url.php
+++ b/mod/url/lang/en/url.php
@@ -30,6 +30,7 @@
$string['configrolesinparams'] = 'Enable if you want to include localized role names in list of available parameter variables.';
$string['configsecretphrase'] = 'This secret phrase is used to produce encrypted code value that can be sent to some servers as a parameter. The encrypted code is produced by an md5 value of the current user IP address concatenated with your secret phrase. ie code = md5(IP.secretphrase). Please note that this is not reliable because IP address may change and is often shared by different computers.';
$string['contentheader'] = 'Content';
+$string['createurl'] = 'Create a URL';
$string['displayoptions'] = 'Available display options';
$string['displayselect'] = 'Display';
$string['displayselect_help'] = 'This setting, together with the URL file type and whether the browser allows embedding, determines how the URL is displayed. Options may include:
diff --git a/mod/url/lib.php b/mod/url/lib.php
index fec574e29d72b..65c21cf77dbed 100644
--- a/mod/url/lib.php
+++ b/mod/url/lib.php
@@ -331,3 +331,39 @@ function url_export_contents($cm, $baseurl) {
return $contents;
}
+
+/**
+ * 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() {
+ return array('types' => array(
+ array('identifier' => 'url', 'message' => get_string('createurl', 'url'))
+ ));
+}
+
+/**
+ * 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 mod_url_dndupload_handle($uploadinfo) {
+ // Gather all the required data.
+ $data = new stdClass();
+ $data->course = $uploadinfo->course->id;
+ $data->name = $uploadinfo->displayname;
+ $data->intro = '
'.$uploadinfo->displayname.'
';
+ $data->introformat = FORMAT_HTML;
+ $data->externalurl = clean_param($uploadinfo->content, PARAM_URL);
+ $data->timemodified = time();
+
+ // Set the display options to the site defaults.
+ $config = get_config('url');
+ $data->display = $config->display;
+ $data->popupwidth = $config->popupwidth;
+ $data->popupheight = $config->popupheight;
+ $data->printheading = $config->printheading;
+ $data->printintro = $config->printintro;
+
+ return url_add_instance($data, null);
+}
diff --git a/repository/upload/lib.php b/repository/upload/lib.php
index 8b2641ea31280..1b2bb9d85f7f5 100644
--- a/repository/upload/lib.php
+++ b/repository/upload/lib.php
@@ -42,9 +42,31 @@ public function print_login() {
* @return array|bool
*/
public function upload($saveas_filename, $maxbytes) {
- global $USER, $CFG;
+ global $CFG;
$types = optional_param_array('accepted_types', '*', PARAM_RAW);
+ $savepath = optional_param('savepath', '/', PARAM_PATH);
+ $itemid = optional_param('itemid', 0, PARAM_INT);
+ $license = optional_param('license', $CFG->sitedefaultlicense, PARAM_TEXT);
+ $author = optional_param('author', '', PARAM_TEXT);
+
+ return $this->process_upload($saveas_filename, $maxbytes, $types, $savepath, $itemid, $license, $author);
+ }
+
+ /**
+ * Do the actual processing of the uploaded file
+ * @param string $saveas_filename name to give to the file
+ * @param int $maxbytes maximum file size
+ * @param mixed $types optional array of file extensions that are allowed or '*' for all
+ * @param string $savepath optional path to save the file to
+ * @param int $itemid optional the ID for this item within the file area
+ * @param string $license optional the license to use for this file
+ * @param string $author optional the name of the author of this file
+ * @return object containing details of the file uploaded
+ */
+ public function process_upload($saveas_filename, $maxbytes, $types = '*', $savepath = '/', $itemid = 0, $license = null, $author = '') {
+ global $USER, $CFG;
+
if ((is_array($types) and in_array('*', $types)) or $types == '*') {
$this->mimetypes = '*';
} else {
@@ -53,13 +75,17 @@ public function upload($saveas_filename, $maxbytes) {
}
}
+ if ($license == null) {
+ $license = $CFG->sitedefaultlicense;
+ }
+
$record = new stdClass();
$record->filearea = 'draft';
$record->component = 'user';
- $record->filepath = optional_param('savepath', '/', PARAM_PATH);
- $record->itemid = optional_param('itemid', 0, PARAM_INT);
- $record->license = optional_param('license', $CFG->sitedefaultlicense, PARAM_TEXT);
- $record->author = optional_param('author', '', PARAM_TEXT);
+ $record->filepath = $savepath;
+ $record->itemid = $itemid;
+ $record->license = $license;
+ $record->author = $author;
$context = get_context_instance(CONTEXT_USER, $USER->id);
$elname = 'repo_upload_file';
diff --git a/theme/base/style/course.css b/theme/base/style/course.css
index 35634332cb92f..7f7cfc1c68741 100644
--- a/theme/base/style/course.css
+++ b/theme/base/style/course.css
@@ -154,3 +154,10 @@ span.editinstructions {
input.titleeditor {
width: 330px;
}
+
+/* Course drag and drop upload styles */
+#dndupload-status {width:60%;margin:0 auto;padding:2px;border:1px solid #ddd;text-align:center;background:#ffc}
+.dndupload-preview {color:#909090;border:1px dashed #909090;}
+.dndupload-progress-outer {width:70px;border:solid black 1px;height:10px;display:inline-block;margin:0;padding:0;overflow:hidden;position:relative;}
+.dndupload-progress-inner {width:0%;height:100%;background-color:green;display:inline-block;margin:0;padding:0;float:left;}
+.dndupload-hidden {display:none;}