1
0
mirror of https://github.com/moodle/moodle.git synced 2025-05-08 17:26:13 +02:00

MDL-22504 Drag and drop upload course - enables upload of files, text and urls to a course page

This commit is contained in:
Davo Smith 2012-02-17 21:23:28 +00:00
parent aa753ac24f
commit 32528f94e4
15 changed files with 1837 additions and 5 deletions
course
lang/en
lib
mod
folder
page
resource
url
repository/upload
theme/base/style

@ -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);
}

39
lang/en/dndupload.php Normal file

@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for 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';

879
lib/ajax/dndupload.js Normal file

@ -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 <http://www.gnu.org/licenses/>.
/**
* 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<sectionclasses.length; i++) {
if (!test.hasClass(sectionclasses[i])) {
return false;
}
return true;
}
}, true);
},
/**
* Work out the number of the section we have been dropped on to, from the section element
* @param DOMElement section the selected section
* @return int the section number
*/
get_section_number: function(section) {
var sectionid = section.get('id').split('-');
if (sectionid.length < 2 || sectionid[0] != 'section') {
return false;
}
return parseInt(sectionid[1]);
},
/**
* Check if the event includes data of the given type
* @param e the event details
* @param type the data type to check for
* @return true if the data type is found in the event data
*/
types_includes: function(e, type) {
var i;
var types = e._event.dataTransfer.types;
for (i=0; i<types.length; i++) {
if (types[i] == type) {
return true;
}
}
return false;
},
/**
* Look through the event data, checking it against the registered data types
* (in order of priority) and return details of the first matching data type
* @param e the event details
* @return mixed false if not found or an object {
* realtype: the type as given by the browser
* addmessage: the message to show to the user during dragging
* namemessage: the message for requesting a name for the resource from the user
* type: the identifier of the type (may match several 'realtype's)
* }
*/
drag_type: function(e) {
if (this.types_includes(e, 'Files')) {
return {
realtype: 'Files',
addmessage: M.util.get_string('addfilehere', 'core_dndupload'),
namemessage: null, // Should not be asked for anyway
type: 'Files'
};
}
// Check each of the registered types
var types = this.handlers.types;
for (var i=0; i<types.length; i++) {
// Check each of the different identifiers for this type
var dttypes = types[i].datatransfertypes;
for (var j=0; j<dttypes.length; j++) {
if (this.types_includes(e, dttypes[j])) {
return {
realtype: dttypes[j],
addmessage: types[i].addmessage,
namemessage: types[i].namemessage,
type: types[i].identifier,
handlers: types[i].handlers
};
}
}
}
return false; // No types we can handle
},
/**
* Check the content of the drag/drop includes a type we can handle, then, if
* it is, notify the browser that we want to handle it
* @param event e
* @return string type of the event or false
*/
check_drag: function(e) {
var type = this.drag_type(e);
if (type) {
// Notify browser that we will handle this drag/drop
e.stopPropagation();
e.preventDefault();
}
return type;
},
/**
* Handle a dragenter event: add a suitable 'add here' message
* when a drag event occurs, containing a registered data type
* @param e event data
* @return false to prevent the event from continuing to be processed
*/
drag_enter: function(e) {
if (!(type = this.check_drag(e))) {
return false;
}
var section = this.get_section(e.currentTarget);
if (!section) {
return false;
}
if (this.currentsection && this.currentsection != section) {
this.currentsection = section;
this.entercount = 1;
} else {
this.entercount++;
if (this.entercount > 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 = '&nbsp;';
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<filehandlers.length; i++) {
if (filehandlers[i].extension == '*' || filehandlers[i].extension == extension) {
handlers.push(filehandlers[i]);
}
}
if (handlers.length == 0) {
// No handlers at all (not even 'resource'?)
return;
}
if (handlers.length == 1) {
this.upload_file(file, section, sectionnumber, handlers[0].module);
return;
}
this.file_handler_dialog(handlers, extension, file, section, sectionnumber);
},
/**
* Show a dialog box, allowing the user to choose what to do with the file they are uploading
* @param handlers the available handlers to choose between
* @param extension the extension of the file being uploaded
* @param file the File object being uploaded
* @param section the DOM element of the section being uploaded to
* @param sectionnumber the number of the selected course section
*/
file_handler_dialog: function(handlers, extension, file, section, sectionnumber) {
if (this.uploaddialog) {
var details = new Object();
details.isfile = true;
details.handlers = handlers;
details.extension = extension;
details.file = file;
details.section = section;
details.sectionnumber = sectionnumber;
this.uploadqueue.push(details);
return;
}
this.uploaddialog = true;
var timestamp = new Date().getTime();
var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
var content = '';
var sel;
if (extension in this.lastselected) {
sel = this.lastselected[extension];
} else {
sel = handlers[0].module;
}
content += '<p>'+M.util.get_string('actionchoice', 'core_dndupload', file.name)+'</p>';
content += '<div id="dndupload_handlers'+uploadid+'">';
for (var i=0; i<handlers.length; i++) {
var id = 'dndupload_handler'+uploadid+handlers[i].module;
var checked = (handlers[i].module == sel) ? 'checked="checked" ' : '';
content += '<input type="radio" name="handler" value="'+handlers[i].module+'" id="'+id+'" '+checked+'/>';
content += ' <label for="'+id+'">';
content += handlers[i].message;
content += '</label><br/>';
}
content += '</div>';
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 += '<label for="'+nameid+'">'+type.namemessage+'</label>';
content += ' <input type="text" id="'+nameid+'" value="" />';
if (type.handlers.length > 1) {
content += '<div id="dndupload_handlers'+uploadid+'">';
var sel = type.handlers[0].module;
for (var i=0; i<type.handlers.length; i++) {
var id = 'dndupload_handler'+uploadid;
var checked = (type.handlers[i].module == sel) ? 'checked="checked" ' : '';
content += '<input type="radio" name="handler" value="'+type.handlers[i].module+'" id="'+id+'" '+checked+'/>';
content += ' <label for="'+id+'">';
content += type.handlers[i].message;
content += '</label><br/>';
}
content += '</div>';
}
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);
});
}
};

39
lib/ajax/dndupload.php Normal file

@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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);

635
lib/dnduploadlib.php Normal file

@ -0,0 +1,635 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* 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();
}
}

@ -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';

@ -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 = '<p>'.$uploadinfo->displayname.'</p>';
$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;
}

@ -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.';

@ -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 = '<p>'.$uploadinfo->displayname.'</p>';
$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;
}

@ -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';

@ -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 = '<p>'.$uploadinfo->displayname.'</p>';
$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;
}

@ -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:

@ -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 = '<p>'.$uploadinfo->displayname.'</p>';
$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;
}

@ -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';

@ -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;}