MDL-82259 course: Propagate visibility from and to delegated section

This commit is contained in:
Amaia Anabitarte
2024-07-26 14:45:55 +02:00
parent d4a887987a
commit 19ab9cc6ec
9 changed files with 272 additions and 73 deletions
+137
View File
@@ -17,6 +17,7 @@
namespace core_courseformat\local;
use core_courseformat\sectiondelegatemodule;
use course_modinfo;
/**
* Course module course format actions.
@@ -26,8 +27,44 @@ use course_modinfo;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cmactions extends baseactions {
/**
* Update a course delegated section linked to the given module.
*
* @param \stdClass $cm
* @param array $sectionfields to change in section database record.
* @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild.
* @return bool true if any delegated section has been updated, false otherwise.
*/
protected function update_delegated(
\stdClass $cm,
array $sectionfields,
bool $rebuildcache = true
): bool {
if (!sectiondelegatemodule::has_delegate_class('mod_' . $cm->modname)) {
return false;
}
// Propagate the changes to delegated section.
$cminfo = \cm_info::create($cm);
if (!$delegatedsection = $cminfo->get_delegated_section_info()) {
return false;
}
$sectionactions = new sectionactions($this->course);
$sectionactions->update($delegatedsection, $sectionfields);
if ($rebuildcache) {
course_modinfo::purge_course_section_cache_by_id($cm->course, $delegatedsection->id);
rebuild_course_cache($cm->course, false, true);
}
return true;
}
/**
* Rename a course module.
*
* @param int $cmid the course module id.
* @param string $name the new name.
* @return bool true if the course module was renamed, false otherwise.
@@ -64,12 +101,16 @@ class cmactions extends baseactions {
]
);
$cm->name = $name;
$fields = new \stdClass();
$fields->name = $name;
\core\event\course_module_updated::create_from_cm($cm)->trigger();
course_modinfo::purge_course_module_cache($cm->course, $cm->id);
rebuild_course_cache($cm->course, false, true);
$this->update_delegated($cm, ['name' => $name]);
// Modules may add some logic to renaming.
$modinfo = get_fast_modinfo($cm->course);
\core\di::get(\core\hook\manager::class)->dispatch(
@@ -87,4 +128,100 @@ class cmactions extends baseactions {
return true;
}
/**
* Update a course module.
*
* @param int $cmid the course module id.
* @param int $visible state of the module
* @param int $visibleoncoursepage state of the module on the course page
* @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild.
* @return bool whether course module was updated
*/
public function set_visibility(int $cmid, int $visible, int $visibleoncoursepage = 1, bool $rebuildcache = true): bool {
global $DB, $CFG;
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/calendar/lib.php');
if (!$cm = get_coursemodule_from_id('', $cmid, 0, false, MUST_EXIST)) {
return false;
}
// Create events and propagate visibility to associated grade items if the value has changed.
// Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) {
return true;
}
if (!$modulename = $DB->get_field('modules', 'name', ['id' => $cm->module])) {
return false;
}
// Updating visible and visibleold to keep them in sync. Only changing a section visibility will
// affect visibleold to allow for an original visibility restore. See set_section_visible().
$cminfo = (object)[
'id' => $cmid,
'visible' => $visible,
'visibleoncoursepage' => $visibleoncoursepage,
'visibleold' => $visible,
];
$DB->update_record('course_modules', $cminfo);
$DB->update_record(
$cm->modname,
(object)[
'id' => $cm->instance,
'timemodified' => time(),
]
);
$fields = ['visible' => $visible, 'visibleold' => $visible];
$this->update_delegated($cm, $fields, false);
if ($rebuildcache) {
\course_modinfo::purge_course_module_cache($cm->course, $cm->id);
rebuild_course_cache($cm->course, false, true);
}
if ($cm->visible == $visible) {
// There is nothing else to change.
return true;
}
if ($events = $DB->get_records('event', ['instance' => $cm->instance, 'modulename' => $modulename])) {
foreach ($events as $event) {
if ($visible) {
$event = new \calendar_event($event);
$event->toggle_visibility(true);
} else {
$event = new \calendar_event($event);
$event->toggle_visibility(false);
}
}
}
// Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
// Note that this must be done after updating the row in course_modules, in case
// the modules grade_item_update function needs to access $cm->visible.
$supportsgrade = plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
component_callback_exists('mod_' . $modulename, 'grade_item_update');
if ($supportsgrade) {
$instance = $DB->get_record($modulename, ['id' => $cm->instance], '*', MUST_EXIST);
component_callback('mod_' . $modulename, 'grade_item_update', [$instance]);
} else {
$gradeitems = \grade_item::fetch_all([
'itemtype' => 'mod',
'itemmodule' => $modulename,
'iteminstance' => $cm->instance,
'courseid' => $cm->course,
]);
if ($gradeitems) {
foreach ($gradeitems as $gradeitem) {
$gradeitem->set_hidden(!$visible);
}
}
}
return true;
}
}
@@ -404,6 +404,18 @@ class sectionactions extends baseactions {
$modules = explode(',', $sectioninfo->sequence);
$cmids = [];
// In case the section is delegated by a module, we change also the visibility for the source module.
if ($sectioninfo->is_delegated()) {
$delegateinstance = $sectioninfo->get_component_instance();
// We only return sections delegated by course modules. Sections delegated to other
// types of components must implement their own methods to get the section.
if ($delegateinstance && ($delegateinstance instanceof \core_courseformat\sectiondelegatemodule)) {
$delegator = $delegateinstance->get_cm();
$modules[] = $delegator->id;
}
}
foreach ($modules as $moduleid) {
$cm = get_coursemodule_from_id(null, $moduleid, $this->course->id);
if (!$cm) {
@@ -121,6 +121,52 @@ class delegatedcontrolmenu extends basecontrolmenu {
];
}
// Hide/Show uses module functionality.
// Hide/Show options will be available for subsections inside visible sections only.
$parentsection = $cm->get_section_info();
$availablevisibility = has_capability('moodle/course:sectionvisibility', $coursecontext, $user) && $parentsection->visible;
if ($availablevisibility) {
$url = clone($baseurl);
if (!is_null($sectionreturn)) {
$url->param('sr', $format->get_sectionid());
}
$strhidefromothers = get_string('hidefromothers', 'format_' . $course->format);
$strshowfromothers = get_string('showfromothers', 'format_' . $course->format);
if ($section->visible) { // Show the hide/show eye.
$url->param('hide', $section->section);
$controls['visiblity'] = [
'url' => $url,
'icon' => 'i/show',
'name' => $strhidefromothers,
'pixattr' => ['class' => ''],
'attr' => [
'class' => 'editing_showhide',
'data-sectionreturn' => $sectionreturn,
'data-action' => ($usecomponents) ? 'sectionHide' : 'hide',
'data-id' => $section->id,
'data-swapname' => $strshowfromothers,
'data-swapicon' => 'i/show',
],
];
} else {
$url->param('show', $section->section);
$controls['visiblity'] = [
'url' => $url,
'icon' => 'i/hide',
'name' => $strshowfromothers,
'pixattr' => ['class' => ''],
'attr' => [
'class' => 'editing_showhide',
'data-sectionreturn' => $sectionreturn,
'data-action' => ($usecomponents) ? 'sectionShow' : 'show',
'data-id' => $section->id,
'data-swapname' => $strhidefromothers,
'data-swapicon' => 'i/hide',
],
];
}
}
// Delete deletes the module.
// Only show the view link if we are not already in the section view page.
if (!$isheadersection && $hasmanageactivities) {
@@ -149,7 +149,7 @@ abstract class sectiondelegatemodule extends sectiondelegate {
controlmenu $controlmenu,
renderer_base $output,
): ?action_menu {
$controlmenuclass = $format->get_output_classname('content\\cm\\controlmenu');
$controlmenuclass = $format->get_output_classname('content\\cm\\delegatedcontrolmenu');
$controlmenu = new $controlmenuclass(
$format,
$this->sectioninfo,
+10
View File
@@ -542,8 +542,18 @@ class stateactions {
course_modinfo::purge_course_modules_cache($course->id, $ids);
rebuild_course_cache($course->id, false, true);
$delegatedsections = [];
foreach ($cms as $cm) {
$updates->add_cm_put($cm->id);
if (!$delegatedsection = $cm->get_delegated_section_info()) {
continue;
}
if (!in_array($delegatedsection->id, $delegatedsections)) {
$delegatedsections[] = $delegatedsection->id;
}
}
foreach ($delegatedsections as $sectionid => $section) {
$updates->add_section_put($sectionid);
}
}
@@ -21,7 +21,7 @@
Example context (json):
{
"menu": "<a href=\"#\" class=\"d-inline-block dropdown-toggle icon-no-margin\">Edit<b class=\"caret\"></b></a>",
"menu": "<a href=\"#\" data-toggle=\"dropdown\" class=\"d-inline-block dropdown-toggle icon-no-margin\">Edit<b class=\"caret\"></b></a>",
"hasmenu": true
}
}}
+4 -63
View File
@@ -679,74 +679,15 @@ function set_downloadcontent(int $id, bool $downloadcontent): bool {
* and rebuilt as appropriate. Consider using this if set_coursemodule_visible is called multiple times
* (e.g. in a loop).
*
* @param int $id of the module
* @param int $cmid course module id
* @param int $visible state of the module
* @param int $visibleoncoursepage state of the module on the course page
* @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild.
* @return bool false when the module was not found, true otherwise
*/
function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1, bool $rebuildcache = true) {
global $DB, $CFG;
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/calendar/lib.php');
if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) {
return false;
}
// Create events and propagate visibility to associated grade items if the value has changed.
// Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) {
return true;
}
if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) {
return false;
}
if (($cm->visible != $visible) &&
($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) {
foreach($events as $event) {
if ($visible) {
$event = new calendar_event($event);
$event->toggle_visibility(true);
} else {
$event = new calendar_event($event);
$event->toggle_visibility(false);
}
}
}
// Updating visible and visibleold to keep them in sync. Only changing a section visibility will
// affect visibleold to allow for an original visibility restore. See set_section_visible().
$cminfo = new stdClass();
$cminfo->id = $id;
$cminfo->visible = $visible;
$cminfo->visibleoncoursepage = $visibleoncoursepage;
$cminfo->visibleold = $visible;
$DB->update_record('course_modules', $cminfo);
// Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
// Note that this must be done after updating the row in course_modules, in case
// the modules grade_item_update function needs to access $cm->visible.
if ($cm->visible != $visible &&
plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
component_callback_exists('mod_' . $modulename, 'grade_item_update')) {
$instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
component_callback('mod_' . $modulename, 'grade_item_update', array($instance));
} else if ($cm->visible != $visible) {
$grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course));
if ($grade_items) {
foreach ($grade_items as $grade_item) {
$grade_item->set_hidden(!$visible);
}
}
}
if ($rebuildcache) {
\course_modinfo::purge_course_module_cache($cm->course, $cm->id);
rebuild_course_cache($cm->course, false, true);
}
return true;
function set_coursemodule_visible($cmid, $visible, $visibleoncoursepage = 1, bool $rebuildcache = true) {
$coursecontext = context_module::instance($cmid)->get_course_context();
return formatactions::cm($coursecontext->instanceid)->set_visibility($cmid, $visible, $visibleoncoursepage, $rebuildcache);
}
/**
@@ -33,10 +33,10 @@ Feature: The module menu replaces the delegated section menu
And I should not see "Assign roles"
And I should not see "Highlight"
And I should see "Edit settings"
# Duplicate, Move and Show/Hide are not implemented yet.
# Duplicate and Move are not implemented yet.
And I should not see "Move"
And I should not see "Duplicate"
And I should not see "Hide"
And I should see "Hide"
# Delete option for subsection page is not implemented yet.
And I should not see "Delete"
And I should see "Permalink"
@@ -50,12 +50,11 @@ Feature: The module menu replaces the delegated section menu
And I should not see "Highlight"
And I should see "View"
And I should see "Edit settings"
# Duplicate, Move and Show/Hide are not implemented yet.
# Duplicate and Move are not implemented yet.
And I should not see "Move"
And I should not see "Duplicate"
And I should not see "Hide"
And I should see "Hide"
And I should see "Delete"
And I should see "Permalink"
@javascript
Scenario: The action menu for subsection module in section page has less options than a regular activity
@@ -67,10 +66,10 @@ Feature: The module menu replaces the delegated section menu
And I should not see "Highlight"
And I should see "View"
And I should see "Edit settings"
# Duplicate, Move and Show/Hide are not implemented yet.
# Duplicate and Move are not implemented yet.
And I should not see "Move"
And I should not see "Duplicate"
And I should not see "Hide"
And I should see "Hide"
And I should see "Delete"
And I should see "Permalink"
@@ -157,3 +156,54 @@ Feature: The module menu replaces the delegated section menu
# Subsection page. Open the section header action menu.
And I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element"
And "Delete" "link" should not exist in the "[data-region='header-actions-container']" "css_element"
@javascript
Scenario: Hide/Show option in subsection action menu
Given I turn editing mode on
And I should not see "Hidden from students"
And I open "Subsection1" actions menu
When I choose "Hide" in the open action menu
Then I should see "Hidden from students"
Given I am on the "C1 > Subsection1" "course > section" page
And I should see "Hidden from students"
# Subsection page. Open the section header action menu.
And I click on "Edit" "icon" in the "[data-region='header-actions-container']" "css_element"
And I choose "Show" in the open action menu
And I should not see "Hidden from students"
And I click on "Section 1" "link" in the ".breadcrumb" "css_element"
And I should not see "Hidden from students"
# Section page. Open Subsection1 module action menu.
And I open "Subsection1" actions menu
And I choose "Hide" in the open action menu
And I should see "Hidden from students"
@javascript
Scenario: Hide/Show option in course page action menu for subsections
Given I am on the "C1" "Course" page
And I turn editing mode on
When I hide section "Subsection1"
Then I should see "Hidden from students"
And I show section "Subsection1"
And I should not see "Hidden from students"
@javascript
Scenario: Hide/Show option in subsection page action menu for subsections
Given I am on the "C1 > Subsection1" "course > section" page
And I turn editing mode on
When I hide section "Subsection1"
Then I should see "Hidden from students"
And I show section "Subsection1"
And I should not see "Hidden from students"
@javascript
Scenario: Subsections can't change visibility in hidden sections.
Given I am on the "C1" "Course" page
And I turn editing mode on
And I hide section "Section 1"
When I open section "Subsection1" edit menu
Then I should not see "Hide"
And I should not see "Show"
And I am on the "C1 > Section 1" "course > section" page
And I open section "Subsection1" edit menu
And I should not see "Hide"
And I should not see "Show"
@@ -66,7 +66,7 @@ final class sectiondelegate_test extends \advanced_testcase {
// Highlight is only present in section menu (not module), so they shouldn't be found in the result.
// Duplicate is not implemented yet, so they shouldn't be found in the result.
// The possible options are: View, Edit, Delete and Permalink.
// The possible options are: View, Edit, Show, Hide, Delete and Permalink.
if (get_string_manager()->string_exists('editsection', 'format_'.$format->get_format())) {
$streditsection = get_string('editsection', 'format_'.$format->get_format());
} else {
@@ -75,9 +75,12 @@ final class sectiondelegate_test extends \advanced_testcase {
$allowedoptions = [
get_string('view'),
$streditsection,
get_string('hidefromothers', 'format_' . $course->format),
get_string('showfromothers', 'format_' . $course->format),
get_string('delete'),
get_string('sectionlink', 'course'),
];
// The default section menu should be different for the delegated section menu.
$result = $delegated->get_section_action_menu($format, $controlmenu, $renderer);
foreach ($result->get_secondary_actions() as $secondaryaction) {