diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index a31bfda80e207..2d2552dbe4b3c 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -341,7 +341,7 @@ protected function define_structure() { 'visibleold', 'groupmode', 'groupingid', 'completion', 'completiongradeitemnumber', 'completionpassgrade', 'completionview', 'completionexpected', - 'availability', 'showdescription', 'downloadcontent')); + 'availability', 'showdescription', 'downloadcontent', 'lang')); $tags = new backup_nested_element('tags'); $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname')); diff --git a/backup/moodle2/restore_stepslib.php b/backup/moodle2/restore_stepslib.php index 3bcf1291f95b0..34a860b523f25 100644 --- a/backup/moodle2/restore_stepslib.php +++ b/backup/moodle2/restore_stepslib.php @@ -4491,6 +4491,10 @@ protected function process_module($data) { $data->availability = upgrade_group_members_only($data->groupingid, $data->availability); } + if (!has_capability('moodle/course:setforcedlanguage', context_course::instance($data->course))) { + unset($data->lang); + } + // course_module record ready, insert it $newitemid = $DB->insert_record('course_modules', $data); // save mapping diff --git a/course/classes/external/helper_for_get_mods_by_courses.php b/course/classes/external/helper_for_get_mods_by_courses.php index 44d497d64f250..8a1a33fbaea15 100644 --- a/course/classes/external/helper_for_get_mods_by_courses.php +++ b/course/classes/external/helper_for_get_mods_by_courses.php @@ -57,6 +57,7 @@ public static function standard_coursemodule_element_values(\stdClass $modinstan $moddetails['coursemodule'] = $modinstance->coursemodule; $moddetails['course'] = $modinstance->course; $moddetails['name'] = $modinstance->name; + $moddetails['lang'] = clean_param($modinstance->lang, PARAM_LANG); if (!$capabilityforintro || has_capability($capabilityforintro, $context)) { $moddetails['intro'] = $modinstance->intro; $moddetails['introformat'] = $modinstance->introformat; @@ -130,6 +131,7 @@ public static function standard_coursemodule_elements_returns(bool $introoptiona 'visible' => new external_value(PARAM_BOOL, 'Visible', VALUE_OPTIONAL), 'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL), 'groupingid' => new external_value(PARAM_INT, 'Group id', VALUE_OPTIONAL), + 'lang' => new external_value(PARAM_SAFEDIR, 'Forced activity language', VALUE_OPTIONAL), ]; } } diff --git a/course/edit_form.php b/course/edit_form.php index 50677e56a530d..0c6d9028ff1c8 100644 --- a/course/edit_form.php +++ b/course/edit_form.php @@ -251,11 +251,12 @@ function definition() { $mform->addElement('select', 'theme', get_string('forcetheme'), $themes); } - $languages=array(); - $languages[''] = get_string('forceno'); - $languages += get_string_manager()->get_list_of_translations(); if ((empty($course->id) && guess_if_creator_will_have_course_capability('moodle/course:setforcedlanguage', $categorycontext)) || (!empty($course->id) && has_capability('moodle/course:setforcedlanguage', $coursecontext))) { + + $languages = ['' => get_string('forceno')]; + $languages += get_string_manager()->get_list_of_translations(); + $mform->addElement('select', 'lang', get_string('forcelanguage'), $languages); $mform->setDefault('lang', $courseconfig->lang); } diff --git a/course/modlib.php b/course/modlib.php index 5ec9c375641d1..ee5490cba97e1 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -70,6 +70,11 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) { if (isset($moduleinfo->downloadcontent)) { $newcm->downloadcontent = $moduleinfo->downloadcontent; } + if (has_capability('moodle/course:setforcedlanguage', context_course::instance($course->id))) { + $newcm->lang = $moduleinfo->lang ?? null; + } else { + $newcm->lang = null; + } $newcm->groupmode = $moduleinfo->groupmode; $newcm->groupingid = $moduleinfo->groupingid; $completion = new completion_info($course); @@ -552,6 +557,13 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) { $moduleinfo->course = $course->id; $moduleinfo = set_moduleinfo_defaults($moduleinfo); + $modcontext = context_module::instance($moduleinfo->coursemodule); + if (has_capability('moodle/course:setforcedlanguage', $modcontext)) { + $cm->lang = $moduleinfo->lang ?? null; + } else { + unset($cm->lang); + } + if (!empty($course->groupmodeforce) or !isset($moduleinfo->groupmode)) { $moduleinfo->groupmode = $cm->groupmode; // Keep original. } @@ -611,8 +623,6 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) { $DB->update_record('course_modules', $cm); - $modcontext = context_module::instance($moduleinfo->coursemodule); - // Update embedded links and save files. if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) { $moduleinfo->intro = file_save_draft_area_files($moduleinfo->introeditor['itemid'], $modcontext->id, @@ -745,6 +755,7 @@ function get_moduleinfo_data($cm, $course) { $data->completiongradeitemnumber = $cm->completiongradeitemnumber; $data->showdescription = $cm->showdescription; $data->downloadcontent = $cm->downloadcontent; + $data->lang = $cm->lang; $data->tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $cm->id); if (!empty($CFG->enableavailability)) { $data->availabilityconditionsjson = $cm->availability; diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index ada5c5e81beec..2dae3216d9c2e 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -625,11 +625,8 @@ protected function standard_coursemodule_elements() { $mform->addElement('modvisible', 'visible', get_string($modvisiblelabel), null, array('allowstealth' => $allowstealth, 'sectionvisible' => $section->visible, 'cm' => $this->_cm)); $mform->addHelpButton('visible', $modvisiblelabel); - if (!empty($this->_cm)) { - $context = context_module::instance($this->_cm->id); - if (!has_capability('moodle/course:activityvisibility', $context)) { - $mform->hardFreeze('visible'); - } + if (!empty($this->_cm) && !has_capability('moodle/course:activityvisibility', $this->get_context())) { + $mform->hardFreeze('visible'); } if ($this->_features->idnumber) { @@ -638,6 +635,13 @@ protected function standard_coursemodule_elements() { $mform->addHelpButton('cmidnumber', 'idnumbermod'); } + if (has_capability('moodle/course:setforcedlanguage', $this->get_context())) { + $languages = ['' => get_string('forceno')]; + $languages += get_string_manager()->get_list_of_translations(); + + $mform->addElement('select', 'lang', get_string('forcelanguage'), $languages); + } + if ($CFG->downloadcoursecontentallowed) { $choices = [ DOWNLOAD_COURSE_CONTENT_DISABLED => get_string('no'), @@ -900,9 +904,8 @@ protected function add_rating_settings($mform, int $itemnumber) { $rolenamestring = null; if ($isupdate) { - $context = context_module::instance($this->_cm->id); $capabilities = ['moodle/rating:rate', "mod/{$this->_cm->modname}:rate"]; - $rolenames = get_role_names_with_caps_in_context($context, $capabilities); + $rolenames = get_role_names_with_caps_in_context($this->get_context(), $capabilities); $rolenamestring = implode(', ', $rolenames); } else { $rolenamestring = get_string('capabilitychecknotavailable', 'rating'); diff --git a/course/tests/backup_restore_activity_test.php b/course/tests/backup_restore_activity_test.php new file mode 100644 index 0000000000000..0fc1d9ebbb255 --- /dev/null +++ b/course/tests/backup_restore_activity_test.php @@ -0,0 +1,137 @@ +. + +namespace core_course; +use backup; + +/** + * Restore date tests. + * + * @package core_course + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \backup_module_structure_step + * @covers \restore_module_structure_step + */ +class backup_restore_activity_test extends \advanced_testcase { + + /** + * Test that duplicating a page preserves the lang setting. + */ + public function test_duplicating_page_preserves_lang() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Make a test course. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + + // Create a page with forced language set. + $page = $generator->create_module('page', ['course' => $course->id, 'lang' => 'en']); + + // Duplicate the page. + $newpagecm = duplicate_module($course, get_fast_modinfo($course)->get_cm($page->cmid)); + + // Verify the settings of the duplicated activity. + $this->assertEquals('en', $newpagecm->lang); + } + + public function test_activity_forced_lang_not_restored_without_capability() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + // Make a test course. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + + // Create a page with forced language set. + $generator->create_module('page', ['course' => $course->id, 'lang' => 'en']); + + // Backup the course. + $backupid = $this->backup_course($course); + + // Create a manger user without 'moodle/course:setforcedlanguage' to do the restore. + $manager = $generator->create_user(); + $generator->role_assign('manager', $manager->id); + role_change_permission($DB->get_field('role', 'id', ['shortname' => 'manager'], MUST_EXIST), + \context_system::instance(), 'moodle/course:setforcedlanguage', CAP_INHERIT); + $this->setUser($manager); + + // Restore the course. + $newcourseid = $this->restore_course($backupid); + + // Verify the settings of the duplicated activity. + $newmodinfo = get_fast_modinfo($newcourseid); + $newcms = $newmodinfo->instances['page']; + $newpagecm = reset($newcms); + $this->assertNull($newpagecm->lang); + } + + /** + * Makes a backup of the course. + * + * @param \stdClass $course The course object. + * @return string Unique identifier for this backup. + */ + protected function backup_course(\stdClass $course): string { + global $CFG, $USER; + + // Turn off file logging, otherwise it can't delete the file (Windows). + $CFG->backup_file_logger_level = backup::LOG_NONE; + + // Do backup with default settings. MODE_IMPORT means it will just + // create the directory and not zip it. + $bc = new \backup_controller(backup::TYPE_1COURSE, $course->id, + backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, + $USER->id); + $backupid = $bc->get_backupid(); + $bc->execute_plan(); + $bc->destroy(); + + return $backupid; + } + + /** + * Restores a backup that has been made earlier. + * + * @param string $backupid The unique identifier of the backup. + * @return int The new course id. + */ + protected function restore_course(string $backupid): int { + global $CFG, $DB, $USER; + + // Turn off file logging, otherwise it can't delete the file (Windows). + $CFG->backup_file_logger_level = backup::LOG_NONE; + + $defaultcategoryid = $DB->get_field('course_categories', 'id', + ['parent' => 0], IGNORE_MULTIPLE); + + // Do restore to new course with default settings. + $newcourseid = \restore_dbops::create_new_course('Restored course', 'R1', $defaultcategoryid); + $rc = new \restore_controller($backupid, $newcourseid, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, + backup::TARGET_NEW_COURSE); + + $precheck = $rc->execute_precheck(); + $this->assertTrue($precheck); + + $rc->execute_plan(); + $rc->destroy(); + + return $newcourseid; + } +} diff --git a/course/tests/behat/add_activities.feature b/course/tests/behat/add_activities.feature index 71e4db9212032..e979323a9735b 100644 --- a/course/tests/behat/add_activities.feature +++ b/course/tests/behat/add_activities.feature @@ -5,36 +5,30 @@ Feature: Add activities to courses I need to add activites to a course Background: - Given the following "users" exist: - | username | firstname | lastname | email | - | student1 | Student | 1 | student1@example.com | - | student2 | Student | 2 | student2@example.com | - And the following "courses" exist: + Given the following "courses" exist: | fullname | shortname | format | - | Course 1 | Course 1 | topics | - And the following "course enrolments" exist: - | user | course | role | - | student1 | Course 1 | student | - | student2 | Course 1 | student | + | Course 1 | Course 1 | topics | @javascript Scenario: Add an activity to a course Given I am on the "Course 1" Course page logged in as admin And I am on "Course 1" course homepage with editing mode on When I add a "Database" to section "3" and I fill the form with: - | Name | Test name | - | Description | Test database description | - | ID number | TESTNAME | - | Allow comments on entries | Yes | + | Name | Test name | + | Description | Test database description | + | ID number | TESTNAME | + | Allow comments on entries | Yes | + | Force language | English | And I turn editing mode off Then I should not see "Adding a new" And I turn editing mode on And I open "Test name" actions menu And I click on "Edit settings" "link" in the "Test name" activity - And I expand all fieldsets - And the field "Name" matches value "Test name" - And the field "ID number" matches value "TESTNAME" - And the field "Allow comments on entries" matches value "Yes" + And the following fields match these values: + | Name | Test name | + | ID number | TESTNAME | + | Allow comments on entries | Yes | + | Force language | English ‎(en)‎ | @javascript Scenario: Add an activity supplying only the name diff --git a/course/tests/modlib_test.php b/course/tests/modlib_test.php index 6313bc2d6ba14..112bdd935082a 100644 --- a/course/tests/modlib_test.php +++ b/course/tests/modlib_test.php @@ -120,6 +120,7 @@ public function test_get_moduleinfo_data() { $expecteddata->showdescription = $assigncm->showdescription; $expecteddata->downloadcontent = $assigncm->downloadcontent; $expecteddata->tags = \core_tag_tag::get_item_tags_array('core', 'course_modules', $assigncm->id); + $expecteddata->lang = null; $expecteddata->availabilityconditionsjson = null; $expecteddata->advancedgradingmethod_submissions = null; if ($items = \grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => 'assign', diff --git a/lang/en/role.php b/lang/en/role.php index 486cd50352315..994768db8fcbc 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -178,7 +178,7 @@ $string['course:downloadcoursecontent'] = 'Download course content'; $string['course:enrolconfig'] = 'Configure enrol instances in courses'; $string['course:enrolreview'] = 'Review course enrolments'; -$string['course:setforcedlanguage'] = 'Force course language'; +$string['course:setforcedlanguage'] = 'Force course or activity language'; $string['course:ignoreavailabilityrestrictions'] = 'Ignore availability restrictions'; $string['course:ignorefilesizelimits'] = 'Use files larger than any file size restrictions'; $string['course:isincompletionreports'] = 'Be shown on completion reports'; diff --git a/lib/classes/string_manager_standard.php b/lib/classes/string_manager_standard.php index fc1c3c44f824e..78f5c940f7b6e 100644 --- a/lib/classes/string_manager_standard.php +++ b/lib/classes/string_manager_standard.php @@ -526,7 +526,7 @@ public function get_list_of_translations($returnall = false) { $cachekey = 'list_'.$this->get_key_suffix(); $cachedlist = $this->menucache->get($cachekey); if ($cachedlist !== false) { - // The cache content is invalid. + // The cache content is valid. if ($returnall or empty($this->translist)) { return $cachedlist; } diff --git a/lib/datalib.php b/lib/datalib.php index aded6e4152a99..dba5309d5435b 100644 --- a/lib/datalib.php +++ b/lib/datalib.php @@ -1352,7 +1352,7 @@ function get_coursemodules_in_course($modulename, $courseid, $extrafields='') { * in the course. Returns an empty array on any errors. * * The returned objects includle the columns cw.section, cm.visible, - * cm.groupmode, and cm.groupingid, and are indexed by cm.id. + * cm.groupmode, cm.groupingid and cm.lang and are indexed by cm.id. * * @global object * @global object @@ -1380,7 +1380,7 @@ function get_all_instances_in_courses($modulename, $courses, $userid=NULL, $incl $params['modulename'] = $modulename; if (!$rawmods = $DB->get_records_sql("SELECT cm.id AS coursemodule, m.*, cw.section, cm.visible AS visible, - cm.groupmode, cm.groupingid + cm.groupmode, cm.groupingid, cm.lang FROM {course_modules} cm, {course_sections} cw, {modules} md, {".$modulename."} m WHERE cm.course $coursessql AND diff --git a/lib/db/access.php b/lib/db/access.php index 16d372aac3591..ef1e11b74f23c 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -1094,6 +1094,7 @@ 'clonepermissionsfrom' => 'moodle/course:update' ), + // Ability to set a forced language for a course or activity. 'moodle/course:setforcedlanguage' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, diff --git a/lib/db/install.xml b/lib/db/install.xml index a070c9d7ef66e..39c44669cf3c5 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -93,7 +93,7 @@ - + @@ -311,6 +311,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 6d3f0da2c05eb..afa05ac6cc22d 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2888,5 +2888,20 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2022072900.00); } + if ($oldversion < 2022081200.01) { + + // Define field lang to be added to course_modules. + $table = new xmldb_table('course_modules'); + $field = new xmldb_field('lang', XMLDB_TYPE_CHAR, '30', null, null, null, null, 'downloadcontent'); + + // Conditionally launch add field lang. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2022081200.01); + } + return true; } diff --git a/lib/modinfolib.php b/lib/modinfolib.php index ae07d8a13e77e..0e2b5dd5d3542 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -865,6 +865,7 @@ public static function get_array_of_activities(stdClass $course, bool $usecache $mods[$cmid]->availability = $rawmods[$cmid]->availability; $mods[$cmid]->deletioninprogress = $rawmods[$cmid]->deletioninprogress; $mods[$cmid]->downloadcontent = $rawmods[$cmid]->downloadcontent; + $mods[$cmid]->lang = $rawmods[$cmid]->lang; $modname = $mods[$cmid]->mod; $functionname = $modname . "_get_coursemodule_info"; @@ -1110,6 +1111,7 @@ public static function purge_course_cache(int $courseid): void { * @property-read string $afterediticons Extra HTML code to display after editing icons (e.g. more icons) - calculated on request * @property-read bool $deletioninprogress True if this course module is scheduled for deletion, false otherwise. * @property-read bool $downloadcontent True if content download is enabled for this course module, false otherwise. + * @property-read bool $lang the forced language for this activity (language pack name). Null means not forced. */ class cm_info implements IteratorAggregate { /** @@ -1433,12 +1435,17 @@ class cm_info implements IteratorAggregate { */ private $downloadcontent; + /** + * @var string|null the forced language for this activity (language pack name). Null means not forced. + */ + private $lang; + /** * List of class read-only properties and their getter methods. * Used by magic functions __get(), __isset(), __empty() * @var array */ - private static $standardproperties = array( + private static $standardproperties = [ 'url' => 'get_url', 'content' => 'get_content', 'extraclasses' => 'get_extra_classes', @@ -1488,8 +1495,9 @@ class cm_info implements IteratorAggregate { 'visibleoncoursepage' => false, 'visibleold' => false, 'deletioninprogress' => false, - 'downloadcontent' => false - ); + 'downloadcontent' => false, + 'lang' => false, + ]; /** * List of methods with no arguments that were public prior to Moodle 2.6. @@ -1948,7 +1956,7 @@ public function get_course_module_record($additionalfields = false) { static $cmfields = array('id', 'course', 'module', 'instance', 'section', 'idnumber', 'added', 'score', 'indent', 'visible', 'visibleoncoursepage', 'visibleold', 'groupmode', 'groupingid', 'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected', 'completionpassgrade', - 'showdescription', 'availability', 'deletioninprogress', 'downloadcontent'); + 'showdescription', 'availability', 'deletioninprogress', 'downloadcontent', 'lang'); foreach ($cmfields as $key) { $cmrecord->$key = $this->$key; @@ -2175,6 +2183,7 @@ public function __construct(course_modinfo $modinfo, $notused1, $mod, $notused2) $this->visibleold = isset($mod->visibleold) ? $mod->visibleold : 0; $this->deletioninprogress = isset($mod->deletioninprogress) ? $mod->deletioninprogress : 0; $this->downloadcontent = $mod->downloadcontent ?? null; + $this->lang = $mod->lang ?? null; // Note: it saves effort and database space to always include the // availability and completion fields, even if availability or completion diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 1ad3d5b881ab1..d555ca6f7a87f 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -7080,7 +7080,7 @@ function clean_filename($string) { * @return string */ function current_language() { - global $CFG, $USER, $SESSION, $COURSE; + global $CFG, $PAGE, $SESSION, $USER; if (!empty($SESSION->forcelang)) { // Allows overriding course-forced language (useful for admins to check @@ -7089,9 +7089,13 @@ function current_language() { // specific language (see force_current_language()). $return = $SESSION->forcelang; - } else if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->lang)) { + } else if (!empty($PAGE->cm->lang)) { + // Activity language, if set. + $return = $PAGE->cm->lang; + + } else if (!empty($PAGE->course->id) && $PAGE->course->id != SITEID && !empty($PAGE->course->lang)) { // Course language can override all other settings for this page. - $return = $COURSE->lang; + $return = $PAGE->course->lang; } else if (!empty($SESSION->lang)) { // Session language can override other settings. @@ -7144,7 +7148,7 @@ function force_current_language($language) { global $SESSION; $sessionforcelang = isset($SESSION->forcelang) ? $SESSION->forcelang : ''; if ($language !== $sessionforcelang) { - // Seting forcelang to null or an empty string disables it's effect. + // Setting forcelang to null or an empty string disables its effect. if (empty($language) || get_string_manager()->translation_exists($language, false)) { $SESSION->forcelang = $language; moodle_setlocale(); diff --git a/lib/testing/generator/data_generator.php b/lib/testing/generator/data_generator.php index 73ee0cb104756..1863c7f19dc35 100644 --- a/lib/testing/generator/data_generator.php +++ b/lib/testing/generator/data_generator.php @@ -949,12 +949,13 @@ public function enrol_user($userid, $courseid, $roleidorshortname = null, $enrol /** * Assigns the specified role to a user in the context. * - * @param int $roleid + * @param int|string $role either an int role id or a string role shortname. * @param int $userid * @param int $contextid Defaults to the system context * @return int new/existing id of the assignment */ - public function role_assign($roleid, $userid, $contextid = false) { + public function role_assign($role, $userid, $contextid = false) { + global $DB; // Default to the system context. if (!$contextid) { @@ -962,15 +963,18 @@ public function role_assign($roleid, $userid, $contextid = false) { $contextid = $context->id; } - if (empty($roleid)) { + if (empty($role)) { throw new coding_exception('roleid must be present in testing_data_generator::role_assign() arguments'); } + if (!is_number($role)) { + $role = $DB->get_field('role', 'id', ['shortname' => $role], MUST_EXIST); + } if (empty($userid)) { throw new coding_exception('userid must be present in testing_data_generator::role_assign() arguments'); } - return role_assign($roleid, $userid, $contextid); + return role_assign($role, $userid, $contextid); } /** diff --git a/lib/testing/generator/module_generator.php b/lib/testing/generator/module_generator.php index bfb543e0af4e4..bb4ef49f11bb4 100644 --- a/lib/testing/generator/module_generator.php +++ b/lib/testing/generator/module_generator.php @@ -268,8 +268,13 @@ public function create_instance($record = null, array $options = null) { debugging('Did you forget to enable completion tracking for the course before generating module with completion tracking?', DEBUG_DEVELOPER); } + if (!empty($record->lang) && !has_capability('moodle/course:setforcedlanguage', context_course::instance($course->id))) { + throw new coding_exception('Attempt to generate an activity when the current user does not have ' . + 'permission moodle/course:setforcedlanguage. This does not work.'); + } + // Add the module to the course. - $moduleinfo = add_moduleinfo($record, $course, $mform = null); + $moduleinfo = add_moduleinfo($record, $course); // Prepare object to return with additional field cmid. $instance = $DB->get_record($this->get_modulename(), array('id' => $moduleinfo->instance), '*', MUST_EXIST); diff --git a/lib/tests/moodlelib_current_language_test.php b/lib/tests/moodlelib_current_language_test.php new file mode 100644 index 0000000000000..2b6c6cf273ede --- /dev/null +++ b/lib/tests/moodlelib_current_language_test.php @@ -0,0 +1,196 @@ +. + +/** + * Unit tests for current_language() in moodlelib.php. + * + * @package core + * @category test + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core; + +use moodle_page; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Unit tests for current_language() in moodlelib.php. + * + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers ::current_language + */ +class moodlelib_current_language_test extends \advanced_testcase { + + public function test_current_language_site_default(): void { + $this->resetAfterTest(); + testable_string_manager_for_current_language_tests::set_fake_list_of_installed_languages( + ['en' => 'English', 'en_ar' => 'English (pirate)']); + + set_config('lang', 'en_ar'); + + $this->assertEquals('en_ar', current_language()); + + testable_string_manager_for_current_language_tests::reset_installed_languages_override(); + } + + public function test_current_language_user_pref(): void { + $this->resetAfterTest(); + testable_string_manager_for_current_language_tests::set_fake_list_of_installed_languages( + ['en' => 'English', 'en_ar' => 'English (pirate)', 'fr' => 'French']); + + set_config('lang', 'en_ar'); + $this->setUser($this->getDataGenerator()->create_user(['lang' => 'fr'])); + + $this->assertEquals('fr', current_language()); + + testable_string_manager_for_current_language_tests::reset_installed_languages_override(); + } + + public function test_current_language_forced(): void { + $this->resetAfterTest(); + testable_string_manager_for_current_language_tests::set_fake_list_of_installed_languages( + ['en' => 'English', 'en_ar' => 'English (pirate)', 'fr' => 'French', 'de' => 'German']); + + set_config('lang', 'en_ar'); + $this->setUser($this->getDataGenerator()->create_user(['lang' => 'fr'])); + force_current_language('en'); + + $this->assertEquals('en', current_language()); + } + + public function test_current_language_course_setting(): void { + global $PAGE; + $this->resetAfterTest(); + testable_string_manager_for_current_language_tests::set_fake_list_of_installed_languages( + ['en' => 'English', 'en_ar' => 'English (pirate)', 'fr' => 'French']); + + set_config('lang', 'en_ar'); + $this->setUser($this->getDataGenerator()->create_user(['lang' => 'fr'])); + $PAGE = new moodle_page(); + $PAGE->set_course($this->getDataGenerator()->create_course(['lang' => 'de'])); + + $this->assertEquals('de', current_language()); + + testable_string_manager_for_current_language_tests::reset_installed_languages_override(); + } + + public function test_current_language_in_course_no_lang_set(): void { + global $PAGE; + $this->resetAfterTest(); + testable_string_manager_for_current_language_tests::set_fake_list_of_installed_languages( + ['en' => 'English', 'en_ar' => 'English (pirate)', 'fr' => 'French']); + + set_config('lang', 'en_ar'); + $PAGE = new moodle_page(); + $PAGE->set_course($this->getDataGenerator()->create_course()); + + $this->assertEquals('en_ar', current_language()); + + testable_string_manager_for_current_language_tests::reset_installed_languages_override(); + } + + public function test_current_language_activity_setting(): void { + global $PAGE; + $this->resetAfterTest(); + testable_string_manager_for_current_language_tests::set_fake_list_of_installed_languages( + ['en' => 'English', 'en_ar' => 'English (pirate)', 'fr' => 'French']); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(['lang' => 'de']); + $pageactivity = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'lang' => 'en']); + $cm = get_fast_modinfo($course)->get_cm($pageactivity->cmid); + + set_config('lang', 'en_ar'); + $this->setUser($this->getDataGenerator()->create_user(['lang' => 'fr'])); + $PAGE = new moodle_page(); + $PAGE->set_cm($cm, $course, $pageactivity); + + $this->assertEquals('en', current_language()); + + testable_string_manager_for_current_language_tests::reset_installed_languages_override(); + } + + public function test_current_language_activity_setting_not_set(): void { + global $PAGE; + $this->resetAfterTest(); + testable_string_manager_for_current_language_tests::set_fake_list_of_installed_languages( + ['en' => 'English', 'en_ar' => 'English (pirate)', 'fr' => 'French']); + + $this->setAdminUser(); + $course = $this->getDataGenerator()->create_course(['lang' => 'de']); + $pageactivity = $this->getDataGenerator()->create_module('page', ['course' => $course->id]); + $cm = get_fast_modinfo($course)->get_cm($pageactivity->cmid); + + set_config('lang', 'en_ar'); + $this->setUser($this->getDataGenerator()->create_user(['lang' => 'fr'])); + $PAGE = new moodle_page(); + $PAGE->set_cm($cm, $course, $pageactivity); + + $this->assertEquals('de', current_language()); + + testable_string_manager_for_current_language_tests::reset_installed_languages_override(); + } +} + + +/** + * Test helper class for test which need Moodle to think there are other languages installed. + * + * @copyright 2022 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class testable_string_manager_for_current_language_tests extends \core_string_manager_standard { + + /** @var array $installedlanguages list of languages which we want to pretend are installed. */ + protected $installedlanguages; + + /** + * Start pretending that the list of installed languages is other than what it is. + * + * You need to pass in an array like ['en' => 'English', 'fr' => 'French']. + * + * @param array $installedlanguages the list of languages to assume are installed. + */ + public static function set_fake_list_of_installed_languages(array $installedlanguages): void { + global $CFG; + + // Re-create the custom string-manager instance using this class, and force the thing we are overriding. + $oldsetting = $CFG->config_php_settings['customstringmanager'] ?? null; + $CFG->config_php_settings['customstringmanager'] = self::class; + get_string_manager(true)->installedlanguages = $installedlanguages; + + // Reset the setting we overrode. + unset($CFG->config_php_settings['customstringmanager']); + if ($oldsetting) { + $CFG->config_php_settings['customstringmanager'] = $oldsetting; + } + } + + /** + * Must be called at the end of any test which called set_fake_list_of_installed_languages to reset things. + */ + public static function reset_installed_languages_override(): void { + get_string_manager(true); + } + + public function get_list_of_translations($returnall = false) { + return $this->installedlanguages; + } +} diff --git a/mod/book/tests/externallib_test.php b/mod/book/tests/externallib_test.php index aec10eee9a604..9508f647e57c6 100644 --- a/mod/book/tests/externallib_test.php +++ b/mod/book/tests/externallib_test.php @@ -160,7 +160,7 @@ public function test_get_books_by_courses() { $this->assertCount(1, $books['books']); $this->assertEquals('First Book', $books['books'][0]['name']); // We see 10 fields. - $this->assertCount(10, $books['books'][0]); + $this->assertCount(11, $books['books'][0]); // As Student you cannot see some book properties like 'section'. $this->assertFalse(isset($books['books'][0]['section'])); @@ -182,7 +182,7 @@ public function test_get_books_by_courses() { $this->assertCount(1, $books['books']); $this->assertEquals('Second Book', $books['books'][0]['name']); // We see 17 fields. - $this->assertCount(17, $books['books'][0]); + $this->assertCount(18, $books['books'][0]); // As an Admin you can see some book properties like 'section'. $this->assertEquals(0, $books['books'][0]['section']); diff --git a/mod/chat/tests/externallib_test.php b/mod/chat/tests/externallib_test.php index 9d7db19807676..3219c8da2cd13 100644 --- a/mod/chat/tests/externallib_test.php +++ b/mod/chat/tests/externallib_test.php @@ -218,7 +218,7 @@ public function test_view_chat() { * Test get_chats_by_courses */ public function test_get_chats_by_courses() { - global $DB, $USER, $CFG; + global $DB, $CFG; $this->resetAfterTest(true); $this->setAdminUser(); @@ -250,7 +250,7 @@ public function test_get_chats_by_courses() { $this->assertCount(1, $chats['chats']); $this->assertEquals('First Chat', $chats['chats'][0]['name']); // We see 12 fields. - $this->assertCount(12, $chats['chats'][0]); + $this->assertCount(13, $chats['chats'][0]); // As Student you cannot see some chat properties like 'section'. $this->assertFalse(isset($chats['chats'][0]['section'])); @@ -273,7 +273,7 @@ public function test_get_chats_by_courses() { $this->assertEquals('Second Chat', $chats['chats'][0]['name']); $this->assertEquals('header_js', $chats['chats'][0]['chatmethod']); // We see 17 fields. - $this->assertCount(17, $chats['chats'][0]); + $this->assertCount(18, $chats['chats'][0]); // As an Admin you can see some chat properties like 'section'. $this->assertEquals(0, $chats['chats'][0]['section']); diff --git a/mod/data/classes/external/database_summary_exporter.php b/mod/data/classes/external/database_summary_exporter.php index 79e522d5b90f6..ec85060293419 100644 --- a/mod/data/classes/external/database_summary_exporter.php +++ b/mod/data/classes/external/database_summary_exporter.php @@ -58,6 +58,11 @@ protected static function define_properties() { 'type' => PARAM_INT, 'default' => FORMAT_MOODLE ), + 'lang' => array( + 'type' => PARAM_LANG, + 'description' => 'Forced activity language', + 'null' => NULL_ALLOWED, + ), 'comments' => array( 'type' => PARAM_BOOL, 'description' => 'comments enabled', diff --git a/mod/data/tests/externallib_test.php b/mod/data/tests/externallib_test.php index 98b43bdad261f..671c3b9bba239 100644 --- a/mod/data/tests/externallib_test.php +++ b/mod/data/tests/externallib_test.php @@ -193,15 +193,19 @@ public function test_mod_data_get_databases_by_courses() { // First for the student user. $expectedfields = array('id', 'coursemodule', 'course', 'name', 'comments', 'timeavailablefrom', 'timeavailableto', 'timeviewfrom', 'timeviewto', 'requiredentries', 'requiredentriestoview', - 'intro', 'introformat', 'introfiles', 'maxentries', 'rssarticles', 'singletemplate', 'listtemplate', + 'intro', 'introformat', 'introfiles', 'lang', + 'maxentries', 'rssarticles', 'singletemplate', 'listtemplate', 'listtemplateheader', 'listtemplatefooter', 'addtemplate', 'rsstemplate', 'rsstitletemplate', - 'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', 'defaultsort', 'defaultsortdir', 'manageapproved'); + 'csstemplate', 'jstemplate', 'asearchtemplate', 'approval', + 'defaultsort', 'defaultsortdir', 'manageapproved'); // Add expected coursemodule. $database1->coursemodule = $database1->cmid; $database1->introfiles = []; + $database1->lang = ''; $database2->coursemodule = $database2->cmid; $database2->introfiles = []; + $database2->lang = ''; $expected1 = array(); $expected2 = array(); diff --git a/mod/feedback/classes/external/feedback_summary_exporter.php b/mod/feedback/classes/external/feedback_summary_exporter.php index 856b67bc983be..08f07fde4e6b6 100644 --- a/mod/feedback/classes/external/feedback_summary_exporter.php +++ b/mod/feedback/classes/external/feedback_summary_exporter.php @@ -62,6 +62,11 @@ protected static function define_properties() { 'default' => FORMAT_MOODLE, 'description' => 'Feedback intro text format.', ), + 'lang' => array( + 'type' => PARAM_LANG, + 'description' => 'Forced activity language', + 'null' => NULL_ALLOWED, + ), 'anonymous' => array( 'type' => PARAM_INT, 'description' => 'Whether the feedback is anonymous.', diff --git a/mod/feedback/tests/external/external_test.php b/mod/feedback/tests/external/external_test.php index 19555d713274f..df8e12b5dbd2f 100644 --- a/mod/feedback/tests/external/external_test.php +++ b/mod/feedback/tests/external/external_test.php @@ -107,7 +107,6 @@ public function populate_feedback($feedback, $pagescount = 1) { * Test test_mod_feedback_get_feedbacks_by_courses */ public function test_mod_feedback_get_feedbacks_by_courses() { - global $DB; // Create additional course. $course2 = self::getDataGenerator()->create_course(); @@ -134,7 +133,7 @@ public function test_mod_feedback_get_feedbacks_by_courses() { // Create what we expect to be returned when querying the two courses. // First for the student user. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'anonymous', + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', 'anonymous', 'multiple_submit', 'autonumbering', 'page_after_submitformat', 'publish_stats', 'completionsubmit'); $properties = feedback_summary_exporter::read_properties_definition(); @@ -144,10 +143,12 @@ public function test_mod_feedback_get_feedbacks_by_courses() { $feedback1->coursemodule = $feedback1->cmid; $feedback1->introformat = 1; $feedback1->introfiles = []; + $feedback1->lang = ''; $feedback2->coursemodule = $feedback2->cmid; $feedback2->introformat = 1; $feedback2->introfiles = []; + $feedback2->lang = ''; foreach ($expectedfields as $field) { if (!empty($properties[$field]) && $properties[$field]['type'] == PARAM_BOOL) { diff --git a/mod/folder/tests/externallib_test.php b/mod/folder/tests/externallib_test.php index 06f4df8717b40..1a0b0a63df518 100644 --- a/mod/folder/tests/externallib_test.php +++ b/mod/folder/tests/externallib_test.php @@ -149,7 +149,7 @@ public function test_mod_folder_get_folders_by_courses() { $returndescription = mod_folder_external::get_folders_by_courses_returns(); // Create what we expect to be returned when querying the two courses. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'revision', + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', 'revision', 'timemodified', 'display', 'showexpanded', 'showdownloadfolder', 'section', 'visible', 'forcedownload', 'groupmode', 'groupingid'); @@ -161,6 +161,7 @@ public function test_mod_folder_get_folders_by_courses() { $folder1->groupmode = 0; $folder1->groupingid = 0; $folder1->introfiles = []; + $folder1->lang = ''; $folder2->coursemodule = $folder2->cmid; $folder2->introformat = 1; @@ -169,6 +170,7 @@ public function test_mod_folder_get_folders_by_courses() { $folder2->groupmode = 0; $folder2->groupingid = 0; $folder2->introfiles = []; + $folder2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $folder1->{$field}; diff --git a/mod/forum/externallib.php b/mod/forum/externallib.php index 43be7e6506a0d..b019ab3e73547 100644 --- a/mod/forum/externallib.php +++ b/mod/forum/externallib.php @@ -97,6 +97,8 @@ public static function get_forums_by_courses($courseids = array()) { list($forum->intro, $forum->introformat) = external_format_text($forum->intro, $forum->introformat, $context->id, 'mod_forum', 'intro', null, $options); $forum->introfiles = external_util::get_area_files($context->id, 'mod_forum', 'intro', false, false); + $forum->lang = clean_param($forum->lang, PARAM_LANG); + // Discussions count. This function does static request cache. $forum->numdiscussions = forum_count_discussions($forum, $cm, $course); $forum->cmid = $forum->coursemodule; @@ -134,6 +136,7 @@ public static function get_forums_by_courses_returns() { 'intro' => new external_value(PARAM_RAW, 'The forum intro'), 'introformat' => new external_format_value('intro'), 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL), + 'lang' => new external_value(PARAM_SAFEDIR, 'Forced activity language', VALUE_OPTIONAL), 'duedate' => new external_value(PARAM_INT, 'duedate for the user', VALUE_OPTIONAL), 'cutoffdate' => new external_value(PARAM_INT, 'cutoffdate for the user', VALUE_OPTIONAL), 'assessed' => new external_value(PARAM_INT, 'Aggregate type'), diff --git a/mod/forum/tests/externallib_test.php b/mod/forum/tests/externallib_test.php index 6d293f082b0f6..5de6f02ffeb4a 100644 --- a/mod/forum/tests/externallib_test.php +++ b/mod/forum/tests/externallib_test.php @@ -145,6 +145,7 @@ public function test_mod_forum_get_forums_by_courses() { $forum1->istracked = true; $forum1->unreadpostscount = 0; $forum1->introfiles = []; + $forum1->lang = ''; $record = new \stdClass(); $record->course = $course2->id; @@ -157,6 +158,7 @@ public function test_mod_forum_get_forums_by_courses() { // Default limited role, no create discussion capability enabled. $forum2->cancreatediscussions = false; $forum2->istracked = false; + $forum2->lang = ''; // Check the forum was correctly created. $this->assertEquals(2, $DB->count_records_select('forum', 'id = :forum1 OR id = :forum2', diff --git a/mod/label/tests/externallib_test.php b/mod/label/tests/externallib_test.php index 95e5544708c07..0a36608c0767a 100644 --- a/mod/label/tests/externallib_test.php +++ b/mod/label/tests/externallib_test.php @@ -78,7 +78,7 @@ public function test_mod_label_get_labels_by_courses() { // Create what we expect to be returned when querying the two courses. $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'timemodified', - 'section', 'visible', 'groupmode', 'groupingid'); + 'section', 'visible', 'groupmode', 'groupingid', 'lang'); // Add expected coursemodule and data. $label1->coursemodule = $label1->cmid; @@ -88,6 +88,7 @@ public function test_mod_label_get_labels_by_courses() { $label1->groupmode = 0; $label1->groupingid = 0; $label1->introfiles = []; + $label1->lang = ''; $label2->coursemodule = $label2->cmid; $label2->introformat = 1; @@ -96,6 +97,7 @@ public function test_mod_label_get_labels_by_courses() { $label2->groupmode = 0; $label2->groupingid = 0; $label2->introfiles = []; + $label2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $label1->{$field}; diff --git a/mod/lesson/classes/external.php b/mod/lesson/classes/external.php index fc99b71d0f0fe..2d1e648717d33 100644 --- a/mod/lesson/classes/external.php +++ b/mod/lesson/classes/external.php @@ -54,6 +54,7 @@ protected static function get_lesson_summary_for_exporter($lessonrecord, $passwo $lesson = new lesson($lessonrecord); $lesson->update_effective_access($USER->id); + $lessonrecord->lang = $lesson->get_cm()->lang; $lessonavailable = $lesson->get_time_restriction_status() === false; $lessonavailable = $lessonavailable && $lesson->get_password_restriction_status($password) === false; $lessonavailable = $lessonavailable && $lesson->get_dependencies_restriction_status() === false; diff --git a/mod/lesson/classes/external/lesson_summary_exporter.php b/mod/lesson/classes/external/lesson_summary_exporter.php index e0fa14a5d53f7..a5b30448ae632 100644 --- a/mod/lesson/classes/external/lesson_summary_exporter.php +++ b/mod/lesson/classes/external/lesson_summary_exporter.php @@ -66,6 +66,11 @@ protected static function define_properties() { 'type' => PARAM_INT, 'default' => FORMAT_MOODLE ), + 'lang' => array( + 'type' => PARAM_LANG, + 'description' => 'Forced activity language', + 'null' => NULL_ALLOWED, + ), 'practice' => array( 'type' => PARAM_BOOL, 'description' => 'Practice lesson?', diff --git a/mod/lesson/tests/external/external_test.php b/mod/lesson/tests/external/external_test.php index 4f21c11978a31..74a369650d67a 100644 --- a/mod/lesson/tests/external/external_test.php +++ b/mod/lesson/tests/external/external_test.php @@ -142,8 +142,8 @@ public function test_mod_lesson_get_lessons_by_courses() { // Create what we expect to be returned when querying the two courses. // First for the student user. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'practice', - 'modattempts', 'usepassword', 'grade', 'custom', 'ongoing', 'usemaxgrade', + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', + 'practice', 'modattempts', 'usepassword', 'grade', 'custom', 'ongoing', 'usemaxgrade', 'maxanswers', 'maxattempts', 'review', 'nextpagedefault', 'feedback', 'minquestions', 'maxpages', 'timelimit', 'retake', 'mediafile', 'mediafiles', 'mediaheight', 'mediawidth', 'mediaclose', 'slideshow', 'width', 'height', 'bgcolor', 'displayleft', 'displayleftif', @@ -155,11 +155,13 @@ public function test_mod_lesson_get_lessons_by_courses() { $lesson1->introformat = 1; $lesson1->introfiles = []; $lesson1->mediafiles = []; + $lesson1->lang = ''; $lesson2->coursemodule = $lesson2->cmid; $lesson2->introformat = 1; $lesson2->introfiles = []; $lesson2->mediafiles = []; + $lesson2->lang = ''; $booltypes = array('practice', 'modattempts', 'usepassword', 'custom', 'ongoing', 'review', 'feedback', 'retake', 'slideshow', 'displayleft', 'progressbar', 'allowofflineattempts'); @@ -1322,7 +1324,7 @@ public function test_get_lesson_user_student() { // Lesson not using password. $result = mod_lesson_external::get_lesson($this->lesson->id); $result = \external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result); - $this->assertCount(36, $result['lesson']); // Expect most of the fields. + $this->assertCount(37, $result['lesson']); // Expect most of the fields. $this->assertFalse(isset($result['password'])); } @@ -1340,7 +1342,7 @@ public function test_get_lesson_user_student_with_missing_password() { // Lesson not using password. $result = mod_lesson_external::get_lesson($this->lesson->id); $result = \external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result); - $this->assertCount(6, $result['lesson']); // Expect just this few fields. + $this->assertCount(7, $result['lesson']); // Expect just this few fields. $this->assertFalse(isset($result['intro'])); } @@ -1358,7 +1360,7 @@ public function test_get_lesson_user_student_with_correct_password() { // Lesson not using password. $result = mod_lesson_external::get_lesson($this->lesson->id, $password); $result = \external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result); - $this->assertCount(36, $result['lesson']); + $this->assertCount(37 , $result['lesson']); $this->assertFalse(isset($result['intro'])); } @@ -1376,7 +1378,7 @@ public function test_get_lesson_teacher() { // Lesson not passing a valid password (but we are teachers, we should see all the info). $result = mod_lesson_external::get_lesson($this->lesson->id); $result = \external_api::clean_returnvalue(mod_lesson_external::get_lesson_returns(), $result); - $this->assertCount(45, $result['lesson']); // Expect all the fields. + $this->assertCount(46, $result['lesson']); // Expect all the fields. $this->assertEquals($result['lesson']['password'], $password); } } diff --git a/mod/lti/tests/externallib_test.php b/mod/lti/tests/externallib_test.php index b9bd638a05f60..62fa2f692d749 100644 --- a/mod/lti/tests/externallib_test.php +++ b/mod/lti/tests/externallib_test.php @@ -192,7 +192,7 @@ public function test_mod_lti_get_ltis_by_courses() { // Create what we expect to be returned when querying the two courses. // First for the student user. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', 'launchcontainer', 'showtitlelaunch', 'showdescriptionlaunch', 'icon', 'secureicon'); // Add expected coursemodule and data. @@ -205,6 +205,7 @@ public function test_mod_lti_get_ltis_by_courses() { $lti1->groupingid = 0; $lti1->section = 0; $lti1->introfiles = []; + $lti1->lang = ''; $lti2->coursemodule = $lti2->cmid; $lti2->introformat = 1; @@ -214,6 +215,7 @@ public function test_mod_lti_get_ltis_by_courses() { $lti2->groupingid = 0; $lti2->section = 0; $lti2->introfiles = []; + $lti2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $lti1->{$field}; @@ -257,7 +259,7 @@ public function test_mod_lti_get_ltis_by_courses() { $additionalfields = array('timecreated', 'timemodified', 'typeid', 'toolurl', 'securetoolurl', 'instructorchoicesendname', 'instructorchoicesendemailaddr', 'instructorchoiceallowroster', 'instructorchoiceallowsetting', 'instructorcustomparameters', 'instructorchoiceacceptgrades', 'grade', - 'resourcekey', 'password', 'debuglaunch', 'servicesalt', 'visible', 'groupmode', 'groupingid', 'section'); + 'resourcekey', 'password', 'debuglaunch', 'servicesalt', 'visible', 'groupmode', 'groupingid', 'section', 'lang'); foreach ($additionalfields as $field) { $expectedltis[0][$field] = $lti1->{$field}; diff --git a/mod/page/tests/externallib_test.php b/mod/page/tests/externallib_test.php index 87ec9f555876f..92fdad8246837 100644 --- a/mod/page/tests/externallib_test.php +++ b/mod/page/tests/externallib_test.php @@ -147,7 +147,7 @@ public function test_mod_page_get_pages_by_courses() { $returndescription = mod_page_external::get_pages_by_courses_returns(); // Create what we expect to be returned when querying the two courses. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', 'content', 'contentformat', 'contentfiles', 'legacyfiles', 'legacyfileslast', 'display', 'displayoptions', 'revision', 'timemodified', 'section', 'visible', 'groupmode', 'groupingid'); @@ -161,6 +161,7 @@ public function test_mod_page_get_pages_by_courses() { $page1->groupingid = 0; $page1->introfiles = []; $page1->contentfiles = []; + $page1->lang = ''; $page2->coursemodule = $page2->cmid; $page2->introformat = 1; @@ -171,6 +172,7 @@ public function test_mod_page_get_pages_by_courses() { $page2->groupingid = 0; $page2->introfiles = []; $page2->contentfiles = []; + $page2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $page1->{$field}; diff --git a/mod/quiz/tests/external/external_test.php b/mod/quiz/tests/external/external_test.php index c77b3ce6408d6..98bdb06062d56 100644 --- a/mod/quiz/tests/external/external_test.php +++ b/mod/quiz/tests/external/external_test.php @@ -214,8 +214,8 @@ public function test_mod_quiz_get_quizzes_by_courses() { // Create what we expect to be returned when querying the two courses. // First for the student user. - $allusersfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'timeopen', - 'timeclose', 'grademethod', 'section', 'visible', 'groupmode', 'groupingid', + $allusersfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', + 'timeopen', 'timeclose', 'grademethod', 'section', 'visible', 'groupmode', 'groupingid', 'attempts', 'timelimit', 'grademethod', 'decimalpoints', 'questiondecimalpoints', 'sumgrades', 'grade', 'preferredbehaviour', 'hasfeedback'); $userswithaccessfields = array('attemptonlast', 'reviewattempt', 'reviewcorrectness', 'reviewmarks', @@ -239,6 +239,7 @@ public function test_mod_quiz_get_quizzes_by_courses() { $quiz1->completionpass = 0; $quiz1->autosaveperiod = get_config('quiz', 'autosaveperiod'); $quiz1->introfiles = []; + $quiz1->lang = ''; $quiz2->coursemodule = $quiz2->cmid; $quiz2->introformat = 1; @@ -251,6 +252,7 @@ public function test_mod_quiz_get_quizzes_by_courses() { $quiz2->completionpass = 0; $quiz2->autosaveperiod = get_config('quiz', 'autosaveperiod'); $quiz2->introfiles = []; + $quiz2->lang = ''; foreach (array_merge($allusersfields, $userswithaccessfields) as $field) { $expected1[$field] = $quiz1->{$field}; @@ -317,7 +319,7 @@ public function test_mod_quiz_get_quizzes_by_courses() { $result = \external_api::clean_returnvalue($returndescription, $result); $this->assertCount(2, $result['quizzes']); // We only see a limited set of fields. - $this->assertCount(4, $result['quizzes'][0]); + $this->assertCount(5, $result['quizzes'][0]); $this->assertEquals($quiz2->id, $result['quizzes'][0]['id']); $this->assertEquals($quiz2->cmid, $result['quizzes'][0]['coursemodule']); $this->assertEquals($quiz2->course, $result['quizzes'][0]['course']); diff --git a/mod/resource/tests/externallib_test.php b/mod/resource/tests/externallib_test.php index 49824f971260e..946ffbeefb664 100644 --- a/mod/resource/tests/externallib_test.php +++ b/mod/resource/tests/externallib_test.php @@ -148,7 +148,7 @@ public function test_mod_resource_get_resources_by_courses() { $returndescription = mod_resource_external::get_resources_by_courses_returns(); // Create what we expect to be returned when querying the two courses. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', 'contentfiles', 'tobemigrated', 'legacyfiles', 'legacyfileslast', 'display', 'displayoptions', 'filterfiles', 'revision', 'timemodified', 'section', 'visible', 'groupmode', 'groupingid'); @@ -162,6 +162,7 @@ public function test_mod_resource_get_resources_by_courses() { $resource1->groupingid = 0; $resource1->introfiles = []; $resource1->contentfiles = []; + $resource1->lang = ''; $resource2->coursemodule = $resource2->cmid; $resource2->introformat = 1; @@ -172,6 +173,7 @@ public function test_mod_resource_get_resources_by_courses() { $resource2->groupingid = 0; $resource2->introfiles = []; $resource2->contentfiles = []; + $resource2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $resource1->{$field}; diff --git a/mod/scorm/tests/externallib_test.php b/mod/scorm/tests/externallib_test.php index 4955e00097b58..f26ee06e80412 100644 --- a/mod/scorm/tests/externallib_test.php +++ b/mod/scorm/tests/externallib_test.php @@ -647,7 +647,7 @@ public function test_mod_scorm_get_scorms_by_courses() { $result = \external_api::clean_returnvalue($returndescription, $result); $this->assertCount(1, $result['warnings']); // Only 'id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles'. - $this->assertCount(7, $result['scorms'][0]); + $this->assertCount(8, $result['scorms'][0]); $this->assertEquals('expired', $result['warnings'][0]['warningcode']); $scorm1->timeopen = $timenow + DAYSECS; @@ -658,7 +658,7 @@ public function test_mod_scorm_get_scorms_by_courses() { $result = \external_api::clean_returnvalue($returndescription, $result); $this->assertCount(1, $result['warnings']); // Only 'id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles'. - $this->assertCount(7, $result['scorms'][0]); + $this->assertCount(8, $result['scorms'][0]); $this->assertEquals('notopenyet', $result['warnings'][0]['warningcode']); // Reset times. @@ -668,7 +668,7 @@ public function test_mod_scorm_get_scorms_by_courses() { // Create what we expect to be returned when querying the two courses. // First for the student user. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'version', 'maxgrade', + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'lang', 'version', 'maxgrade', 'grademethod', 'whatgrade', 'maxattempt', 'forcecompleted', 'forcenewattempt', 'lastattemptlock', 'displayattemptstatus', 'displaycoursestructure', 'sha1hash', 'md5hash', 'revision', 'launch', 'skipview', 'hidebrowse', 'hidetoc', 'nav', 'navpositionleft', 'navpositiontop', 'auto', @@ -681,12 +681,14 @@ public function test_mod_scorm_get_scorms_by_courses() { $scorm1->visible = true; $scorm1->groupmode = 0; $scorm1->groupingid = 0; + $scorm1->lang = ''; $scorm2->coursemodule = $scorm2->cmid; $scorm2->section = 0; $scorm2->visible = true; $scorm2->groupmode = 0; $scorm2->groupingid = 0; + $scorm2->lang = ''; // SCORM size. The same package is used in both SCORMs. $scormcontext1 = \context_module::instance($scorm1->cmid); diff --git a/mod/survey/tests/externallib_test.php b/mod/survey/tests/externallib_test.php index d9bf2fb213d26..95fba65130386 100644 --- a/mod/survey/tests/externallib_test.php +++ b/mod/survey/tests/externallib_test.php @@ -96,8 +96,8 @@ public function test_mod_survey_get_surveys_by_courses() { // Create what we expect to be returned when querying the two courses. // First for the student user. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'template', 'days', - 'questions', 'surveydone'); + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', + 'template', 'days', 'questions', 'surveydone'); // Add expected coursemodule and data. $survey1 = $this->survey; @@ -109,6 +109,7 @@ public function test_mod_survey_get_surveys_by_courses() { $survey1->groupmode = 0; $survey1->groupingid = 0; $survey1->introfiles = []; + $survey1->lang = ''; $survey2->coursemodule = $survey2->cmid; $survey2->introformat = 1; @@ -120,6 +121,7 @@ public function test_mod_survey_get_surveys_by_courses() { $tempo = $DB->get_field("survey", "intro", array("id" => $survey2->template)); $survey2->intro = nl2br(get_string($tempo, "survey")); $survey2->introfiles = []; + $survey2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $survey1->{$field}; diff --git a/mod/url/tests/externallib_test.php b/mod/url/tests/externallib_test.php index 9b8efa93e4b34..d361c3d21ca1e 100644 --- a/mod/url/tests/externallib_test.php +++ b/mod/url/tests/externallib_test.php @@ -147,9 +147,9 @@ public function test_mod_url_get_urls_by_courses() { $returndescription = mod_url_external::get_urls_by_courses_returns(); // Create what we expect to be returned when querying the two courses. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'externalurl', - 'display', 'displayoptions', 'parameters', 'timemodified', 'section', 'visible', 'groupmode', - 'groupingid'); + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', + 'externalurl', 'display', 'displayoptions', 'parameters', 'timemodified', 'section', 'visible', 'groupmode', + 'groupingid'); // Add expected coursemodule and data. $url1->coursemodule = $url1->cmid; @@ -159,6 +159,7 @@ public function test_mod_url_get_urls_by_courses() { $url1->groupmode = 0; $url1->groupingid = 0; $url1->introfiles = []; + $url1->lang = ''; $url2->coursemodule = $url2->cmid; $url2->introformat = 1; @@ -167,6 +168,7 @@ public function test_mod_url_get_urls_by_courses() { $url2->groupmode = 0; $url2->groupingid = 0; $url2->introfiles = []; + $url2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $url1->{$field}; diff --git a/mod/wiki/tests/externallib_test.php b/mod/wiki/tests/externallib_test.php index 7e762a3fc0f45..9f16ca4e02303 100644 --- a/mod/wiki/tests/externallib_test.php +++ b/mod/wiki/tests/externallib_test.php @@ -168,9 +168,9 @@ public function test_mod_wiki_get_wikis_by_courses() { // Create what we expect to be returned when querying the two courses. // First for the student user. - $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'firstpagetitle', - 'wikimode', 'defaultformat', 'forceformat', 'editbegin', 'editend', 'section', 'visible', - 'groupmode', 'groupingid'); + $expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'lang', + 'firstpagetitle', 'wikimode', 'defaultformat', 'forceformat', 'editbegin', 'editend', 'section', 'visible', + 'groupmode', 'groupingid'); // Add expected coursemodule and data. $wiki1 = $this->wiki; @@ -181,6 +181,7 @@ public function test_mod_wiki_get_wikis_by_courses() { $wiki1->groupmode = 0; $wiki1->groupingid = 0; $wiki1->introfiles = []; + $wiki1->lang = ''; $wiki2->coursemodule = $wiki2->cmid; $wiki2->introformat = 1; @@ -189,6 +190,7 @@ public function test_mod_wiki_get_wikis_by_courses() { $wiki2->groupmode = 0; $wiki2->groupingid = 0; $wiki2->introfiles = []; + $wiki2->lang = ''; foreach ($expectedfields as $field) { $expected1[$field] = $wiki1->{$field}; diff --git a/mod/workshop/classes/external/workshop_summary_exporter.php b/mod/workshop/classes/external/workshop_summary_exporter.php index 45edd78d9625e..02c12d2134ac4 100644 --- a/mod/workshop/classes/external/workshop_summary_exporter.php +++ b/mod/workshop/classes/external/workshop_summary_exporter.php @@ -64,6 +64,11 @@ protected static function define_properties() { 'default' => FORMAT_MOODLE, 'description' => 'Workshop intro text format.', ), + 'lang' => array( + 'type' => PARAM_LANG, + 'description' => 'Forced activity language', + 'null' => NULL_ALLOWED, + ), 'instructauthors' => array( 'type' => PARAM_RAW, 'description' => 'Instructions for the submission phase.', diff --git a/mod/workshop/tests/external/external_test.php b/mod/workshop/tests/external/external_test.php index 02bc686b6c4d1..e8906a0ed721f 100644 --- a/mod/workshop/tests/external/external_test.php +++ b/mod/workshop/tests/external/external_test.php @@ -161,6 +161,7 @@ public function test_mod_workshop_get_workshops_by_courses() { $workshop1->coursemodule = $workshop1->cmid; $workshop1->introformat = 1; $workshop1->introfiles = []; + $workshop1->lang = ''; $workshop1->instructauthorsfiles = []; $workshop1->instructauthorsformat = 1; $workshop1->instructreviewersfiles = []; @@ -173,6 +174,7 @@ public function test_mod_workshop_get_workshops_by_courses() { $workshop2->coursemodule = $workshop2->cmid; $workshop2->introformat = 1; $workshop2->introfiles = []; + $workshop2->lang = ''; $workshop2->instructauthorsfiles = []; $workshop2->instructauthorsformat = 1; $workshop2->instructreviewersfiles = []; diff --git a/version.php b/version.php index f287acb30b352..b0cf33b1f7072 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2022081200.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2022081200.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.1dev (Build: 20220812)'; // Human-friendly version name