diff --git a/backup/moodle2/backup_stepslib.php b/backup/moodle2/backup_stepslib.php index 577b909a22d..d1324de88d3 100644 --- a/backup/moodle2/backup_stepslib.php +++ b/backup/moodle2/backup_stepslib.php @@ -275,7 +275,7 @@ class backup_module_structure_step extends backup_structure_step { 'visibleold', 'groupmode', 'groupingid', 'completion', 'completiongradeitemnumber', 'completionpassgrade', 'completionview', 'completionexpected', - 'availability', 'showdescription')); + 'availability', 'showdescription', 'downloadcontent')); $tags = new backup_nested_element('tags'); $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname')); diff --git a/course/externallib.php b/course/externallib.php index 0ae4854ca0c..42a2369e245 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -275,6 +275,7 @@ class core_course_external extends external_api { $module['afterlink'] = $cm->afterlink; $module['customdata'] = json_encode($cm->customdata); $module['completion'] = $cm->completion; + $module['downloadcontent'] = $cm->downloadcontent; $module['noviewlink'] = plugin_supports('mod', $cm->modname, FEATURE_NO_VIEW_LINK, false); $module['dates'] = $activitydates; @@ -477,6 +478,7 @@ class core_course_external extends external_api { 'completion' => new external_value(PARAM_INT, 'Type of completion tracking: 0 means none, 1 manual, 2 automatic.', VALUE_OPTIONAL), 'completiondata' => $completiondefinition, + 'downloadcontent' => new external_value(PARAM_INT, 'The download content value', VALUE_OPTIONAL), 'dates' => new external_multiple_structure( new external_single_structure( array( @@ -2828,6 +2830,7 @@ class core_course_external extends external_api { $info->groupmode = $cm->groupmode; $info->groupingid = $cm->groupingid; $info->completion = $cm->completion; + $info->downloadcontent = $cm->downloadcontent; } // Format name. $info->name = external_format_string($cm->name, $context->id); @@ -2871,6 +2874,7 @@ class core_course_external extends external_api { 'completionview' => new external_value(PARAM_INT, 'Completion view setting', VALUE_OPTIONAL), 'completionexpected' => new external_value(PARAM_INT, 'Completion time expected', VALUE_OPTIONAL), 'showdescription' => new external_value(PARAM_INT, 'If the description is showed', VALUE_OPTIONAL), + 'downloadcontent' => new external_value(PARAM_INT, 'The download content value', VALUE_OPTIONAL), 'availability' => new external_value(PARAM_RAW, 'Availability settings', VALUE_OPTIONAL), 'grade' => new external_value(PARAM_FLOAT, 'Grade (max value or scale id)', VALUE_OPTIONAL), 'scale' => new external_value(PARAM_TEXT, 'Scale items (if used)', VALUE_OPTIONAL), diff --git a/course/lib.php b/course/lib.php index 43733242a18..13091f1f194 100644 --- a/course/lib.php +++ b/course/lib.php @@ -467,7 +467,7 @@ function get_array_of_activities($courseid) { $mod[$seq]->showdescription = $rawmods[$seq]->showdescription; $mod[$seq]->availability = $rawmods[$seq]->availability; $mod[$seq]->deletioninprogress = $rawmods[$seq]->deletioninprogress; - + $mod[$seq]->downloadcontent = $rawmods[$seq]->downloadcontent; $modname = $mod[$seq]->mod; $functionname = $modname."_get_coursemodule_info"; @@ -852,6 +852,23 @@ function set_coursemodule_idnumber($id, $idnumber) { return ($cm->idnumber != $idnumber); } +/** + * Set downloadcontent value to course module. + * + * @param int $id The id of the module. + * @param bool $downloadcontent Whether the module can be downloaded when download course content is enabled. + * @return bool True if downloadcontent has been updated, false otherwise. + */ +function set_downloadcontent(int $id, bool $downloadcontent): bool { + global $DB; + $cm = $DB->get_record('course_modules', ['id' => $id], 'id, course, downloadcontent', MUST_EXIST); + if ($cm->downloadcontent != $downloadcontent) { + $DB->set_field('course_modules', 'downloadcontent', $downloadcontent, ['id' => $cm->id]); + rebuild_course_cache($cm->course, true); + } + return ($cm->downloadcontent != $downloadcontent); +} + /** * Set the visibility of a module and inherent properties. * diff --git a/course/modlib.php b/course/modlib.php index 6e18ea6facf..c2b3bd8c8eb 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -67,6 +67,9 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) { if (isset($moduleinfo->cmidnumber)) { $newcm->idnumber = $moduleinfo->cmidnumber; } + if (isset($moduleinfo->downloadcontent)) { + $newcm->downloadcontent = $moduleinfo->downloadcontent; + } $newcm->groupmode = $moduleinfo->groupmode; $newcm->groupingid = $moduleinfo->groupingid; $completion = new completion_info($course); @@ -460,6 +463,10 @@ function set_moduleinfo_defaults($moduleinfo) { $moduleinfo->visibleoncoursepage = 1; } + if (!isset($moduleinfo->downloadcontent)) { + $moduleinfo->downloadcontent = DOWNLOAD_COURSE_CONTENT_ENABLED; + } + return $moduleinfo; } @@ -662,6 +669,10 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) { set_coursemodule_idnumber($moduleinfo->coursemodule, $moduleinfo->cmidnumber); } + if (isset($moduleinfo->downloadcontent)) { + set_downloadcontent($moduleinfo->coursemodule, $moduleinfo->downloadcontent); + } + // Update module tags. if (core_tag_tag::is_enabled('core', 'course_modules') && isset($moduleinfo->tags)) { core_tag_tag::set_item_tags('core', 'course_modules', $moduleinfo->coursemodule, $modcontext, $moduleinfo->tags); @@ -731,6 +742,7 @@ function get_moduleinfo_data($cm, $course) { $data->completionpassgrade = $cm->completionpassgrade; $data->completiongradeitemnumber = $cm->completiongradeitemnumber; $data->showdescription = $cm->showdescription; + $data->downloadcontent = $cm->downloadcontent; $data->tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $cm->id); if (!empty($CFG->enableavailability)) { $data->availabilityconditionsjson = $cm->availability; @@ -830,6 +842,7 @@ function prepare_new_moduleinfo_data($course, $modulename, $section) { $data->id = ''; $data->instance = ''; $data->coursemodule = ''; + $data->downloadcontent = DOWNLOAD_COURSE_CONTENT_ENABLED; // Apply completion defaults. $defaults = \core_completion\manager::get_default_completion($course, $module); diff --git a/course/moodleform_mod.php b/course/moodleform_mod.php index d9018c16bb4..88d84601031 100644 --- a/course/moodleform_mod.php +++ b/course/moodleform_mod.php @@ -27,6 +27,7 @@ require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/plagiarismlib.php'); +use core\content\export\exporters\component_exporter; use core_grades\component_gradeitems; /** @@ -637,6 +638,22 @@ abstract class moodleform_mod extends moodleform { $mform->addHelpButton('groupmode', 'groupmode', 'group'); } + if ($CFG->downloadcoursecontentallowed) { + $choices = [ + DOWNLOAD_COURSE_CONTENT_DISABLED => get_string('no'), + DOWNLOAD_COURSE_CONTENT_ENABLED => get_string('yes'), + ]; + $mform->addElement('select', 'downloadcontent', get_string('downloadcontent', 'course'), $choices); + $downloadcontentdefault = $this->_cm->downloadcontent ?? DOWNLOAD_COURSE_CONTENT_ENABLED; + $mform->addHelpButton('downloadcontent', 'downloadcontent', 'course'); + if (has_capability('moodle/course:configuredownloadcontent', $this->get_context())) { + $mform->setDefault('downloadcontent', $downloadcontentdefault); + } else { + $mform->hardFreeze('downloadcontent'); + $mform->setConstant('downloadcontent', $downloadcontentdefault); + } + } + if ($this->_features->groupings) { // Groupings selector - used to select grouping for groups in activity. $options = array(); diff --git a/course/tests/behat/course_download_content_cm.feature b/course/tests/behat/course_download_content_cm.feature new file mode 100644 index 00000000000..3f84f88cdc3 --- /dev/null +++ b/course/tests/behat/course_download_content_cm.feature @@ -0,0 +1,66 @@ +@core @core_course +Feature: Activities content download can be controlled + In order to allow or restrict access to download activity content + As a teacher + I can disable the content download of an activity + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | manager1 | Manager | 1 | manager1@example.com | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | manager1 | C1 | manager | + And the following "activities" exist: + | activity | name | intro | introformat | course | + | page | Page1 | PageDesc1 | 1 | C1 | + And the following "activities" exist: + | activity | name | intro | introformat | course | downloadcontent | + | folder | Folder1 | FolderDesc1 | 1 | C1 | 0 | + And I log in as "admin" + And the following config values are set as admin: + | downloadcoursecontentallowed | 1 | + And I log out + + Scenario: "Include in course downloads (if that feature is enabled)" field default is set to "Yes" if nothing has been set + Given I am on the Page1 "Page Activity editing" page logged in as admin + Then the field "Include in course downloads (if that feature is enabled)" matches value "Yes" + + Scenario: "Include in course downloads (if that feature is enabled)" field is not visible if course content is disabled on site level + Given I log in as "admin" + And the following config values are set as admin: + | downloadcoursecontentallowed | 0 | + And I am on the Page1 "Page Activity editing" page + Then "Include in course downloads (if that feature is enabled)" "select" should not exist + + Scenario: "Include in course downloads (if that feature is enabled)" field is visible even if course content is disabled on course level + Given I log in as "admin" + And I am on "Course 1" course homepage + And I navigate to "Settings" in current page administration + When I set the field "Enable download course content" to "No" + And I press "Save and display" + And I am on the Page1 "Page Activity editing" page + Then "Include in course downloads (if that feature is enabled)" "select" should exist + + Scenario: "Include in course downloads (if that feature is enabled)" field should be visible but not editable for users without configuredownloadcontent capability + Given I log in as "manager1" + And I am on the Folder1 "Folder Activity editing" page + And "Include in course downloads (if that feature is enabled)" "field" should exist + And I log out + And I log in as "admin" + When I set the following system permissions of "Manager" role: + | capability | permission | + | moodle/course:configuredownloadcontent | Prohibit | + And I log out + And I log in as "manager1" + And I am on the Folder1 "Folder Activity editing" page + Then I should see "Include in course downloads (if that feature is enabled)" + And I should see "No" + And "Include in course downloads (if that feature is enabled)" "select" should not exist diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php index 32f66639e07..06229494c3f 100644 --- a/course/tests/courselib_test.php +++ b/course/tests/courselib_test.php @@ -7233,4 +7233,30 @@ class core_course_courselib_testcase extends advanced_testcase { $this->assertEquals(1, average_number_of_participants(true)); } + /** + * Test the set_downloadcontent() function. + */ + public function test_set_downloadcontent() { + $this->resetAfterTest(); + + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $page = $generator->create_module('page', ['course' => $course]); + + // Test the module 'downloadcontent' field is set to enabled. + set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_ENABLED); + $modinfo = get_fast_modinfo($course)->get_cm($page->cmid); + $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $modinfo->downloadcontent); + + // Now let's test the 'downloadcontent' value is updated to disabled. + set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_DISABLED); + $modinfo = get_fast_modinfo($course)->get_cm($page->cmid); + $this->assertEquals(DOWNLOAD_COURSE_CONTENT_DISABLED, $modinfo->downloadcontent); + + // Nothing to update, the download course content value is the same, it should return false. + $this->assertFalse(set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_DISABLED)); + + // The download course content value has changed, it should return true in this case. + $this->assertTrue(set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_ENABLED)); + } } diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index 48650aa6cc7..a2d03e22c4d 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -1366,6 +1366,28 @@ class externallib_test extends externallib_advanced_testcase { $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]); } + /** + * Test get_course_contents returns downloadcontent value. + */ + public function test_get_course_contents_downloadcontent() { + $this->resetAfterTest(); + + list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); + + // Test exclude modules. + $sections = core_course_external::get_course_contents($course->id, [ + ['name' => 'modname', 'value' => 'page'], + ['name' => 'modid', 'value' => $pagecm->instance] + ]); + + // We need to execute the return values cleaning process to simulate the web service server. + $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); + $this->assertCount(1, $sections[0]['modules']); + $this->assertEquals('page', $sections[0]['modules'][0]['modname']); + $this->assertEquals($pagecm->downloadcontent, $sections[0]['modules'][0]['downloadcontent']); + $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $sections[0]['modules'][0]['downloadcontent']); + } + /** * Test get course contents completion manual */ @@ -2373,7 +2395,7 @@ class externallib_test extends externallib_advanced_testcase { $this->assertCount(0, $result['warnings']); // Test we retrieve all the fields. - $this->assertCount(29, $result['cm']); + $this->assertCount(30, $result['cm']); $this->assertEquals($record['name'], $result['cm']['name']); $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); $this->assertEquals(100, $result['cm']['grade']); @@ -2381,6 +2403,7 @@ class externallib_test extends externallib_advanced_testcase { $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']); $this->assertEmpty($result['cm']['advancedgrading'][0]['method']); $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']); + $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']); $student = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); @@ -2405,7 +2428,7 @@ class externallib_test extends externallib_advanced_testcase { $this->assertCount(0, $result['warnings']); // Test we retrieve only the few files we can see. - $this->assertCount(11, $result['cm']); + $this->assertCount(12, $result['cm']); $this->assertEquals($assign->cmid, $result['cm']['id']); $this->assertEquals($course->id, $result['cm']['course']); $this->assertEquals('assign', $result['cm']['modname']); @@ -2441,10 +2464,11 @@ class externallib_test extends externallib_advanced_testcase { $this->assertCount(0, $result['warnings']); // Test we retrieve all the fields. - $this->assertCount(27, $result['cm']); + $this->assertCount(28, $result['cm']); $this->assertEquals($record['name'], $result['cm']['name']); $this->assertEquals($record['grade'], $result['cm']['grade']); $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); + $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']); $student = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); @@ -2469,7 +2493,7 @@ class externallib_test extends externallib_advanced_testcase { $this->assertCount(0, $result['warnings']); // Test we retrieve only the few files we can see. - $this->assertCount(11, $result['cm']); + $this->assertCount(12, $result['cm']); $this->assertEquals($quiz->cmid, $result['cm']['id']); $this->assertEquals($course->id, $result['cm']['course']); $this->assertEquals('quiz', $result['cm']['modname']); diff --git a/course/tests/modlib_test.php b/course/tests/modlib_test.php index 937fcd281fd..473e8a7458d 100644 --- a/course/tests/modlib_test.php +++ b/course/tests/modlib_test.php @@ -63,6 +63,8 @@ class core_course_modlib_testcase extends advanced_testcase { $expecteddata->coursemodule = ''; $expecteddata->advancedgradingmethod_submissions = ''; // Not grading methods enabled by default. $expecteddata->completion = 0; + $expecteddata->downloadcontent = DOWNLOAD_COURSE_CONTENT_ENABLED; + // Unset untestable. unset($data->introeditor); unset($data->_advancedgradingdata); @@ -115,6 +117,7 @@ class core_course_modlib_testcase extends advanced_testcase { $expecteddata->completionpassgrade = $assigncm->completionpassgrade; $expecteddata->completiongradeitemnumber = null; $expecteddata->showdescription = $assigncm->showdescription; + $expecteddata->downloadcontent = $assigncm->downloadcontent; $expecteddata->tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $assigncm->id); $expecteddata->availabilityconditionsjson = null; $expecteddata->advancedgradingmethod_submissions = null; diff --git a/lang/en/course.php b/lang/en/course.php index 5d5861e095f..a5a83064a8a 100644 --- a/lang/en/course.php +++ b/lang/en/course.php @@ -79,6 +79,8 @@ $string['customfieldsettings'] = 'Common course custom fields settings'; $string['downloadcourseconfirmation'] = 'You are about to download a zip file of course content (excluding items which cannot be downloaded and any files larger than {$a}).'; $string['downloadcoursecontent'] = 'Download course content'; $string['downloadcoursecontent_help'] = 'This setting determines whether course content may be downloaded by users with the download course content capability (by default users with the role of student or teacher).'; +$string['downloadcontent'] = 'Include in course downloads (if that feature is enabled)'; +$string['downloadcontent_help'] = 'This setting determines whether this activity can be downloaded when download course content is enabled for this course'; $string['enabledownloadcoursecontent'] = 'Enable download course content'; $string['errorendbeforestart'] = 'The end date ({$a}) is before the course start date.'; $string['favourite'] = 'Starred course'; diff --git a/lib/classes/content.php b/lib/classes/content.php index d9ad2c91599..20bf4a156d9 100644 --- a/lib/classes/content.php +++ b/lib/classes/content.php @@ -71,6 +71,13 @@ class content { } } else if ($currentcontext->contextlevel == CONTEXT_MODULE) { + $cm = get_fast_modinfo($currentcontext->get_course_context()->instanceid)->cms[$currentcontext->instanceid]; + + // Do not export course content if disabled at activity level. + if (isset($cm->downloadcontent) && $cm->downloadcontent == DOWNLOAD_COURSE_CONTENT_DISABLED) { + return false; + } + // Modules can only be exported if exporting is allowed in their course context. $canexport = self::can_export_context($currentcontext->get_course_context(), $user); } diff --git a/lib/db/install.xml b/lib/db/install.xml index d59520dde26..5f0e91518ee 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -308,6 +308,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 4f79b7bbd54..d9abd50a0bf 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -3116,5 +3116,19 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2021110100.00); } + if ($oldversion < 2021110800.02) { + // Define a field 'downloadcontent' in the 'course_modules' table. + $table = new xmldb_table('course_modules'); + $field = new xmldb_field('downloadcontent', XMLDB_TYPE_INTEGER, '1', null, null, null, 1, 'deletioninprogress'); + + // Conditionally launch add field 'downloadcontent'. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2021110800.02); + } + return true; } diff --git a/lib/modinfolib.php b/lib/modinfolib.php index 191e5e062a6..b962a133385 100644 --- a/lib/modinfolib.php +++ b/lib/modinfolib.php @@ -843,6 +843,7 @@ class course_modinfo { * @property-read string $afterlink Extra HTML code to display after link - calculated on request * @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. */ class cm_info implements IteratorAggregate { /** @@ -1156,6 +1157,11 @@ class cm_info implements IteratorAggregate { */ private $deletioninprogress; + /** + * @var int enable/disable download content for this course module + */ + private $downloadcontent; + /** * List of class read-only properties and their getter methods. * Used by magic functions __get(), __isset(), __empty() @@ -1209,7 +1215,8 @@ class cm_info implements IteratorAggregate { 'visible' => false, 'visibleoncoursepage' => false, 'visibleold' => false, - 'deletioninprogress' => false + 'deletioninprogress' => false, + 'downloadcontent' => false ); /** @@ -1656,7 +1663,8 @@ class cm_info implements IteratorAggregate { 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'); + 'showdescription', 'availability', 'deletioninprogress', 'downloadcontent'); + foreach ($cmfields as $key) { $cmrecord->$key = $this->$key; } @@ -1869,6 +1877,7 @@ class cm_info implements IteratorAggregate { $this->score = isset($mod->score) ? $mod->score : 0; $this->visibleold = isset($mod->visibleold) ? $mod->visibleold : 0; $this->deletioninprogress = isset($mod->deletioninprogress) ? $mod->deletioninprogress : 0; + $this->downloadcontent = $mod->downloadcontent ?? null; // Note: it saves effort and database space to always include the // availability and completion fields, even if availability or completion diff --git a/version.php b/version.php index 0f8193e3f90..fd8a130bf19 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2021110800.01; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2021110800.02; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.0dev+ (Build: 20211106)'; // Human-friendly version name