";function j(){b.get_string("loading").then(function(a){c.create({type:c.types.DEFAULT,title:a,body:q,large:!0}).done(function(a){p=a;p.getRoot().on("click","#id_submitreturn",l);p.getRoot().on("click","#id_submitdisplay",function(a){a.formredirect=!0;l(a)});p.getRoot().on("click","#id_cancel",function(a){a.preventDefault();p.setBody(q);p.hide()})})}).catch(function(){h.exception(new Error("Failed to load string: loading"))})}function k(a){if("undefined"==typeof a){a={}}var c={jsonformdata:JSON.stringify(a),courseid:o.id};p.setBody(q);b.get_string("copycoursetitle","backup",o.shortname).then(function(a){p.setTitle(a);p.setBody(g.loadFragment("course","new_base_form",n,c))}).catch(function(){h.exception(new Error("Failed to load string: copycoursetitle"))})}function l(b){b.preventDefault();var c=p.getRoot().find("form").serialize(),d=JSON.stringify(c),e=a.merge(p.getRoot().find("[aria-invalid=\"true\"]"),p.getRoot().find(".error"));if(e.length){e.first().focus();return}f.call([{methodname:"core_backup_submit_copy_form",args:{jsonformdata:d}}])[0].done(function(){p.setBody(q);p.hide();if(!0==b.formredirect){var a=i.wwwroot+"/backup/copyprogress.php?id="+o.id;window.location.assign(a)}}).fail(function(){k(c)})}m.init=function(b){n=b;j();a(".action-copy").on("click",function(a){a.preventDefault();var b=new URL(this.getAttribute("href")),c=new URLSearchParams(b.search),d=c.get("id");f.call([{methodname:"core_course_get_courses",args:{options:{ids:[d]}}}])[0].done(function(a){o=a[0];k()}).fail(function(){h.exception(new Error("Failed to load course"))});p.show()})};return m}); +//# sourceMappingURL=copy_modal.min.js.map diff --git a/course/amd/build/copy_modal.min.js.map b/course/amd/build/copy_modal.min.js.map new file mode 100644 index 00000000000..3ec3b14aeea --- /dev/null +++ b/course/amd/build/copy_modal.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/copy_modal.js"],"names":["define","$","Str","ModalFactory","ModalEvents","ajax","Fragment","Notification","Config","CopyModal","contextid","course","modalObj","spinner","createModal","get_string","then","title","create","type","types","DEFAULT","body","large","done","modal","getRoot","on","processModalForm","e","formredirect","preventDefault","setBody","hide","catch","exception","Error","updateModalBody","formdata","params","JSON","stringify","id","shortname","setTitle","loadFragment","copyform","find","serialize","formjson","invalid","merge","length","first","focus","call","methodname","args","jsonformdata","redirect","wwwroot","window","location","assign","fail","init","context","url","URL","getAttribute","URLSearchParams","search","courseid","get","response","show"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see
'\n + ''\n + '
';\n\n /**\n * Creates the modal for the course copy form\n *\n * @private\n */\n function createModal() {\n // Get the Title String.\n Str.get_string('loading').then(function(title) {\n // Create the Modal.\n ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n title: title,\n body: spinner,\n large: true\n })\n .done(function(modal) {\n modalObj = modal;\n // Explicitly handle form click events.\n modalObj.getRoot().on('click', '#id_submitreturn', processModalForm);\n modalObj.getRoot().on('click', '#id_submitdisplay', function(e) {\n e.formredirect = true;\n processModalForm(e);\n\n });\n modalObj.getRoot().on('click', '#id_cancel', function(e) {\n e.preventDefault();\n modalObj.setBody(spinner);\n modalObj.hide();\n });\n });\n return;\n }).catch(function() {\n Notification.exception(new Error('Failed to load string: loading'));\n });\n }\n\n /**\n * Updates the body of the modal window.\n *\n * @param {Object} formdata\n * @private\n */\n function updateModalBody(formdata) {\n if (typeof formdata === \"undefined\") {\n formdata = {};\n }\n\n var params = {\n 'jsonformdata': JSON.stringify(formdata),\n 'courseid': course.id\n };\n\n modalObj.setBody(spinner);\n Str.get_string('copycoursetitle', 'backup', course.shortname).then(function(title) {\n modalObj.setTitle(title);\n modalObj.setBody(Fragment.loadFragment('course', 'new_base_form', contextid, params));\n return;\n }).catch(function() {\n Notification.exception(new Error('Failed to load string: copycoursetitle'));\n });\n }\n\n /**\n * Updates Moodle form with selected information.\n *\n * @param {Object} e\n * @private\n */\n function processModalForm(e) {\n e.preventDefault(); // Stop modal from closing.\n\n // Form data.\n var copyform = modalObj.getRoot().find('form').serialize();\n var formjson = JSON.stringify(copyform);\n\n // Handle invalid form fields for better UX.\n var invalid = $.merge(\n modalObj.getRoot().find('[aria-invalid=\"true\"]'),\n modalObj.getRoot().find('.error')\n );\n\n if (invalid.length) {\n invalid.first().focus();\n return;\n }\n\n // Submit form via ajax.\n ajax.call([{\n methodname: 'core_backup_submit_copy_form',\n args: {\n jsonformdata: formjson\n },\n }])[0].done(function() {\n // For submission succeeded.\n modalObj.setBody(spinner);\n modalObj.hide();\n\n if (e.formredirect == true) {\n // We are redirecting to copy progress display.\n let redirect = Config.wwwroot + \"/backup/copyprogress.php?id=\" + course.id;\n window.location.assign(redirect);\n }\n\n }).fail(function() {\n // Form submission failed server side, redisplay with errors.\n updateModalBody(copyform);\n });\n }\n\n /**\n * Initialise the class.\n *\n * @param {Object} context\n * @public\n */\n CopyModal.init = function(context) {\n contextid = context;\n // Setup the initial Modal.\n createModal();\n\n // Setup the click handlers on the copy buttons.\n $('.action-copy').on('click', function(e) {\n e.preventDefault(); // Stop. Hammer time.\n let url = new URL(this.getAttribute('href'));\n let params = new URLSearchParams(url.search);\n let courseid = params.get('id');\n\n ajax.call([{ // Get the course information.\n methodname: 'core_course_get_courses',\n args: {\n 'options': {'ids': [courseid]},\n },\n }])[0].done(function(response) {\n // We have the course info get the modal content.\n course = response[0];\n updateModalBody();\n\n }).fail(function() {\n Notification.exception(new Error('Failed to load course'));\n });\n\n modalObj.show();\n });\n\n };\n\n return CopyModal;\n});\n"],"file":"copy_modal.min.js"} \ No newline at end of file diff --git a/course/amd/src/copy_modal.js b/course/amd/src/copy_modal.js new file mode 100644 index 00000000000..dc1644c5977 --- /dev/null +++ b/course/amd/src/copy_modal.js @@ -0,0 +1,190 @@ +// 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' + + '' + + '
'; + + /** + * Creates the modal for the course copy form + * + * @private + */ + function createModal() { + // Get the Title String. + Str.get_string('loading').then(function(title) { + // Create the Modal. + ModalFactory.create({ + type: ModalFactory.types.DEFAULT, + title: title, + body: spinner, + large: true + }) + .done(function(modal) { + modalObj = modal; + // Explicitly handle form click events. + modalObj.getRoot().on('click', '#id_submitreturn', processModalForm); + modalObj.getRoot().on('click', '#id_submitdisplay', function(e) { + e.formredirect = true; + processModalForm(e); + + }); + modalObj.getRoot().on('click', '#id_cancel', function(e) { + e.preventDefault(); + modalObj.setBody(spinner); + modalObj.hide(); + }); + }); + return; + }).catch(function() { + Notification.exception(new Error('Failed to load string: loading')); + }); + } + + /** + * Updates the body of the modal window. + * + * @param {Object} formdata + * @private + */ + function updateModalBody(formdata) { + if (typeof formdata === "undefined") { + formdata = {}; + } + + var params = { + 'jsonformdata': JSON.stringify(formdata), + 'courseid': course.id + }; + + modalObj.setBody(spinner); + Str.get_string('copycoursetitle', 'backup', course.shortname).then(function(title) { + modalObj.setTitle(title); + modalObj.setBody(Fragment.loadFragment('course', 'new_base_form', contextid, params)); + return; + }).catch(function() { + Notification.exception(new Error('Failed to load string: copycoursetitle')); + }); + } + + /** + * Updates Moodle form with selected information. + * + * @param {Object} e + * @private + */ + function processModalForm(e) { + e.preventDefault(); // Stop modal from closing. + + // Form data. + var copyform = modalObj.getRoot().find('form').serialize(); + var formjson = JSON.stringify(copyform); + + // Handle invalid form fields for better UX. + var invalid = $.merge( + modalObj.getRoot().find('[aria-invalid="true"]'), + modalObj.getRoot().find('.error') + ); + + if (invalid.length) { + invalid.first().focus(); + return; + } + + // Submit form via ajax. + ajax.call([{ + methodname: 'core_backup_submit_copy_form', + args: { + jsonformdata: formjson + }, + }])[0].done(function() { + // For submission succeeded. + modalObj.setBody(spinner); + modalObj.hide(); + + if (e.formredirect == true) { + // We are redirecting to copy progress display. + let redirect = Config.wwwroot + "/backup/copyprogress.php?id=" + course.id; + window.location.assign(redirect); + } + + }).fail(function() { + // Form submission failed server side, redisplay with errors. + updateModalBody(copyform); + }); + } + + /** + * Initialise the class. + * + * @param {Object} context + * @public + */ + CopyModal.init = function(context) { + contextid = context; + // Setup the initial Modal. + createModal(); + + // Setup the click handlers on the copy buttons. + $('.action-copy').on('click', function(e) { + e.preventDefault(); // Stop. Hammer time. + let url = new URL(this.getAttribute('href')); + let params = new URLSearchParams(url.search); + let courseid = params.get('id'); + + ajax.call([{ // Get the course information. + methodname: 'core_course_get_courses', + args: { + 'options': {'ids': [courseid]}, + }, + }])[0].done(function(response) { + // We have the course info get the modal content. + course = response[0]; + updateModalBody(); + + }).fail(function() { + Notification.exception(new Error('Failed to load course')); + }); + + modalObj.show(); + }); + + }; + + return CopyModal; +}); diff --git a/course/classes/management/helper.php b/course/classes/management/helper.php index d055ddc1782..c4b9535eec4 100644 --- a/course/classes/management/helper.php +++ b/course/classes/management/helper.php @@ -375,6 +375,14 @@ class helper { 'attributes' => array('class' => 'action-edit') ); } + // Copy. + if (self::can_copy_course($course->id)) { + $actions[] = array( + 'url' => new \moodle_url('/backup/copy.php', array('id' => $course->id, 'returnto' => 'catmanage')), + 'icon' => new \pix_icon('t/copy', \get_string('copycourse')), + 'attributes' => array('class' => 'action-copy') + ); + } // Delete. if ($course->can_delete()) { $actions[] = array( @@ -996,4 +1004,24 @@ class helper { return array($parent); } } + + /** + * Get an array of the capabilities required to copy a course. + * + * @return array + */ + public static function get_course_copy_capabilities(): array { + return array('moodle/backup:backupcourse', 'moodle/restore:restorecourse', 'moodle/course:view', 'moodle/course:create'); + } + + /** + * Returns true if the current user can copy this course. + * + * @param int $courseid + * @return bool + */ + public static function can_copy_course(int $courseid): bool { + $coursecontext = \context_course::instance($courseid); + return has_all_capabilities(self::get_course_copy_capabilities(), $coursecontext); + } } diff --git a/course/lib.php b/course/lib.php index 7268824761a..5e783d56e46 100644 --- a/course/lib.php +++ b/course/lib.php @@ -4028,7 +4028,6 @@ function course_get_user_administration_options($course, $context) { $isfrontpage = $course->id == SITEID; $completionenabled = $CFG->enablecompletion && $course->enablecompletion; $hascompletiontabs = count(core_completion\manager::get_available_completion_tabs($course, $context)) > 0; - $options = new stdClass; $options->update = has_capability('moodle/course:update', $context); $options->editcompletion = $CFG->enablecompletion && @@ -4039,6 +4038,7 @@ function course_get_user_administration_options($course, $context) { $options->reports = has_capability('moodle/site:viewreports', $context); $options->backup = has_capability('moodle/backup:backupcourse', $context); $options->restore = has_capability('moodle/restore:restorecourse', $context); + $options->copy = \core_course\management\helper::can_copy_course($course->id); $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context)); if (!$isfrontpage) { @@ -4944,3 +4944,40 @@ function course_get_course_dates_for_user_ids(stdClass $course, array $userids): function course_get_course_dates_for_user_id(stdClass $course, int $userid): array { return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid]; } + +/** + * Renders the course copy form for the modal on the course management screen. + * + * @param array $args + * @return string $o Form HTML. + */ +function course_output_fragment_new_base_form($args) { + + $serialiseddata = json_decode($args['jsonformdata'], true); + $formdata = []; + if (!empty($serialiseddata)) { + parse_str($serialiseddata, $formdata); + } + + $context = context_course::instance($args['courseid']); + $copycaps = \core_course\management\helper::get_course_copy_capabilities(); + require_all_capabilities($copycaps, $context); + + $course = get_course($args['courseid']); + $mform = new \core_backup\output\copy_form( + null, + array('course' => $course, 'returnto' => '', 'returnurl' => ''), + 'post', '', ['class' => 'ignoredirty'], true, $formdata); + + if (!empty($serialiseddata)) { + // If we were passed non-empty form data we want the mform to call validation functions and show errors. + $mform->is_validated(); + } + + ob_start(); + $mform->display(); + $o = ob_get_contents(); + ob_end_clean(); + + return $o; +} diff --git a/course/management.php b/course/management.php index ad74a3e2f3b..39ad5a77b7b 100644 --- a/course/management.php +++ b/course/management.php @@ -107,6 +107,7 @@ $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); $PAGE->set_title($strmanagement); $PAGE->set_heading($pageheading); +$PAGE->requires->js_call_amd('core_course/copy_modal', 'init', array($context->id)); // This is a system level page that operates on other contexts. require_login(); diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index c1877c1c3d0..753544a0f2b 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -2283,7 +2283,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { $adminoptions->{$option['name']} = $option['available']; } if ($course['id'] == SITEID) { - $this->assertCount(16, $course['options']); + $this->assertCount(17, $course['options']); $this->assertFalse($adminoptions->update); $this->assertFalse($adminoptions->filters); $this->assertFalse($adminoptions->reports); @@ -2298,8 +2298,9 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { $this->assertFalse($adminoptions->reset); $this->assertFalse($adminoptions->roles); $this->assertFalse($adminoptions->editcompletion); + $this->assertFalse($adminoptions->copy); } else { - $this->assertCount(14, $course['options']); + $this->assertCount(15, $course['options']); $this->assertFalse($adminoptions->update); $this->assertFalse($adminoptions->filters); $this->assertFalse($adminoptions->reports); @@ -2314,6 +2315,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { $this->assertFalse($adminoptions->reset); $this->assertFalse($adminoptions->roles); $this->assertFalse($adminoptions->editcompletion); + $this->assertFalse($adminoptions->copy); } } } diff --git a/course/view.php b/course/view.php index 6d65bc9c594..611ed6c54b3 100644 --- a/course/view.php +++ b/course/view.php @@ -243,7 +243,7 @@ $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); - if ($USER->editing == 1 && !empty($CFG->enableasyncbackup)) { + if ($USER->editing == 1) { // MDL-65321 The backup libraries are quite heavy, only require the bare minimum. require_once($CFG->dirroot . '/backup/util/helper/async_helper.class.php'); diff --git a/lang/en/backup.php b/lang/en/backup.php index 0c08c50eb0a..1e363e565e2 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -160,6 +160,21 @@ $string['confirmcancelyes'] = 'Cancel backup'; $string['confirmcancelno'] = 'Do not cancel'; $string['confirmnewcoursecontinue'] = 'New course warning'; $string['confirmnewcoursecontinuequestion'] = 'A temporary (hidden) course will be created by the course restoration process. To abort restoration click cancel. Do not close the browser while restoring.'; +$string['copiesinprogress'] = 'This course has copies in progress. View in progress copies.'; +$string['copycoursedesc'] = 'This course will be duplicated and put into the given course category.'; +$string['copycourseheading'] = 'Copy a course'; +$string['copycoursetitle'] = 'Copy course: {$a}'; +$string['copydest'] = 'Destination'; +$string['copyingcourse'] = 'Course copying in progress'; +$string['copyingcourseshortname'] = 'copying'; +$string['copyformfail'] = 'Ajax submission of course copy form has failed.'; +$string['copyop'] = 'Current operation'; +$string['copyprogressheading'] = 'Course copies in progress'; +$string['copyprogressheading_help'] = 'This table shows the status of all of your unfinished course copies.'; +$string['copyprogresstitle'] = 'Course copy progress'; +$string['copyreturn'] = 'Copy and return'; +$string['copysource'] = 'Source'; +$string['copyview'] = 'Copy and view'; $string['coursecategory'] = 'Category the course will be restored into'; $string['courseid'] = 'Original ID'; $string['coursesettings'] = 'Course settings'; @@ -253,6 +268,8 @@ $string['lockedbyconfig'] = 'This setting has been locked by the default backup $string['lockedbyhierarchy'] = 'Locked by dependencies'; $string['loglifetime'] = 'Keep logs for'; $string['managefiles'] = 'Manage backup files'; +$string['keptroles'] = 'Keep enrolments of role'; +$string['keptroles_help'] = 'Select which roles and the users with those roles in the source course that you want to keep enrolments for in the new course. Any users with those roles will be copied into the new course.'; $string['missingfilesinpool'] = 'Some files could not be saved during the backup, and so it will not be possible to restore them.'; $string['moodleversion'] = 'Moodle version'; $string['moreresults'] = 'There are too many results, enter a more specific search.'; @@ -368,6 +385,7 @@ $string['skipmodifprev'] = 'Skip courses not modified since previous backup'; $string['skipmodifprevhelp'] = 'Choose whether to skip courses that have not been modified since the last automatic backup. This requires logging to be enabled.'; $string['status'] = 'Status'; $string['successful'] = 'Backup successful'; +$string['successfulcopy'] = 'Copy successful'; $string['successfulrestore'] = 'Restore successful'; $string['timetaken'] = 'Time taken'; $string['title'] = 'Title'; @@ -375,6 +393,8 @@ $string['totalcategorysearchresults'] = 'Total categories: {$a}'; $string['totalcoursesearchresults'] = 'Total courses: {$a}'; $string['undefinedrolemapping'] = 'Role mapping undefined for \'{$a}\' archetype.'; $string['unnamedsection'] = 'Unnamed section'; +$string['userdata'] = 'Keep user data'; +$string['userdata_help'] = 'When enabled user generated data in the source course will be copied into the new course. This includes forum posts, assignment submissions, etc.'; $string['userinfo'] = 'Userinfo'; $string['module'] = 'Module'; $string['morecoursesearchresults'] = 'More than {$a} courses found, showing first {$a} results'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 4885cc4598e..6ba40104038 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -289,6 +289,7 @@ $string['cookiesenabledonlysession_help'] = 'This site uses one session cookie, $string['cookiesnotenabled'] = 'Unfortunately, cookies are currently not enabled in your browser'; $string['copy'] = 'copy'; $string['copyasnoun'] = 'copy'; +$string['copycourse'] = 'Copy course'; $string['copyingcoursefiles'] = 'Copying course files'; $string['copyingsitefiles'] = 'Copying site files used in course'; $string['copyinguserfiles'] = 'Copying user files'; diff --git a/lib/accesslib.php b/lib/accesslib.php index 1e56b09d145..eaefd6b8dac 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -861,6 +861,34 @@ function require_capability($capability, context $context, $userid = null, $doan } } +/** + * A convenience function that tests has_capability for a list of capabilities, and displays an error if + * the user does not have that capability. + * + * This is just a utility method that calls has_capability in a loop. Try to put + * the capabilities that fewest users are likely to have first in the list for best + * performance. + * + * @category access + * @see has_capability() + * + * @param array $capabilities an array of capability names. + * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance(). + * @param int $userid A user id. By default (null) checks the permissions of the current user. + * @param bool $doanything If false, ignore effect of admin role assignment + * @param string $errormessage The error string to to user. Defaults to 'nopermissions'. + * @param string $stringfile The language file to load the error string from. Defaults to 'error'. + * @return void terminates with an error if the user does not have the given capability. + */ +function require_all_capabilities(array $capabilities, context $context, $userid = null, $doanything = true, + $errormessage = 'nopermissions', $stringfile = ''): void { + foreach ($capabilities as $capability) { + if (!has_capability($capability, $context, $userid, $doanything)) { + throw new required_capability_exception($context, $capability, $errormessage, $stringfile); + } + } +} + /** * Return a nested array showing all role assignments for the user. * [ra] => [contextpath][roleid] = roleid diff --git a/lib/classes/task/asynchronous_copy_task.php b/lib/classes/task/asynchronous_copy_task.php new file mode 100644 index 00000000000..6504d408cdc --- /dev/null +++ b/lib/classes/task/asynchronous_copy_task.php @@ -0,0 +1,211 @@ +. + +/** + * Adhoc task that performs asynchronous course copies. + * + * @package core + * @copyright 2020 onward The Moodle Users Association