Skip to content

Commit

Permalink
MDL-74608 activities: a new option to force the activity language
Browse files Browse the repository at this point in the history
For a long time, Moodle has had the feature to force the language
for a whole course. This change adds the same feature at activity
level.

The course-level feature was controlled by a capability
moodle/course:setforcedlanguage, and I decided to use the same
capability to control this feature. I think a new capability would be
overkill.
  • Loading branch information
timhunt committed Aug 15, 2022
1 parent 896e126 commit 8babdbd
Show file tree
Hide file tree
Showing 44 changed files with 528 additions and 85 deletions.
2 changes: 1 addition & 1 deletion backup/moodle2/backup_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
4 changes: 4 additions & 0 deletions backup/moodle2/restore_stepslib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions course/classes/external/helper_for_get_mods_by_courses.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
];
}
}
7 changes: 4 additions & 3 deletions course/edit_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
15 changes: 13 additions & 2 deletions course/modlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 10 additions & 7 deletions course/moodleform_mod.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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'),
Expand Down Expand Up @@ -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');
Expand Down
137 changes: 137 additions & 0 deletions course/tests/backup_restore_activity_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?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 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;
}
}
30 changes: 12 additions & 18 deletions course/tests/behat/add_activities.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions course/tests/modlib_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion lang/en/role.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion lib/classes/string_manager_standard.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions lib/datalib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/db/access.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion lib/db/install.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
<FIELD NAME="groupmode" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="groupmodeforce" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="defaultgroupingid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="default grouping used in course modules, does not have key intentionally"/>
<FIELD NAME="lang" TYPE="char" LENGTH="30" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="lang" TYPE="char" LENGTH="30" NOTNULL="true" SEQUENCE="false" COMMENT="Forced language for this course. Null or '' means 'Do not force'. Otherwise a Moodle lang pack name like 'fr' or 'en_us'."/>
<FIELD NAME="calendartype" TYPE="char" LENGTH="30" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="theme" TYPE="char" LENGTH="50" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
Expand Down Expand Up @@ -309,6 +309,7 @@
<FIELD NAME="availability" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Availability restrictions for viewing this activity, in JSON format. Null if no restrictions."/>
<FIELD NAME="deletioninprogress" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="downloadcontent" TYPE="int" LENGTH="1" NOTNULL="false" DEFAULT="1" SEQUENCE="false" COMMENT="Whether the ability to download course module content is enabled for this activity"/>
<FIELD NAME="lang" TYPE="char" LENGTH="30" NOTNULL="false" SEQUENCE="false" COMMENT="Forced language for this activity. Null or '' means 'Do not force'. Otherwise a Moodle lang pack name like 'fr' or 'en_us'."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
Expand Down
Loading

0 comments on commit 8babdbd

Please sign in to comment.