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'; + content += '
'; + 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(); + // 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;}