This commit is contained in:
Ilya Tregubov
2024-01-31 09:37:37 +08:00
27 changed files with 717 additions and 53 deletions
+40 -6
View File
@@ -22,6 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use tool_uploadcourse\permissions;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->dirroot . '/course/lib.php');
@@ -448,6 +450,11 @@ class tool_uploadcourse_course {
return false;
}
if ($error = permissions::check_permission_to_delete($this->shortname)) {
$this->error('coursedeletionpermission', $error);
return false;
}
$this->do = self::DO_DELETE;
return true;
}
@@ -680,9 +687,20 @@ class tool_uploadcourse_course {
return false;
}
if ($error = permissions::check_permission_to_update($coursedata)) {
$this->error('cannotupdatepermission', $error);
return false;
}
$this->do = self::DO_UPDATE;
} else {
$coursedata = $this->get_final_create_data($coursedata);
if ($error = permissions::check_permission_to_create($coursedata)) {
$this->error('courseuploadnotallowed', $error);
return false;
}
$this->do = self::DO_CREATE;
}
@@ -799,6 +817,7 @@ class tool_uploadcourse_course {
$this->data = $coursedata;
// Get enrolment data. Where the course already exists, we can also perform validation.
// Some data is impossible to validate without the existing course, we will do it again during actual upload.
$this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
$courseid = $coursedata['id'] ?? 0;
$errors = $this->validate_enrolment_data($courseid, $this->enrolmentdata);
@@ -823,6 +842,11 @@ class tool_uploadcourse_course {
return false;
}
if ($this->restoredata && ($error = permissions::check_permission_to_restore($this->do, $this->data))) {
$this->error('courserestorepermission', $error);
return false;
}
// We can only reset courses when allowed and we are updating the course.
if ($this->importoptions['reset'] || $this->options['reset']) {
if ($this->do !== self::DO_UPDATE) {
@@ -833,6 +857,11 @@ class tool_uploadcourse_course {
$this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse'));
return false;
}
if ($error = permissions::check_permission_to_reset($this->data)) {
$this->error('courseresetpermission', $error);
return false;
}
}
return true;
@@ -889,7 +918,7 @@ class tool_uploadcourse_course {
$rc->execute_plan();
$this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse'));
} else {
$this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
$this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringcourse', 'tool_uploadcourse'));
}
$rc->destroy();
}
@@ -913,7 +942,7 @@ class tool_uploadcourse_course {
/**
* Validate passed enrolment data against an existing course
*
* @param int $courseid
* @param int $courseid id of the course where enrolment methods are created/updated or 0 if it is a new course
* @param array[] $enrolmentdata
* @return lang_string[] Errors keyed on error code
*/
@@ -1050,8 +1079,13 @@ class tool_uploadcourse_course {
// Create/update enrolment.
$plugin = $enrolmentplugins[$enrolmethod];
if ($plugin->is_csv_upload_supported()) {
// In case we could not properly validate enrolment data before the course existed
// let's repeat it again here.
$errors = $plugin->validate_enrol_plugin_data($method, $course->id);
if (!$errors) {
$status = ($todisable) ? ENROL_INSTANCE_DISABLED : ENROL_INSTANCE_ENABLED;
$method += ['status' => $status, 'courseid' => $course->id, 'id' => $instance->id ?? null];
$method = $plugin->fill_enrol_custom_fields($method, $course->id);
// Create a new instance if necessary.
@@ -1150,9 +1184,9 @@ class tool_uploadcourse_course {
$plugin->update_instance($instance, $modifiedinstance);
} else {
$this->error('errorunsupportedmethod',
new lang_string('errorunsupportedmethod', 'tool_uploadcourse',
$enrolmethod));
foreach ($errors as $key => $message) {
$this->error($key, $message);
}
}
}
}
@@ -540,6 +540,11 @@ class tool_uploadcourse_helper {
$params = array('idnumber' => $idnumber);
$id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
if ($id && !core_course_category::get($id, IGNORE_MISSING)) {
// Category is not visible to the current user.
$id = false;
}
// Little hack to be able to differenciate between the cache not set and a category not found.
if ($id === false) {
$id = -1;
@@ -578,6 +583,11 @@ class tool_uploadcourse_helper {
break;
}
$record = reset($records);
if (!core_course_category::get($id, IGNORE_MISSING)) {
// Category is not visible to the current user.
$id = -1;
break;
}
$id = $record->id;
$parent = $record->id;
} else {
@@ -0,0 +1,262 @@
<?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/>.
namespace tool_uploadcourse;
use context_course;
use context_coursecat;
use core_course_category;
use core_tag_tag;
use lang_string;
use tool_uploadcourse_course;
/**
* Checks various permissions related to the course upload process.
*
* @package tool_uploadcourse
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class permissions {
/**
* Check permission to use tool_uploadcourse in a given category.
*
* @param int $catid
* @param lang_string|null $customerror
* @return lang_string|null
*/
protected static function check_permission_to_use_uploadcourse_tool(
int $catid,
?lang_string $customerror = null
): ?lang_string {
$category = core_course_category::get($catid, IGNORE_MISSING);
if (!$category || !has_capability('tool/uploadcourse:use', $category->get_context())) {
if ($customerror) {
return $customerror;
}
return new lang_string('courseuploadnotallowed', 'tool_uploadcourse',
$category ? $category->get_formatted_name() : $catid);
}
return null;
}
/**
* Check capabilities to delete a course and to use tool_uploadcourse for it.
*
* @param string $shortname course shortname
* @return lang_string|null
*/
public static function check_permission_to_delete(string $shortname): ?lang_string {
global $DB;
$course = $DB->get_record('course', ['shortname' => $shortname]);
if ($error = self::check_permission_to_use_uploadcourse_tool($course->category)) {
return $error;
}
if (!has_capability('moodle/course:delete', context_course::instance($course->id))) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:delete'));
}
return null;
}
/**
* Check capability in a course (that exists or is about to be created).
*
* @param int $do one of tool_uploadcourse_course::DO_UPDATE or tool_uploadcourse_course::DO_ADD
* @param array $coursedata data to update/create course with, must contain either 'id' or 'category' respectively
* @param string $capability capability to check
* @return lang_string|null error string or null
*/
protected static function check_capability(int $do, array $coursedata, string $capability): ?lang_string {
if ($do == tool_uploadcourse_course::DO_UPDATE) {
$context = context_course::instance($coursedata['id']);
$hascap = has_capability($capability, $context);
} else {
$catcontext = context_coursecat::instance($coursedata['category']);
$hascap = guess_if_creator_will_have_course_capability($capability, $catcontext);
}
if (!$hascap) {
return new lang_string('nopermissions', 'error', get_capability_string($capability));
}
return null;
}
/**
* Check permission to update the course.
*
* This checks capabilities:
* - to use tool_uploadcourse in the category where course is in and in the category where it will be moved to (if applicable).
* - to change course category (if applicable).
* - to update course details.
* - to force course language (if applicable).
* - to change course idnumber, shortname, fullname, summary, visibility, tags (if applicable).
*
* @param array $coursedata data to update a course with, always contains 'id'
* @return lang_string|null
*/
public static function check_permission_to_update(array $coursedata): ?lang_string {
$course = get_course($coursedata['id']);
if ($error = self::check_permission_to_use_uploadcourse_tool($course->category,
new lang_string('courseuploadupdatenotallowed', 'tool_uploadcourse'))) {
return $error;
}
if (!has_capability('moodle/course:update', context_course::instance($course->id))) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:update'));
}
// If user requested to change course category check permissions to use tool in target category
// and capabilities to change category.
if (!empty($coursedata['category']) && $coursedata['category'] != $course->category) {
if ($error = self::check_permission_to_use_uploadcourse_tool($coursedata['category'])) {
return $error;
}
if (!has_capability('moodle/course:changecategory', context_coursecat::instance($course->category))) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:changecategory'));
}
if (!has_capability('moodle/course:changecategory', context_coursecat::instance($coursedata['category']))) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:changecategory'));
}
}
$context = context_course::instance($coursedata['id']);
// If lang is specified, check the user is allowed to set that field.
if (!empty($coursedata['lang']) && $coursedata['lang'] !== $course->lang) {
if (!has_capability('moodle/course:setforcedlanguage', $context)) {
return new lang_string('cannotforcelang', 'tool_uploadcourse');
}
}
// Check permission to change course idnumber.
if (array_key_exists('idnumber', $coursedata) && $coursedata['idnumber'] !== $course->idnumber &&
!has_capability('moodle/course:changeidnumber', $context)) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:changeidnumber'));
}
// Check permission to change course shortname.
if (array_key_exists('shortname', $coursedata) && $coursedata['shortname'] !== $course->shortname &&
!has_capability('moodle/course:changeshortname', $context)) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:changeshortname'));
}
// Check permission to change course fullname.
if (array_key_exists('fullname', $coursedata) && $coursedata['fullname'] !== $course->fullname &&
!has_capability('moodle/course:changefullname', $context)) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:changefullname'));
}
// Check permission to change course summary.
if (array_key_exists('summary', $coursedata) && $coursedata['summary'] !== $course->summary &&
!has_capability('moodle/course:changesummary', $context)) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:changesummary'));
}
// Check permission to change course visibility.
if (array_key_exists('visible', $coursedata) && $coursedata['visible'] !== $course->visible &&
!has_capability('moodle/course:visibility', $context)) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:visibility'));
}
// If tags are specified and enabled check if user can updat them.
if (core_tag_tag::is_enabled('core', 'course') &&
(array_key_exists('tags', $coursedata) && strval($coursedata['tags']) !== '') &&
($error = self::check_capability(tool_uploadcourse_course::DO_UPDATE, $coursedata, 'moodle/course:tag'))) {
return $error;
}
return null;
}
/**
* Check permission to create course.
*
* This checks capabilities:
* - to use tool_uploadcourse in the category where course will be created.
* - to create a course.
* - to force course language (if applicable).
* - to set course tags (if applicable).
*
* @param array $coursedata data to create a course with, always contains 'category'
* @return lang_string|null
*/
public static function check_permission_to_create(array $coursedata): ?lang_string {
if ($error = self::check_permission_to_use_uploadcourse_tool($coursedata['category'])) {
return $error;
}
$catcontext = context_coursecat::instance($coursedata['category']);
// Check user is allowed to create courses in this category.
if (!has_capability('moodle/course:create', $catcontext)) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:create'));
}
// If lang is specified, check the user is allowed to set that field.
if (!empty($coursedata['lang'])) {
if (!guess_if_creator_will_have_course_capability('moodle/course:setforcedlanguage', $catcontext)) {
return new lang_string('cannotforcelang', 'tool_uploadcourse');
}
}
// Check permission to change course visibility.
if (array_key_exists('visible', $coursedata) && !$coursedata['visible'] &&
!guess_if_creator_will_have_course_capability('moodle/course:visibility', $catcontext)) {
return new lang_string('nopermissions', 'error', get_capability_string('moodle/course:visibility'));
}
// If tags are specified and enabled check if user can updat them.
if (core_tag_tag::is_enabled('core', 'course') &&
(array_key_exists('tags', $coursedata) && strval($coursedata['tags']) !== '') &&
($error = self::check_capability(tool_uploadcourse_course::DO_CREATE, $coursedata, 'moodle/course:tag'))) {
return $error;
}
return null;
}
/**
* Check if the user is able to reset a course.
*
* Capability to use the tool and update the course is already checked earlier.
*
* @param array $coursedata data to update course with, always contains 'id'
* @return lang_string|null error string or null
*/
public static function check_permission_to_reset(array $coursedata): ?lang_string {
return self::check_capability(tool_uploadcourse_course::DO_UPDATE, $coursedata, 'moodle/course:reset');
}
/**
* Check if the user is able to restore the mbz into a course.
*
* This method does not need to check if the course can be updated/created, this is checked earlier.
*
* @param int $do one of tool_uploadcourse_course::DO_UPDATE or tool_uploadcourse_course::DO_ADD
* @param array $coursedata data to update/create course with, must contain either 'id' or 'category' respectively
* @return lang_string|null error string or null
*/
public static function check_permission_to_restore(int $do, array $coursedata): ?lang_string {
return self::check_capability($do, $coursedata, 'moodle/restore:restorecourse');
}
}
@@ -174,7 +174,7 @@ class tool_uploadcourse_processor {
/**
* Execute the process.
*
* @param object $tracker the output tracker to use.
* @param tool_uploadcourse_tracker $tracker the output tracker to use.
* @return void
*/
public function execute($tracker = null) {
@@ -324,7 +324,7 @@ class tool_uploadcourse_processor {
* This only returns passed data, along with the errors.
*
* @param integer $rows number of rows to preview.
* @param object $tracker the output tracker to use.
* @param tool_uploadcourse_tracker $tracker the output tracker to use.
* @return array of preview data.
*/
public function preview($rows = 10, $tracker = null) {
@@ -74,6 +74,9 @@ class tool_uploadcourse_step1_form extends tool_uploadcourse_base_form {
$mform->addElement('hidden', 'showpreview', 1);
$mform->setType('showpreview', PARAM_INT);
$mform->addElement('hidden', 'categoryid');
$mform->setType('categoryid', PARAM_INT);
$this->add_action_buttons(false, get_string('preview', 'tool_uploadcourse'));
}
}
@@ -82,7 +82,7 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
$mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse'));
$mform->setExpanded('defaultheader', true);
$displaylist = core_course_category::make_categories_list('moodle/course:create');
$displaylist = core_course_category::make_categories_list('tool/uploadcourse:use');
$mform->addElement('autocomplete', 'defaults[category]', get_string('coursecategory'), $displaylist);
$mform->addRule('defaults[category]', null, 'required', null, 'client');
$mform->addHelpButton('defaults[category]', 'coursecategory');
@@ -223,6 +223,9 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
$mform->addElement('hidden', 'previewrows');
$mform->setType('previewrows', PARAM_INT);
$mform->addElement('hidden', 'categoryid');
$mform->setType('categoryid', PARAM_INT);
$this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse'));
// Prepare custom fields data.
+36
View File
@@ -0,0 +1,36 @@
<?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/>.
/**
* Capability definitions for tool_uploadcourse.
*
* @package tool_uploadcourse
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
'tool/uploadcourse:use' => [
'riskbitmask' => RISK_SPAM,
'captype' => 'write',
'contextlevel' => CONTEXT_COURSECAT,
'archetypes' => [
'manager' => CAP_ALLOW,
],
],
];
+21 -4
View File
@@ -26,15 +26,31 @@ require(__DIR__ . '/../../../config.php');
require_once($CFG->libdir . '/adminlib.php');
require_once($CFG->libdir . '/csvlib.class.php');
admin_externalpage_setup('tooluploadcourse');
$importid = optional_param('importid', '', PARAM_INT);
$categoryid = optional_param('categoryid', 0, PARAM_INT);
$previewrows = optional_param('previewrows', 10, PARAM_INT);
$returnurl = new moodle_url('/admin/tool/uploadcourse/index.php');
if ($categoryid) {
// When categoryid is specified, setup the page for this category and check capability in its context.
require_login(null, false);
$category = core_course_category::get($categoryid);
$categoryname = isset($category) ? $category->get_formatted_name() : $SITE->fullname;
$context = context_coursecat::instance($categoryid);
require_capability('tool/uploadcourse:use', $context);
$PAGE->set_context($context);
$PAGE->set_url(new moodle_url('/admin/tool/uploadcourse/index.php', ['categoryid' => $categoryid]));
$PAGE->set_pagelayout('admin');
$PAGE->set_title("$categoryname: " . get_string('uploadcourses', 'tool_uploadcourse'));
$PAGE->set_heading($categoryname);
} else {
admin_externalpage_setup('tooluploadcourse');
}
if (empty($importid)) {
$mform1 = new tool_uploadcourse_step1_form();
$mform1->set_data(['categoryid' => $categoryid]);
if ($form1data = $mform1->get_data()) {
$importid = csv_import_reader::get_new_iid('uploadcourse');
$cir = new csv_import_reader($importid, 'uploadcourse');
@@ -58,7 +74,8 @@ if (empty($importid)) {
}
// Data to set in the form.
$data = array('importid' => $importid, 'previewrows' => $previewrows);
$categorydefaults = $categoryid ? ['category' => $categoryid] : [];
$data = ['importid' => $importid, 'previewrows' => $previewrows, 'categoryid' => $categoryid, 'defaults' => $categorydefaults];
if (!empty($form1data)) {
// Get options from the first form to pass it onto the second.
foreach ($form1data->options as $key => $value) {
@@ -117,7 +134,7 @@ if ($form2data = $mform2->is_cancelled()) {
// Weird but we still need to provide a value, setting the default step1_form one.
$options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW);
}
$processor = new tool_uploadcourse_processor($cir, $options, array());
$processor = new tool_uploadcourse_processor($cir, $options, $categorydefaults);
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse'));
$processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
@@ -67,6 +67,8 @@ $string['coursetemplatename'] = 'Restore from this course after upload';
$string['coursetemplatename_help'] = 'Enter an existing course shortname to use as a template for the creation of all courses.';
$string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist';
$string['courseupdated'] = 'Course updated';
$string['courseuploadnotallowed'] = 'No permission to upload courses in category: {$a}';
$string['courseuploadupdatenotallowed'] = "Course with this shortname exists and you don't have permission to use upload course tool to update it";
$string['createall'] = 'Create all, increment shortname if needed';
$string['createnew'] = 'Create new courses only, skip existing ones';
$string['createorupdate'] = 'Create new courses, or update existing ones';
@@ -128,6 +130,7 @@ $string['updatemodedoessettonothing'] = 'Update mode does not allow anything to
$string['updateonly'] = 'Only update existing courses';
$string['updatewithdataordefaults'] = 'Update with CSV data and defaults';
$string['updatewithdataonly'] = 'Update with CSV data only';
$string['uploadcourse:use'] = 'Use upload course tool';
$string['uploadcourses'] = 'Upload courses';
$string['uploadcourses_help'] = 'Courses may be uploaded via text file. The format of the file should be as follows:
+44
View File
@@ -0,0 +1,44 @@
<?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/>.
/**
* Plugin callbacks for tool_uploadcourse.
*
* @package tool_uploadcourse
* @copyright 2019 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Extends the navigation of the category admin menu with the upload courses link.
*
* @param navigation_node $navigation The navigation node to extend
* @param context $coursecategorycontext The context of the course category
*/
function tool_uploadcourse_extend_navigation_category_settings(navigation_node $navigation, context $coursecategorycontext): void {
if (has_capability('tool/uploadcourse:use', $coursecategorycontext)) {
$title = get_string('uploadcourses', 'tool_uploadcourse');
$path = new moodle_url('/admin/tool/uploadcourse/index.php', ['categoryid' => $coursecategorycontext->instanceid]);
$settingsnode = navigation_node::create(
$title,
$path,
navigation_node::TYPE_SETTING,
null,
null,
new pix_icon('i/course', ''));
$navigation->add_node($settingsnode);
}
}
+7 -4
View File
@@ -24,7 +24,10 @@
defined('MOODLE_INTERNAL') || die();
if ($hassiteconfig) {
$ADMIN->add('courses', new admin_externalpage('tooluploadcourse',
get_string('uploadcourses', 'tool_uploadcourse'), "$CFG->wwwroot/$CFG->admin/tool/uploadcourse/index.php"));
}
$ADMIN->add('courses',
new admin_externalpage('tooluploadcourse',
get_string('uploadcourses', 'tool_uploadcourse'),
"$CFG->wwwroot/$CFG->admin/tool/uploadcourse/index.php",
'tool/uploadcourse:use'
)
);
@@ -145,3 +145,39 @@ Feature: An admin can create courses using a CSV file
And I am on the "C2" "enrolment methods" page
And I should see "manualtest"
And I should not see "ltitest"
@javascript
Scenario: Manager can use upload course tool in course category
Given the following "users" exist:
| username | firstname | lastname | email |
| user1 | User | 1 | user1@example.com |
And the following "categories" exist:
| name | category | idnumber |
| Cat 1 | 0 | CAT1 |
| Cat 2 | 0 | CAT2 |
| Cat 3 | CAT1 | CAT3 |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| user1 | manager | Category | CAT1 |
When I log in as "user1"
And I am on course index
And I follow "Cat 1"
And I navigate to "Upload courses" in current page administration
And I upload "admin/tool/uploadcourse/tests/fixtures/courses_manager1.csv" file to "File" filemanager
And I click on "Preview" "button"
Then I should see "The course exists and update is not allowed" in the "C1" "table_row"
And I should see "No permission to upload courses in category: Cat 2" in the "C2" "table_row"
And I set the field "Course category" to "Cat 1 / Cat 3"
And I click on "Upload courses" "button"
And I should see "Course created"
And I should see "Courses total: 5"
And I should see "Courses created: 3"
And I should see "Courses errors: 2"
And I am on course index
And I follow "Cat 1"
And I should see "Course 4"
And I follow "Cat 3"
And I should see "Course 5"
# Course 3 did not have category specified in CSV file and it was uploaded to the current category.
And I should see "Course 3"
And I log out
@@ -87,3 +87,50 @@ Feature: An admin can update courses using a CSV file
And I should not see "Manual enrolments"
And I should see "Published course"
And I should not see "ltitest"
@javascript
Scenario: Manager can use upload course tool to update courses in course category
Given the following "users" exist:
| username | firstname | lastname | email |
| user1 | User | 1 | user1@example.com |
And the following "categories" exist:
| name | category | idnumber |
| Cat 1 | 0 | CAT1 |
| Cat 2 | 0 | CAT2 |
| Cat 3 | CAT1 | CAT3 |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C01 | CAT1 |
| Course 2 | C02 | CAT2 |
| Course 3 | C03 | CAT3 |
| Course 4 | C04 | CAT3 |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| user1 | manager | Category | CAT1 |
When I log in as "user1"
And I am on course index
And I follow "Cat 1"
And I navigate to "Upload courses" in current page administration
And I upload "admin/tool/uploadcourse/tests/fixtures/courses_manager2.csv" file to "File" filemanager
And I set the field "Upload mode" to "Only update existing courses"
And I set the field "Update mode" to "Update with CSV data only"
And I click on "Preview" "button"
# Course C01 is in "our" category but can not be moved to Cat 2 because current user can not manage it.
Then I should see "No permission to upload courses in category: Cat 2" in the "C01" "table_row"
# Course C02 can not be updated (no capability in "Cat 2" context).
And I should see "Course with this shortname exists and you don't have permission to use upload course tool to update it" in the "C02" "table_row"
# Course with short name "C05" does not exist.
And I should see "The course does not exist and creating course is not allowed" in the "C05" "table_row"
And I click on "Upload courses" "button"
And I should see "Course updated"
And I should see "Courses total: 5"
And I should see "Courses updated: 2"
And I should see "Courses errors: 3"
And I am on course index
And I follow "Cat 1"
# Course C04 was moved from Cat 3 to Cat 1.
And I should see "Course 4"
And I should see "Course 1"
And I follow "Cat 3"
And I should see "Course 3"
And I log out
+103 -26
View File
@@ -22,14 +22,54 @@ use tool_uploadcourse_course;
/**
* Course test case.
*
* @covers \tool_uploadcourse_course
* @package tool_uploadcourse
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
*/
class course_test extends \advanced_testcase {
public function test_proceed_without_prepare() {
/** @var \testing_data_generator $datagenerator */
protected $datagenerator;
/** @var \stdClass $user */
protected $user;
/**
* Initialise defaults for each testcase.
*
* @param int $roleid
* @throws \coding_exception
*/
protected function initialise_test(int $roleid = 1): void {
// Reset the database after test.
$this->resetAfterTest(true);
// Get a new data generator.
$this->datagenerator = $this->getDataGenerator();
// Create a user.
$this->prepare_user($roleid);
}
/**
* Create random user and assign default role Manager (roleid = 1).
*
* @param int $roleid
* @throws \coding_exception
*/
protected function prepare_user(int $roleid): void {
// Generate a random user.
$user = $this->datagenerator->create_user();
// Log the user in (set the $USER global variable).
$this->setUser($user);
// Assign a role to the current user.
$this->datagenerator->role_assign($roleid, $user->id, false);
}
public function test_proceed_without_prepare(): void {
$this->initialise_test();
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
$data = array();
@@ -39,7 +79,7 @@ class course_test extends \advanced_testcase {
}
public function test_proceed_when_prepare_failed() {
$this->resetAfterTest(true);
$this->initialise_test();
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
$data = array();
@@ -50,7 +90,7 @@ class course_test extends \advanced_testcase {
}
public function test_proceed_when_already_started() {
$this->resetAfterTest(true);
$this->initialise_test();
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
$data = array('shortname' => 'test', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
@@ -62,7 +102,7 @@ class course_test extends \advanced_testcase {
}
public function test_invalid_shortname() {
$this->resetAfterTest(true);
$this->initialise_test();
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
$data = array('shortname' => '<invalid>', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
@@ -88,7 +128,7 @@ class course_test extends \advanced_testcase {
}
public function test_invalid_fullname_too_long() {
$this->resetAfterTest();
$this->initialise_test();
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
@@ -103,7 +143,7 @@ class course_test extends \advanced_testcase {
}
public function test_invalid_visibility() {
$this->resetAfterTest(true);
$this->initialise_test();
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
$data = array('shortname' => 'test', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1, 'visible' => 2);
@@ -194,9 +234,48 @@ class course_test extends \advanced_testcase {
$this->assertArrayHasKey('invaliddownloadcontent', $upload->get_errors());
}
/**
* Test a role's capability to use the upload course tool.
*
* @covers \permissions::check_permission_to_use_uploadcourse_tool
*/
public function test_invalid_role(): void {
global $DB;
$rolesallowed = ['manager'];
$roles = get_all_roles();
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
foreach ($roles as $role) {
$this->initialise_test($role->id);
$data = ['shortname' => 'newcourse', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1];
$co = new tool_uploadcourse_course($mode, $updatemode, $data);
if (in_array($role->shortname, $rolesallowed)) {
$this->assertTrue($co->prepare());
$co->proceed();
$courseid = $DB->get_field('course', 'id', ['shortname' => 'newcourse'], MUST_EXIST);
$this->assertEquals(0, course_get_format($courseid)->get_course()->coursedisplay);
// Delete course for next assertion.
$importoptions = ['candelete' => true];
$data = ['shortname' => 'newcourse', 'delete' => 1];
$co = new tool_uploadcourse_course($mode, $updatemode, $data, [], $importoptions);
$this->assertTrue($co->prepare());
$co->proceed();
} else {
$this->assertFalse($co->prepare());
$this->assertArrayHasKey('courseuploadnotallowed', $co->get_errors());
}
}
}
public function test_create() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
// Existing course.
$c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1', 'summary' => 'Yay!'));
@@ -245,7 +324,7 @@ class course_test extends \advanced_testcase {
public function test_create_with_sections() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
$defaultnumsections = get_config('moodlecourse', 'numsections');
@@ -275,7 +354,7 @@ class course_test extends \advanced_testcase {
public function test_delete() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
@@ -320,7 +399,7 @@ class course_test extends \advanced_testcase {
public function test_update() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
$c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1'));
@@ -398,8 +477,7 @@ class course_test extends \advanced_testcase {
public function test_data_saved() {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser(); // To avoid warnings related to 'moodle/course:setforcedlanguage' capability check.
$this->initialise_test();
set_config('downloadcoursecontentallowed', 1);
@@ -617,8 +695,7 @@ class course_test extends \advanced_testcase {
public function test_default_data_saved() {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$this->initialise_test();
set_config('downloadcoursecontentallowed', 1);
@@ -742,7 +819,7 @@ class course_test extends \advanced_testcase {
public function test_rename() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
$c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1'));
$c2 = $this->getDataGenerator()->create_course(array('shortname' => 'c2'));
@@ -837,7 +914,7 @@ class course_test extends \advanced_testcase {
public function test_restore_course() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
$this->setAdminUser();
$c1 = $this->getDataGenerator()->create_course();
@@ -881,7 +958,7 @@ class course_test extends \advanced_testcase {
public function test_restore_file() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
$this->setAdminUser();
$c1 = $this->getDataGenerator()->create_course();
@@ -933,7 +1010,7 @@ class course_test extends \advanced_testcase {
*/
public function test_restore_file_settings() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
$this->setAdminUser();
// Set admin config setting so that activities are not restored by default.
@@ -956,7 +1033,7 @@ class course_test extends \advanced_testcase {
}
public function test_restore_invalid_file() {
$this->resetAfterTest();
$this->initialise_test();
// Restore from a non-existing file should not be allowed.
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
@@ -983,7 +1060,7 @@ class course_test extends \advanced_testcase {
}
public function test_restore_invalid_course() {
$this->resetAfterTest();
$this->initialise_test();
// Restore from an invalid file should not be allowed.
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
@@ -1000,7 +1077,7 @@ class course_test extends \advanced_testcase {
*/
public function test_reset() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
$c1 = $this->getDataGenerator()->create_course();
$c1ctx = \context_course::instance($c1->id);
@@ -1092,7 +1169,7 @@ class course_test extends \advanced_testcase {
public function test_create_bad_category() {
global $DB;
$this->resetAfterTest(true);
$this->initialise_test();
// Ensure fails when category cannot be resolved upon creation.
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
@@ -1146,7 +1223,7 @@ class course_test extends \advanced_testcase {
}
public function test_enrolment_data() {
$this->resetAfterTest(true);
$this->initialise_test();
// We need to set the current user as one with the capability to edit manual enrolment instances in the new course.
$this->setAdminUser();
@@ -1425,7 +1502,7 @@ class course_test extends \advanced_testcase {
}
public function test_idnumber_problems() {
$this->resetAfterTest(true);
$this->initialise_test();
$c1 = $this->getDataGenerator()->create_course(array('shortname' => 'sntaken', 'idnumber' => 'taken'));
$c2 = $this->getDataGenerator()->create_course();
@@ -1476,7 +1553,7 @@ class course_test extends \advanced_testcase {
}
public function test_generate_shortname() {
$this->resetAfterTest(true);
$this->initialise_test();
$c1 = $this->getDataGenerator()->create_course(array('shortname' => 'taken'));
@@ -1529,7 +1606,7 @@ class course_test extends \advanced_testcase {
public function test_mess_with_frontpage() {
global $SITE;
$this->resetAfterTest(true);
$this->initialise_test();
// Updating the front page.
$mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
@@ -0,0 +1,6 @@
shortname,fullname,summary,idnumber,category_idnumber
C1,Course 1,Summary 1,ID1,CAT2
C2,Course 2,Summary 2,ID2,CAT2
C3,Course 3,Summary 3,ID3,
C4,Course 4,Summary 4,ID4,CAT1
C5,Course 5,Summary 5,ID5,CAT3
1 shortname fullname summary idnumber category_idnumber
2 C1 Course 1 Summary 1 ID1 CAT2
3 C2 Course 2 Summary 2 ID2 CAT2
4 C3 Course 3 Summary 3 ID3
5 C4 Course 4 Summary 4 ID4 CAT1
6 C5 Course 5 Summary 5 ID5 CAT3
@@ -0,0 +1,6 @@
shortname,category_idnumber
C01,CAT2
C02,CAT2
C03,
C04,CAT1
C05,CAT3
1 shortname category_idnumber
2 C01 CAT2
3 C02 CAT2
4 C03
5 C04 CAT1
6 C05 CAT3
@@ -381,12 +381,15 @@ class helper_test extends \advanced_testcase {
$c1 = $this->getDataGenerator()->create_category(array('idnumber' => 'C1'));
$c2 = $this->getDataGenerator()->create_category(array('idnumber' => 'C2'));
$c3 = $this->getDataGenerator()->create_category(['idnumber' => 'C3', 'visible' => false]);
// Doubled for cache check.
$this->assertEquals($c1->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C1'));
$this->assertEquals($c1->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C1'));
$this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C2'));
$this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C2'));
$this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_idnumber('C3'));
$this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_idnumber('C3'));
$this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_idnumber('DoesNotExist'));
$this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_idnumber('DoesNotExist'));
}
@@ -436,8 +439,8 @@ class helper_test extends \advanced_testcase {
// Hidden parent.
$path = array('Cat 2', 'Cat 2.1', 'Cat 2.1.2');
$this->assertEquals($cat2_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
$this->assertEquals($cat2_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
$this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
$this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
// Does not exist.
$path = array('No cat 3', 'Cat 1.2');
@@ -35,6 +35,7 @@ class processor_test extends \advanced_testcase {
public function test_basic() {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$content = array(
"shortname,fullname,summary",
@@ -134,6 +135,7 @@ class processor_test extends \advanced_testcase {
public function test_shortname_template() {
global $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
$content = array(
"shortname,fullname,summary,idnumber",
+1 -1
View File
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2023100900; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2023112800; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2023100400; // Requires this Moodle version.
$plugin->component = 'tool_uploadcourse'; // Full name of the plugin (used for diagnostics).
+7 -3
View File
@@ -620,15 +620,19 @@ class enrol_cohort_plugin extends enrol_plugin {
* @return lang_string|null Error
*/
public function validate_plugin_data_context(array $enrolmentdata, ?int $courseid = null) : ?lang_string {
$error = null;
if (isset($enrolmentdata['customint1'])) {
$cohortid = $enrolmentdata['customint1'];
$coursecontext = \context_course::instance($courseid);
if (!cohort_get_cohort($cohortid, $coursecontext)) {
$error = new lang_string('contextcohortnotallowed', 'cohort', $enrolmentdata['cohortidnumber']);
return new lang_string('contextcohortnotallowed', 'cohort', $enrolmentdata['cohortidnumber']);
}
}
return $error;
$enrolmentdata += [
'customint1' => null,
'customint2' => null,
'roleid' => 0,
];
return parent::validate_plugin_data_context($enrolmentdata, $courseid);
}
/**
+5
View File
@@ -246,7 +246,11 @@ class lib_test extends \advanced_testcase {
$enrolmentdata = [
'customint1' => $cohort1->id,
'cohortidnumber' => $cohort1->idnumber,
'courseid' => $course->id,
'id' => null,
'status' => ENROL_INSTANCE_ENABLED,
];
$enrolmentdata = $cohortplugin->fill_enrol_custom_fields($enrolmentdata, $course->id);
$error = $cohortplugin->validate_plugin_data_context($enrolmentdata, $course->id);
$this->assertNull($error);
}
@@ -313,6 +317,7 @@ class lib_test extends \advanced_testcase {
*/
public function test_validate_enrol_plugin_data() {
$this->resetAfterTest();
$this->setAdminUser();
$cat = $this->getDataGenerator()->create_category();
$cat1 = $this->getDataGenerator()->create_category(['parent' => $cat->id]);
+10
View File
@@ -519,6 +519,16 @@ class enrol_guest_plugin extends enrol_plugin {
return $instance;
}
/**
* Fill custom fields data for a given enrolment plugin.
*
* @param array $enrolmentdata enrolment data.
* @param int $courseid Course ID.
* @return array Updated enrolment data with custom fields info.
*/
public function fill_enrol_custom_fields(array $enrolmentdata, int $courseid): array {
return $enrolmentdata + ['password' => ''];
}
}
/**
+14
View File
@@ -675,6 +675,20 @@ class enrol_manual_plugin extends enrol_plugin {
}
return $instance;
}
/**
* Fill custom fields data for a given enrolment plugin.
*
* @param array $enrolmentdata enrolment data.
* @param int $courseid Course ID.
* @return array Updated enrolment data with custom fields info.
*/
public function fill_enrol_custom_fields(array $enrolmentdata, int $courseid): array {
return $enrolmentdata + [
'expirynotify' => 0,
'expirythreshold' => 0,
];
}
}
/**
+4 -1
View File
@@ -468,7 +468,10 @@ class enrol_meta_plugin extends enrol_plugin {
} else if (isset($enrolmentdata['groupname'])) {
$enrolmentdata['customint2'] = groups_get_group_by_name($courseid, $enrolmentdata['groupname']);
}
return $enrolmentdata;
return $enrolmentdata + [
'customint1' => null,
'customint2' => null,
];
}
/**
+2 -2
View File
@@ -1108,13 +1108,13 @@ class plugin_test extends \advanced_testcase {
$enrolmentdata = $metaplugin->fill_enrol_custom_fields($enrolmentdata, $course1->id);
$this->assertArrayHasKey('customint1', $enrolmentdata);
$this->assertEquals($course2->id, $enrolmentdata['customint1']);
$this->assertArrayNotHasKey('customint2', $enrolmentdata);
$this->assertNull($enrolmentdata['customint2']);
$enrolmentdata['metacoursename'] = 'notexist';
$enrolmentdata = $metaplugin->fill_enrol_custom_fields($enrolmentdata, $course1->id);
$this->assertArrayHasKey('customint1', $enrolmentdata);
$this->assertFalse($enrolmentdata['customint1']);
$this->assertArrayNotHasKey('customint2', $enrolmentdata);
$this->assertNull($enrolmentdata['customint2']);
$enrolmentdata['metacoursename'] = $course2->shortname;
+10
View File
@@ -1110,6 +1110,16 @@ class enrol_self_plugin extends enrol_plugin {
return $instance;
}
/**
* Fill custom fields data for a given enrolment plugin.
*
* @param array $enrolmentdata enrolment data.
* @param int $courseid Course ID.
* @return array Updated enrolment data with custom fields info.
*/
public function fill_enrol_custom_fields(array $enrolmentdata, int $courseid): array {
return $enrolmentdata + ['password' => ''];
}
}
/**
+27 -1
View File
@@ -2707,6 +2707,9 @@ abstract class enrol_plugin {
/**
* Check if enrolment plugin is supported in csv course upload.
*
* If supported, plugins are also encouraged to override methods:
* {@see self::fill_enrol_custom_fields()}, {@see self::validate_plugin_data_context()}
*
* @return bool
*/
public function is_csv_upload_supported(): bool {
@@ -3485,7 +3488,10 @@ abstract class enrol_plugin {
/**
* Fill custom fields data for a given enrolment plugin.
*
* @param array $enrolmentdata enrolment data.
* For example: resolve linked entities from the idnumbers (cohort, role, group, etc.)
* Also fill the default values that are not specified.
*
* @param array $enrolmentdata enrolment data received in CSV file in tool_uploadcourse
* @param int $courseid Course ID.
* @return array Updated enrolment data with custom fields info.
*/
@@ -3514,11 +3520,31 @@ abstract class enrol_plugin {
/**
* Check if plugin custom data is allowed in relevant context.
*
* This is called from the tool_uploadcourse if the plugin supports instance creation in
* upload course ({@see self::is_csv_upload_supported()})
*
* The fallback is to call the edit_instance_validation() but it will be better if the plugins
* implement this method to return better error messages.
*
* @param array $enrolmentdata enrolment data to validate.
* @param int|null $courseid Course ID.
* @return lang_string|null Error
*/
public function validate_plugin_data_context(array $enrolmentdata, ?int $courseid = null) : ?lang_string {
if ($courseid) {
$enrolmentdata += ['courseid' => $courseid, 'id' => 0, 'status' => ENROL_INSTANCE_ENABLED];
$instance = (object)[
'id' => null,
'courseid' => $courseid,
'status' => $enrolmentdata['status'],
'type' => $this->get_name(),
];
$formerrors = $this->edit_instance_validation($enrolmentdata, [], $instance, context_course::instance($courseid));
if (!empty($formerrors)) {
$errors = array_map(fn($key) => "{$key}: {$formerrors[$key]}", array_keys($formerrors));
return new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse', $errors);
}
}
return null;
}