";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 0000000000000..3ec3b14aeeac8 --- /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 0000000000000..dc1644c5977f1 --- /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 d055ddc178206..c4b9535eec469 100644 --- a/course/classes/management/helper.php +++ b/course/classes/management/helper.php @@ -375,6 +375,14 @@ public static function get_course_listitem_actions(\core_course_category $catego '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 @@ public static function get_expanded_categories($withpath = null) { 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 7268824761a4d..5e783d56e46b7 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 ad74a3e2f3b3d..39ad5a77b7b75 100644 --- a/course/management.php +++ b/course/management.php @@ -107,6 +107,7 @@ $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 c1877c1c3d029..753544a0f2ba0 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -2283,7 +2283,7 @@ public function test_get_user_administration_options() { $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 @@ public function test_get_user_administration_options() { $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 @@ public function test_get_user_administration_options() { $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 6d65bc9c59409..611ed6c54b358 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 0c08c50eb0a7d..1e363e565e2c6 100644 --- a/lang/en/backup.php +++ b/lang/en/backup.php @@ -160,6 +160,21 @@ $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['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['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['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 4885cc4598ee3..6ba40104038fc 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -289,6 +289,7 @@ $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 1e56b09d14570..eaefd6b8dac17 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 0000000000000..6504d408cdc45 --- /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