From 32528f94e417049716708cfbdeba5fc395a7af6c Mon Sep 17 00:00:00 2001
From: Davo Smith
Date: Fri, 17 Feb 2012 21:23:28 +0000
Subject: [PATCH] MDL-22504 Drag and drop upload course - enables upload of
files, text and urls to a course page
---
course/lib.php | 4 +
lang/en/dndupload.php | 39 ++
lib/ajax/dndupload.js | 879 ++++++++++++++++++++++++++++++
lib/ajax/dndupload.php | 39 ++
lib/dnduploadlib.php | 635 +++++++++++++++++++++
mod/folder/lang/en/folder.php | 1 +
mod/folder/lib.php | 46 ++
mod/page/lang/en/page.php | 1 +
mod/page/lib.php | 51 ++
mod/resource/lang/en/resource.php | 1 +
mod/resource/lib.php | 57 ++
mod/url/lang/en/url.php | 1 +
mod/url/lib.php | 46 ++
repository/upload/lib.php | 36 +-
theme/base/style/course.css | 6 +
15 files changed, 1837 insertions(+), 5 deletions(-)
create mode 100644 lang/en/dndupload.php
create mode 100644 lib/ajax/dndupload.js
create mode 100644 lib/ajax/dndupload.php
create mode 100644 lib/dnduploadlib.php
diff --git a/course/lib.php b/course/lib.php
index 382ae358cee..8dca6d7dfc8 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir.'/completionlib.php');
require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->libdir.'/dnduploadlib.php');
define('COURSE_MAX_LOGS_PER_PAGE', 1000); // records
define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds
@@ -4557,4 +4558,7 @@ function include_course_ajax($course, $modules = array(), $config = null) {
foreach ($modules as $module => $modname) {
$PAGE->requires->string_for_js('pluginname', $module);
}
+
+ // Load drag and drop upload AJAX.
+ dndupload_add_to_course($course);
}
diff --git a/lang/en/dndupload.php b/lang/en/dndupload.php
new file mode 100644
index 00000000000..3dbe1cc6085
--- /dev/null
+++ b/lang/en/dndupload.php
@@ -0,0 +1,39 @@
+.
+
+/**
+ * Strings for component 'dndupload', language 'en', branch 'master'
+ *
+ * @package core
+ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['actionchoice'] = 'What do you want to do with the file \'{$a}\'?';
+$string['addfilehere'] = 'Add file(s) here';
+$string['addlinkhere'] = 'Add link here';
+$string['addpagehere'] = 'Add page here';
+$string['dndworking'] = 'Drag and drop files, text or links onto course sections to upload them';
+$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['filetoolarge'] = 'is too large to upload';
+$string['nameforlink'] = 'What do you want to call this link?';
+$string['nameforpage'] = 'What do you want to call this page?';
+$string['noajax'] = 'AJAX course editing must be enabled for drag and drop upload to work';
+$string['nofilereader'] = 'Your browser does not support drag and drop upload of files';
+$string['noscript'] = 'Javascript must be enabled for drag and drop upload to work';
+$string['servererror'] = 'An error occurred whilst communicating with the server';
diff --git a/lib/ajax/dndupload.js b/lib/ajax/dndupload.js
new file mode 100644
index 00000000000..3030495b13e
--- /dev/null
+++ b/lib/ajax/dndupload.js
@@ -0,0 +1,879 @@
+// 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 + '/lib/ajax/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', 'core_dndupload'));
+ },
+
+ /**
+ * 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', 'core_dndupload');
+ 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', 'core_dndupload', 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', 'core_dndupload'));
+ 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', 'core_dndupload'));
+ }
+ }
+ };
+
+ // 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', 'core_dndupload'));
+ }
+ }
+ };
+
+ // 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/lib/ajax/dndupload.php b/lib/ajax/dndupload.php
new file mode 100644
index 00000000000..8845ef0b41a
--- /dev/null
+++ b/lib/ajax/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->libdir.'/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_processor($courseid, $section, $type, $modulename);
+$dndproc->process($displayname, $contents);
diff --git a/lib/dnduploadlib.php b/lib/dnduploadlib.php
new file mode 100644
index 00000000000..2cee00cdd54
--- /dev/null
+++ b/lib/dnduploadlib.php
@@ -0,0 +1,635 @@
+.
+
+/**
+ * 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
+ */
+function dndupload_add_to_course($course) {
+ global $CFG, $PAGE;
+
+ // Get all handlers.
+ $handler = new dndupload_handler($course);
+
+ // Add the javascript to the page.
+ $jsmodule = array(
+ 'name' => 'dndupload',
+ 'fullpath' => new moodle_url('/lib/ajax/dndupload.js'),
+ 'strings' => array(
+ array('addfilehere', 'core_dndupload'),
+ array('dndworking', 'core_dndupload'),
+ array('filetoolarge', 'core_dndupload'),
+ array('nofilereader', 'core_dndupload'),
+ array('noajax', 'core_dndupload'),
+ array('actionchoice', 'core_dndupload'),
+ array('servererror', 'core_dndupload'),
+ array('upload', 'core'),
+ array('cancel', 'core')
+ ),
+ '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) {
+ 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', 'core_dndupload'),
+ get_string('nameforlink', 'core_dndupload'), 10);
+ $this->add_type('text/html', array('text/html'), get_string('addpagehere', 'core_dndupload'),
+ get_string('nameforpage', 'core_dndupload'), 20);
+ $this->add_type('text', array('text', 'text/plain'), get_string('addpagehere', 'core_dndupload'),
+ get_string('nameforpage', 'core_dndupload'), 30);
+
+ // Loop through all modules to find handlers.
+ $mods = get_plugin_list('mod');
+ foreach ($mods as $modname => $modpath) {
+ if (!course_allowed_module($course, $modname)) {
+ continue;
+ }
+ $resp = plugin_callback('mod', $modname, 'dndupload', 'register', array());
+ 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[] = $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) {
+ foreach ($this->types as $knowntype) {
+ if ($knowntype->identifier == $type) {
+ $add = new stdClass;
+ $add->type = $type;
+ $add->module = $module;
+ $add->message = $message;
+
+ $knowntype->handlers[] = $add;
+ return;
+ }
+ }
+
+ throw new coding_exception("Trying to add handler for unknown type $type");
+ }
+
+ /**
+ * 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) {
+ $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) {
+ foreach ($this->types as $knowntype) {
+ if ($knowntype->identifier == $type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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) {
+ foreach ($this->types as $knowntype) {
+ if ($knowntype->identifier == $type) {
+ foreach ($knowntype->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_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;
+
+ $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 || $section > $this->course->numsections) {
+ 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);
+ }
+
+ 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', 'core_dndupload');
+ }
+ $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) {
+ // 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.
+ $modcontext = context_module::instance($this->cm->id);
+ delete_context(CONTEXT_MODULE, $this->cm->id);
+ $DB->delete_records('course_modules', array('id' => $this->cm->id));
+ throw new moodle_exception('errorcreatingactivity', 'core_dndupload');
+ }
+
+ $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);
+ $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) {
+ $resp = new stdClass();
+ $resp->error = self::ERROR_OK;
+ $resp->icon = $mod->get_icon_url().'';
+ $resp->name = $mod->name;
+ $resp->link = $mod->get_url().'';
+ $resp->elementid = 'module-'.$mod->id;
+ $resp->commands = make_editing_buttons($mod, true, true, 0, $mod->sectionnum);
+ $resp->onclick = $mod->get_on_click();
+
+ echo json_encode($resp);
+ die();
+ }
+}
\ No newline at end of file
diff --git a/mod/folder/lang/en/folder.php b/mod/folder/lang/en/folder.php
index 3a55366797b..7196e3d3ec7 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 365dd923aab..8085b5b705d 100644
--- a/mod/folder/lib.php
+++ b/mod/folder/lib.php
@@ -370,3 +370,49 @@ 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 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 folder_dndupload_handle($uploadinfo) {
+ global $DB, $USER;
+
+ $folder = new stdClass();
+ $folder->course = $uploadinfo->course->id;
+ $folder->name = $uploadinfo->displayname;
+ $folder->intro = '
'.$uploadinfo->displayname.'
';
+ $folder->introformat = FORMAT_HTML;
+ $folder->timemodified = time();
+
+ $folder->id = $DB->insert_record('folder', $folder);
+
+ // 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 $folder->id;
+ }
+
+ $DB->delete_records('folder', array('id' => $folder->id));
+ return false;
+}
diff --git a/mod/page/lang/en/page.php b/mod/page/lang/en/page.php
index 98af5a64d2e..843b9d45fd4 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 4512f5e7fca..321848b98ec 100644
--- a/mod/page/lib.php
+++ b/mod/page/lib.php
@@ -484,3 +484,54 @@ 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 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 page_dndupload_handle($uploadinfo) {
+ global $DB, $CFG;
+ require_once("$CFG->libdir/resourcelib.php");
+
+ $config = get_config('page');
+ $display = $config->display;
+ if ($display == RESOURCELIB_DISPLAY_POPUP) {
+ $displayoptions['popupwidth'] = $config->popupwidth;
+ $displayoptions['popupheight'] = $config->popupheight;
+ }
+ $displayoptions['printheading'] = $config->printheading;
+ $displayoptions['printintro'] = $config->printintro;
+ $displayoptions = serialize($displayoptions);
+
+ $page = new stdClass();
+ $page->course = $uploadinfo->course->id;
+ $page->name = $uploadinfo->displayname;
+ $page->intro = '
'.$uploadinfo->displayname.'
';
+ $page->introformat = FORMAT_HTML;
+ if ($uploadinfo->type == 'text/html') {
+ $page->contentformat = FORMAT_HTML;
+ $page->content = clean_param($uploadinfo->content, PARAM_CLEANHTML);
+ } else {
+ $page->contentformat = FORMAT_PLAIN;
+ $page->content = clean_param($uploadinfo->content, PARAM_TEXT);
+ }
+ $page->display = $display;
+ $page->displayoptions = $displayoptions;
+ $page->timemodified = time();
+
+ $page->id = $DB->insert_record('page', $page);
+
+ return $page->id;
+}
diff --git a/mod/resource/lang/en/resource.php b/mod/resource/lang/en/resource.php
index ae0ef794e7f..11ad55cc309 100644
--- a/mod/resource/lang/en/resource.php
+++ b/mod/resource/lang/en/resource.php
@@ -53,6 +53,7 @@ $string['displayselect_help'] = 'This setting, together with the file type and w
* 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 978f2048c54..d43a48d6cb8 100644
--- a/mod/resource/lib.php
+++ b/mod/resource/lib.php
@@ -490,3 +490,60 @@ 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 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 resource_dndupload_handle($uploadinfo) {
+ global $DB, $CFG;
+ require_once("$CFG->libdir/resourcelib.php");
+
+ // Set display options to site defaults.
+ $config = get_config('resource');
+ $display = $config->display;
+ $displayoptions = array();
+ if ($display == RESOURCELIB_DISPLAY_POPUP) {
+ $displayoptions['popupheight'] = $config->popupheight;
+ $displayoptions['popupwidth'] = $config->popupwidth;
+ }
+ if (in_array($display, array(RESOURCELIB_DISPLAY_AUTO, RESOURCELIB_DISPLAY_EMBED, RESOURCELIB_DISPLAY_FRAME))) {
+ $displayoptions['printheading'] = $config->printheading;
+ $displayoptions['printintro'] = $config->printintro;
+ }
+ $displayoptions = serialize($displayoptions);
+
+ // Create the database entry.
+ $resource = new stdClass();
+ $resource->course = $uploadinfo->course->id;
+ $resource->name = $uploadinfo->displayname;
+ $resource->intro = '
'.$uploadinfo->displayname.'
';
+ $resource->introformat = FORMAT_HTML;
+ $resource->display = $display;
+ $resource->displayoptions = $displayoptions;
+ $resource->timemodified = time();
+
+ $resource->id = $DB->insert_record('resource', $resource);
+
+ // Retrieve the file from the draft file area.
+ $context = context_module::instance($uploadinfo->coursemodule);
+ file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_resource', 'content', 0, array('subdirs' => false));
+ $fs = get_file_storage();
+ $files = $fs->get_area_files($context->id, 'mod_resource', 'content', 0, 'sortorder', false);
+ // Only ever one file - set it as the 'main' file.
+ $file = reset($files);
+ file_set_sortorder($context->id, 'mod_resource', 'content', 0, $file->get_filepath(), $file->get_filename(), 1);
+
+ return $resource->id;
+}
diff --git a/mod/url/lang/en/url.php b/mod/url/lang/en/url.php
index dacfb0cbe9c..6837d6149b7 100644
--- a/mod/url/lang/en/url.php
+++ b/mod/url/lang/en/url.php
@@ -30,6 +30,7 @@ $string['configframesize'] = 'When a web page or an uploaded file is displayed w
$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 197b75a9cfd..30c770701aa 100644
--- a/mod/url/lib.php
+++ b/mod/url/lib.php
@@ -343,3 +343,49 @@ 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 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 url_dndupload_handle($uploadinfo) {
+ global $DB, $CFG;
+ require_once("$CFG->libdir/resourcelib.php");
+
+ $config = get_config('url');
+ $display = $config->display;
+ if ($display == RESOURCELIB_DISPLAY_POPUP) {
+ $displayoptions['popupwidth'] = $config->popupwidth;
+ $displayoptions['popupheight'] = $config->popupheight;
+ }
+ if (in_array($display, array(RESOURCELIB_DISPLAY_AUTO, RESOURCELIB_DISPLAY_EMBED, RESOURCELIB_DISPLAY_FRAME))) {
+ $displayoptions['printheading'] = $config->printheading;
+ $displayoptions['printintro'] = $config->printintro;
+ }
+ $displayoptions = serialize($displayoptions);
+
+ $url = new stdClass();
+ $url->course = $uploadinfo->course->id;
+ $url->name = $uploadinfo->displayname;
+ $url->intro = '
'.$uploadinfo->displayname.'
';
+ $url->introformat = FORMAT_HTML;
+ $url->externalurl = clean_param($uploadinfo->content, PARAM_URL);
+ $url->display = $display;
+ $url->displayoptions = $displayoptions;
+ $url->timemodified = time();
+
+ $url->id = $DB->insert_record('url', $url);
+
+ return $url->id;
+}
diff --git a/repository/upload/lib.php b/repository/upload/lib.php
index 8b2641ea312..1b2bb9d85f7 100644
--- a/repository/upload/lib.php
+++ b/repository/upload/lib.php
@@ -42,9 +42,31 @@ class repository_upload extends repository {
* @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 @@ class repository_upload extends repository {
}
}
+ 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 1ea71e6c13f..7b6e4d488ea 100644
--- a/theme/base/style/course.css
+++ b/theme/base/style/course.css
@@ -127,3 +127,9 @@
table.category_subcategories {margin-bottom:1em;}
table.category_subcategories td {white-space: nowrap;}
+/* 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:clip;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;}